Bomb Lab 来自《深入理解计算机系统》(CSAPP)一书的第三章的配套实验,该实验的目的是通过反汇编可执行程序,来反推出程序执行内容,进而能够正确破解”密码“,解除“炸弹”。
本实验共有6个 phase,对于每个 phase,你需要输入一段字符串,然后让代码中 explode_bomb函数不执行,这样就不会 boom !
准备工作
在拆炸弹之前我们先复习一下第三章的知识
1.关于跳转指令
指令解释jz如果ZF=1,跳转至指定位置jnz如果ZF=0,跳转至制定位置je与jz类似,但通常在一条cmp指令后使用。如果源操作数与目的操作数相等,则跳转jne与jnz类似,但通常在一条cmp指令后使用。如果源操作数与目的操作数不相等,则跳转jgcmp指令做有符号比较之后,如果目的操作数大于原操作数,跳转jgecmp指令做有符号比较之后,如果目的操作数大于或等于原操作数,跳转ja与jg类似,但使用无符号比较jae与jge类似,但使用无符号比较jlcmp指令做有符号比较之后,如果目的操作数小于原操作数,则跳转jlecmp指令做有符号比较之后,如果目的操作数小于或等于原操作数,则跳转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 0x4025cf
0x4025cf: "%d %d"
好下面我们开始找 explode_bomb 函数,发现有四个( 400f6f 行也算进去了)
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 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 ...... 可以得出一个跳转表
index01234567address0x400f7c0x400fb90x400f830x400f8a0x400f910x400f980x400f9f0x400fa6%eax207311707256389206682327
为了跳过 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 = edx
eax = eax - esi
ecx = eax
ecx = 0
eax = eax + ecx
eax = eax / 2
ecx = esi + eax
if(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) = cl
rdx = *(rsp)
edx = edx & 0xf
edx = 0x4024b0 + rdx
rsp + 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],因此输入的数字需要将链表重排成为升序
地址值对应值输入值0x6032d014c160x6032e0a8250x6032f039c340x6033002b3430x6033101dd520x6033201bb61
原链表的从小到大顺序是 5 6 1 2 3 4, 重排后为 4 3 2 1 6 5
即可得到答案
4 3 2 1 6 5
总结
其实这篇博客算是我写的最久的一篇了(可能下一篇会更久),上学期写项目有压力的时候开始写(结果写完更有压力了bushi)一直到所有比赛结束,我才开始收心写这篇博客。
不过也正是因为如此我做了很多遍这个lab,同时让我对汇编、逆向有了更深层次的了解,希望能给你们带来帮助❤️
版权归原作者 prician 所有, 如有侵权,请联系我们删除。