【NOTES.0xFF】一些暂时没地方堆放的零碎知识点
本文最后更新于:2023年8月13日 晚上
写完笔记就永远不会看了(悲)
0x00.一切开始之前
在笔者的学习过程中学到了各种各样的知识,其中不乏成体系的或是零碎的知识,比较成体系的知识点笔者倒是容易整合成一篇博客记录下来,但是那些零碎的不知如何分类的知识点却不好也跟着单开一篇新的博客,但似乎又没有地方存放,于是笔者选择在此处新开一篇博文,专门用来存放这些较为零碎的知识点,待到其中某一类积攒了足够数量之后,笔者再将之取出整合成一篇新的博客
当然,由于笔者实际上非常懒,这篇博客可能基本上不会堆太多东西(笑)
0x01. 通过修改 PE Header 完成恶意 DLL 的装载
大三上学期“软件逆向工程”这门课程的一个上机作业,因为笔者以前主要还是 hacking on Linux 所以通过这次实验也学到很多东西…
一、PE Header 修改思路
IMPORT Directory Table 修改思路
我们首先使用 PEview 来分析 notepad.exe
文件,将可执行文件拖入 PEview 中,查看其 IMPORT Directory Table
,一个可执行文件在执行时需要加载的 DLL 文件都在其中进行描述
我们需要向导入表中插入一个新的项,从而让程序加载我们的 DLL 文件,但这一块附近并没有多余的空间来插入我们自己的 DLL 文件(IDT 以一个全空的导入表项结尾,为了注入的稳定性,我们不能够覆盖后面的合法数据),因此我们需要把导入表给移动到一个合适的足够大的位置,在那里再在末尾添加我们的新的 IMAGE_IMPORT_DESCRIPTOR
结构
接下来我们需要寻找该 PE 文件中是否有一个足够合适的位置
迁移位置:SECTION .reloc
现在让我们将目光放到 SECTION .reloc
上,我们可以注意到其末尾是没有数据的
使用 010 Editor 打开 notepad.exe 文件,我们发现在 .reloc 节区的末尾有着一大块的空白,这为我们移动导入表提供了可能——我们可以将导入表移动到 .reloc 节区的末尾
IMPORT Directory Table 的 RAW 为 0x94A0
,有效数据一直到 0x95CC
,这里我们将其直接整个复制到 .reloc
节末尾
虽然在 PEview 中 .reloc 节有数据的位置只到 0x2BC36
,但保险起见,同时也为了方便计算,笔者选择从 0x2BC50
这个位置开始写入
添加新导入表项
接下来我们需要在移动的导入表后添加一个新的导入表项,从而让我们的恶意 DLL 文件能够随着程序的启动被顺利载入内存中
空间布局
我们需要添加一个 IMAGE_IMPORT_DESCRIPTOR
结构体,其定义如下:
1 |
|
其中的 TimeDateStamp
与 ForwarderChain
字段的值都可以忽略,这里我们参照该 PE 文件中其他的 IDT 项设为 0xFFFFFFFF
Name
字段应当指向存放该 DLL 名字字符串的 RVA 地址
OriginalFirstThunk
字段应当指向_导入名称表(INT)_,为一个IMAGE_THUNK_DATA 结构体数组,其定义如下:
1 |
|
该结构体只占一个 DWORD,其内只有一个联合体,对于 INT 而言其使用的是 AddressOfData
字段,存储的是IMAGE_IMPORT_BY_NAME
数组的 RVA,其结构体定义如下:
1 |
|
其中 Hint 字段表示函数在其原始 DLL 文件的输出表中的序号;Name 字段为函数的名称字符串,以NULL字符结尾;这里我们只需要 dll 文件开头的 dummy()
,因此 Hint 字段值为 0
,Name 字段为 dummy
字符串
FirstThunk
则应当指向_导入地址表(IAT)_,同样是一个 IMAGE_THUNK_DATA
结构体数组,其中存放的是函数的地址,这个值由 PE loader 来完成修改,这里笔者直接设为 0xFFFFFFFF
需要注意的是 INT 与 IAT 的末尾同样需要一个 NULL 表项作为结尾标识
这里笔者选择将这两个表项同样布置在 .reloc 节区的末尾
在这里由于剩余空间足够多,且为了方便计算地址,笔者选择尽量按照 0x10
进行对齐,初步的布局如下图所示
地址计算
按照这个布局,我们进行相应的 RVA 的计算:
.reloc RVA & RAW
我们首先需要获得 SECTION .reloc
的 RVA,查看 PEview 中该节区的 IMAGE_SECTION_HEADER
字段,如下:
获得该节区的 RVA 为 0x2F000
查看 PEview,得到该节区的 RAW 为 0X2AE00
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}
$$
注入后空间布局
完成新导入表项的添加之后的布局如下,这里我们需要注意的是数据存放的形式为小端序
SECTION 权限修改
这里我们需要注意的一点是,在运行过程中 PE loader 会动态地修改 IAT ,因此我们还需要修改节区的 header 为 .reloc
节区添加可写权限
我们只需要为 Characteristics
字段加上写权限对应的值 IMAGE_SCN_MEM_WRITE
即可,即加上 0x80000000
,最终该字段的值为 0xC2000040
修改 IMAGE_OPTIONAL_HEADER
完成新的导入表的布置之后,我们还需要修改 PE Header 中的 IMAGE_OPTIONAL_HEADER
修改 IMPORT Table 的 RVA 与 SIZE
我们需要修改 PE Header 中的 IMAGE_OPTIONAL_HEADER 项中存储的导入表的 RVA 修改为我们的新导入表的 RVA
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 即可
修改 BOUND IMPORT Table
若我们需要正常导入 MyDll3.dll,则还需要向绑定导入表中添加信息
但实际上绑定导入表可以不存在,因此我们只需要将其 RVA 置 0 即可
二、测试修改后可执行文件
接下来我们在虚拟机中测试我们的修改后的可执行文件,观察其是否成功载入我们的 MyDll3.dll
文件并将网页下载
我们使用 Process Explorer
查看 patch 后的 notepad 文件所载入的 DLL,在这里我们可以清晰地看到,我们的 MyDll3.dll
文件被成功载入
观察可执行文件所在目录(笔者放到了桌面),可以看到 index.html
被成功下载到了桌面上,本次实验圆满结束
0x02. IDA 修复 switch 生成跳表的方式
PRE.什么是跳表(jump table)?
跳转表是 编译器对 switch 结构进行优化后生成的跳转结构:对于有着多个分支的 switch 而言,编译器会将其优化成一个特殊的结构——跳表(jump table)
例如如下代码是一个十分典型的多条件 switch 结构:
1 |
|
在 case 较少时,编译器默认生成的就是常规的逐一比较的代码,但是当 case 达到一定数量(好像是五个,笔者也忘了),编译器就会选择生成“跳表”这一结构,它会将条件跳转的 多个结果代码块放置在连续的一段内存中,将每个块的偏移放置在 .rodata
段 ,形成一张表
在 switch 需要根据条件进行跳转时,会对这张表进行索引,取出跳转目标的对应偏移,加上条件跳转代码块的基址,就形成了最后的跳转地址
使用 IDA 修复跳表
当我们使用 IDA 进行逆向时,遇到编译器优化 switch 生成的跳表,通常 IDA 是无法直接识别的,这个时候就需要我们手动去完成跳表的识别
首先在对应的代码位置按下 tab
,来到原始的跳转代码处,注意分辨几个关键代码的作用
之后将光标放在取跳表值的指令 (图上的 lea rdx, ds:0[rax * 4]
, 单个跳表项4字节),IDA 导航栏选中 Edit->Other->Specify switch idiom...
,进行如下设置
之后重新 F5,跳表就修好了,我们便能在 IDA 中看到正常的 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++ 为了实现多态而使用的一种技术,
To be 🕊🕊🕊…