Compare commits

..
Author SHA1 Message Date
Ole André Vadla Ravnås b9a4940b5e ci: Run the Barebone agents in live guests
Build frida-tests and run each guest's live tests, resuming the
prepared guest from the cache. Also run when the host side or the
harness changes.
2026-11-14 03:40:34 +01:00
Ole André Vadla Ravnås d149e681a2 barebone: Park Win9x on Wait_Semaphore
Get_System_Time is also called on the way into a wait, where the
host's allocations left the thread on two scheduler lists at once.
2026-11-14 03:40:34 +01:00
Ole André Vadla Ravnås 4b6e0bc214 barebone: Start the Win9x agent from a new thread
Creating a ring 0 thread from the global event that starts the agent
blocked until the thread existed, which sometimes never happened.
Create one asynchronously from the event, and spawn the worker there.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås a3d59c1696 barebone: Cancel only pending Win9x time-outs
VMM reuses the handle of an expired time-out, so cancelling one that
had fired could cancel another waiter's, leaving it waiting forever.
This made injection hang intermittently.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås edaecf5117 barebone: Find where Linux tasks keep their stack
Without BTF, reading another thread's registers failed. Find the field
by looking for the agent's own stack base in its task.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås 7d99ae6521 barebone: Read Linux task ids via the task layout
Without BTF the id offset was unknown, so a spawn never learned its
pid. Use the layout the process enumeration discovers.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås f22fb63f92 barebone: Keep a Linux spawn's words on the stack
The helper is waited on until it has executed, so the hold callback
can reach them without subprocess_info's layout, which only BTF
describes. Also stop reading the id after the cleanup freed it.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås 8f886f0a8d barebone: Pass kernel_write a null position on Arm
It was declared taking the position by value, which 32-bit Arm passes
elsewhere than the pointer, so the kernel used a junk position and the
copy was never woken.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås db8a5ae7b0 tests: Resume live guests from prepared snapshots
Add prepare-<guest> and test-<guest> for win95 and the Linux guests.
Preparing boots the guest until it says it is ready and snapshots it
in the cached image. Testing resumes a fresh QEMU from that snapshot
for each test, and shows the guest's console when one fails.

Win95 comes from an archive.org VHD, patched with patcher9x to run on
fast CPUs, with its first-boot prompts edited out offline.
2026-11-13 22:37:58 +01:00
Ole André Vadla Ravnås f345c339eb barebone: Hook arm64 fixup_exception under ftrace
With patchable function entries, the prologue starts with what ftrace
patches, so the hook was never installed and a faulting read in the
agent took the kernel down.
2026-11-13 21:32:13 +01:00
Ole André Vadla Ravnås ad7aef7304 barebone: Tell Linux per-CPU symbols by section
Treating everything below _text as per-CPU put 32-bit Arm's
swapper_pg_dir out of reach, so writes to execmem faulted. Only
symbols in a per-CPU section linked at zero are.
2026-11-13 21:32:13 +01:00
Ole André Vadla Ravnås dbf6d008e9 barebone: Find pcpu_hot members without BTF
Without type information, as on Alpine's x86 kernel, the agent panicked
on its first read of the current task. Fall back to the fixed offsets
of the leading members.
2026-11-13 21:32:13 +01:00
Ole André Vadla Ravnås a8f5b8b6c3 tests: Drop the second fetch_system_map 2026-11-13 20:29:00 +01:00
11 changed files with 764 additions and 173 deletions

No files matched your search

+77 -2
View File
@@ -3,13 +3,17 @@ name: Barebone agent
on:
push:
paths:
- 'src/barebone/agent/**'
- 'src/barebone/**'
- 'tests/test-barebone.vala'
- 'tests/vm.py'
- '.github/workflows/barebone-agent.yml'
pull_request:
branches:
- main
paths:
- 'src/barebone/agent/**'
- 'src/barebone/**'
- 'tests/test-barebone.vala'
- 'tests/vm.py'
- '.github/workflows/barebone-agent.yml'
workflow_dispatch:
@@ -206,6 +210,77 @@ jobs:
name: frida-barebone-agent-${{ matrix.flavor }}-${{ matrix.arch }}
path: src/barebone/agent/target-${{ matrix.flavor }}/${{ matrix.rust_target }}/release/frida-barebone-agent
host:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v6
with:
submodules: recursive
- name: Build
run: |
./configure --disable-compiler-backend
make
- name: Upload tests
uses: actions/upload-artifact@v7
with:
name: frida-tests-linux-x86_64
path: build/tests/frida-tests
live:
needs: [blob, host]
strategy:
matrix:
include:
- { guest: win95, agent: win9x-x86, qemu: qemu-system-x86 }
- { guest: linux-x86, agent: linux-x86, qemu: qemu-system-x86 }
- { guest: linux-x86_64, agent: linux-x86_64, qemu: qemu-system-x86 }
- { guest: linux-arm, agent: linux-arm, qemu: qemu-system-arm }
- { guest: linux-arm64, agent: linux-arm64, qemu: qemu-system-arm }
fail-fast: false
runs-on: ubuntu-latest
env:
AGENT_DIR: ${{ github.workspace }}/agent
TESTS_DIR: ${{ github.workspace }}/tests-bin
steps:
- name: Check out repo
uses: actions/checkout@v6
- name: Install QEMU
run: |
sudo apt-get update
sudo apt-get install -y ${{ matrix.qemu }} qemu-utils mtools
- name: Download agent
uses: actions/download-artifact@v8
with:
name: frida-barebone-agent-${{ matrix.agent }}
path: ${{ env.AGENT_DIR }}
- name: Download tests
uses: actions/download-artifact@v8
with:
name: frida-tests-linux-x86_64
path: ${{ env.TESTS_DIR }}
- name: Restore prepared guest
id: guest
uses: actions/cache/restore@v5
with:
path: ~/.cache/frida-tests
key: live-guest-${{ matrix.guest }}-${{ hashFiles('tests/vm.py') }}
- name: Prepare guest
if: steps.guest.outputs.cache-hit != 'true'
run: python3 tests/vm.py prepare-${{ matrix.guest }}
- name: Save prepared guest
if: steps.guest.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ~/.cache/frida-tests
key: live-guest-${{ matrix.guest }}-${{ hashFiles('tests/vm.py') }}
- name: Test
run: >-
chmod +x "$TESTS_DIR/frida-tests" &&
python3 tests/vm.py test-${{ matrix.guest }}
--agent "$AGENT_DIR/frida-barebone-agent"
--tests "$TESTS_DIR/frida-tests"
kext:
needs: gum-macos-kernel
runs-on: macos-latest
+18 -8
View File
@@ -15,7 +15,7 @@ use super::layout::field_offset;
use super::layout::struct_size;
use super::STACK_SPAN;
use super::native;
use super::processes::task_with_id;
use super::processes::{id_of, task_with_id};
use super::arena::{Arena, HOME, REPORTED, WOKEN};
use super::user::entry_offset;
@@ -465,11 +465,27 @@ fn registers_of_thread(id: u32) -> Option<usize> {
}
let mut places = describe_registers()?;
if places.stack_of_task == 0 {
places.stack_of_task = field_offset("task_struct", "stack")?;
places.stack_of_task = where_tasks_keep_their_stack()?;
}
Some(registers_of_task(task, &places))
}
fn where_tasks_keep_their_stack() -> Option<usize> {
if let Some(at) = field_offset("task_struct", "stack") {
return Some(at);
}
let marker = 0usize;
let own_stack = (&marker as *const usize as usize) & !(STACK_SPAN - 1);
let this_task = native::current_task() as usize as *const usize;
(0..TASK_WORDS_SEARCHED_FOR_STACK)
.find(|&slot| unsafe { this_task.add(slot).read() } == own_stack)
.map(|slot| slot * size_of::<usize>())
}
const TASK_WORDS_SEARCHED_FOR_STACK: usize = 64;
#[cfg(target_arch = "aarch64")]
pub fn capture_registers(id: u32) -> Option<crate::kernel::CpuState> {
let regs = registers_of_thread(id)?;
@@ -1035,12 +1051,6 @@ fn group_of(task: usize) -> Option<u32> {
Some(unsafe { ((task + at) as *const u32).read() })
}
pub fn id_of(task: usize) -> Option<u32> {
let at = field_offset("task_struct", "pid")?;
Some(unsafe { ((task + at) as *const u32).read() })
}
static COPY_TASK: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
static WATCHING: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
+60 -14
View File
@@ -1,10 +1,14 @@
use core::ffi::{c_int, c_void};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use core::ffi::c_long;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
use core::ptr;
#[cfg(target_arch = "arm")]
use core::sync::atomic::AtomicBool;
#[cfg(target_arch = "aarch64")]
use core::sync::atomic::AtomicPtr;
#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
use core::sync::atomic::{AtomicBool, Ordering};
use core::sync::atomic::Ordering;
use crate::bindings::{
gum_barebone_handle_exception, GumCpuContext,
@@ -302,6 +306,9 @@ unsafe extern "C" {
#[cfg(target_arch = "arm")]
const PROLOGUE: [u32; 2] = [0xe92d4010, 0xe52de004];
#[cfg(target_arch = "arm")]
static ACTIVATED: AtomicBool = AtomicBool::new(false);
#[cfg(target_arch = "arm")]
static mut ORIGINAL_STUB: usize = 0;
@@ -442,10 +449,38 @@ unsafe extern "C" {
}
#[cfg(target_arch = "aarch64")]
const PROLOGUE: [u32; 4] = [0xd503233f, 0xa9be7bfd, 0x910003fd, 0xa90153f3];
struct Prologue {
words: [u32; 4],
call_original: unsafe extern "C" fn(*mut c_void) -> c_int,
}
#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
static ACTIVATED: AtomicBool = AtomicBool::new(false);
#[cfg(target_arch = "aarch64")]
static PROLOGUES: [Prologue; 2] = [
Prologue {
words: [PACIASP, STP_FRAME_RECORD, MOV_FRAME_POINTER, STP_X19_X20],
call_original: frida_arm64_call_original,
},
Prologue {
words: [MOV_X9_X30, NOP, PACIASP, STP_FRAME_RECORD],
call_original: frida_arm64_call_original_traceable,
},
];
#[cfg(target_arch = "aarch64")]
const MOV_X9_X30: u32 = 0xaa1e03e9;
#[cfg(target_arch = "aarch64")]
const NOP: u32 = 0xd503201f;
#[cfg(target_arch = "aarch64")]
const PACIASP: u32 = 0xd503233f;
#[cfg(target_arch = "aarch64")]
const STP_FRAME_RECORD: u32 = 0xa9be7bfd;
#[cfg(target_arch = "aarch64")]
const MOV_FRAME_POINTER: u32 = 0x910003fd;
#[cfg(target_arch = "aarch64")]
const STP_X19_X20: u32 = 0xa90153f3;
#[cfg(target_arch = "aarch64")]
static INSTALLED: AtomicPtr<Prologue> = AtomicPtr::new(ptr::null_mut());
#[cfg(target_arch = "aarch64")]
#[unsafe(no_mangle)]
@@ -454,11 +489,10 @@ static mut FRIDA_ARM64_ORIGINAL_CONT: u64 = 0;
#[cfg(target_arch = "aarch64")]
pub fn install() {
let target = unsafe { _fixup_exception } as *const u32;
for slot in 0..4 {
if unsafe { target.add(slot).read_volatile() } != PROLOGUE[slot] {
return;
}
}
let present = core::array::from_fn(|slot| unsafe { target.add(slot).read_volatile() });
let Some(prologue) = PROLOGUES.iter().find(|prologue| prologue.words == present) else {
return;
};
unsafe { FRIDA_ARM64_ORIGINAL_CONT = target.add(4) as u64 };
@@ -470,19 +504,21 @@ pub fn install() {
_aarch64_insn_patch_text_nosync(addr.add(1) as *mut c_void, jump[1]);
_aarch64_insn_patch_text_nosync(addr as *mut c_void, jump[0]);
}
ACTIVATED.store(true, Ordering::Release);
INSTALLED.store(prologue as *const Prologue as *mut Prologue, Ordering::Release);
}
#[cfg(target_arch = "aarch64")]
pub fn uninstall() {
if !ACTIVATED.swap(false, Ordering::AcqRel) {
let prologue = INSTALLED.swap(ptr::null_mut(), Ordering::AcqRel);
if prologue.is_null() {
return;
}
let target = unsafe { _fixup_exception } as *mut u32;
let words = unsafe { (*prologue).words };
unsafe {
for slot in 0..4 {
_aarch64_insn_patch_text_nosync(target.add(slot) as *mut c_void, PROLOGUE[slot]);
_aarch64_insn_patch_text_nosync(target.add(slot) as *mut c_void, words[slot]);
}
}
}
@@ -504,12 +540,21 @@ core::arch::global_asm!(
"add x16, x16, :lo12:{cont}",
"ldr x16, [x16]",
"br x16",
".globl frida_arm64_call_original_traceable",
"frida_arm64_call_original_traceable:",
"paciasp",
"stp x29, x30, [sp, #-32]!",
"adrp x16, {cont}",
"add x16, x16, :lo12:{cont}",
"ldr x16, [x16]",
"br x16",
cont = sym FRIDA_ARM64_ORIGINAL_CONT,
);
#[cfg(target_arch = "aarch64")]
unsafe extern "C" {
fn frida_arm64_call_original(regs: *mut c_void) -> c_int;
fn frida_arm64_call_original_traceable(regs: *mut c_void) -> c_int;
}
#[cfg(target_arch = "aarch64")]
@@ -517,7 +562,8 @@ unsafe extern "C" fn frida_arm64_fixup(regs: *mut c_void) -> c_int {
if unsafe { recovered(regs) } {
return 1;
}
unsafe { frida_arm64_call_original(regs) }
let prologue = INSTALLED.load(Ordering::Acquire);
unsafe { ((*prologue).call_original)(regs) }
}
#[cfg(target_arch = "aarch64")]
+15 -18
View File
@@ -89,25 +89,10 @@ pub unsafe fn pipe_read(file: *mut c_void, into: *mut u8, count: usize, position
unsafe { _kernel_read(file, into, count, position) }
}
#[cfg(not(target_arch = "x86"))]
pub unsafe fn pipe_write(file: *mut c_void, from: *const u8, count: usize, position: *mut i64) -> isize {
let write: unsafe extern "C" fn(*mut c_void, *const u8, usize, *mut i64) -> isize =
unsafe { core::mem::transmute(_kernel_write) };
unsafe { write(file, from, count, position) }
}
#[cfg(target_arch = "x86")]
pub unsafe fn pipe_write(file: *mut c_void, from: *const u8, count: usize, position: *mut i64) -> isize {
unsafe { _kernel_write(file, from, count, position) }
}
#[cfg(not(target_arch = "x86"))]
pub fn leave_a_word(file: *mut c_void) {
let one: u64 = 1;
unsafe { _kernel_write(file, &one as *const u64 as *const u8, 8, 0) };
}
#[cfg(target_arch = "x86")]
pub fn leave_a_word(file: *mut c_void) {
let one: u64 = 1;
unsafe { _kernel_write(file, &one as *const u64 as *const u8, 8, core::ptr::null_mut()) };
@@ -921,9 +906,22 @@ fn hot_per_cpu_place(member: &str) -> Option<usize> {
return None;
}
Some(hot + super::layout::field_offset("pcpu_hot", member)?)
Some(hot + hot_member_offset(member)?)
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn hot_member_offset(member: &str) -> Option<usize> {
super::layout::field_offset("pcpu_hot", member).or_else(|| {
PCPU_HOT_LEADING_MEMBERS
.iter()
.position(|leading| *leading == member)
.map(|index| index * size_of::<usize>())
})
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
const PCPU_HOT_LEADING_MEMBERS: [&str; 2] = ["current_task", "preempt_count"];
#[cfg(target_arch = "x86_64")]
fn read_per_cpu_word(place: usize) -> usize {
let value: usize;
@@ -1280,8 +1278,7 @@ unsafe extern "C" {
#[cfg(not(target_arch = "x86"))]
static _kernel_read: unsafe extern "C" fn(*mut c_void, *mut u8, usize, *mut i64) -> isize;
#[cfg(not(target_arch = "x86"))]
#[cfg(not(target_arch = "x86"))]
static _kernel_write: unsafe extern "C" fn(*mut c_void, *const u8, usize, i64) -> isize;
static _kernel_write: unsafe extern "C" fn(*mut c_void, *const u8, usize, *mut i64) -> isize;
// Allocation profiling renamed the entry points in 6.10; before that the
// size-plus-flags pair went to __kmalloc.
static ___kmalloc_noprof: Option<unsafe extern "C" fn(usize, u32) -> *mut u8>;
@@ -129,6 +129,10 @@ pub fn task_with_id(id: u32) -> Option<usize> {
found
}
pub fn id_of(task: usize) -> Option<u32> {
Some(read_id(task, task_layout()?))
}
pub fn describe_process(process: &ProcessInfo) -> *const u8 {
process.name
}
-7
View File
@@ -570,7 +570,6 @@ frida_k_call_usermodehelper_setup (const void * a0, const void * a1, const void
}
extern int frida_cb_hold_spawn (void * info, void * credentials);
extern void frida_cb_release_words (void * info);
__attribute__ ((regparm (3))) int
frida_kcb_hold_spawn (void * info, void * credentials)
@@ -578,12 +577,6 @@ frida_kcb_hold_spawn (void * info, void * credentials)
return frida_cb_hold_spawn (info, credentials);
}
__attribute__ ((regparm (3))) void
frida_kcb_release_words (void * info)
{
frida_cb_release_words (info);
}
extern void * _get_user_pages_unlocked;
long
+21 -48
View File
@@ -1,11 +1,9 @@
use alloc::ffi::CString;
use alloc::vec::Vec;
use core::ffi::{c_int, c_void};
use core::mem::size_of;
use core::ptr;
use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering};
use super::layout::field_offset;
use super::native;
use super::processes::task_with_id;
@@ -16,35 +14,29 @@ pub fn spawn_process(words: &[&str]) -> u32 {
super::injection::hold_what_is_spawned();
let held = native::alloc(size_of::<Held>()) as *mut Held;
unsafe {
held.write(Held {
id: 0,
words,
environment: environment(),
})
let mut held = Held {
id: 0,
words,
environment: environment(),
};
SPAWNING.store(&mut held, Ordering::Release);
let info = unsafe {
_call_usermodehelper_setup(
(*held).words.first_word(),
(*held).words.as_argv(),
(*held).environment.as_argv(),
held.words.first_word(),
held.words.as_argv(),
held.environment.as_argv(),
GFP_KERNEL,
HOLD_SPAWN,
RELEASE_WORDS,
held as *mut c_void,
None,
ptr::null_mut(),
)
};
if info.is_null() {
return 0;
}
let started = !info.is_null() && unsafe { _call_usermodehelper_exec(info, UMH_WAIT_EXEC) } == 0;
if unsafe { _call_usermodehelper_exec(info, UMH_WAIT_EXEC) } != 0 {
return 0;
}
SPAWNING.store(ptr::null_mut(), Ordering::Release);
unsafe { (*held).id }
if started { held.id } else { 0 }
}
pub fn resume_process(id: u32) -> bool {
@@ -69,7 +61,7 @@ pub fn holds_this_one(task: usize) -> bool {
return true;
}
let Some(id) = super::injection::id_of(task) else {
let Some(id) = super::processes::id_of(task) else {
return false;
};
@@ -162,13 +154,10 @@ static mut HELD_SPAWNS: Vec<(u32, Vec<u8>)> = Vec::new();
static GATING: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
#[unsafe(no_mangle)]
pub unsafe extern "C" fn frida_cb_hold_spawn(info: *mut c_void, _credentials: *mut c_void) -> c_int {
let Some(at) = field_offset("subprocess_info", "data") else {
return 0;
};
let held = unsafe { ((info as usize + at) as *const usize).read() } as *mut Held;
pub unsafe extern "C" fn frida_cb_hold_spawn(_info: *mut c_void, _credentials: *mut c_void) -> c_int {
let held = SPAWNING.load(Ordering::Acquire);
let id = super::injection::id_of(native::current_task() as usize).unwrap_or(0);
let id = super::processes::id_of(native::current_task() as usize).unwrap_or(0);
unsafe {
(*held).id = id;
(&raw mut HELD).write(id);
@@ -177,17 +166,6 @@ pub unsafe extern "C" fn frida_cb_hold_spawn(info: *mut c_void, _credentials: *m
0
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn frida_cb_release_words(info: *mut c_void) {
let Some(at) = field_offset("subprocess_info", "data") else {
return;
};
let held = unsafe { ((info as usize + at) as *const usize).read() } as *mut Held;
unsafe { ptr::drop_in_place(held) };
native::free(held as *mut u8, size_of::<Held>());
}
fn said_as(words: &[&str]) -> Option<Words> {
let said: Vec<CString> = words.iter().map(|word| CString::new(*word).unwrap()).collect();
if said.is_empty() {
@@ -226,6 +204,7 @@ impl Words {
}
static mut HELD: u32 = 0;
static SPAWNING: AtomicPtr<Held> = AtomicPtr::new(ptr::null_mut());
const GFP_KERNEL: u32 = 0xcc0;
const UMH_WAIT_EXEC: c_int = 1;
@@ -239,7 +218,7 @@ unsafe extern "C" {
*const *const u8,
u32,
unsafe extern "C" fn(*mut c_void, *mut c_void) -> c_int,
unsafe extern "C" fn(*mut c_void),
Option<unsafe extern "C" fn(*mut c_void)>,
*mut c_void,
) -> *mut c_void;
#[cfg(not(target_arch = "x86"))]
@@ -261,19 +240,13 @@ unsafe extern "C" {
a2: *const *const u8,
a3: u32,
a4: unsafe extern "C" fn(*mut c_void, *mut c_void) -> c_int,
a5: unsafe extern "C" fn(*mut c_void),
a5: Option<unsafe extern "C" fn(*mut c_void)>,
a6: *mut c_void,
) -> *mut c_void;
fn frida_kcb_hold_spawn(info: *mut c_void, credentials: *mut c_void) -> c_int;
fn frida_kcb_release_words(info: *mut c_void);
}
#[cfg(target_arch = "x86")]
const HOLD_SPAWN: unsafe extern "C" fn(*mut c_void, *mut c_void) -> c_int = frida_kcb_hold_spawn;
#[cfg(not(target_arch = "x86"))]
const HOLD_SPAWN: unsafe extern "C" fn(*mut c_void, *mut c_void) -> c_int = frida_cb_hold_spawn;
#[cfg(target_arch = "x86")]
const RELEASE_WORDS: unsafe extern "C" fn(*mut c_void) = frida_kcb_release_words;
#[cfg(not(target_arch = "x86"))]
const RELEASE_WORDS: unsafe extern "C" fn(*mut c_void) = frida_cb_release_words;
+55 -8
View File
@@ -154,11 +154,28 @@ pub fn run_when_ready(action: fn()) {
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_on_event() {
unsafe {
vwin32_create_ring0_thread(
THREAD_STACK_SIZE as u32,
0,
frida_win9x_ready_thread_thunk as *const () as u32,
frida_win9x_ready_thread_failed_thunk as *const () as u32,
);
}
}
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_on_ready_thread() {
if let Some(action) = unsafe { READY_ACTION } {
action();
}
}
#[unsafe(no_mangle)]
extern "C" fn frida_win9x_on_ready_thread_failed() {
log("frida: failed to spawn the starter\n\0");
}
static mut READY_ACTION: Option<fn()> = None;
#[unsafe(no_mangle)]
@@ -181,17 +198,32 @@ pub fn wait(token: *const u8, timeout_us: Option<u64>, check: &mut dyn FnMut() -
fn block_until_signalled_or_timed_out(semaphore: u32, timeout_us: u64) {
let timeout_ms = (timeout_us / 1000).max(1) as u32;
let timeout = unsafe { set_global_time_out(timeout_ms, semaphore) };
let mut time_out = TimeOut { semaphore, expired: 0 };
let handle = unsafe { set_global_time_out(timeout_ms, &mut time_out) };
unsafe {
wait_semaphore(semaphore, BLOCK_SVC_INTS);
}
if timeout != 0 {
unsafe {
cancel_time_out(timeout);
}
if handle != 0 {
cancel_unless_expired(handle, &time_out);
}
}
fn cancel_unless_expired(handle: u32, time_out: &TimeOut) {
unsafe {
core::arch::asm!("cli");
if core::ptr::addr_of!(time_out.expired).read_volatile() == 0 {
cancel_time_out(handle);
}
core::arch::asm!("sti");
}
}
#[repr(C)]
struct TimeOut {
semaphore: u32,
expired: u32,
}
pub fn poke_loop_wakeup() {
if in_copy() {
crate::win9x_user::poke_loop_wakeup();
@@ -2787,7 +2819,7 @@ unsafe extern "C" {
fn vpicd_physically_unmask(handle: u32);
fn vpicd_phys_eoi(handle: u32);
fn vpicd_force_default_behavior(handle: u32) -> u32;
fn set_global_time_out(milliseconds: u32, semaphore: u32) -> u32;
fn set_global_time_out(milliseconds: u32, time_out: *mut TimeOut) -> u32;
fn cancel_time_out(timeout: u32);
fn hook_vmm_fault(fault: u32, handler: unsafe extern "C" fn()) -> u32;
fn vmm_add_ddb(ddb: u32) -> u32;
@@ -2805,9 +2837,11 @@ unsafe extern "C" {
fn get_initial_thread_handle(vm: u32) -> u32;
fn get_next_thread_handle(thread: u32) -> u32;
fn ifsmgr_ring0_file_io(function: u32, ebx: u32, ecx: u32, edx: u32, esi: u32, edi: u32) -> u32;
fn vwin32_create_ring0_thread(stack_size: u32, parameter: u32, entry: u32, event: u32) -> u32;
fn vwin32_create_ring0_thread(stack_size: u32, parameter: u32, entry: u32, on_failure: u32) -> u32;
fn frida_win9x_hw_int_thunk();
fn frida_win9x_thread_thunk();
fn frida_win9x_ready_thread_thunk();
fn frida_win9x_ready_thread_failed_thunk();
}
core::arch::global_asm!(
@@ -3217,7 +3251,8 @@ cancel_time_out:
.global frida_win9x_time_out_thunk
frida_win9x_time_out_thunk:
pushad
mov eax, edx
mov dword ptr [edx + 4], 1
mov eax, [edx]
CALL_SERVICE _Signal_Semaphore
popad
ret
@@ -3293,6 +3328,18 @@ frida_win9x_thread_thunk:
call frida_win9x_thread_start
ret
.global frida_win9x_ready_thread_thunk
frida_win9x_ready_thread_thunk:
call frida_win9x_on_ready_thread
ret
.global frida_win9x_ready_thread_failed_thunk
frida_win9x_ready_thread_failed_thunk:
pushad
call frida_win9x_on_ready_thread_failed
popad
ret
.global frida_win9x_hw_int_thunk
frida_win9x_hw_int_thunk:
pushad
+4 -4
View File
@@ -397,10 +397,10 @@ namespace Frida.Barebone {
public Win9xKernelFlavor (Machine machine, Gee.Map<string, SymbolInfo> symbols) throws Efni.Error {
this.machine = machine;
SymbolInfo? get_system_time = symbols["Get_System_Time"];
if (get_system_time == null)
throw new Efni.Error.NOT_SUPPORTED ("Missing symbol for Get_System_Time");
yield_point = get_system_time.offset;
SymbolInfo? wait_semaphore = symbols["Wait_Semaphore"];
if (wait_semaphore == null)
throw new Efni.Error.NOT_SUPPORTED ("Missing symbol for Wait_Semaphore");
yield_point = wait_semaphore.offset;
}
public async void prepare (Cancellable? cancellable) throws Efni.Error, IOError {
+6 -4
View File
@@ -21,12 +21,13 @@ namespace Frida.Barebone {
size = span_of (symbols, linked_base),
});
uint64 per_cpu_end = address_of (symbols, PER_CPU_END_SYMBOL);
bool per_cpu_linked_at_zero = per_cpu_end <= linked_base;
foreach (var symbol in symbols) {
if (symbol.offset >= linked_base) {
symbol.offset -= linked_base;
} else {
if (per_cpu_linked_at_zero && symbol.offset < per_cpu_end)
symbol.per_cpu = true;
}
else
symbol.offset -= linked_base;
}
return new LinuxLayout (running_base, modules, symbols);
@@ -196,6 +197,7 @@ namespace Frida.Barebone {
private const uint64 IMAGE_MAGIC_OFFSET = 0x38;
private const string KERNEL_END_SYMBOL = "_end";
private const string KERNEL_BANNER_SYMBOL = "linux_banner";
private const string PER_CPU_END_SYMBOL = "__per_cpu_end";
private const string KERNEL_BANNER = "Linux version ";
private const uint64 KERNEL_ALIGNMENT = 2 * 1024 * 1024;
private const uint64 KERNEL_TEXT_MIN = 0xffffffff80000000;
+504 -60
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import re
@@ -19,7 +20,8 @@ import urllib.request
def main():
if len(sys.argv) >= 2 and sys.argv[1].split("-", maxsplit=1)[0] in ("check", "boot", "rewind",
"live", "livecheck"):
"live", "livecheck", "prepare",
"test"):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -68,6 +70,19 @@ def main():
rewind.add_argument("--qmp", type=Path, required=True)
rewind.add_argument("--snapshot", default=SNAPSHOT_NAME)
for name, preparation in PREPARATIONS.items():
description = preparation.description
subparsers.add_parser(f"prepare-{name}",
help=f"fetch a {description} guest and save it booted, for live tests to "
"resume")
test = subparsers.add_parser(f"test-{name}",
help=f"run the {preparation.suite} live tests against a prepared "
f"{description} guest")
test.add_argument("--agent", type=Path, required=True)
test.add_argument("--tests", type=Path, required=True)
test.add_argument("names", nargs="*")
subparsers.add_parser("check-kmod",
help="verify that a kmod guest can be booted, fetching what it needs")
@@ -82,7 +97,12 @@ def main():
args = parser.parse_args()
action, target = args.command.split("-", maxsplit=1)
if target == "kmod":
if action == "prepare":
image, _ = prepare_guest(target)
print(image)
elif action == "test":
test_guest(target, args)
elif target == "kmod":
if action == "check":
check_kmod_guest()
else:
@@ -235,44 +255,49 @@ def boot_live_guest(arch: str, args):
if args.console_commands is not None:
threading.Thread(target=feed_console, args=(args.console_commands, process), daemon=True).start()
asked = False
ready = False
where_it_is = []
try:
where_it_is = bring_up_link(guest, process)
print(f"system-map {system_map}", flush=True)
for name, value in where_it_is.items():
print(f"{name} {value}", flush=True)
print(f"bus {VIRTIO_SERIAL_ID}.0", flush=True)
print("ready", flush=True)
for raw_line in process.stdout:
line = raw_line.decode(errors="replace").rstrip()
print(line, file=sys.stderr)
if not asked and LIVE_READY_MARKER in line:
asked = True
if guest.hostlink == "mmio":
process.stdin.write((WHERE_THE_LINK_IS + "\n").encode())
else:
process.stdin.write((LET_THE_LINK_GO + "\n").encode())
process.stdin.flush()
if not ready and LINK_ANSWER_MARKER in line and LINK_GONE_MARKER not in line:
fields = line.split(LINK_ANSWER_MARKER, maxsplit=1)[1].split()
where_it_is = [f"mmio 0x{fields[0]}", f"irq {fields[1]}"]
process.stdin.write((LET_THE_LINK_GO + "\n").encode())
process.stdin.flush()
if not ready and LINK_GONE_MARKER in line:
ready = True
print(f"system-map {system_map}", flush=True)
for detail in where_it_is:
print(detail, flush=True)
print(f"bus {VIRTIO_SERIAL_ID}.0", flush=True)
print("ready", flush=True)
if not ready:
raise Unavailable("guest exited before it said where the link is")
print(raw_line.decode(errors="replace").rstrip(), file=sys.stderr)
finally:
process.kill()
def bring_up_link(guest: "Guest", process) -> dict:
asked = False
where_it_is = {}
for raw_line in process.stdout:
line = raw_line.decode(errors="replace").rstrip()
print(line, file=sys.stderr)
if not asked and LIVE_READY_MARKER in line:
asked = True
if guest.hostlink == "mmio":
process.stdin.write((WHERE_THE_LINK_IS + "\n").encode())
else:
process.stdin.write((LET_THE_LINK_GO + "\n").encode())
process.stdin.flush()
if LINK_ANSWER_MARKER in line and LINK_GONE_MARKER not in line:
fields = line.split(LINK_ANSWER_MARKER, maxsplit=1)[1].split()
where_it_is = {"mmio": f"0x{fields[0]}", "irq": fields[1]}
process.stdin.write((LET_THE_LINK_GO + "\n").encode())
process.stdin.flush()
if LINK_GONE_MARKER in line:
return where_it_is
raise Unavailable("guest exited before it said where the link is")
def hostlink_arguments(guest: "Guest") -> [str]:
if guest.hostlink == "mmio":
return [
@@ -320,10 +345,7 @@ def boot_windows_guest(guest: "WindowsGuest", args):
diagnostics = []
if args.debugcon is not None:
diagnostics = [
"-chardev", f"file,id={DEBUGCON_ID},path={args.debugcon}",
"-device", f"isa-debugcon,iobase={DEBUGCON_PORT:#x},chardev={DEBUGCON_ID}",
]
diagnostics = debugcon_arguments(args.debugcon)
process = subprocess.Popen([
qemu,
@@ -408,6 +430,337 @@ def rewind_windows_guest(args):
qmp.monitor("cont")
def prepare_guest(name: str) -> ("Path", dict):
directory = cache_root() / name
image = directory / f"{name}.qcow2"
details_path = directory / f"{name}.json"
if image.exists():
return image, json.loads(details_path.read_text())
with tempfile.TemporaryDirectory() as workdir:
staged, details = PREPARATIONS[name].prepare(Path(workdir))
directory.mkdir(parents=True, exist_ok=True)
details_path.write_text(json.dumps(details))
shutil.move(staged, image)
return image, details
def test_guest(name: str, args):
preparation = PREPARATIONS[name]
image, details = prepare_guest(name)
names = [test for test in live_tests(args.tests, preparation.suite)
if not args.names or any(wanted in test for wanted in args.names)]
failed = []
with tempfile.TemporaryDirectory() as workdir:
disk = Path(workdir) / image.name
shutil.copyfile(image, disk)
qmp_path = Path(workdir) / "qmp.sock"
console_path = Path(workdir) / "console.log"
agent_log_path = Path(workdir) / "agent.log"
emulator_path = Path(workdir) / "emulator.log"
variables = preparation.variables
for index, test in enumerate(names):
console_path.write_bytes(b"")
agent_log_path.write_bytes(b"")
with Listeners(qmp_path) as listeners, emulator_path.open("wb") as emulator_output:
process = subprocess.Popen(
preparation.arguments(disk, details, listeners) + [
"-serial", f"file:{console_path}",
"-loadvm", PREPARED_SNAPSHOT,
] + preparation.agent_log(agent_log_path),
pass_fds=listeners.fds, stdout=subprocess.DEVNULL, stderr=emulator_output)
try:
environment = os.environ | {
f"FRIDA_TEST_{variables}_AGENT": str(args.agent),
f"FRIDA_TEST_{variables}_QMP": str(qmp_path),
f"FRIDA_TEST_{variables}_GDB_PORT": str(listeners.gdb_port),
f"FRIDA_TEST_{variables}_BUS": f"{VIRTIO_SERIAL_ID}.0",
} | {f"FRIDA_TEST_{variables}_{key}": value
for key, value in preparation.environment(details).items()}
outcome = subprocess.run([args.tests, "-p", test], env=environment,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
emulator_gave_up = process.poll() is not None
finally:
process.terminate()
process.wait()
if outcome.returncode == 0:
print(f"ok {index + 1} {test}", flush=True)
else:
failed.append(test)
print(f"not ok {index + 1} {test}", flush=True)
print(outcome.stdout.decode(errors="replace"), file=sys.stderr)
said = [("guest console", console_path), ("agent", agent_log_path)]
if emulator_gave_up:
said.append(("emulator", emulator_path))
report_what_the_guest_said(said)
print(f"1..{len(names)}")
if failed:
sys.exit(1)
def live_tests(runner: Path, suite: str) -> [str]:
listing = subprocess.run([runner, "-l", "-p", f"/Barebone/{suite}"], capture_output=True, check=True)
names = []
for line in listing.stdout.decode().splitlines():
test = line.removeprefix("# ")
if test.startswith(f"/Barebone/{suite}/") and test.endswith("-in-live-guest"):
names.append(test)
return names
def report_what_the_guest_said(said: [(str, Path)]):
for title, path in said:
text = path.read_bytes().decode(errors="replace").strip()
if text:
print(f"--- {title} ---\n{text}", file=sys.stderr)
def prepare_linux(arch: str, workdir: Path) -> (Path, dict):
guest = GUESTS[arch]
qemu_img = require_program("qemu-img")
details = {
"kernel": str(fetch_kernel(guest)),
"ramdisk": str(fetch_ramdisk(guest)),
"system_map": str(fetch_system_map(guest)),
}
image = workdir / f"linux-{arch}.qcow2"
subprocess.run([qemu_img, "create", "-q", "-f", "qcow2", image, LIVE_STATE_DISK_SIZE], check=True)
control_path = workdir / "control.sock"
with Listeners(control_path) as listeners:
process = subprocess.Popen(live_guest_arguments(guest, image, details, listeners) + ["-serial", "stdio"],
pass_fds=listeners.fds, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
try:
details |= bring_up_link(guest, process)
control = Qmp(control_path, process)
control.monitor("stop")
control.monitor(f"savevm {PREPARED_SNAPSHOT}")
finally:
process.kill()
process.wait()
return image, details
def live_guest_arguments(guest: "Guest", image: Path, details: dict, listeners: "Listeners") -> [str]:
machine_arguments = []
if guest.live_machine != "":
machine_arguments += ["-machine", guest.live_machine]
if guest.cpu != "":
machine_arguments += ["-cpu", guest.cpu]
return [
require_qemu(guest),
"-m", str(LIVE_MEMORY_SIZE),
] + machine_arguments + [
"-kernel", details["kernel"],
"-initrd", details["ramdisk"],
"-append", f"console={guest.console} panic=-1 {guest.live_cmdline}".rstrip(),
"-drive", f"if=none,id=state,file={image},format=qcow2",
"-display", "none",
"-monitor", "none",
"-no-reboot",
] + listener_arguments(listeners) + hostlink_arguments(guest)
def live_guest_environment(guest: "Guest", details: dict) -> dict:
environment = {"SYSTEM_MAP": details["system_map"]}
if guest.hostlink == "mmio":
environment["MMIO"] = details["mmio"]
elif guest.ecam != 0:
environment["ECAM"] = hex(guest.ecam)
return environment
def prepare_win95(workdir: Path) -> (Path, dict):
guest = WINDOWS_GUESTS["win95"]
qemu = require_windows_emulator(guest)
qemu_img = require_program("qemu-img")
for program in MTOOLS_PROGRAMS:
require_program(program)
disk = workdir / "disk.raw"
subprocess.run([qemu_img, "convert", "-O", "raw", fetch_verified(WIN95_DISK), disk], check=True)
system = f"{disk}@@{WIN95_PARTITION_OFFSET}"
rewrite_dos_file(system, "::/MSDOS.SYS",
lambda text: replace_once(text, b"[Options]\r\n", b"[Options]\r\nAutoScan=0\r\n"))
rewrite_dos_file(system, "::/WINDOWS/SYSTEM.INI", without_display_surprises)
write_dos_file(system, "::/WINDOWS/Start Menu/Programs/StartUp/READY.BAT", ANNOUNCE_READINESS)
write_dos_file(system, "::/WINDOWS/INF/FRIDA.INF", HOSTLINK_INF)
mtools("mren", "-i", system, "::/WINDOWS/WELCOME.EXE", "::/WINDOWS/WELCOME.OFF")
floppy = workdir / "patcher9x.img"
shutil.copyfile(fetch_verified(PATCHER9X_FLOPPY), floppy)
write_dos_file(str(floppy), "::/AUTOEXEC.BAT", PATCH_AND_ANNOUNCE)
run_until_marker([
qemu,
"-machine", "pc,accel=tcg",
"-cpu", guest.cpu,
"-m", str(guest.memory),
"-drive", f"file={disk},format=raw,if=ide",
"-drive", f"file={floppy},format=raw,if=floppy,readonly=on",
"-boot", "a",
"-nic", "none",
"-display", "none",
"-monitor", "none",
"-serial", "stdio",
], PATCHED_MARKER)
image = workdir / "win95.qcow2"
subprocess.run([qemu_img, "convert", "-O", "qcow2", disk, image], check=True)
control_path = workdir / "control.sock"
with Listeners(control_path) as listeners:
process = subprocess.Popen(windows_guest_arguments(guest, image, listeners) + ["-serial", "stdio"],
pass_fds=listeners.fds, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
wait_for_marker(process, READY_MARKER)
control = Qmp(control_path, process)
control.monitor("stop")
control.monitor(f"savevm {PREPARED_SNAPSHOT}")
finally:
process.kill()
process.wait()
return image, {}
def fetch_verified(download: "Download") -> Path:
cached = cache_root() / "downloads" / download.name
if not cached.exists():
cached.parent.mkdir(parents=True, exist_ok=True)
staging = cached.with_suffix(".partial")
try:
with urllib.request.urlopen(download.url, timeout=60) as response, staging.open("wb") as f:
shutil.copyfileobj(response, f)
except Exception as e:
staging.unlink(missing_ok=True)
raise Unavailable(f"unable to download {download.url}: {e}")
staging.replace(cached)
digest = hashlib.sha256(cached.read_bytes()).hexdigest()
if digest != download.sha256:
cached.unlink()
raise Unavailable(f"{download.url} is not what it was: its SHA-256 is {digest}")
return cached
def rewrite_dos_file(image: str, path: str, change):
original = mtools("mtype", "-i", image, path)
attributes = mtools("mattrib", "-i", image, path).split(b":", 1)[0].decode()
mtools("mattrib", "-i", image, "-r", "-s", "-h", path)
write_dos_file(image, path, change(original))
flags = [f"+{flag.lower()}" for flag in attributes if flag in "RSH"]
if flags:
mtools("mattrib", "-i", image, *flags, path)
def without_display_surprises(text: bytes) -> bytes:
text = replace_once(text, b"display.drv=pnpdrvr.drv\r\n", b"display.drv=vga.drv\r\n")
return re.sub(rb"SCRNSAVE\.EXE=[^\r]*\r\n", b"", text)
def replace_once(text: bytes, old: bytes, new: bytes) -> bytes:
if old not in text:
raise Unavailable(f"the image is not the one expected: it lacks {old!r}")
return text.replace(old, new, 1)
def write_dos_file(image: str, path: str, content: bytes):
with tempfile.NamedTemporaryFile() as staged:
staged.write(content)
staged.flush()
mtools("mcopy", "-o", "-i", image, staged.name, path)
def mtools(*arguments) -> bytes:
return subprocess.run(arguments, env=os.environ | {"MTOOLS_SKIP_CHECK": "1"}, capture_output=True,
check=True).stdout
def run_until_marker(arguments: [str], marker: str):
process = subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
wait_for_marker(process, marker)
finally:
process.kill()
process.wait()
def windows_guest_arguments(guest: "WindowsGuest", image: Path, listeners: "Listeners") -> [str]:
return [
require_windows_emulator(guest),
"-machine", "pc,accel=tcg",
"-cpu", guest.cpu,
"-m", str(guest.memory),
"-drive", f"file={image},format=qcow2,if={guest.disk_interface}",
"-vga", "cirrus",
"-nic", "none",
"-display", "none",
"-monitor", "none",
"-device", "virtio-serial-pci,id=" + VIRTIO_SERIAL_ID,
] + listener_arguments(listeners)
def listener_arguments(listeners: "Listeners") -> [str]:
return [
"-chardev", f"socket,id={GDB_ID},fd={listeners.gdb.fileno()},server=on,wait=off,nodelay=on",
"-gdb", f"chardev:{GDB_ID}",
"-chardev", f"socket,id={QMP_ID},fd={listeners.qmp.fileno()},server=on,wait=off",
"-mon", f"chardev={QMP_ID},mode=control",
]
def debugcon_arguments(path: Path) -> [str]:
return [
"-chardev", f"file,id={DEBUGCON_ID},path={path}",
"-device", f"isa-debugcon,iobase={DEBUGCON_PORT:#x},chardev={DEBUGCON_ID}",
]
class Listeners:
def __init__(self, qmp_path: Path):
self.qmp = socket.socket(socket.AF_UNIX)
self.qmp.bind(str(qmp_path))
self.qmp.listen()
self._qmp_path = qmp_path
self.gdb = socket.socket()
self.gdb.bind(("127.0.0.1", 0))
self.gdb.listen()
@property
def gdb_port(self) -> int:
return self.gdb.getsockname()[1]
@property
def fds(self) -> [int]:
return [self.qmp.fileno(), self.gdb.fileno()]
def __enter__(self):
return self
def __exit__(self, *exc_info):
self.qmp.close()
self.gdb.close()
self._qmp_path.unlink(missing_ok=True)
def wait_for_marker(process, marker: str):
for raw_line in process.stdout:
if marker in raw_line.decode(errors="replace"):
return
raise Unavailable(f"guest exited before it said {marker}")
class Qmp:
def __init__(self, path, process=None):
deadline = time.monotonic() + QMP_CONNECT_TIMEOUT_SECONDS
@@ -468,11 +821,16 @@ def disk_arguments(args) -> [str]:
def require_windows_guest(guest: "WindowsGuest", args) -> str:
qemu = require_windows_emulator(guest)
if not args.image.exists():
raise Unavailable(f"{args.image} does not exist")
return qemu
def require_windows_emulator(guest: "WindowsGuest") -> str:
qemu = shutil.which(guest.emulator)
if qemu is None:
raise Unavailable(f"{guest.emulator} is not installed")
if not args.image.exists():
raise Unavailable(f"{args.image} does not exist")
return qemu
@@ -591,25 +949,6 @@ def fetch_system_map(guest: "Guest") -> Path:
return fetch_cached(guest, names[-1], netboot + names[-1])
def fetch_system_map(guest: "Guest") -> Path:
"""
The map is named after the kernel it describes, which the mirror versions in place, so
the directory says which one is being served today.
"""
netboot = guest.kernel_url.rsplit("/", maxsplit=1)[0] + "/"
try:
with urllib.request.urlopen(netboot, timeout=60) as response:
index = response.read().decode(errors="replace")
except Exception as e:
raise Unavailable(f"unable to list {netboot}: {e}")
names = sorted(set(re.findall(r'System\.map-[0-9][^"<]*-lts', index)))
if not names:
raise Unavailable(f"{netboot} serves no System.map")
return fetch_cached(guest, names[-1], netboot + names[-1])
def host_kernel() -> Path:
return Path("/boot") / ("vmlinuz-" + os.uname().release)
@@ -673,9 +1012,13 @@ def fetch_cached(guest: "Guest", name: str, url: str) -> Path:
def cache_dir(guest: "Guest") -> Path:
return cache_root() / f"qemu-{guest.arch}"
def cache_root() -> Path:
base = os.environ.get("XDG_CACHE_HOME")
root = Path(base) if base is not None else Path.home() / ".cache"
return root / "frida-tests" / f"qemu-{guest.arch}"
return root / "frida-tests"
class Unavailable(Exception):
@@ -720,6 +1063,10 @@ GUESTS = {
# Left where it lands by default, the configuration space is above what a
# kernel without LPAE can address.
live_machine="virt,highmem=off", ecam=0x3f000000),
Guest("arm64", "qemu-system-aarch64", ALPINE_NETBOOT_URL.format("aarch64"),
machine="virt", cpu="max", console="ttyAMA0",
ramdisk_url=ALPINE_RAMDISK_URL.format("aarch64"), live_machine="virt", ecam=0x4010000000,
live_cmdline="initcall_blacklist=virtio_console_init"),
]
}
@@ -731,6 +1078,8 @@ VIRTIO_SERIAL_ID = "frida-vserial"
GDB_ID = "frida-gdb"
QMP_ID = "frida-qmp"
# Newest model that boots 95; older ones lack the CMOVs the agent emits.
class WindowsGuest(NamedTuple):
description: str
@@ -751,6 +1100,101 @@ WINDOWS_GUESTS = {
None),
}
class Preparation(NamedTuple):
description: str
suite: str
variables: str
prepare: object
arguments: object
environment: object
agent_log: object
def windows_preparation(name: str, suite: str, variables: str, prepare) -> Preparation:
guest = WINDOWS_GUESTS[name]
return Preparation(guest.description, suite, variables, prepare,
lambda image, details, listeners: windows_guest_arguments(guest, image, listeners),
lambda details: {},
debugcon_arguments)
def linux_preparation(arch: str, suite: str, variables: str) -> Preparation:
guest = GUESTS[arch]
return Preparation(f"Linux ({arch})", suite, variables,
lambda workdir: prepare_linux(arch, workdir),
lambda image, details, listeners: live_guest_arguments(guest, image, details, listeners),
lambda details: live_guest_environment(guest, details),
lambda path: [])
PREPARATIONS = {
"win95": windows_preparation("win95", "Win9x", "WIN9X", prepare_win95),
"linux-x86": linux_preparation("x86", "IA32", "LINUX_X86"),
"linux-x86_64": linux_preparation("x86_64", "X64", "LINUX_X86_64"),
"linux-arm": linux_preparation("arm", "ARM", "LINUX_ARM"),
"linux-arm64": linux_preparation("arm64", "ARM", "LINUX_ARM"),
}
PREPARED_SNAPSHOT = "ready"
LIVE_MEMORY_SIZE = 256
LIVE_STATE_DISK_SIZE = "16M"
class Download(NamedTuple):
name: str
url: str
sha256: str
WIN95_DISK = Download(
"win95-en.vhd",
"https://archive.org/download/windows-95-vhd/Windows%2095%20VHD.iso/"
"VHD%20%28Virtual%20Hard%20Disk%29%2FWin95-en%20with%20MS-DOS_6.%20Hard%20Disk.vhd",
"bea8244016d7ffe8c3e6ab0efbd9ef37219d76ebc74590a0e307d0dc7dc74d25")
WIN95_PARTITION_OFFSET = 63 * 512
PATCHER9X_FLOPPY = Download(
"patcher9x-0.9.91-boot.img",
"https://github.com/JHRobotics/patcher9x/releases/download/v0.9.91/patcher9x-0.9.91-boot.img",
"ca4fddeb6ad44f63f42f50c668b5e9e1587f9686b21d5a68c3beac1f5781ed03")
MTOOLS_PROGRAMS = ["mtype", "mattrib", "mcopy", "mren"]
PATCHED_MARKER = "patched"
READY_MARKER = "ready"
PATCH_AND_ANNOUNCE = (
b"@ECHO OFF\r\n"
b"SET PATH=A:\\\r\n"
b"patch9x -auto C:\\WINDOWS\\SYSTEM\r\n"
b"ECHO patched > COM1\r\n"
)
ANNOUNCE_READINESS = (
b"@ECHO OFF\r\n"
b"ECHO ready > COM1\r\n"
)
HOSTLINK_INF = (
b"[Version]\r\n"
b"Signature=\"$CHICAGO$\"\r\n"
b"Class=System\r\n"
b"Provider=%Frida%\r\n"
b"\r\n"
b"[Manufacturer]\r\n"
b"%Frida%=Frida\r\n"
b"\r\n"
b"[Frida]\r\n"
b"%Hostlink%=NoDriver,PCI\\VEN_1AF4&DEV_1003\r\n"
b"\r\n"
b"[NoDriver]\r\n"
b"\r\n"
b"[Strings]\r\n"
b"Frida=\"Frida\"\r\n"
b"Hostlink=\"Frida hostlink\"\r\n"
)
DEBUGCON_ID = "frida-debugcon"
DEBUGCON_PORT = 0xe9