花了一段时间做完了暑假作业, 二进制炸弹破解的过程可谓苦尽甘来
现在把自己写的解析报告放上来, 纯原创, 没有参考任何关于此炸弹的解析
可能会与你拿到的炸弹有所不同, 本文只提供思路
第一关 字符串比较
1. 通过read_line从标准输入流输入测试字符串例如: 1234567
2. 进入phase_1 函数
3. 进入strings_not_equal 函数
4. 进入第一个string_length函数
5. 根据string_length函数返回值 %eax = 7 推断第二个string_length函数计算的是标准字符串(密码)的长度
6. 进入第二个stirng_length函数, 根据add 与 jne 指令判断该函数通过遍历字符数组到’\0’来计算长度, 易知 %ebx 存储该数组的首地址
7. 遍历完成后得到 %eax 值为 41, 表明密码字符串长度为41,
8. restart 程序并执行到第6步
9. 运用 print $ebx 得到起始地址 134521080
10. 通过 x/41cb (x查看内存值 41表明单位个数c以字符形式显示b表明每次取一个字节长度) 指令 得到密码字符串的内容
第一关密钥的内容第一关密钥:
For NASA, space is still a high priority.
第二关 循环
1. 进入read_line函数, 随便输入几个字符 "asdasfa"
2. 进入, read_six_numbers函数
3. 按s继续执行,进入sscanf函数
4. 继续按s可以看到sscanf函数的定义, 如下
第二关sscanf函数的定义5. 可知应该输入6个数字, 每个数字用空格隔开
6. 重新进入read_line, 按n后输入数字1 2 3 4 5 6
7. 继续执行到sscanf后, 易知0x4(%esp)取输入的第一个数字, 即1, 并与1比较,相等即继续执行
8. 通过观察寄存器值的变化, 可以判断出phase_2 + 54 到 phase_2 + 73为循环过程, 遍历我们刚刚输入的6个数, 整理循环的运行逻辑如下, 以a1 – a6 表示他们.
start = a1;
x = a2;
if (start == 1)
do {
start += start;
if ( start != x );
BOOM!!!;
x = a.next;
} while ( x != a6 )
else BOOM!!!!;
9. 根据上述算法依次类推, 即可得到应该输入的密码为 1 2 4 8 16 32.
第二关密钥执行结果第二关密钥:
1 2 4 8 16 32
第三关 条件/分支
这一关通得很快, 因为有多解, 碰巧凑了一个, 感觉运气成分比较大 (逃
1. 同上, 进入read_line函数后随便输入一串字符 "hahaha"
2. si 进入sscanf函数, 再按一次s可以调出sscanf函数原型, 如下
第三关sscanf函数原型3. 可知这关密码的格式是 整数空格字符空格整数
4. 退出重新进入, 输入测试字符串1<空格>a<空格>2
( 体现运气的时刻到了)
5. 通过 cmpl $0x7, 0x4(%esp) <换行> ja blabla 语句可知, 第一个数字需要小于7
6. 通过 jmp *0x804a160 (, %eax, 4) 语句可知跳转目的语句与第一个数字的值有关
7. 猜测一共有7种破解密码, 我这里遇到的是第2种(第一个数字为1)
8. 跳转到phase_3+113, 将第二个整数与0xbe (即190 ) 作比较, 相等即可继续进行, 由此推断 第二个整数是0xbe, 即190
9. 观察跳转到phase_3+323后需要将%al中的值与字符参数做比较相等才可以完成密码匹配
10. 通过 print $al 查看寄存器中的值, 发现为113, 查ASCII码表, 发现为字符’q’
那么, 1 q 190即为解之一
第三关密钥执行结果第三关密钥 ( 之一)
1 q 190
第四关 递归调用和栈
1. 同样, 输入测试字符进入sscanf函数, 输入s指令观察到sscanf函数的原型
第四关sscanf函数原型2. 重新进入, 输入测试字符1 2
3. 根据phase_4+78的 cmp $0x5, %eax 语句以及 cmpl $0x5, 0x8(%esp) 语句, 可以推断出以第一个数字为参数的fun4函数返回值应该为5, 第二个数字的值应该为5
4. 进入fun函数,观察%eax, %ebx, %esi, %ecx的变化
5. 经过初步推断, %ecx存储函数参数值, %eax为返回值, %ebx与%esi分别存储函数中间值, 并赋初始值分别为0和14, %edx作为函数比较的中间值, 其作为中转站与%eax性质相同
6. 通过分析比较代码, 可以大致推断出原函数的算法如下, 其中以origin作为%esi存储的值, 以sub作为%ebx的值, 以center作为%eax和%edx所存储的值, x为函数参数, 也就是我们输入的第一个数字
origin = 14;
center = origin;
sub = 0;
fun4 ( int x ) {
center = ( origin – sub ) / 2 + sub; //函数的开头计算用于比较的center值( %eax )
if ( x == center ) return 0; //对应汇编代码fun4 + 0 ---- fun4 + 33
else if ( x < center ) {
center -= 1; //汇编代码fun4+ 40
origin = center; //汇编代码push%edx
return 2 * fun ( x ) ; //汇编代码 fun4 +54 add指令
}
else if ( x > center ) {
center += 1; //汇编代码fun4+ 71
sub = center; //汇编代码push %edx
return 2 * fun ( x ) + 1; //汇编代码fun4 + 84
}
}
7. 根据上述代码, 反向递推, 令最后返回的%eax值为5
5 = 2 * 2 * ( 2 * 0 + 1 ) + 1, 只有10可以满足左侧递推式
第四关密钥执行结果第四关密钥
10 5
第五关 指针
1. 进入read_line函数后进入phase5中的sscanf函数, 按下s得到sscanf的函数原型
第五关sscanf函数原型2. 可知本关依然需要两个整数作为密钥
3. 重新进入, 输入测试数据1和2并观察函数执行情况
4. 根据下方的 mov <地址>(, %eax, 4 ) 可以推断出输入的第一个整数作为地址的偏移量
即整形数组中的下标
5. 利用 gdb x/70cb <地址> 指令查看程序内部提供整形数组的所有值, 并记录为表格如下
查看内存中数组的存储情况 数组下标与元素对应情况6. 根据后续的 jne $0xf 以及对起计数作用的%edx中的值可知, 该程序进行15次循环后使得%eax的值为15, 即通过15次对地址的重新计算得到最终值15, 下标为5, 按此逻辑写出该程序段的大致算法如下:
result = input ( 第一个参数);
counter = 0;
sum = 0;
do{
counter ++;
result= array [ result ];
sum+= result;
while ( result != 15 && counter != 15 );
7. 按照上述算法, 从15开始递推 15次, 得到第一次result的值, 也就是我们需要输入的一个整数值为5.
15 6 14 2 1 10 0 8 4 9 13 11 7 3 12 ➡ array [ 5 ] = 12
8. 又根据 cmp 0x8 ( %esp ), %ecx 可知, 第二个参数需要与sum值相同, 即所有遍历过的值的和, 15 + 6 + ……+ 12 = 115
第五关密钥执行情况第五关密钥
5 115
第六关 链表/指针/结构
1. 进入read_line函数, 根据phase_6 + 26的read_six_numbers函数, 根据第二关已知, 需要输入6个数字.
2. 重新进入函数, 并输入测试数字组1 2 3 4 5 6
3. 进入phase函数并跳过read_six_numbers函数, 就来到了本关的第一个循环判断, 如下
第六关第一个坎以array数组表示用户输入的六个数字,该部分代码可用以下算法来描述
for ( int I = 0; I < = 5 ; I ++ ) {
if ( array [ I ] <= 6 ) {
for ( int m = I + 1; m <=5; m ++ )
if( array [ I ] == array [ m ] )
BOOM!!!;
}
else BOOM!!!;
}
根据该段代码可知用户输入的每个数字都应该小于6并且各数字互不相等
4. phase_6+104 ~ phase_6+115是7减去原来每个数后重新存入源地址中, 算法不赘述, 即phase[x] = 7 – phase [7]
5. phase_6+124 ~ phase_6+167是通过计算好的新数组, 将一个六结点链表初始地址每次移动(array[x] – 1) 个8字节后得到值依次保存于栈帧中. 原理可用下述公式表示, 在这里观察到 0x8(%edx) 直接就是移动到下一个结点的首地址了, 而每个结点所占空间是12个字节, 推翻上述分析, 移动8个地址后即是下一个结点首地址,从而构成单向链表.
即此部分是取第array [ x ] 个结点值并依次保存
第六关第二个坎6. 通过 x/18dw 指令观察链表六个结点首元素的值:
链表node1 - 6从上图可以看出:
链表node1 - 67. 下述代码作为此关的终极武器, 经过寄存器值的查看即跳转条件分析, 只有满足存入栈帧的结点降序排列才可躲避phase_6+219的爆炸函数
第六关第三个坎8. 手动排序, node6 > node4 > node1> node5 > node2 > node3
整理得最终结果与输入值的对应关系如下
输入值的变化过程 第六关密钥执行结果第六关密钥:
1 3 6 2 5 4
隐藏关卡
1. 进入phase4的defused函数, 观察到一条指令
cmpl $0x6, 0x804c3cc
使用x指令查看地址0x804c3cc中的值, 发现这个值记录的是输入字符串的个数
也就是在输入6个字符串, 即通过6个关卡后才可以跳转到下方的sscanf函数附近
2. 猜测defused函数中的sscanf函数记录着有关隐藏关的密码
3. 继续进行到第六关, 重新进入phase_defused函数, 进入sscanf函数使用 s 指令查看到
有关隐藏关密钥的格式, 即我们需要在第四关密钥后加一个字符串.
隐藏关sscanf函数原型4. 重新开始, 第四关密钥处输入测试字符串 "10 5 haha" , 进入到第六关后的pahse_defused函数, 进入其中的strings_not_equal函数.
5. 按照第一关流程, 直接进入第二个string_length函数, 使用
x/8cb $edx
指令查看%s, 即第四关密钥后需要输入什么字符串
进入隐藏关的密钥据此写出%s的内容: DrEvil.
看起来像一个人名, 猜测这个彩蛋密钥应该算类似“炸弹制作者名单”
6. 重新进入该phase_defused函数, 进行到puts函数, 发现了进入隐藏关的提示语
已进入隐藏关不慌, 继续执行进入secret_phase函数
7. read_line函数, 输入一行字符串, "lstnb"
8. 进入到strtol函数, 该函数是将字符串转化为数字, base参数表明进制, 查看原型:
隐藏关strtol函数原型base = 10, 由此可知将输入数字转化为十进制整数
9. 根据 cmp $0x3e8, %eax / jbe ~~
可知输入一个小于等于1000的数字可以避免爆炸
10. 重新开始, 输入1000, 进入fun7函数, 发现fun7又是一个递归函数……$%!&^%!*#&^%
fun7函数11. 经过一定观察, 发现fun7函数比第四关的fun4函数简单了不少, 长舒一口气
整理得到fun7函数所描述的算法如下
middle = array [ 0 ]; //array首地址存储在%edx中, middle存储在middle中
fun7 ( input ) { //input存储在%ecx中
if( input == middle ) return 0;
else if ( input < middle) middle = *( array + 1 ) return 2 * fun ( input ) ;
else middle = *(array + 2)return 2 * fun ( input ) + 1;
}
12. 根据secretphase函数中的 cmp 0x2 %eax 指令, 可知fun7返回值应为2
2 = 2 * ( 2 * 0 + 1) 进行两次递归调用, 利用x指令查看array中的部分值
数组array根据递推关系36→ 8 →22 可知需要用户输入的值为22
隐藏关密钥:
22
网友评论