mirror of
https://github.com/frida/frida-core.git
synced 2026-11-13 18:26:59 -10:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9a4940b5e | ||
|
|
d149e681a2 | ||
|
|
4b6e0bc214 | ||
|
|
a3d59c1696 | ||
|
|
edaecf5117 | ||
|
|
7d99ae6521 | ||
|
|
f22fb63f92 | ||
|
|
8f886f0a8d | ||
|
|
db8a5ae7b0 | ||
|
|
f345c339eb | ||
|
|
ad7aef7304 | ||
|
|
dbf6d008e9 | ||
|
|
a8f5b8b6c3 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user