Initial import

This commit is contained in:
Sean Heelan 2020-01-14 14:32:06 +00:00
commit 3f6517aae2
5 changed files with 246 additions and 0 deletions

1
GetRuntimeAddresses/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
GetRuntimeAddresses

View File

@ -0,0 +1,94 @@
package ebpf
/*
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <linux/unistd.h>
#include <linux/bpf.h>
// See the bpf man page for details on the following functions
static int bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
{
return syscall(__NR_bpf, cmd, attr, size);
}
static int bpf_create_map(
enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
int bpf_get_next_key(int fd, const void *key, void *next_key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.next_key = (__u64) (unsigned long) next_key};
return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}
int bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = (__u64) (unsigned long) key,
.value = (__u64) (unsigned long) value,
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
)
// CreateMap creates an eBPF map from int->uint64. The file descriptor of the
// created map is returned.
func CreateMap() (int, error) {
mapFD := C.bpf_create_map(C.BPF_MAP_TYPE_HASH, 4, 8, 64)
return int(mapFD), nil
}
// GetMap gets the key/value pairs from the specified eBPF map as a Go map
func GetMap(mapFD int) (map[int]uint64, error) {
retMap := make(map[int]uint64)
mapFDC := C.int(mapFD)
keyC := C.int(0)
nextKeyC := C.int(0)
valC := C.uint64_t(0)
for {
r, errno := C.bpf_get_next_key(mapFDC, unsafe.Pointer(&keyC), unsafe.Pointer(&nextKeyC))
if r == -1 {
if errno == syscall.ENOENT {
// The provided key was the last element. We're done iterating.
return retMap, nil
}
return nil, fmt.Errorf("bpf_get_next_key failed with errno %d", errno)
}
r = C.bpf_lookup_elem(mapFDC, unsafe.Pointer(&nextKeyC), unsafe.Pointer(&valC))
if r == -1 {
return nil, fmt.Errorf("bpf_lookup_elem failed")
}
retMap[int(nextKeyC)] = uint64(valC)
keyC = nextKeyC
}
}

View File

@ -0,0 +1,34 @@
package main
import (
"fmt"
"os"
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/ebpf"
)
func main() {
mapFD, err := ebpf.CreateMap()
if err != nil {
fmt.Printf("Failed to create eBPF map: %s\n", err)
os.Exit(1)
}
fmt.Printf("Created eBPF map (FD: %d)\n", mapFD)
//
// Solution to your tasks goes here
//
mapContents, err := ebpf.GetMap(mapFD)
if err != nil {
fmt.Printf("Failed to get the map contents: %s", err)
os.Exit(1)
}
fmt.Printf("Printing contents of map %d\n", mapFD)
for k, v := range mapContents {
fmt.Printf("\t%d -> 0x%x\n", k, v)
}
os.Exit(0)
}

110
README.md Normal file
View File

@ -0,0 +1,110 @@
# Overview
The goal of this exercise is to construct a Go program that populates an eBPF
map with the address in memory of the `_PyRuntime` global variable for all live
Python 3.7 processes. This variable is typically found within `libpython`, which
is dynamically loaded. The target system can be assumed to be a 64-bit Linux OS
running a 5.x kernel.
As an example, say there are two live processes that have mapped
`libpython3.7m.so.1.0` (on your OS `libpython` may have a different name) into
their address spaces, with process IDs 100 and 200. In process 100 the library
is mapped at address 0x10000, and in process 200 the library is mapped at
address 0x20000. Furthermore, assume that `_PyRuntime` is at offset 256
within `libpython3.7m.so.1.0`.
Your solution should populate an eBPF map with the following two entries: (100
-> 0x10256), (200 -> 0x20256).
### Your Solution
Treat your solution as if this was code you would have to live with for a while,
rather than just a throw-away proof-of-concept. i.e. ideally it should be
modular, with a reasonable breakdown of functionality into packages and functions
as required, documented and formatted as per the Go standards, with any tests as
may be necessary (aiming for full test coverage isn't necessary. Use your
judgement as to what may need a test and what may not).
When you have completed your solution, create a pull request for it to the master
branch.
## Task 1
Enumerate all live processes that contain a Python 3.7 interpreter. Note that
this includes both Python REPL processes that have Python as their main binary,
and other processes that have libpython.*.so loaded in their address space.
## Task 2
For all processes containing a Python interpreter determine the address of the
`_PyRuntime` global variable within each process. In Python 3.7 this variable is
in the dynamic symbols of the Python library and the symbol information will be
available regardless of whether or not debug symbols are available. Note that
due to address space layout randomization the actual address of the variable
within each process will differ.
## Task 3
For all live processes containing a Python interpreter, add a mapping from
process ID to the address of `_PyRuntime` to an eBPF map. The skeleton solution
provides an `ebpf` package with a function for creating a map, as well as a
function for reading a map. You need to add a function for writing to the map.
## Getting Started
You will need to following in order to complete the exercise:
1. A functioning Go installation. See https://golang.org for information on how
to get started with this if you have not already done so.
2. A functioning Python 3.7 install. Run `python3 --version` to see what the
default installed version of Python is on your system. It is also necessary
for your Python install to be dynamically linked against `libpython`. To
check if this is the case run `ldd /path/to/python3.7` and check for a
`libpython` entry in the output. If there is no such entry the easiest
solution is to download the Python 3.7 source code from
https://www.python.org/downloads/release/python-376/ and compile/install it
via ``./configure --prefix=`pwd`/install --enable-shared && make && make
install``. Once that completes you will have a Python interpreter at
`./install/bin/python3.7` that you can use.
3. A running Linux kernel with eBPF enabled. It is enabled by default on most
modern kernel configurations. To check if this is the case for yours, search
the config of your running kernel for `CONFIG_BPF=y`. On Fedora this can be
done via `cat /boot/config-$(uname -r)| grep -i "CONFIG_BPF=y"`.
Clone this repository into `$GOPATH/src/github.com/optimyze-interviews`. A
skeleton Go program is provided in the `GetRuntimeAddresses` sub-directory. From
within `GetRuntimeAddresses` you can build the program via `go build`. This will
create a binary called `./GetRuntimeAddresses`. In its current state it will
simply create an eBPF map, attempt to print the contents of this map, and then
exit. The output would look something like the following:
```
$ ./GetRuntimeAddresses
Created eBPF map (FD: 3)
Printing contents of map 3
```
Finally, a Python script called `runforever.py` is also provided. When run via
`python3.7 ./runforever.py` this script simply prints the ID of the Python process and
enters an infinite loop. You can run a few of these simulataneously to test your
solution, if you wish.
# Supporting Material
## Golang
* https://golang.org/
* https://golang.org/doc/
* https://tour.golang.org/welcome/1
# ELF Symbols
* https://www.intezer.com/executable-linkable-format-101-part-2-symbols/
## eBPF
* https://lwn.net/Articles/740157/
* http://man7.org/linux/man-pages/man2/bpf.2.html

7
runforever.py Executable file
View File

@ -0,0 +1,7 @@
import time
import os
print("Process ID: {}".format(os.getpid()))
print("Entering infinite loop ...")
while 1:
time.sleep(2)