Ассемблер GAS не использует 2-байтовое относительное кодирование смещения JMP (только 1-байтовый или 4-байтовый) - PullRequest
0 голосов
/ 15 мая 2018

Я пытаюсь написать шелл-код для вызова CTF, который не допускает 0x00 байт (он будет интерпретирован как терминатор). Из-за ограничений в задании я должен сделать что-то вроде этого:

[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 находится в пределах 127 байт от shellcode. В этом случае ассемблер (GAS) выведет короткий прыжок формата:

Opcode  Mnemonic
EB cb   JMP rel8

Однако, поскольку у меня есть жесткое ограничение, что мне нужно 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 выводить 2-байтовый вариант инструкции jmp? Я играл с несколькими меньшими 1 байтами jmp s, но GAS продолжал выводить 4 Байтовый вариант. Я также пытался вызвать GCC с -Os для оптимизации размера, но он настаивал на использовании 4-байтовой кодировки относительного смещения.

Код операции перехода Intel определен здесь для справки.

1 Ответ

0 голосов
/ 15 мая 2018

jmp rel16 кодируется только с размером операнда 16, который усекает EIP до 16 бит . (Для кодирования требуется префикс размера операнда 66 в 32- и 64-битном режиме). Как описано в ссылке на набор инструкций, которую вы связали, или в этом более современном PDF-> HTML-преобразовании руководства Intel , jmp делает EIP ← tempEIP AND 0000FFFFH;, когда размер операнда равен 16. Вот почему ассемблеры никогда не используют его, если вы не запросите его вручную 1 , и почему вы не можете использовать jmp rel16 в 32- или 64-битном коде , за исключением очень необычного случая, когда цель отображается в низком 64 КБ виртуального адресного пространства 2 .


Избежание jmp rel32

Вы только прыгаете вперед, поэтому вы можете использовать call rel32, чтобы выдвинуть адрес ваших данных, и потому, что вы хотите, чтобы ваши данные были полностью в конце вашей длинной дополненной полезной нагрузки.

Вы можете создать строку в стеке с помощью push imm32/imm8/reg и mov ebx, esp. (У вас уже есть нулевой регистр, который вы можете нажать для завершения нулевого байта).

Если вы не хотите создавать данные в стеке и вместо этого использовать данные, являющиеся частью вашей полезной нагрузки, используйте для них независимый от позиции код / ​​относительную адресацию. Возможно, у вас есть значение в регистре, которое является известным смещением от EIP , например, если ваш код эксплойта был достигнут с помощью jmp esp или другой атаки ret-2-reg . В этом случае вы можете просто
mov ecx, 0x12345678 / shr ecx, 16 / lea ebx, [esp+ecx].

Или, если вам пришлось использовать салазки NOP и , вы не знаете точное значение EIP относительно какого-либо значения регистра , вы можете получить текущее значение EIP с помощью инструкции call с отрицательным смещением. Перепрыгните вперед через цель 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

Или используйте REA-относительный 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.


Сохранение байтов кода:

В вашем случае сохранение размера кода не поможет вам избежать смещений при длинных скачках, но, вероятно, для некоторых других людей это будет:

Вы можете сохранить байты кода, используя эти Советы по игре в гольф в машинном коде x86 / x64 :

  • xor eax,eax / cdq сохраняет 1 байт против xor edx,edx.
  • 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. Вы можете поместить произвольные маленькие константы в регистры всего в 3 байта с push imm8 / pop или с одним lea, если у вас есть другой регистр с известным значением.

Сноска 1: попросить вашего ассемблера кодировать jmp rel16 вне 16-битного режима :

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 '*'), и этот источник ГАЗА: .org 1024; addr: .zero 128; jmpw addr дает вам

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

Сноска 2: Вы не можете использовать jmp rel16 в 32/64-битном коде , если только вы не атакуете какой-то код, отображаемый в 64-килобайтном виртуальном адресном пространстве, например, возможно, что-то работающее подДОСЕМУ или ВИНО.По умолчанию в Linux для /proc/sys/vm/mmap_min_addr установлено значение 65536, а не 0, поэтому обычно ничто не может mmap памяти, даже если вы хотите, или предположительно загружать свой текстовый сегмент по этому адресу через загрузчик программ ELF.(Таким образом, NULL-указатель разыменовывается со смещением по умолчанию вместо тихого доступа к памяти).Вы можете быть уверены, что ваша цель CTF не будет работать с EIP = IP, а усечение EIP до IP будет просто segfault.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...