【问题标题】:ELF - Entry point patching with x86 zero-extended addressELF - 使用 x86 零扩展地址修补入口点
【发布时间】:2020-01-16 20:24:43
【问题描述】:

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

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

我有一个包含这些操作码的数组,一旦我解析 ELF 标头以获取入口点,我就会对其进行修补:

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

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

编辑: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指令。

为什么会出现分段错误?

【问题讨论】:

  • ELF 入口点是 64 位地址,不使用 32 位地址大小前缀截断 + 零扩展回 64 位。你说的是shellcode吗?如果您需要在包含零的寄存器中创建一个值,您可以使用 mov rax, (-1) ^ 0x47fe8d 之类的东西来实现; not rax:即使用不包含零的立即数常量,然后异或或减去某个值以获得寄存器中所需的值。
  • @PeterCordes 是的,彼得,我已经用更多细节更新了这个问题。
  • 究竟是什么导致了段错误?运行 shellcode 的程序,还是修补过的二进制文件?使用调试器查看哪些指令错误。那些十六进制字符串的代码块,如果不包括反汇编,对大多数人来说基本上是没用的。
  • @PeterCordes 根据 gdb 它在 000000000047FEAE 崩溃,这是我使用 call 指令将字符串放入堆栈时。出于某种原因IDA 解码错误。
  • IDA 没有解码错误,你的十六进制字符串是错误的。如果我将该 C 数组编译成 .o 并用 objdump -D -rwC -Mintel foo.o 反汇编它以让 objdump 反汇编 .data 部分,这就是它对我的反汇编方式。如果您在 GDB 中按照出错的指令进行反汇编,您也应该会看到同样的情况;它可能是特权的in 指令。 (为什么这甚至是一个 char 数组中的 shellcode,而不仅仅是一个用 asm 编写的函数,组装成 .o,然后链接到您的程序中?)

标签: x86-64 elf shellcode


【解决方案1】:

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

我通过将 C 数组复制/粘贴到 .c 并将其编译到 .o 中注意到了这一点。然后我用objdump -D -rwC -Mintel foo.o 反汇编得到objdump 来反汇编.data 部分。它与 IDA 一致,证明 IDA 是正确的,并且您在将 NASM 输出转换为十六进制字符串的任何操作中都犯了错误。 (IDK 你为什么要这样做,而不是仅仅链接到 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 的最高有效字节是应该是jmp 操作码的FF。您的 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-04
    • 2011-11-09
    • 2012-05-09
    • 2017-10-13
    • 2011-03-17
    • 1970-01-01
    • 1970-01-01
    • 2016-08-09
    相关资源
    最近更新 更多