【NOTES.0xFF】一些暂时没地方堆放的零碎知识点

本文最后更新于:2022年4月28日 下午

写完笔记就永远不会看了(悲)

0x00.一切开始之前

在笔者的学习过程中学到了各种各样的知识,其中不乏成体系的或是零碎的知识,比较成体系的知识点笔者倒是容易整合成一篇博客记录下来,但是那些零碎的不知如何分类的知识点却不好也跟着单开一篇新的博客,但似乎又没有地方存放,于是笔者选择在此处新开一篇博文,专门用来存放这些较为零碎的知识点,待到其中某一类积攒了足够数量之后,笔者再将之取出整合成一篇新的博客

当然,由于笔者实际上非常懒,这篇博客可能基本上不会堆太多东西(笑)

0x01. 通过修改 PE Header 完成恶意 DLL 的装载

大三上学期“软件逆向工程”这门课程的一个上机作业,因为笔者以前主要还是 hacking on Linux 所以通过这次实验也学到很多东西…

一、PE Header 修改思路

IMPORT Directory Table 修改思路

我们首先使用 PEview 来分析 notepad.exe 文件,将可执行文件拖入 PEview 中,查看其 IMPORT Directory Table,一个可执行文件在执行时需要加载的 DLL 文件都在其中进行描述

image.png

我们需要向导入表中插入一个新的项,从而让程序加载我们的 DLL 文件,但这一块附近并没有多余的空间来插入我们自己的 DLL 文件(IDT 以一个全空的导入表项结尾,为了注入的稳定性,我们不能够覆盖后面的合法数据),因此我们需要把导入表给移动到一个合适的足够大的位置,在那里再在末尾添加我们的新的 IMAGE_IMPORT_DESCRIPTOR 结构

接下来我们需要寻找该 PE 文件中是否有一个足够合适的位置

迁移位置:SECTION .reloc

现在让我们将目光放到 SECTION .reloc 上,我们可以注意到其末尾是没有数据的

image.png

使用 010 Editor 打开 notepad.exe 文件,我们发现在 .reloc 节区的末尾有着一大块的空白,这为我们移动导入表提供了可能——我们可以将导入表移动到 .reloc 节区的末尾

image.png

IMPORT Directory Table 的 RAW 为 0x94A0,有效数据一直到 0x95CC,这里我们将其直接整个复制到 .reloc 节末尾

image.png

虽然在 PEview 中 .reloc 节有数据的位置只到 0x2BC36,但保险起见,同时也为了方便计算,笔者选择从 0x2BC50 这个位置开始写入

image.png

添加新导入表项

接下来我们需要在移动的导入表后添加一个新的导入表项,从而让我们的恶意 DLL 文件能够随着程序的启动被顺利载入内存中

空间布局

我们需要添加一个 IMAGE_IMPORT_DESCRIPTOR 结构体,其定义如下:

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

其中的 TimeDateStamp ForwarderChain 字段的值都可以忽略,这里我们参照该 PE 文件中其他的 IDT 项设为 0xFFFFFFFF

Name 字段应当指向存放该 DLL 名字字符串的 RVA 地址

OriginalFirstThunk 字段应当指向_导入名称表(INT)_,为一个IMAGE_THUNK_DATA 结构体数组,其定义如下:

1
2
3
4
5
6
7
8
typedef struct _IMAGE_THUNK_DATA32 {  
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;

该结构体只占一个 DWORD,其内只有一个联合体,对于 INT 而言其使用的是 AddressOfData 字段,存储的是IMAGE_IMPORT_BY_NAME 数组的 RVA,其结构体定义如下:

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

其中 Hint 字段表示函数在其原始 DLL 文件的输出表中的序号;Name 字段为函数的名称字符串,以NULL字符结尾;这里我们只需要 dll 文件开头的 dummy() ,因此 Hint 字段值为 0,Name 字段为 dummy 字符串

FirstThunk 则应当指向_导入地址表(IAT)_,同样是一个 IMAGE_THUNK_DATA 结构体数组,其中存放的是函数的地址,这个值由 PE loader 来完成修改,这里笔者直接设为 0xFFFFFFFF

需要注意的是 INT 与 IAT 的末尾同样需要一个 NULL 表项作为结尾标识

这里笔者选择将这两个表项同样布置在 .reloc 节区的末尾

在这里由于剩余空间足够多,且为了方便计算地址,笔者选择尽量按照 0x10 进行对齐,初步的布局如下图所示

image.png

地址计算

按照这个布局,我们进行相应的 RVA 的计算:

.reloc RVA & RAW

我们首先需要获得 SECTION .reloc 的 RVA,查看 PEview 中该节区的 IMAGE_SECTION_HEADER 字段,如下:

image.png

获得该节区的 RVA 为 0x2F000

查看 PEview,得到该节区的 RAW 为 0X2AE00

image.png

Name RVA

字符串 MyDll3.dll 被存放在 RAW 0x2BD90 处,计算得到其 RVA 应为:
$$
\begin{split}
RVA_{Name} &= RAW_{Name} - RAW_{SECTION.reloc} + RVA_{SECTION.reloc}\
&= 0x2BD90 - 0x2AE00 + 0x2F000\
&= 0x2FF90
\end{split}
$$

INT RVA

INT 的 RAW 为 0x2BDB0 ,计算得到其 RVA 应为:
$$
\begin{split}
RVA_{INT} &= RAW_{INT} - RAW_{SECTION.reloc} + RVA_{SECTION.reloc}\
&= 0x2BDB0 - 0x2AE00 + 0x2F000\
&= 0x2FFB0
\end{split}
$$

IAT RVA

IAT 的 RAW 为 0x2BDC0 ,计算得到其 RVA 应为:
$$
\begin{split}
RVA_{IAT} &= RAW_{IAT} - RAW_{SECTION.reloc} + RVA_{SECTION.reloc}\
&= 0x2BDC0 - 0x2AE00 + 0x2F000\
&= 0x2FFC0
\end{split}
$$

dummy 项 RVA

该结构的 RAW 为 0x2BDA0,计算得到其 RVA 应为:
$$
\begin{split}
RVA_{dummy} &= RAW_{dummy} - RAW_{SECTION.reloc} + RVA_{SECTION.reloc}\
&= 0x2BDA0 - 0x2AE00 + 0x2F000\
&= 0x2FFA0
\end{split}
$$

注入后空间布局

完成新导入表项的添加之后的布局如下,这里我们需要注意的是数据存放的形式为小端序

image.png

SECTION 权限修改

这里我们需要注意的一点是,在运行过程中 PE loader 会动态地修改 IAT ,因此我们还需要修改节区的 header 为 .reloc 节区添加可写权限

image.png

我们只需要为 Characteristics 字段加上写权限对应的值 IMAGE_SCN_MEM_WRITE 即可,即加上 0x80000000,最终该字段的值为 0xC2000040

image.png

修改 IMAGE_OPTIONAL_HEADER

完成新的导入表的布置之后,我们还需要修改 PE Header 中的 IMAGE_OPTIONAL_HEADER

修改 IMPORT Table 的 RVA 与 SIZE

我们需要修改 PE Header 中的 IMAGE_OPTIONAL_HEADER 项中存储的导入表的 RVA 修改为我们的新导入表的 RVA

image.png

IDT 的 RAW 为 0x2BC50 ,计算得到其 RVA 应为:
$$
\begin{split}
RVA_{IDT} &= RAW_{IDT} - RAW_{SECTION.reloc} + RVA_{SECTION.reloc}\
&= 0x2BC50 - 0x2AE00 + 0x2F000\
&= 0x2FE50
\end{split}
$$
除了 RVA 以外,由于我们添加了新的表项, size 也需要进行修改,加上一个表项的大小 5 * 4 = 20 即可

image.png

修改 BOUND IMPORT Table

若我们需要正常导入 MyDll3.dll,则还需要向绑定导入表中添加信息

image.png

但实际上绑定导入表可以不存在,因此我们只需要将其 RVA 置 0 即可

image.png

二、测试修改后可执行文件

接下来我们在虚拟机中测试我们的修改后的可执行文件,观察其是否成功载入我们的 MyDll3.dll 文件并将网页下载

我们使用 Process Explorer 查看 patch 后的 notepad 文件所载入的 DLL,在这里我们可以清晰地看到,我们的 MyDll3.dll 文件被成功载入

image.png

观察可执行文件所在目录(笔者放到了桌面),可以看到 index.html 被成功下载到了桌面上,本次实验圆满结束

image.png

0x02. IDA 修复 switch 生成跳表的方式

PRE.什么是跳表(jump table)?

跳转表是 编译器对 switch 结构进行优化后生成的跳转结构:对于有着多个分支的 switch 而言,编译器会将其优化成一个特殊的结构——跳表(jump table)

例如如下代码是一个十分典型的多条件 switch 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int condition
switch(condition)
{
case condition1:
statements;
break;
case condition2:
statements;
break;
case condition3:
statements;
break;
//....
case conditionn:
statements;
break;
default:
default_statment;
break;
}

在 case 较少时,编译器默认生成的就是常规的逐一比较的代码,但是当 case 达到一定数量(好像是五个,笔者也忘了),编译器就会选择生成“跳表”这一结构,它会将条件跳转的 多个结果代码块放置在连续的一段内存中,将每个块的偏移放置在 .rodata ,形成一张表

在 switch 需要根据条件进行跳转时,会对这张表进行索引,取出跳转目标的对应偏移,加上条件跳转代码块的基址,就形成了最后的跳转地址

使用 IDA 修复跳表

当我们使用 IDA 进行逆向时,遇到编译器优化 switch 生成的跳表,通常 IDA 是无法直接识别的,这个时候就需要我们手动去完成跳表的识别

只有一个 jmp rax

首先在对应的代码位置按下 tab,来到原始的跳转代码处,注意分辨几个关键代码的作用

之后将光标放在取跳表值的指令 (图上的 lea rdx, ds:0[rax * 4], 单个跳表项4字节),IDA 导航栏选中 Edit->Other->Specify switch idiom...,进行如下设置

修复跳转表

之后重新 F5,跳表就修好了,我们便能在 IDA 中看到正常的 switch 结构

修复后的 switch

0x03. 常见函数调用约定与传参方式

一、x86

__cdecl(Linux)

参数从右向左入栈,调用者完成清栈,返回值存放在 eax 寄存器中

__stdcall(WinAPI)

参数从右向左入栈,被调用者完成清栈,返回值存放在 eax 寄存器中

__fastcall

前两个参数通过寄存器(ecx、edx)传递,剩余参数从右向左入栈,被调用者完成清栈,返回值存放在 eax 寄存器中

thiscall(MS C++)

微软的 C++ 类成员调用约定,参数从右向左入栈,this 指针 存放在 ecx 寄存器中,被调用者完成清栈,返回值存放在 eax 寄存器中

二、x64

__fastcall(WinAPI)

前四个参数通过寄存器传递(rcx、rdx、r8、r9),剩余参数从右向左入栈,被调用者完成清栈,返回值存放在 rax 寄存器中

thiscall(MS C++)

微软的 C++ 类成员调用约定,this 指针 存放在 rcx 寄存器中,前三个参数通过寄存器传递(rdx、r8、r9),剩余参数从右向左入栈,被调用者完成清栈,返回值存放在 rax 寄存器中

__fastcall (System V AMD64 ABI)

Linux 遵循的标准叫 System V AMD64 ABI 调用约定,主要在 Solaris,GNU/Linux,FreeBSD 和其他非微软 OS 上使用

  • 前六个参数通过寄存器传递(rdi、rsi、rdx、rcx、r8、r9),剩余参数从右向左入栈,调用者完成清栈,返回值存放在 rax 寄存器中

  • 对于长度 8 字节内的参数,可以直接使用寄存器进行传递,对于大于 8 字节的参数(如结构体),则选择通过寄存器传递指针

  • 对于长度为 128 位的返回值,其低 64 位存放在 rax 中,高 64 位存放在 rdx 中

  • 浮点数:通过 xmm0~xmm7 寄存器传参,浮点数返回值存放在 xmm0 寄存器中

  • (Linux)返回值为结构体:由调用者在栈上开辟,通过 rcx 传递指向该空间的指针,这种情况下只有 5 个寄存器用来传参,剩余参数通过栈传递,之后会将结构体存放在该空间中,rax 中存放着指向该空间的指针

0x04. Linux 系统调用传参方式

32 位

Linux 下 32 位系统调用通过 0x80 号中断 实现

传参约定:

  • eax:系统调用号
  • ebx、ecx、edx、esi、edi、ebp:第一到第六个参数

返回值存放在 eax 寄存器中

64位

Linux 下 64 位系统调用通过 syscall 指令实现,但在兼容 32 位的情况下仍保留了 0x80 号中断 作为 32位程序的系统调用入口

传参约定:

  • rax:系统调用号
  • rdi、rsi、rdx、r10、r8、r9:第一到第六个参数

返回值存放在 rax 寄存器中

0x05. C++ 的虚表

虚函数表是 C++ 为了实现多态而使用的一种技术,


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!