Bomb Lab 来自《深入理解计算机系统》(CSAPP)一书的第三章的配套实验,该实验的目的是通过反汇编可执行程序,来反推出程序执行内容,进而能够正确破解”密码“,解除“炸弹”。
本实验共有6个 phase,对于每个 phase,你需要输入一段字符串,然后让代码中 explode_bomb函数不执行,这样就不会 boom !
准备工作
在拆炸弹之前我们先复习一下第三章的知识
1.关于跳转指令
指令 | 解释 |
---|---|
jz | 如果ZF=1,跳转至指定位置 |
jnz | 如果ZF=0,跳转至制定位置 |
je | 与jz类似,但通常在一条cmp指令后使用。如果源操作数与目的操作数相等,则跳转 |
jne | 与jnz类似,但通常在一条cmp指令后使用。如果源操作数与目的操作数不相等,则跳转 |
jg | cmp指令做有符号比较之后,如果目的操作数大于原操作数,跳转 |
jge | cmp指令做有符号比较之后,如果目的操作数大于或等于原操作数,跳转 |
ja | 与jg类似,但使用无符号比较 |
jae | 与jge类似,但使用无符号比较 |
jl | cmp指令做有符号比较之后,如果目的操作数小于原操作数,则跳转 |
jle | cmp指令做有符号比较之后,如果目的操作数小于或等于原操作数,则跳转 |
jb | 与jl类似,但使用无符号比较 |
jbe | 与jle类似,但使用无符号比较 |
jo | 如果前一条指令置位了溢出标志位(OF=1),则跳转 |
js | 如果符号标志位被置位(SF=1),则跳转 |
jecxz | 如果ECX=0,则跳转 |
2. 关于栈
当某个函数运行时,机器需要分配一定的内存去进行函数内的各种操作,这个过程中分配的那部分栈称为栈帧。下图描述了栈帧的通用结构。
栈帧是一段有界限的内存区间,由最顶端的两个指针界定,寄存器 %ebp 为帧指针(栈底),而寄存器 %esp 为栈指针(也就是说寄存器%ebp保存了所分配内存的最高地址,寄存器%esp保存了所分配内存的最低地址)。
当程序执行时,栈指针(栈顶)可以移动,因此大多数信息的访问都是相对于桢指针的。
在函数被调用之前,调用者会为调用函数做准备,具体来说就是传参。(传参对应的寄存器分别为:第一个参数%rdi,第二个参数%rsi......)
(c 程序使用栈存放局部变量、函数参数和返回地址)
被调用函数运行时,我们在函数里面是要执行各种操作的,所以我们需要给新栈帧分配一定的内存。也就是:
sub $0x10,%esp
(举个例子,具体分配内存大小按照所需空间分配),将%esp低地址移动16个字节。有了这么多的储存空间,才能支持函数里面的各种操作。
被调用函数运行结束时,恢复原来栈顶的状态。
⚠️这里要注意:函数的返回值一般储存在%eax寄存器中
请把下面这张图刻进 DNA
3.关于sscanf
sscanf(char *string, char *format, arg1,arg2, arg3....)
其中是%rdi寄存器作为指向string的头指针
同理,%rsi作为format
arg1, arg2, arg3, arg4分别是由寄存器%rdx, %rcx, %r8, %r9存储
如果上面的知识你已经掌握了,那我们来开始拆炸弹吧!
Bomb Lab
首先拿到代码我们先看 README 文件,好吧,什么都没有,我们继续看其他文件。
发现 bomb.c 文件,但是没有头文件,所以不能进行运行和编译。但可以看出该程序要求从命令行或者文件以“行”为单位读入字符串,每行字符串对应一个phase的输入。如果phase执行完毕,会调用phase_defused 函数表明该 phase 成功搞定。
最后剩一个可执行文件,我们通过gdb调试,反汇编bomb文件,尝试得到该可执行文件的汇编代码。
具体操作如下:
objdump -d bomb > bomb.asm
这样就把反汇编得到的汇编代码存在一个文件 bomb.asm 中,便于查看。
然后通过打 breakpoint 的方法,以及查看寄存器和内存里存的值的情况,结合汇编语句,推算出应该输入的语句。
phase_1
在汇编代码中找到phase_1函数
0000000000400ee0 <phase_1>: 400ee0:48 83 ec 08 sub $0x8,%rsp 400ee4:be 00 24 40 00 mov $0x402400,%esi 400ee9:e8 4a 04 00 00 callq 401338 <strings_not_equal> 400eee:85 c0 test %eax,%eax 400ef0:74 05 je 400ef7 <phase_1+0x17> 400ef2:e8 43 05 00 00 callq 40143a <explode_bomb> 400ef7:48 83 c4 08 add $0x8,%rsp 400efb:c3 retq
阅读代码我们先找到了 explode_bomb,在继续往上看
由于我们不能执行 explode_bomb 函数,所以需要在该函数之前执行跳转指令跳过explode_bomb 函数,那么什么条件可以执行该跳转指令呢?我们在继续往上看
发现 %eax 作为上一个函数的返回值,若 %eax 为0,才可以执行跳转
继续往上推发现了函数 strings_not_equal ,通过阅读代码可以发现这个函数是判断输入的两个字符串是否相等,根据上面我们提到的知识可以知道函数传进去的参数分别在寄存器 %edi 和 %es i中,其中 %edi 是我们输入的字符串
答案出来了,寄存器 %esi 里的值就是本题答案,寄存器 %esi 是被地址在 0x402400 的内容赋值,我们可以通过gdb查看一下该地址内容
先进入bomb(注意此时路径一定要在bomb文件下)
gdb bomb
查看该地址内容
(gdb) x/s 0x402400
出现本题答案
Border relations with Canada have never been better.
测试结果如下:
phase_2 (循环)
还是阅读代码找到 explode_bomb 函数,发现有两个
我们先看一下如何避免第一个引爆函数的执行
好与上题同理,这里需让 %rsp 的值为1,后执行跳转指令 je
目标地址 400f30 处是 lea 指令,功能为加载有效地址
两条指令执行后运行时栈的状态为
设置完栈地址后又执行了跳转指令
400f17:8b 43 fc mov -0x4(%rbx),%eax 400f1a:01 c0 add %eax,%eax 400f1c:39 03 cmp %eax,(%rbx) 400f1e:74 05 je 400f25 <phase_2+0x29> 400f20:e8 15 05 00 00 callq 40143a <explode_bomb> 400f25:48 83 c3 04 add $0x4,%rbx 400f29:48 39 eb cmp %rbp,%rbx 400f2c:75 e9 jne 400f17 <phase_2+0x1b> 400f2e:eb 0c jmp 400f3c <phase_2+0x40>
我们来仔细看一下这几条指令
首先 %eax 为 %rsp 指针指向的位置(值为1),%eax = %eax + %eax(此时值为2)
比较 %eax 与% rbx 指针所指向的值,这里为了不引爆炸弹,所以 %rbx 指针指向的值必须等于 %eax(值为2),那么我们可以知道 %rbx 指针指向的位置的值为 2
下一步将 %rbx + 4(其实也就是%rbx指针向上移动4个字节),比较 %rbp 与 %rbx ,相等则跳到结束语句,不相等则继续执行上几条指令
由此我们可以看出这是几条语句是循环指令,当 %rbp 与 %rbx 的值相等,则结束循环,若不相等则仍然执行上述指令
同理可得整个栈的内容为
由此phase_2的功能大概可以清楚了,首先将字符串解析成6个数字的正数数组,其中第一个整数必须为1,后面的每一个元素都是前一个元素的两倍。
即可得到本题答案
1 2 4 8 16 32
phase_3 (switch-case)
这次我们从上往下开始看
进入 phase_3 之后,首先是通过 sscanf 解析字符串,通过 gdb 调试可看出 sscanf 的 format 是%d %d
因此推断输出两个数字
(gdb) x/s 0x4025cf0x4025cf:"%d %d"
好下面我们开始找 explode_bomb 函数,发现有四个( 400f6f 行也算进去了)
400f60:83 f8 01 cmp $0x1,%eax400f63:7f 05 jg 400f6a <phase_3+0x27>400f65:e8 d0 04 00 00 callq 40143a <explode_bomb>
在找到第一个 explode_bomb 函数后往上看,发现在地址 400f63 处必须跳转
也就等同于输入的第一个值要大于等于 1
下面我们找到第二个函数
400f6a:83 7c 24 08 07 cmpl $0x7,0x8(%rsp) 400f6f:77 3c ja 400fad <phase_3+0x6a> #bomb
此时 7 必须大于 0x8(%rsp) --就是输入的第一个值,才不会发生爆炸
我们继续往下看
将输入的第一个值存到 %eax 中,然后执行一个间接跳转,地址为内存地址 0x402470(,%rax,8)处的值(这里%rax是我们输入的第一个值)
下面我们用 gdb 调试一下看看内存地址为 0x402470(,%rax,8) 的值
这里我们能看出,当输入值(%rax)为 0 的时候,跳转到 0x400f7c
同理为 1 的时候,跳到 0x400fb9 ...... 可以得出一个跳转表
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
address | 0x400f7c | 0x400fb9 | 0x400f83 | 0x400f8a | 0x400f91 | 0x400f98 | 0x400f9f | 0x400fa6 |
%eax | 207 | 311 | 707 | 256 | 389 | 206 | 682 | 327 |
为了跳过 400fad 行的炸弹,我们需要完成跳转表跳到 400fbe
这里为跳过 400fc4 行的 explode_bomb 函数,我们输入的第二个值要对应上面的跳转表
所以答案出来了,可以是以下答案的任意一种
1 311 / 2 707 / 3 256 / 4 389 / 5 206 / 6 682 / 7 327
phase_4 (递归)
这回我们还是从上往下看,发现很多代码与上一个相同
这里的 sscanf 也是输入两个数字,分别在地址0x8(%rsp)、0xc(%rsp) 上
401029:83 f8 02 cmp $0x2,%eax 40102c:75 07 jne 401035 <phase_4+0x29> #bomb
在 40102c 行我们遇到了第一个炸弹,此时限制条件是输入的第一个值不能为 2
40102e:83 7c 24 08 0e cmpl $0xe,0x8(%rsp) 401033:76 05 jbe 40103a <phase_4+0x2e> 401035:e8 00 04 00 00 callq 40143a <explode_bomb>
在这里的为了跳过这个炸弹,限制条件是0x8(%rsp) --输入的第一个值要小于 14(0xe)
40103a:ba 0e 00 00 00 mov $0xe,%edx 40103f:be 00 00 00 00 mov $0x0,%esi 401044:8b 7c 24 08 mov 0x8(%rsp),%edi 401048:e8 81 ff ff ff callq 400fce <func4>
再解决这两个限制条件之后,将 edx、esi、edi 赋值,将参数传进 func4函数 中
当执行完 func4 函数之后返回的值在寄存器 %eax 中
40104d:85 c0 test %eax,%eax 40104f:75 07 jne 401058 <phase_4+0x4c> 401051:83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) 401056:74 05 je 40105d <phase_4+0x51> 401058:e8 dd 03 00 00 callq 40143a <explode_bomb>
此时为了防止调用 explode_bomb 函数,返回值 %eax 要为 0
并且,0xc(%rsp) --输入的第二个值 也必须为 0
至此我们知道:
解析字符串为两个数字 x、y
x 必须小于等于14
调用 func4,x、0、14 分别为其参数
func4 返回值必须为0,y 必须为0
那下面看一下 func4 函数
这个函数最初看着可能有点麻烦,需要整理一下
eax = edxeax = eax - esiecx = eax ecx = 0eax = eax + ecxeax = eax / 2ecx = esi + eaxif(edi >= ecx){ return eax = 0 //正好是我们想要的结果 edi <= ecx }else{ edx = rcx - 1 func4() eax = eax + eax return}
我们将之前对参数带入发现:
寄存器 %eax 和 %ecx 存了7
%ecx 寄存器的值与 %edi 里的值比较 (%edi里是我们输入的第一个数)
这里比较的时候有两种情况
发生跳转时
400ff2:b8 00 00 00 00 mov $0x0,%eax 400ff7:39 f9 cmp %edi,%ecx 400ff9:7d 0c jge 401007 <func4+0x39>
此时 %edi 大于等于 7
地址400ff2处把%eax置为0,正好是我们想要的结果
比较要求 7 大于等于 %edi,后跳转可以顺利退出
我们发现当 %edi = 7 的时候可以满足这些条件顺利退出(这是一个答案,也就是说第一个输入的值可以为 7 )
不发生跳转时
400fe6:8d 51 ff lea -0x1(%rcx),%edx 400fe9:e8 e0 ff ff ff callq 400fce <func4> 400fee:01 c0 add %eax,%eax 400ff0:eb 15 jmp 401007 <func4+0x39>
这里用到递归,但也不要怕,我们只需要按照上面的指令,对寄存器 %eax 和 %ecx 一顿操作就可以了
至此答案就出来了:我们第一个输入的值可以是 0、1、3、7,第二个输入的值为 0
0 0 / 1 0 / 3 0 / 7 0
phase_5 (循环 字符串 ascii 码)
从上往下看,首先将输入值赋给寄存器 rbx ,第40106a行是金丝雀值,这里不用管,我们可以简单的看成从内存中读取一个数到寄存器 rax 中(忘记的可以回头看看书,这里是为了验证是否发生缓冲区溢出)
401062:53 push %rbx 401063:48 83 ec 20 sub $0x20,%rsp 401067:48 89 fb mov %rdi,%rbx 40106a:64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401071:00 00 401073:48 89 44 24 18 mov %rax,0x18(%rsp) 401078:31 c0 xor %eax,%eax 40107a:e8 9c 02 00 00 callq 40131b <string_length> 40107f:83 f8 06 cmp $0x6,%eax 401082:74 4e je 4010d2 <phase_5+0x70> 401084:e8 b1 03 00 00 callq 40143a <explode_bomb>
其次将寄存器 eax 清零,调用 string_length 函数(之前我们看过,这里不再赘述,就是比较字符串长度的函数)
此时为了防止炸弹爆炸,我们知道了输入的字符串长度应该为 6
4010d2:b8 00 00 00 00 mov $0x0,%eax 4010d7:eb b2 jmp 40108b <phase_5+0x29>
寄存器 eax 清零,继续执行跳转
40108b:0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx 40108f:88 0c 24 mov %cl,(%rsp) 401092:48 8b 14 24 mov (%rsp),%rdx 401096:83 e2 0f and $0xf,%edx 401099:0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx 4010a0:88 54 04 10 mov %dl,0x10(%rsp,%rax,1) 4010a4:48 83 c0 01 add $0x1,%rax 4010a8:48 83 f8 06 cmp $0x6,%rax 4010ac:75 dd jne 40108b <phase_5+0x29>
这里我们将汇编代码做一下调整
for(rax = 0,rax != 6,rax ++){ ecx = rbx + rax*(rsp) = clrdx = *(rsp)edx = edx & 0xfedx = 0x4024b0 + rdxrsp + rax + 0x10 = dl}
取输入字符串的字符,然后逐次将每个字符与 0xf “与”操作,得到的值作为 0x4024b0
处字符串的下标。
与 0xf “与运算”
操作意味着能取到 0x4024b0
处字符串的范围是 0-15,通过 gdb 查看 0x4024b0
处字符串:
得到了前16为字符串:maduiersnfotvbyl
通过 for 循环能够生成一个字符串,该字符串由输入的每个字符和 0xf “与运算”
得到的值,作为 maduiersnfotvbyl
的下标,来选择字符
那么生成的字符串是什么呢?我们继续往下看
4010ae:c6 44 24 16 00 movb $0x0,0x16(%rsp) 4010b3:be 5e 24 40 00 mov $0x40245e,%esi 4010b8:48 8d 7c 24 10 lea 0x10(%rsp),%rdi 4010bd:e8 76 02 00 00 callq 401338 <strings_not_equal> 4010c2:85 c0 test %eax,%eax 4010c4:74 13 je 4010d9 <phase_5+0x77> 4010c6:e8 6f 03 00 00 callq 40143a <explode_bomb>
4010ae 行是字符串的结束符,将生成的字符串与 0x40245e 处的字符串比较,为了防止炸弹爆炸,我们生成的字符串就要和 0x40245e 处的字符串相等,那我们查看一下
所以结果出来了:
1.输入的字符串长度为6
2.输入字符串的每个字符与 0xf “与运算”得到的数值在“maduiersnfotvbyl
”中寻找字符,最后得到一段字符串
3.循环得到的字符串应该为“flyers”
我们可以发现“flyers”六个字母对应 “maduiersnfotvbyl
” 的下标分别为 9、15、14、5、6、7
查一下 ascii 表,可以找到六个 & 0xf 分别为 9、15、14、5、6、7 的字符
答案为:
ionefg
phase_6 (链表、循环)
代码看着很长,我们拆成六个模块一点一点看
第一个模块就是调用 read_six_numbers 函数
4010f4:41 56 push %r14 4010f6:41 55 push %r13 4010f8:41 54 push %r12 4010fa:55 push %rbp 4010fb:53 push %rbx 4010fc:48 83 ec 50 sub $0x50,%rsp 401100:49 89 e5 mov %rsp,%r13 401103:48 89 e6 mov %rsp,%rsi 401106:e8 51 03 00 00 callq 40145c <read_six_numbers> 40110b:49 89 e6 mov %rsp,%r14 40110e:41 bc 00 00 00 00 mov $0x0,%r12d
不停的赋值,然后读入六个数,得到的栈如下图
第二个模块就是一个嵌套循环,利用上图的栈,对输入的数进行一些操作,这里就是看指针的变化
401114:4c 89 ed mov %r13,%rbp 401117:41 8b 45 00 mov 0x0(%r13),%eax 40111b:83 e8 01 sub $0x1,%eax 40111e:83 f8 05 cmp $0x5,%eax 401121:76 05 jbe 401128 <phase_6+0x34> 401123:e8 12 03 00 00 callq 40143a <explode_bomb> 401128:41 83 c4 01 add $0x1,%r12d 40112c:41 83 fc 06 cmp $0x6,%r12d 401130:74 21 je 401153 <phase_6+0x5f> 401132:44 89 e3 mov %r12d,%ebx 401135:48 63 c3 movslq %ebx,%rax 401138:8b 04 84 mov (%rsp,%rax,4),%eax 40113b:39 45 00 cmp %eax,0x0(%rbp) 40113e:75 05 jne 401145 <phase_6+0x51> 401140:e8 f5 02 00 00 callq 40143a <explode_bomb> 401145:83 c3 01 add $0x1,%ebx 401148:83 fb 05 cmp $0x5,%ebx 40114b:7e e8 jle 401135 <phase_6+0x41> 40114d:49 83 c5 04 add $0x4,%r13 401151:eb c1 jmp 401114 <phase_6+0x20>//双层循环
这里大家可能要回去复习一下几种寻址方式
比如(%r13)表示的是 r13 寄存器指向的值而不是 r13 寄存器的值
好,那我们看一下这个嵌套循环是怎么实现的呢?
40111e 行中 eax 寄存器其实是外层循环,循环六次,每次 r12 寄存器分别指向 num1、num2、num3......
内层循环就是拿 r12 寄存器的值和剩下的五个对比,这里为了跳过炸弹,这里告诉我们六个值都是不一样的且都小于等于6
用 c 代码给大家清楚的看一下
for (int r12 = 0; r12 != 6; r12++) { rbp = r13; eax = *r13; eax -= 1; if ((eax > 5)// 最大为6 explode_bomb(); for (ebx = r12+1; ebx <= 5; ebx++) { rax = ebx; eax = *(rsp+rax*4); if (*rbp == eax) //所有值不能相等 explode_bomb(); } r13 += 4;}
第三个模块就是一个循环,让每个栈中的 num 的值修改为 “7 - num”
401153:48 8d 74 24 18 lea 0x18(%rsp),%rsi 401158:4c 89 f0 mov %r14,%rax 40115b:b9 07 00 00 00 mov $0x7,%ecx 401160:89 ca mov %ecx,%edx 401162:2b 10 sub (%rax),%edx 401164:89 10 mov %edx,(%rax) 401166:48 83 c0 04 add $0x4,%rax 40116a:48 39 f0 cmp %rsi,%rax 40116d:75 f1 jne 401160 <phase_6+0x6c>//num = 7 - num
这里还是要注意一个寻址方式的问题哦~
第四个模块是本题组重要的环节,真的很复杂,可以多看几遍
40116f:be 00 00 00 00 mov $0x0,%esi 401174:eb 21 jmp 401197 <phase_6+0xa3> 401176:48 8b 52 08 mov 0x8(%rdx),%rdx 40117a:83 c0 01 add $0x1,%eax 40117d:39 c8 cmp %ecx,%eax 40117f:75 f5 jne 401176 <phase_6+0x82> 401181:eb 05 jmp 401188 <phase_6+0x94> 401183:ba d0 32 60 00 mov $0x6032d0,%edx 401188:48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) 40118d:48 83 c6 04 add $0x4,%rsi 401191:48 83 fe 18 cmp $0x18,%rsi 401195:74 14 je 4011ab <phase_6+0xb7> 401197:8b 0c 34 mov (%rsp,%rsi,1),%ecx 40119a:83 f9 01 cmp $0x1,%ecx 40119d:7e e4 jle 401183 <phase_6+0x8f> 40119f:b8 01 00 00 00 mov $0x1,%eax 4011a4:ba d0 32 60 00 mov $0x6032d0,%edx 4011a9:eb cb jmp 401176 <phase_6+0x82>//将六个节点的起始地址按照某顺序放到栈上
对栈里的6个数字,先判断他们是否为1
如果为1,就把0x6032d0放在栈里;如果不为1,就循环操作
对这个地址进行计算之后的地址里的值放在寄存器里,可以得到5个地址
分别是 0x6032e0、0x6032f0、 0x603300,、0x603310、0x603320
分别对应值2、3、4、5、6,我们用 gdb 查看对应的值
然后把他们按照某种顺序放在栈的 %rsp + 0x20 到 %rsp + 0x48 处
第五个模块对每个地址里存的值做了操作,其实就是将链表重新连接
4011ab:48 8b 5c 24 20 mov 0x20(%rsp),%rbx 4011b0:48 8d 44 24 28 lea 0x28(%rsp),%rax 4011b5:48 8d 74 24 50 lea 0x50(%rsp),%rsi 4011ba:48 89 d9 mov %rbx,%rcx 4011bd:48 8b 10 mov (%rax),%rdx 4011c0:48 89 51 08 mov %rdx,0x8(%rcx) 4011c4:48 83 c0 08 add $0x8,%rax 4011c8:48 39 f0 cmp %rsi,%rax 4011cb:74 05 je 4011d2 <phase_6+0xde> 4011cd:48 89 d1 mov %rdx,%rcx 4011d0:eb eb jmp 4011bd <phase_6+0xc9> 4011d2:48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) 4011d9:00 //重新连接链表
第六个模块为了防止炸弹爆炸,我们只能将链表降序连接
4011da:bd 05 00 00 00 mov $0x5,%ebp 4011df:48 8b 43 08 mov 0x8(%rbx),%rax 4011e3:8b 00 mov (%rax),%eax 4011e5:39 03 cmp %eax,(%rbx) 4011e7:7d 05 jge 4011ee <phase_6+0xfa> 4011e9:e8 4c 02 00 00 callq 40143a <explode_bomb> 4011ee:48 8b 5b 08 mov 0x8(%rbx),%rbx 4011f2:83 ed 01 sub $0x1,%ebp 4011f5:75 e8 jne 4011df <phase_6+0xeb>
好啦答案就出来啦
链表排序,输入的数字为原链表位置,输入的次序为新链表的位置
又因为第二个循环对array每个数字 7 - array[i],因此输入的数字需要将链表重排成为升序
地址 | 值 | 对应值 | 输入值 |
0x6032d0 | 14c | 1 | 6 |
0x6032e0 | a8 | 2 | 5 |
0x6032f0 | 39c | 3 | 4 |
0x603300 | 2b3 | 4 | 3 |
0x603310 | 1dd | 5 | 2 |
0x603320 | 1bb | 6 | 1 |
原链表的从小到大顺序是 5 6 1 2 3 4, 重排后为 4 3 2 1 6 5
即可得到答案
4 3 2 1 6 5
总结
其实这篇博客算是我写的最久的一篇了(可能下一篇会更久),上学期写项目有压力的时候开始写(结果写完更有压力了bushi)一直到所有比赛结束,我才开始收心写这篇博客。
不过也正是因为如此我做了很多遍这个lab,同时让我对汇编、逆向有了更深层次的了解,希望能给你们带来帮助❤️