Phase_1
汇编代码
思路分析
- 由此两行可知程序将一个地址放入了%esi并调用了 strings_not_equal 函数,应该是将输入的内容与 %esi 中地址存储的内容比较,若不相等则爆炸。
- 用examine命令查看地址中的内容
- 得到一个字符串应该就是拆弹密码。
- 退出gdb并验证
- 拆弹成功。
Phase_2
汇编代码
思路分析
-
一开始先保护寄存器值,然后将栈的长度增加0x28用于存储。
-
之后通过callq调用read_six_number函数,应该是用于读取6个数字,之前增长的栈空间应该是用来存这个有6个元素的数组,而数组首地址被放在了%rsi。
-
用gdb获取read_siz_number函数的代码
-
可以得知read_six_number函数是通过sscanf函数得到输入,由
可以得知sscanf所需要的格式化字符串所在的地址,查看得
是六个有符号整数。 -
设这个数组为a[6],sscanf还需要6参数即数组中每个元素的地址。
-
根据调用规定,前4个元素的地址应该用寄存器%rdx,%rcx,%r8,%r9来传递,后2个通过栈来传递。在代码中对应寻找可以看到:
&a[0]
&a[1]
&a[2] 和 &a[3]
&a[4]
&a[5]
-
回到phase_2函数
我们已经知道%rsp里存放着数组首元素,所以此处将数组第一个元素和1比较,不同就爆炸,相同就跳转到0x400f30。 -
这里使一开始就压入栈内的两个寄存器指向a[1]和a[6](a[5]后的一个int大小位置),由之后的跳转指令猜测是为了循环准备。
-
跳转到0x400f17进入循环体:
mov -0x4(%rbx),%eax
即为使%eax指向%rbx所代表项的前一项,之后将前一项乘以2,再与当前项比较,不同就引爆炸弹,相同则%rbx指向下一个元素,直到末尾(此时a[6]的作用就显现了)。 -
已知数组第一个元素一定是1,得到拆弹密码:1 2 4 8 16 32
-
退出gdb并检验:
成功。
Phase_3
汇编代码
思路分析
-
首先从寄存器%esi中取出sscanf的格式化字符串
得到输入类型是2个有符号整数,定为n1,n2。之后判断sscanf的返回值(即读取字符个数),小于1则引爆炸弹。 -
接着判断n1,大于7则跳转到0x400fad引爆炸弹。
-
这里是拆弹核心重点,由此我们得到一张跳转表
n1的范围为0~7,写出伪代码
switch (n1)
case 0: goto 0x0000000000400f7c if(n2!=0xcf) boom
case 1: goto 0x0000000000400fb9 if(n2!=0x137) boom
case 2: goto 0x0000000000400f83 if(n2!=0x2c3) boom
case 3: goto 0x0000000000400f8a if(n2!=0x100) boom
case 4: goto 0x0000000000400f91 if(n2!=0x185) boom
case 5: goto 0x0000000000400f98 if(n2!=0xce) boom
case 6: goto 0x0000000000400f9f if(n2!=0x2aa) boom
case 7: goto 0x0000000000400fa6 if(n2!=0x147) boom
- 拆弹密码即为:
(0,207)、(1,311)、(2,707)、(3,256)、(4,389)、(5,206)、(6,682)、(7,327) - 退出gdb并检验:
成功。
Phase_4
汇编代码
思路分析
-
首先通过明码地址分析输入的类型
两个有符号整数,仍设为n1(0x8(%rsp)
),n2(0xc(%rsp)
)。 -
分析代码可以看到
即输入字符个数不为2,炸弹爆炸,n1大于14,炸弹爆炸。当n1小于等于14时跳转到0x4013a。 -
再通过观察代码可知
若n2不为0,炸弹爆炸,因此n2一定为0。在0x4013a,将传入func4的两个参数设置为n1,14和0。 -
下面分析func4
-
由func4内部调用了func4可知,这是一个递归。
-
为了直观,定义%edi为a,%esi为b,%edx为c,%eax为x,%ecx为y。
x=c-b
x=(x>>31+x)>>1
y=(x>>31+x) >> 1 + b
比较a和y,如小于等于就跳转到0x400ff2,%rax设置为0,否则改变参数重新调用func4。
此处再比较,若大于等于(即等于)则跳转去0x401007,函数结尾,否则改变参数重新调用func4。 -
可以逆向得到如下C语言代码
-
回到phase_4函数中分析剩余代码
可知当func4的返回值为0时拆弹成功,否则炸弹引爆。 -
通过编写如下程序可得到答案
得到输出结果为
则拆弹密码为(0,0)、(1,0)、(3,0)、(7,0) -
退出gdb,任取一组密钥测试
成功。
Phase_5
汇编代码
思路分析
-
此处表明密钥一定是一个长度为6的字符串,否则炸弹爆炸。
-
跳转到4010d2初
此处将%eax的值设置为0,再跳转到0x40108b。 -
0x40108b处有一个循环体,重点关注
最后一行的利用掩码,只取%edx字符数组的低4位。 -
利用刚才得到的字符低4位数值作为索引来访问一个首地址为0x4024b0的数组。利用gdb查看该数组内容
-
后面的内容明示前面那个单词“不能用复制来解决炸弹”,即不是密钥。
-
仍然是访问数组,数组首地址为%rdx+0x10(应该和上一个存在映射关系)。
-
循环结束之后把数组第(0x16-0x10)个元素置为0,可知该数组有6个元素。
-
在phase_1见过的strings_not_equal函数,在这里用来比较刚才的数组和储存在0x4025e处的数组。用gdb查看得到:
此时很明了,我们输入的6个字符的低4位作为索引去访问0x4024b0的数组,最后要得到flyers。 -
数组对照如下:
m a d u i e r s n f o t v b y l
1 2 3 4 5 6 7 8 9 a b c d e f g
即下标值为9,15,14,5,6,7,转换为16进制即为0x09,0x0f,0x0e,0x05,0x06,0x07。- 低4位为0x09的字符有:’)’、‘9’、‘I’、‘Y’、‘i’、‘y’
- 低4位为0x0f的字符有:/’、‘?’、‘O’、‘_’、‘o’
- 低4位为0x0e的字符有:.’、‘>’、‘N’、‘^’、n‘、‘~’
- 低4位为0x05的字符有:’%’、‘5’、‘E’、‘U’、‘e’、‘u‘
- 低4为为0x06的字符有:‘&’、‘6’、‘F’、‘V’、‘f’、‘v’
- 低4为为0x07的字符有:‘’’、‘7’、‘G’、‘W’、‘g’、‘w’
-
以上六种中每种任选一个组合即为拆弹密钥。
-
退出gdb,任取一组密钥测试
Phase_6
汇编代码
思路分析
-
将五个寄存器入栈,然后开辟栈空间,0x50字节不小。
-
熟悉的read_six_numbers函数,断定在此处定义了一个数组a[6],数组首地址就是当前栈顶(%rsp)
-
取出%rbp指向的数组元素再与6比较,大于6则炸弹爆炸,否则跳转到0x41128。
-
此处%r12d加1,它在程序开头被压入栈,可知它是一个计数器,然后判断它是否等于6,若相等则跳转至0x401153,此时跳出了循环。
-
若小于,则将%r12d的值赋给%ebx,之后又是一个循环,这个循环的将后续的数组元素和a[%r12d]比较,若相等则爆炸,即数组中各元素大小不相等。
之后把%ebx加一,并比较和5的大小关系,小于等于则跳转,继续循环(内循环)。
这个双层循环我们得知数组a的6个元素值小于等于6,且彼此不相等。 -
也是之前见过的形式,%rax指向首地址,%rsi指向尾地址后面的区块,这是一个循环,在循环体中用7减去数组a的每个元素。
-
明显看到这段代码仍然是一个二层循环。仔细发现里面有一个基址为%rsp+0x20,步长为8的内存区域。
-
还发现一个明码地址0x6032d0,调用gdb查看其中内容
果然是链表,此时猜测基址为%rsp+0x20,步长为8的内存区域是一个数组,元素类型为struct Node*,定为nodes[6]。 -
这里先是访问链表首结点entry,后访问next。
-
这里有一个循环,作用是给nodes数组赋值。将数组a的值和链表节点顺序对应起来,即a[%rsi]的值为nodes[%rsi]指向的Node节点的顺序
-
还是一个循环,它从nodes[0]开始遍历链表,并且每个结点的entry值是递增的。
通过回顾之前的链表结点信息,可以得到此时的数组a的值为3 4 5 6 1 2。
从前面的代码我们已经得知数组a的6个元素就是我们的输入,并且原数组减去7才得到新的数组。
于是得到原始数组,即拆弹密钥为4 3 2 1 6 5 -
退出gdb,测试得
成功!
Secret_phase
汇编代码
思路分析
-
隐藏关是因为在浏览汇编代码文本的时候发现的
-
首先来探索进入条件
-
再次查看文本,发现第六关结束后会执行phase_defused的函数,即关卡结束。
-
调用gdb查看内容:
-
里面有很多明码地址,一 一查看。
第一个是sscanf函数所需的格式化字符串,表示需要输入两个整型和一个字符串,之前的关卡只有第四关和第三关的密钥是两个整型,所以猜测应该是在密钥后再输入字符串“DrEvil”以进入隐藏关卡。 -
检验得知猜测成立。
-
分析secret_phase的汇编代码
-
调用read_line读取我们输入的一行内容,返回值为%rax。之后给寄存器赋值作为传入strtol函数,看名字猜测这个函数将我们输入的内容转换长整型数。
-
判断(输入的值-1)和0x3e8(即1000)的大小关系,小于等于则跳转,否则炸弹爆炸。
-
更新了两个寄存器的值作为参数传入fun7,第一个是我们输入的字符n2,另一个n1保存在一个明码地址里。
-
调用gdb查看内容
-
查看fun7的汇编代码
-
首先对%rdi做一个判断,等于0则直接返回-1。
-
不等于0就将%rdi的值放入%edx,再将其与我们输入的数字进行比较,若小于等于则跳转到0x401220,将%eax置为0,再做一次相同的比较,若相等则跳转至0x40123d,函数返回0。
-
若第二次比较不相等(即输入的数小于%edx),则继续执行,取%rdi中存放的数据结构里的下一个位(步长为16),
-
之前猜想它只是一个0x24看来太天真,重新查看,得到
-
查询资料得知这是一个二叉树结构,每个节点第1个8字节存放数据,第2个8字节存放左子树地址,第3个8字节存放右子树位置。
-
将%rdi移到右子树位置,再调用fun7,返回后令
%eax=2*%rax+1
。
-
若第一次比较大于,则令%rdi移到左子树位置,再调用fun7,返回后令
%eax=2* %eax
。下面跳至返回处。
-
综上,edi指向一个树的节点,令edi节点的值与我们读入的值进行比较。
- 如果两者相等:返回0
- 如果前者大于后者:%rdi移至左子树,返回
2* %rax
- 如果后者大于前者:%rdi移至右子树,返回
2* %rax+1
-
返回到secret_phase函数
-
fun7的返回值必须等于2,不然爆炸。
-
那么我们需要返回2,应该在最后一次调用返回0,倒数第二次调用返回
2*%rax+1
,第一次调用返2*%rax
。换句话说,这个数应该在第三层,比父节点大且比根结节小。观察二叉树,唯一的拆弹密钥是:0x16(22)。 -
退出gdb,验证得
成功!
小结
这个实验耗时很长,但是很有意思,在第三章学的内容都有体现,比如各个存储器的具体作用,各种栈操作和寄存器操作,条件控制和条件传送实现的条件分支,数据对齐等等都有所体现。