Reverse | 逆向

Reverse | 逆向

https://hello-ctf.com/HC_Reverse

 

 Reverse | 逆向


项目名称Usage项目地址其他
微步沙箱 文件敏感操作检查  UsagePage /
Binaryai 基于开源项目代码匹配度在线反编译工具  UsagePage /
IDA 最常用的静态逆向工具 官网 ida pro 权威指南
Ghidra 开源的静态逆向工具,和 IDA 作用相同 官网 /
Ollydbg 同为反汇编调试器 (官方已经停止维护) 官网 /
x64dbg / x32dbg 在 windows 上使用的开源 x64 / x32 调试器 官网 /
DIE 查壳工具,拿到程序第一件事就是分析文件类型,是否有壳 GitHub GitHub
Exeinfope 同为查壳工具 官网 /
Cheat Engine 对程序的内存数据进行扫描和调试。 官网 /
GDB 一般用于 ELF 的动态调试,配合插件 (如 pwngdb,gdb-peda) 使用更佳 使用包管理工具安装 Sourceware
z3 开源的约束求解器,针对约束求解题型 GitHub /
dnSpy 强大的 .NET 调试、修改和反编译的工具 (已停止维护) GitHub /
----Java 反编译      
JADX 开源 更好的代码可读性 自动恢复丢失的类和方法、变量和方法名称、可以将反编译结果导出为 Eclipse 或 IDEA 项目 GitHub /
JD-GUI 更好的代码可读性 可导出为 Java 文件或 Jar 包 GitHub /
JEB 支持 wasm 可交叉引用、可看字节码、反编译结果纯粹 官网 /
GDA 支持 apk, dex, odex, oat, jar, class, aar 文件的反编译, 支持 python 及 java 脚本自动化分析 官网 /
Fernflower IDEA 采用的反编译工具 , 支持 Jar 包反编译。 GitHub /
----Python 反编译      
pycdc pyc 反编译 , 对高版本有不错兼容性。 GitHub /
Unpy2exe 对 py2exe 打包的 python 程序提取字节码文件 (.pyc) GitHub /
Pyinstxtractor 对 pyInstaller 打包的 python 程序提取字节码文件 (.pyc) GitHub /
[Python]uncompyle 用于对 Python 字节码文件 (.pyc) 的反汇编,将其变成 python 源代码。 官网 /

 

引导篇:灵魂 F&Q

本章节将会通过三个问题作为切入点,向你介绍关于 CTF 中的逆向工程 (Reserve) 的一些基本情况:是什么?有什么?怎么做?

1. 是什么?🤨

你说得对,但逆向工程一般指软件逆向工程,即对已经编译完成的 可执行文件 进行分析,通过反汇编工具查看程序的二进制代码,以及汇编块,研究程序的行为和算法,然后以此为依据,得出出题人想隐藏的 flag。当然,这只是通俗易懂的针对 CTF 比赛的解释,实战中的逆向工程,会涉及到更多方面,解释也会更加复杂,这里不再赘述。

💡 Tips:建议尽你所能去了解一下什么是 可执行文件,本板块中不会对可执行文件进行完整的解释。

2. 有什么?🧐

逆向类题目是 CTF 中难度相对较高的题型,现已覆盖 Windows 逆向、Linux 逆向和 Android 逆向,再加上 Flash 逆向、Python 逆向、.NET 逆向、ARM 逆向等。

目前主流的逆向文件平台有 Windows 和 Linux,这两个平台的逆向工程发展时间最久,其可变灵活度也很高,所以目前 CTF 中的逆向题目也就自然而然地成为了难度很大的方向。

CTF 中给出的题目常见形式有 ELF 文件、exe 文件、dll 文件等。同时,也不乏有直接将一段汇编代码塞到 txt 里直接丢给你的情况。以上这些在后续的例题中也会随之讲到。

以上这些文件形式又会以不同的加密方法出现:混淆、花指令、加壳、复杂算法、复杂汇编等。我们都需要突破这些障碍,去拿到我们想要的 flag 字符串。

3. 怎么做?🤔

目前,从我个人观点来讲,在进行 CTF 逆向时,我们需要做到以下方面的准备:Tool(工具)、Language(语言)、Stick [肝 (坚持 )],简称 TLS 传输层安全协议 (bushi) TLS,如果非要多加一个那就是 Observation and Analysis(代码分析能力)。

Tool : 指在进行逆向分析时所需要用到的软件,我们常见的有 DIE、IDA、GDB、Ollydbg(简称 OD)、Cheat Engine(简称 CE) 等。

这些工具的用法在后面的章节中都会教大家如何 Quick Start。

Language : 兄弟,你知道我想说什么。语言 这个词在这里提出来,那肯定不是单纯的英语了,这里指计算机语言。

在进行逆向工程的过程中,我们难免要编写一些脚本去计算或者模拟出题人给我们的一些过程,从而去寻找我们想要的 flag。

最常见也最常用的编程语言就是 Python,其次是 C++ 和 Java。除去编程语言,还有一个语言比较重要,那就是汇编语言 (简称 ASM):汇编语言与早期的编程环境比较接近,与机器码 (HEX、BIN) 比较接近,但是可以让人比较容易看懂,搞嵌入式的同学可能会经常搞这些。在 CTF 逆向中,我们需要面临的有 x86 汇编和 x64 汇编,也即 32 位汇编和 64 位汇编;两者虽然同为汇编语言,但是它们之间还是存在一些差别的,所以在学习的时候两个都不能有过于倾向的情况,都要认真去学习。

在后面的章节中,我会提到一些关于 ASM 的 Quick Start 方法。

Stick : 这个就不多说了吧,原神玩家最清楚这种体验 () 。对于 re 手来说,一次看几万行代码的情况,很常见。这种高强度的折磨对于没有耐心的小伙伴来说简直是虐待,在这种情况下你要盯着屏幕一直看,还要保持细心和睿智。所以很多 re 手比较 擅长摸鱼发呆 (bushi) 沉稳和精明。

4. 章节总结 😆

本章主要介绍了 CTF 中逆向工程方向的一些基本情况,目的是 劝退各位✌ 带各位大致了解一下 CTF 中的逆向工程是什么样子的,以及之后的学习方向都有哪些。如果有错误请及时指出哟~

 

Quick Start:可执行文件

本章节将会对目前两大主流平台的可执行文件进行简单的介绍,便于我们了解可执行文件的基本结构。

目前我们常见的两大主流平台分别是 Linux 和 Windows;在前者环境下,其主要的可执行文件对应的名称为 ELF(Executable and Linking Format)文件;在后者环境下,其可执行文件对应的名称为 PE(Portable Executable)文件。

ELF 和 PE 文件都是基于 Unix 的 COFF (Common Object File Format) 改造而来,更加具体的来说,他是来源于当时著名的 DEC(Digital Equipment Corporation) 的 VAX/VMS 上的 COFF 文件格式。


首先我们来介绍 ELF 文件:

ELF 文件标准里把系统中采用 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。

ELF 文件结构:

ELF Header
.text
.data
.bss
Other Section ...
Section Header Table
**String Tables /

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

typedef struct elf32_hdr
{
      unsigned char e_ident [EI_NIDENT];    /* Magic number and other info */
      Elf32_Half    e_type;         /* Object file type */
      Elf32_Half    e_machine;      /* Architecture */
      Elf32_Word    e_version;      /* Object file version */
      Elf32_Addr    e_entry;        /* Entry point virtual address */
      Elf32_Off e_phoff;        /* Program header table file offset */
      Elf32_Off e_shoff;        /* Section header table file offset */
      Elf32_Word    e_flags;                       /* Processor-specific flags */
      Elf32_Half    e_ehsize;       /* ELF header size in bytes */
      Elf32_Half    e_phentsize;        /* Program header table entry size */
      Elf32_Half    e_phnum;        /* Program header table entry count */
      Elf32_Half    e_shentsize;        /* Section header table entry size */
      Elf32_Half    e_shnum;        /* Section header table entry count */
      Elf32_Half    e_shstrndx;     /* Section header string table index */
} Elf32_Ehdr;
/*
e_ident : ELF 的一些标识信息,前四位为.ELF, 其他的信息比如大小端等
e_machine : 文件的目标体系架构,比如 ARM
e_version : 0 为非法版本,1 为当前版本
e_entry : 程序入口的虚拟地址
e_phoff : 程序头部表偏移地址
e_shoff : 节区头部表偏移地址
e_flags :保存与文件相关的,特定于处理器的标志
e_ehsize :ELF 头的大小
e_phentsize : 每个程序头部表的大小
e_phnum :程序头部表的数量
e_shentsize:每个节区头部表的大小
e_shnum : 节区头部表的数量
e_shstrndx:节区字符串表位置
*/

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

3、.data:包括已经初始化的 全局静态变量 和 局部静态变量

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

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

—————————(以上 2、3、4、5 都表示 ELF 中最基本的区段名称,它们都属于 区段 )

6、Section Header Table:是一个重要的部分,它描述了 ELF 文件包含的所有段的信息,比如每个段的段名、段长度、在文件中的偏移、读写权限和一些段的其他属性。

typedef struct{
    Elf32_Word sh_name;   //节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。
    Elf32_Word sh_type;    //为节区类型
    Elf32_Word sh_flags;    //节区标志
    Elf32_Addr sh_addr;    //如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。
    Elf32_Off sh_offset;    //此成员的取值给出节区的第一个字节与文件头之间的偏移。
    Elf32_Word sh_size;   //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
    Elf32_Word sh_link;   //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
    Elf32_Word sh_info;       //此成员给出附加信息,其解释依赖于节区类型。
    Elf32_Word sh_addralign;    //某些节区带有地址对齐约束.
    Elf32_Word sh_entsize;    //某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。
}Elf32_Shdr;

最基本的一些区段名称及其作用:

名称类型属性含义
.bss SHT_NOBITS SHF_ALLOC + SHF_WRITE 包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。
.comment SHT_PROGBITS (无) 包含版本控制信息。
.data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.debug SHT_PROGBITS (无) 此节区包含用于符号调试的信息。
.dynamic SHT_DYNAMIC   此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含了动态链接符号表。
.fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。
.got SHT_PROGBITS   此节区包含全局偏移表。
.hash SHT_HASH SHF_ALLOC 此节区包含了一个符号哈希表。
.init SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。
.interp SHT_PROGBITS   此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.line SHT_PROGBITS (无) 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。
.note SHT_NOTE (无) 此节区中包含注释信息,有独立的格式。
.plt SHT_PROGBITS   此节区包含过程链接表(procedure linkage table)。
.relname .relaname SHT_REL SHT_RELA   这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.rodata .rodata1 SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.shstrtab SHT_STRTAB   此节区包含节区名称。
.strtab SHT_STRTAB   此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.symtab SHT_SYMTAB   此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含 SHF_ALLOC 位,否则该位置为 0。
.text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含程序的可执行指令。

7、String Tables:字符串表。在 ELF 文件中,会用到很多字符串,比如节名,变量名等。所以 ELF 将所有的字符串集中放到一个表里,每一个字符串以’/0’分隔,然后使用字符串在表中的偏移来引用字符串。这样在 ELF 中引用字符串只需要给出一个数组下标即可。字符串表在 ELF 也以段的形式保存, .shstrtab 是专供 section name 的字符串表区段。

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


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

在此之前,我们来了解一些基本概念:

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

1、PE 文件结构:

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 文件的结构大差不差,而且这几个常见的节名称作用也都是差不多的,这里就不再重复了。

2、RVA 与 VA 的关系:

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

由此我们可以知道:

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


那在逆向工程的过程当中,我们如何来辨别这两种文件呢?

我们将其拖到 xdbg 中,查看 HEX 窗口,在右侧的字符串区域,可以找到答案:

ELF 文件会出现 “ELF” 相关的字符串,一本都是出现在头部;PE 文件在其头部有 “MZ” 字样,其附近还会出现 “DOS” 以及 “PE” 等关键字样。通过这些特征,我们即可方便又快速地找到其文件类型。同时,我们还可以使用查壳工具进行辨别。

 

Quick Start:汇编语言

本章节将会简单介绍一下汇编语言,汇编语言在逆向工程中占据的地位还是比较重要的。此章节并不会系统地去介绍整个汇编语言体系,而是以 CTF 逆向 为背景,介绍比较实用的部分。

下面我们以一个简单的 Hello World 程序为背景,初步介绍汇编:

#include <stdio.h>
int main(int argc, char* argv []) {
    printf("Hello, world!\n");
    return 0;
 }

我们通过 GCC 将以上程序编译为 ASM 形式并输出,可以得到以下内容:

      .section .rodata
  .LC0:
      .string "Hello, world!"
      .text
      .globl   main
      .type    main, @function
  main:
      push     rbp
      mov      rbp, rsp
      sub      rsp, 16
      mov      DWORD PTR [rbp-4], edi
      mov      QWORD PTR [rbp-16], rsi
      mov      edi, OFFSET FLAT:.LC0
      call     puts
      mov      eax, 0
      leave
      ret

这是一个很简单的执行过程,下面我们来简单的介绍一下这块汇编:

首先 .LC0: 定义了一个字符串,也即我们的 “Hello, world!”,它的作用就是告诉编译器这个字符串的位置(地址)。对于我们直接输入的字符串,系统会自动为其分配一个临时内存,用来临时储存这个字符串。

下面就是 main 函数的汇编块了:

从第 8 行到第 12 行,进行的操作就是初始化堆栈,为这个函数执行开辟适合的内存区域,其中的 rbp,rsp,edi 等都是寄存器,它们负责储存数据。

从第 13 行开始就进入了主要执行段:通过 mov 指令将字符串的值赋值给 edi,作为 printf 函数的第一个参数,之后传参完毕,通过 call 指令调用 printf 函数,输出字符串到屏幕;接着将 eax 赋值为 0,这里 eax 一般作为函数的返回值;之后 leave 然后 ret 返回,该函数就执行完毕了。

在这个过程中,我们可以看到一些常见的汇编指令:mov、sub、call、ret 等,下面来说说它们的作用吧。 mov 是赋值指令,它的格式为 mov 被赋值方, 要赋的值; sub 是递减指令,其格式为 sub 目标, 递减的值; call 为调用指令,其格式为 call 要调用的函数地址; ret 和 leave 连用是出栈指令,也即代表了一个函数的结束。通常在 masm 里,我们只会看到 retleave 并不常见。


了解了一些基本的汇编指令后,下面我们来看看 CTF 中常见的一些指令:

1、loop 循环指令:

mov rax,0
mov rcx,236
s:
add rax,123
loop s
leave
ret

上面的指令执行的是对 123 累加 236 次,从以上代码中我们可以看到,rcx 寄存器保存的是循环次数,loop 指令每执行一次,rcx 的数值就会减少 1,当其数值减少到 0 时,loop 指令就会停止,继续执行下面的指令。

2、无条件跳转指令:

lable:
mov edx,0

jmp lable

无条件跳转指令为 jmp,其意思从字面上就可以看出来,只要是执行到 jmp 这里,无论什么情况,都会直接跳转到 lable 的代码块中继续往下执行指令。

3、条件跳转:

lable:
mov ebx,1

cmp ebx,0
je lable

以上代码将 ebx 的值与 0 对比,如果相等,则会跳转到 lable 处,条件跳转指令 je 代表相等则跳转,还有其他与之条件不一样的条件跳转指令,一般条件跳转指令是与 cmptest 指令混在一起用的。以上代码也可以用 test 来写:

lable:
mov ebx,1

test ebx,0
jnz lable

test 是逻辑与指令,以上代表的即为 ebx&0jnz 代表标志位为 0 就跳转,其实现效果与 cmp 相同。

下面是一些常见的条件跳转指令:

  • 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)
  • JP 或 JPE:奇偶标志位为 1(Jump if Parity/Even)
  • JNP 或 JPO:奇偶标志位为 0(Jump if Not Parity/Odd)
  • JB 或 JNAE:以下标志位为 1(Jump if Below/Not Above or Equal)
  • JAE 或 JNB:以下标志位为 0(Jump if Above or Equal/Not Below)
  • JBE 或 JNA:以下标志位或零标志位为 1(Jump if Below or Equal/Not Above)
  • JA 或 JNBE:以下标志位和零标志位都为 0(Jump if Above/Not Below or Equal)
  • JO:溢出标志位为 1(Jump if Overflow)
  • JNO:溢出标志位为 0(Jump if Not Overflow)
  • JC 或 JB:进位标志位为 1(Jump if Carry/Not Below)
  • JNC 或 JAE:进位标志位为 0(Jump if Not Carry/Below or Equal)
  • JG 或 JNLE:大于(Jump if Greater/Not Less or Equal)
  • JGE 或 JNL:大于或等于(Jump if Greater or Equal/Not Less)
  • JL 或 JNGE:小于(Jump if Less/Not Greater or Equal)
  • JLE 或 JNG:小于或等于(Jump if Less or Equal/Not Greater)

下面是一些常见的位运算指令:

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

4、函数传参:

在汇编中,我们有时候需要知道参数被保存在那个寄存器里,这里我列举一般情况:

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

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

5、lea 地址加载:

其使用格式为 lea rdx,[my_var] 将 my_var 的地址而不是内容赋值给 rdx 寄存器。

6、xchg 数值交换指令:

其使用格式为 xchg ax,bx 将 ax 和 bx 的数值交换。

 

Quick Start:Example problems

下面是几道简单的 CTF 逆向 例题 在正式做题之前,我们要厘清解决 RE 题的大致思路。

1. 尝试运行程序,猜测程序的大致运行逻辑

例如 hit-plane.exe 这个文件,我们运行之后发现: 

是一个躲避掉落物的小游戏,用 WASD 可以上下左右移动 , 下面有一个 Score。我们就可以猜测 Score 达到一定分数的时候我们就能获得 flag。从而得到后面的思路。

2. 利用查壳工具查询附件的详细信息

运行分析之后,我们就要进入工具分析了。先用查壳工具打开文件。(这里以 DIE 为例) 

可以看到这是一个 32 位的 PE 文件,使用了 UPX 工具进行了打包。下面还有编译器、工具的相关信息。 这些信息在未来我们解题过程中都可能会有帮助。 (文件名和题目的信息也可能是解题相关的提示哦!)

3. 进行正式代码分析

获取初步的信息之后,我们就可以使用静态或动态分析工具开始对文件的源代码进行分析和解题了。 其中的技巧和难点非常多,这里先不展开。


Exp1:

拿到题目之后,从题目名字中的 "asm" 可以看出,这是一道考察 汇编语言 的题目,我们要有一定的汇编语言基础。下载下来可以看到给了我们一个 txt 文本,那么这里面一定是汇编块。打开之后确实是汇编块。

那么接下来就是 找到右上方的 (x) 用小手点击一下鼠标左键 哐哐一顿分析啦,详细的分解步骤就不多说了,这里涉及到汇编语言的知识,我简单解释一下它的逻辑:这段汇编代码的功能是对一个字符串(flag)进行加密处理,然后输出加密后的结果。具体的操作步骤如下:

首先,定义一个变量 var_4 ,用来存储字符串的索引,初始值为 0。

然后,进入一个循环,对字符串的每个字符进行两次异或和减法操作,具体如下:

  • 将 var_4 作为偏移量,从 flag 中取出一个字符,与十六进制数 0x1E 异或,得到新的字符。

  • 将新的字符存回 flag 中覆盖原来的字符。

  • 再次从 flag 中取出同一个字符,减去十六进制数 0x0A ,得到最终的加密字符。
  • 将最终的加密字符存回 flag 中覆盖原来的字符。
  • 将 var_4 加一,准备处理下一个字符。
  • 循环结束的条件是 var_4 等于十六进制数 0x27 ,也就是字符串的长度。
  • 最后,调用 printf 函数,输出加密后的字符串。

下面是我们对这段 ASM 加密算法翻译后的代码:

那么知道了加密的原理,下面就是解密啦,我们只需要把这段加密过程逆过来运算即可:

  • 将每一个字符都加上十六进制数 0x0A
  • 再将每一个字符都与十六进制数 0x1E 进行异或操作
  • 最后逐一储存在 result 中并返回原始字符串

将题目提供的加密后的字符串填进去并运行这段代码,即可输出正确的 flag :flag{It_is_als0_impor@nt_t0_13arn_4sm!}


Exp2:

将题目下载下来,发现这是一个可执行程序,那么我们要做的第一步就是查壳,确定程序的 PE 类型:(查壳图片略),查壳过后我们发现这是一个 x64 可执行程序,打开之后我们会发现他是直接让我们输入 flag 的:

那根据我们的直觉或者经验,可以看出,他肯定有一套验证 flag 是否正确的算法,我们直接拖进 IDA 看看之后发现,所有的验证代码全都在 Main函数 里,而且没有任何混淆:

从伪代码的这一部分分析可以知道,处理过后的字符串通过与 des 字符数组对比,判断是否相等,从而得出是否正确,那么 des 的字节就是我们需要的,我们双击 des 可以跳转到 des 的数据块,从这里可以看到 des 的所有字节数据:

我们直接从 HEX 进制窗口中复制这一段字节:

66 C6 16 76 B7 45 27 97 F5 47 03 F5 37 03 C6 67 33 F5 47 86 56 F5 26 96 E6 16 27 97 F5 07 27 03 26 C6 33 D6 D7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

代码段 Str[i] = (Str[i] >> 4) | (16 * Str[i]); 其实是位操作中的典型算法:位交换,即把每一个字节的高低位互换位置,那么如果我们想要得到原始数据,就需要把 des 的高低位互换回去,我们只需要重新调用一次这个算法即可:

将右图字节集转换为字符串即为 flag:flag{Try_t0_s0lv3_the_binary_pr0bl3m}


Exp3:

下载题目,发现是一个可执行程序,查壳发现是一个 32 位的可执行程序,运行,随便输入一个密钥,会输出给我们一份加密后的字符串,我们直接拖进 IDA 查看 Main函数

进入 IDA 后,我们发现,程序开始对 ArgList 已经赋值了,紧接着又对其部分位置的字符进行了修改,此处的代码编译器进行了优化,我们很难看出来他的执行逻辑,那么我们可以另辟蹊径,通过动态调试来拿到修改后的 ArgList 的数值,此处动态调试的过程不再展示。紧接着他又利用我们输入的那个密钥对 ArgList 进行了进一步的加密,然后输出了加密后的字符串。此处出题人其实是在迷惑我们,其实第一次处理后的 ArgList 就是用 Base64 编码后的字符串,我们直接动态调试出第一次处理后 ArgList 的字节集,然后用伪代码中出题人给的异或算法输出 Base64 编码后的字节集,将输出的字节集转换为字符串,并将字符串用 Base64 解码即可得到 flag:flag{a10e7ccc-b802-e3eb-c85940e226d}

 


Exp4:

下载题目,发现是一个可执行程序,查壳发现是一个 64 位的可执行程序,运行,啥玩意都没有,我们直接拖进 IDA 查看 Main函数,好了,flag 就在眼前:flag{flag1sinarray}


Exp5:

下载题目,发现是一个可执行程序,查壳发现是一个 64 位的可执行程序,运行,直接让我们输入 flag,我们直接拖进 IDA 查看 Main 函数,可以看到这里与 Exp1 是大同小异的,都是异或操作之后与内存中的一串字节 des 对比,我们直接逆运算即可得出来原本的输入数据,要注意这里是与取随机数之后的 v5 数组进行异或,我们要先模拟 v5 ,再进行异或。


Exp6:

下载题目,发现是一个 py文件,里面是关于对 flag 进行加密的算法,分析一下算法可知:

  • 要求输入 flag ,并且程序会检查输入的字符串是否是 42 个字符长。如果不是,程序将打印出 "Check your length!" 并且退出程序。
  • 如果输入的字符串长度符合要求,程序将进入一个循环。在这个循环中,输入的字符串被分成 6 个部分,每个部分包含 7 个字符。然后将每个部分中的字符转换成对应的 ASCII 码,再转换成十六进制的形式。这些十六进制数值会被存储在列表 l 中。
  • 接下来的部分是一系列的条件判断。程序会检查列表 l 中的值是否满足一组复杂的方程式,其中每个方程式的右边都是一个十六进制数。如果所有的条件都满足,程序会打印 "Good job!",否则打印 "Wrong/nTry again!!!" 并退出程序。

其实到了这一步,看到了解方程这里,这道题目就很简单了,我们直接调用 python 的 sympy库 对方程进行求解,即可得到原始 flag 的数据,下面是简单的 python代码 :

运行后即可得到原始的 flag 字符串,即为:flag{N0_One_kn0ws_m@th_B3tter_Th@n_me!!!!}


Exp7:

下载题目,发现是一个可执行文件,打开之后需要我们输入 Key ,Key 的范围是 0-100 ,我直接用爆破的方式了,解出来 key 是 23,输入之后,下面让我们输入 flag,我们将其拖入 DIE 查看,会发现他的 Packer 是 PyInstaller,从这里可以判断这个是用 Py 打包的方式生成的可执行文件:

那么我们直接调用 pyinstxtractor 对其进行解包,拿到其中的 python 脚本文件 (.pyc) ,此程序的主要脚本是 main.pyc ,然后再使用 python 字节码反汇编工具 pycdc 得到正常的 python 源代码:

反汇编出来 python 源代码之后,我们要注意的是:我们需要配置与他的源代码一致的 python 环境,从反汇编的信息上来看,这个脚本是 python3.8 的版本,我们配置完环境之后直接调试这段代码即可,我们在图中指向的地方下断点,这里的主要目的是为了拿到他处理后的执行代码,flag 的字节就在里面。

断下来之后,我们查看输出窗口,找到 co_consts ,展开就可以看到与 flag 有关的信息,从中我们可以看到,flag 是一个用 rc4算法 加密之后的字符串,并且加密时用的 key 也显示了出来。程序用 rc4算法 对你输入的 flag 进行加密,验证是否与加密后的 flag 相等,如果相等,就会输出 yes,否则输出 no,那么到了这里我们直接调用 rc4 的解密算法来对 flag 进行解密即可。

运行以上代码,即可得到 flag:flag{d8e8d9d0-b2b1-7304-74b760-90b11ab6a3}


以上是 7 道基础的逆向题目,其中涉及到了很多知识点,包括但不限于 Python 语言、汇编语言、C++,以及 IDA 的使用、查壳工具的使用,还有 Python 解包工具等。这些东西需要我们日复一日的积累,熟能生巧,才可以熟记于心中。在需要的时候把它们拿出来用。

 

从零开始的 IDA

对于 re 手来说,最基础和最重要的工具就是我们的 老婆 IDA 了。

IDA Pro(Interactive Disassembler Professional)简称“IDA”,是 Hex-Rays 公司出品的一款交互式反汇编工具,是目前最棒的静态反编译软件~之一~。

随意打开一个文件,我们可以看到主界面 

Function

左边的 Function 视窗,我们可以看到程序内识别出的所有函数。 Ctrl+F 还可以搜索我们想找的函数 

IDA View-A

右侧 IDA View-A 我们可以看到反汇编得到的汇编代码,以及控制流。

按空格键可以切换 “平铺” 和 “控制流” 两种模式。

平铺模式可以让我们看到所有代码在内存中保存的位置、顺序。

控制流模式可以让我们清晰地看到程序控制流的变化。

IDA View-A 下的其他快捷键:

  • G:指定地址跳转

  • P:解析函数

  • C:将数据转化为代码

  • D:将代码转化为数据

  • Shift+E:提取数据

在 IDA View-A 视窗下按 Tab 或 F5 就可以自动将汇编代码翻译成 ** 伪 C 语言代码 **。(也就是传说中的 “万能 F5” ) 伪 C 代码可以近似当做 C 语言代码阅读,只是有些地方语法不一定准确,也会受到源程序编码语言的影响 (如 Rust 语言反编译出的依托答辩) 

Pseudocode-A

Pseudocode-A 视窗下的快捷键: - /:添加注释 - \:隐藏类型描述 - Y:更改变量类型 - R:将选中的数字转换为相应的 ASCII 字符 - X:查看函数的交叉引用 - N:更改变量名称

String

除此之外

  • Shift+F12:打开 String 界面,查看字符串 

至此,你就大致掌握了逆向工程最基础的静态分析工具!

 

概念

“壳”,顾名思义,是程序外面的 “保护层”,主要分为压缩壳和加密壳两种。比赛中常见的一般是压缩壳,它在程序中加入一些代码隐藏程序真正的入口,使其难以被反编译。

查壳

软件:EXEInfoPE、PEID、StudyPE+、DIE 等

它们的使用都差不多,下面以 DIE 为例: 

我们可以看到程序使用的操作系统位数、加壳的情况等等。

脱壳

既然知道了如何查壳,那么如何脱去这层烦人的外衣呢? 这里分为两种脱壳方法:

  • 工具脱壳:用软件脱壳,方便快捷,只不过能解决的壳有限,不一定能解决魔改壳。

  • 手动脱壳:用 xdbg、OD 等动态调试软件脱壳,难度高,但通杀。

以最常见的 UPX 壳举例: - 工具脱壳:使用官方工具 upx.exe,使用命令即为 upx.exe -d <文件(加后缀)>

  • 手动脱壳:
    • 32 位:OllyDbg 脱壳(较简单,可参考网上大量教程)
    • 64 位:x64dbg 示例为 [SWPUCTF 2022 新生赛]upx 中的附件(64 位,UPX3.96 壳),可以用工具脱壳。下面尝试手动脱壳。

XP 后的系统都有 ASLR(地址空间随机化),导致 dump 后程序运行出错,因此我们首先用 CFF Explorer 修改该文件的 Nt Header,禁用 ASLR

在 Nt Header 下的 Optional Header 里修改 Characteristics,勾选 Relocation info srtipped from file。关闭后记得保存。
用 x64dbg 打开文件,进入系统断点(push rbx)。

按 F9 到达该断点(这里要按两次,上面还有个别的什么断点)
F7 走完 push 压栈部分

观察到 RSP 的变化,在其上右键“在内存窗口中转到”

在右下角该地址右键,设置硬件断点

F9 运行到断点处,看到下面的 jmp 大跳应该是入口,设置断点,F9 跳过去

F7 步入程序

往下翻可以看到提示字符串
这就正式进入了原程序,可以进行 dump 了

先点“IAT Autosearch”,再点“Get Imports”,在“Imports”中删除掉带有红色叉叉的,再点击“Dump”,之后“Fix Dump”选中之前的 Dump 文件,修复成功。

p04 副本 _dump_SCY.exe 也可以正常运行
拖入 IDA 中,这里会报错,Yes 不用管它

(由于某种神秘力量?)没有 main 函数,只能通过字符串来找函数

X 键找一下位置

可以看到大致的代码
如果想把字符串展开看,可以(退到 IDA View-A 中)修改 Segment,取消 w 勾选

修复后

显然 sub_140001540 是 printf 函数,脱壳结束

干扰

虽然我们有脱壳的各种方法,但是出题人并不总是想让你顺利地解出答案。

魔改 UPX 头

让我们以 NCTF2023 的中文编程 2 为例:

当我们查壳时发现 UPX 的版本号被抹去,那就是文件头被魔改了。这时候手动脱壳依然是有效的,不过有时候太麻烦,所以我们也可以手动把 UPX 头改回来。

用 010Editor 打开文件:

发现字符串提示,这些地方应该是被改了。 对比一下正常的 upx 文件:

分别是 UPX0、UPX1、UPX! 改一下(注意在左边改,在右边改会把后面的.覆盖掉)

版本号出来了,可以正常脱壳

成功了!我的朋友!

 

========= End

 

posted @ 2024-12-16 21:30  lsgxeva  阅读(200)  评论(0编辑  收藏  举报