在ShellCode里面使用異常處理(Win64位平臺)
網上關于ShellCode編寫的文章很多,但介紹如何在ShellCode里面使用異常處理的卻很少。筆者前段時間寫了一個ShellCode,其中有一個功能是內存加載多個別人寫的DLL插件,然后調用里面的函數,結果因為某個DLL函數里面發生了異常,導致ShellCode進程直接閃退,所以學習了一下在ShellCode里面如何使用異常處理的方法。
Windows程序的異常處理,其實是三者結合的:操作系統、編譯器和程序代碼。因為x86下異常處理的文章太多,所以本文只介紹Win64下的。X86的異常(本文僅談論SEH異常)一般的流程為:進入 try...語句之前先注冊到異常鏈表,執行完代碼后,再摘除,不管有沒有異常發生,這個步驟都是必不可少的,所以多多少少會影響程序的性能。
而Win64的異常處理,是基于表的。也就是說,編譯器在編譯代碼的時候,會同時對每個函數(還分非葉,不深究)生成一個異常表,最后鏈接到PE的異常表里。當程序發生異常的時候,操作系統跟根據當前地址,枚舉所有異常表,如果地址位于某個表的開始和結束地址之間,則使用這個表處理。相對來說,這個比X86更加安全和高效。
這個異常表,稱為RUNTIME_FUNCTION,MSDN里面定義如下:
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress;
DWORD EndAddress;
union {
DWORD UnwindInfoAddress;
DWORD UnwindData;
} DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;
這些表連續存放在PE的異常目錄中,每個表對應一個函數,所有成員都是相當于ImageBase的開始相對地址。其中BeginAddress表示這個函數的開始地址,EndAddress則對應結束地址,最重要的,是UnwindInfoAddress,它對應另外一個結構UNWIND_INFO:
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes;
UBYTE FrameRegister : 4;
UBYTE FrameOffset : 4;
UNWIND_CODE UnwindCode[1];/* UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
* union {
* OPTIONAL ULONG ExceptionHandler;
* OPTIONAL ULONG FunctionEntry;
* };
* OPTIONAL ULONG ExceptionData[]; */} UNWIND_INFO, *PUNWIND_INFO;
成員如下:
Version:版本號,一般為1,最高目前是2。
Flags:取值如下
#define UNW_FLAG_EHANDLER 0x01 ---表示有except塊
#define UNW_FLAG_UHANDLER 0x02 ---表示有finally塊
#define UNW_FLAG_CHAININFO 0x04 ---表示后面是另外一個Runtime_Function
SizeOfProlog:函數頭到try塊的位置。
CountOfCodes:表示后面有多少個UNWIND_CODE 結構。注意:這個數值是偶數對齊的,如果為奇數,說明最后一個為空值的UNWIND_CODE。
UNWIND_CODE數組:實際上就是回滾表,保存了進入異常代碼前的寄存器狀態,用于異常處理后回滾到原來的狀態。
ExceptionHandler(可選):如果Flags包含了UNW_FLAG_EHANDLER 或UNW_FLAG_UHANDLER,則ExceptionHandler指向異常處理函數。其中Delphi語言是指向system.pas里面的函數_DelphiExceptionHandler;VS相對來說復雜一些,如果是SEH異常,一般是指向__C_specific_handler函數。
ExceptionData(可選):這個具體語言是不同的,所以windbg解釋異常表的時候也是只解釋到上一個成員。這個一般是(Delphi語言則肯定也只會)指向一個SCOPE_TABLE結構:
typedef struct _SCOPE_TABLE
{
ULONG Count;
struct
{
ULONG BeginAddress;
ULONG EndAddress;
ULONG HandlerAddress;
ULONG JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;
簡單一點來說,Runtime_function是給操作系統判斷異常發生在哪個函數里面,SCOPE_TABLE則是在異常處理函數里面使用的(觸發異常處理函數的時候,會傳遞過去),主要記錄了更精確的異常發生位置區間和需要跳轉處理的地址。
如果想在ShellCode里面使用異常,則請進行如下步驟:
1、提取語言的異常處理函數,讓它稱為ShellCode的一個函數(Delphi語言是system.pas里面的_DelphiExceptionHandler函數,VS則應該提取C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\amd64\chandler.c里面的__C_specific_handler函數)。注意提取后的ShellCode化(修正API調用等)。當然,你也可以完全自己編寫處理函數。
2、在需要使用異常處理的函數像往常一樣使用try catch...try except...,在保存ShellCode的時候,使用API函數RtlLookupFunctionEntry查找這個函數的Runtime_Function,然后查找成員UNWIND_INFO,修正所有的相對地址后,跟ShellCode放在一起。
3、ShellCode運行后,調用API函數RtlAddFunctionTable將第二步修正的表添加到系統。
附件說明:
1、PE64異常表枚舉工具.exe:一個用于枚舉異常表的小工具。對于Delphi編譯的PE,還會枚舉SCOPE_TABLE。
2、ShellCode64.bin:一個在ShellCode里面使用異常的DEMO。
3、LoadTest.exe:加載ShellCode64.bin進行測試的程序,代碼見LoadTest.dpr。
4、Dll.dll:測試調用Dll里面有異常發生的函數。
5、Dll.dpr、LoadTest.dpr。Delphi代碼。
其中ShellCode64.bin的部分代碼如下:
“程序內置單個異常測試”按鈕:
procedure OnButton1Click(hForm1: HWND);
var
bExcept: Boolean;
i, j, nRet: Integer;
szMaker: array[0..3] of AnsiChar;
szBuffer: array[0..127] of AnsiChar;
begin
i := 10;
j := 0;
bExcept := False;
try
nRet := i div j;
except
WindowsAPI^.fnMessageBoxA(hForm1, '進入except節', FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
nRet := 0;
bExcept := True;
end;
if bExcept then
begin
WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('發生異常!nRet=%d'), nRet);
end
else
begin
WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('沒有異常!nRet=%d'), nRet);
end;
WindowsAPI^.fnMessageBoxA(hForm1, szBuffer, FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
end;
“程序內置鑲套異常測試”按鈕:這個實際上是先觸發除0異常,再在異常里面通過給一個空指針賦值產生第二個異常:
procedure OnButton2Click(hForm1: HWND);
var
bExcept: Boolean;
i, j, nRet: Integer;
szMaker: array[0..3] of AnsiChar;
szBuffer: array[0..127] of AnsiChar;
p: Pointer;
begin
i := 10;
j := 0;
p := nil;//空指針
bExcept := False;
try
nRet := i div j;
except
WindowsAPI^.fnMessageBoxA(hForm1, '進入except節1', FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
try
PInteger(p)^ := 999;
except
bExcept := True;
WindowsAPI^.fnMessageBoxA(hForm1, '進入except節2', FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
end;
nRet := 0;
bExcept := True;
end;
if bExcept then
begin
WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('發生異常!nRet=%d'), nRet);
end
else
begin
WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('沒有異常!nRet=%d'), nRet);
end;
WindowsAPI^.fnMessageBoxA(hForm1, szBuffer, FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
end;
“Dll函數1"按鈕和“Dll函數2"按鈕的代碼其實是一樣的,只不過是Dll導出的函數名稱不一樣而已:
procedure OnButton4Click(hForm1, hEdit: HWND);
type
TGetInteger = function: Integer; stdcall;
var
szDllFileName: array[0..MAX_PATH - 1] of AnsiChar;
hDll: HMODULE;
MyGetIntege: TGetInteger;
var
bExcept: Boolean;
nRet: Integer;
szMaker: array[0..3] of AnsiChar;
szBuffer: array[0..127] of AnsiChar;
begin
WindowsAPI^.fnGetWindowTextA(hEdit, szDllFileName, MAX_PATH);
hDll := WindowsAPI^.fnLoadLibraryA(szDllFileName);
if hDll = 0 then
begin
WindowsAPI^.fnMessageBoxA(hForm1, FixPAnsiChar('Dll文件加載失敗!'), FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
Exit;
end;
@MyGetIntege := WindowsAPI^.fnGetProcAddress(hDll, FixPAnsiChar('GetInteger1'));
if @MyGetIntege = nil then
begin
WindowsAPI^.fnMessageBoxA(hForm1, FixPAnsiChar('函數GetInteger1獲取失敗!'), FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
WindowsAPI^.fnFreeLibrary(hDll);
Exit;
end;
bExcept := False;
try
nRet := MyGetIntege;
except
WindowsAPI^.fnMessageBoxA(hForm1, '進入except節', FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
nRet := 0;
bExcept := True;
end;
if bExcept then
begin
WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('發生異常!nRet=%d'), nRet);
end
else
begin
WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('沒有異常!nRet=%d'), nRet);
end;
WindowsAPI^.fnMessageBoxA(hForm1, szBuffer, FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
end
Dll主要導出兩個函數,一個有異常,一個沒有異常(GetInteger2實際上也是可能存在異常的---如果隨機數為0),你也可以使用VS之類編寫一個DLL來測試:
function GetInteger1:Integer;stdcall; var i,j:Integer; begin i:=0; j:=Random(100); Result:=j div i; end; function GetInteger2:Integer;stdcall; var i,j:Integer; begin i:=Random(100); j:=Random(10000); Result:=j div i; end;
ShellCode的入口函數:
procedure _Start;
var
p: PAnsiChar;
nCount: Integer;
SDKForm: TSDKForm; //SDK窗口類
begin
if not InitWindowsAPI then Exit; //初始化全局API函數表
p := FixPAnsiChar(fnA); //獲取異常表信息位置
nCount := PInteger(p)^;
Inc(p, sizeof(Integer));
if not WindowsAPI^.fnRtlAddFunctionTable(p, nCount, DWORD64(@_Start)) then
begin
WindowsAPI^.fnMessageBoxA(0, FixPAnsiChar('fnRtlAddFunctionTable Error'), FixPAnsiChar('Caption'), MB_ICONWARNING + MB_TOPMOST);
Exit;
end;
SDKForm.CreateWindow;
SDKForm.MessageLoop;
DoneWindowsAPI;//釋放全局API函數表
end;
里面的函數FixPAnsiChar其實是在X86下使用的,Win64下Delphi使用的都是相對地址了,可以直接跟平時一樣使用字符串的。
這是我寫的第五個shellcode程序,如有錯漏之處,敬請指正!