Stolen Code とは
(一番下に添付した論文で見かけたのを実際にやってみたのでこれが一般的な用語なのかはわからない…)
マルウェアがどんな挙動をするのか解析する際、
- API Monitor で呼ばれてる Windows API を監視
- IAT Hookでインポートされてる関数(API)の処理を横取りして情報抜き取る
のような手法が使われるが、このような解析手法からマルウェアが自分の使ってるAPIを隠す手法の一つがStolen Code。
Stolen Code の仕組み
例えば一部のAPIモニターは、DLLのロードされてるベースアドレスから計算したエクスポート関数の場所のアドレスをあらかじめ記憶しておき、そのアドレスが実行されたら、そのDLLの関数が呼び出されたなと判断する。
この場合、例えば kernel32.dll の Beep という Windows API を監視する際、Beep関数の先頭アドレスをあらかじめ計算しておいて、そこが実行されたかどうかでBeepが呼び出されたかどうかを判断するが、もしBeepの先頭アドレス付近のコード数行を別の場所に移動しておき、Beepを使う時は移動した場所のアドレスに jmp
するような細工をしておいて、移動したコードを実行後本来のBeepの方のコードの続きに処理を戻せば、Beepの本来の "先頭アドレス" には実行が移らなくなる。
今のを図で書くと以下のようになる。
また、IAT Hookをする事を考えても、IATを変えてBeep関数のアドレスであるはずのアドレスを自分の解析用の関数のアドレスに変えたとしても、そもそもマルウェアでは call <アドレスA>
で別の場所にコピーしておいたBeepの先頭のコードがあるアドレスに飛ぶので、IATを参照しないので、フックをかける事ができなくなる。
Stolen Code の流れをx64dbgで確認
実際に Stolen Code をするプログラムを書く前に、x64dbgでどういうことをするのかを具体的に確認する。
まず、以下のような3秒ごとにビープ音を鳴らすプログラムを書いてコンパイルする。
# include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
while (1) {
Beep(5000, 500);
Sleep(3000);
}
return TRUE;
}
これをx64dbgから起動する。
無限ループするプログラムなので適当に実行してしばらくたったら一時停止し、何回かステップ実行すると、
このようにBeepを呼び出す call
命令が見えるので、[<&Beep>]
をダブルクリックしてBeepの先頭アドレスに飛ぶ。
これでBeepが見つかったので、上から3行分の命令のバイト列(48 89 5C 24 18 57 48 81 EC F0 03 00 00
)をコピーして置き、nop
で潰す。
この際、0x00007FFB477468CD
から続きが始まることをメモしておく。
次に、コピーした先頭3行をどこか適当な場所に置きたい。この実行ファイルのtext領域の最後らへんは大体いつも0で埋められてるので、そこのどこかに先ほどコピーしたバイト列を入れる。
そして、最後に mov rax,0x00007FFB477468CD; jmp rax
を入れて続きに戻れるようにする。
これで完成なので、このコピーした場所(00007FF7B4898CA7
)を覚えておく。
では、実際にBeep関数をcallしてる所を、コピーした 00007FF7B4898CA7
を呼び出すように変更する。
これで完成なので、一回 Ctrl-P
して Export
でこの変更を保存して置き、リロードしてから Ctrl-P > Import
でパッチを適用してから実行して一時停止する。
これでちゃんとエラーが起きずにビープ音が3秒ごとになっていて、かつデバッガで追うとちゃんと細工したように実行が流れてればOK。
これでデバッガでパッチをあてる事によりStolenCodeする事ができ、x64dbgには Ctrl-P > Patch File
でファイルにこのパッチをあてる事ができるので、そうすればStolenCodeの実行ファイル完成ではないかと思うかもしれないが、よくよく考えるとパッチしているのはこの実行ファイルだけではなく、kernel32.dllの方もそうなので、kernel32.dllをパッチしては意味がない(被害者のPCでは動かないみたいなしょぼいマルウェアになってしまう)。
Stolen Code 実装
上記のStolenCodeの流れを実行ファイルからやった例が以下。
# include <windows.h>
# include <stdio.h>
typedef unsigned __int64 QWORD;
typedef BOOL(*BEEP)(DWORD dwFreq, DWORD dwDuration);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
// 1: nop kernel32.dll Beep's first instructions
HMODULE hkernel32 = GetModuleHandle("kernel32.dll");
QWORD patch_addr = (QWORD)GetProcAddress(hkernel32, "Beep");
DWORD curProtection, temp;
int len = 13;
VirtualProtect((void*)patch_addr, len, PAGE_EXECUTE_READWRITE, &curProtection);
memset((void*)patch_addr, 0x90, len);
VirtualProtect((void*)patch_addr, len, curProtection, &temp);
// 2: write stolen code to code cave
TCHAR szMyPath[MAX_PATH];
GetModuleFileName(NULL, szMyPath, _countof(szMyPath));
QWORD base = (QWORD)GetModuleHandle((LPCSTR)szMyPath);
QWORD stolen_addr = base + 0x7efd;
int len2 = 25;
VirtualProtect((void*)stolen_addr, len2, PAGE_EXECUTE_READWRITE, &curProtection);
memset((void*)stolen_addr, 0x90, len2);
*(BYTE*)(stolen_addr + 0) = 0x48;
*(BYTE*)(stolen_addr + 1) = 0x89;
*(BYTE*)(stolen_addr + 2) = 0x5c;
*(BYTE*)(stolen_addr + 3) = 0x24;
*(BYTE*)(stolen_addr + 4) = 0x18;
*(BYTE*)(stolen_addr + 5) = 0x57;
*(BYTE*)(stolen_addr + 6) = 0x48;
*(BYTE*)(stolen_addr + 7) = 0x81;
*(BYTE*)(stolen_addr + 8) = 0xec;
*(BYTE*)(stolen_addr + 9) = 0xf0;
*(BYTE*)(stolen_addr + 10) = 0x03;
*(BYTE*)(stolen_addr + 11) = 0x00;
*(BYTE*)(stolen_addr + 12) = 0x00;
*(BYTE*)(stolen_addr + 13) = 0x48;
*(BYTE*)(stolen_addr + 14) = 0xb8;
*(QWORD*)(stolen_addr + 15) = (QWORD)(patch_addr + len); // mov rax, <original addr of kernel32.dll beep>
*(BYTE*)(stolen_addr + 23) = 0xff;
*(BYTE*)(stolen_addr + 24) = 0xe0; // jmp rax
VirtualProtect((void*)stolen_addr, len2, curProtection, &temp);
// 3: custom beep to first go to stolen code
BEEP custom_beep = (BEEP)stolen_addr;
// 4: use beep
while (1) {
custom_beep(5000, 500);
Sleep(3000);
}
return TRUE;
}
やってることはx64dbgでの流れと同じだが、違うところはBeepの場所などのアドレス計算の所であり、例えばBeepの先頭アドレスの場所(patch_addr
)は以下のようにして計算している。
HMODULE hkernel32 = GetModuleHandle("kernel32.dll");
QWORD patch_addr = (QWORD)GetProcAddress(hkernel32, "Beep");
また、0埋めされていた領域にコピーしたコードを貼り付けたが、そのアドレスはまず自分のファイルのロードされてるアドレスを下記コードの最初の3行で取得し、最後の一行でオフセットを足して0埋めの領域のアドレスを計算しているが、0x7efd
の部分は、先ほど作成したパッチ(.1337
のファイル)を見てもらえればいい。
TCHAR szMyPath[MAX_PATH];
GetModuleFileName(NULL, szMyPath, _countof(szMyPath));
QWORD base = (QWORD)GetModuleHandle((LPCSTR)szMyPath);
QWORD stolen_addr = base + 0x7efd;
そして、最後に call Beep
ではなく、 call <コピーしたコードのアドレス>
にしているのが、
BEEP custom_beep = (BEEP)stolen_addr;
...
custom_beep(5000, 500);
の部分。
API Monitor 回避できてるか確認
では実際に API Monitor で検知されないかを、普通にBeepを呼び出してるファイルとStolenCodeの方のファイルで比較してみる。
まず普通にBeepを呼び出してる方を監視すると、このように何度もBeepが検知されてる。
次に、StolenCodeの方を見てみると、一回Beepが呼ばれた後その後Beep音が鳴りっぱなしのままハングした。
正直なんでこんな挙動になったかはわからないし、なんで一回だけ検知されるのかわからないが、もしかしたらBeepの場所を nop
に変える時にBeepの先頭アドレスを触るので検知されてるのかなとか思ったがよくわからない。
けど、API Monitorから起動するとクラッシュして止まるのに、ちゃんと(API Monitor挟まないで)実行するとクラッシュせずに3秒おきに実行されるので、一応 API Monitor を回避できることがわかる。
IAT Hook 回避できてるか確認
実際に下記のコードでビルドしたDLLを、普通にBeepを呼び出すプログラムとStolenCodeの方のプログラム両方に DLL Injection で注入してみたが、ちゃんとBeepの方はフックがかかってMessageBoxが表示されるのに対し、StolenCodeの方はフックがかからずMessageBoxが表示されなかった。
BeepをIAT HookするDLLのソースコード
# include "pch.h"
# include <windows.h>
# include <Dbghelp.h>
# include <sstream>
# pragma comment(lib, "Dbghelp")
typedef BOOL* (WINAPI* OriginalBeep)(DWORD dwFreq, DWORD dwDuration);
OriginalBeep originalBeep;
void WINAPI MyBeep(DWORD dwFreq, DWORD dwDuration) {
MessageBox(NULL, TEXT("Hooked Beep!!!"), TEXT("success"), MB_OK | MB_ICONEXCLAMATION);
originalBeep(dwFreq, dwDuration);
return;
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
TCHAR szMyPath[MAX_PATH];
ULONG cbSize = 0;
GetModuleFileName(NULL, szMyPath, _countof(szMyPath));
HANDLE hModule = GetModuleHandle((LPCSTR)szMyPath);
std::stringstream s;
s << szMyPath << std::endl;
MessageBox(NULL, s.str().c_str(), TEXT("target"), MB_OK);
originalBeep = (OriginalBeep)GetProcAddress(GetModuleHandle("kernel32.dll"), "Beep");
PIMAGE_IMPORT_DESCRIPTOR pImageImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(hModule, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &cbSize);
for (; pImageImportDescriptor->Name; pImageImportDescriptor++) {
LPCSTR pModuleName = (LPCSTR)((PBYTE)hModule + pImageImportDescriptor->Name);
DWORD ModuleBase = (DWORD)GetModuleHandle(pModuleName);
PIMAGE_THUNK_DATA pFirstThunk = (PIMAGE_THUNK_DATA)((PBYTE)hModule + pImageImportDescriptor->FirstThunk);
PIMAGE_THUNK_DATA pOriginalFirstThunk = (PIMAGE_THUNK_DATA)((PBYTE)hModule + pImageImportDescriptor->OriginalFirstThunk);
for (; pFirstThunk->u1.Function; pFirstThunk++, pOriginalFirstThunk++) {
FARPROC pfnImportedFunc = (FARPROC)(pFirstThunk->u1.Function);
PIMAGE_IMPORT_BY_NAME pImageImportByName = (PIMAGE_IMPORT_BY_NAME)((PBYTE)hModule + pOriginalFirstThunk->u1.AddressOfData);
if (pfnImportedFunc == (FARPROC)originalBeep) {
MEMORY_BASIC_INFORMATION mbi;
DWORD dwJunk = 0;
VirtualQuery(pFirstThunk, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
if (!VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, &mbi.Protect)) {
MessageBox(NULL, TEXT("VirtualProtect Failed!"), TEXT("error"), MB_OK | MB_ICONERROR);
return FALSE;
}
pFirstThunk->u1.Function = (ULONGLONG)(DWORD_PTR)MyBeep;
if (VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwJunk))
break;
}
}
}
}
return TRUE;
}
ソース