picoCTF の rev 全部解く
CTF の問題を解いていると,解けはするけど異常に時間がかかる,みたいなことが多い. rev に限れば,これは,ツールの使い方が甘かったり,つまみ食い的に rev をやってきたので常識が欠落していて遠回りしていたり,そもそもデコンパイル結果を読む筋力が全然なかったりすることが原因だと分かっている. なので,数をこなそう作戦で,知り合いにオススメされた picoCTF の rev を全部解いてみる. 2~3 日くらいで完走できたら良い
Transformation
''.join([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)])
をデコードする
encoded_flag = "灩捯䍔䙻ㄶ形楴獟楮獴㌴摟潦弸弲㘶㠴挲ぽ"
flag = []
for i in range(len(encoded_flag)):
tmp1 = ord(encoded_flag[i]) & 0xFF
tmp2 = ord(encoded_flag[i]) >> 8
flag += chr(tmp2)
flag += chr(tmp1)
print("".join(flag))
keygenme-py
- Arcane Calculator なるものが動いていて,ライセンスを入れるといいらしい
def check_key(key, username_trial):
global key_full_template_trial
if len(key) != len(key_full_template_trial):
return False
else:
# Check static base key part --v
i = 0
for c in key_part_static1_trial:
if key[i] != c:
return False
i += 1
# TODO : test performance on toolbox container
# Check dynamic part --v
if key[i] != hashlib.sha256(username_trial).hexdigest()[4]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[5]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[3]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[6]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[2]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[7]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[1]:
return False
else:
i += 1
if key[i] != hashlib.sha256(username_trial).hexdigest()[8]:
return False
return True
これを見て,あとは事前に定義されている文字列を当てはめればいい
ARMssebly 0
ARM のアセンブリがわたされるのでクロスコンパイルする
aarch64-linux-gnu-as chall.S -o chall
Ghidra で見ると整数の大小を比較しているだけなので大きい方が答え
vault-door-training
java のソースを読むだけ
speeds and feeds
G-code が出てくるのでプロットする. いいサンプルサイトがあった
Shop
負の個数買うと所持金が増える
Armeembly 1
return 0xd2a - param_1
ARMssembly 2
for (local_4 = 0; local_4 < param_1; local_4 = local_4 + 1) {
local_8 = local_8 + 3;
}
なんか何がしたいのか分からない問題が続くので,ちゃんと手を動かした問題だけのせる
GDB baby step2
gdb debugger0_b
info functions
b main
layout asm
r
この時点で,main 関数の ret (関数抜け)のアドレスが分かるので
b *0x401142
c
info register rip
print/d $eax
GDB baby step4
gdb で main に breakpoint を設置し,ステップ実行すると imul が見える
0x401106 <func1> endbr64
0x40110a <func1+4> push rbp
0x40110b <func1+5> mov rbp, rsp
0x40110e <func1+8> mov dword ptr [rbp - 4], edi
0x401111 <func1+11> mov eax, dword ptr [rbp - 4]
► 0x401114 <func1+14> imul eax, eax, 0x3269
0x40111a <func1+20> pop rbp
0x40111b <func1+21> ret
ということで 0x3269
Picker I
入力した関数を実行してくれるインスタンスが動いている. win() を実行すると flag.txt を展開してくれるので,これを文字列に直せばいい
Picker II
こんどは入力がフィルタされていて,「win」 が打てなくなっている. 一方で,main の eval はそのままなので,exec のようにコマンドインジェクションが通る.
==> print(open('flag.txt', 'r').read())
picoCTF{f1l73r5_f41l_c0d3_r3f4c70r_m1gh7_5ucc33d_95d44590}
'NoneType' object is not callable
Picker III
必ず呼び出される call_func()
関数の最後に eval(func_name+'()')
があるのでこれを利用する.
def get_func(n):
global func_table
# Check table for viability
if( not check_table() ):
print(CORRUPT_MESSAGE)
return
# Get function name from table
func_name = ''
func_name_offset = n * FUNC_TABLE_ENTRY_SIZE
for i in range(func_name_offset, func_name_offset+FUNC_TABLE_ENTRY_SIZE):
if( func_table[i] == ' '):
func_name = func_table[func_name_offset:i]
break
if( func_name == '' ):
func_name = func_table[func_name_offset:func_name_offset+FUNC_TABLE_ENTRY_SIZE]
return func_name
func_name が win になればいい
func_table を参照しているので,書き換えたい.
┌──(kali㉿kali)-[~/CTF]
└─$ nc saturn.picoctf.net 51363
==> 3
Please enter variable name to write: func_table
Please enter new value of variable: 'win read_variable write_variable '
==> 1
0x70 0x69 0x63 0x6f 0x43 0x54 0x46 0x7b 0x37 0x68 0x31 0x35 0x5f 0x31 0x35 0x5f 0x77 0x68 0x34 0x37 0x5f 0x77 0x33 07 0x31 0x37 0x68 0x5f 0x75 0x35 0x33 0x72 0x35 0x5f 0x31 0x6e 0x5f 0x63 0x68 0x34 0x72 0x67 0x33 0x5f 0x32 0x32 0x36 0x7d
==>
packer
strings
するとなんか難読化された文字列がたくさん出てくる.
packer だし,strings
の結果にも UPX packer
みたいな記述があるので,アンパックしてみる
upx -d out
strings out | grep flag
asm1
アセンブリ,ちゃんとまとめたことがないので真面目にやる
asm1:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: cmp DWORD PTR [ebp+0x8],0x3fb
<+10>: jg 0x512 <asm1+37>
<+12>: cmp DWORD PTR [ebp+0x8],0x280
<+19>: jne 0x50a <asm1+29>
<+21>: mov eax,DWORD PTR [ebp+0x8]
<+24>: add eax,0xa
<+27>: jmp 0x529 <asm1+60>
<+29>: mov eax,DWORD PTR [ebp+0x8]
<+32>: sub eax,0xa
<+35>: jmp 0x529 <asm1+60>
<+37>: cmp DWORD PTR [ebp+0x8],0x559
<+44>: jne 0x523 <asm1+54>
<+46>: mov eax,DWORD PTR [ebp+0x8]
<+49>: sub eax,0xa
<+52>: jmp 0x529 <asm1+60>
<+54>: mov eax,DWORD PTR [ebp+0x8]
<+57>: add eax,0xa
<+60>: pop ebp
<+61>: ret
まず push ebp で,main 関数の上に今のベースポインタアドレス(ebp)を格納する. このとき,push したので esp の値は -4 される. 次に,mov ebp,esp で,esp の値を ebp に格納する. これにより,今の ebp(main 関数の底)が,esp(main 関数 + push ebp した値のスタックトップ)になるので,esp と ebp が同じ値になる.
<+0>: push ebp
<+1>: mov ebp,esp
逆に,関数から戻るときはまず mov esp,ebp
で esp(スタックトップ) に ebp(実行している関数のベースアドレス)の値を代入する.
次に pop ebp
をする.
このとき pop されるのは,push ebp
で push された main関数の ebp なので,main 関数のベースアドレスが元に戻り,esp も +4 されて,関数から戻ってこれるようになる.
今回の場合は,引数がある. 引数は ebp+8 で表現されている. (ebp の退避先,esp の退避先,引数の順) 引数は 0x2e0 で,0x2e0 < 0x3fb なので jg のジャンプは起こらない
cmp DWORD PTR [ebp+0x8],0x3fb
jg 0x512 <asm1+37>
次に 0x2e0 != 0x280 なので ZF = 0 で,移動する
<+12>: cmp DWORD PTR [ebp+0x8],0x280
<+19>: jne 0x50a <asm1+29>
jmp は何もなくても移動する
<+29>: mov eax,DWORD PTR [ebp+0x8]
<+32>: sub eax,0xa
<+35>: jmp 0x529 <asm1+60>
<+60>: pop ebp
<+61>: ret
Bbbbloat
ghidra で見ると比較部分があるので,これをそのまま入力する
if (local_48 == 0x86187)
asm 2
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 0x50c <asm2+31>
<+20>: add DWORD PTR [ebp-0x4],0x1
<+24>: add DWORD PTR [ebp-0x8],0xd1
<+31>: cmp DWORD PTR [ebp-0x8],0x5fa1
<+38>: jle 0x501 <asm2+20>
<+40>: mov eax,DWORD PTR [ebp-0x4]
<+43>: leave
<+44>: ret
<+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
引数を esp+0x10 で広げたスタック分に詰める.
<+31>: cmp DWORD PTR [ebp-0x8],0x5fa1
このあと, ebp-0x8 と 0x5fa1 が比較され,0x5fa1 の値を超えなければ 0xd1 が加算される. なので,0x2d と 0x5fa1 の差を比べ,0xd1 を足した回数分,0x11 に 0x1 を足す.
- {(0x5fa1 - 0x2d) // 0xd1 } + 1 + 0x2d
unpackme
たぶん pakcker だろうと思いながら解く. radare2 の使い方を勉強しながら解いてみる.
正しい数字を入力するとフラグを返してくれるプログラムっぽい
┌──(kali㉿kali)-[~/CTF]
└─$ r2 unpackme-upx
[0x00401c60]> aaa
[ ] Analyze all flags starting with sym. and entry0 (aa)
Cannot find basic block for switch case at 0x0043f6d3 bbdelta = 30
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information (aanr)
[x] Use -AA or aaaa to perform additional experimental analysis.
afl コマンドで main 関数があることが分かっているので main を解析する
[0x00401c60]> s sym.main
[0x00401e43]> pdf
-- snip --
│ 0x00401e9c 488d3d61110b. lea rdi, str.Whats_my_favorite_number__ ; 0x4b3004 ; "What's my favorite number? " ; int64_t arg1
│ 0x00401ea3 b800000000 mov eax, 0
│ 0x00401ea8 e8f3ec0000 call sym.__printf
│ 0x00401ead 488d45c4 lea rax, [var_3ch]
│ 0x00401eb1 4889c6 mov rsi, rax
│ 0x00401eb4 488d3d65110b. lea rdi, [0x004b3020] ; "%d" ; const char *format
│ 0x00401ebb b800000000 mov eax, 0
│ 0x00401ec0 e86bee0000 call sym.__isoc99_scanf ; int scanf(const char *format)
│ 0x00401ec5 8b45c4 mov eax, dword [var_3ch]
│ 0x00401ec8 3dcb830b00 cmp eax, 0xb83cb <- これ
-- snip --
forky
{
int *piVar1;
piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0);
*piVar1 = 1000000000;
fork();
fork();
fork();
fork();
*piVar1 = *piVar1 + 0x499602d2;
doNothing(*piVar1);
return 0;
}
doNothing
に渡される引数の値が flag になっている.
piVar1 が明らかに鍵なので, fork() の動作を見る.
とは言っても単純で,プロセスが 2^4 = 16 個作られるだけ
1000000000 + 0x499602d2 * 16 = -721750240
ARMssembly3
undefined4 func1(uint param_1)
{
uint local_14;
undefined4 local_4;
local_4 = 0;
for (local_14 = param_1; local_14 != 0; local_14 = local_14 >> 1) {
if ((local_14 & 1) != 0) {
local_4 = func2(local_4);
}
}
return local_4;
}
func2
は引数に 3 を足して返す関数.
param1 のビットを見て後ろから 1 なら +3 する,みたいな感じでやればいい
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
num = 2541039191
i = 0
while num != 0:
if num&1 != 0:
i = i+3
num = num >> 1
print(i)
reverse_cipher
void main(void)
{
size_t sVar1;
char flag [23];
char local_41;
int local_2c;
FILE *fp_output;
FILE *fp_flag;
uint local_14;
int local_10;
char local_9;
fp_flag = fopen("flag.txt","r");
fp_output = fopen("rev_this","a");
if (fp_flag == (FILE *)0x0) {
puts("No flag found, please make sure this is run on the server");
}
if (fp_output == (FILE *)0x0) {
puts("please run this on the server");
}
sVar1 = fread(flag,0x18,1,fp_flag);
local_2c = (int)sVar1;
if ((int)sVar1 < 1) {
/* WARNING: Subroutine does not return */
exit(0);
}
for (local_10 = 0; local_10 < 8; local_10 = local_10 + 1) {
local_9 = flag[local_10];
fputc((int)local_9,fp_output);
}
for (local_14 = 8; (int)local_14 < 0x17; local_14 = local_14 + 1) {
if ((local_14 & 1) == 0) {
local_9 = flag[(int)local_14] + '\x05';
}
else {
local_9 = flag[(int)local_14] + -2;
}
fputc((int)local_9,fp_output);
}
local_9 = local_41;
fputc((int)local_41,fp_output);
fclose(fp_output);
fclose(fp_flag);
return;
}
なんとなく変数名を直したのがこれ. flag.txt からフラグを読んで,ちょっと計算して rev.txt に書き出すだけのプログラム. なのでその計算に応じたソルバを書いてあげる
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
ans = "w1{1wq84fb<1>49}"
cnt = 0
for i in ans:
if cnt&1 == 0:
tmp = ord(i) - 0x5
print(chr(tmp), end="")
else:
tmp = ord(i) + 0x2
print(chr(tmp), end="")
cnt = cnt+1
asm3
asm3:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: xor eax,eax // eax -> 0
<+5>: mov ah,BYTE PTR [ebp+0xa] // ah <- 33
<+8>: shl ax,0x10 // ax <- 3300 <- 0000
<+12>: sub al,BYTE PTR [ebp+0xc] //ae 0x00 - 0xae = FFFFFF52, al <- 52
<+15>: add ah,BYTE PTR [ebp+0xd] //ah 0x00 + 0x72 = 72, ah <- 72
<+18>: xor ax,WORD PTR [ebp+0x10] // ax <- 0x7252 xor b139
<+22>: nop
<+23>: pop ebp
<+24>: ret
変数の積み方とリトルエンディアンの復習になった
初期状態は以下
stack |
---|
main ebp |
ret |
d73346ed(引数1) |
d48672ae(引数2) |
d3c8b139(引数3) |
main |
リトルエンディアンなので,例えば [ebp+0xa] では引数1 の 33 にアクセスできる
asm4
アセンブリが渡されるが,多すぎて一つ一つ見るのは大変なのでコンパイルできるようにする.
32 bit なので gcc -masm=intel -m32 solve.c -o solve
とする.
実行に sudo apt install libc6-dev-i386
が必要.
#include <stdio.h>
#include <stdlib.h>
int asm4(char* in)
{
int val;
asm (
"nop;"
"nop;"
"nop;"
//"push ebp;"
//"mov ebp,esp;"
"push ebx;"
"sub esp,0x10;"
"mov DWORD PTR [ebp-0x10],0x246;"
"mov DWORD PTR [ebp-0xc],0x0;"
"jmp _asm_27;"
"_asm_23:"
"add DWORD PTR [ebp-0xc],0x1;"
"_asm_27:"
"mov edx,DWORD PTR [ebp-0xc];"
"mov eax,DWORD PTR [%[pInput]];"
"add eax,edx;"
"movzx eax,BYTE PTR [eax];"
"test al,al;"
"jne _asm_23;"
"mov DWORD PTR [ebp-0x8],0x1;"
"jmp _asm_138;"
"_asm_51:"
"mov edx,DWORD PTR [ebp-0x8];"
"mov eax,DWORD PTR [%[pInput]];"
"add eax,edx;"
"movzx eax,BYTE PTR [eax];"
"movsx edx,al;"
"mov eax,DWORD PTR [ebp-0x8];"
"lea ecx,[eax-0x1];"
"mov eax,DWORD PTR [%[pInput]];"
"add eax,ecx;"
"movzx eax,BYTE PTR [eax];"
"movsx eax,al;"
"sub edx,eax;"
"mov eax,edx;"
"mov edx,eax;"
"mov eax,DWORD PTR [ebp-0x10];"
"lea ebx,[edx+eax*1];"
"mov eax,DWORD PTR [ebp-0x8];"
"lea edx,[eax+0x1];"
"mov eax,DWORD PTR [%[pInput]];"
"add eax,edx;"
"movzx eax,BYTE PTR [eax];"
"movsx edx,al;"
"mov ecx,DWORD PTR [ebp-0x8];"
"mov eax,DWORD PTR [%[pInput]];"
"add eax,ecx;"
"movzx eax,BYTE PTR [eax];"
"movsx eax,al;"
"sub edx,eax;"
"mov eax,edx;"
"add eax,ebx;"
"mov DWORD PTR [ebp-0x10],eax;"
"add DWORD PTR [ebp-0x8],0x1;"
"_asm_138:"
"mov eax,DWORD PTR [ebp-0xc];"
"sub eax,0x1;"
"cmp DWORD PTR [ebp-0x8],eax;"
"jl _asm_51;"
"mov eax,DWORD PTR [ebp-0x10];"
"add esp,0x10;"
"pop ebx;"
//"pop ebp;"
//"ret ;"
"nop;"
"nop;"
"nop;"
:"=r"(val)
: [pInput] "m"(in)
);
return val;
}
int main(int argc, char** argv)
{
printf("0x%x\n", asm4("picoCTF_a3112"));
return 0;
}
Need For Speed
void set_timer(void)
{
__sighandler_t p_Var1;
p_Var1 = __sysv_signal(0xe,alarm_handler);
if (p_Var1 == (__sighandler_t)0xffffffffffffffff) {
puts("\n\nSomething bad happened here. ");
/* WARNING: Subroutine does not return */
exit(0);
}
alarm(1);
return;
}
alarm(1) によって,一秒後に signal が送られて実行が中断する. そこで,set_timer() を実行しないようにしたい.
方法1: SIGALRM がセットされていることが分かっているので,(gdb) handle SIGALRM ignore
が使えるらしい.
方法2: set_timer() の呼び出しを nop に書き変えることでも回避できる.
┌──(kali㉿kali)-[~/CTF]
└─$ r2 -d need-for-speed
[0x7f4ef7a0b4d0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Skipping type matching analysis in debugger mode (aaft)
[x] Propagate noreturn information (aanr)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x7f4ef7a0b4d0]> s main
[0x55894100091a]> pdf
; DATA XREF from entry0 @ 0x55894100067d
┌ 62: int main (int argc, char **argv);
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_4h @ rbp-0x4
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x55894100091a 55 push rbp
│ 0x55894100091b 4889e5 mov rbp, rsp
│ 0x55894100091e 4883ec10 sub rsp, 0x10
│ 0x558941000922 897dfc mov dword [var_4h], edi ; argc
│ 0x558941000925 488975f0 mov qword [var_10h], rsi ; argv
│ 0x558941000929 b800000000 mov eax, 0
│ 0x55894100092e e8a5ffffff call sym.header
│ 0x558941000933 b800000000 mov eax, 0
│ 0x558941000938 e8f2feffff call sym.set_timer
│ 0x55894100093d b800000000 mov eax, 0
│ 0x558941000942 e836ffffff call sym.get_key
│ 0x558941000947 b800000000 mov eax, 0
│ 0x55894100094c e85bffffff call sym.print_flag
│ 0x558941000951 b800000000 mov eax, 0
│ 0x558941000956 c9 leave
└ 0x558941000957 c3 ret
これで call sym.set_timer
の場所が分かったので,nop に書きかえる.
[0x55894100091a]> s 0x558941000938
[0x558941000938]> wao nop
[0x558941000938]> pdf
; DATA XREF from entry0 @ 0x55894100067d
┌ 62: int main (int argc, char **argv);
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_4h @ rbp-0x4
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x55894100091a 55 push rbp
│ 0x55894100091b 4889e5 mov rbp, rsp
│ 0x55894100091e 4883ec10 sub rsp, 0x10
│ 0x558941000922 897dfc mov dword [var_4h], edi ; argc
│ 0x558941000925 488975f0 mov qword [var_10h], rsi ; argv
│ 0x558941000929 b800000000 mov eax, 0
│ 0x55894100092e e8a5ffffff call sym.header
│ 0x558941000933 b800000000 mov eax, 0
│ 0x558941000938 90 nop
..
│ 0x55894100093d b800000000 mov eax, 0
│ 0x558941000942 e836ffffff call sym.get_key
│ 0x558941000947 b800000000 mov eax, 0
│ 0x55894100094c e85bffffff call sym.print_flag
│ 0x558941000951 b800000000 mov eax, 0
│ 0x558941000956 c9 leave
└ 0x558941000957 c3 ret
[0x558941000938]> dc
Keep this thing over 50 mph!
============================
Creating key...
Finished
Printing flag:
PICOCTF{Good job keeping bus #1f2ac4ec speeding along!}
(128407) Process exited with status=0x0
OTP Implementation
なんか OTP っぽいプログラムが渡される
- jumble: ある規則に従って数字をこねくり回す
- main: さらにこねくり回す
これを逆から見ていけば所望の鍵が手に入るので,flag と xor すればいい
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes
flag = "1fcb81cd1f6f1e12b429092e3647153b6c212772554ca004145b82367e1e6b7870827dc249a319601776f727434e6b6227d1"
cmp_str = "mlaebfkoibhoijfidblechbggcgldicegjbkcmolhdjihgmmieabohpdhjnciacbjjcnpcfaopigkpdfnoaknjlnlaohboimombk"
candidate = "1234567890abcdef"
def jumble(param1):
ans = param1
if(0x60 < param1):
ans = param1 + 0x9
ans = (ans % 0x10) * 0x02
if(0x0f < ans):
ans = ans + 0x01
return ans
def body(counter, chara, pre):
if(counter == 0):
after_jumble = jumble(ord(chara))
result = after_jumble % 0x10
#print("OK1")
else:
after_jumble = jumble(ord(chara))
Val = after_jumble + pre >> 0x1f
result = (after_jumble + pre + (Val >> 4) & 0xf) - (Val >> 4)
#print("OK")
counter = counter + 1
return result
pre = 0
results = [0]*100
for c in range(100):
for i in candidate:
result = body(c, i, pre)
result = chr(result + 0x61)
if(cmp_str[c] == result):
results[c] = i
pre = ord(result)
#print(chr(int(i, 16) ^ ord(cmp_str[c])))
#print("OK")
break
enc = ''
for i in range(100):
print(long_to_bytes(int(results[i], 16) ^ ord(flag[i])))
#print(chr(int(results[i], 16) ^ ord(flag[i])), end='')
gogo
golang のデコンパイル結果こんな見にくいのか
まず ghidra で見ると checkPassword がある.
key[0] = 0x38;
key[1] = 0x36;
key[2] = 0x31;
key[3] = 0x38;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:39
*/
key[4] = 0x33;
key[5] = 0x36;
key[6] = 0x66;
key[7] = 0x31;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:43
*/
key[8] = 0x33;
key[9] = 0x65;
key[10] = 0x33;
key[11] = 100;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:47
*/
key[12] = 0x36;
key[13] = 0x32;
key[14] = 0x37;
key[15] = 100;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:51
*/
key[16] = 0x66;
key[17] = 0x61;
key[18] = 0x33;
key[19] = 0x37;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:55
*/
key[20] = 0x35;
key[21] = 0x62;
key[22] = 100;
key[23] = 0x62;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:59
*/
key[24] = 0x38;
key[25] = 0x33;
key[26] = 0x38;
key[27] = 0x39;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:63
*/
key[28] = 0x32;
key[29] = 0x31;
key[30] = 0x34;
key[31] = 0x65;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:67
*/
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:68
*/
runtime.duffcopy_0x20_FUN_08090fe0(local_20,(uint4 *)main.statictmp_4);
uVar1 = 0;
iVar2 = 0;
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:70
*/
while( true ) {
if (0x1f < (int)uVar1) {
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:75
*/
if (iVar2 == 0x20) {
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:76
*/
return;
}
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:78
*/
return;
}
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:71
*/
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:70
*/
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:71
*/
if ((param_2 <= uVar1) || (0x1f < uVar1)) break;
if ((*(byte *)(param_1 + uVar1) ^ key[uVar1]) == *(byte *)((int)local_20 + uVar1)) {
/* /opt/hacksports/shared/staging/gogo_5_8320186217489444/problem_files/enter_pa ssword.go:72
*/
iVar2 = iVar2 + 1;
}
uVar1 = uVar1 + 1;
}
key と main.statictmp_4 の中身を XOR しているっぽいので,まずこれを計算する.
>>> for i in range(0x20):
... print(chr(tmp[i]^input[i]),end='')
reverseengineericanbarelyforward
次に,鍵を unhash しろ,と出てくる. コード中に md5 とあるのでハッシュを探すと goldfish が見つかる.
Let’s get dynamic
bool main(void)
{
~~ snip ~~
local_20 = *(long *)(in_FS_OFFSET + 0x28);
local_98 = 0x396c109a7067b614;
local_90 = 0x32ea1ab1495990f0;
local_88 = 0xd09aa897d230c8fe;
local_80 = 0x2c227b84b00f7d0b;
local_78 = 0xb0a880f7d99ea817;
local_70 = 0xc8f18206086afe7c;
local_68 = 0x61;
local_58 = 0x563f52ce0f15cd77;
local_50 = 0x719435c3652ef38f;
local_48 = 0x8bec9fe9be05a4c9;
local_40 = 0x521c05fe8d590431;
local_38 = 0xdbf1c3a6dadcef7b;
local_30 = 0xc4fc8b585631f076;
local_28 = 0x3f;
fgets(user_input,0x31,stdin);
counter = 0;
while( true ) {
str_length = strlen((char *)&local_98);
if (str_length <= (ulong)(long)counter) break;
local_118[counter] =
(byte)counter ^
*(byte *)((long)&local_98 + (long)counter) ^ *(byte *)((long)&local_58 + (long)counter) ^
0x13;
counter = counter + 1;
}
iVar1 = memcmp(user_input,local_118,0x31);
if (iVar1 == 0) {
puts("No, that\'s not right.");
}
else {
puts("Correct! You entered the flag.");
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return iVar1 == 0;
}
入力文字列と既存のフラグを比較している. 文字数は 0x31. 途中にある while 文を注目すればよくて,local_xx にあるデータをこねくりまわしている. ここまでは簡単なんだがデータがリトルエンディアンなのでスタックの最後から取り出さなければいけない. ここにつまづいて 5 年かけました
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
local_98 = [0x39,0x6c,0x10,0x9a,0x70,0x67,0xb6,0x14]
local_90 = [0x32,0xea,0x1a,0xb1,0x49,0x59,0x90,0xf0]
local_88 = [0xd0,0x9a,0xa8,0x97,0xd2,0x30,0xc8,0xfe]
local_80 = [0x2c,0x22,0x7b,0x84,0xb0,0x0f,0x7d,0x0b]
local_78 = [0xb0,0xa8,0x80,0xf7,0xd9,0x9e,0xa8,0x17]
local_70 = [0xc8,0xf1,0x82,0x06,0x08,0x6a,0xfe,0x7c]
local_68 = [0x61]
local_58 = [0x56,0x3f,0x52,0xce,0x0f,0x15,0xcd,0x77]
local_50 = [0x71,0x94,0x35,0xc3,0x65,0x2e,0xf3,0x8f]
local_48 = [0x8b,0xec,0x9f,0xe9,0xbe,0x05,0xa4,0xc9]
local_40 = [0x52,0x1c,0x05,0xfe,0x8d,0x59,0x04,0x31]
local_38 = [0xdb,0xf1,0xc3,0xa6,0xda,0xdc,0xef,0x7b]
local_30 = [0xc4,0xfc,0x8b,0x58,0x56,0x31,0xf0,0x76]
local_28 = [0x3f];
output = [0]*0x31
counter = 0
plus = 40
while(True):
output[counter] = chr(plus ^ (local_30[7-counter]) ^ (local_70[7-counter]) ^ 0x13)
counter = counter + 1
plus = plus+1
print(output)
print(output)
別解
stack 表示を見ればいいらしい
pwndbg> b memcmp
Breakpoint 1 at 0x1060
pwndbg> r
~~snip~~
pwndbg> stack 20
00:0000│ rsp 0x7fffffffda18 —▸ 0x7ffff7fdb432 (_dl_fixup+642) ◂— mov qword ptr [rsp + 8], rax
01:0008│ 0x7fffffffda20 —▸ 0x7ffff7ddeba0 ◂— 0x10001a0000487e /* '~H' */
02:0010│ 0x7fffffffda28 —▸ 0x7ffff7e630f0 (memcmp) ◂— mov rax, qword ptr [rip + 0x137db9]
03:0018│ 0x7fffffffda30 —▸ 0x7fffffffdca0 ◂— 0x31 /* '1' */
04:0020│ 0x7fffffffda38 —▸ 0x7fffffffddf0 ◂— 0x1
05:0028│ 0x7fffffffda40 ◂— 0x0
06:0030│ 0x7fffffffda48 —▸ 0x7fffffffdf18 —▸ 0x7fffffffe290 ◂— 'COLORFGBG=15;0'
07:0038│ 0x7fffffffda50 —▸ 0x555555557dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555130 (__do_global_dtors_aux) ◂— endbr64
08:0040│ 0x7fffffffda58 —▸ 0x7ffff7fdd443 (_dl_runtime_resolve_fxsave+67) ◂— mov r11, rax
09:0048│ 0x7fffffffda60 —▸ 0x7fffffffdd20 ◂— 0x2f2f2f2f2f2f000a /* '\n' */
0a:0050│ 0x7fffffffda68 —▸ 0x7fffffffdce0 ◂— 'picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_14bfa700}'
0b:0058│ 0x7fffffffda70 ◂— 0x31 /* '1' */
0c:0060│ 0x7fffffffda78 —▸ 0x7fffffffdce0 ◂— 'picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_14bfa700}'
0d:0068│ 0x7fffffffda80 —▸ 0x7fffffffdd20 ◂— 0x2f2f2f2f2f2f000a /* '\n' */
0e:0070│ 0x7fffffffda88 ◂— 0x0
0f:0078│ 0x7fffffffda90 ◂— 0x410
10:0080│ 0x7fffffffda98 ◂— 0x0
11:0088│ 0x7fffffffdaa0 ◂— 0x37f
12:0090│ 0x7fffffffdaa8 ◂— 0x0
13:0098│ 0x7fffffffdab0 ◂— 0x0
なんて便利なんだ…
Easy as GDB
undefined4 FUN_000108c4(char *param_1,uint param_2)
{
char *__dest;
char *__dest_00;
uint local_18;
__dest = (char *)calloc(param_2 + 1,1);
strncpy(__dest,param_1,param_2);
FUN_000107c2(__dest,param_2,0xffffffff);
__dest_00 = (char *)calloc(param_2 + 1,1);
strncpy(__dest_00,&DAT_00012008,param_2);
FUN_000107c2(__dest_00,param_2,0xffffffff);
puts("checking solution...");
local_18 = 0;
while( true ) {
if (param_2 <= local_18) {
return 1;
}
if (__dest[local_18] != __dest_00[local_18]) break;
local_18 = local_18 + 1;
}
return 0xffffffff;
}
入力文字列と内蔵文字列が一致していない or ループ回数が入力文字列数を超える で, ループを抜ける. つまり,入力した文字列がすべて正しければループ回数が入力文字列を超えるので (local_18 (ループ回数)が param_2 (入力文字列数) を超える), ループ回数を見ることでフラグが分かる. 以下に gdb スクリプト
import gdb
import string
gdb.execute('file ./brute')
gdb.execute('set pagination off')
gdb.Breakpoint('*0x565559a5')
pattern = string.printable
flag = ''
for i in range(30):
for chara in pattern:
payload = flag + chara
gdb.execute('run ' + chara)
for _ in range(i + 1):
try:
gdb.execute('continue')
except gdb.error:
pass
msg = gdb.execute('i b', to_string=True)
if 'hit {} times'.format(i + 2) in msg:
flag += chara
gdb.execute('continue')
break
print('flag:', flag + chara)
print(msg)
print(flag)
gdb.execute('quit')
相対アドレスは忘れていたので注意
Ghidra で表示されているアドレスは gdb とズレてしまうことに注意する.
Ghidra は 0x00010000
からアドレスが始まる.
gdb の方は info proc mapping
でアドレスの内容を見る
pwndbg> info proc mapping
process 614292
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x56555000 0x56556000 0x1000 0x0 r-xp /home/kali/CTF/brute
0x56556000 0x56557000 0x1000 0x0 r--p /home/kali/CTF/brute
0x56557000 0x56558000 0x1000 0x1000 rw-p /home/kali/CTF/brute
...
で,Ghidra でブレークポイントを設置したい命令は 000109a5 72 d1 JC LAB_00010978
なので,
ベースアドレス +0x9a5 の ところに所望の命令があることが分かる.
あとはせいぜい 3 つなので 3 つとも試してみてブレークポイントが貼れる & スクリプトが正常に動作する
ものを選べばいいが,ダメ押しで x/16xb 0x565559a5
みたいな感じでやると
逆アセンブルコードを表示してくれるので,Ghidra の表示と一致するものを選べばいい.
ARMssembly 4
計算するだけ. なんだこれは
not crypto
"I heard you wanted to bargain for a flag... whatcha got?
という文字列を探して,おそらく main 関数は FUN_00101070 だと推測する.
入力は fread(local_198,1,0x40,_stdin);
のように標準入力から受け取り,最後の方で iVar19 = memcmp(local_88,local_198,0x40);
みたいな比較をしている.
local_198 がそのままであれば local_88 が flag になっているはずなので, memcmp のあたりでレジスタを見ればよさそう.
ブレークポイントを memcmp において(アドレスに注意する),rdi を見るとフラグをゲト
Ready Gladiator 2
Core wars らしい. imp(小鬼) と呼ばれるプログラムに全勝すればいいらしい.
;redcode
;name Imp Ex
;assert 1
JMP 0, <-5
end
なんか上手くいった...
No way out
Unity 製のゲームが動いている. ハシゴを上ると旗が見えるが,透明な壁があって通り抜けられない.
dll のデコンパイル,インターンで教わったな...と思いながら dnSpy をインストールする.
が,Unity 製の dll がどれをデコンパイルするべきか見当もつかなかったので writeup を見る.
/Managed/Assembly-CSharp.dll
を見ればいいらしい.
全部の exe でそうなんだろうか
dll をなんとなく見ると,以下の記述が目に付く.
if (Input.GetButton("Jump") && this.canMove && this.characterController.isGrounded && !this.isClimbing)
昔ゲームを作った時にジャンプ判定で苦労した記憶を掘り出して,ジャンプできるのはプレイヤーの着地判定がトリガーしているときだけだったことを思い出す. なので && 以下をコメントアウトして再コンパイルする. File -> Save Module を忘れずに(戒め)
ゲームを起動して旗に近づくとフラグが見える.
FactCheck
local_20 = *(long *)(in_FS_OFFSET + 0x28);
std::allocator<char>::allocator();
// try { // try from 001012cf to 001012d3 has its CatchHandler @ 00101975
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_248,(allocator *)"picoCTF{wELF_d0N3_mate_");
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 0010130a to 0010130e has its CatchHandler @ 00101996
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_228,(allocator *)&DAT_0010201d);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101345 to 00101349 has its CatchHandler @ 001019b1
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_208,(allocator *)&DAT_0010201f);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101380 to 00101384 has its CatchHandler @ 001019cc
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_1e8,(allocator *)&DAT_00102021);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 001013bb to 001013bf has its CatchHandler @ 001019e7
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_1c8,(allocator *)&DAT_0010201d);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 001013f6 to 001013fa has its CatchHandler @ 00101a02
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_1a8,(allocator *)&DAT_00102023);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101431 to 00101435 has its CatchHandler @ 00101a1d
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_188,(allocator *)&DAT_00102025);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 0010146c to 00101470 has its CatchHandler @ 00101a38
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_168,(allocator *)&DAT_00102027);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 001014a7 to 001014ab has its CatchHandler @ 00101a53
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_148,(allocator *)&DAT_00102029);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 001014e2 to 001014e6 has its CatchHandler @ 00101a6e
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_128,(allocator *)&DAT_0010202b);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 0010151d to 00101521 has its CatchHandler @ 00101a89
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_108,(allocator *)&DAT_0010202d);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101558 to 0010155c has its CatchHandler @ 00101aa4
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_e8,(allocator *)&DAT_00102025);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101593 to 00101597 has its CatchHandler @ 00101abf
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_c8,(allocator *)&DAT_0010202f);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 001015ce to 001015d2 has its CatchHandler @ 00101ada
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_a8,(allocator *)&DAT_00102031);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101606 to 0010160a has its CatchHandler @ 00101af5
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_88,(allocator *)&DAT_00102033);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 0010163e to 00101642 has its CatchHandler @ 00101b0d
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_68,(allocator *)&DAT_0010201d);
std::allocator<char>::~allocator(&local_249);
std::allocator<char>::allocator();
// try { // try from 00101676 to 0010167a has its CatchHandler @ 00101b25
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
((char *)local_48,(allocator *)&DAT_00102033);
std::allocator<char>::~allocator(&local_249);
// try { // try from 00101699 to 0010185f has its CatchHandler @ 00101b3d
pcVar2 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
operator[]((ulong)local_208);
if (*pcVar2 < 'B') {
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_c8);
}
pcVar2 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
operator[]((ulong)local_a8);
if (*pcVar2 != 'A') {
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_68);
}
pcVar2 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
operator[]((ulong)local_1c8);
cVar1 = *pcVar2;
pcVar2 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
operator[]((ulong)local_148);
if ((int)cVar1 - (int)*pcVar2 == 3) {
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_1c8);
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_1e8);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_188);
pcVar2 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
operator[]((ulong)local_168);
if (*pcVar2 == 'G') {
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_168);
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_1a8);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_88);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_228);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_128);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,'}');
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string
(local_48);
前半で文字列をメモリに当てこんでいて,後半でその各メモリの値をチェックしてフラグの文字列に追加するかどうかを判断している. 例えば以下
pcVar2 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
operator[]((ulong)local_208);
if (*pcVar2 < 'B') {
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=
(local_248,local_c8);
}
local_208
の値が <'B'
なら,local_c8 (2)
を追加する,みたいな感じ.
最後に std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(local_248,'}');
があるのでここでおしまい.
Keygenme
なんか libcrypto.so.1.1 が足りないって怒られたのでビルドする
wget https://www.openssl.org/source/old/1.1.0/openssl-1.1.0l.tar.gz
tar xvf openssl-1.1.0l.tar.gz
cd openssl-1.1.0l
./config
make
export LD_LIBRARY_PATH=./openssl-1.1.0l:$LD_LIBRARY_PATH
実行するとライセンスキーを求められる.
printf("Enter your license key: ");
みたいな記述がある関数があるのでこれが main ?
以下少し丁寧にやる
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_98 = 0x7b4654436f636970;
local_90 = 0x30795f676e317262;
local_88 = 0x6b5f6e77305f7275;
local_80 = 0x5f7933;
local_ba = 0x7d;
/* 27words */
sVar1 = strlen((char *)&local_98);
MD5((uchar *)&local_98,sVar1,local_b8);
sVar1 = strlen((char *)&local_ba);
MD5((uchar *)&local_ba,sVar1,local_a8);
local_d0 = 0;
for (local_cc = 0; local_cc < 0x10; local_cc = local_cc + 1) {
sprintf(local_78 + local_d0,"%02x",(ulong)local_b8[local_cc]);
local_d0 = local_d0 + 2;
}
local_d0 = 0;
for (local_c8 = 0; local_c8 < 0x10; local_c8 = local_c8 + 1) {
sprintf(local_58 + local_d0,"%02x",(ulong)local_a8[local_c8]);
local_d0 = local_d0 + 2;
}
for (local_c4 = 0; local_c4 < 0x1b; local_c4 = local_c4 + 1) {
acStack_38[local_c4] = *(char *)((long)&local_98 + (long)local_c4);
}
acStack_38[27] = local_43;
acStack_38[28] = local_62;
acStack_38[29] = local_62;
acStack_38[30] = local_78[0];
acStack_38[31] = local_5b;
acStack_38[32] = local_43;
acStack_38[33] = local_6a;
acStack_38[34] = local_60;
acStack_38[35] = (undefined)local_ba;
sVar1 = strlen(param_1);
if (sVar1 == 0x24) {
for (local_c0 = 0; local_c0 < 0x24; local_c0 = local_c0 + 1) {
if (param_1[local_c0] != acStack_38[local_c0]) {
uVar2 = 0;
goto LAB_00101475;
}
}
uVar2 = 1;
}
local_98~~ みたいなとこがフラグの前半になっていて,残りのマジックナンバーを探そうという感じ.
sVar1 = strlen((char *)&local_98);
MD5((uchar *)&local_98,sVar1,local_b8);
まず,local_98 ~~
には 0x7b ~~
のフラグが入っているので,最初は sVar1 = 17
で,MD5 を計算している.
この結果は local_b8 から見れる.
逆に local_b1
には 0x7d
しか入ってないので 1 文字だけ.
local_d0 = 0;
for (local_cc = 0; local_cc < 0x10; local_cc = local_cc + 1) {
sprintf(local_78 + local_d0,"%02x",(ulong)local_b8[local_cc]);
local_d0 = local_d0 + 2;
}
local_d0 = 0;
for (local_c8 = 0; local_c8 < 0x10; local_c8 = local_c8 + 1) {
sprintf(local_58 + local_d0,"%02x",(ulong)local_a8[local_c8]);
local_d0 = local_d0 + 2;
}
次にここを考える. local_b8, local_a8 には既にハッシュ値が入っていて,local_78 の変数に格納していく. ここで,local_78 は小さい方に伸びることを失念していて時間を使ってしまった. つまり,local_78 -> local_76 みたいな感じで伸びていく.
for (local_c4 = 0; local_c4 < 0x1b; local_c4 = local_c4 + 1) {
acStack_38[local_c4] = *(char *)((long)&local_98 + (long)local_c4);
}
ここは既存のフラグを代入しているだけ
acStack_38[27] = local_43;
acStack_38[28] = local_62;
acStack_38[29] = local_62;
acStack_38[30] = local_78[0];
acStack_38[31] = local_5b;
acStack_38[32] = local_43;
acStack_38[33] = local_6a;
acStack_38[34] = local_60;
acStack_38[35] = (undefined)local_ba;
マジックナンバーを入れるところ. 先に決めていたハッシュ値を見れば代入できる.
静的解析で頑張ったが,gdb でやったら 2 秒で終わった. つまり,マジックナンバーを代入しているアドレスで止めて eax を見ればいい.
001013c8 0f b6 45 c5 MOVZX EAX,byte ptr [RBP + local_43]
001013cc 88 45 eb MOV byte ptr [RBP + local_1d],AL
001013cf 0f b6 45 a6 MOVZX EAX,byte ptr [RBP + local_62]
001013d3 88 45 ec MOV byte ptr [RBP + local_1c],AL
001013d6 0f b6 45 a6 MOVZX EAX,byte ptr [RBP + local_62]
静的解析,パズルで楽しいし筋トレにはなるけど眼の負担がエグいし動的解析が一瞬で終わった時虚無すぎる
Wizardlike
マジでわかんね~と思いながら中見てたら ASCII コードが見えたので気合と根性.
Classic Crackme 0x100
パスワード系の典型問題. memcmp があるのでレジスタを見ればいいか,と思ったが,入力をこねくりまわしている & フラグがサーバ側にある,でちゃんと静的解析する必要がありそう
cmp_flag = 0x747774746971747a;
local_60 = 0x7372667965697478;
local_58 = 0x766f78757a74676c;
local_50 = 0x6e7372626e64666c;
local_48 = 0x647368687976726c;
local_40 = 0x6e786f66727878;
uStack_39 = 0x6c626a;
setvbuf(stdout,(char *)0x0,2,0);
printf("Enter the secret password: ");
__isoc99_scanf(&DAT_00402024,user_input);
local_c = 0;
flag_length = strlen((char *)&cmp_flag);
flag_length__ = (int)flag_length;
local_18 = 0x55;
local_1c = 0x33;
local_20 = 0xf;
local_21 = 'a';
for (; local_c < 3; local_c = local_c + 1) {
for (counter_input = 0; counter_input < flag_length__; counter_input = counter_input + 1) {
local_28 = (counter_input % 0xff >> 1 & local_18) + (counter_input % 0xff & local_18);
local_2c = ((int)local_28 >> 2 & local_1c) + (local_1c & local_28);
iVar1 = ((int)local_2c >> 4 & local_20) +
((int)user_input[counter_input] - (int)local_21) + (local_20 & local_2c);
user_input[counter_input] = local_21 + (char)iVar1 + (char)(iVar1 / 0x1a) * -0x1a;
}
}
iVar1 = memcmp(user_input,&cmp_flag,(long)flag_length__);
重要なのは for を回している部分とハードコードされている情報はリトルエンディアンであること. ソルバは以下
import string
cmp_flag =[0x7a,0x74,0x71,0x69,0x74,0x74,0x77,0x74,0x78,0x74,0x69,0x65,0x79,0x66,0x72,0x73,0x6c,0x67,0x74,0x7a,0x75,0x78,0x6f,0x76,0x6c,0x66,0x64,0x6e,0x62,0x72,0x73,0x6e,0x6c,0x72,0x76,0x79,0x68,0x68,0x73,0x64,0x78,0x78,0x72,0x66,0x6f,0x78,0x6e,0x6a,0x62,0x6c]
local_18 = 0x55
local_1c = 0x33
local_20 = 0xf
local_21 = 'a'
user_input = [None] * 50
pattern = string.ascii_letters
for j in range(50):
for chara in pattern:
tmp_user_input = chara
tmp_user_input = ord(tmp_user_input)
for i in range(3):
local_28 = (j % 0xff >> 1 & local_18) + (j % 0xff & local_18)
local_2c = (local_28 >> 2 & local_1c) + (local_1c & local_28)
iVar1 = (local_2c >> 4 & local_20) + (tmp_user_input - ord(local_21)) + (local_20 & local_2c)
tmp_user_input = ord(local_21) + iVar1 + (iVar1 // 0x1a) * (-1*0x1a)
if(hex(tmp_user_input) == hex(cmp_flag[j])):
print(chara, end="")
break
WinAntiDbg0x100
Windows の実行ファイルをデバッグしてフラグを取ろう,みたいな感じらしい. 実行しながらやるといいらしいので勉強がてら IDA を使ってやってみる.
単純に IDA で実行すると以下のようなメッセージが出る
Debugged application message:
_ _____ _______ ______
(_) / ____|__ __| ____|
_ __ _ ___ ___ | | | | | |__
| '_ \| |/ __/ _ \| | | | | __|
| |_) | | (_| (_) | |____ | | | |
| .__/|_|\___\___/ \_____| |_| |_|
| |
|_|
Welcome to the Anti-Debug challenge!
Debugged application message: ### Level 1: Why did the clever programmer become a gardener? Because they discovered their talent for growing a 'patch' of roses!
Debugged application message: ### Oops! The debugger was detected. Try to bypass this check to get the flag!
デバッガ検出の分岐があるっぽいので,メッセージから見て当該箇所を探す.
push offset asc_6D3438 ; "\n"
call ds:OutputDebugStringW
push offset asc_6D343C ; "\n"
call ds:OutputDebugStringW
call sub_6D11B0
call sub_6D1200
test eax, eax
jnz short loc_6D15E7
test
のところを右クリックすると Add breakpoint
という表示があるのでブレークポイントを設置する.
で,実行すると止まってくれるので,IDA 側で eax
を 0 に書き換えてあげればフラグが返ってくる.
WinAntiDbg0x200
WinAntiDbg0x100 とほとんど同じで,分岐ポイントのフラグを書きかえればOK
weirdSnake
python のデコンパイル結果が渡されるので手でデコンパイルする.
breadth
二つの同じようなバイナリが与えられる. diff を取ってみる.
objdump -d -M intel breadth.v1 > v1.txt
objdump -d -M intel breadth.v2 > v2.txt
diff v1-disassm.txt v2-disassm.txt
2c2
< breadth.v1: file format elf64-x86-64
---
> breadth.v2: file format elf64-x86-64
164220,164225c164220,164225
< 95049: 48 8b 54 24 f0 mov rdx,QWORD PTR [rsp-0x10]
< 9504e: b8 3a 80 37 d0 mov eax,0xd037803a
< 95053: 48 39 c2 cmp rdx,rax
< 95056: 74 08 je 95060 <fcnkKTQpF+0x20>
< 95058: c3 ret
< 95059: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
---
> 95049: 48 8b 44 24 f0 mov rax,QWORD PTR [rsp-0x10]
> 9504e: 48 3d 3e c7 1b 04 cmp rax,0x41bc73e
> 95054: 74 0a je 95060 <fcnkKTQpF+0x20>
> 95056: c3 ret
> 95057: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
> 9505e: 00 00
当該部分にハードコードされているフラグが答えだった
飽きてきたので他の CTF の rev を解こうと思います