Ассемблер GAS не использует 2-байтовую относительную кодировку смещения JMP (только 1-байтовую или 4-байтовую)

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

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


person sherrellbc    schedule 15.05.2018    source источник
comment
jmp rel16 доступен только в 16-битном коде. Если вы можете закодировать его в 32-битном режиме с префиксами, он усечет EIP до 16-битного IP.   -  person Peter Cordes    schedule 15.05.2018
comment
@PeterCordes Очень хорошая ссылка! Спасибо. Эта конкретная задача не очень ограничена доступным размером, но другие ограничиваются. Спасибо за это! Я не совсем понимаю, что вы имели в виду под своим первым комментарием. Допустимы ли префиксы в 32-битном коде? Я использовал префиксы только в 16-битном коде.   -  person sherrellbc    schedule 15.05.2018
comment
В 32-битном режиме префикс размера операнда и размера адреса заменяет значение по умолчанию от 32 до 16, а не наоборот. Префиксы lock и _2 _ / _ 3 _ / _ 4_ работают так же, как в 16-битном режиме. Префиксы переопределения сегмента тоже такие же, но используются только для локального хранилища потока (gs: или fs:), поскольку в основных операционных системах используется плоская модель памяти для CS / DS / ES / SS. Как показывает ваша собственная ссылка на инструкцию jmp, с 16-битным размером операнда это EIP ← tempEIP AND 0000FFFFH;, поэтому вы не можете использовать jmp rel16.   -  person Peter Cordes    schedule 15.05.2018
comment
@PeterCordes, видимо я не сразу знаком с тем, как 127 выглядит в базе 16 (0x7f). Каждый раз, когда я тестировал это, я вставлял слишком много отступов между маленькими прыжками. Когда я уделил достаточно внимания, я уменьшил заполнение байтов заполнителя и смог заставить GAS сгенерировать rel8 jmp.   -  person sherrellbc    schedule 15.05.2018
comment
@PeterCordes, я думал о подходе к вычислению адресов (с jmp eax), но единственный знакомый мне метод получения EIP - это выполнение call и извлечение адреса из стека. Если относительное смещение call не является отрицательным, этот также кодируется нулевыми байтами. Может, я смогу создать что-нибудь подобное. В любом случае, спасибо за урок сборки сегодня вечером. Я многому научился.   -  person sherrellbc    schedule 15.05.2018
comment
Да, вы можете делать короткие jmp вперед, а затем call назад. Работаем над ответом, чтобы завершить этот беспорядок комментариев. Re: смещение: помните, что это знаковое расширение rel8, поэтому старшие байты полного смещения должны быть такими же, как старший бит младшего байта. Знак 0x80 и выше расширяется до FFFFFFxy вместо 000000xy   -  person Peter Cordes    schedule 15.05.2018
comment
Вы также можете сделать это, просто указав байты вручную.   -  person David Hoelzer    schedule 15.05.2018
comment
@DavidHoelzer: что делать? Кодировать jmp rel16? Это вам не поможет, потому что EIP обрезается до IP. NASM может его кодировать, но да, если вы когда-нибудь найдете ему применение, вы можете вручную кодировать его в AT&T или GAS .intel_syntax.   -  person Peter Cordes    schedule 15.05.2018
comment
@sherrellbc: Я удалил большинство своих комментариев после публикации своего ответа.   -  person Peter Cordes    schedule 15.05.2018


Ответы (1)


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

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


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

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

Вы можете сохранить байты кода, используя эти советы по игре в гольф. в машинном коде 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

См. что такое инструкция jmpl в x86? - это безумное несоответствие в том, как GAS обрабатывает синтаксис AT&T, применяется даже к jmpl. Обычный jmp 0x400 при сборке в 16-битном режиме будет относительным скачком к этому абсолютному смещению.

В крайне маловероятном случае, если вам понадобится jmp rel16 в других режимах, вам придется собрать его самостоятельно с .byte и .short. Я не думаю, что есть способ заставить ассемблер выдать его за вас.


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

Вы можете быть уверены, что ваша цель CTF не будет работать с EIP = IP, и что усечение EIP до IP будет просто ошибкой segfault.

person Peter Cordes    schedule 15.05.2018