玩了半个月,该学习了=_=
在网上找到了一个计算机系统的教学:https://wdxtub.com/2016/04/16/thin-csapp-2/,写的挺好的,简化了很多《深入理解计算机系统》中的知识。
个人觉得可以先去看上面网址的教程,再去决定要不要去看《深入理解计算机系统》这本书,比较书太厚了,想要快速了解可能不太适合。
汇编
寄存器介绍 :
寄存器(%rax, %rbx, %rcx, %rdx, %rsi, %rdi)称为通用寄存器,有其『特定』的用途,而 %rsp(%esp) 和 %rbp(%ebp) 则是作为栈指针和基指针来使用的:
%rax(%eax) 用于做累加 //返回值
%rcx(%ecx) 用于计数 //第4个参数
%rdx(%edx) 用于保存数据 //第3个参数
%rbx(%ebx) 用于做内存查找的基础地址 //被调用者保存
%rsi(%esi) 用于保存源索引值 //第2个参数
%rdi(%edi) 用于保存目标索引值 //第1个参数
movl指令以寄存器为目的时,他会把寄存器的高位4字节设置为0.(mov后面的字母中:b为传送字节,w为传送字(2个字节),l为传送4个字节,q为传送8个字节),其中X86-64架构处理器不能让两个操作数(源操作与目的操作)都指向内存位置,需要两条指令实现,将内存的值传送给寄存器,然后第二条指令将寄存器的数据传给内存,以及正确使用mov的各种版本,需要具体看目的操作数和源操作数的位数或者说是范围,如 movl %eax (%rsp)
,rsp是64位的,而eax是32位的,也就是rsp=eax,rsp高位4字节设置为0,使用movl就足够了。(或者看源操作数与目的操作数中,若两者中有一个是立即数或者是内存,则看另一个寄存器来决定mov的size)
在阅读《深入理解计算机系统》的时候,看到练习题3.3中有一个题:
movb $0xF, (%ebx)
//书上给这个操作是错误的解释是ebx不能用来当地址寄存器,感觉挺懵的,为什么不能当呢?去查一下stackoverflow,也没有相关的解释=_=
下面是一个fib函数,以及相应的汇编代码。
代码:
#include"apue.h"
#include<stdio.h>
int fib(int n){
if(n==1&&n==2){
return 1;
}
return fib(n-1)+fib(n-2);
}
汇编以及个人的一些注释解释:
.file "test2.c"
.text
.globl fib
.type fib, @function
fib:
.LFB0:
.cfi_startproc
pushq %rbp //把基指针(地址)push到栈中,可用来找传入的参数(被调用者保存寄存器)
//执行完pushq %ebp,SP与CFA偏了16字节(rbq占8,返回或者回退地址占8)
.cfi_def_cfa_offset 16 .cfi_offset 6, -16
movq %rsp, %rbp // 把rsp的数据存入rbp,或者是rbp=rsp (指针)
.cfi_def_cfa_register 6
pushq %rbx //用于存储后面递归调用fib(n-1)+fib(n-2)存储值(被调用者保存寄存器)
subq $24, %rsp //为局部变量分配地址
.cfi_offset 3, -24
movl %edi, -20(%rbp)//把edi赋值给n
cmpl $1, -20(%rbp) //比较 n和1
jne .L2 //如果n!=1跳转到L2
cmpl $2, -20(%rbp)
jne .L2
movl $1, %eax
jmp .L3
.L2:
movl -20(%rbp), %eax //参数传给eax
subl $1, %eax //eax-1 === n-1
movl %eax, %edi //把n-1传给fib的第一个参数
call fib
movl %eax, %ebx //ebx存放结果
movl -20(%rbp), %eax //eax取参数n的值
subl $2, %eax //n-2
movl %eax, %edi
call fib
addl %ebx, %eax //fib(n-1)结果+fib(n-2)结果
.L3:
addq $24, %rsp
popq %rbx
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size fib, .-fib
.section .rodata
gcc -Og -S (-Og告诉编译器按照原始C代码结构的机器代码优化等级生成汇编代码):
.file "test2.c"
.text
.globl fib
.type fib, @function
fib:
.LFB31:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pushq %rbx
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
subq $8, %rsp
.cfi_def_cfa_offset 32
movl %edi, %ebx
leal -1(%rdi), %edi
call fib
movl %eax, %ebp
leal -2(%rbx), %edi
call fib
addl %ebp, %eax
addq $8, %rsp
.cfi_def_cfa_offset 24
popq %rbx
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE31:
.size fib, .-fib
.globl m_sum
.type m_sum, @function
可以看到经过编译器生成的汇编代码比我们所编写的代码要长出许多,且我们编写的fib函数是一个递归函数。
跳转指令表:
拆除bomb实验
该实验是在《深入理解计算机系统》官网下的lab。
第一关
从官网上到的bomb.tar文件解压得到以下文件:
既然是拆除bomb,我们就看看里面bomb里面有什么吧。
在终端输入objdump -t bomb|less
为了方便看,我们搜索与bomb相关的内容,输入/bomb,于是看到相关的内容有这几个。
1、bomb.c
2、initialize_bomb_solve
3、explode_bomb
4、bomb_id
5、initialize_bomb
可以在符号表那里看到2、3、5都是函数,而3是使bomb爆炸的函数,然后我们在看看该文件反汇编后的汇编代码。
在终端输入objdump -d bomb|less
bomb反汇编代码:
我们可以大概看出400e19
地址那里是调用initialize_bomb
,然后接着往下看
后面有调用phase_1,phase_2之类的内容,我们看看phase_1是什么吧
可以看到在phase_1中,在call_strings_not_equal后,test %eax %eax
, 如果相等就跳转到400ef7
,也就是避过了调用explode_bomb,算是炸弹拆除成功。
其实在此之前,我们去看看bomb是怎么expload。
可以看到,程序需要我们输入字符来继续程序,随便输入一个后。
可以看到,爆炸啦~,所以综合上面,我们的拆弹工作开始了。
首先综合上面,我们要关注的点是expload_bomb
和第一关phase_1
.
即:
然后gdb直接运行到第一个断点处(run/r),然后就到了输入字符处:
可以看到程序运行到了我们打的断点上,phase_1,然后查看该处的汇编代码。
箭头处是$rip,也就是程序计数器所运行的指令。
这时候我们并看不出什么,我们需要知道寄存器中存储着什么值.
这时候我们看到%rax存储着一个值,于是查看一下,发现正是我们输入的值”hello”
我们在一步步运行,看看指令在执行到call_strings_equal之前的寄存器存储的状态。
此时可以看到%rsi的值与上图中的不同 ,可以看出%rdi存储的是我们输入的字符串,也就是该函数的第一个参数,而rsi可能就是第二个参数或者是一个局部变量,然后我们查看 %esi中保存的是什么值,这个值也许就是解除bomb的线索。
接下来再步进运行,可以看到eax中的值是1,也就是两个字符串不相等,后面就不用截图了,接下来的结果就会执行到 call explode_bomb,触发爆炸,所以我们可以得出Border relations with Canada have never been better.
,就是我们的解。
测试我们得出的解是否正确。
正确,进入下一关~
第二关
第二关难度增大了,需要对寄存器有一些了解。
推荐去看一下这篇关注如何查看寄存器内容:http://blog.chinaunix.net/uid-20593827-id-1918499.html
因为是我们存入的数据一般都是4字节或者以下,所以使用 x/4wd的方式去查看寄存器或者内存地址。
开始第二关,日常设置断点,以及输入我们的猜测值,hello
。
phase_2反汇编代码:
拆炸弹可以看到其中call的函数,我们先去read_six_numbers
函数看看,如下图,可以看到运行到最后会因为%eax中的值小于5而导致expload,这是因为sscanf在格式化字符串(偏移量+36那里,%esi
被赋值为”%d %d %d %d %d %d",用x/s去查看%esi得知的)
失败返回的值为0,从这里我们也看出了,我们所需要输入到phase_2中的key应该是6个数字的字符串,且每个数字中间需要有空格。
read_six_numbers反汇编代码:
上面是分析read_six_numbers函数,现在我们再来看看phase_2,从上面我们知道可能会产生6个变量用于存储我们的字符数字,当程序顺利执行 read_six_numbers
且第一个数字变量可以看出必定是1,否则就会call explode_bomb
,接下来跳转到+52的位置,赋值给把%rsp+0X4
位置的值赋给%rbx
,这个值可能就是第二个变量(不考虑压栈顺序,一共6个变量),然后把%rsp+0x18
位置的值给%rbp
,看到这里的时候我有一些疑惑,因为这些知识我就匆匆忙忙的扫一眼,不太清楚+0X18能到哪,如果换算成10进制,就是移动24个字节,假设一个变量占4字节,那么就是移动到最后一位,也就是最后一个变量,然后就跳转到+27位置,这时候就相当于判断第一个变量(数)与后一个的值,因为 eax=rbx-0X4
,而rbx=rsp+0x4
,然后执行je判断值是否相等(2*eax==rbx)
,我们就可以猜出这是一个等比数列,也就是第二关的解是“1 2 4 8 16 32”
当然,我们上面只是假设,而且根据判断,上面的答案90%是对的,我们现在需要验证的是,%rsp指向的地址存放的值,是不是我们所猜想的那样,也就是是否有6个变量压入栈中。
(这里是解释+57位置0x18(%rsp)
的含义:上面图中的拆弹实验初始输入的是“1 2 3 4 5 6”
,查看了%rsp
到其后3个单位(0x4一个单位)的值,可以看到%rsp
当前地址存放的值为1,而移动0x4位后就到2,以此类推,而上面移动0x18位的解释是移动的地址满16进位,然后再移动8位,也就是6)
第二关的解是“1 2 4 8 16 32”
到此,第二关通关啦~
=============================2017.7.21更新=============================
第三关
有了前面两个练习的基础,后面的关卡分析就知道怎么去拆除bomb了。
第三关开始,日常打断点,反汇编,题目已经告诉我们要输入两个数字了,所以就尝试性的输入1 2
试试。
phase_3反汇编代码:
从上面的代码可以分析出,%eax
值必须大于1(输入的数字数量大于1),以及0x8(%rsp)
的值小于7,然后一路到+50的位置,jmpq跳转到内存地址为*0x402470+8*%rax
的位置,我们可以用查内存的,查处该地址存放的是哪条指令。
代码执行到+39位置时,%rsp状态:
*0x402470+8*%rax
的内存地址存放的指令(%rax存放的值为1):
从上图可以看到跳转后的地址为0x00400fb9,也就是+118的位置。
此时接下去运行就是判断%eax
(0x137)与0xc(%rsp)
(2)值是否相等,不相等就expload ,所以我们就可以得出,我们应该输入的数字是 1 311
。
这时候我们就很容易看出,其实这就是一个switch语句,switch(第一个数字),然后看case中的数字是否和我们输入的第二个数字是否相等,否则就expload,如我们输入的是“1 2”
,即switch(1),然后if(2==311)。
所以,第三关,通关啦~
第四关
这关算是加上了前几关的一些内容,如调用函数求值,与检测字符
开始闯关,日常打断点,反汇编。~_~(有点后悔之前不直接把控制台信息复制过来,截图容易崩掉上传图片的一个控件=_=)
此关需要了解一下算数左/右移与逻辑左/右移的区别,c语言中的移动好像都是逻辑移动。
控制台信息如下:
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
1 311
Halfway there!
14 13
Breakpoint 1, 0x000000000040100c in phase_4 ()
(gdb) disas
Dump of assembler code for function phase_4:
=> 0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx
0x000000000040101a <+14>: mov $0x4025cf,%esi
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
0x0000000000401048 <+60>: callq 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq
我们可以看到前面内容和第3关差不多,检测字符,然后判断数字个数是否等于2,再然后就判断第一个参数是否小于14,接下来的内容,我们可以看出,第一个参数的值是小于14的,而+69位置可以判断第二个参数为0,所以我们就看+60位置调用func4传入的参数,以及其反汇编代码。
从+51位置看起,可以知道调用func4的第二个参数为0,第一个参数为我们传入phase_4的第一个数字或者说是字符(因为传字符进来转化为数字,用变量存着了),当调用完func4函数后,查看%eax & %eax
的值是否为0,只有为0才能运行,也就是说func4的返回值必须为0,才能拆除bomb!
那我们就来看看func4的反汇编吧:
Dump of assembler code for function func4:
=> 0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax
0x0000000000400fd4 <+6>: sub %esi,%eax
0x0000000000400fd6 <+8>: mov %eax,%ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx
0x0000000000400fdb <+13>: add %ecx,%eax
0x0000000000400fdd <+15>: sar %eax
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx
0x0000000000400fe2 <+20>: cmp %edi,%ecx
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx
0x0000000000400fe9 <+27>: callq 0x400fce <func4>
0x0000000000400fee <+32>: add %eax,%eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov $0x0,%eax
0x0000000000400ff7 <+41>: cmp %edi,%ecx
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57>
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq
我们可以看到该汇编中含有调用自身的指令,所以这是一个递归过程的函数,我看了挺久没分析出来,觉得这类问题,最好能还原出原代码才容易去分析,当然这个函数还算好还原,所以比较容易的按照上面的汇编代码一步一步还原出来。
还原代码:
int f(int x, int y) {
int ret = x - y;
int c = ret > 0 ? 1 : 0;
ret = (ret + c) >> 1;
c = ret + y;
if (c <= x) {
if (c == x) {
return ret;
}
ret = 2 * f(x, c + 1) + 1;
}
else {
ret = 2 * f(c - 1, y);
}
return ret;
}
从代码中很容易看出,当第一次传入f的参数x为0的时候,才会返回0,我也测试了一下0-14之间,只有当x=0才符合。
所以第四关的解是 0 0
.
第五关
第五关的难度就大大增大了,代码量也挺多的。
为了简短一些,直接展示其汇编代码吧,本关卡我一开始输入的字符串是“hellow”。
Start it from the beginning? (y or n) y
Starting program: /root/Desktop/bomb/bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
1 311
Halfway there!
0 0
So you got that one. Try this one.
hellow
phase_5反汇编:
Dump of assembler code for function phase_5:
=> 0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax
0x0000000000401073 <+17>: mov %rax,0x18(%rsp)
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1)
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq
一天做两个就花了一堆时间,再看到这样的代码,还是有点无力。
分析开始:
首先从+24,+29行,我们可以知道,我们输入的字符串长度必须为6,然后跳转到+122位置,把%eax
设置为0后,再跳转到+41的位置。
到+41的位置的时候要注意,使用的是movzbl(传送1字节到一个可以容纳4字节的地方,然后其他位填充0和movl用途差不多),此时就是把%rbx+%rax
位置的单字节传送给%ecx
,而此时的%rax
为0,也就是而%rbx
保存的是输入的字符串“hellow”
,所以此时的%ecx
值为h
。
接下去,就是把%cl
(%ecx
的最小的寄存器,表示0~7位)传给%rdx
。
然后到+52位置这里做了一个and运算,用%edx = %edx and 0xf
,即取0~4位,那取这个来干嘛呢?我们继续看下去,又是一个movzbl 0x4024b0(%rdx),%edx
,这时候出现了一个内存地址,0x4024b0(%rdx)
,因为前面%edx
只取后4位,所以%rdx
的范围就是0~15
,而h的ascii码值的16进制,我们可以在查找到。
(gdb) p /x $cl
$15 = 0x65
不能用x/ 命令去内存中查,因为该寄存器就存着0x65这个值,而不是地址,可以使用命令i register
去查看主要寄存器状态,然后再决定用x还是p去查看。
所以得出了%rdx
的值为0x5
,然后我们再来看看,0x4024b0
与0x4024b0+0x5
的地址存放着什么。
(gdb) x/ 0x4024b0
0x4024b0 <array.3449>: 0x6d
(gdb) x/s 0x4024b0
0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
可以看到,这个地址存放着的是个字符数组,我们的第一个命令查到的是第一个字符的ascii值,所以接下来就x/s查询,而0x4024b0+0x5
我们就知道了,是从e开始的字符串,即:ersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
。
所以我们就知道了,movzbl 0x4024b0(%rdx),%edx
之后,%edx
存放的是字符e
。
接下来就是把%dl
传送给%rsp+%rax+0x10
位置,而%rax
会自增,增加到0x6
,小于6则又跳转到+41位置,开始上面的循环,所以为了方便,也知道是循环了,就设置断点到后面的地址。
(gdb) b *0x4010ae
Breakpoint 2 at 0x4010ae
我们打的断点在+76的位置,而+76位置的指令是给%rsp+0x16的数据设置为0,而从上面循环中,知道%rsp+0x10+(0x0~0x5)
的位置是存放字符,而这个指令会不会是添加‘\0’
字符呢?我们去看看吧。
寄存器状态:
(gdb) i registers
rax 0x6 6
rbx 0x6038c0 6305984
rcx 0x77 119
rdx 0x73 115
rsi 0x6038c0 6305984
rdi 0x6038c0 6305984
rbp 0x0 0x0
rsp 0x7fffffffdfe0 0x7fffffffdfe0
r8 0x7ffff7ff7007 140737354100743
r9 0x0 0
r10 0x6 6
r11 0x246 582
r12 0x400c90 4197520
r13 0x7fffffffe0f0 140737488347376
r14 0x0 0
r15 0x0 0
rip 0x4010b3 0x4010b3 <phase_5+81>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
我们要查找的%rsp
地址是0x7fffffffdfe0
,那么要查找保存字符的地方,就是 0x7fffffffdff0
。
(gdb) x/ 0x7fffffffdff0
0x7fffffffdff0: "nevvls"
这个字符串的构成,就是按我们输入的字符串中的每个字符的ascii码值的前4位(0~3位)的值设为x(或者说是偏移量)+0x4024b0
这个字符串首地址,得到0x4024b0
这个地址偏移x位的字符。
0x4024b0:"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
接下来的这几条指令,很容易就看出,是比较由我们输入的字符串构成的新字符串,和$0x40245e这个字符串对比,判断是否相等。
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
下面这个就是$0x40245e
的值了,一开始我以为立即数在内存里面查不了,发现还是能查的,哈哈,似乎是存在一个地址中,而那个地址,我自己猜可能那个是十进制的地址吧。
(gdb) x/s 0x40245e
0x40245e: "flyers"
或者
(gdb) p 0x40245e
$18 = 4203614
(gdb) x/s 4203614
0x40245e: "flyers"
可以看到的是,我们要对比的字符串是"flyers"
,和我们构成的字符串"nevvls"
相差甚远,但我们也知道了,去选取字符串来构成 "flyers"
,那就是我们输入的字符串第一个字符的ascii值的十六进制应该是(范围是0x61-0x7a,注意小写以及对应的ascii为字符):0x69,0x6f,0x6e,0x65,0x66,0x67,好像解应该只有这一组,因为判断字符相等应该也判断大小写是否相等 。
至此,复杂的第五关通关~
[root@yanghao bomb]# ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
1 311
Halfway there!
0 0
So you got that one. Try this one.
ionefg
Good work! On to the next...
第六关
这关的反汇编代码,炒鸡长…..心态爆炸!!!
开始今天的闯关,听说还有隐藏关,我!今!天!不!做!了!
phase_6反汇编代码:
Dump of assembler code for function phase_6:
=> 0x00000000004010f4 <+0>: push %r14
0x00000000004010f6 <+2>: push %r13
0x00000000004010f8 <+4>: push %r12
0x00000000004010fa <+6>: push %rbp
0x00000000004010fb <+7>: push %rbx
0x00000000004010fc <+8>: sub $0x50,%rsp
0x0000000000401100 <+12>: mov %rsp,%r13
0x0000000000401103 <+15>: mov %rsp,%rsi
0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers>
0x000000000040110b <+23>: mov %rsp,%r14 //把rsp的地址给r14
0x000000000040110e <+26>: mov $0x0,%r12d//r12d值为0
0x0000000000401114 <+32>: mov %r13,%rbp
0x0000000000401117 <+35>: mov 0x0(%r13),%eax //第一个元素传送给eax
0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax // 得出eax<=6
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb>
0x0000000000401128 <+52>: add $0x1,%r12d //r12d++
0x000000000040112c <+56>: cmp $0x6,%r12d // r12d==6
0x0000000000401130 <+60>: je 0x401153 <phase_6+95>
0x0000000000401132 <+62>: mov %r12d,%ebx //ebx=r12d
0x0000000000401135 <+65>: movslq %ebx,%rax //rax=ebx
//eax=rsp+4*rax <==>eax=rsp+4*r12d
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax
//比较第一个元素和第rax或者r12d个元素是否相等
0x000000000040113b <+71>: cmp %eax,0x0(%rbp)
0x000000000040113e <+74>: jne 0x401145 <phase_6+81>
0x0000000000401140 <+76>: callq 0x40143a <explode_bomb>
0x0000000000401145 <+81>: add $0x1,%ebx //ebx之前等于(r12d++),ebx+=1
0x0000000000401148 <+84>: cmp $0x5,%ebx //比较是否ebx<=5
//如果jle成立,则循环判断所有元素是否相等
0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
0x000000000040114d <+89>: add $0x4,%r13 //r13=r13+0x4 第二个元素
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>//循环
0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi
0x0000000000401158 <+100>: mov %r14,%rax
0x000000000040115b <+103>: mov $0x7,%ecx
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx
0x0000000000401164 <+112>: mov %edx,(%rax)
0x0000000000401166 <+114>: add $0x4,%rax
---Type <return> to continue, or q <return> to quit---
0x000000000040116a <+118>: cmp %rsi,%rax
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
0x000000000040116f <+123>: mov $0x0,%esi
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx
0x000000000040117a <+134>: add $0x1,%eax
0x000000000040117d <+137>: cmp %ecx,%eax
0x000000000040117f <+139>: jne 0x401176 <phase_6+130>
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148>
0x0000000000401183 <+143>: mov $0x6032d0,%edx
0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2)
0x000000000040118d <+153>: add $0x4,%rsi
0x0000000000401191 <+157>: cmp $0x18,%rsi
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183>
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx
0x000000000040119a <+166>: cmp $0x1,%ecx
0x000000000040119d <+169>: jle 0x401183 <phase_6+143>
0x000000000040119f <+171>: mov $0x1,%eax
0x00000000004011a4 <+176>: mov $0x6032d0,%edx
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx
0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax
0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi
0x00000000004011ba <+198>: mov %rbx,%rcx
0x00000000004011bd <+201>: mov (%rax),%rdx
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx)
0x00000000004011c4 <+208>: add $0x8,%rax
0x00000000004011c8 <+212>: cmp %rsi,%rax
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222>
0x00000000004011cd <+217>: mov %rdx,%rcx
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx)
0x00000000004011da <+230>: mov $0x5,%ebp
0x00000000004011df <+235>: mov 0x8(%rbx),%rax
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx)
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250>
0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx
---Type <return> to continue, or q <return> to quit---
0x00000000004011f2 <+254>: sub $0x1,%ebp
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235>
0x00000000004011f7 <+259>: add $0x50,%rsp
0x00000000004011fb <+263>: pop %rbx
0x00000000004011fc <+264>: pop %rbp
0x00000000004011fd <+265>: pop %r12
0x00000000004011ff <+267>: pop %r13
0x0000000000401201 <+269>: pop %r14
0x0000000000401203 <+271>: retq
End of assembler dump.
我们可以看到其中有call read_six_numbers
函数,所以知道这个的解一定是输入包含6个数字的字符串。
分析开始:
当程序运行到+23行的时候,查看当前寄存器状态(如下),可以看出 r13和rsp指向相同,执行到+24的时候,r14也指向rsp所指向的地址0x7fffffffdf90
。
(gdb) i registers
rax 0x6 6
rbx 0x0 0
rcx 0x7fffffffdf80 140737488347008
rdx 0x7fffffffdfa4 140737488347044
rsi 0x7fffffffd970 140737488345456
rdi 0x1999999999999999 1844674407370955161
rbp 0x0 0x0
rsp 0x7fffffffdf90 0x7fffffffdf90
r8 0x7ffff7dd6060 140737351868512
r9 0x0 0
r10 0x6 6
r11 0x0 0
r12 0x400c90 4197520
r13 0x7fffffffdf90 140737488347024
r14 0x0 0
r15 0x0 0
rip 0x40110b 0x40110b <phase_6+23>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
而我们可以查看%rsp
的状态:
(gdb) x/10 $rsp
0x7fffffffdf90: 1 2 3 4
0x7fffffffdfa0: 5 6 0 0
可以看到%rsp
指向的数据是1,间隔0x4指向下一个数据。
总体来看前面的代码,+65位置到+87是一个小循环, +32到+93是一个大循环,要弄清楚循环在做什么,就必须知道寄存器说存储的值以及作用。
在+65到+87中,rax=r12d的值,而r12d在+52的位置,也就是大循环位置内,小循环外,每次执行就会自增,r12d在大循环内是从1开始每次大循环r12d++
,所以小循环内执行的内容是%rsp+0x4*rax
即遍历6个变量与%rbp
比较是否相等,那我们就要看看%rbp
是什么了,%rbp
在+32位置,大循环内,被%r13
初始化,第一次大循环的时候,%r13=%rsp
,所以%rbp
是第一个元素。
所以小循环结束后,第一次小循环是判断后面5个是否与相等,结束后%r13+0x4
于是第二次大循环的时候,再执行小循环,就是第二个元素与后面三个元素是否相同,所以以此类推,可以得出这个小循环是用于判断输入的数字是否有重复的,重复则expload!
然后我们再来看大循环,大循环跳出去的关键在于+60位置的跳转语句,我们来看看+60跳出的条件以及运行到那的顺序,首先要从+42位置判断,即判断%eax==6
,而%eax==%r13
,%r13
相当于大循环中依次遍历6个元素的变量 ,所以我们可以判断出,这个大循环的作用是,依次判断每个元素的值是否小于等于6,且执行小循环检查是否有重复元素。
===========================光分析这前几条汇编就花了好多时间T_T===================
按照上面的分析,我们可以看出+100位置到+121位置是一个循环,+143位置到+169位置是一个循环,+130到+181是个大循环,+201到+220是一个循环,+235到+257又是一个循环!!!上面说的循环,只是可能会造成循环,也可能是一个只执行一次的跳转语句。
这最后一关,很痛苦!一个个分析吧。
+100到+121处,在+100处,%rax
被初始化为"r14"
(地址存放着第一个元素),之后给%ecx
赋值为0x7
,之后%rdx=%rdx-%rax
相当于%rdx=7-%rax
,而%rax=%rax+0x4
,一直到+114位置,我们可以总结为假设6个元素存放在ary[6]
中,以上的语句经过执行第一次循环后变成ary[i]=7-ary[i]
。
之后就到了+163到mov %rsp,%rsi,1),%ecx
,执行+163后%ecx
值为第一个元素,然后cmp $0x1,%ecx
,如果%ecx
小于等于1,则跳转至+143。
这时候我们就要注意了,在+143的位置指令为mov $0x6032d0,%edx
,而经过查询。(因为查看后发现是node1,所以猜测是一种结构体,且会给我们创建出6个节点符合上面检测6个变量的要求)。
(gdb) x/24 0x6032d0
0x6032d0 <node1>: 332 1 6304480 0
0x6032e0 <node2>: 168 2 6304496 0
0x6032f0 <node3>: 924 3 6304512 0
0x603300 <node4>: 691 4 6304528 0
0x603310 <node5>: 477 5 6304544 0
0x603320 <node6>: 443 6 0 0
可以看到每个节点应该有3个成员组成,第一个位置应该什么数值,第二个是应该是序号,第三个是指针地址,转换成十六进制就会发现,该链表的顺序为1->2->3->4->6,这应该就是默认方式,我们就分析出来了这个结构体大概如下:
struct node{
int val;
int id;
node *next;
};
所以查找下一个节点就可以通过首节点地址0x6032d0+0X8就可以访问到指针了,也就是下一个节点。
所以该段代码大概意思可能猜出来做什么,接下来我们就要看目的,因为汇编代码太难看难懂了,所以我就加了一个断点到这里:
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx)
0x00000000004011da <+230>: mov $0x5,%ebp
0x00000000004011df <+235>: mov 0x8(%rbx),%rax
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx)
=> 0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250>
+243处于+235到+257位置,这里面的循环就是遍历每一个节点,判断前一个节点的值是否大于后一个节点,否则就出发expload程序,所以,我们也大概明白了,这是要我们给节点设置顺序链接,判断是否是数值是降序链接。
下面是我最先看程序汇编前半部分,输入 1 2 3 4 5 6
字符的结果,那为什么是6->5->4->3->2->1呢?这就得看前面我们+100位置到+121位置的分析,因为我们输入的字符在那被处理成了,7-1 7-2 7-3 7-4 7-5 7-6,也可以查找%rsp可以看到值的情况:
十六进制版本节点情况:
(gdb) x/24wx 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x00000000 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032d0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x006032e0 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x006032f0 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603300 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00603310 0x00000000
此时%rsp的值:
(gdb) x/6 $rsp
0x7fffffffdf90: 0x00000006 0x00000005 0x00000004 0x00000003
0x7fffffffdfa0: 0x00000002 0x00000001
所以这关卡的解就是: 3 4 5 6 1 2
,当然这不是我们要输入的数据,这是转换后的解。而我们要输入的数据就是4 3 2 1 6 5
。
至此,两天通了6关,耗费了很多时间,没有去学习接下来的章节,所以隐藏关卡就暂时不考虑啦!