深入理解计算机系统(CS:APP) - Bomb Lab详解

本文首发于我的博客

Bomb Lab

实验代码见GitHub

简介

BombLab是CS:APP中对应第三章内容:程序的机器级表示的lab。主要内容为提供一个二进制对象文件bomb,当运行时,它会要求用户输入六个字符串,如果其中的任何一个不正确,炸弹就会爆炸,输出一行错误信息并向计分服务器提交(自学所用的材料不会向服务器提交信息,但这不代表我们可以随意让炸弹爆炸),学生必须通过反汇编和逆向工程来找到六个正确的字符串来解除自己的炸弹(理论上每个人的炸弹答案都不同,但自学材料的答案都是一样的,本文针对的是自学材料)。

所用工具

objdump-用于反汇编二进制对象文件

VS Code-用于查看反汇编后的结果与文本文件的编写

gdb-用于运行时单步调试与查看运行时内存与寄存器信息

解题过程

前期

由于之前没有接触过类似的逆向工程问题,拿到问题以后第一时间很难马上开始解决。所以先查看我们能看到的文件信息。

目录中提供了一个bomb.c文件,文件内容十分简单,有一份非常有趣的LISENCE:

/***************************************************************************

* Dr. Evil's Insidious Bomb, Version 1.1

* Copyright 2011, Dr. Evil Incorporated. All rights reserved.

* LICENSE:

* Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the

* VICTIM) explicit permission to use this bomb (the BOMB). This is a

* time limited license, which expires on the death of the VICTIM.

* The PERPETRATOR takes no responsibility for damage, frustration,

* insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other

* harm to the VICTIM. Unless the PERPETRATOR wants to take credit,

* that is. The VICTIM may not distribute this bomb source code to

* any enemies of the PERPETRATOR. No VICTIM may debug,

* reverse-engineer, run "strings" on, decompile, decrypt, or use any

* other technique to gain knowledge of and defuse the BOMB. BOMB

* proof clothing may not be worn when handling this program. The

* PERPETRATOR will not apologize for the PERPETRATOR's poor sense of

* humor. This license is null and void where the BOMB is prohibited

* by law.

***************************************************************************/

接下来的部分就是main函数,从主函数中我们可以看到整个程序的结构与输入方式:可以从标准输入或文件中读取,一行作为一题的解,解出一个问题以后可以进入下一个问题,注意到返回前的一段注释:

​ /* Wow, they got it! But isn't something... missing? Perhaps

​ * something they overlooked? Mua ha ha ha ha! */

暗示了我们隐藏问题的存在,除此之外再也没有任何关于这个炸弹的信息。

下面我们使用objdump命令将炸弹文件反汇编出来:

objdump -d bomb > bomb.asm

然后通过VS Code来查看反汇编的结果,VS Codex86 and x86_64 Assembly这个插件可以高亮汇编,看起来会舒服许多。

反汇编出来的代码有近六千行,但是因为有符号表的存在,说明保留了调试所需的信息,我们可以通过gdb进行单步调试来查看程序的运行过程。

在使用gdb 的时候,我们可以加上-tui命令并用layout asm命令切换到汇编指令模式,就可以在调试的时候查看对应的汇编代码了。界面如下:

bomblab1.jpg

可以看到地址0x400da0就是main函数的地址。

一直向下查看,我们就可以看到C文件中出现的initialize_bomb函数,然后就到了phase_1函数,我们可以推测这个函数就是判断是否通过的核心函数。

这时候就要用到gdb的指令了,在汇编模式下的指令与普通模式有一些不同。我们可以使用ni(next instruction)和si(step into)来实现普通模式下的单步向下执行与步入操作。

打断点需要使用b <func_name>b *<address>来进行比如我们可以看到调用phase_1函数的call指令的地址是0x400e3a,所以我们可以使用b phase_1b *0x400e3a来打断点的,这两条命令有一点不同就在于断在地址会停在地址 上也就是call指令的位置,断在函数名会进入函数中,相当于再进行了一次si操作。

断点停后有可能出现字符重叠的情况,我们可以使用refresh命令刷新界面。

下面把断点打在phase_1函数之后就可以使用r命令来运行指令了,程序会提示我们输入字符串,这个时候因为我们打了断点不用担心炸弹会爆炸,可以随意输入。执行后程序会停在phase_1函数的位置,我们可以看到函数内部的情况。

下面就可以根据函数内部的逻辑来解决炸弹了。

代码来自objdump -d反汇编出来的代码,与gdb的汇编模式下看到的代码是一样的。

主函数

主函数代码比较长,只贴我们需要分析的关键部分。

  400e32:   e8 67 06 00 00          callq  40149e <read_line>
  400e37:   48 89 c7                mov    %rax,%rdi

第一句调用了read_line函数,我们可以转到函数入口地址40149e去查看read_line的代码(事实上一开始我也这么做了),但是会发现代码中包含了许多对系统库函数的调用,仔细分析的难度比较大并且没有必要。从提供的C代码与函数名称,我们可以推测出这个函数的作用是读取一行输入。根据返回值一般存放在rax中的约定,rax中应该就是读入的数据的地址,第二句中我们把这个值复制到了rdi中。

  400e3a:   e8 a1 00 00 00          callq  400ee0 <phase_1>
  400e3f:   e8 80 07 00 00          callq  4015c4 <phase_defused>

接下来两句分别开始调用phase_1phase_defused,下面的五个阶段也是上面这样的模式。

阶段一

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  

阶段一的代码比较短,第二行中把一个地址给了esi,接下来调用了strings_not_equal这个函数,我们可以跳到函数入口地址查看这个函数。

  40133c:   48 89 fb                mov    %rdi,%rbx
  40133f:   48 89 f5                mov    %rsi,%rbp
  401342:   e8 d4 ff ff ff          callq  40131b <string_length>

函数中这两行分别把rdi rsi的值复制到了rbxrbp,然后调用了string_length,这个时候就不用去看string_length函数了,我们可以直接猜测出rbxrbp就是函数的参数。那么可以说明rdi rsi就是给string_not_equal的函数,那么string_not_equal的返回值是什么呢?

看到string_not_equal返回后的5、6两句,测试了eax的值,在eax等于0时就跳转到400ef7,如果不为0,那么会继续向下执行,下面一句是调用explode_bomb函数,不用说这一定是触发炸弹的函数,所以我们需要令string_not_equal的返回值为0,那么从名字判断,我们需要令两个字符串相等,两个字符串之前说过存放在rdirsi中,rdi是我们读入的字符串,而rsi中存放的是400ee4复制的0x402400,这个时候用gdb去查看该地址中存放的字符串比较方便:

bomblab2.png

这串字符就是第一阶段的答案。

阶段二

  400efc:   55                      push   %rbp
  400efd:   53                      push   %rbx
  400efe:   48 83 ec 28             sub    $0x28,%rsp
  400f02:   48 89 e6                mov    %rsp,%rsi
  400f05:   e8 52 05 00 00          callq  40145c <read_six_numbers>
  400f0a:   83 3c 24 01             cmpl   $0x1,(%rsp)
  400f0e:   74 20                   je     400f30 <phase_2+0x34>
  400f10:   e8 25 05 00 00          callq  40143a <explode_bomb>
  400f15:   eb 19                   jmp    400f30 <phase_2+0x34>
  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>
  400f30:   48 8d 5c 24 04          lea    0x4(%rsp),%rbx
  400f35:   48 8d 6c 24 18          lea    0x18(%rsp),%rbp
  400f3a:   eb db                   jmp    400f17 <phase_2+0x1b>
  400f3c:   48 83 c4 28             add    $0x28,%rsp
  400f40:   5b                      pop    %rbx
  400f41:   5d                      pop    %rbp
  400f42:   c3                      retq  

进入phase_2函数,观察它的代码,可以发现第5行调用了一个名为read_six_numbers这个函数,顾名思义,这个函数的作用应该是从输入中读取6个数字,那么问题来了,这6个数字是怎么返回的呢?我们注意到第4行中把rsp的值复制给了rsi,我们可以猜测这个函数是使用栈来返回读入的结果。

当然只是猜测是不行的,我们需要用实验去验证我们的想法,我们在输入文件中设置1 2 3 4 5 6这一行输入,然后将断点打在*400f0a这个函数刚返回的位置(注意输入中应该含有第一阶段的答案,不然炸弹就炸在第一阶段了)。运行停在断点之后查看栈中的内容:

bomblab3.png

我们打出了rsp开始32字节的内容,发现栈中依次存放了输入的6个数,之后就是返回的地址。那么我们可以确定读取的数值就是依次存放在栈中的。

接下来看第6、7、8行,它将rsp中存放的值与1进行比较,如果相等则跳过第8行的引爆代码,说明我们需要输入的第一个数为1 。再看跳转到的位置(19、20行)将rsp+0x4rsp+0x18的值分别存放到了rbxrbp。下一行又进行了一次跳转,来到了第10行,第10行将rbx的地址减4中存放的内容复制到了eax中,rbx的地址减4也就意味着与rsp相等,它的值也就是第一个读入的值。下一行将eax的值乘二,接下来将乘二后的值与rbx也就是第二个值进行比较,如果相同则跳过引爆代码。上面这一系列操作总结起来就是如果第二个值是第一个值的两倍则不引爆。

再往下就是把rbx的值加上4,因为一个int占4个字节,也就是把rbx指向了下一个读入的值。下一步将rbxrbp的值进行比较,回想rbp的值为的rsp+0x18也就是 rsp+24,指向6个int值之后的位置,所以与它进行比较就是判断是否到达临界条件。如果没有到达临界条件,则跳到上一段中比较的部分继承进行。看到这里,我们已经可以判断出phase_2的要求是读入的6个数第一个数必为1,而后面的数字都是前面一个数字的两倍。

所以阶段2的答案为1 2 3 4 5 6.

阶段三

阶段三的代码比较长,我们分开来看:

0000000000400f43 <phase_3>:
  400f43:   48 83 ec 18             sub    $0x18,%rsp
  400f47:   48 8d 4c 24 0c          lea    0xc(%rsp),%rcx
  400f4c:   48 8d 54 24 08          lea    0x8(%rsp),%rdx
  400f51:   be cf 25 40 00          mov    $0x4025cf,%esi
  400f56:   b8 00 00 00 00          mov    $0x0,%eax
  400f5b:   e8 90 fc ff ff          callq  400bf0 <__isoc99_sscanf@plt>
  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>
  400f6a:   83 7c 24 08 07          cmpl   $0x7,0x8(%rsp)
  400f6f:   77 3c                   ja     400fad <phase_3+0x6a>
  400f71:   8b 44 24 08             mov    0x8(%rsp),%eax
  400f75:   ff 24 c5 70 24 40 00    jmpq   *0x402470(,%rax,8)

第3、4两行将rsp+0xcrsp+0x8的值分别给rcxrdx,下一行将一个地址值复制给了esi,接着将eax置为0,下一步调用了库函数sscanf,我们想到sscanf中的参数中需要一个格式化字符串,那么esi中的这个地址值就很有可能存放了这个字符串,我们同样使用gdb在运行时查看这个字符串:

bomblab4.png

可以看到这就是格式化字符串,读入的是两个整型值。这两个值存放在哪里呢?我们想到之前把rsp+0xcrsp+0x8的值分别给rcxrdx,这是两个地址值,我们可以用之前的方法验证栈中存放的确实是我们读入的这两个值。

下面第8行将eax与1进行比较,eax一般用于存放函数返回值,而sscanf 的返回值是成功读入的数值个数,也就是说这几行将成功读入的个数与1进行比较,如果大于1则跳过引爆的代码。

下面第11行将rsp+0x8中存放的值与0x7进行比较,如果大于0x7则跳到400fad的位置,我们看这个地址的指令:

  400fad:   e8 88 04 00 00          callq  40143a <explode_bomb>

引爆炸弹。

下面的两行比较关键:第13行将rsp+0x8中存放的值复制入eax,第14行进行一个跳转,跳转到的地址为0x402470(,%rax,8),这就是一个典型的switch语句的实现:直接跳转到索引*位移的指令位置。

  x = 0
  400f7c:   b8 cf 00 00 00          mov    $0xcf,%eax
  400f81:   eb 3b                   jmp    400fbe <phase_3+0x7b>
  x = 2
  400f83:   b8 c3 02 00 00          mov    $0x2c3,%eax
  400f88:   eb 34                   jmp    400fbe <phase_3+0x7b>
  x = 3
  400f8a:   b8 00 01 00 00          mov    $0x100,%eax
  400f8f:   eb 2d                   jmp    400fbe <phase_3+0x7b>
  x = 4
  400f91:   b8 85 01 00 00          mov    $0x185,%eax
  400f96:   eb 26                   jmp    400fbe <phase_3+0x7b>
  x = 5
  400f98:   b8 ce 00 00 00          mov    $0xce,%eax
  400f9d:   eb 1f                   jmp    400fbe <phase_3+0x7b>
  x = 6
  400f9f:   b8 aa 02 00 00          mov    $0x2aa,%eax
  400fa4:   eb 18                   jmp    400fbe <phase_3+0x7b>
  x = 7
  400fa6:   b8 47 01 00 00          mov    $0x147,%eax
  400fab:   eb 11                   jmp    400fbe <phase_3+0x7b>

  400fad:   e8 88 04 00 00          callq  40143a <explode_bomb>
  400fb2:   b8 00 00 00 00          mov    $0x0,%eax
  400fb7:   eb 05                   jmp    400fbe <phase_3+0x7b>
  x = 1
  400fb9:   b8 37 01 00 00          mov    $0x137,%eax
  400fbe:   3b 44 24 0c             cmp    0xc(%rsp),%eax
  400fc2:   74 05                   je     400fc9 <phase_3+0x86>
  
  400fc4:   e8 71 04 00 00          callq  40143a <explode_bomb>
  400fc9:   48 83 c4 18             add    $0x18,%rsp
  400fcd:   c3                      retq   

上面的代码已经加了注释,假设读入的第一个数为x,看到所有分支最后都跳转到了400fbe这行判断中,将eax中的值与rsp+0xc也就是我们读入的第二个数进行判断,如果相等的话跳过引爆代码。

而每个分支都将一个数复制到了eax中,也就是说我们只要根据不同的第一个参数的值读入对应的第二个参数就可以了,所以我们可以随意选择一个x值,这里我选择x=1,对应的第二个参数为0x137换成十进制是311,所以第3阶段的(一个)答案为:

1 311

阶段四

000000000040100c <phase_4>:
  40100c:   48 83 ec 18             sub    $0x18,%rsp
  401010:   48 8d 4c 24 0c          lea    0xc(%rsp),%rcx
  401015:   48 8d 54 24 08          lea    0x8(%rsp),%rdx
  40101a:   be cf 25 40 00          mov    $0x4025cf,%esi
  40101f:   b8 00 00 00 00          mov    $0x0,%eax
  401024:   e8 c7 fb ff ff          callq  400bf0 <__isoc99_sscanf@plt>
  401029:   83 f8 02                cmp    $0x2,%eax
  40102c:   75 07                   jne    401035 <phase_4+0x29>
  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>
  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>
  40104d:   85 c0                   test   %eax,%eax
  40104f:   75 07                   jne    401058 <phase_4+0x4c> 

前面的代码比较熟悉,同样是调用了sscanf函数,我们查看格式字符串:

bomblab5.png

也是读入两个参数存放在rcxrdx中。

同样对读入参数的个数进行了判断,要求成功读入参数的个数等于两个,第11、12行要求输入的第一个参数小于0xe

接下来把0xe赋给edx、0x0赋给esirsp+0x8的值赋给edi。接下来调用了func4函数。

在去查看func4函数的代码之前,我们先查看函数返回后的代码,了解我们需要的结果。第17、18行测试了eax的值如果不为0,就跳转到引爆代码。

所以我们的目标是返回时eax的值为0.下面进入func4函数。

0000000000400fce <func4>:
  400fce:   48 83 ec 08             sub    $0x8,%rsp
  400fd2:   89 d0                   mov    %edx,%eax
  400fd4:   29 f0                   sub    %esi,%eax
  400fd6:   89 c1                   mov    %eax,%ecx
  400fd8:   c1 e9 1f                shr    $0x1f,%ecx
  400fdb:   01 c8                   add    %ecx,%eax
  400fdd:   d1 f8                   sar    %eax
  400fdf:   8d 0c 30                lea    (%rax,%rsi,1),%ecx
  400fe2:   39 f9                   cmp    %edi,%ecx
  400fe4:   7e 0c                   jle    400ff2 <func4+0x24>
  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>
  400ff2:   b8 00 00 00 00          mov    $0x0,%eax
  400ff7:   39 f9                   cmp    %edi,%ecx
  400ff9:   7d 0c                   jge    401007 <func4+0x39>
  400ffb:   8d 71 01                lea    0x1(%rcx),%esi
  400ffe:   e8 cb ff ff ff          callq  400fce <func4>
  401003:   8d 44 00 01             lea    0x1(%rax,%rax,1),%eax
  401007:   48 83 c4 08             add    $0x8,%rsp
  40100b:   c3                      retq    

这段代码之中我们调用了func4,这是一个递归的过程,像之间那样直接分析比较困难,这里我们就将这个代码逆向为C语言再来分析,下面是逆向出的C语言代码:

int fun(int a1, int a2, int x){
    int b = (a1 - a2) >> 31;
    int result = ((a1-a2) + b) >> 1;
    b = result + a2;
    if(b == x) return 0;
    if(b < x) {
        result = fun(a1, b + 1, x);
        return result * 2 + 1;
    }else{
        result = fun(b - 1, a2, x);
        return result * 2;
    }
}

这里的a1``a2初始值分别为之前的0xe0x0。我们可以直接写个测试程序来跑出能返回0的输入值:

int main(void){
    for(int i = 0; i <= 0xe; i++){
        if(fun(0xe,0,i) == 0){
            printf("%d\n",i) ;
            return 0;
        }
    }
    return 0; 
}

得出允许的值有0 1 3 7.

回到phase_4的代码:

  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>
  40105d:   48 83 c4 18             add    $0x18,%rsp
  401061:   c3                      retq  

第1、2行将输入的第二个参数与0进行比较,如果不为0就引爆炸弹。所以输入的第二个参数必为0。

综上我们得出(一个)答案为:

0 0

阶段五

后面的阶段难度开始加大,我们分部分进行分析:

0000000000401062 <phase_5>:
  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)

第4行把输入的地址rdirbx,第5、7行则是在栈中压入了一个哨兵变量。

  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>

第1行清空了eax,第2行中调用了string_length,我们想到之前的把输入放入rbx这个动作,可以推测这个函数是为了统计输入字符的个数,并存放在了eax中。

下面将eax的值与0x6进行比较,等于则进行跳转避免引爆炸弹。我们进入跳转到的位置:

  4010d2:   b8 00 00 00 00          mov    $0x0,%eax
  4010d7:   eb b2                   jmp    40108b <phase_5+0x29>

eax置为0后进行跳转。

继续进入跳转到的位置:

 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>

第1行中movzbl命令将从rbx(输入)开始的rax位置的一个字节赋给ecx的低16位。

接下来的两行先把cl中的值(上一步得到)复制到rsp处,再将rsp中的值复制到rdx中,第4行使用掩码0xfedx的低4位。到这里我们总结一下上面的操作:取读入的字符串中rax位置处的字符,再取它的低4位放在edx中。

下面第5行中,将地址0x4024b0+rdx中的一个字节放入edx的低16位中。第6行将这16位复制到了rsp+0x10+rax的位置中。

接下来把rax加1,我们从前面可以看出来这个rax起的是一个索引的作用。第 8行与6进行比较,如果不等于6则跳到第1行重复这个过程。

在这段之中,循环一共进行了6次,分别读取了输入的6个字符,记录这个6个字符的低6位作为索引rdx,从0x4024b0+rdx的位置复制一个字节到rsp+0x10开始的6字节中。结束之后,rsp+0x10开始存放了6个字符。

  4010ae:   c6 44 24 16 00          movb   $0x0,0x16(%rsp)

接下来一行在rsp+0x16的位置也就是6个字符之后置上一个0x0也就是终止符\0

  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>

接下来将0x40245e这个地址赋给esi,把rsp+0x10这个地址赋给rdi,接下来调用strings_not_equal这个函数,之前的经验告诉我们esirdi就是要比较的两个字符串的首地址。如果两个字符串不相同就引爆炸弹。

我们先看0x40245e位置的字符串:

bomblab6.png

这就是我们应该构造并存放在rsp+0x10处的字符串。

接下来再查看我们复制到rsp中的字符来源也就是0x4024b0开始的字符:

bomblab7.png

可以看到我们需要的字符flyers的索引分别为9 15 14 5 6 7。这个索引就是我们输入的字符的低4位,那我们只要找到低4位分别是以上数值的字符就可以了。

所以阶段5的(一个)答案为:

ionefg

阶段六

阶段六可以说是最复杂的一个阶段,同样一步步分析:

00000000004010f4 <phase_6>:
  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>

读入6个数字,存放位置还是栈中。

  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>

前面是一系列的赋值操作,第5行将eax减1,eax中的值是rsp位置存放的值。第6、7两行将减一以后的值与5进行比较,小于等于5则跳过引爆代码。也就是说rsp中存放的第一个数必须小于等于6.

之前将r12d置为0,第9行中将r12d的值增加1,下一行与6进行比较,如果相等则跳入下一个阶段。

第12行中把r12d中的值复制给了ebx,下一步又赋给了rax,接下来的一行movrsp+rax*4中的值(也就是第rax+1个读入的int值)给了eax

下一步将eax中的值与rbp地址指向的值进行比较,如果不相同则跳过引爆代码。说明这两个值需要不同,再接下来将ebx中的值加1,再与5进行比较,如果小于等于5则跳到第13行中,更新rax的值,再去从栈中取下一个新的int值和rbp中的进行比较。到这里我们可以看出,从13行到20行相当于一个内循环,从r12d开始,到5结束,不断地取栈中的值与rbp的值比较,也就是要求rbp之后的值需要与rbp不同。

第21、22行则是外循环,它更新了r13的值,令r13指向下一个int值。跳到第3行用r13的值更新rbp的值,也就是把比较的对象向后移一个。同样要求该值小于等于5。后面再进行内循环比较之后的值。

这里我们就可以明白这段代码的作用:限制读入的6个数必须小于等于6并且互不相等。

  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>

第1行中将rsp+0x18的值赋给rsi

第2行将r14的值赋给raxr14的值是之前保存的rsp

第3行将0x7赋给ecx,第4行又将ecx复制给edx

下一步将edx减去rax存放的地址指向的值,接下来又将edx的值赋回rax存放的地址指向的值。

第7行将rax的值加4,也就是指向了下一个int值,接着与之前设定的rsi进行的比较,如果不相等则重复这个过程。rsi实际上指向的是6个int值之后的位置,作为一个标记使用。

这段代码总结起来就是将栈中的6个值(假设为x)变为7-x。

  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>

进入下一段代码,一开始先将esi归零,然后跳到第14行处执行。

第14行中从rsp+rsi的位置(也就是栈中我们读入的位置)取出一个数赋给ecx,接下来对取出的这个值进行判断,如果它小于等于1则跳到第9行处。

我们在这里假设这个数确实小于等于1。到第9行,将一个地址值赋给了edx,接下来将edx的值赋给了rsp+2*rsi+0x20的地址指向的值,这里我们可以知道rsi起到的是索引的作用,下面一行将rsi增加4,说明从rsp+0x20开始存放8个字节的数据。再将rsi的值与0x18作比较,说明整个过程要进行6次。接下来又到了第14行将下一个int值给rcx

那么如果rcx的值不小于等于1,继续往下走,第18行将0x1赋给eax,19行将0x6032d0这个地址赋给edx,接下来跳转到了第3行。第3-6行的代码是一起的,也是理解这个过程的关键。

首先第3行的命令,把edx+0x8地址指向的值赋给了edx,这步操作一开始比较难以理解,我们需要先看看edx的初始状态是什么样的,使用gdb在运行时查看内存:

bomblab8.png

我们可以从这个信息中看出,其实它就是一个链表的结构,首先名字就是node给了提示,再者每一个node中偏移8个字节中储存的都是下一个节点的地址,那么前面8个字节自然就是节点储存的数据。

我们再回过头来看第3行的代码,就不难理解这个操作就是我们常用的p = p -> next,也就是指向下一个节点。

第4行把eax增1,再将eaxecx进行比较,如果不等就再跳到第3步指向链表下一个节点,那么可以看出这4行代码的作用就是从edx这个初始位置开始向后移动ecx-1次,第7行跳过了第9行,把edx赋给了rsp+0x20开始的第rsi个8字节的位置。如果rsi达到0x18则跳出这部分代码。

我们整理一下这个过程,其实就是依次从栈中读取存放的6个数放入rcx,再根据rcx的值找到链表中对应的节点,把节点的地址放入rsp+0x20开始的对应位置中。

  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)

这段代码前三行分别将rsp+0x20地址指向值、rsp+0x28的值、rsp+0x50的值赋给了rbxraxrsi。第4行将rbx复制到rcx中,第5行将raxrsp+0x20)中存放的地址复制入rdx,第6行将这个数据赋给了rcx(也就是rbx*(rsp+0x20))节点的指针域。下一步将rax增加8,指向栈中的下一个位置。再与rsi这个临界地址进行比较,如果rax超出末端则跳出这段代码到第12行的位置。

下面把rdx中存放的地址值赋给rcx,跳转到第5行重复过程。

仔细分析,这个过程其实就是按照链表节点在栈中的位置重新将链表连接起来。

最后跳出的第12行则是把新的表尾的指针域赋为NULL

  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>
  4011f7:   48 83 c4 50             add    $0x50,%rsp
  4011fb:   5b                      pop    %rbx
  4011fc:   5d                      pop    %rbp
  4011fd:   41 5c                   pop    %r12
  4011ff:   41 5d                   pop    %r13
  401201:   41 5e                   pop    %r14
  401203:   c3                      retq   

第2行将ebp赋上0x5,第三行中rbx的值是之前的rsp+0x20,那么rbx+0x8这个地址中存放的值就是下一个节点的地址,赋给了rax

第4行将rax代表的节点的数据取出放入eax,再与rbx代表的节点的数据的值的低4位进行比较,如果前一个节点的数据的低4字节大于等于后一个节点的,则跳过引爆代码。

第8行又是熟悉的操作:使rbx指向下一个节点。

第9、10行减小ebp这个循环变量再进行判断,保证循环进行5次。

也就是说,我们需要使新的链表中前一个节点存放的数据值的低4字节都大于后一个节点的。

弄清楚了过程,下面就可以开始反推答案了:

先找到正确的链表节点排列,根据图:

bomblab8.png

数据由大到小的排列依次是3 4 5 6 1 2

由于有一步x = 7 - x,所以倒推回来的答案应该是:

4 3 2 1 6 5

秘密阶段

在之前C代码的暗示以及我们查看汇编代码的过程中都可以猜测出有一个秘密阶段的存在,secret_phase的代码就在phase_6后的func7之后。第一个问题是我们如何进入secret_phase

这里可以用一个简单的方法,直接在反汇编代码中搜索secret_phase的入口地址,很快就可以发现在每个阶段的phase_x之后都有一行phase_defused,就在这个函数里面存在callq secret_phase的代码。

我们就开始分析这个phase_defused

00000000004015c4 <phase_defused>:
  4015c4:   48 83 ec 78             sub    $0x78,%rsp
  4015c8:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  4015cf:   00 00 
  4015d1:   48 89 44 24 68          mov    %rax,0x68(%rsp)
  4015d6:   31 c0                   xor    %eax,%eax
  4015d8:   83 3d 81 21 20 00 06    cmpl   $0x6,0x202181(%rip)        # 603760 <num_input_strings>
  4015df:   75 5e                   jne    40163f <phase_defused+0x7b>
  4015e1:   4c 8d 44 24 10          lea    0x10(%rsp),%r8
  4015e6:   48 8d 4c 24 0c          lea    0xc(%rsp),%rcx
  4015eb:   48 8d 54 24 08          lea    0x8(%rsp),%rdx
  4015f0:   be 19 26 40 00          mov    $0x402619,%esi
  4015f5:   bf 70 38 60 00          mov    $0x603870,%edi
  4015fa:   e8 f1 f5 ff ff          callq  400bf0 <__isoc99_sscanf@plt>
  4015ff:   83 f8 03                cmp    $0x3,%eax
  401602:   75 31                   jne    401635 <phase_defused+0x71>
  401604:   be 22 26 40 00          mov    $0x402622,%esi
  401609:   48 8d 7c 24 10          lea    0x10(%rsp),%rdi
  40160e:   e8 25 fd ff ff          callq  401338 <strings_not_equal>
  401613:   85 c0                   test   %eax,%eax
  401615:   75 1e                   jne    401635 <phase_defused+0x71>
  401617:   bf f8 24 40 00          mov    $0x4024f8,%edi
  40161c:   e8 ef f4 ff ff          callq  400b10 <puts@plt>
  401621:   bf 20 25 40 00          mov    $0x402520,%edi
  401626:   e8 e5 f4 ff ff          callq  400b10 <puts@plt>
  40162b:   b8 00 00 00 00          mov    $0x0,%eax
  401630:   e8 0d fc ff ff          callq  401242 <secret_phase>
  401635:   bf 58 25 40 00          mov    $0x402558,%edi
  40163a:   e8 d1 f4 ff ff          callq  400b10 <puts@plt>
  40163f:   48 8b 44 24 68          mov    0x68(%rsp),%rax
  401644:   64 48 33 04 25 28 00    xor    %fs:0x28,%rax
  40164b:   00 00 
  40164d:   74 05                   je     401654 <phase_defused+0x90>
  40164f:   e8 dc f4 ff ff          callq  400b30 <__stack_chk_fail@plt>
  401654:   48 83 c4 78             add    $0x78,%rsp
  401658:   c3                      retq   

可以看到第7行将函数num_input_strings的返回值与6进行比较,如果不等于6则的直接跳过中间代码到达最后的结束部分。

从函数名我们可以推测这个函数的作用的是检测读取的字符串的数量,当读取了6个字符串时,就不会跳过中间的代码。我们继续看中间的代码:

第9到14行又是熟悉的sscanf调用过程,我们已经知道esi指向的是格式化字符串的首地址,我们先来查看它的内容:

bomblab9.png

读取两个整数和一个字符串。

有所不同的是在12行之后又有一行给edi赋上了一个地址值,我们之前所有阶段中edi的值都是来自于我们read_line的地址,想到sscanf 参数中确实存在一个输入,我们可以推测这个edi中存放的是我们读取位置的首地址。

那么我们就可以在运行时查看这个地址的内容,看是从哪里进行读取的:

bomblab10.png

首先符号表告诉我们这段数据的名字叫做input_strings也就是我们输入的字符串,那么这个地址上的0 0代表的应该就是我们的第4行输入。两个整型数字正好与格式化字符串也是匹配的。现在我们知道,应该在这两个0之后再追加一个字符串作为输入。

第15、16行对成功输入的数据个数进行了一个判断,如果不为3个则跳过调用secret_phase的代码。

第17-19行是对strings_not_equal的调用,我们已经知道它的两个参数分别是esiediesi被赋上了一个地址值,edi被赋上了esp+0x10,我们可以推测出edi的地址就是指向我们读入的第三个字符串的,那么需要比较的对象是什么呢?我们在运行时查看内存的内容:

bomblab11.png

这就是我们需要的第三个参数。

可以看到如果第三个参数与上面这个字符串相同的话就会调用两次puts输出提示信息,然后进入secret_phase阶段。

0000000000401242 <secret_phase>:
  401242:   53                      push   %rbx
  401243:   e8 56 02 00 00          callq  40149e <read_line>
  401248:   ba 0a 00 00 00          mov    $0xa,%edx
  40124d:   be 00 00 00 00          mov    $0x0,%esi
  401252:   48 89 c7                mov    %rax,%rdi
  401255:   e8 76 f9 ff ff          callq  400bd0 <strtol@plt>
  40125a:   48 89 c3                mov    %rax,%rbx
  40125d:   8d 40 ff                lea    -0x1(%rax),%eax
  401260:   3d e8 03 00 00          cmp    $0x3e8,%eax
  401265:   76 05                   jbe    40126c <secret_phase+0x2a>
  401267:   e8 ce 01 00 00          callq  40143a <explode_bomb>
  40126c:   89 de                   mov    %ebx,%esi
  40126e:   bf f0 30 60 00          mov    $0x6030f0,%edi
  401273:   e8 8c ff ff ff          callq  401204 <fun7>
  401278:   83 f8 02                cmp    $0x2,%eax
  40127b:   74 05                   je     401282 <secret_phase+0x40>
  40127d:   e8 b8 01 00 00          callq  40143a <explode_bomb>
  401282:   bf 38 24 40 00          mov    $0x402438,%edi
  401287:   e8 84 f8 ff ff          callq  400b10 <puts@plt>
  40128c:   e8 33 03 00 00          callq  4015c4 <phase_defused>
  401291:   5b                      pop    %rbx
  401292:   c3                      retq   

可以看到第3行调用了read_line函数,接着把read_line的返回值赋给了rdi,并调用了strtol函数,这个标准库函数的作用是把一个字符串转换成对应的长整型数值。返回值还是存放在rax中,第8行将rax复制给了rbx,第9行将rax减1赋给eax,第十行与0x3e8进行比较,如果这个值小于等于0x3e8就跳过引爆代码。看到这里我们可以知道我们需要再加入一行数据,它应该是一个小于等于1001的数值。

接下来将ebx赋给了esi,也就是我们一开始输入的rax值。第14行将一个地址值赋给了edi,15行调用了fun7函数。我们还是先往下了解一下我们需要得到的结果。

函数返回后令返回值eax0x2做了一个比较,如果相等则跳过引爆代码。

所以我们需要返回2。

下面查看fun7的代码:

0000000000401204 <fun7>:
  401204:   48 83 ec 08             sub    $0x8,%rsp
  401208:   48 85 ff                test   %rdi,%rdi
  40120b:   74 2b                   je     401238 <fun7+0x34>
  40120d:   8b 17                   mov    (%rdi),%edx
  40120f:   39 f2                   cmp    %esi,%edx
  401211:   7e 0d                   jle    401220 <fun7+0x1c>
  401213:   48 8b 7f 08             mov    0x8(%rdi),%rdi
  401217:   e8 e8 ff ff ff          callq  401204 <fun7>
  40121c:   01 c0                   add    %eax,%eax
  40121e:   eb 1d                   jmp    40123d <fun7+0x39>
  401220:   b8 00 00 00 00          mov    $0x0,%eax
  401225:   39 f2                   cmp    %esi,%edx
  401227:   74 14                   je     40123d <fun7+0x39>
  401229:   48 8b 7f 10             mov    0x10(%rdi),%rdi
  40122d:   e8 d2 ff ff ff          callq  401204 <fun7>
  401232:   8d 44 00 01             lea    0x1(%rax,%rax,1),%eax
  401236:   eb 05                   jmp    40123d <fun7+0x39>
  401238:   b8 ff ff ff ff          mov    $0xffffffff,%eax
  40123d:   48 83 c4 08             add    $0x8,%rsp
  401241:   c3                      retq   

第3、4两行先对我们输入的这个数作一个判断,如果等于0直接跳到第19行,返回-1,这显然不是我们想要的结果。

第5行将rdi的值读入到了edx中,第6行则将这个数与我们读入的数进行比较,如果这个数小于等于我们读入的数就跳至第12行,第12行将eax置0,再进行一次相同的比较,如果相等则跳至第20行返回。

如果不等(也就是edx小于我们读入的数),则继续向下执行第15行,这行代码有些与之前的链表跳至下一个节点类似,到这里,我们就需要查看一下rdi这个地址里存放的是怎样一种数据结构:

bomblab12.png

仔细观察可以发现这是一个二叉树的结构,每个节点第1个8字节存放数据,第2个8字节存放左子树地址,第3个8字节存放右子树位置。并且命令也有规律,naba代表层数,b代表从左至右第b个节点。

根据这个结构,我们可以把树画出来以便我们进行分析。随意找了个工具表示一下:

bomblab13.png

下面我们回到代码,现在我们知道第15行代码的作用是将rdi移到它的右子树的位置,接着调用fun7,在返回后令eax = 2 * rax + 1

如果第6行的比较中树节点的值大于我们读入的数呢?

代码会进行到第8行,令rdi移到它的左子树的位置,接下来调用fun7在返回后令eax = 2 * eax。下面跳至返回处。

总结上面的过程:edi指向一个树的节点,令edi节点的值与我们读入的值进行比较。

  • 如果两者相等:返回0
  • 如果前者大于后者:rdi移至左子树,返回2 * rax
  • 如果后者大于前者:rdi移至右子树,返回2 * rax + 1

那么我们需要返回2,应该在最后一次调用返回0,倒数第二次调用返回2 * rax + 1,第一次调用返回2 * rax。换句话说,这个数应该在第三层,比父节点大且比根结节小。观察上图,唯一的答案是:

0x16(22)


至此,炸弹全部解除:

bomblab14.png

实验小结

整个实验包括秘密部分用时九个小时,引爆了3次炸弹(一次因为错误的尝试,两次因为将ni命令错打成n)。

一开始拿到题目的时候会比较蒙,需要先去学习工具的使用与一些编译的基础知道(符号表、定址表等等)花费了一些时间。前几个阶段过于关注函数的具体实现而没有根据常识去推测一些明显函数的作用花费了一些时间。

前4个阶段都算比较简单,考查了一些常用结构在汇编中的出现形式。第5、6与秘密阶段分别考察了堆、链表、二叉树这三个数据结构在内存中的结构与汇编级的使用,受益良多。

这个实验需要细致的分析与大胆的猜测与实验验证,还需要小心操作,最重要的是耐心,面对非常晦涩的汇编代码如何一步步地弄清代码的作用很需要毅力。当然也可以通过自己写出等价的C代码来帮助自己理解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,653评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,321评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,833评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,472评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,306评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,274评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,658评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,335评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,638评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,697评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,454评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,311评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,699评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,986评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,254评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,647评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,847评论 2 335

推荐阅读更多精彩内容