Bootstrap

计算机系统实验:二进制炸弹+缓冲区炸弹 (自我学习笔记)

本来没想写个博客,结果得知还要验收,发现自己全忘了,那就趁着复习的功夫再捋一遍吧>-<

一、使用工具: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返回地址
0019FED4esp0此后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

;