UPX加载逻辑的处理细节分析

科技   2024-11-20 18:06   上海  

在本人第一篇博客UPX代码buildloader函数分析(https://bbs.kanxue.com/thread-283702.htm),对加载器初始化过程阐明了不同条件下的加载逻辑,本文根据其加载逻辑进行更细致的分析其操作细节。


首先在文件src\stub\src\amd64-win64.pe.S中发现PE文件加壳入口点:




.intel_syntax noprefix是用于汇编语言的编译器指令,主要用于告诉汇编器使用Intel 语法来解析接下来的代码,并且在操作数之前不加任何前缀(noprefix)。

接下来按照加载逻辑的添加顺序分别分析其处理细节:


PEISDLL0与PEISEFI0

如果是dll文件,添加逻辑PEISDLL0

section         PEISDLL0
mov [rsp + 8], rcx
mov [rsp + 0x10], rdx
mov [rsp + 0x18], r8


在 Windows的 x64调用约定中,rcx是第一个参数,rdx是第二个参数,r8是第三个参数。因此在函数调用前保存参数。

如果是efi文件,添加逻辑PEISEFI0

section         PEISEFI0
push rcx
push rdx


此处处理过程与 PEISDLL0类似,保存了 rcx和 rdx 的内容,但这里的压栈方式不同,是直接将它们压入栈顶。可能是为了适应 EFI 系统环境的不同调用约定。



主程序解密逻辑

如果是dll文件,添加逻辑PEISDLL1

section         PEISDLL1
cmp dl, 1
jnz reloc_end_jmp


如果是dll文件,在主程序入口前还需要添加初始化逻辑,判断是否是dll文件的卸载操作。在 Windows 系统中,dl 在 DLL 入口点(DllMain 函数)中传递给 DLL 的 fdwReason 参数,值为 1 时表示 DLL 正在被卸载(DLL_PROCESS_DETACH)。在此处如果值并不为1(不是卸载操作),则程序将跳转到reloc_end_jmp标签,继续正常的初始化流程。

PEMAINO1——主程序的入口点

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]


这一部分代码是为压缩数据解压作准备的主要逻辑。将压缩后的数据解压到内存中的指定位置,然后继续执行原始程序代码。


a)   首先保存寄存器rbx、rsi、rdi 和 rbp的值。


b)lea rsi, [rip + start_of_compressed]计算压缩数据的起始地址,并存入 rsi 寄存器,其中rip 是当前指令地址。


c)lea rdi, [rsi + start_of_uncompressed]则计算未压缩数据的起始地址,存入 rdi 寄存器。


最后rsi 指向压缩数据,而 rdi 指向解压后的数据。

图标对应指令PEICONS1和PEICONS2

section         PEICONS1
incw [rdi + icon_offset]
section PEICONS2
add [rdi + icon_offset], IMM16(icon_delta)


1.将内存地址[rdi + icon_offset]处的 16 位字增加 1,更新图标的索引。

2.将立即数icon_delta加到内存地址[rdi + icon_offset]上,过增量更新图标的偏移。


如果 tmp_tlsindex 有效,添加指令PETLSHAK

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


处理TLS 相关的初始化操作,包括保存和恢复被 TLS 索引覆盖的数据,确保数据正确性。


1.将rdi + tls_address的有效地址加载到 rax 寄存器中,指向 TLS(线程本地存储)地址。

2.将 rax 寄存器指向的内存值(即 TLS 索引)压入栈中保存。

3.将 tls_value 存入 rax 指向的内存位置,恢复之前被 TLS 索引覆盖的数据。

4.将 rax 寄存器的值压入栈中保存。


如果压缩方法是LZMA,执行解压缩

.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


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,执行解压缩



NRV算法对应有2B、2D以及2E,这里以2E为例,即执行指令NRV2E。可见指令同样引入文件nrv2e_d.S,这里对解压缩主要流程进行分析。


1. 字节处理


top_n2e:
movb (%rsi),%dl # speculate: literal, or bottom 8 bits of offset
jnextb1yp lit_n2e


从 rsi(源指针)处获取下一个字节,并存储到 %dl 中。这一步推测该字节是字面值(literal)还是偏移值的一部分。然后根据寄存器的数据,跳转到 lit_n2e 处理字面值数据。


lit_n2e:
incq %rsi; movb %dl,(%rdi)
incq %rdi


如果判定当前字节是字面值数据,则将其存储到目标位置 %rdi,并递增源和目标地址指针,准备处理下一个字节。


2. 偏移与长度计算


off_n2e:
dec off
getnextbp(off)
getoff_n2e:
getnextbp(off)
jnextb0np off_n2e


处理偏移值(off),首先从输入数据中获取偏移值的高位字节。


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


调整偏移值,判断是否需要跨越多个字节计算。如果偏移值满足某些条件(例如低位较小),会跳转到 offprev_n2e。


off 最终得到的是需要从目标位置向后移动的距离,它决定了从哪里复制数据。


len_n2e:
getnextb(len)
jnextb0n len_n2e
addl $6-2-2,len


从源数据中获取解压数据块的长度,利用 getnextb 函数从输入流中读取长度值。


3. 数据复制


gotlen_n2e:
cmpq $-0x500,dispq
adcl $2,len # len += 2+ (disp < -0x500);
call copy


根据前面解析出来的偏移值和长度,调用 copy 函数,复制解压出来的数据块到目标位置。这里的 adcl 指令根据偏移的大小,调整解压出的数据块长度。

NRV2E 压缩格式的解压流程:通过多次从压缩数据中读取字节或位,代码能够逐步解析出偏移和长度信息,随后将数据块从先前的位置复制到新的位置,完成解压缩过程。


导入表处理


1. 设置栈空间

sub     rsp, 0x28
lea rdi, [rsi + compressed_imports]


分配栈空间,并将 compressed_imports 加载到 rdi 中,准备开始解析导入表。

2. 加载导入表

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


读取dll的名称地址,如果名称为空(eax=0),则跳转到imports_done结束导入表的修复。否则,继续准备加载dll的导入表,调用系统API函数LoadLibraryA加载dll,并将结果保存在rbp中。

3.遍历dll

next_func:
mov al, [rdi]
inc rdi
or al, al
jz next_dll


从 rdi 读取当前导入函数的标识符。如果标识符为 0,说明所有函数已处理完毕,跳转到 next_dll 处理下一个 DLL。

4.按序号导入

section PEK32ORD
jpe not_kernel32
mov eax, [rdi]
add rdi, 4
mov rax, [rax + rsi + kernel32_ordinals]
jmp SHORT(next_imp)


如果当前导入函数属于 kernel32.dll,则根据序号查找该函数的地址。

5.按名称导入

byname:
mov rcx, rdi
mov rdx, rdi
dec eax
repne
scasb
first_imp:
mov rcx, rbp
call [rip + GetProcAddress]


如果函数按名称导入,使用 GetProcAddress 来获取函数地址。这里先通过 repne scasb 搜索字符串(函数名称),然后调用 GetProcAddress 获取函数的内存地址。

6.存取函数地址

next_imp:
mov [rbx], rax
add rbx, 8
jmp SHORT(next_func)


将获取到的函数地址存储到 IAT 中,并继续处理下一个函数。

7.错误处理

imp_failed:
add rsp, 0x28
pop rbp
pop rdi
pop rsi
pop rbx
xor eax, eax
ret


如果导入失败,则清理栈空间,返回 eax = 0 表示失败。


这段代码动态解析并加载 PE 文件的导入表,加载所需的 DLL 并获取函数地址,完成导入表修复的任务。


重定位表处理

1.重定位表遍历

section PERELOC1
lea rdi, [rsi + start_of_relocs]

section PERELOC2
add rdi, 4
section PERELOC3
lea rbx, [rsi - 4]
reloc_main:
xor eax, eax
mov al, [rdi]
inc rdi
or eax, eax
jz SHORT(reloc_endx)
cmp al, 0xEF
ja reloc_fx


首先将重定位表地址初始化。然后每次从重定位表读取一个字节并检查其值。如果为 0x00,表示重定位表处理完毕,跳转到 reloc_endx 结束处理。如果字节值大于 0xEF,则跳转到 reloc_fx 处理其他类型的重定位项。

2.应用重定位

reloc_add:
add rbx, rax
mov rax, [rbx]
bswap rax
add rax, rsi
mov [rbx], rax
jmp reloc_main


将偏移量加到 rbx,然后从 rbx 地址加载目标地址(目标地址是反向字节序,因此使用 bswap 交换字节顺序)。接着,将目标地址加上基址 rsi,以便修正地址引用,并将结果存回原位置。

3.处理特殊重定位项

reloc_fx:
and al, 0x0F
shl eax, 16
mov ax, [rdi]
add rdi, 2


对于特殊的重定位项,使用低 4 位并移位来计算偏移。然后从 rdi 中读取额外的 2 个字节作为偏移量,进行地址修正。

4.高位和低位重定位的处理

1.低位重定位

section PERLOHI0
xchg rdi, rsi
lea rcx, [rdi + reloc_delt]
section PERELLO0
jmp 1f
rello0:
add [rdi + rax], cx
1:
lodsd
or eax, eax
jnz rello0


处理低位重定位,将 reloc_delt 加到目标地址中,并使用循环结构逐个应用。

2.高位重定位

section PERELHI0
shr ecx, 16
jmp 1f
relhi0:
add [rdi + rax], cx
1:
lodsd
or eax, eax
jnz relhi0


类似低位重定位的处理逻辑,不过这里处理的是高 16 位的重定位,修正高位地址。

这段代码实现 PE 文件中的重定位表修复,遍历重定位表中的各个项,并对内存中的地址进行修正。这可以确保当程序被加载到不同的内存地址时,所有地址引用都能被正确调整。





看雪ID:Bogger

https://bbs.kanxue.com/user-home-1006242.htm

*本文为看雪论坛优秀文章,由 Bogger 原创,转载请注明来自看雪社区


# 往期推荐

1、PWN入门:整数溢出

2、野蛮fuzz:持久性fuzz

3、修改PE导入表注入DLL——实例图文教程

4、【入门篇】Android漏洞挖掘,实战演示挖掘技巧

5、野蛮fuzz:深入了解代码覆盖率


球分享

球点赞

球在看




点击阅读原文查看更多

看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
 最新文章