-
-
[原创]UPX加载逻辑的处理细节分析
-
发表于: 2024-10-18 11:11 6484
-
在本人第一篇博客UPX代码buildloader函数分析,对加载器初始化过程阐明了不同条件下的加载逻辑,本文根据其加载逻辑进行更细致的分析其操作细节。
首先在文件src\stub\src\amd64-win64.pe.S中发现PE文件加壳入口点:.intel_syntax noprefix
是用于汇编语言的编译器指令,主要用于告诉汇编器使用 Intel 语法 来解析接下来的代码,并且在操作数之前不加任何前缀(noprefix
)。
接下来按照加载逻辑的添加顺序分别分析其处理细节:
在 Windows的 x64调用约定中,rcx是第一个参数,rdx是第二个参数,r8是第三个参数。因此在函数调用前保存参数。
此处处理过程与 PEISDLL0类似,保存了 rcx和 rdx 的内容,但这里的压栈方式不同,是直接将它们压入栈顶。可能是为了适应 EFI 系统环境的不同调用约定。
如果是dll文件,在主程序入口前还需要添加初始化逻辑,判断是否是dll文件的卸载操作。 在 Windows 系统中,dl 在 DLL 入口点(DllMain 函数)中传递给 DLL 的 fdwReason 参数,值为 1 时表示 DLL 正在被卸载(DLL_PROCESS_DETACH)
。在此处如果值并不为1(不是卸载操作),则程序将跳转到 reloc_end_jmp
标签,继续正常的初始化流程。
这一部分代码是为压缩数据解压作准备的主要逻辑。将压缩后的数据解压到内存中的指定位置,然后继续执行原始程序代码。
a) 首先保存寄存器rbx、rsi、rdi 和 rbp的值。
b) lea rsi, [rip + start_of_compressed]
计算压缩数据的起始地址,并存入 rsi 寄存器,其中rip 是当前指令地址。
c) lea rdi, [rsi + start_of_uncompressed]
则计算未压缩数据的起始地址,存入 rdi 寄存器。
最后rsi 指向压缩数据,而 rdi 指向解压后的数据。
处理 TLS 相关的初始化操作,包括保存和恢复被 TLS 索引覆盖的数据,确保数据正确性。
LZMA_HEAD 初始化了解压缩参数,如压缩和解压数据的长度、内存位置等。
LZMA_TAIL 则是清理操作,负责恢复栈并弹出数据,标志着解压过程的结束。
其中的regs.h头文件保存了对寄存器的相关信息
lzma_d.S文件则是其中解压缩的算法实现文件,主要涉及解码和处理 LZMA 压缩数据的逻辑,对应添加指令LZMA_ELF00,LZMA_DEC20
在调用解密函数前,进行了调用约定、压缩类型检查、解码器初始化以及堆栈分配对齐。最后根据条件编译,选择不同的文件引入解码函数。
这里其中lzma_d_cs.S、lzma_d_cf.S以及lzma_d_cn.S都是由汇编机器码组成的汇编文件。
NRV算法对应有2B、2D以及2E,这里以2E为例,即执行指令NRV2E。可见指令同样引入文件nrv2e_d.S,这里对解压缩主要流程进行分析。
1. 字节处理
从 rsi(源指针)处获取下一个字节,并存储到 %dl 中。这一步推测该字节是字面值(literal)还是偏移值的一部分。然后根据寄存器的数据,跳转到 lit_n2e 处理字面值数据。
如果判定当前字节是字面值数据,则将其存储到目标位置 %rdi,并递增源和目标地址指针,准备处理下一个字节。
2. 偏移与长度计算
处理偏移值(off),首先从输入数据中获取偏移值的高位字节。
调整偏移值,判断是否需要跨越多个字节计算。如果偏移值满足某些条件(例如低位较小),会跳转到 offprev_n2e。
off 最终得到的是需要从目标位置向后移动的距离,它决定了从哪里复制数据。
从源数据中获取解压数据块的长度,利用 getnextb 函数从输入流中读取长度值。
3. 数据复制
根据前面解析出来的偏移值和长度,调用 copy 函数,复制解压出来的数据块到目标位置。这里的 adcl 指令根据偏移的大小,调整解压出的数据块长度。
NRV2E 压缩格式的解压流程:通过多次从压缩数据中读取字节或位,代码能够逐步解析出偏移和长度信息,随后将数据块从先前的位置复制到新的位置,完成解压缩过程。
分配栈空间,并将 compressed_imports 加载到 rdi 中,准备开始解析导入表。
读取dll的名称地址,如果名称为空(eax=0),则跳转到imports_done结束导入表的修复。否则,继续准备加载dll的导入表,调用系统API函数LoadLibraryA加载dll,并将结果保存在rbp中。
从 rdi 读取当前导入函数的标识符。如果标识符为 0,说明所有函数已处理完毕,跳转到 next_dll 处理下一个 DLL。
如果当前导入函数属于 kernel32.dll,则根据序号查找该函数的地址。
如果函数按名称导入,使用 GetProcAddress 来获取函数地址。这里先通过 repne scasb 搜索字符串(函数名称),然后调用 GetProcAddress 获取函数的内存地址。
将获取到的函数地址存储到 IAT 中,并继续处理下一个函数。
如果导入失败,则清理栈空间,返回 eax = 0 表示失败。
这段代码动态解析并加载 PE 文件的导入表,加载所需的 DLL 并获取函数地址,完成导入表修复的任务。
首先将重定位表地址初始化。然后每次从重定位表读取一个字节并检查其值。如果为 0x00,表示重定位表处理完毕,跳转到 reloc_endx 结束处理。如果字节值大于 0xEF,则跳转到 reloc_fx 处理其他类型的重定位项。
将偏移量加到 rbx,然后从 rbx 地址加载目标地址(目标地址是反向字节序,因此使用 bswap 交换字节顺序)。接着,将目标地址加上基址 rsi,以便修正地址引用,并将结果存回原位置。
对于特殊的重定位项,使用低 4 位并移位来计算偏移。然后从 rdi 中读取额外的 2 个字节作为偏移量,进行地址修正。
处理低位重定位,将 reloc_delt 加到目标地址中,并使用循环结构逐个应用。
类似低位重定位的处理逻辑,不过这里处理的是高 16 位的重定位,修正高位地址。
这段代码实现 PE 文件中的重定位表修复,遍历重定位表中的各个项,并对内存中的地址进行修正。这可以确保当程序被加载到不同的内存地址时,所有地址引用都能被正确调整。
section PEISDLL0
mov [rsp
+
8
], rcx
mov [rsp
+
0x10
], rdx
mov [rsp
+
0x18
], r8
section PEISDLL0
mov [rsp
+
8
], rcx
mov [rsp
+
0x10
], rdx
mov [rsp
+
0x18
], r8
section PEISEFI0
push rcx
push rdx
section PEISEFI0
push rcx
push rdx
section PEISDLL1
cmp
dl,
1
jnz reloc_end_jmp
section PEISDLL1
cmp
dl,
1
jnz reloc_end_jmp
section PEMAIN01
/
/
; remember to keep stack aligned!
push rbx
push rsi
push rdi
push rbp
lea rsi, [rip
+
start_of_compressed]
lea rdi, [rsi
+
start_of_uncompressed]
section PEMAIN01
/
/
; remember to keep stack aligned!
push rbx
push rsi
push rdi
push rbp
lea rsi, [rip
+
start_of_compressed]
lea rdi, [rsi
+
start_of_uncompressed]
section PEICONS1
incw [rdi
+
icon_offset]
section PEICONS2
add [rdi
+
icon_offset], IMM16(icon_delta)
section PEICONS1
incw [rdi
+
icon_offset]
section PEICONS2
add [rdi
+
icon_offset], IMM16(icon_delta)
section PETLSHAK
lea rax, [rdi
+
tls_address]
push [rax]
/
/
save the TLS index
mov [rax], IMM32(tls_value)
/
/
restore compressed data overwritten by the TLS index
push rax
section PETLSHAK
lea rax, [rdi
+
tls_address]
push [rax]
/
/
save the TLS index
mov [rax], IMM32(tls_value)
/
/
restore compressed data overwritten by the TLS index
push rax
.intel_syntax noprefix
section LZMA_HEAD
mov eax, IMM32(lzma_u_len)
push rax
mov rcx, rsp
mov rdx, rdi
mov rdi, rsi
mov esi, IMM32(lzma_c_len)
.att_syntax
#define NO_RED_ZONE
#include "arch/amd64/regs.h"
#include "arch/amd64/lzma_d.S"
.intel_syntax noprefix
section LZMA_TAIL
leave
pop rax
.intel_syntax noprefix
section LZMA_HEAD
mov eax, IMM32(lzma_u_len)
push rax
mov rcx, rsp
mov rdx, rdi
mov rdi, rsi
mov esi, IMM32(lzma_c_len)
.att_syntax
#define NO_RED_ZONE
#include "arch/amd64/regs.h"
#include "arch/amd64/lzma_d.S"
.intel_syntax noprefix
section LZMA_TAIL
leave
pop rax
top_n2e:
movb (
%
rsi),
%
dl
# speculate: literal, or bottom 8 bits of offset
jnextb1yp lit_n2e
top_n2e:
movb (
%
rsi),
%
dl
# speculate: literal, or bottom 8 bits of offset
jnextb1yp lit_n2e
lit_n2e:
incq
%
rsi; movb
%
dl,(
%
rdi)
incq
%
rdi
lit_n2e:
incq
%
rsi; movb
%
dl,(
%
rdi)
incq
%
rdi
off_n2e:
dec off
getnextbp(off)
getoff_n2e:
getnextbp(off)
jnextb0np off_n2e
off_n2e:
dec off
getnextbp(off)
getoff_n2e:
getnextbp(off)
jnextb0np off_n2e
subl $
3
,off; jc offprev_n2e
shll $
8
,off; movzbl
%
dl,
%
edx
orl
%
edx,off; incq
%
rsi
xorl $~
0
,off; jz eof
sarl off
# Carry= original low bit
subl $
3
,off; jc offprev_n2e
shll $
8
,off; movzbl
%
dl,
%
edx
orl
%
edx,off; incq
%
rsi
xorl $~
0
,off; jz eof
sarl off
# Carry= original low bit
len_n2e:
getnextb(
len
)
jnextb0n len_n2e
addl $
6
-
2
-
2
,
len
len_n2e:
getnextb(
len
)
jnextb0n len_n2e
addl $
6
-
2
-
2
,
len
gotlen_n2e:
cmpq $
-
0x500
,dispq
adcl $
2
,
len
# len += 2+ (disp < -0x500);
call copy
gotlen_n2e:
cmpq $
-
0x500
,dispq
adcl $
2
,
len
# len += 2+ (disp < -0x500);
call copy
sub rsp,
0x28
lea rdi, [rsi
+
compressed_imports]
sub rsp,
0x28
lea rdi, [rsi
+
compressed_imports]
next_dll:
mov eax, [rdi]
or
eax, eax
jz SHORT(imports_done)
mov ebx, [rdi
+
4
]
/
/
iat
lea rcx, [rax
+
rsi
+
start_of_imports]
add rbx, rsi
add rdi,
8
call [rip
+
LoadLibraryA]
xchg rax, rbp
next_dll:
mov eax, [rdi]
or
eax, eax
jz SHORT(imports_done)
mov ebx, [rdi
+
4
]
/
/
iat
lea rcx, [rax
+
rsi
+
start_of_imports]
add rbx, rsi
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创]UPX加载逻辑的处理细节分析 6485
- dll文件判断方法——UPX加壳器中isdll变量 8102
- UPX代码buildloader函数分析 2119