1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Stolen Code

Last updated at Posted at 2021-06-30

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の本来の "先頭アドレス" には実行が移らなくなる。
今のを図で書くと以下のようになる。

image.png

また、IAT Hookをする事を考えても、IATを変えてBeep関数のアドレスであるはずのアドレスを自分の解析用の関数のアドレスに変えたとしても、そもそもマルウェアでは call <アドレスA> で別の場所にコピーしておいたBeepの先頭のコードがあるアドレスに飛ぶので、IATを参照しないので、フックをかける事ができなくなる。

Stolen Code の流れをx64dbgで確認

実際に Stolen Code をするプログラムを書く前に、x64dbgでどういうことをするのかを具体的に確認する。

まず、以下のような3秒ごとにビープ音を鳴らすプログラムを書いてコンパイルする。

normal-beep.cpp
# include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	while (1) {
		Beep(5000, 500);
		Sleep(3000);
	}
	return TRUE;
}

これをx64dbgから起動する。
無限ループするプログラムなので適当に実行してしばらくたったら一時停止し、何回かステップ実行すると、
image.png
このようにBeepを呼び出す call 命令が見えるので、[<&Beep>] をダブルクリックしてBeepの先頭アドレスに飛ぶ。
image.png
これでBeepが見つかったので、上から3行分の命令のバイト列(48 89 5C 24 18 57 48 81 EC F0 03 00 00)をコピーして置き、nop で潰す。
image.png
この際、0x00007FFB477468CD から続きが始まることをメモしておく。

次に、コピーした先頭3行をどこか適当な場所に置きたい。この実行ファイルのtext領域の最後らへんは大体いつも0で埋められてるので、そこのどこかに先ほどコピーしたバイト列を入れる。
image.png
そして、最後に mov rax,0x00007FFB477468CD; jmp rax を入れて続きに戻れるようにする。
image.png
これで完成なので、このコピーした場所(00007FF7B4898CA7)を覚えておく。

では、実際にBeep関数をcallしてる所を、コピーした 00007FF7B4898CA7 を呼び出すように変更する。
image.png

これで完成なので、一回 Ctrl-P して Export でこの変更を保存して置き、リロードしてから Ctrl-P > Import でパッチを適用してから実行して一時停止する。

これでちゃんとエラーが起きずにビープ音が3秒ごとになっていて、かつデバッガで追うとちゃんと細工したように実行が流れてればOK。

これでデバッガでパッチをあてる事によりStolenCodeする事ができ、x64dbgには Ctrl-P > Patch File でファイルにこのパッチをあてる事ができるので、そうすればStolenCodeの実行ファイル完成ではないかと思うかもしれないが、よくよく考えるとパッチしているのはこの実行ファイルだけではなく、kernel32.dllの方もそうなので、kernel32.dllをパッチしては意味がない(被害者のPCでは動かないみたいなしょぼいマルウェアになってしまう)。

Stolen Code 実装

上記のStolenCodeの流れを実行ファイルからやった例が以下。

stolen-code.cpp
# 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が検知されてる。

image.png

次に、StolenCodeの方を見てみると、一回Beepが呼ばれた後その後Beep音が鳴りっぱなしのままハングした。

image.png

正直なんでこんな挙動になったかはわからないし、なんで一回だけ検知されるのかわからないが、もしかしたら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;
}

ソース

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?