实验简介
Bomb LAB 目的是熟悉汇编。
一共有7关,六个常规关卡和一个隐藏关卡,每次我们需要输入正确的拆弹密码才能进入下一关,而具体的拆弹密码藏在汇编代码中。实验中的bomb
实际上是一个程序的二进制文件,该程序由一系列phase组成,每个phase需要我们输入一个字符串,然后该程序会进行校验,如果输入的字符串不满足拆弹要求,那么就会打印BOOM!!!
完成整个实验的思路是通过objdump
对bomb
进行反编译(objdump -d bomb > bomb.txt
),获取所有的汇编代码。提取每个阶段对应的代码并借助gdb
进行分析,逐一拆弹。
Github地址:Bomb Lab
准备
汇编复习
类型 | 语法 | 例子 | 备注 |
---|---|---|---|
常量 | 符号$ 开头 |
$-42 , $0x15213 |
一定要注意十进制还是十六进制 |
寄存器 | 符号 % 开头 |
%esi , %rax |
可能存的是值或者地址 |
内存地址 | 括号括起来 | (%rbx) , 0x1c(%rax) , 0x4(%rcx, %rdi, 0x1) |
括号实际上是去寻址的意思 |
一些汇编语句与实际命令的转换:
指令 | 效果 |
---|---|
mov %rbx, %rdx |
rdx = rbx |
add (%rdx), %r8 |
r8 += value at rdx |
mul $3, %r8 |
r8 *= 3 |
sub $1, %r8 |
r8-- |
lea (%rdx, %rbx, 2), %rdx |
rdx = rdx + rbx*2 |
比较与跳转是拆弹的关键,基本所有的字符判断就是通过比较来实现的,比方说 cmp b,a
会计算 a-b
的值,test b, a
会计算 a&b
,注意运算符的顺序。例如
cmpl %r9, %r10
jg 8675309
等同于 if %r10 > %r9, jump to 8675309
各种不同的跳转:
指令 | 效果 | 指令 | 效果 |
---|---|---|---|
jmp | Always jump | ja | Jump if above(unsigned >) |
je/jz | Jump if eq / zero | jae | Jump if above / equal |
jne/jnz | Jump if !eq / !zero | jb | Jump if below(unsigned <) |
jg | Jump if greater | jbe | Jump if below / equal |
jge | Jump if greater / eq | js | Jump if sign bits is 1(neg) |
jl | Jump if less | jns | Jump if sign bit is 0 (pos) |
jle | Jump if less / eq | x | x |
举几个例子
cmp $0x15213, %r12
jge deadbeef
若 %r12 >= 0x15213
,则跳转到 0xdeadeef
cmp %rax, %rdi
jae 15213b
如果 %rdi
的无符号值大于等于 %rax
,则跳转到 0x15213b
test %r8, %r8
jnz (%rsi)
如果 %r8 & %r8
不为零,那么跳转到 %rsi
存着的地址中。
x86-64
寄存器规则: 默认函数的第一个参数是%rdi
第二个参数%rsi
第三个参数%rdx
反汇编
# 检查符号表
# 然后可以寻找跟 bomb 有关的内容
objdump -t bomb | less
# 反编译
# 搜索 explode_bomb
objdump -d bomb > bomb.txt
# 显示所有字符
strings bomb | less
GDB
gdb bomb
help # 获取帮助
break explode_bomb # 设置断点
break phase_1
run # 开始运行
disas # 反汇编
info registers # 查看寄存器内容
print $rsp # 打印指定寄存器
stepi # (单步跟踪进入)执行一行代码,如果函数调用,则进入该函数 可以使用s简化
n # (单步跟踪) 执行一行代码,如果函数调用,则一并执行
x/4wd $rsp # 检查寄存器或某个地址,查看内存地址里面的内容(常用)
用 ctl+c
可以退出,每次进入都要设置断点(保险起见),炸弹会用 sscanf
来读取字符串,了解清楚到底需要输入什么。
Phase 1
Dump of assembler code for function phase_1:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq
先 gdb bomb
,然后设置断点 break explode_bomb
和 break phase_1
这段代码还是挺好理解的,保存Stack pointer,将$0x402400
传给%esi
,调用位于0x401338
的strings_not_equal
函数,比较%eax
是否为0,不为零则调用explode_bomb
函数,为零则返回设置断点 phase_1
和explode_bomb
,输入命令r
运行会在断点处停下,此时随便输入一个字符串用于测试“abcd”
,然后disas
查看反汇编代码:=>箭号为当前运行的位置
查看寄存器内容 info register
,eax
就是rax
的低位用print $eax
打印出来,是一个地址用x/s $eax
,查看出地址里的内容,发现是输入字符串
用stepi
逐步执行,执行完 mov
之后,把地址中的内容传到%esi
中,用print
查看!得到字符串,这就是第一关的答案。退出后新建一个文本 touch sol.txt
,方便之后输入
Phase 2
这次我们有了第一关的答案,进入gdb后设置好断点,和命令参数。
试运行,在phase_1
停住,然后continue
,答案正确,触发 phase_2
的断点,这次输入abc
反汇编Phase2部分的代码
(gdb) disas
Dump of assembler code for function phase_2:
=> 0x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers> #读取6个数字
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) #第一个数字和1比较,不相等则爆炸
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52> #相等,跳转到<52>
0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb>
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax #将(%rbx)前一个数字存到%eax
0x0000000000400f1a <+30>: add %eax,%eax #%eax数字加倍
0x0000000000400f1c <+32>: cmp %eax,(%rbx) #%eax和(%rbx)比较,=(%rbx)则跳过爆炸
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx #(%rbx)地址+4,下一个数字
0x0000000000400f29 <+45>: cmp %rbp,%rbx #比较%rbp和%rbx,循环是否结束
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx #指向第2个数字,%rbx保存第2个数字地址
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp #0x18 = 0x0 + 4 bit * 6 个数字
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq
End of assembler dump.
根据Phase1,很敏感的会发现movl $0x4025c3, %esi
这行。通过之前一样的方法,得到0x4025c3
内存里的字符串
(gdb) x/s $esi
0x4025c3: "%d %d %d %d %d %d
再根据bomb[0x40148a] <+46>: callq 0x400bf0 ; symbol stub for: __isoc99_sscanf
这句,猜一下,立马就能联想到scanf("%d %d %d %d %d %d",a,b,c,d,e,f);
,也就是说,输入的格式已经确定了。
解读出循环中,从1开始,是一个等比数列,公比为2。1 2 4 8 16 32