某CTFのバイナリ問題 "w4rmup"のwriteup
※このCTFチャレンジは個人的なツテで入手したもので、作問者からCTFのイベント名を公開しないことを条件にWrite Up記載の許可をもらっています。
サマリ
提供されるバイナリは"w4rmup"という64ビットのELFファイル。ユーザーの入力した値を暗号化した後、ファイル内に暗号化された状態でハードコードされている正解flagと比較して、一致していれば"Key Accepted! Congrats!!!"と表示して終了。一致していなければ"Oooh. Are you sure you typed the key correctly??? Try harder!"と表示して終了。
解析
※逆アセンブル内の変数名や関数名は一部解析に当たり、わかりやすいようにデフォルトのものから変更しています。
"w4rmup"をIDAで開いてみると、アドレスの値が極端に小さいことに気がつく。これはバイナリがPIE形式であるため。
以下はreadelfコマンドの実行結果。
$ readelf -h w4rmup
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x6e0
Start of program headers: 64 (bytes into file)
Start of section headers: 10832 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
PIE形式ではプログラム実行時のアドレスを絶対アドレスではなく、相対アドレスで表す。なので、プログラムがどこのアドレスにロードされるかは実際にプログラムを実行してみないとわからない。もし本プログラムをGDBでデバッグする場合はまず"_start"にブレークポイントを設定して(b _start)、ロード先のアドレスを確認した後、適宜解析したい箇所にブレークポイントを設定する。
逆アセンブルを眺めてみると、まず以下のコードが目につく。
.text:0000000000000AF5 C6 45 90 31 mov [rbp+dummy_key], 31h ; '1'
.text:0000000000000AF9 C6 45 91 7A mov [rbp+var_6F], 7Ah ; 'z'
.text:0000000000000AFD C6 45 92 5F mov [rbp+var_6E], 5Fh ; '_'
.text:0000000000000B01 C6 45 93 74 mov [rbp+var_6D], 74h ; 't'
.text:0000000000000B05 C6 45 94 68 mov [rbp+var_6C], 68h ; 'h'
.text:0000000000000B09 C6 45 95 31 mov [rbp+var_6B], 31h ; '1'
.text:0000000000000B0D C6 45 96 7A mov [rbp+var_6A], 7Ah ; 'z'
.text:0000000000000B11 C6 45 97 5F mov [rbp+var_69], 5Fh ; '_'
.text:0000000000000B15 C6 45 98 74 mov [rbp+var_68], 74h ; 't'
.text:0000000000000B19 C6 45 99 68 mov [rbp+var_67], 68h ; 'h'
.text:0000000000000B1D C6 45 9A 33 mov [rbp+var_66], 33h ; '3'
.text:0000000000000B21 C6 45 9B 5F mov [rbp+var_65], 5Fh ; '_'
.text:0000000000000B25 C6 45 9C 72 mov [rbp+var_64], 72h ; 'r'
.text:0000000000000B29 C6 45 9D 33 mov [rbp+var_63], 33h ; '3'
.text:0000000000000B2D C6 45 9E 34 mov [rbp+var_62], 34h ; '4'
.text:0000000000000B31 C6 45 9F 6C mov [rbp+var_61], 6Ch ; 'l'
.text:0000000000000B35 C6 45 A0 5F mov [rbp+var_60], 5Fh ; '_'
.text:0000000000000B39 C6 45 A1 66 mov [rbp+var_5F], 66h ; 'f'
.text:0000000000000B3D C6 45 A2 6C mov [rbp+var_5E], 6Ch ; 'l'
.text:0000000000000B41 C6 45 A3 34 mov [rbp+var_5D], 34h ; '4'
.text:0000000000000B45 C6 45 A4 67 mov [rbp+var_5C], 67h ; 'g'
.text:0000000000000B49 C6 45 A5 5F mov [rbp+var_5B], 5Fh ; '_'
.text:0000000000000B4D C6 45 A6 31 mov [rbp+var_5A], 31h ; '1'
.text:0000000000000B51 C6 45 A7 7A mov [rbp+var_59], 7Ah ; 'z'
.text:0000000000000B55 C6 45 A8 5F mov [rbp+var_58], 5Fh ; '_'
.text:0000000000000B59 C6 45 A9 74 mov [rbp+var_57], 74h ; 't'
.text:0000000000000B5D C6 45 AA 68 mov [rbp+var_56], 68h ; 'h'
.text:0000000000000B61 C6 45 AB 31 mov [rbp+var_55], 31h ; '1'
.text:0000000000000B65 C6 45 AC 7A mov [rbp+var_54], 7Ah ; 'z'
.text:0000000000000B69 C6 45 AD 5F mov [rbp+var_53], 5Fh ; '_'
.text:0000000000000B6D C6 45 AE 6A mov [rbp+var_52], 6Ah ; 'j'
.text:0000000000000B71 C6 45 AF 75 mov [rbp+var_51], 75h ; 'u'
.text:0000000000000B75 C6 45 B0 35 mov [rbp+var_50], 35h ; '5'
.text:0000000000000B79 C6 45 B1 74 mov [rbp+var_4F], 74h ; 't'
.text:0000000000000B7D C6 45 B2 5F mov [rbp+var_4E], 5Fh ; '_'
.text:0000000000000B81 C6 45 B3 66 mov [rbp+var_4D], 66h ; 'f'
.text:0000000000000B85 C6 45 B4 34 mov [rbp+var_4C], 34h ; '4'
.text:0000000000000B89 C6 45 B5 6E mov [rbp+var_4B], 6Eh ; 'n'
.text:0000000000000B8D C6 45 B6 74 mov [rbp+var_4A], 74h ; 't'
.text:0000000000000B91 C6 45 B7 34 mov [rbp+var_49], 34h ; '4'
.text:0000000000000B95 C6 45 B8 73 mov [rbp+var_48], 73h ; 's'
.text:0000000000000B99 C6 45 B9 79 mov [rbp+var_47], 79h ; 'y'
.text:0000000000000B9D C6 45 BA 00 mov [rbp+var_46], 0
"1z_th1z_th3_r34l_fl4g_1z_th1z_ju5t_f4nt4sy" はダミーのflagで、このダミーflagを入力すると、不正解としてプログラムは終了する。
また、実行時に以下のコードで自身がデバッガーでデバッグされているか確認し (GDBなどのデバッガーはプロセスのアタッチにptrace関数を利用する)、デバッガーの存在を確認した場合プログラムを終了する。
.text:0000000000000BBA E8 01 FB FF FF call _ptrace ; anti debugger
.text:0000000000000BBF 48 85 C0 test rax, rax
.text:0000000000000BC2 79 0B jns short loc_BCF
もし、本プログラムをGDBなどのデバッガーでデバッグする場合は上記のコードをNOPするなどして、書き換える必要がある。
ユーザーが入力したflagはstrcmp関数へと渡される。このstrcmp関数はカスタムの関数で、ユーザーが入力したflagを暗号化し、同じく暗号化された正解flagと一致するか確認する。flagが一致していれば戻り値に「1」を返し、一致しない場合は戻り値に「0」を返す。
strcmp関数は、まずユーザーの入力したflagが先述したダミーflagかどうかチェックする。次に入力したflagが42バイトかどうかチェックする。両方のチェックをパスすると、ユーザーの入力したflagを暗号化して、正解flagと一致するか確認する。
以降は暗号化ルーチンの解析。
.text:00000000000008B5 C6 45 B2 69 mov [rbp+XOR_key], 69h ; 'i'
------
.text:0000000000000924 8B 45 B4 mov eax, [rbp+var_4C]
.text:0000000000000927 48 63 D0 movsxd rdx, eax
.text:000000000000092A 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:000000000000092E 48 01 D0 add rax, rdx
.text:0000000000000931 0F B6 00 movzx eax, byte ptr [rax]
.text:0000000000000934 38 45 B2 cmp [rbp+XOR_key], al
.text:0000000000000937 74 22 jz short loc_95B
------
.text:0000000000000939 8B 45 B4 mov eax, [rbp+var_4C]
.text:000000000000093C 48 63 D0 movsxd rdx, eax
.text:000000000000093F 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:0000000000000943 48 01 D0 add rax, rdx
.text:0000000000000946 0F B6 00 movzx eax, byte ptr [rax]
.text:0000000000000949 8B 55 B4 mov edx, [rbp+var_4C]
.text:000000000000094C 48 63 CA movsxd rcx, edx
.text:000000000000094F 48 8B 55 A8 mov rdx, [rbp+usr_input]
.text:0000000000000953 48 01 CA add rdx, rcx
.text:0000000000000956 32 45 B2 xor al, [rbp+XOR_key]
.text:0000000000000959 88 02 mov [rdx], al
ユーザーの入力したflagの先頭1バイトを取り出し、XOR_keyの値と等しいか確認する。等しくない場合は入力された値1バイトとXOR_keyの値をXORする。初回のXOR_keyの値は0x69
.text:000000000000095B 8B 45 B4 mov eax, [rbp+var_4C]
.text:000000000000095E 48 63 D0 movsxd rdx, eax
.text:0000000000000961 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:0000000000000965 48 01 D0 add rax, rdx
.text:0000000000000968 0F B6 00 movzx eax, byte ptr [rax]
.text:000000000000096B 0F B6 C0 movzx eax, al
.text:000000000000096E C1 E0 04 shl eax, 4
.text:0000000000000971 89 C1 mov ecx, eax
.text:0000000000000973 8B 45 B4 mov eax, [rbp+var_4C]
.text:0000000000000976 48 63 D0 movsxd rdx, eax
.text:0000000000000979 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:000000000000097D 48 01 D0 add rax, rdx
.text:0000000000000980 0F B6 00 movzx eax, byte ptr [rax]
.text:0000000000000983 C0 E8 04 shr al, 4
.text:0000000000000986 09 C1 or ecx, eax
.text:0000000000000988 8B 45 B4 mov eax, [rbp+var_4C]
.text:000000000000098B 48 63 D0 movsxd rdx, eax
.text:000000000000098E 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:0000000000000992 48 01 D0 add rax, rdx
.text:0000000000000995 89 CA mov edx, ecx
.text:0000000000000997 88 10 mov [rax], dl
.text:0000000000000999 8B 45 B4 mov eax, [rbp+var_4C]
.text:000000000000099C 48 63 D0 movsxd rdx, eax
.text:000000000000099F 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:00000000000009A3 48 01 D0 add rax, rdx
.text:00000000000009A6 0F B6 00 movzx eax, byte ptr [rax]
.text:00000000000009A9 88 45 B2 mov [rbp+XOR_key], al
.text:00000000000009AC 8B 45 B4 mov eax, [rbp+var_4C]
.text:00000000000009AF 48 63 D0 movsxd rdx, eax
.text:00000000000009B2 48 8B 45 A8 mov rax, [rbp+usr_input]
.text:00000000000009B6 48 01 D0 add rax, rdx
.text:00000000000009B9 0F B6 10 movzx edx, byte ptr [rax]
.text:00000000000009BC 8B 45 B4 mov eax, [rbp+var_4C]
.text:00000000000009BF 48 98 cdqe
.text:00000000000009C1 0F B6 44 05 C0 movzx eax, [rbp+rax+enc_flag]
.text:00000000000009C6 38 C2 cmp dl, al ; dl holds user input, al holds encrypted flag
1. XORした値を左へ4シフトさせてecxレジスタに格納
2. XORした値を右へ4シフトさせてeax (al)レジスタに格納
3. ecxの値とeaxの値をOR演算して結果をedxレジスタに格納
4. 3.のOR演算結果の下位8ビットをXOR_keyに格納し、次回のXOR演算時のキーとして使用する。
5. enc_flag (正解flag) の先頭1バイトをeaxレジスタに格納。enc_flagには以下のデータが格納されている。
0xD19BAA0D2541B3CEAB3CF0FA88BBF8496170C15F83CD9B3FE0A96FE01D3734B68DDB48E26D30440747E3
最後にedx (dl) に格納されている暗号化されたユーザー入力のflag1バイトとeax (al) に格納されている暗号化された正解flag1バイトが等しいか確認する。この処理を繰り返してユーザーの入力したflagを1バイトずつ検証する。
。。。文章にしても、いまいちスッと頭に入ってこないので上述の暗号化ルーチンを擬似コードに落とし込んでみる
original_encrypted_flag = "D1 9B AA 0D 25 41 B3 CE AB 3C F0 FA 88 BB F8 49 61 70 C1 5F 83 CD 9B 3F E0 A9 6F E0 1D 37 34 B6 8D DB 48 E2 6D 30 44 07 47 E3"
XOR_key = 0x69 //初回のXORキー
i = 0
loop:
if (usr_input[i] != XOR_key):
XORed_user_input = (usr_input[i] ^ XOR_key)
left_shifted = XORed_user_input << 4
right_shifted = XORed_user_input >> 4
encrypted_user_typed_flag = (left_shifted | right_shifted)
XOR_key = encrypted_user_typed_flag[-8:] //暗号化したユーザー入力の下位8ビットを次回のXORキーとして使用する
is(original_encrypted_flag[i] == encrypted_user_typed_flag)
i += 1
平文の正解flagを取得するには上記の暗号化ルーチンと逆の処理をする必要がある。
以下のスクリプトを書いた。
#!/usr/bin/env python
## Get XOR keys
enc_flag = "209 155 170 13 37 65 179 206 171 60 240 250 136 187 248 73 97 112 193 95 131 205 155 63 224 169 111 224 29 55 52 182 141 219 72 226 109 48 68 7 71 227" ## 0xD19BAA0D2541B3CEAB3CF0FA88BBF8496170C15F83CD9B3FE0A96FE01D3734B68DDB48E26D30440747E3
enc_flag_list = enc_flag.split(' ')
enc_flag_length = len(enc_flag_list)
XOR_key = []
al = 0
i = 0
n = 0
while (n < enc_flag_length):
OR_ed = (al << 4 | al >> 4)
lsb = int(bin(OR_ed).replace('0b', '')[-8:], 2)
if (lsb == int(enc_flag_list[i])):
#print(al)
XOR_key.append(al)
al = 0
i += 1
n += 1
else:
al += 1
## Perform XOR with encrypted flag
encrypted_flag = "105 209 155 170 13 37 65 179 206 171 60 240 250 136 187 248 73 97 112 193 95 131 205 155 63 224 169 111 224 29 55 52 182 141 219 72 226 109 48 68 7 71" # 105 (0x69) is hard-coded initial XOR key
encrypted_flag = encrypted_flag.split(" ")
flag_length = len(encrypted_flag)
decoded_flag = ""
for i in range(0, (flag_length)):
decoded_flag += chr(int(encrypted_flag[i]) ^ int(XOR_key[i]))
print(decoded_flag)
$ python re200-solver-final.py
th1z_1z_th3_r34l_fl4g_th1z_a1nt_n0_f4nt4sy
flagはth1z_1z_th3_r34l_fl4g_th1z_a1nt_n0_f4nt4sy
以上。