Skip to content

Reverse | 逆向工程

ELF 文件

分类

  • 可重定位文件(Relocatable File),这类文件包含代码和数据,可用来连接成可执行文件或共享目标文件,静态链接库归为此类,对应于 Linux 中的 .o ,Windows 的 .obj。
  • 可执行文件(Executable File),这类文件包含了可以直接执行的程序,它的代表就是 ELF 可执行文件,他们一般没有扩展名。比如 .bin .bash,以及 Windows 下的 .exe。
  • 共享目标文件(Shared Object File),这种文件包含代码和数据,链接器可以使用这种文件跟其他可重定位文件的共享目标文件链接,产生新的目标文件。另外是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像来运行。对应于 Linux 中的 .so,Windows 中的 .dll。
  • 核心转储文件(Core Dump File),当进程意外终止,系统可以将该进程地址空间的内容及终止时的一些信息转存到核心转储文件。 对应 Linux 下的 core dump。

文件结构

image-20240302201709312

ELF 文件头 ( ELF Header )

位于文件的开始位置,它的主要目的是定位文件的其他部分。它包含了整个文件的基本属性:如文件大小、版本、目标机型、程序入口等。

.text

反汇编读取并处理的部分,这一部分是以机器码的形式存储,没有 .text 区段,我们很难去对一个可执行文件进行反汇编分析,也很难去看懂程序的二进制代码。

.data

包括已经初始化的 全局静态变量局部静态变量

.bss

存放的是 未初始化的全局变量和局部变量。在未初始化的情况下,单独用一个段来保存,可以不在一开始就为其分配空间,而是在链接成可执行文件的时候,再通过 .bss 段 分配空间。

Other Section

还有一些可选的段,比如 .rdata 表示这里存储只读数据,.debug 表示调试信息等等,具体遇到可以查看相关文档。

以上四个段都表示 ELF 中最基本的区段名称,它们都属于 区段

Symbol Tables

符号表。在链接的过程中需要把多个不同的目标文件合并在一起,不同的目标文件相互之间会引用变量和函数。在链接过程中,我们将函数和变量统称为 符号,函数名和变量名就是 符号名。每个定义的符号都有一个相应的值,叫做符号值(Symbol Value),对于变量和函数,符号值就是它们的地址。

PE 文件

实际上 PE 与 ELF 文件基本相同,也是采用了基于段的格式,同时 PE 也允许程序员将变量或者函数放在自定义的段中(使用 GCC 中 /* attribute/*(section('name')) 扩展属性)。

Name Describtion
Image File 镜像文件:包含以 EXE 文件为代表的 可执行文件、以 DLL 文件为代表的 动态链接库。因为他们常常被直接“复制”到内存中执行,有“镜像”的某种意思。
Section :是 PE 文件中 代码数据 的基本单元。原则上讲,节只分为 “代码节” 和 “数据节” 。
VA 基址:英文全称 Virtual Address。即为在内存中的地址。
RVA 相对基址偏移:英文全称 Relatively Virtual Address。偏移(相对虚拟地址)。相对镜像基址的偏移。

文件结构

pefile

DOS 头

是用来兼容 MS-DOS 操作系统的,目的是当这个文件在 MS-DOS 上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode. 还有一个目的,就是指明 NT 头在文件中的位置。

NT 头

包含 windows PE 文件的主要信息,其中包括一个 'PE' 字样的签名,PE 文件头(IMAGE_FILE_HEADER) 和 PE 可选头(IMAGE_OPTIONAL_HEADER32)。

节表

是 PE 文件后续节的描述,Windows 根据节表的描述加载每个节。

每个节实际上是一个容器,可以包含代码、数据 等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。

此处我们可以看到,其与 ELF 文件的结构大差不差,而且这几个常见的节名称作用也都是差不多的。

RVA 与 VA 的关系

在我们执行一个 PE 文件之后,这个 PE 文件会被装载到内存中,之后这个 PE 文件中的每一个部分都会有一个固定的虚拟地址 (VA),我们通常叫做内存地址。其中,一个 PE 文件最头部的地址叫做基地址(ImageBase),也可以简单理解为开始地址。此后,以基地址为基础,后面的所有地址都会有自己的相对基址偏移RVA),也就是其虚拟地址与基地址的差值,称为偏移量

由此我们可以知道:

假如一个 PE 文件的头地址 ImageBase 为 0x140000000,其中的某一个虚拟地址 VA 为 0x140000EF48,那么该地址相对于头地址的偏移量 RVA = 0x140000EF48 - 0x140000000 = 0xEF48

ASM 汇编语言

基本指令

跳转指令

一般条件跳转指令是在 cmp(比较)、test (逻辑与)指令之后用的

cmp

CMP ax, bx 修改标志位 符号描述
ax = bx ZF = 1(零标志位) 相等
ax != bx ZF = 0 不相等
ax < bx CF = 1(进位标志位) 小于
ax >= bx CF = 0 大于等于
ax > bx CF = 0 并且 ZF = 0 大于
ax <= bx CF = 1 或者 ZF = 1 小于等于
  • JMP:无条件跳转
  • JE:等于(Jump if Equal)
  • JNE:不等于(Jump if Not Equal)
  • JZ:零标志位为 1(Jump if Zero)
  • JNZ:零标志位为 0(Jump if Not Zero)
  • JS:符号标志位为 1(Jump if Sign)
  • JNS:符号标志位为 0(Jump if Not Sign)
  • JPJPE:奇偶标志位为 1(Jump if Parity/Even)
  • JNPJPO:奇偶标志位为 0(Jump if Not Parity/Odd)
  • JBJNAE:以下标志位为 1(Jump if Below/Not Above or Equal)
  • JAEJNB:以下标志位为 0(Jump if Above or Equal/Not Below)
  • JBEJNA:以下标志位或零标志位为 1(Jump if Below or Equal/Not Above)
  • JAJNBE:以下标志位和零标志位都为 0(Jump if Above/Not Below or Equal)
  • JO:溢出标志位为 1(Jump if Overflow)
  • JNO:溢出标志位为 0(Jump if Not Overflow)
  • JCJB:进位标志位为 1(Jump if Carry/Not Below)
  • JNCJAE:进位标志位为 0(Jump if Not Carry/Below or Equal)
  • JGJNLE:大于(Jump if Greater/Not Less or Equal)
  • JGEJNL:大于或等于(Jump if Greater or Equal/Not Less)
  • JLJNGE:小于(Jump if Less/Not Greater or Equal)
  • JLEJNG:小于或等于(Jump if Less or Equal/Not Greater)

位运算指令

  • AND:与运算,AND AX,BXAXBX 进行逻辑与运算,并将结果保存到 AX 寄存器中
  • OR:或运算,OR AX,BXAXBX 进行逻辑或运算,并将结果保存到 AX 寄存器中
  • XOR:异或运算,XOR AX,BXAXBX 进行异或运算,并将结果保存到 AX 寄存器中
  • NOT:取反操作,NOT CXCX 进行取反,并将结果保存到 CX 寄存器中
  • TEST:逻辑与运算,TEST AX,BXAXBX 进行与运算,并设置标志位,结果不保存
  • INC:自增指令

其它

  • LEA:地址加载, lea rdx,[my_var]my_var 的地址而不是内容赋值给 rdx 寄存器。
  • XCHG:数值交换指令,xchg ax,bxaxbx 的数值交换。
  • INT:引发中断,只有一个参数表示中断号,也可以称为中断类型码,是一个字节大小的正整数,范围为“0 - 255”。
  • LODSB/LODSW/LODSD:将 esi 指向的地址处的数据(字节/字/双字节)取出来赋给AL寄存器,esi++,即mov eax, [esi]; esi=esi+ 1/2/4;
  • STOSB/STOSW/STOSD:将 AL 寄存器的值(字节/字/双字节)赋给 edi 所指向的地址处,即mov [edi], eax; edi=edi+ 1/2/4;

函数传参

通常函数的返回值保存在 ax 寄存器里,比如 eax,rax 等; 函数的参数一般按顺序保存在 cx、dx、si 中,在 64 位汇编中,一般按 rcx rdx r8 r9 r10 r11 ... 的顺序保存参数。

有时候根据函数调用约定以及编译器和平台的不同,这个储存规律也会发生改变,需要根据当时情况动态调整。

常用工具

IDA

常见加密算法和编码识别

在对数据进行变换的过程中,除了简单的字节操作之外,还会使用一些常用的编码加密算法,因此如果能够快速识别出对应的编码或者加密算法,就能更快的分析出整个完整的算法。CTF 逆向中通常出现的加密算法包括 base64、TEA、AES、RC4、MD5 等。

Tea

在密码学中,微型加密算法(Tiny Encryption Algorithm,TEA)是一种易于描述和执行的块密码,通常只需要很少的代码就可实现。其设计者是剑桥大学计算机实验室的大卫 · 惠勒与罗杰 · 尼达姆。

参考代码:

#include <stdint.h>

void encrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */
    uint32_t delta=0x9e3779b9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i < 32; i++) {                       /* basic cycle start */
        sum += delta;
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);  
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

void decrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;  /* set up */
    uint32_t delta=0x9e3779b9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                         /* basic cycle start */
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;                                   
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

在 Tea 算法中其最主要的识别特征就是 拥有一个 magic number :0x9e3779b9 。当然,这 Tea 算法也有魔改的。

RC4

在密码学中,RC4(来自 Rivest Cipher 4 的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法。RC4 是有线等效加密(WEP)中采用的加密算法,也曾经是 TLS 可采用的算法之一。

参考代码:

void rc4_init(unsigned char *s, unsigned char *key, unsigned long Len) //初始化函数
{
    int i =0, j = 0;
    char k[256] = {0};
    unsigned char tmp = 0;
    for (i=0;i<256;i++) {
        s[i] = i;
        k[i] = key[i%Len];
    }
    for (i=0; i<256; i++) {
        j=(j+s[i]+k[i])%256;
        tmp = s[i];
        s[i] = s[j]; //交换s[i]和s[j]
        s[j] = tmp;
    }
 }

void rc4_crypt(unsigned char *s, unsigned char *Data, unsigned long Len) //加解密
{
    int i = 0, j = 0, t = 0;
    unsigned long k = 0;
    unsigned char tmp;
    for(k=0;k<Len;k++) {
        i=(i+1)%256;
        j=(j+s[i])%256;
        tmp = s[i];
        s[i] = s[j]; //交换s[x]和s[y]
        s[j] = tmp;
        t=(s[i]+s[j])%256;
        Data[k] ^= s[t];
     }
} 

通过分析初始化代码,可以看出初始化代码中,对字符数组 s 进行了初始化赋值,且赋值分别递增。之后对 s 进行了 256 次交换操作。通过识别初始化代码,可以知道 rc4 算法。

参考解密代码(复制的,来自对应例题:《从 0 到 1》RE 篇——BabyAlgorithm):

import base64
def rc4_main(key = "init_key", message = "init_message"):
    print("RC4解密主函数调用成功")
    print('\n')
    s_box = rc4_init_sbox(key)
    crypt = rc4_excrypt(message, s_box)
    return crypt
def rc4_init_sbox(key):
    s_box = list(range(256))
    print("原来的 s 盒:%s" % s_box)
    print('\n')
    j = 0
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len(key)])) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    print("混乱后的 s 盒:%s"% s_box)
    print('\n')
    return s_box
def rc4_excrypt(plain, box):
    print("调用解密程序成功。")
    print('\n')
    plain = base64.b64decode(plain.encode('utf-8'))
    plain = bytes.decode(plain)
    res = []
    i = j = 0
    for s in plain:
        i = (i + 1) % 256
        j = (j + box[i]) % 256
        box[i], box[j] = box[j], box[i]
        t = (box[i] + box[j]) % 256
        k = box[t]
        res.append(chr(ord(s) ^ k))
    print("res用于解密字符串,解密后是:%res" %res)
    print('\n')
    cipher = "".join(res)
    print("解密后的字符串是:%s" %cipher)
    print('\n')
    print("解密后的输出(没经过任何编码):")
    print('\n')
    return cipher
a=[] #cipher
key=""
s=""
for i in a:
    s+=chr(i)
s=str(base64.b64encode(s.encode('utf-8')), 'utf-8')
rc4_main(key, s)

MD5

MD5 消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个 128 位(16 字节)的散列值(hash value),用于确保信息传输完整一致。MD5 由美国密码学家罗纳德 · 李维斯特(Ronald Linn Rivest)设计,于 1992 年公开,用以取代 MD4 算法。这套算法的程序在 RFC 1321 中被加以规范。

其鲜明的特征是:

    h0 = 0x67452301;
    h1 = 0xefcdab89;
    h2 = 0x98badcfe;
    h3 = 0x10325476;

代码混淆

花指令

花指令(junk code)是一种专门用来迷惑反编译器的指令片段,这些指令片段不会影响程序的原有功能,但会使得反汇编器的结果出现偏差,从而使破解者分析失败。比较经典的花指令技巧有利用 jmpcallret 指令改变执行流,从而使得反汇编器解析出与运行时不相符的错误代码。

需要手动检查汇编代码进行修补。

爆红直接 nop(90) ?

Angr