中山大学计算机组成原理实验bomblab.docx
《中山大学计算机组成原理实验bomblab.docx》由会员分享,可在线阅读,更多相关《中山大学计算机组成原理实验bomblab.docx(40页珍藏版)》请在冰点文库上搜索。
中山大学计算机组成原理实验bomblab
《计算机组成原理实验》
实验报告
(实验一)
学院名称
:
数据科学与计算机学院
专业(班级)
:
学生姓名
:
学号
时间
:
2019
年
10
月
8
日
成绩
:
实验一
:
X86汇编基础二进制炸弹
一.实验目的
(1)初步认识X86汇编语言;
(2)掌握阅读程序反汇编代码的方法,了解程序在机器上运行的实质;
(3)熟悉Linux环境、掌握调试器gdb和反汇编工具objdump的使用。
二.实验内容
使用课程知识拆除一个“BinaryBomb”(,简称炸弹)来增强对程序的机器级表示、汇编语言、调试器和逆向工程等理解。
二进制炸弹是一个Linux可执行C程序,包含phase_1~phase_6共6个阶段和一个隐藏阶段secret_phase。
你将获得一个唯一且每位同学差异化的炸弹程序。
炸弹运行各阶段要求输入一个字符串,若输入符合程序预期,该阶段炸弹被“拆除”,否则“爆炸”。
实验目标是你需要拆除尽可能多的炸弹。
每个炸弹阶段考察机器级语言程序不同方面,难度递增。
阶段1:
字符串比较
阶段2:
循环
阶段3:
条件/分支:
含switch语句
阶段4:
递归调用和栈
阶段5:
指针
阶段6:
链表/指针/结构
隐藏阶段,第4阶段的之后附加一特定字符串后才会出现
拆弹技术:
为了完成二进制炸弹拆除任务,你需要
1.使用gdb调试器和objdump反汇编工具;
2.单步跟踪调试每一阶段的机器代码
3.理解汇编语言代码的行为或作用
4.进而设法“推断”出拆除炸弹所需的目标字符串。
5.需要在每一阶段的开始代码前和引爆炸弹的函数前设置断点,便于调试。
三.实验器材
PC机一台,装有Linux操作系统的虚拟机一套。
四.实验过程与结果
说明:
根据需要书写相关内容,如:
程序流程图、设计的思想与方法、分析、实验步骤和实验结果及分析等。
4.1第一关
在刚接触这一大堆汇编代码时,我是迷茫的。
好在pdf上给出了第一关的解决方法,而且第一关的难度也并不大。
Phase_1函数一开始进行栈指针修改,后面我们知道这是函数调用时分配栈空间的必经之路。
然后函数压栈两个数据,执行一个函数并进行一个判断,最后返回。
观察到strings_not_equal函数的调用,它判断栈顶的前两个元素。
如果它们相等,就修改eax为0;如此,紧随其后的jne就不会触发,也就不会导致的调用。
栈顶的两个元素分别为我们的输入,和$0x804a21c内存处的数据。
用gdb查看,可知它是字符串,为
只要让它们相等即可,所以我们本关的答案毫无疑问就是
IcanseeRussiafrommyhouse!
4.2第二关
第二关脱离了指导开始独立作业,我再一次迷茫了。
第二关的代码数量突然陡增,同样先观察代码;首先仍然是准备工作,然后程序调用了一个叫做的函数,我们找到这个函数一探究竟。
可以看到函数用scanf函数读取用户输入的六个数据,地址间间隔为4;再深究可以在gdb中找到格式化字符为%d整形,所以本关应该是要求我们输入6个整数型。
我们知道0x8(%ebp)是我们的输入,那么就是把这个输入变成六个整形放入-0x24(%ebp)中,可以假设这里是一个整形数组arr。
再向下看,函数进行一次判断
也就是arr[0]>5时就触发爆炸。
我们知道了输入的第一个数不能大于5.
再继续,下面进入一个循环语句。
Ebx应该是循环的控制单位,每次循环自增1,等于6时跳出循环。
eax保存一个变址寻址的结果,然后进行eax位左移,并把它与当前寻址结果的前一个地址进行比较,不相等就触发爆炸。
如果循环顺利执行完毕,本关就被破解。
至此逻辑已经很明确了,我们可以把它翻译成C代码。
if(arr[0]>5)
explode_bomb();
for(inti=1;i!
=6;i++){
inttmp=arr[i-1];
tmp<
if(tmp!
=arr[i])
explode_bomb();
}
End
就这样,假设我们的arr[0]是1,则第一次左移1位,第二次左移2位…循环5次,我们就得到arr数组应该装着的6个整形数字。
Step1:
1
Step2:
2
Step3:
8
Step4:
64
Step5:
1024
Step6:
32768
所以本关的答案是12864102432768
4.3第三关
顺利完成第二关终于提升了我的一点信心,再看第三关就没那么可怕(不)
一开始函数压栈4个数值,然后调用scanf函数,可以推断它们都是scanf的参数。
-0x10(%ebp)和-0x14(%ebp)都是明确的地址,0x8(%ebp)是我们的输入,这个0x804a46d就显得很可疑,gdb一看就知道它是格式化字符串。
那么本关就是让我们输入两个整形,它们分别存储在-0x14(%ebp)和-0x10(%ebp)两个位置。
scanf函数的返回值放在eax中,它的返回值是2(接受两个参数),我们设-0x14(%ebp)和-0x10(%ebp)的两个参数为a和b。
紧跟其后的是判断语句cmpl$0x7,-0x14(%ebp)ja8048ca8//爆炸
限制了a的取值,a必须小于等于7.又由于ja比较无符号数,ja还要大于等于0
下面的语句是解决本题的关键性语句jmp*0x804a26c(,%eax,4)
一个基址寻址,*0x804a26c里面的东西是什么?
可能是一个跳转表,用gdb查看之。
果然如此,如此一来,我们的输入a(0~7)就会导致8种跳转。
这应该是一个switch语句!
再往下就是一些算术操作,对不同的a输入会有不同的算术。
唯一的生死点是一个判断语句cmpl,它限制了a的取值为0<=a<=5.
最后让eax,也就是算术结果和第二个输入b相等即可;我们也不需要通读代码来计算输入,只需要当程序执行到这句时,用gdb查看一下eax的值,就能拿到我们的第二个输入b。
本题应该有a为0,1,2,3,4,5六组答案,我们只取一种a=0的情况,这时计算得到的b是-117.第三关的答案就是0-117!
4.4第四关
第四关与前面的不同在于它多了一个函数。
先观察phase_4主体,发现和第三关的输入方式一样,都是两个整形a和b。
这次我们观察到a的输入范围被限制到了0<=a<=14
紧跟其后的是压栈三个参数,14,0和a,然后调用函数func4,我们判断func4接受三个参数,而这次调用就是func4(a,0,14)
好的我们进入func4的代码
首先是从栈中拿出参数放入寄存器,然后对参数进行了一系列的加减移位计算,最后进行两次判断,这样就是三条分支语句;应该是if-elseif-else语句。
判断1:
ebx>eba时:
这时把eax,edx,ebx-1递归传入func4,返回func4(eax,edx,ebx-1)。
判断2:
ebx这时把eax,ebx+1,原始的ecx递归传入func4,返回func4(eax,ebx+1,ecx+edx)
乍一看这个函数很难知道它想用递归实现什么,但是用高级语言复现这个函数是很简单的,我们可以用python实现该函数,并逐一尝试输入a为0~14的情况。
Func4执行完毕后,返回值在eax中,随后执行最后的步骤
判断返回值和输入b是否为0x1f(31),这样我们的输入a必须让函数返回31,b必须是31.
使用python模拟该程序的运行,结果如下。
可见输入为13时,func4返回31.即a=13,b=31就是我们要的答案。
第四关答案为1331!
4.5第五关
我们已经掌握了一些套路,接下来的关卡虽然难度大了,但并没有前几关那么棘手了。
一开始还是观察输入格式,发现并没有scanf的格式化输入,可以判断本关输入是一个字符串;程序一开始有mov0x8(%ebp),%ebx
push%ebx
call8049029
cmp$0x6,%eax
jne8048dee
把我们的输入字符串放入一个string_length函数中,通过阅读这个函数发现它会返回字符串长度,保存在eax中。
然后eax与6作比较,不相等则爆炸,于是我们判断本关是要求我们输入一个6长度字符串。
紧随其后的是一段循环语句
8048da2:
b800000000mov$0x0,%eax
8048da7:
0fb61403movzbl(%ebx,%eax,1),%edx
8048dab:
83e20fand$0xf,%edx
8048dae:
0fb6928ca20408movzbl0x804a28c(%edx),%edx
8048db5:
885405edmov%dl,-0x13(%ebp,%eax,1)
8048db9:
83c001add$0x1,%eax
8048dbc:
83f806cmp$0x6,%eax
8048dbf:
75e6jne8048da7
Ebx是输入的字符串,设为str,eax应该是for循环的控制单位,也做下标使用,设为i。
程序每次循环读取str[i]的值,然后用一个and0xf取其后四位。
随后有一个基址寻址0x804a28c(%edx),不知道这块内存存储着什么,打开看一看。
是一串字符串,设它为s,那么这句就是取s[str[i]&0xf];后面还把它放入了-0x13(%ebp,%eax,1)这段地址中,如此一来我们就可以翻译这段代码为C代码:
for(inti=0;i!
=6;i++){
chartmp=s[str[i]&0xf];
arr[i]=tmp;
}
再往后就是一个函数的调用
8048dc8:
6862a20408push$0x804a262
8048dcd:
8d45edlea-0x13(%ebp),%eax
8048dd0:
50push%eax
8048dd1:
e875020000call804904b
8048dd6:
83c410add$0x10,%esp
8048dd9:
85c0test%eax,%eax
8048ddb:
7518jne8048df5
函数比较-0x13(%ebp),也就是上面的arr,和0x804a262内存处的字符串。
打开0x804a262看看里面是什么.
一个单词bruins,要让arr和它相等,就要从s中分别找到这六个字母的下标,然后用把这些下标作为输入的六个字符的后四位来形成我们需要的目的字符串。
寻找下标和转为字符串的工作可以用高级语言实现,下面是python实现过程。
于是我们就得到了一种可能的答案MFCDHG。
即第五关的答案为MFCDHG。
4.6第六关
还是从输入数据格式入手,发现和第二关的处理输入的部分相同,都是借助函数,把输入用scanf截获为六个整数型。
它们保存在-0x3c(%ebp)处的一个序列,设为arr。
紧随其后的是一个嵌套的二重循环语句
第一层循环先拿出arr[i],然后做一个判断
8048e28:
8b44b5c4mov-0x3c(%ebp,%esi,4),%eax
8048e2c:
83e801sub$0x1,%eax
8048e2f:
83f805cmp$0x5,%eax
8048e32:
770cja8048e40//爆炸
就是判断arr[i]是否小于等于6,因为使用了无符号数判断ja,还要满足arr[i]>=0。
后面有一个小循环,取当前的i并加1,然后增长到5,设这个变量为ii,则
8048e53:
3944b5c0cmp%eax,-0x40(%ebp,%esi,4)
8048e57:
75eejne8048e47
8048e59:
e821040000call804927f
判断语句让arr[ii]==arr[i-1]时触发炸弹。
把这段语句翻译为C代码如下:
for(inti=0;i!
=6;){
if(arr[i]-1>5)
Explode_bomb();
i++;
for(intii=i;ii<=5;ii++){
if(arr[ii]==arr[i-1])
Explode_bomb();
}
}
也就是arr中的元素必须是0~6的数字,而且不能重复。
下面的一段程序又是一个二重循环
8048e60:
8b5208mov0x8(%edx),%edx
8048e63:
83c001add$0x1,%eax
8048e66:
39c8cmp%ecx,%eax
8048e68:
75f6jne8048e60
8048e6a:
8954b5dcmov%edx,-0x24(%ebp,%esi,4)
8048e6e:
83c301add$0x1,%ebx
8048e71:
83fb06cmp$0x6,%ebx
8048e74:
741eje8048e94
8048e76:
89demov%ebx,%esi
8048e78:
8b4c9dc4mov-0x3c(%ebp,%ebx,4),%ecx
8048e7c:
b801000000mov$0x1,%eax
8048e81:
ba54c10408mov$0x804c154,%edx
8048e86:
83f901cmp$0x1,%ecx
8048e89:
7fd5jg8048e60
8048e8b:
ebddjmp8048e6a
这段代码是循环寻找0x804c154+arr[i]*8的地址,然后把这块地址放到-0x24(%ebp,%esi,4)中,它应该是储存指针的数组,至于这是指向什么的指针,我们可以在gdb中找到0x804c154看一看。
一个叫做node的结构体,那么数组-0x24(%ebp)就是node*nodes[]类型。
再观察结构体的内部数据,由三部分组成,value,number和next,这是我们熟悉的链表结构,C语言代码如下:
Structnode{
Intval;
Intnum;
Node*next;
}
如此一来上面的步骤就说得通了,翻译成C代码(0x804c154用L表示)
For(inti=0;i!
=6;i++){
Intc=arr[i],a=1;
Node*d=L;
If(c>1){
For(;a!
=c;a++){
d=d->next;
}
Nodes[i]=d;
}
很显然这段代码就是按照我们输入arr中的六个数,以L为链表头搜索链表的第arr[i]个结点。
并把链表结点保存在nodes[i]中。
再下面的一段程序,很显然是按照nodes的顺序重新串接链表的一系列操作。
最后一段判断程序,遍历链表并比较链表前一个元素和后一个元素
8048ebc:
be05000000mov$0x5,%esi
8048ec1:
eb08jmp8048ecb
8048ec3:
8b5b08mov0x8(%ebx),%ebx
8048ec6:
83ee01sub$0x1,%esi
8048ec9:
7410je8048edb
8048ecb:
8b4308mov0x8(%ebx),%eax
8048ece:
8b00mov(%eax),%eax
8048ed0:
3903cmp%eax,(%ebx)
8048ed2:
7defjge8048ec3
8048ed4:
e8a6030000call804927f
8048ed9:
ebe8jmp8048ec3
Eax是ebx的后一个链表结点,必须满足ebx>eax才能不触发炸弹,也就是整个链表的值必须是降序的。
这样这一关的用意就很明显了,就是要我们输入6个数字,这六个数字就是重排后链表结点的顺序;我们要保证这个顺序是降序的,就要按照合适的顺序输入123456.
排序的工作可以借助高级语言完成,python的实现如下。
即本关答案为265431
4.7隐藏关
我们惊喜地发现事情还没有结束!
在phase_6之后还有一段,它要怎么触发呢?
我们在源码中搜索它,发现它出现在下面的位置。
在函数中,而这个函数是每次拆弹成功后才被调用的。
读代码可以发现这里先调用scanf,要我们输入一段字符串;然后调用,当相等时才能调用函数。
可是每一关最后都调用这个函数,这个输入应该出现在哪里呢?
找到scanf的输入参数
查看其中的格式化字符如下:
是跟在两个整数后面的一段字符串!
要输入两个整数的是第三关和第四关,而第三关已经有了输入数量的限制,所以触发关卡是要在第四关后面多输入一串特殊字符串。
后面把输入字符串和0x804a4d0进行比较,打开0x804a4d0查看,它就是特殊字符串。
进关条件就是在第四关输入1331SecretSYSU
我们进入了关卡,下面浏览这一关的代码。
可以看见函数一开始调用了read_line,把输入放在eax中。
然后是函数的调用,strtol(%eax,NULL,10)。
也就是我们输入的字符串应该是10进制数,它会被转换成一个长整形。
紧随其后有一条判断
8048f62:
8d40fflea-0x1(%eax),%eax
8048f65:
83c410add$0x10,%esp
8048f68:
3de8030000cmp$0x3e8,%eax
8048f6d:
7735ja8048fa4
判断(%eax-1)>0x3e8时触发爆炸,限制了我们的输入范围。
之后是fun7的调用,参数分别为我们的输入和$0x804c0a0,查看内容可知为24.
执行fun7(0x804c0a0,input),后面可以看到如果返回值为3就完成本关。
查看fun7函数,通读可知是一个if-elseif-else语句。
8048efa:
8b5508mov0x8(%ebp),%edx
8048efd:
8b4d0cmov0xc(%ebp),%ecx
8048f00:
85d2test%edx,%edx
8048f02:
743cje8048f40
从栈中获取参数,设两个参数为int*p,intx
首先判断p==NULL,如果相等就ret并返回-1.
8048f40:
b8ffffffffmov$0xffffffff,%eax
8048f45:
ebccjmp8048f13
然后是分支判断语句
8048f04:
8b1amov(%edx),%ebx
8048f06:
39cbcmp%ecx,%ebx
8048f08:
7f0ejg8048f18
8048f0a:
b800000000mov$0x0,%eax
8048f0f:
39cbcmp%ecx,%ebx
8048f11:
7518jne8048f2b
8048f13:
8b5dfcmov-0x4(%ebp),%ebx
8048f16:
c9leave
8048f17:
c3ret
当*p>ecx时,返回fun7(*(a+4),b)*2
当*p==ecx时,返回0
Else,返回fun7(*(a+8),b)*2+1
用C语言描述为
intfun7(int*a,intb)
{
if(a==NULL)
return-1;
intret=0;
if(*a-b>0)
{
ret=fun7(*(a+4),b);
ret*=2
}
elseif(*a-b==0)
return0;
else
{
ret=fun7(*(a+8),b);
ret=ret*2+1;
}
returnret;
}
涉及到内存0x804c0a0连续的块中的内容,查看之。
很难一眼看出答案,所以不妨借助高级语言进行模拟。
对内存的访问可以借助高级语言的哈希表实现,比如这里我使用python的字典,它的灵活性让哈希表可以用地址的数值模拟访问。
这样借助mem函数和memory哈希表,对定义好的地址有确定的返回值,未定义的地址有内容为0.
再寻求返回值为3的情况,找到输入b=107时可以达成。
所以这隐藏关的答案也被我们找到,是107.
怎么感觉隐藏关的难度并没有phase6大…
五.实验心得
虽然说本实验中,7个关卡的难度是不断增加的,但从我个人的体验上,解决问题的难度却是不断减小的。
这应该是因为在实验过程中,我不断在学习新的调试工具,不断提升对汇编码的熟悉度,以及不断增强对逆向过程的理解。
这个实验的核心就是逆向工程,七个关卡依次是C语言中的函数调用、循环和数组、switch语句、递归、寻址方式、多重循环及结构体,和隐藏关卡的综合题。
由于我并没有任何汇编和x86基础,在实验的开始,我几乎没法理解汇编代码在做些什么。
然而借助帮助手册和搜索引擎,我逐渐开始能够理解汇编代码的含义和工作方式。
应该说“看不懂汇编代码”是实验中我面临的第