【问题标题】:GAS assembler not using 2-byte relative JMP displacement encoding (only 1-byte or 4-byte)GAS 汇编器不使用 2 字节相对 JMP 位移编码(仅 1 字节或 4 字节)
【发布时间】:2018-05-15 01:32:11
【问题描述】:

我正在尝试为不允许 0x00 字节的 CTF 质询编写 shellcode(它将被解释为终止符)。由于挑战的限制,我必须这样做:

[shellcode bulk]
[(0x514 - sizeof(shellcode bulk)) filler bytes]
[fixed constant data to overwrite global symbols]
[shellcode data]

看起来像这样

.intel_syntax noprefix
.code32

shellcode:
    jmp sc_data

shellcode_main:
    #open
    xor eax, eax
    pop ebx         //file string
    xor ecx, ecx    //flags
    xor edx, edx    //mode
    mov al, 5       //sys_OPEN
    int 0x80

    ...  // more shellcode

.org 514, 0x41     // filler bytes
.long 0xffffffff   // bss constant overwrite

sc_data:
    call shellcode_main
    .asciz "/path/to/fs/file"

如果sc_data 位于shellcode 的 127 个字节以内,这将非常有效。在这种情况下,汇编器(GAS)将输出格式的短跳转:

Opcode  Mnemonic
EB cb   JMP rel8

但是,由于我有一个硬性限制,即批量 shellcode 和填充字节需要 0x514 字节,因此这个相对偏移量至少需要 2 个字节。这可以工作,因为jmp 指令有一个 2 字节的相对编码:

Opcode  Mnemonic
E9 cw   JMP rel16

很遗憾,GAS 不输出这种编码。而是使用 4 字节偏移编码:

Opcode  Mnemonic
E9 cd   JMP rel32

这会导致两个 MSB 字节的零。类似于:

e9 01 02 00 00

我的问题是:可以强制 GAS 输出 jmp 指令的 2 字节变体吗? 我玩弄了多个较小的 1 字节 jmps,但 GAS 一直在输出4 字节变体。我还尝试使用-Os 调用 GCC 来优化大小,但它坚持使用 4 字节相对偏移编码。

Intel 跳转操作码定义here 供参考。

【问题讨论】:

  • jmp rel16 仅在 16 位代码中可用。如果您可以使用前缀以 32 位模式对其进行编码,它会将 EIP 截断为 16 位 IP。
  • @PeterCordes 很好的链接!谢谢你。这个特殊的挑战不受可用大小的限制,但其他挑战是。谢谢你!不过,我不确定您的第一条评论是什么意思。前缀在 32 位代码中有效吗?我只在 16 位代码中使用了前缀。
  • 在 32 位模式下,操作数大小和地址大小前缀会覆盖从 32 到 16 的默认值,而不是相反。 lockrep/repz/repnz 前缀的操作与 16 位模式相同。段覆盖前缀也是相同的,但仅用于线程本地存储(gs:fs:),因为主流操作系统为 CS/DS/ES/SS 使用平面内存模型。正如您自己的jmp 指令链接所示,16 位操作数大小为it does EIP ← tempEIP AND 0000FFFFH;,因此您不能使用jmp rel16
  • @PeterCordes,显然我并不立即熟悉 127 在 base 16 (0x7f) 中的样子。每次我测试这个时,我都会在小跳跃之间放置太多的填充。当我足够关注时,我减少了填充字节填充,并且能够让 GAS 生成 rel8 jmp。
  • 是的,您可以向前短-jmp,然后向后短call。正在寻找一个答案来解决这个乱七八糟的 cmets。回复:位移:记住它是符号扩展rel8,所以全位移的高字节必须与低字节的高位相同。 0x80 和更高的符号扩展到 FFFFFFxy 而不是 000000xy

标签: assembly x86 gnu-assembler shellcode machine-code


【解决方案1】:

jmp rel16 只能使用 16 的操作数大小进行编码,这会将 EIP 截断为 16 位。 (编码需要 32 位和 64 位模式下的 66 操作数大小前缀)。如您链接的指令集参考中所述,或in this more up-to-date PDF->HTML conversion of Intel's manualjmp 在操作数大小为 16 时执行EIP ← tempEIP AND 0000FFFFH;。这就是为什么汇编程序从不使用它的原因,除非您手动请求它1,以及为什么您不能在 32 位或 64 位代码中使用 jmp rel16,除非在非常罕见的情况下,目标映射到低 64kiB 的虚拟地址空间2.


避免jmp rel32

您只是向前跳跃,因此您可以使用call rel32 推送数据的地址,并且因为您希望您的数据一直位于长填充负载的末尾。

您可以使用push imm32/imm8/regmov ebx, esp 在堆栈上构造一个字符串。 (您已经有一个归零的寄存器,您可以推送终止的零字节)。

如果您不想在堆栈上构造数据,而是使用属于有效负载一部分的数据,请使用与位置无关的代码/相对寻址。 也许您的寄存器中有一个值与 EIP 有一个已知的偏移量,例如如果您的漏洞利用代码通过 jmp esp 或其他 ret-2-reg 攻击 到达。在这种情况下,您也许可以
mov ecx, 0x12345678 / shr ecx, 16 / lea ebx, [esp+ecx]

或者,如果您必须使用 NOP 雪橇并且您不知道 EIP 相对于任何寄存器值的确切值,您可以使用 call 获取 EIP 的当前值负位移指令。 向前跳过call 目标,然后call 回到它。您可以在call 之后放置数据。 (但避免数据中的零字节是不方便的;一旦获得指向它的指针,您就可以存储一些。)

 # Position-independent 32-bit code to find EIP
 # and get label addresses into registers
 # and insert zeros into data that we jumped over.

               jmp  .Lcall

.Lget_eip:
               pop   ebx
               jmp   .Lafter_call       # jmp rel8
.Lcall:        call  .Lget_eip          # backward rel32 = 0xffffff??
          # execution never returns here
   .Lmsg:   .ascii "/path/to/fs/file/"    # last byte to be overwritten
   msglen = . - .Lmsg
   .Loffset_data2: .long .Ldata2 - .Lmsg   # relative offset to other data, or make this a 16-bit int to avoid zeros
               # max data size 127 - 5 bytes

.Lafter_call:
               # EBX = OFFSET .Lmsg just from the call + pop
               # Insert a zero at runtime because the data wasn't at the end of the payload
               mov  byte ptr [ebx+ msglen - 1], al   # with al=0


               # ESI = OFFSET .Ldata2 using an offset loaded from memory
               mov  esi, ebx
               add  esi, [ebx + .Loffset_data2 - .Lmsg]   # [ebx + disp8]

               # with an immediate displacement, avoiding zero bytes
               mov  ecx, ((.Ldata3 - .Lmsg) << 17) | 0xffff
               shr  ecx, 17                # choose shift count to avoid high zeros
               lea  edi, [ebx + ecx]       # edi = OFFSET .Ldata3

               # if disp8 doesn't work but 8 * disp8 does: small code size
               push  (.Ldata3 - .Lmsg)>>8   # push imm8
               pop   ecx
               lea   edi, [ebx + ecx*8 + (.Ldata3 - .Lmsg)&7]  # disp8 of the low 3 bits

           ...

  # at the end of your payload
  .Ldata2:
    whatever you want, arbitrary size

  .Ldata3:

在 64 位代码中,这要容易得多:

 # In 64-bit code

     jmp  .Lafter_data
 .Lmsg1:   .ascii "/foo/bar/"    # last bytes to be replaced
 .Lmsg2:   .ascii "/bin/sh/"
 .Lafter_data:
     lea  rdi, [RIP + .Lmsg1]            # negative rel32 
     lea  rsi, [rdi + .Lmsg2 - .Lmsg1]   # disp8
     xor  eax,eax
     mov  byte ptr [rsi - 1], al         # insert zeros
     mov  byte ptr [rsi + len], al

或者使用相对于 RIP 的 LEA 来获取标签地址,并使用一些避免零的方法向其添加立即常量,以获取有效负载末尾的标签地址。

  .Lbase:
      lea  rdi, [RIP + .Lbase]
      xor  ecx,ecx
      mov  cx, .Lpath - .Lbase
      add  rdi, rcx          # RDI = .Lpath address
      ...
      syscall

       ...   # more than 128 bytes
   .Lpath:
       .asciz "/foo/bar"

如果您真的需要跳得更远,而不仅仅是对远处的“静态”数据进行与位置无关的寻址。

一连串的短前跳跃会起作用。

或者使用上述任何一种方法在寄存器中查找后面标签的地址,并使用jmp eax


保存代码字节:

在您的情况下,节省代码大小并不能帮助您避免跳远位移,但可能对其他人来说会:

您可以使用这些Tips for golfing in x86/x64 machine code 保存代码字节:

  • xor eax,eax / cdqxor edx,edx 节省 1 个字节。
  • xor ecx, ecx / mul ecx 将 4 个字节中的三个寄存器归零(ECX 和 EDX:EAX)
  • 实际上,int 0x80 设置的最佳选择可能是
    xor ecx,ecx (2B) / lea eax, [ecx+5] (3B) / cdq (1B),并且根本不要使用 mov al,5 .您可以使用push imm8 / pop 或一个lea 将任意小的常量放入寄存器中,只需3 个字节,如果您有另一个具有已知值的寄存器。

脚注 1:要求您的汇编程序在 16 位模式之外对 jmp rel16 进行编码

NASM(在 16、32 或 64 位模式下)

addr:
; times 256 db 0      ; padding to make it jump farther.
o16 jmp near addr     ; force 16-bit operand-size and near (not short) displacement

AT&T 语法:

objdump -d 将其解码为jmpw:对于上述 NASM 源组装成 32 位静态 ELF 二进制文件,objdump -drwC foo 显示 EIP 的截断:

0000000000400080 <addr>:
  400080:       66 e9 fc ff             jmpw   80 <addr-0x400000>

但 GAS 似乎认为助记符仅用于间接跳转(这意味着 16 位负载)。 (foo.S:5: Warning: indirect jmp without '*'),这个 GAS 来源:.org 1024; addr: .zero 128; jmpw addr 给你

480:   66 ff 25 00 04 00 00    jmpw   *0x400   483: R_386_32   .text

参见what is jmpl instruction in x86? - GAS 如何处理 AT&T 语法的这种疯狂的不一致甚至适用于jmpl。在 16 位模式下组装时,纯 jmp 0x400 将是相对跳转到该绝对偏移量。

在极不可能的情况下,您需要在其他模式下使用 jmp rel16,您必须自己使用 .byte.short 组装它。我认为甚至没有办法让汇编器为你发出它。


脚注 2:您不能在 32/64 位代码中使用 jmp rel16,除非您正在攻击映射在低 64kiB 虚拟地址空间中的某些代码,例如也许在 DOSEMU 或 WINE 下运行的东西。 Linux 对/proc/sys/vm/mmap_min_addr 的默认设置是 65536,而不是 0,因此即使您想通过 ELF 程序加载器将其文本段加载到该地址,通常也没有任何东西可以mmap 该内存。 (所以 NULL 指针取消引用偏移段错误,而不是静默访问内存)。

您可以确定您的 CTF 目标不会碰巧在 EIP = IP 的情况下运行,并且将 EIP 截断为 IP 只会出现段错误。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-28
    • 2012-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多