CSAPP的实验笔记(网址http://csapp.cs.cmu.edu/3e/labs.html)。真够无聊的……
Bomb Lab
先反汇编,从main
开始,依次调用initialize_bomb
phase_1
phase_defused
...直到phase_6
phase_defused
一系列函数。
phase_defused
先从这个被调用了6次的函数看起,因为我估计这个应该相对简单,算热身一下。
它会将num_input_string
的值和6比较(一个位于0x603760
的全局变量,应该是输入过多少次字符串了),如果不相等直接结束。
随后它调用sscanf,(这里已经优化成用寄存器传参了)r8 rcx rdx分别是三个接收的变量,传入的格式串为0x402619处的"%d %d %s"
,输入串则在0x603870
,不知道实际是什么。
如果成功读到3个数,会调用strings_not_equal
,参数包括0x402622
的"DrEvil"
和一个变量(推测这个是刚刚读入的%s
)。如果返回0,那么调用puts
输出"Curses, you've found the secret phase!" "But finding it and solving it are quite different"
;随后调用secret_phase
函数。
不论是否返回0,都会输出"Congratulations! You've defused the bomb!"
,随后结束。如果没有成功读到3个数那么直接结束。
从这里我们可以看出,这是第6关之后的一个隐藏关,需要某个字符串具有%d %d DrEvil
的形式才会开启。我们等6关玩完了再解它。
initialize_bomb
这个函数很短,调用了signal
函数,参数包括0x4012a0
(函数指针,指向sig_handler
)以及一个数2。查阅资料,这个函数用于设置程序对某种信号的响应策略,2是某种信号值常量。
再看这个信号处理函数,它会输出"So you think you can stop the bomb with ctrl-c, do you?"
,sleep 3秒,然后printf"Well..."
,fflush stdout,再sleep 1秒,输出"OK. :-)"
结束。
试一下就知道,这个处理其实只是阻断一下Ctrl-C的退出而已,没什么重要的。
phase_1
也很短,比较一下输入字符串和0x402400
即字符串"Border relations with Canada have never been better."
(注意有一个点,ASCII码0x2e),失败则调用explode_bomb
爆炸。
phase_2
首先调用read_six_numbers
,参数是%rsp=%rsi
,然后传入了%rsi + 0/0x4/0x14/0x10/0xc/0x8
六个参数(部分寄存器部分栈),以及格式串0x4025c3即"%d %d %d %d %d %d"
。如果返回值大于6则结束,否则爆炸。
随后会进行一系列判断,包括(%rsp)=1
、2*(%rsp)=(%rsp+0x4)
、2*(%rsp+0x4)=(%rsp+0x8)
……其实就是要求输入等比数列1 2 4 8 16 32
。
phase_3
首先用sscanf
输入,格式串为0x4025cf
的"%d %d
,存入%rsp+0xc/0x8
。然后要求(%rsp+0x8)<=7
,跳转到0x402470+8*(%rsp+0x8)
。这是一个表:
1 | 402470 7c0f4000 00000000 b90f4000 00000000 |
可以看到,根据(%rsp+0x8)
的值为0~7,会跳转到不同的地方,对应会给%eax赋予0xcf 0x137 0x2c3 0x100 0x185 0xce 0x2aa 0x147
,随后统一跳转,并要求(%rsp+0xc)=%eax
。
由于前面的要求,(%rsp+0x8)
只能为7,从而(%rsp+0xc)
只能为0x147也就是327。输入7 327
。
phase_4
首先还是用sscanf
输入两个数字存入%rsp+0xc/0x8
。然后要求0xe<=(%rsp+0x8)
,然后调用func4
,参数为%edx=0xe
%esi=0
%edi=(%rsp+0x8)
,要求返回0。随后再要求(%rsp+0xc)=0
。
func4(a,b,c)
是递归函数,首先计算%ecx = d = (((a-b)>>>31)+(a-b))>>1 + b
,将其与c
相比。如果c<d
,返回func4(d-1,b,c)*2
;如果c=d
,返回0;如果c>d
,返回func4(a,d+1,c)*2+1
。
很多符号方面的细节感觉搞得不太对。总而言之,这个func4
是一个二分的过程,当(a+b)/2=c
的时候可以直接结束,因此我们尝试输入7 0
,发现直接成功。
phase_5
首先调用string_length
函数,要求长度为6,输入的字符串应该在%rdi=%rbx。然后%rax从0开始逐渐增加到6,这个过程中计算0x4024b0 + (%rax+%rbx) & 0xf
,将这个地址对应的字节存入%rsp+%rax+0x10
的位置。之后再对0x40245e
和%rsp+0x10
的字符串调用strings_not_equal
,要求返回0(也就是两个字符串相同)。
0x4024b0
开始的16个字符为maduiersnfotvbyl
。0x40245e
开始的字符串为flyers
。这应该是某种置换过程,对应观察,输入的六个字符的低16位应该分别为0x9 0xf 0xe 0x5 0x6 0x7
。在ASCII的小写字母区查找(不在小写字母区应该也可以,但我没有试了),对应字符串应该为ionefg
,确实可行。
phase_6
首先还是调用read_six_numbers
,参数同样是%rsp,所以传入的6个数仍然是%rsp + 0/0x4/0x14/0x10/0xc/0x8
。然后和变帽子一样把值在寄存器里传来传去:
%rsp
的值存入%r13 %rsi %r14 %rbp,%r12d=0
。然后要求(%r13)-0x1<=0x5
,并让%ebx=%r12d+1
,检查要求(%rsp+%ebx*4)!=(%r13+0)
直到%ebx=5
,再%r13=%rsp+4
,重复循环。 因此可以估计到这个循环是要求所有数大于等于6,并且互不相同,也就是所有数构成1~6的一个排列才对。
接下来,%rsi=%rsp+0x18
,%rax=%r14=%rsp
,%ecx=7
。然后,%edx=%ecx
,并让%edx-=(%rax)
,存入(%rax)
,%rax+=4
,循环直到%rsi=%rax
。 这个循环是将6个数从\(x\)变成\(7-x\)。
接下来,%rsi=0
,检查%ecx=(%rsp+%rsi)
,如果小于等于1,将0x6032d0
存入(%rsp+2*%rsi+0x20)
,%rsi+=4
,如果%rsi
等于0x18,就结束循环;否则,重复上面的操作。 如果大于1,%eax=1
,%edx=0x6032d0
,然后一个新循环,%rdx=(%rdx+0x8)
,%eax+=1
,比较%ecx
与%eax
,如果不等则继续此循环,相等则将%rdx存入(%rsp+2*%rsi+0x20)
,%rsi+=4
,检查大循环条件。
这整个循环比较复杂,%rsp+0x20
开始的是一个新的数组。考察一下0x6032d0
的内容:
1 | 6032d0 4c010000 01000000 e0326000 00000000 |
从而,数据的传递链为:0x6032d0 -> 0x6032e0 -> 0x6032f0 -> 0x603300 -> 0x603310 -> 0x603320 -> 0
。我们注意到这其实是一个链表结构。
所以,这个循环根据输入数的值,在新数组中存入对应的地址,对应关系为:
原来的数 | 6 | 5 | 4 | 3 | 2 | 1 |
---|---|---|---|---|---|---|
7-x处理后 | 1 | 2 | 3 | 4 | 5 | 6 |
新数组中 | 6032d0 | 6032e0 | 6032f0 | 603300 | 603310 | 603320 |
接下来,%rcx=%rbx=(%rsp+0x20) %rax=%rsp+0x20+0x8 %rsi=%rsp+0x20+0x30
。然后循环开始,每轮将%rdx=(%rax)
存入(%rcx+8)
,再%rax+=8
,如果%rax=%rsi
就结束循环,否则将上面那个值也就是%rdx
存入%rcx
,继续循环。循环结束后(%rdx+0x8)=0
。 这个循环从1号元素开始,更新了一遍上面的表(指第3 4列)。新数组1号元素对应的表值被存入0号元素对应的表……以此类推。最后一个存入0。这也就是说我们按照新数组的顺序重排了链表,现在新数组0号元素的后继是新数组1号元素,以此类推。
最后的最后,%ebp=5
,循环开始,%rax=(%rbx+0x8)
,%eax=(%rax)
,并要求%eax < (%rbx)
。之后%rbx=(%rbx+0x8)
,%ebp-=1
继续循环直到%ebp=0
。 很明显这是一种有序性的要求。具体而言,要求链表从新数组0号元素出发,其值单调递减。并且,这个要求的是4个字节大小的范围。
我们甚至可以写出大概的代码:
1 | struct node{ |
回到原来的链表,表中元素的值以及他们应当对应的新数组位置为:
表地址 | 6032d0 | 6032e0 | 6032f0 | 603300 | 603310 | 603320 |
---|---|---|---|---|---|---|
表中元素 | 014c | 00a8 | 039c | 02b3 | 01dd | 01bb |
对应位置 | 4 | 5 | 0 | 1 | 2 | 3 |
从而反推输入的值:
新数组地址 | 6032f0 | 603300 | 603310 | 603320 | 6032d0 | 6032e0 |
---|---|---|---|---|---|---|
反转后输入 | 3 | 4 | 5 | 6 | 1 | 2 |
输入值 | 4 | 3 | 2 | 1 | 6 | 5 |
输入4 3 2 1 6 5
。
附加关
第6关做的呕吐,所以附加关不做了,直接看网上答案怎么做的就行。
Attack Lab
主要是利用栈的漏洞进行攻击,分为代码注入和面向返回攻击两部分。
代码注入
代码顺序main -> stable_launch -> launch -> test
。里面主要是处理服务器相关的东西,我们不用管,test
的结构说明书上已经写了。
Level 1
我们的目的是构造特殊的输入,使得缓冲区溢出,从而跳转到touch1
函数。用gdb执行调试,在getbuf
开始时停顿,反汇编。 我们可以看到,%rsp-0x28
是buf的开始位置,返回地址存放于%rsp
处,而touch1
的值为0x4017c0
。 我们只需要前0x28=40个字符非换行(比如,空格0x20),然后再8个字符等于返回地址0x00000000004017c0
即可,答案如下:
1 | 20 20 20 20 |
Level 2
我们需要构造一段程序,将%rdi赋予cookie(0x59b997fa
),然后通过ret函数跳转到touch2
函数。为此我们需要构造如下的程序:
1 | mov $0x59b997fa, %rdi |
buf开始的先存放这部分代码,末尾存放指向这个代码的返回地址;末尾的后面则要再追加touch2
的地址0x4017ec
(用于上面的ret
指令,注意栈是从高地址往低地址增长的,出栈则会反过来)。 此外通过gdb
可以得知,buf的起始地址为0x5561dc78
,这就是第一个返回地址。答案如下:
1 | 48 c7 c7 fa |
Level 3
我们需要构造一段程序,将一个指向内容为"59b997fa"
的字符串指针赋给%rdi,然后调用touch2
。
根据ASCII对应,这个字符串应该为35 39 62 39 39 37 66 61 00
。 不妨将这个字符串就存放在第一个返回地址的前面,可以计算出这个位置为 根据说明书的指导,我们不能放在这种位置,因为它会被0x5561dc78+0x28-0x9=0x5561dc97
。hexmatch
等函数所使用的堆栈覆盖掉。
我们需要把字符串放在第二个返回地址的后面,可以计算出这个位置为0x5561dc78+56=0x5561dcb0
。 程序代码不需要动,把立即数响应修改一下就可以了。 第二个返回地址也应该修改为touch3
的地址0x4018fa
。答案如下:
1 | 48 c7 c7 b0 |
面向返回攻击
面向返回攻击的大概思想是,由于x86的编码机制,可以从指令的中间截断并将后半部分视为一个新指令;我们希望在已有的程序中发掘一些形如“有用的指令+ret”的片段,将它们组合成一段可用的程序。
Level 2
我们在start_farm
到mid_farm
中间一段的代码中寻找可能有用的片段。
getval_280
的指令片段58 90 c3
对应代码popq %rax; nop; retq
,起始地址0x4019ca+2=0x4019cc
。
addval_273
的指令片段48 89 c7 c3
对应代码mov %rax, %rdi; reqt
,起始地址0x4019a0+2=0x4019a2
。
有了这两段指令,我们从栈上取出0x59b997fa
到%rax
,再传入%rdi
,调用touch2
,完成。还是注意栈从高往低增长,所以我们的地址从低往高放。答案为:
1 | 20 20 20 20 |
Level 3
首先考虑一下我们的思路。我们会在栈上构造一个"59b997fa"
,并希望将它的地址传给%edi
。但是,由于栈随机化的存在,我们需要将%rsp
的值传给%rdi
,然后用popq
指令调整到正确的返回地址。
这个过程不会是一下子完成的。我们先考虑最后一步,一定是popq
之后做一些可能没用的操作,然后ret
。可以这样做的情况包括:
函数 | 指令 |
---|---|
addval_219 |
pop %rax |
addval_280 |
pop %rax |
getval_481 |
pop %rsp; movl %eax,%edx |
好像没什么有意义的。再看看mov %rsp, ???
的指令:
函数 | 指令 |
---|---|
addval_190 |
movq %rsp, %rax |
addval_110 |
movl %esp, %eax |
setval_181 |
movl %esp, %eax |
setval_350 |
movq %rsp, %rax |
再考虑出现过那几个功能性nop的指令:
函数 | 指令 |
---|---|
getval_311 |
movl %edx, %ecx |
addval_187 |
movl %ecx, %esi |
getval_159 |
movl %edx, %ecx |
addval_487 |
movl %eax, %edx |
好吧我承认我这里没思路了……
上网看了一下答案,有一个重要的工具是add_xy
里的lea (%rdi,%rsi,1),%rax
。为此我们的思路为:
%rsp -> %rax -> %rdi
,得到一个栈上的地址;pop %rax(%eax) -> %edx -> %ecx -> %esi(%rsi)
,从栈上获得一个偏移量;%rdi+%rsi -> %rax -> %rdi
,此时加起来的值要恰好指向我们在栈上的字符串地址。
所以序列为:
指令 | 起始地址 |
---|---|
movq %rsp, %rax |
0x401a06 |
movq %rax, %rdi |
0x4019a2 |
popq %rax |
0x4019cc |
movl %eax, %edx |
0x401a42 |
movl %edx, %ecx |
0x401a34 |
movl %ecx, %esi |
0x401a27 |
lea (%rdi, %rsi, 1), %rax |
0x4019d6 |
movq %rax, %rdi |
0x4019a2 |
touch3... |
0x4018fa |
在0x4018fa
的后面放置字符串。获取到的rsp恰好是第二个地址的位置,中间相隔8个地址和1个偏移量,所以偏移量等于9*8=72=0x48。
1 | 20 20 20 20 |
Malloc Lab
按照指令,我们需要在mm.c
里实现mm_init
mm_malloc
mm_free
mm_realloc
四个函数进行动态内存分配,而底层则是memlib.c
里的mem_sbrk
函数(这个函数可以扩大堆空间的范围)。
没心思做太多优化,而且公开的压缩包里也只有两个小规模的数据集。所以我们参考CSAPP书上的内容,实现一个非常简单的首次分配算法的动态内存分配。
- 首先实现基础设施,包括提取header的位置、计算长度、查看和设置是否已经被分配了;
mm_malloc
遍历所有块,找到可用且空间足够的块,否则就申请新的内存;mm_free
不仅释放块,而且将它与后面的空闲块合并;mm_realloc
先将块与后面的空闲块合并;如果可以分裂,就将块分裂成两部分;否则就用mm_malloc
申请一个新的。
1 | trace valid util ops secs Kops |
效果比较差,不管他了。
Shell Lab
我们主要要实现shell中的多任务功能。UNIX系统调用接口真够难用的,尤其是waitpid
的机制、进程组的概念这两个地方。
按照设计,我们需要依次完成以下几个函数:
eval
:调用parseline
解析命令行参数;分辨命令名,如果是内置命令(quit
jobs
bg
fg
)就调用builtin_cmd
直接执行;否则:fork
- 用
setpgid
生成一个新的进程组ID(这一步是为了能够区分每个job的后代,每个job的第一个进程和后代进程共享一个进程组ID) execvp
- 调用
addjob
添加任务,设置任务状态,并且:- 如果前台执行,调用
waitfg
等待子进程执行完毕; - 如果后台执行,输出这个job的信息提示。
- 如果前台执行,调用
builtin_cmd
:再分辨命令名,quit
直接退出,jobs
调用listjobs
,否则调用do_fgbg
;do_fgbg
:首先进行参数检查并得到要处理的job,然后和上面一样更新任务状态并waitfg
或者输出信息;waitfg
:使用waitpid
系统调用,阻塞式地等待前台进程所在进程组ID的所有进程阻塞或终止,然后根据进程的状态输出相应信息;sigchld_handler
:使用waitpid
系统调用,非阻塞式地查询是否有任一子进程阻塞或终止,然后根据进程的状态输出相应信息;sigint_handler
sigtstp_handler
:将相同信号用kill
系统调用再次传给前台进程所在进程组ID的所有进程。