首页
社区
课程
招聘
[原创]UPX加载逻辑的处理细节分析
发表于: 5小时前 303

[原创]UPX加载逻辑的处理细节分析

5小时前
303

在本人第一篇博客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

[峰会]看雪.第八届安全开发者峰会10月23日上海龙之梦大酒店举办!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//