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 一些常量,如 -1
或 0x80
,使每个字节都非零,然后 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
我设法修补了一个 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 一些常量,如 -1
或 0x80
,使每个字节都非零,然后 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