🐇

materiaのfirst.dllをRustで読み込もうとした作業メモ

2023/03/12に公開

実行環境

  • Macbook Air M1 2020
  • Parallels Desktop 18
  • Windows 11
  • rustc 1.67.1 (d5a82bbd2 2023-02-07)
  • stable-i686-pc-windows-msvc

本環境にてmateria.exeまたはssp.exefirst.dllが動作することは確認済み。

SHIORI DLL仕様

http://usada.sakura.vg/contents/shiori.html#shiori20

extern "C" __declspec(dllexport) BOOL __cdecl load(HGLOBAL h, long len);
function load(h: hglobal; len: longint): boolean; cdecl;
extern "C" __declspec(dllexport) BOOL __cdecl unload();
function unload: boolean; cdecl;
extern "C" __declspec(dllexport) HGLOBAL __cdecl request(HGLOBAL h, long *len);
function request(h: hglobal; var len: longint): hglobal; cdecl; export;

load の第1引数に DLL のディレクトリパスが渡される。SHIORI が固有のデータファイル等を持つ場合は、ここからカレントディレクトリを取得し、そこに自らのデータファイル一式を納めなくてはならない。

http://usada.sakura.vg/contents/specification2.html#shioriwindows

通信データは Windows API globalalloc によって確保される gptr を用いて作成される。リクエスト文字列、レスポンス文字列ともにこの形式のバッファを用いて送受信される。

loadの第一引数にはGlobalAlloc(GPTR, length)で確保したポインタにDLLのディレクトリパスを引き渡す。第二引数に確保したメモリ長を引き渡す。
ここで引き渡すディレクトリパスはC:\materia\ghost\first\master\ghost\\で終わる文字列であり、C系の文字列でよくある\0の終端は含まない。(これは実際にダミーのdllを作りmateria.exeで確認した)

読み込みコード

fn main() {
    let library_path = "C:\\materia\\ghost\\first\\ghost\\master\\first.dll";
    let lib = Library::open(library_path).unwrap();
    let unload = unsafe { lib.symbol::<unsafe extern "C" fn() -> bool>("unload") }.unwrap();
    let load = unsafe { lib.symbol::<unsafe extern "C" fn(h: isize, len: usize) -> bool>("load") }
        .unwrap();
    let request = unsafe {
        lib.symbol::<unsafe extern "C" fn(h: isize, len: *mut usize) -> isize>("request")
    }
    .unwrap();

    let path = b"C:\\materia\\ghost\\first\\ghost\\master\\";
    let hglobal = unsafe { GlobalAlloc(GPTR, path.len()) };
    if hglobal == 0 {
        let err = unsafe { GetLastError() };
        panic!("failed to allocate memory: {err:?}\n");
    }
    let size = unsafe { GlobalSize(hglobal) };

    let ptr = hglobal as *mut u8;
    let ptr = unsafe { std::slice::from_raw_parts_mut(ptr, size) };
    unsafe {
        memcpy(
            ptr.as_mut_ptr() as *mut c_void,
            path.as_ptr() as *const c_void,
            size,
        );
    }

    println!("execute load(0x{hglobal:x}, {size})");
    let ret = unsafe { load(hglobal, size) };
    println!("executed load(): {ret}");
}

DLL読み込みにdlopen、Windows関係でwindowsのcrateを利用している。

実行結果

PS Y:\uka-rs\target\release> .\dll_loader.exe
execute load(0x932c920, 36)
PS Y:\uka-rs\target\release> $LastExitCode
0

loadの呼び出し後のprintln!が動作せず、DLL内部でexit(0)されているような挙動になっている。
念の為println!だけではなくstdout().flush().unwrap()の呼び出しや、ファイルの作成など副作用を伴う操作を行ってみたが同様の挙動になる。

他のunloadrequestではこのような挙動にはならず読み込みには成功している。
また、SSPに同梱されたemily4yaya.dllでは想定した挙動になることも確認している。

materia.exessp.exeから呼び出すことができるため、仮説としてはloadを呼び出す前に何かしらの事前条件を満たす必要があるのではと考えている。

DLL Export Viewer

DLL Export Viewerで表示した例。

http://ssp.shillest.net/ukadoc/manual/spec_dll.html

☆Borland系C/C++コンパイラでは_loadのように頭にアンダースコアがつくようですがそちらにも対応した方が良いでしょう。

とある通り、呼び出そうした関数名が間違っている可能性も考えたが、DLL Export Viewer上では想定通りの関数名になっている。
また、関数名を意図的に間違えた場合にはlib.symbol::<unsafe extern "C" fn(h: isize, len: usize) -> bool>("load")のところでエラーが発生する。

Mutexの設定

http://usada.sakura.vg/contents/objects.html#mutex

SAKURA は起動中 "sakura" という名前の MUTEX オブジェクトを保持します。

伺かの仕様としてMutexがある。そこでMutexもDLL読み込み前に作成しておく。

let name = PCSTR::from_raw("sakura").as_ptr();
let result = unsafe { CreateMutexA(None, FALSE, name) };
if result.is_err() {
    let err = GetLastError();
    panic!("create mutex error: {err:?}");
}

CreateMutexACreateMutexWの両方で試したが動作は変わらなかった。

念の為IDA Freeでfirst.dllをリバースエンジニアリングしたところ、load内でsakuraという名前でOpenMutexを行っているところは見つけきれなかった。(調査不足の可能性はあり)

FMOの設定

http://usada.sakura.vg/contents/objects.html#fmo

SAKURA は起動中 "Sakura" という名前の filemapping object を保持します。

伺かの仕様としてFMOがある。そこでMutexもDLL読み込み前に作成しておく。

・メモリマップ
0-3 全体のサイズを示す long 値
4- データ本体

http://ssp.shillest.net/ukadoc/manual/spec_fmo_mutex.html

最初の4バイト(0~3バイト目)は確保されているFMOのサイズを示します。
これは書き込まれている情報の長さではなく、あくまでもFMO自体の確保サイズを示す固定値です。
値は現在のところリトルエンディアンで0x00010000、つまり64KB固定になります。

長さはFMO自体のサイズになることに注意。

let result = CreateFileMappingA(
    INVALID_HANDLE_VALUE,
    None,
    PAGE_READWRITE,
    0,
    1024 * 64,
    PCSTR::from_raw("Sakura".as_ptr()),
);
if result.is_err() {
    let err = GetLastError();
    panic!("create file mapping error: {err:?}");
}
let handle = result.unwrap();

let ptr = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 1024 * 64);
if ptr.is_null() {
    let err = GetLastError();
    panic!(""map view of file error: {err:?}");
}
let bytes = format!(
    "53137ee8825085dba1707e3bea9e474b.path\x01C:\\materia\\\r\n\
    53137ee8825085dba1707e3bea9e474b.hwnd\x010\r\n\
    53137ee8825085dba1707e3bea9e474b.name\x01sakura\r\n\
    53137ee8825085dba1707e3bea9e474b.keroname\x01unyu\r\n\
    53137ee8825085dba1707e3bea9e474b.sakura.surface\x010\r\n\
    53137ee8825085dba1707e3bea9e474b.kero.surface\x010\r\n\0").as_bytes();
let len = (1024 * 64) as i32;
let v = [len.to_be_bytes().to_vec(), bytes.to_vec()].concat();
memcpy(ptr, v.as_ptr() as *const c_void, v.len());

しかし、これで動作は変わらなかった。

念の為IDA Freeでfirst.dllをリバースエンジニアリングしたところ、load内でSakuraという文字列を扱っているところを見つけきれなかった。(調査不足の可能性はあり)

RegisterWindowMessageAの設定

IDA Freeでmateria.exeをリバースエンジニアリングしたところ、RegisterWindowMessageAをDLLの呼び出し前に読んでいることを確認できた。

let name = PCSTR::from_raw("Sakura".as_ptr());
let res = RegisterWindowMessageA(name);
if res == 0 {
    let err = GetLastError();
    panic!("RegisterWindowMessageA error: {err:?}");
}

しかし、これも動作は変わらなかった。

Windowの作成

伺かはGUIアプリケーションであり、ウィンドウに依存した何かがあるかもしれないと考える。
そこで、実際にウィンドウを作成する。

let class_name = PCSTR::from_raw("main window class".as_ptr);
let mut wc = WNDCLASSA::default();
wc.lpfnWndProc = Some(window_proc);
wc.hInstance = HINSTANCE(0);
wc.lpszClassName = class_name;
let atom = RegisterClassA(&wc);
if atom == 0 {
    let err = GetLastError();
    panic!("RegisterClassA error: {err:?}");
}
let hWnd = CreateWindowExA(
WINDOW_EX_STYLE::default(),
    class_name,
    PCSTR::from_raw("main window".as_ptr()),
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    HWND(0),
    HMENU(0),
    HINSTANCE(0),
    None,
);

しかし、これも動作は変わらなかった。

exeの実行ディレクトリをmateria.exeに合わせる

IDA Freeでfirst.dllをリバースエンジニアリングしたところ、loadのところでmateria.exeを見ている箇所があった。
そこで、materia.exeと同一ディレクトリに置いて実行することで、解決できないかと考えたが動作は変わらなかった。

その他、考えられるところ

IDA Freeでfirst.dllをリバースエンジニアリングしたところ、loadのところでLoadCursorA(hInstance, (LPCSTR)0x7D00)の呼び出しがあることを確認している。
しかし0x7D00に対応するカーソルリソースは定義されていない。

https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-loadcursora

そのため、これに対応するCreate処理が必要かもしれない。ただし、materia.exe側ではCreateCursorの呼び出しはないようにも見えるため、ここを解決することで呼び出せるようになるかは不明になる。

また、いくつか対応を行ってきたが、その呼び出し方を間違えている可能性はある。
特にWin32 APIに不慣れなためその可能性は十分にあるのだが、正しく呼び出せているかの検証方法が思いつかないため詰めることができない。

現時点ではここを詰めるにはリバースエンジニアリングを行い、そこで生成されるコードを本気で読み込むぐらいしかない。これまで雰囲気で機械語を読んでいたがそろそろ覚えるべきか。

追記(2023/03/21)

https://twitter.com/ponapalt/status/1636187187615240193

Twitterで嘆いていたら教えていただくことができました。ありがとうございます!

mai.dll/sayuri.dllを取り出す

let mut exe_path = "C:\\materia2\\materia.exe\0";
let result = LoadLibraryExA(
    PCSTR::from_raw(exe_path.as_ptr()),
    None,
    LOAD_LIBRARY_AS_DATAFILE,
);
if result.is_err() {
    // error処理
}
let h_instance = result.unwrap();

let result = FindResourceA(h_instance, PCSTR("#102".as_ptr()), PCSTR("#12".as_ptr());
if result.is_err() {
    // error処理
}
let hr_src = result.unwrap();

let result = LoadResource(h_instance, hr_src);
if result.is_err() {
    // error処理
}
let h_global = result.unwrap();

let ptr = LockResource(h_global);
if ptr.is_null() {
    // error処理
}

let size = SizeofResource(h_instance, hr_src);
if size == 0 {
    // error処理
}

let data = unsafe { std::slice::from_raw_parts(ptr as *const u8, size as usize) };
let mut f = File::create("C:\\materia\\mai.dll").unwrap();
f.write_all(data).unwrap();

上記のコードはmai.dllのものですが同様に#104sayuri.dllと保存することができます。

このDLLの取り出しをした状態で実行ファイル名をmateria.exeにすることでfirst.dllloadを呼び出しができるようになります。

IAT Hookを利用した実行ファイル名の変更

type GetModuleFileNameA =
    extern "stdcall" fn(h_module: *const c_void, lp_filename: PSTR, n_size: u32) -> u32;
static ORIGINAL_GET_MODULE_FILE_NAME_A: Lazy<Mutex<GetModuleFileNameA>> = Lazy::new(|| {
    Mutex::new({
        let hmodule = unsafe { GetModuleHandleA(PCSTR("kernel32.dll\0".as_ptr())).unwrap() };
        let lpprocname = PCSTR("GetModuleFileNameA\0".as_ptr());
        let address = unsafe { GetProcAddress(hmodule, lpprocname) }.unwrap();

        unsafe { *(address as *const GetModuleFileNameA) }
    })
});

extern "stdcall" fn get_module_file_name_a(
    h_module: *const c_void,
    lp_filename: PSTR,
    n_size: u32,
) -> u32 {
    if h_module.is_null() {
        let fake_exe_name = "C:\\materia\\materia.exe";
        unsafe {
            ptr::copy_nonoverlapping(
                fake_exe_name.as_ptr() as *const c_void,
                lp_filename.0 as *mut c_void,
                fake_exe_name.len(),
            );
        }
        (fake_exe_name.len()) as u32
    } else {
        match ORIGINAL_GET_MODULE_FILE_NAME_A.lock() {
            Ok(f) => f(h_module, lp_filename, n_size),
            Err(_) => 0,
        }
    }
}

unsafe fn hook_iat(module_name: &str) {
    let module_name = format!("{}\0", module_name);
    let h_module = GetModuleHandleA(PCSTR(module_name.as_ptr())).unwrap();

    let p_dos_header = h_module.0 as *const IMAGE_DOS_HEADER;
    let p_nt_headers = (h_module.0 as *const u8).add((*p_dos_header).e_lfanew as usize)
        as *const IMAGE_NT_HEADERS32;

    let p_import_directory = (*p_nt_headers).OptionalHeader.DataDirectory.get(1).unwrap();
    let p_import_descriptor = (h_module.0 as *const u8)
        .add(p_import_directory.VirtualAddress as usize)
        as *const IMAGE_IMPORT_DESCRIPTOR;

    let mut p_import_descriptor_mut = p_import_descriptor;
    let mut hooked = false;

    let address = GetProcAddress(
        GetModuleHandleA(PCSTR("kernel32.dll\0".as_ptr())).unwrap(),
        PCSTR("GetModuleFileNameA\0".as_ptr()),
    );
    let original_function: *mut c_void = mem::transmute(address);

    while (*p_import_descriptor_mut).Name != 0 && !hooked {
        let p_thunk = (h_module.0 as *const u8).add((*p_import_descriptor_mut).FirstThunk as usize)
            as *mut IMAGE_THUNK_DATA32;

        let mut p_thunk_mut = p_thunk;
        while (*p_thunk_mut).u1.Ordinal != 0 {
            let p_function = (*p_thunk_mut).u1.Function as *mut _;

            if p_function == original_function {
                let get_module_file_name_a: GetModuleFileNameA = get_module_file_name_a;
                let new_function: u32 = get_module_file_name_a as *const () as u32;

                let mut old_protect = PAGE_PROTECTION_FLAGS(0);
                let protect_result = VirtualProtect(
                    p_thunk_mut as *mut _ as _,
                    std::mem::size_of::<u32>() as _,
                    Memory::PAGE_EXECUTE_READWRITE,
                    &mut old_protect as *mut _ as _,
                );

                if protect_result == BOOL(0) {
                    panic!("VirtualProtect failed to change memory protection.");
                }

                (*p_thunk_mut).u1.Function = new_function;

                // Restore original protection
                let mut dummy_protect = PAGE_PROTECTION_FLAGS(0);
                VirtualProtect(
                    p_thunk_mut as *mut _ as _,
                    std::mem::size_of::<u32>() as _,
                    old_protect,
                    &mut dummy_protect as *mut _ as _,
                )
                .ok();

                hooked = true;
                break;
            }

            p_thunk_mut = p_thunk_mut.add(1);
        }

        p_import_descriptor_mut = p_import_descriptor_mut.add(1);
    }
}

hook_iatをDLLの読み込み後にhook_iat("/path/to/file.dll")と呼び出すことでGetModuleFileNameAを上書きすることができます。
コード自体はChatGPTを利用して生成しているため、ポインタの計算や多重ループなどでかなり冗長で危険なコードになっています。実際に組み込むさいにはPE形式を扱うライブラリなどを利用するのが無難かと思います。

コード

https://github.com/k-kinzal/uka-rs/blob/cc119077fd531b929efcb4276647164321858b06/dll_loader/src/main.rs

特に整理していないため読むのは難儀だと思いますが、もし同じような問題に遭遇した人の参考になるように検証していたコードを置いておきます。

Discussion