CS61系列实验之一 解除二进制×××

一、背景介绍

最近和组内同学一起在学习CS61Harvard课程)和《深入理解计算机系统》,二者虽然出处不同,但是所讲内容实际上是一致的,用一句话概括就是程序如何在计算机上运行,主要内容包括程序的机器级表示、处理器体系结构、存储器层次结构、动态内存分配、虚拟存储器、链接、多线程、同步、死锁等计算机关键问题。

此外,课程和书籍包含多个实验,借用《深入理解计算系统》的一句话,学计算机唯一有效的方法就是做(do)系统,所以只看书不够,一定要做实验。以此为契机,决定完成该课程的6个实验,并写专题博客。

1、课程CS61

CS61Harvard大学计算机科学的一门基础课程,全称为“Computer Science 61 and E61 Systems Programming and MachineOrganization”,简称CS61。官方网站是http://cs61.seas.harvard.edu/wiki/2012/Home,大家可以从中下载到讲义和实验作业。本人用得是2011版讲义,因此内容可能有少许不同。

2、《深入理解计算机系统》(第二版)

本书出自CMU的大学教材,后来修订成书,英文名为《Computer Systems: A Programmer’sPerspective》,被奉为计算机经典教材,也是CS61的推荐配套教科书。本书豆瓣评分9.7分(http://book.douban.com/subject/5333562/),足以说明这是本top书籍。强调一点,分数高不说明内容有多难,恰恰相反,说明这本书深入浅出。

3、背景知识

CS61和书籍主要讲程序如何在计算机上运行,所以需要对编程语言(最好是C/C++)有较好的基础,此外最好对计算机组成原理、操作系统等有一定了解。


二、问题描述

   本实验是解除二进制×××。给你一个二进制可执行文件,运行该文件,你需要在没有任何提示的情况下输入6password,如果都输入正确,则×××被解除(完成了实验);如果输入错误,则×××爆炸(当然不是真爆炸了,需要重做)。从计算机的角度来描述,就是通过二进制文件分析这个程序作了些什么。由于我们有反汇编工具,也可以说是通过汇编程序,分析这个程序作了什么。本实验的目的是理解汇编语言和掌握调试工具gdb

   具体来说,本实验给出一个二进制可执行文件bomb和一个C语言源程序bomb.c。运行./bomb,被要求输入password,我随意输入“I have no idea.”,当然不正确。提示我已经挂了,退出程序。

162152316.png

   源文件bomb.c并不是bomb的全部内容,其主要内容如表1所示,只是告诉我们它对六次输入做了不同的处理,但是并没有告知这六次处理的是如何做的,即phase_1(input); phase_2(input);phase_3(input); phase_4(input); phase_5(input); phase_6(input); 等六个函数的内容我们并不知道。

1 bomb.c 主要内容

printf("Welcome to my fiendish  little bomb. You have 6 phases with\n");
   printf("which to blow yourself up.  Have a nice day!\n");
   /* Hmm...   Six phases must be more secure than one phase! */
   input = read_line();             /* Get input                   */
   phase_1(input);                  /* Run the phase               */
   phase_defused();                 /* Drat!  They figured it out! */
                                  /* Let me know how they did it. */
   printf("Phase 1 defused. How about  the next one?\n");
   /* The second phase is harder.  No one will ever figure out
    * how to defuse this... */
   input = read_line();
   phase_2(input);
   phase_defused();
   printf("That's number 2.  Keep going!\n");
   /* I guess this is too easy so far.  Some more complex code will
    * confuse people. */
   input = read_line();
   phase_3(input);
   phase_defused();
   printf("Halfway there!\n");
   /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */
   input = read_line();
   phase_4(input);
   phase_defused();
   printf("So you got that one.  Try this one.\n");
   /* Round and 'round in memory we go,  where we stop, the bomb blows! */
   input = read_line();
   phase_5(input);
   phase_defused();
   printf("Good work!  On to the next...\n");
   /* This phase will never be used, since  no one will get past the
    * earlier ones.  But just in case, make this one extra hard.  */
   input = read_line();
   phase_6(input);
   phase_defused();


三、基本思路或使用工具

本实验需要使用objdump(反汇编工具)和gdb(调试工具),此外需要对理解汇编语言。上述工具容易上手,需要的话可以Google一下。特别说明一下,汇编语言分为两种风格,一种是AT&T,一种是Intel。本文所述工具(objdumpgdb)使用的汇编语言是前者,即AT&T风格,主要特点是源寄存器在前,目的寄存器在后,如MOVL %ESP, %EBP,表达的意思是把ESP的内容放入EBP中。

2 一些主要命令

反汇编: objdump -d bomb

查看所有寄存器内容: info registers

打印某一寄存器: p $eip

打印内存内容: x 0x8049034

打印内存内容(以字符串形式): x/s 0x8049034

设置断点:   b

设置断点(地址):   b* 0x8049034

运行: run

执行单步:si

此外,本实验并不是严谨的数学推导,在分析的基础上需要适当结合经验和推测。


四、解决问题

1、第一关 phase_1

GO GO GO!

执行objdump -d bomb,可以看到bomb的汇编代码,其中一段如下所示,不难看出这就是函数phase_1()的汇编代码。

162336837.png

4行是压栈以及对寄存器ebpesp的操作,这是函数调用时的必要流程。总体来看,phase_1先调用了一个名为<strings_not_equal>的函数,如果相等则则正常退出,进入下一关;如果不相等则调用函数<explode_bomb>,×××炸了,这关就失败了。

重点关注这一行: 8048974:   68 34 90 04 08           push  $0x8049034 。这是调用函数<strings_not_equal>前的压栈操作,$0x8049034看上去像是一个静态变量的地址,我们来打印一下。

162347686.png

内容是0x20776f4e,这很可能是一个字符串,我们以字符串形式再次打印。(备注:0x80******通常是内存地址,其他的内容则很可能是字符串,当然这要结合上下文来看。)

162419325.png

果真是个字符串。记下它并运行./bomb,输入该字符串,成功通过啦!

162432981.png

2、第二关 phase_2

这次我们看<phase_2>的汇编代码,代码如下:

162449268.png

80489a6这行来看,调用了名为<read_six_numbers>的函数,应该是读入了6个数字。总体来看,在读入6个数字后,进行了多次循环。重点关注80489c5及其以下两行,这是比较寄存器eax和内存内某一地址的内容是否相等,如果相等则正常跳转,如果不等则调用<explode_bomb>,×××爆炸。所以了解-0x2c(%ebp,%edx,4)%eax的内容十分关键。

设置断点并运行,第二关输入时直接输入6个数字“1 2 3 4 5 6”

162518540.png

打印%eax发现结果是1,证明eax内存放的是我输入的数字,那么-0x2c(%ebp,%edx,4)内很可能就是正确的数字。打印各寄存器内数据,笔算出-0x2c(%ebp,%edx,4)的实际被地址,即0xbffff6bc,打印这个内存。

162557798.png

结果为4,第一个数字应当是4,那后面5个数也是4还是各不相同呢?先试试呗,运行./bomb,在第二关时输入64,成功通过了!

162607540.png

3、第三关 phase_3

<phase_3>的汇编代码如下:

162619587.png

162634106.png

备注:代码行数太多,笔记本屏幕有点小,一张放不下,截了两张图。第三关有点难了,不像之前重点关注两三行就行,需要做详细分析。

先看8048a048048a14这几行,8048a14这行调用了<sscanf>函数,之前的几行都是调用函数前的压栈操作,8048a0c这行操作为push  $0x8049057,压入了一个静态字符串,打印出来看一下。

162659888.png看来是两个int型变量。

8048a048048a08两行,说明这两个变量分别位于 -0x8(%ebp) -0x4(%ebp)中,由于sscanf的参数是从后向前压栈,所以变量a-0x4(%ebp)中,变量b-0x8(%ebp)中。(为便于表示,假设两个变量依次为ab

然后看8048a2a这行“cmpl   $0x7,-0x4(%ebp)”,这是用变量a7比,如果a大于7,就跳转至8048a86,调用<explode_bomb>,×××就爆了。所以a不能大于7

再往下看,从8048a3e8048a84行,每两行一组,共8组,都是先将一个立即数放入-0xc(%ebp)中,然后跳转至8048a8b,类似switch结构。8048a8b及之后的操作是将-0xc(%ebp)中的内容让入eax,然后用变量-0x8(%ebp)(即变量b)与%eax作比较,如果相等则正常离开;如果不等则×××爆炸。

总结一下,我们需要输入变量ab。变量a必须小于等于7,后面的操作类似switch结构,分8种情况,因此我推测a的取值范围是[0, 7]。变量b的取值依赖变量a,应当就是switch结构那段的8个立即数。我们将上述结果算出,得到下表:

变量a

变量b16进制)

变量b10进制)

0

0x3d    

61

1

0x1b7

439

2

0xa4

164

3

0x1bb

443

4

0x15a

346

5

0x27f

639

6

0x2c0

704

7

0x99

153

所以结果可以是上述8种情况的任意一种,我随意尝试一种。成功通过!

123551974.png

4、第四关 phase_4

<phase_4>的汇编代码如下:

162752651.png

先看8048ad98048ae5这几行,这是调用sscanf函数,8048add这行(push $0x8048080)压入了一个静态字符串,打印出来看看。

162806356.png输入是一个int型变量,这里称之为aa的内存地址为-0x8(%ebp)

   然后看8048af6这行cmpl   $0x0,-0x8(%ebp),这是用0和变量a比较,如果a小于等于0,就跳转至8048afe,×××就炸了。所以a必须大于0

再往下看8048b06,这是调用函数func4func4的汇编代码如下。由于其内部又调用了func4,所以这应该是一个递归函数。

162833463.png

先不深究func4具体做了什么,往下看到8048b11这行,这是用立即数0x1cb91(对应10进制数是117649)与变量a比较,如果不等就爆炸。所以变量a需要满足这样一个条件:func4(a) = 117649。我用二分搜索的思路试了几次,第四次就得出了结果,a应当等于6。通过!

162852611.png

5、第五关 phase_5

<phase_5>的汇编代码如下:

162909311.png

先看8048b2d8048b38,大意是先调用<string_length>函数获得字符串的长度,如果等于6就继续往下做,如果不等于6就直接调用<explode_bomb>,炸了。所以字符串的长度应当等于6

然后先看比较靠后的8048b76,这是将-0x8(%ebp)的指向的变量和0x25作比较(我们称该变量为a)。如果a等于0x25,则过关。特别说明一下,8048b43这行将变量a初始化为0

现在重点看8048b518048b74,这是一个做了6次的循环,每次循环对应一个字符。变量a就是这6次循环算出的结果,那每次循环具体做了什么呢?(这段读代码+测试搞了好久)。先看8048b5d,我测试时发现,单步完成这条代码后,eax内容刚好是对应字符的ASCII码,然后下一步是将eax的内容和0xf按位相与。这两行的意思其实就是先获得字符的ASCII码,然后%16。再往下看8048b638048b6d,这是将一个静态数组中的某个值放入%edx,再累加到变量a中,数组中的索引和上一步%16的余数相对应。我们将这个静态数组打印出来:

162925126.png

   归纳成一个表

%16余数

数组变量

0

<array.0>

0x2

1

<array.0+4>

0xa

2

<array.0+8>

0x6

3

<array.0+12>

0x1

4

<array.0+16>

0xc

5

<array.0+20>

0x10

6

<array.0+24>

0x9

7

<array.0+28>

0x3

总结一下,我们需要从中找到6个数的和刚好是0x25,我找的是0xa+0xa+0xa+0x2+0x2+0x3= 0x25,对应的字符串是111007。输入我的字符串结果如下,通过!本题答案不唯一。

162949466.png

6、第六关 phase_6

<phase_6>的汇编代码如下:(太长了,没法截图,直接复制代码)

08048b83 <phase_6>:
 8048b83:   55                      push   %ebp
 8048b84:   89 e5                   mov    %esp,%ebp
 8048b86:   83 ec 68                sub    $0x68,%esp
 8048b89:   c7 45 f4 9c a6 04 08    movl   $0x804a69c,-0xc(%ebp)
 8048b90:   83 ec 08                sub    $0x8,%esp
 8048b93:   8d 45 c8                lea    -0x38(%ebp),%eax
 8048b96:   50                      push   %eax
 8048b97:   ff 75 08                pushl  0x8(%ebp)
 8048b9a:   e8 a2 fc ff ff          call   8048841 <read_six_numbers>
 8048b9f:   83 c4 10                add    $0x10,%esp
 8048ba2:   c7 45 a4 00 00 00 00    movl   $0x0,-0x5c(%ebp)
 8048ba9:   83 7d a4 05             cmpl   $0x5,-0x5c(%ebp)
 8048bad:   7f 4b                   jg     8048bfa <phase_6+0x77>
 8048baf:   8b 45 a4                mov    -0x5c(%ebp),%eax
 8048bb2:   83 7c 85 c8 00          cmpl   $0x0,-0x38(%ebp,%eax,4)
 8048bb7:   7e 0c                   jle    8048bc5 <phase_6+0x42>
 8048bb9:   8b 45 a4                mov    -0x5c(%ebp),%eax
 8048bbc:   83 7c 85 c8 06          cmpl   $0x6,-0x38(%ebp,%eax,4)
 8048bc1:   7f 02                   jg     8048bc5 <phase_6+0x42>
 8048bc3:   eb 05                   jmp    8048bca <phase_6+0x47>
 8048bc5:   e8 14 fc ff ff          call   80487de <explode_bomb>
 8048bca:   8b 45 a4                mov    -0x5c(%ebp),%eax
 8048bcd:   40                      inc    %eax
 8048bce:   89 45 a0                mov    %eax,-0x60(%ebp)
 8048bd1:   83 7d a0 05             cmpl   $0x5,-0x60(%ebp)
 8048bd5:   7f 1c                   jg     8048bf3 <phase_6+0x70>
 8048bd7:   8b 45 a4                mov    -0x5c(%ebp),%eax
 8048bda:   8b 55 a0                mov    -0x60(%ebp),%edx
 8048bdd:   8b 44 85 c8             mov    -0x38(%ebp,%eax,4),%eax
 8048be1:   3b 44 95 c8             cmp    -0x38(%ebp,%edx,4),%eax
 8048be5:   75 05                   jne    8048bec <phase_6+0x69>
 8048be7:   e8 f2 fb ff ff          call   80487de <explode_bomb>
 8048bec:   8d 45 a0                lea    -0x60(%ebp),%eax
 8048bef:   ff 00                   incl   (%eax)
 8048bf1:   eb de                   jmp    8048bd1 <phase_6+0x4e>
 8048bf3:   8d 45 a4                lea    -0x5c(%ebp),%eax
 8048bf6:   ff 00                   incl   (%eax)
 8048bf8:   eb af                   jmp    8048ba9 <phase_6+0x26>
 8048bfa:   c7 45 a4 00 00 00 00    movl   $0x0,-0x5c(%ebp)
 8048c01:   83 7d a4 05             cmpl   $0x5,-0x5c(%ebp)
 8048c05:   7f 3a                   jg     8048c41 <phase_6+0xbe>
 8048c07:   8b 45 f4                mov    -0xc(%ebp),%eax
 8048c0a:   89 45 f0                mov    %eax,-0x10(%ebp)
 8048c0d:   c7 45 a0 01 00 00 00    movl   $0x1,-0x60(%ebp)
 8048c14:   8b 45 a4                mov    -0x5c(%ebp),%eax
 8048c17:   8b 44 85 c8             mov    -0x38(%ebp,%eax,4),%eax
 8048c1b:   3b 45 a0                cmp    -0x60(%ebp),%eax
 8048c1e:   7e 10                   jle    8048c30 <phase_6+0xad>
 8048c20:   8b 45 f0                mov    -0x10(%ebp),%eax
 8048c23:   8b 40 08                mov    0x8(%eax),%eax
 8048c26:   89 45 f0                mov    %eax,-0x10(%ebp)
 8048c29:   8d 45 a0                lea    -0x60(%ebp),%eax
 8048c2c:   ff 00                   incl   (%eax)
 8048c2e:   eb e4                   jmp    8048c14 <phase_6+0x91>
 8048c30:   8b 55 a4                mov    -0x5c(%ebp),%edx
 8048c33:   8b 45 f0                mov    -0x10(%ebp),%eax
 8048c36:   89 44 95 a8             mov    %eax,-0x58(%ebp,%edx,4)
 8048c3a:   8d 45 a4                lea    -0x5c(%ebp),%eax
 8048c3d:   ff 00                   incl   (%eax)
 8048c3f:   eb c0                   jmp    8048c01 <phase_6+0x7e>
 8048c41:   8b 45 a8                mov    -0x58(%ebp),%eax
 8048c44:   89 45 f4                mov    %eax,-0xc(%ebp)
 8048c47:   8b 45 f4                mov    -0xc(%ebp),%eax
 8048c4a:   89 45 f0                mov    %eax,-0x10(%ebp)
 8048c4d:   c7 45 a4 01 00 00 00    movl   $0x1,-0x5c(%ebp)
 8048c54:   83 7d a4 05             cmpl   $0x5,-0x5c(%ebp)
 8048c58:   7f 1d                   jg     8048c77 <phase_6+0xf4>
 8048c5a:   8b 55 f0                mov    -0x10(%ebp),%edx
 8048c5d:   8b 45 a4                mov    -0x5c(%ebp),%eax
 8048c60:   8b 44 85 a8             mov    -0x58(%ebp,%eax,4),%eax
 8048c64:   89 42 08                mov    %eax,0x8(%edx)
 8048c67:   8b 45 f0                mov    -0x10(%ebp),%eax
 8048c6a:   8b 40 08                mov    0x8(%eax),%eax
 8048c6d:   89 45 f0                mov    %eax,-0x10(%ebp)
 8048c70:   8d 45 a4                lea    -0x5c(%ebp),%eax
 8048c73:   ff 00                   incl   (%eax)
 8048c75:   eb dd                   jmp    8048c54 <phase_6+0xd1>
 8048c77:   8b 45 f0                mov    -0x10(%ebp),%eax
 8048c7a:   c7 40 08 00 00 00 00    movl   $0x0,0x8(%eax)
 8048c81:   8b 45 f4                mov    -0xc(%ebp),%eax
 8048c84:   89 45 f0                mov    %eax,-0x10(%ebp)
 8048c87:   c7 45 a4 00 00 00 00    movl   $0x0,-0x5c(%ebp)
 8048c8e:   83 7d a4 04             cmpl   $0x4,-0x5c(%ebp)
 8048c92:   7f 24                   jg     8048cb8 <phase_6+0x135>
 8048c94:   8b 4d f0                mov    -0x10(%ebp),%ecx
 8048c97:   8b 45 f0                mov    -0x10(%ebp),%eax
 8048c9a:   8b 50 08                mov    0x8(%eax),%edx
 8048c9d:   8b 01                   mov    (%ecx),%eax
 8048c9f:   3b 02                   cmp    (%edx),%eax
 8048ca1:   7d 05                   jge    8048ca8 <phase_6+0x125>
 8048ca3:   e8 36 fb ff ff          call   80487de <explode_bomb>
 8048ca8:   8b 45 f0                mov    -0x10(%ebp),%eax
 8048cab:   8b 40 08                mov    0x8(%eax),%eax
 8048cae:   89 45 f0                mov    %eax,-0x10(%ebp)
 8048cb1:   8d 45 a4                lea    -0x5c(%ebp),%eax
 8048cb4:   ff 00                   incl   (%eax)
 8048cb6:   eb d6                   jmp    8048c8e <phase_6+0x10b>
 8048cb8:   c9                      leave
 8048cb9:   c3                      ret

代码太长,我简要概括一下,这段代码要求输入6个数字,然后经过处理得到一个6个元素的链表,最后进行比较。我将代码分为5部分,每块主要作用如下:

序号

代码行

主要作用

1

8048b93-8048b9a

调用<read_six_numbers>函数,读入6个数字

2

8048ba2-8048bf8

第一个循环,保证6个数字大于0、小于等于6、互不相等

3

8048bfa-8048c3f

第二个循环,构建链表

4

8048c54-8048c75

第三个循环,构建链表

5

8048c8e-8048cb6

第四个循环,进行比较

第一个循环,保证6个数字都大于0、小于等于6、互不相等。这就是说6个数字分别是123456,我们要做的就是确定它们的顺序。此外,这段代码需要反复调试单步,这里总结几个重要变量,便于大家理解。

变量描述

地址(表达式)

地址(计算后)

输入数组(保存6个数字)

-0x38(%ebp)

0xbffff6b0

循环变量(类似i, j

-0x5c(%ebp)

/

链表头指针

-0x58(%ebp)

0xbffff690

指针变量

-0x10(%ebp)

/

指针变量

-0xc(%ebp)

/

   本段代码对链表进行操作,那就产生一个关键问题:链表的内存分配是怎样的,链表的元素是怎样的struct?通过下面这张截图可以看出,每个元素是一个struct(称之为“Node”),每个Node有三个成员,我们依次称之为int aint bNode*next。变量b是我们输入的数字,变量a是根据b算出的数值,指针next指向下一个元素,Node的结构如下代码所示。整个链表包括6个元素,逻辑顺序为node1->node2->node3->node4->node5->node6

123923848.png

struct Node {
    int a; // calculate according to b
    int b; // from input
    Node* next;
};


重点看8048c94-8048c9f这几行,大意是前一个元素的a要大于后一个元素的a,即node1->a > node2->a。根据上图,我们把变量b和变量a的对应关系记下,如下表:

变量b(输入)

变量a(根据变量b计算而得)

1

0xce

2

0x3c8

3

0x106

4

0x21f

5

0x276

6

0x158

我们根据变量a的大小关系,对变量b进行排序,得到:2 5 4 6 3 1,这就是答案啦!验证一下,六关全部通过!oye

124006647.png