picoCTF 2024 Writeup - Reverse Engineering
packer - 100 points
ELFのバイナリが提供される
stringsでもフラグっぽいものが見当たらない
$ strings out | grep -i pico
$ strings out | grep -i flag
タイトルがpackerなのでupxでunpackする
$ upx -d out
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 3rd 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
877724 <- 336520 38.34% linux/amd64 out
Unpacked 1 file.
stringsすると今度は引っかかる
$ strings out | grep -i flag
Password correct, please see flag: 7069636f4354467b5539585f556e5034636b314e365f42316e34526933535f39343130343638327d
(mode_flags & PRINTF_FORTIFY) != 0
WARNING: Unsupported flag value(s) of 0x%x in DT_FLAGS_1.
version == NULL || !(flags & DL_LOOKUP_RETURN_NEWEST)
flag.c
_dl_x86_hwcap_flags
_dl_stack_flags
7069636f4354467b5539585f556e5034636b314e365f42316e34526933535f39343130343638327dをhexとして解釈すると、フラグは以下になる。
picoCTF{U9X_UnP4ck1N6_B1n4Ri3S_94104682}
FactCheck - 200 points
バイナリが提供される。
デコンパイラのHex-Raysでデコンパイルして、フラグを作っている部分を抜き出すと以下のようなコードになる。
std::allocator<char>::allocator(&v21, argv, envp);
std::string::basic_string(v22, "picoCTF{wELF_d0N3_mate_", &v21);
std::string::basic_string(v23, "4", &v21);
std::string::basic_string(v24, "5", &v21);
std::string::basic_string(v25, "6", &v21);
std::string::basic_string(v26, "3", &v21);
std::string::basic_string(v27, "e", &v21);
std::string::basic_string(v28, "5", &v21);
std::string::basic_string(v29, "a", &v21);
std::string::basic_string(v30, "e", &v21);
std::string::basic_string(v31, "e", &v21);
std::string::basic_string(v32, "d", &v21);
std::string::basic_string(v33, "b", &v21);
std::string::basic_string(v34, "f", &v21);
std::string::basic_string(v35, "6", &v21);
std::string::basic_string(v36, "e", &v21);
if ( *(char *)std::string::operator[](v24, 0LL) <= 65 ) // true
std::string::operator+=(v22, v34); // v22 += "f"
if ( *(_BYTE *)std::string::operator[](v35, 0LL) != 65 ) // true
std::string::operator+=(v22, v37); // v22 += "d"
if ( "Hello" == "World" ) // false
std::string::operator+=(v22, v25);
v19 = *(char *)std::string::operator[](v26, 0LL);
if ( v19 - *(char *)std::string::operator[](v30, 0LL) == 3 ) // false
std::string::operator+=(v22, v26);
std::string::operator+=(v22, v25); // v22 += "6"
std::string::operator+=(v22, v28); // v22 += "5"
if ( *(_BYTE *)std::string::operator[](v29, 0LL) == 71 ) // false
std::string::operator+=(v22, v29);
std::string::operator+=(v22, v27); // v22 += "e"
std::string::operator+=(v22, v36); // v22 += "e"
std::string::operator+=(v22, v23); // v22 += "4"
std::string::operator+=(v22, v31); // v22 += "e"
std::string::operator+=(v22, 125LL); // v22 += "}"
この文字列連結を実行すると、フラグは以下になる。
picoCTF{wELF_d0N3_mate_fd65ee4e}
Classic Crackme 0x100 - 300 points
バイナリが提供されるが、これはサンプルフラグになっており、実際のフラグはインスタンスにアクセスして取得する。
バイナリをHex-Raysでデコンパイルするとmain関数は以下のようになる。
//----- (0000000000401176) ----------------------------------------------------
int __fastcall main(int argc, const char **argv, const char **envp)
{
char input[51]; // [rsp+0h] [rbp-A0h] BYREF
char output[51]; // [rsp+40h] [rbp-60h] BYREF
int random2; // [rsp+7Ch] [rbp-24h]
int random1; // [rsp+80h] [rbp-20h]
char fix; // [rsp+87h] [rbp-19h]
int secret3; // [rsp+88h] [rbp-18h]
int secret2; // [rsp+8Ch] [rbp-14h]
int secret1; // [rsp+90h] [rbp-10h]
int len; // [rsp+94h] [rbp-Ch]
int i_0; // [rsp+98h] [rbp-8h]
int i; // [rsp+9Ch] [rbp-4h]
strcpy(output, "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze");
setvbuf(_bss_start, 0LL, 2, 0LL);
printf("Enter the secret password: ");
__isoc99_scanf("%50s", input);
i = 0;
len = strlen(output);
secret1 = 85;
secret2 = 51;
secret3 = 15;
fix = 97;
while ( i <= 2 )
{
for ( i_0 = 0; i_0 < len; ++i_0 )
{
random1 = (secret1 & (i_0 % 255)) + (secret1 & ((i_0 % 255) >> 1));
random2 = (random1 & secret2) + (secret2 & (random1 >> 2));
input[i_0] = ((random2 & secret3) + input[i_0] - fix + (secret3 & (random2 >> 4))) % 26 + fix;
}
++i;
}
if ( !memcmp(input, output, len) )
printf("SUCCESS! Here is your flag: %s\n", "picoCTF{sample_flag}");
else
puts("FAILED!");
return 0;
}
ユーザの入力(input)に対して、while (i<=2)のブロックで変換処理を入れて、outputと等しくなれば正解としてフラグを出力する動きになっている。
outputに対して変換処理の逆演算を実行することで、入力すべき文字列を作成するスクリプトを作る
output = "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze"
length = len(output)
secret1 = 85
secret2 = 51
secret3 = 15
fix = 97
input = list(output)
for _ in range(3):
for i in range(length):
random1 = (secret1 & (i % 255)) + (secret1 & ((i % 255) >> 1))
random2 = (random1 & secret2) + (secret2 & (random1 >> 2))
input[i] = chr((ord(input[i]) - fix - (secret3 & (random2 >> 4)) - (random2 & secret3)) % 26 + fix)
print("".join(input))
これを実行すると以下のように出力される
qezjdvjltvuxavgiibmkrsncqrntxfykgmntwsepmyvyguzwtv
インスタンスにアクセスしてこの文字列を入力するとフラグを取得できる。
Enter the secret password: qezjdvjltvuxavgiibmkrsncqrntxfykgmntwsepmyvyguzwtv
SUCCESS! Here is your flag: picoCTF{s0lv3_angry_symb0ls_4699696e}
weirdSnake - 300 points
ファイルが提供される。
このファイルは以下のようなテキストで、pythonのバイトコードをdisassembleした結果のようである。
1 0 LOAD_CONST 0 (4)
2 LOAD_CONST 1 (54)
4 LOAD_CONST 2 (41)
6 LOAD_CONST 3 (0)
8 LOAD_CONST 4 (112)
10 LOAD_CONST 5 (32)
12 LOAD_CONST 6 (25)
14 LOAD_CONST 7 (49)
16 LOAD_CONST 8 (33)
18 LOAD_CONST 9 (3)
20 LOAD_CONST 3 (0)
disassembleしたものを直接実行するようなツールは探してみたが見つからなかったので、自力でpythonのコードに変換してみる。
基本的にはdisモジュールにdisassembleした命令の定義が記載されている。最新のバージョンではなく少し古いバージョンで書かれているようなので、古いドキュメントを読む必要がある。
def listcomp(iterator):
result = []
for char in iterator:
result.append(ord(char))
return result
def listcomp2(iterator):
result = []
for a, b in iterator:
result.append(a ^ b)
return result
input_list = [4,54,41,0,112,32,25,49,33,3,0,0,57,32,108,23,48,4,9,70,7,110,36,8,108,7,49,10,4,86,43,104,44,91,7,18,106,124,89,78]
key_str = 'J'
key_str = '_' + key_str
key_str = key_str + 'o'
key_str = key_str + '3'
key_str = 't' + key_str
key_list = listcomp(iter(key_str))
while len(key_list) < len(input_list):
key_list.extend(key_list)
result = listcomp2(iter(zip(input_list, key_list)))
result_text = ''.join(map(chr, result))
print(result_text)
これを実行すると以下のフラグが出力される
picoCTF{N0t_sO_coNfus1ng_sn@ke_7f44f566}
Discussion