суббота, 6 августа 2011 г.

Препятствуем статическому анализу

Делать это можно по разному, можно через полиморфизм/метаморфизм/обфускацию, а можно изменить подход - убрать сам объект скрытия. Ведь нет кода - нечего и обнаруживать. Что же тогда процессор будет выполнять, если кода нет? Ну, на самом деле он есть, просто в другом модуле. Речь пойдет о Return Oriented Programming (rop).

Rонцепция rop проста - в уже загруженных модулях ищутся так называемые rop-gadgets: последовательности инструкций, заканчивающиеся инструкцией ret. Затем на эти готовые инструкции передается управление. Таким образом, весь исполняемый код программы как бы собирается из кусочков чужих модулей, что позволяет не иметь своего кода, вместо него будет лишь череда аргументов/адресов возврата.

В качестве примера напишем простейшее приложение на masm выводящее "Hello" через MessageBox:

.386
.model flat, stdcall
option casemap :none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.code

start:
    jmp @F
      szTitle    db "Hello",0
    @@:
   
    push MB_OK
    push offset szTitle
    push offset szTitle
    push 0
    call MessageBox

    push 0   
    call ExitProcess

end start

В IDA будет виден простой и понятный код, теперь перепишем его используя rop технику, избавившись от констант, статической линковки user32 и адресов функций ( для простоты вместо адресов ф-ций будет хардкод ).

Rop-gadgets будем искать в ntdll.dll, так как ее грузит ядро во все процессы, то есть она есть везде.

User32.dll для нашей программы придется грузить динамически, так что ищем соответствующий гаджет в ntdll.dll.
Чтобы не искать вручную, напишем простенький скрипт:

import idaapi
import idautils
import idc

def GetNodeDisasmView( nodeStart, nodeEnd ):
    result = ""
    instructions = Heads( nodeStart, nodeEnd )

    for address in instructions:
        result += hex(address) + "    " + GetDisasm( address ) + "\n"
       
    return result

def SaveTwoNodes( fileHandle, prevNodeStartEA, prevNodeEndEA, idaNodeStartEA, idaNodeEndEA ):
    result = GetNodeDisasmView( prevNodeStartEA, prevNodeEndEA )
    fileHandle.write( result )
    result = GetNodeDisasmView( idaNodeStartEA, idaNodeEndEA )
    result = result + "\n"
    fileHandle.write( result )

def SaveOneNode( fileHandle, idaNodeStartEA, idaNodeEndEA ):
    result = GetNodeDisasmView( idaNodeStartEA, idaNodeEndEA )
    result = result + "\n"
    fileHandle.write( result )

#      
# entry point
#

print "Script Start..."

ea = ScreenEA()
fileHandle = file("C:\\retNodes.txt", "w");

for functionAddress in Functions( SegStart(ea), SegEnd(ea) ):
    rawNodes = idaapi.FlowChart( idaapi.get_func( functionAddress ), None, idaapi.FC_PREDS )

    for idaNode in rawNodes:
        if idaapi.is_ret_block( idaNode.type ):
            if idaNode.id > 0:
                prevNode = rawNodes[ idaNode.id - 1 ]
                SaveTwoNodes( fileHandle, prevNode.startEA, prevNode.endEA, idaNode.startEA, idaNode.endEA )
            else:
                SaveOneNode( fileHandle, idaNode.startEA, idaNode.endEA )

fileHandle.close()

print "Script End..."

Далее в текстовом файле среди rop-gadgets ищем подходящий, и находим:

0x7c927e1c    mov     edi, edi
0x7c927e1e    push    ebp
0x7c927e1f    mov     ebp, esp
0x7c927e21    push    [ebp+arg_C]
0x7c927e24    push    [ebp+arg_8]
0x7c927e27    push    [ebp+arg_4]
0x7c927e2a    call    [ebp+arg_0]
0x7c927e2d    pop     ebp
0x7c927e2e    retn    10h

Вызывается ф-ция с тремя аргументами, адрес ф-ции также передается через стек.
HMODULE WINAPI LoadLibrary( __in LPCTSTR lpFileName ); - один параметр, не подходит
Зато вполне подходит LoadLibraryEx:
HMODULE WINAPI LoadLibraryEx( __in LPCTSTR lpFileName, __reserved HANDLE hFile, __in DWORD dwFlags );
Собственно одного гаджета вполне хватит для нашей программы, дальнейшая работа будет заключаться только в подготовке 
параметров для последующих ф-ций. Конечный результат:
.386
.model flat, stdcall
option casemap :none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.code

start:
    ; rop gadget:

    ; 0x7c927e1c    mov     edi, edi
    ; 0x7c927e1e    push    ebp
    ; 0x7c927e1f    mov     ebp, esp
    ; 0x7c927e21    push    [ebp+arg_C]
    ; 0x7c927e24    push    [ebp+arg_8]
    ; 0x7c927e27    push    [ebp+arg_4]
    ; 0x7c927e2a    call    [ebp+arg_0]
    ; 0x7c927e2d    pop     ebp
    ; 0x7c927e2e    retn    10h

    push 00006C6Ch ; "ll"
    push 642E3233h ; "32.d"
    push 72657375h ; "user"
    push 0000006Fh ; "o"
    push 6C6C6548h ; "Hell" (yeah! =])

    push 0         ; uExitCode for ExitProcess
    push 0deadc0deh; 
 
    push 0         ; 
    push 0012FFB0h ; ptr to "Hello"
    push 0012FFB0h ; ptr to "Hello"
    push 0         ; 
    push 7C81CB12h ; return address ( to ExitProcess ) 
  
    push 0                               ; arg_C
    push 0                               ; arg_8
    push 0012FFB8h ; ptr to "user32.dll" ; arg_4
    push 7C801D53h ; LoadLibraryExA      ; arg_0
    push 7E3A07EAh ; return address ( to MessageBoxA )

    push 7c927e1ch ; goto rop-gadget 0x7c927e1c
    ret
end start 
Теперь статическим анализом ничего понять из данного кода будет невозможно, вернее, именно для этого простейшего 
примера легко можно загрузить ntdll.dll, и найти соответствия адресов и инструкций. Однако в более сложных случаях 
код может собираться из многих модулей, модули изза ASLR/конфликтов адресов могут грузиться по разным адресам, и в 
таком случае поможет только динамический анализ( простейший трейсер ).

3 комментария:

  1. Интересно. Но не слишком ли сложно? С тем же успехом можно использовать идею стекового полиморфизма, только использовать вместо стека буфер с флагом "+x".

    ОтветитьУдалить
  2. Можно, отчего же нельзя, об этом сказано в первом приложении поста. Просто хотелось рассказать о ROP, а несчет сложности, это как раз не сложно, jump-oriented-programming посложнее будет.

    ОтветитьУдалить