Делать это можно по разному, можно через полиморфизм/метаморфизм/обфускацию, а можно изменить подход - убрать сам объект скрытия. Ведь нет кода - нечего и обнаруживать. Что же тогда процессор будет выполнять, если кода нет? Ну, на самом деле он есть, просто в другом модуле. Речь пойдет о 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
Вызывается ф-ция с тремя аргументами, адрес ф-ции также передается через стек.
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/конфликтов адресов могут грузиться по разным адресам, и в
таком случае поможет только динамический анализ( простейший трейсер ).
Интересно. Но не слишком ли сложно? С тем же успехом можно использовать идею стекового полиморфизма, только использовать вместо стека буфер с флагом "+x".
ОтветитьУдалитьМожно, отчего же нельзя, об этом сказано в первом приложении поста. Просто хотелось рассказать о ROP, а несчет сложности, это как раз не сложно, jump-oriented-programming посложнее будет.
ОтветитьУдалитьИнтересно. Благодарю!
ОтветитьУдалить