本来没想写个博客,结果得知还要验收,发现自己全忘了,那就趁着复习的功夫再捋一遍吧>-<
一、使用工具:IDA-pro
简单使用方式:
1、打开IDA,open需要反汇编的exe。
选则win32debugger,process option可以传入命令行参数(该实验中为学号)。
2、F9会运行exe,直到断点停止。F8 step over, F7 step into; F5显示C代码(32bit的IDA有,64位没有)
3、展示出的汇编指令为x86,采用Inter格式 ,no ATT,通常目的操作数在前(与课内正好相反)。
4、空格键使得代码在纯文本<->graph之间切换。graph便于查看分支,跳转的目的地。
F9后,右侧可查看寄存器与栈的地址与对应的值,地址为32位,用16进制表示。下方有存储器不过不常用。
5、用IDA 运行exe,在exit后,运行框直接消失;
所有答案弄完后,用命令行会方便些,便于查看所有结果。
win+r, cmd, cd 到指定文件夹,bomb.exe 001044;
或者用powershell ,在文件夹中shift+右键运行powershell .\bomb.exe 001044
二、解题思路
首先找正确通关条件。找到后向上推,寻找关键的控制指令,满足xxx条件才可正确跳转。
当然指令不一定完全按graph的顺序执行,有时按图中的路径无法运行到想要的代码,这时需要强行修改pc使跳转到正确通关的位置。
理论上暂时想到强行修改pc的方式有以下几种:
ret: pc=stack[esp--]
jmp: pc=JTA
mov eip xxx
三、流程
首先是一些关于读入的函数:(仅限于大意,不求甚解,不影响流程推进即可)
readline:把输入的一行当成字符串读到起始目标地址。
sscanf: 把readline得到的字符串转化为数字,返回数字个数
二进制炸弹:
第一关: 字符串比较
进入phase1,由explode_bomb向上推 ,-> eax==0 -> al==0 ,然后就不太好懂,必须开始推。
观察generaterandomstring函数:
int __cdecl GenerateRandomString(int a1)
{
signed int v1; // edi@1
int result; // eax@5
v1 = 0;
do
{
GenerateRandomNumber(2);
if ( rand_div == 1 )
*(_BYTE *)(v1 + a1) = 65;
else
*(_BYTE *)(v1 + a1) = 97;
GenerateRandomNumber(26);
result = v1 + a1;
*(_BYTE *)(v1++ + a1) += rand_div;
}
while ( v1 < 10 );
*(_BYTE *)(v1 + a1) = 0;
return result;
}
不然看出函数随机生成了一个长度为10的字符串,起始地址为a1,并将a1[10]置为0;
回到汇编中,结合栈结构,其实a1=ebp-8 ,则[ebp-8,ebp+1]存的就是随机生成的字符串。
由右方可知,ebx为起始地址存了输入的字符串。
接下来的逻辑:
起始时ecx+edx=生成字符串的起始地址(ebp-8),ecx为读入字符串的起始地址。
至于为何ecx为首地址:
push eax
call _read_line
pop eax
IDA的逻辑中,通常传参的方法是在call之前把参数push入栈。
我们的目的是 do [ecx]==[ecx+edx](while ecx++),直到[ecx+1]==0时达到目的。显然输入的字符串前10位要与生成的相同即可。 运行完randstring()后读取出生成的字符串即可。
通关密码:AxOlSPKPVo (后面可以随便追加字符)
第二关:寻找6个数满足的一定规则
本人的_rand_div为6,进入phase_2_6:
观察call read_six_number 上下文逻辑,得到:读入了长度为6的int类型数组,起始地址edx=ebp-0x18
依次关注以下的分支逻辑:
a[0]=_rand_div+1=4; 6个数都>0; rep(i,2,5)a[i]=a[i-1]*a[i-2]; a[2]+a[3]+a[4]+a[5]>4
综合上述条件,一组通关密码:4 1 4 4 16 64
第三关:
先看读入:
call _read_line
push eax ; Src
call _phase_3
_phase_3中:
push ebx
mov ebx, [esp+4+Src]
则ebx=_readline_line返回值
然后对于每个子阶段,都push ebx,即传入的参数。
本人randomnumber修改randdiv=4,进入phase3_4
3_4中,将输入的字符串变为了起始位置为ebp-4的int数组。
观察分支指令,输入的第一个整数应为rand(8)+0xDC
此后rand_div+6=第二个数
通关密码:223 83 (后面的输入无所谓)
第四关:
进入4_20。同上,容易得到ebp-4为读入int数组的首地址。
观察分支,要求返回值为1,那么只能输入一个数!
接下来两个分支要求输入的一个数(记为temp)>1 且 >3E8h,那不就是>3E8h吗,这个>1属于没看懂有啥意义。
接着观察分支,要求[ebp+eax*4+var_20]==ebx。 跑一遍得到左边==720。而ebx=func4_2([ebp-4])。 结合F5容易看出func4_2(x)=x!。
然后就是让我百思不得其解的一段指令:
mov edx, 10624DD3h
imul edx
sar edx, 6
mov eax, edx
shr eax, 1Fh
add eax, edx
F5才知道这段指令功能是eax/=1000,具体为啥一脸懵,堪比平方根倒数算法带给我的震撼程度。
那就不求甚解了。(temp/1000)!=720 , 则输入数字在[6000,7000)内即可.
第五关:
先输入Y。 再输入突防指令,字符串地址eax作为参数传入phase_impossible。
先来看phase_impossible调用的几个内置函数:
call __imp__GetTickCount@0 ; GetTickCount() ,返回从操作系统启动所经过的毫秒数,具体用处听老师说是检测程序运行时间不能过长,否则爆炸(不太明白,暂咕,回来补上)。不过好像对本关没啥限制。
call __imp__IsDebuggerPresent:函数会检测debugger是否正在工作,如在工作就炸。这意味着我们不能设断点动态调试?其实不然。
当指令运行到该函数上方,强行在此函数后一条指令set IP,跳过即可。(实际上要跳得更后,避开这个分支。)
按照之前找通关点的逻辑看到最后,也没发现如何正确通关,而是全部指向bomb。那就可能涉及到指令强行修改pc。观察_goto_buf_0,1,2,发现其作用均为 设置pc=eax; 而eax=ebp-0x100
_goto_buf_0,1,2分别对应rand()函数的返回值为0,1,2。而rand()参数为3,也就是说不论如何都会调用其中一个goto_buf。
现在要关注的是,ebp-0x100处是啥。观察_to_hex函数。 由函数名可猜测,这是把某个东西转化成16进制。函数有两个参数,ebx,eax. 分析可知,ebx为输入字符串的首地址,eax=ebp-0x100.
此时就可以盲猜结论了(不求甚解),_to_hex函数的作用是将输入的字符串转化为16进制机器码,存储于ebp-0x100处。而_goto_buf后会运行输入的这些指令。
我们的目标是,输入一串指令使得pc跳转到正确通关的地方。
先找到目标pc。在main函数的graph中找到通关部分的指令:
那么pc要跳到0x00401240.
同时为了维护下栈的平衡,要把esp初始为phase_impossible之前的状态。容易想到如下指令:
mov esp, ebp
pop ebp
push 0x00401240
ret
这还没做完。_to_hex下面还有函数_check_buf_valid()来控制分支。其传入的参数为_rand_div,ebp-0x100. F5后得到函数功能是:将从ebp-0x100开始的256个字节存储值异或起来,判断是否==_rand_div的低8位。
为使之相等,可得到上述指令的机器码,后面加上补偿异或值的对应数字即可。
ps:x86指令->机器码可参考网站: 链接
不过注意到这里的通关提示还有彩蛋,于是找彩蛋。在左侧的函数栏中成功找到_phase_secret。
指令修改为如下:
mov esp, ebp
pop ebp
push 0x00401240
push 0x004014F0
ret
update:不过这样还是没有维护栈平衡,似乎漏掉了call _phase_impossible的返回地址、、、
那就修改成如下,不返回0x00401240了,直接返回call _phase_impossible的返回地址。
mov esp, ebp
pop ebp
push 0x004014F0
ret
最终的第五关指令:89EC5D68F0144000C3F0
最终效果:
缓冲区炸弹:
首先与二进制炸弹不同,缓冲区的1-4关不是连续,一次程序跑完的,而是相互独立的,也就是说要跑四次,每次都有一个密码。具体原因可分析流程,发现关于读入的只有 main函数->test函数->getbuf函数,然后停止到getxs函数处理读入。
推进一遍流程,发现按照graph顺序跑一遍是找不到想要的木马的,那就必须要用到强行修改pc的方式。观察哪些指令有可能协助pc跳转。
一番搜寻发现唯一可疑的地方是getbuf函数的ret指令。那就先研究一下代码逻辑和栈结构,如下:
栈地址 | 内容 | 具体值 | 操作说明 | ||||
test返回地址 | |||||||
0019FEF8 | 原来的ebp | 此后ebp=此地址,记为esp0 | |||||
金丝雀 | 0DEADBEEF | ||||||
…… | |||||||
0019FEDC | 第二关=cookie | ||||||
getbuf返回地址 | |||||||
0019FED4 | esp0 | 此后ebp=此地址,记为esp1 | |||||
esp1-12 | 输入机器码的起始地址 | 第三关跳到这里 | |||||
esp1-12 | 作为参数传入getxs | ||||||
getxs返回地址 |
test函数到getbuf之前,大概实现了设置金丝雀,并调用chkstk函数(仔细研究发现其功能就是开辟栈空间,就是_alloca),不过chkstk对通关没啥影响。。
进入getbuf。如上方栈结构,记esp1=0x0019FED4. 然后令eax=esp1-0xC,并作为参数传入getxs。 随便输入几次字符后可以猜测出getxs的功能 : 处理输入的字符串,变为以eax为首地址的对应机器码。 红色标注的栈内容,是getbuf函数的返回地址。试想:如果我们输入的字符串覆盖掉了原先的返回地址,那么可以为所欲为了。
现在开始分析关卡。
第一关:
把红色的返回地址覆盖为0x004011E0即可。覆盖后有call exit,程序会自动退出。
密码: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E0 11 40 00 (前24字节随意)
由于小端寻址,13-16 字节要与实际值反过来。
第二关:
要求除了要强行把pc设置到这里来,还要使得[esp+8]==cookie。
先跑一遍得到cookie的值,然后分析esp值:
getbuf的ret指令之前,esp=0019FED4, 对应输入字符的 13-16 字节。ret后esp+=4, trojan 2中esp-=4; 则mov ebx,[esp+8]指令中的esp=0019FED4. 那么把输入字符的 21-24 位修改为cookie值即可。
通关密码:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 12 40 00 00 00 00 00 FE 65 C6 71
第三关:
容易看出通关要求global value==cookie. 那我们要做的就是通过插入指令修改global value。
我的做法是,使getbuf ret到修改global value的指令。 修改global value的指令完之后,要再紧跟修改pc的指令跳到trojan 3部分。这里我采用ret ,同时往栈中塞入trojan 3地址。
通关密码:B9 FE 65 C6 71 89 0D 04 90 40 00 C3 00 00 00 00 C8FE1900 50124000
首先将getbuf返回地址覆盖为 0019FEC8,也就是读入机器码的起始地址。 然后前12个字节代表的指令为:
mov ecx,0x71C665FE
mov [0x00409004],ecx
ret
注意:mov指令的目的操作数为存储器时,源操作数必须指定长度,所以直接:mov [0x00409004], 0x71C665FE 是错误指令。
ret后pc跳转到0x401250。
第四关:
与第三关唯一的不同是,第四关函数结尾不是exit, 而是ret。分析trojan 4栈结构,发现其函数是栈平衡的(即:call塞入的返回地址使esp-4 ,函数执行过程前后esp不变,ret使esp+4)。
这意味着,如果继续延用第三关密码,pc将ret到输入字符的21-24字节。
那我们把21-24字节改成想要的地址就好了。我的做法是设置21-24字节为exit()函数的地址。
通关密码:B9 FE 65 C6 71 89 0D 04 90 40 00 C3 00 00 00 00 C8FE1900 A0 12 40 00 70 15 40 00
ps: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E4FE1900 A0 12 40 00 70154000 B9 FE 65 C6 71 89 0D 04 90 40 00这也是个合理的通关密码,不过字节数长了。
第0关:
老师的要求是显示:“ 不错哦,缓冲区溢出成功,而且getbuf返回xxxxxxxx ”。
观察到这段话出现在test函数中,要求
(1)金丝雀不变。其实金丝雀那个地址的值一直都不变,输入字节没那么长到影响金丝雀的地步。但是我们观察指令是:cmp [ebp+var_4], 0DEADBEEF。
这意味着getbuf结束后ebp要回到原来的状态!
结合栈结构可知,设置输入字符的13-16字节为ebp原先值即可。
(2)eax=cookie. 类似trojan 2, 在1-12字节填充指令:mov eax ,0x71C665FE;ret 。
再修改一下返回地址即可。
通关密码:B8 FE 65 C6 71 C3 00 00 00 00 00 00 F8 FE 19 00 C8FE1900 81114000