This is a write-up for task segfault labyrinth
in google ctf 2022
The problem is consisted with a single amd64 binary. What it does is simple. It just runs your shellcode with some limitations. For example, there is a seccomp filter which prevents you from getting new file descriptors.
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x00000005 if (A != fstat) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000004 if (A != stat) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x06 0x00 0x00 0x00000000 return KILL
Then it would be impossible to read flag.txt
without some serious dark magic. Therefore, the binary opens the flag and loads it onto the memory. However its address has a bit of fluctuations. It creates A LOT of randomized memory maps, and puts the flag in only one of them. Fortunately, the address is not completely random, but predictable as it's just a glibc's rand()
with the default seed(1).
I first made a stupid code that reads every single possible page, regardless if it's actually allocated or not, and prints the contents of it. As we were able to use syscalls like write()
, we don't need to rely on sophiscated methods(i.e. Transactions) to avoid crashing the program by invalid memory access, as system calls just return negative rax
on failure.
While this stupid version of the solver was working, I worked on porting glibc's rand()
into a compact assembly. However, while I was working for it, the stupid version just got the flag right.