《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.4)

来源:互联网 发布:淘宝店铺模版怎么使用 编辑:程序博客网 时间:2024/06/15 23:34

1.4.Hello World

       1.4.1.x86

MSVC

       用这个编译器编译的二进制。其汇编代码大致过程是保存EBP,压栈字符串参数,调用printf,然后恢复ESP和EBP。

       注意恢复ESP用的是ADD ESP, 4,其实也可以用POP ECX。用前者可以不使用任何额外寄存器,也就不会改变ECX的值。用后者,汇编代码指令的长度会短一点。

对于return 0的处理,因为EAX放置返回值,所以使用XOREAX, EAX指令。

GCC

       用IDA反汇编得到的代码比上一个长很多。多了一个and esp, 0FFFFFFF0h的指令。这是一个对齐指令,是为了让ESP指向的地址值是对齐的,这样有助于提升CPU的性能。然后在栈上为字符串参数分配空间:SUB ESP, 10h,虽然只是传字符串地址,只需要4bytes,但是为了对齐分配了16bytes。

       最后的LEAVE指令相当于MOV ESP,EBP指令和POP EBP指令。在程序开始,使用了MOV EBP, ESP指令,先把ESP的值存在了EBP中,然后我们改变了ESP在栈上为参数分配空间。最后,我们把EBP中存放的原ESP值恢复给ESP,然后再把存储在栈上的EBP值恢复给EBP。所以汇编代码一开头都是PUSH EBP。

GCC:AT&T语法

       Intel和AT&T的不同有:①源和目的操作数的位置相反。②AT&T的寄存器前面要加符号%,立即数前面加符号$。③用前缀定义操作数大小。

Stringpatching (Win32)

       在二进制编辑器里面把Hello World字符串对应的二进制代码修改就可以了。挺有意思的。注意字符串要以0A-00结尾,其中0A是“\n”的二进制表示。

Stringpatching (Linux x64)

       用rada.re来patch Linux x64的二进制文件。这个rada.re应该是Linux下的一个工具,可以用一些指令来搜索二进制中的字符串,比如以“hello”作为关键字。记录字符串开始的地址,设置seek为这个地址,然后就可以从这个地址dump数据。用00+指令将二进制转变为可写的模式,然后用w在seek位置写入字符串。

       1.4.2.x86-64

       还记得此前是怎么传递参数的吗?把参数压栈。在64位CPU中,为了减少对栈的访问,使用寄存器来传参(即fastcall调用约定)。实际上函数中有一些传参用寄存器,有一些用栈。在Win64中,4个参数会用寄存器RCX, RDX, R8, R9。

       注意因为结尾返回的是int类型,是32bits的,所以将EAX设置为0而不是RAX。

       似乎没有用到栈,但是却在栈上分配了40bytes。后面再解释。

GCC:x86-64

       使用的传参寄存器是6个:RDI, RSI,RDX, RCX, R8, R9。注意当MOV指令后面接的是寄存器的低32位部分,则高32位部分会清零。

       1.4.3.GCC – one more thing

       编译器对于重复的字符串会只生成一个,以节省内存空间。

       1.4.4.ARM

      32位ARM包括Thumb和Thumb-2模式。64位的是ARM64。

Non-optimizingKeil (ARM mode)

       Keil是一个嵌入式领域的编译器。用IDA反汇编Keil编译的二进制,可以发现所有的指令都是4byte,是等长的。这是ARM模式。

       第一条指令:STMFD SP!, {R4, LR} 类似PUSH指令,将两个寄存器R4和LR的值写入栈中。如果是Thumb模式的话提供PUSH指令。这条指令先减小SP,这样就可以为新的值腾出空间。然后把R4和LR的值存入到减小后的SP内的地址所指位置。STMFD这个指令不仅用于压栈,它能够将寄存器的值存入任何内存地址。

       第二条指令:ADR R0, aHelloWorld 加或减PC寄存器存的值,达到helloworld字符串所在的位置偏移。ADR可以计算出当前PC与目标字符串之间的偏移。把这个偏移放入R0,就可以访问字符串的位置。

       第三条指令:BL __2printf 调用printf()函数。将BL指令之后的地址放在LR中,将控制权转交给printf(),通过把这个函数的地址写入PC。当printf()执行完之后,返回地址的信息在LR中。

       第四条指令:MOV R0, #0 把0写入R0,R0就是放返回值的。

       最后一条指令:LDMFD SP!, R4, PC从栈中加载值,恢复R4和PC。并且增加SP的值,即恢复栈。这一句指令类似POP。为什么要把原来的LR中的值恢复给PC呢?因为我们刚刚知道LR放返回地址,反正如果恢复给LR,它还是要传给PC以函数返回的。

       还有DCB类似x86里的DB命令,用来定义字符串数据。

Non-optimizingKeil (Thumb mode)

       可以发现这里有2byte的指令,也有4byte的指令。大多数是2byte,称为Thumb。调用printf()的之所以是4byte,因为一个指令放不下这么长的地址。另外,用PUSH和POP代替了STMFD/LDMFD。

OptimizingXcode 4.6.3 (LLVM) (ARM mode)

       我们分析优化后的代码。与Keil不同的是,直接用MOV指令将字符串的地址偏移放进R0。用R7寄存器作栈帧指针。还要专门用一个MOVT指令向R0的高16位写入0,因为之前的MOV指令只会写低16位。要记住ARM模式的每个指令都是32bits。然后用ADD指令把R0与PC中的值相加,得到字符串的绝对地址,放在R0里。然后调用puts()指令代替printf()。后面就是差不多一样的了。

OptimizingXcode 4.6.3 (LLVM) (Thumb-2 mode)

       Thumb-2指令都是以0xFx或者0xEx开头的。BL换成了BLX。在执行过程中,系统为了效率,会生成一个thunk function,这个函数是一个临时函数,是ARM模式的,为了将控制转移给动态库。

Moreabout thunk-functions

       这个临时函数不好理解,可以当作从一个类型到另一个类型的转换器。有时叫做wrappers。通常用在16位到32位的转换。

ARM64(GCC)

       ARM64里面没有Thumb和Thumb-2模式,只有ARM模式。所以只有32位的指令。寄存器是64位的。前缀为X。32位前缀为W。

       STP指令将两个寄存器中的值放在栈中,X29和X30。在栈上需要16bytes来放置这两个8bytes的寄存器的值。感叹号说明先减去SP的值。相当于PUSH X29和PUSH X30。X29用作栈帧指针,X30用作LR即存放返回地址。结尾处会恢复这两个寄存器的值。

       然后把当前的SP放到X29里面。等于重建函数栈帧。

       用ADRP和ADD两个指令把字符串地址设置好。

       然后调用puts函数。

       最后把W0寄存器设置为0,即返回值。

       产生了一个新指令RET,相当于BX LR。

1.4.5.MIPS

       MIPS当中有一个重要概念是全局指针。每个MIPS指令有32位,所以一个指令中不可能放一个32位的地址。所以就需要指令对。也可以使用地址偏移(16位)。我们可以为这个目的分配一些寄存器,这些寄存器就叫做全局指针,我们同时分配一个64KB的区域,全局指针就指向这个区域。这个区域包括全局变量、导入函数地址(如printf)。在ELF文件中,这个64KB的区域分片放在sections.sbss(意思为小的BSS)防止未初始化数据和.sdata防止初始化的数据。程序员可以把需要经常访问的数据放在这两个section。老的MS-DOS内存模型就是所有内存被划分为64KB的块。

OptimizingGCC

      首先设置$GP寄存器,指向区域中间。RA寄存器的值也放在栈里。puts()函数的地址加载到$25寄存器里,使用LW指令。字符串地址加载到$4寄存器中,使用LUI和ADDIU这个指令对来加载。LUI设置高16位,ADDIU设置低16位。注意在ADDIU前有一个JALR指令,这是分支延迟槽的作用。$4寄存器也叫做$A0,用来传递函数的第一个参数。JALR即jump and link register,跳转到存放在$25寄存器里的指令,在这里就是puts()函数。然后用LW把RA的值从栈中恢复。用MOVE指令把$0的值(为0)复制到$2寄存器中。MIPS有一个常量寄存器的值始终为0。J指令跳转到RA中的地址。

Non-optimizingGCC

       用FP寄存器作为栈帧的指针。还有3个nop指令,有2个加在分支指令后面,可能是因为分支延迟槽。这是没开优化的情况,如果开了优化这些nop就会被删除了。

       如果用IDA看的话,会发现LUI/ADDIU指令对被合成了一个LA指令。LA只是一个伪指令,并不是MIPS的指令。而且IDA并没有识别出NOP指令,用一个OR $AT, $ZERO代替了。这个指令其实也是一个空指令,没什么作用的。

       栈帧的作用:字符串地址传递到寄存器里,那为什么还要一个局部栈呢?因为RA和GP的值总要存在一些地方,因为执行过程中会改变。

OptimizingGCC: load it into GDB

       看起来舒服多了:

lui            gp, 0x42

addiu       sp, sp, -32

addiu       gp, gp, -30624

sw            ra, 28(sp)

sw            gp, 16(sp)

lw             t9, -32716(gp)

lui            a0, 0x40

jalr           t9

addiu       a0, a0, 2080

lw             ra, 28(sp)

move       v0, zero

jr              ra

addiu       sp, sp, 32

       1.4.6.结论

       x86/ARM和x64/ARM64的区别主要在于指向字符串的指针在后者是64位。

       1.4.7.练习

(1)http://chalenges.re/48

       这个win32函数的作用是,调用MessageBeep函数,传入的参数是-1。

(2)http://chalenges.re/49

       这个使用了AT&T风格的Linux函数,而且是在64位机器上的。先保存了RBP的值,然后往EDI里面复制了一个2,然后调用sleep函数。猜测是让程序休眠2秒。但是为什么要往EDI里面复制一个2呢?这个寄存器并不是用来传参的。EDI通常是用作存放偏移量,也许是通过偏移来寻找参数?


0 0
原创粉丝点击