commit 3f6517aae226779acdaff1b1ded1c33b6ddff089 Author: Sean Heelan Date: Tue Jan 14 14:32:06 2020 +0000 Initial import diff --git a/GetRuntimeAddresses/.gitignore b/GetRuntimeAddresses/.gitignore new file mode 100644 index 0000000..f83b949 --- /dev/null +++ b/GetRuntimeAddresses/.gitignore @@ -0,0 +1 @@ +GetRuntimeAddresses diff --git a/GetRuntimeAddresses/ebpf/ebpf.go b/GetRuntimeAddresses/ebpf/ebpf.go new file mode 100644 index 0000000..9e480bb --- /dev/null +++ b/GetRuntimeAddresses/ebpf/ebpf.go @@ -0,0 +1,94 @@ +package ebpf + +/* +#include +#include +#include +#include +#include + +// 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 + } +} diff --git a/GetRuntimeAddresses/main.go b/GetRuntimeAddresses/main.go new file mode 100644 index 0000000..c5e13a0 --- /dev/null +++ b/GetRuntimeAddresses/main.go @@ -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) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb81ab8 --- /dev/null +++ b/README.md @@ -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 + diff --git a/runforever.py b/runforever.py new file mode 100755 index 0000000..9b52052 --- /dev/null +++ b/runforever.py @@ -0,0 +1,7 @@ +import time +import os + +print("Process ID: {}".format(os.getpid())) +print("Entering infinite loop ...") +while 1: + time.sleep(2)