ELF - 使用 x86 zero-extended 地址修补入口点

ELF - Entry point patching with x86 zero-extended address

我设法修补了一个 ELF 文件的入口点,使它指向其他地方并在返回到原始入口点之前执行一段代码。以下是我试图跳回到 OEP 的方式:

mov rax, 0x4141414141414141  ( 48 b8 41 41 41 41 41 41 41 41 )
jmp rax                      (ff e0)

我有一个包含这些操作码的数组,我在解析 ELF header 以获取入口点后立即对其进行修补:

uint64_t oep = ehdr->e_entry;
memcpy(&opcode[23], &oep, 8);

但入口点总是类似于:0x47fe8d 这会使数组的其余部分无效,因为操作码需要一个不带零的 8 字节地址。我试图通过扩展地址的符号来替换它,例如:0xffffffff47fe8d 但它没有用。这似乎是正常行为,因为 x86 地址是

编辑:shellcode 数组如下所示:

 _start:
       xor rax, rax
       xor rax, rax
       xor rsi, rsi
       jmp get_str
 shellcode:
       pop rsi
       mov al, 1
       mov dil, 1
       mov dl, 9
       syscall ; writes a string

       mov rax, 0x4141414141414141 ; patched with the EP
       jmp rax
   get_str:
         call shellcode
         db "strings!", 0xa

 // write syscall + jmp OEP (mov rax, addr, jmp rax). patch at 23
unsigned char shellcode[] = "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\xeb"
                  "\x16\x5e\xb0\x01\x40\xb7\x01\xb2\x09\x0f"
                  "\x05\x48\xb8\x41\x41\x41\x41\x41\x41\x41"
                  "\xff\xe0\xe8\xe5\xff\xff\xff\x68\x69\x6a"
                  "\x61\x63\x6b\x65\x64\x0a";

我做了一个函数,在修补它之前打印这个数组。这是它的样子:

\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\xeb\x16\x5e\xb0\x01\x40\xb7\x01\xb2\x09\x0f\x05\x48\xb8\x41\x41\x41\x41\x41\x41\x41\xff\xe0\xe8\xe5\xff\xff\xff\x68\x69\x6a\x61\x63\x6b\x65\x64\x0a

但在用 0x47fe8d 修补 jmp 指令后,地址的高位字节变为零:

\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\xeb\x16\x5e\xb0\x01\x40\xb7\x01\xb2\x09\x0f\x05\x48\xb8\x20\x1b\x40

由于某种原因,这会导致分段错误。我使用 IDA 搜索补丁文件的入口点,这是我找到的:

LOAD:000000000047FE8D start:                                  ; DATA XREF: LOAD:0000000000400018↑o
LOAD:000000000047FE8D                 xor     rax, rax
LOAD:000000000047FE90                 xor     rdi, rdi
LOAD:000000000047FE93                 xor     rsi, rsi
LOAD:000000000047FE96
LOAD:000000000047FE96 loc_47FE96:                             ; CODE XREF: LOAD:000000000047FEAC↓j
LOAD:000000000047FE96                 jmp     short loc_47FEAE
LOAD:000000000047FE98 ; ---------------------------------------------------------------------------
LOAD:000000000047FE98                 pop     rsi
LOAD:000000000047FE99                 mov     al, 1
LOAD:000000000047FE9B                 mov     dil, 1
LOAD:000000000047FE9E                 mov     dl, 9
LOAD:000000000047FEA0                 syscall                 ; $!
LOAD:000000000047FEA2                 mov     rax, offset _start
LOAD:000000000047FEAC                 loopne  loc_47FE96
LOAD:000000000047FEAE
LOAD:000000000047FEAE loc_47FEAE:                             ; CODE XREF: LOAD:loc_47FE96↑j
LOAD:000000000047FEAE                 in      eax, 0FFh       ; $!
LOAD:000000000047FEAE ; ---------------------------------------------------------------------------
LOAD:000000000047FEB0                 dq 6B63616A6968FFFFh
LOAD:000000000047FEB8                 db 65h, 64h, 0Ah
LOAD:000000000047FEB8 LOAD            ends

因此,尽管 IDA 错误地编码了位于 000000000047FEAC 的指令,但文件似乎已成功修补,_start 符号指向以下路径:

public _start
_start proc near
endbr64
xor     ebp, ebp
mov     r9, rdx         ; rtld_fini
pop     rsi             ; argc
mov     rdx, rsp        ; ubp_av
and     rsp, 0FFFFFFFFFFFFFFF0h
push    rax
push    rsp             ; stack_end
mov     r8, offset __libc_csu_fini ; fini
mov     rcx, offset __libc_csu_init ; init
mov     rdi, offset main ; main
db      67h
call    __libc_start_main
hlt
_start endp

这样就调用了原来的main函数,一切正常。

经过进一步检查,我发现 000000000047FEAE 处的指令是罪魁祸首,尽管我不太明白为什么。 这是我用来将字符串的地址压入堆栈的 call 指令。

为什么我会遇到分段错误?

IDA 没有解码错误,你的机器代码的十六进制字符串版本是错误的;一个 \x41 字节短,因此 mov r64, imm64 使用以下 FF 字节作为其立即数的一部分,而不是 jmp 的操作码。这就是它在 0e e8 loopne` 处解码的原因。

我通过 copy/pasting 将您的 C 数组转换为 .c 并将其编译为 .o 来注意到这一点。然后我用 objdump -D -rwC -Mintel foo.o 反汇编它,让 objdump 反汇编 .data 部分。它与 IDA 一致,证明 IDA 是正确的,无论您将 NASM 输出转换为十六进制字符串,您确实犯了一个错误。 (不知道你为什么要这样做,而不是仅仅链接 NASM .o 输出以首先以正常方式测试它,或者它与修改 ELF 二进制文件有什么关系。)

 // write syscall + jmp OEP (mov rax, addr, jmp rax). patch at 23
unsigned char shellcode[] = "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\xeb"
                  "\x16\x5e\xb0\x01\x40\xb7\x01\xb2\x09\x0f"
                  "\x05\x48\xb8\x41\x41\x41\x41\x41\x41\x41"  // this is only 7 x41 bytes
                  "\xff\xe0\xe8\xe5\xff\xff\xff\x68\x69\x6a"
                  "\x61\x63\x6b\x65\x64\x0a";

objdump -D 显示 48 b8 41 41 41 41 41 41 41 ff movabs rax,0xff41414141414141 - 您的 mov imm64 的最重要字节是 FF,它应该是 jmp 操作码。你的 C 字符串只有 7 \x41 个字节。

如果在 GDB 中对出错的指令进行反汇编,您也应该会看到同样的情况;可能是 in 指令具有特权。


使用 shellcode

在寄存器中创建包含 0 的值

这部分很简单。 XOR 或 ADD 一些常量,如 -10x80,使每个字节都非零,然后 NOT、xor-immediate 或 sub-immediate。或填充低垃圾和右移。

例如要在寄存器中创建 3 字节 0x47fe8d,您可以执行

   mov eax, 0x47fe8d61       ; (0x47fe8d << 8) + 'a'
   shr eax, 8

写入 32 位寄存器隐式零扩展到 64 位,所以这留下了
RAX = 0 0 0 0 0 47 fe 8d = 0x47fe8d.

    mov eax, ~0x47fe8d          ; none of the bytes are FF -> none of ~x are 0
    not eax                     ; still leaving the upper 32 bits zeroed