From 7b40e4d204a3ce86c247d09234fed18438a98f0b Mon Sep 17 00:00:00 2001 From: mcxiaochenn Date: Sun, 18 Oct 2026 08:10:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(rkp):=20=E5=8A=A0=E5=85=A5=20--rkp-pro?= =?UTF-8?q?be=20=E5=A4=9A=E5=8F=98=E5=BC=82=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机上 challenge 能取到,但 send_cmd 返回 0 且响应缓冲区零字节 (written: 0、status_le: 0),既不是帧格式问题也不是长度字段问题。 这类"空成功"只靠一次调用无法区分原因,因此把可疑变量拆开逐项试探。 - ta 层拆出 CallOptions:缓冲区初始化(哨兵 / 全 0 / 前 4 字节写容量)、 security_level(1 / 0)、请求封装(裸 / 长度前缀) - 新增 6 个变体,每个变体在独立子进程中 dlopen 并调用一次, 某个变体触发崩溃只丢失该组结果 - 记录 status、written、request_changed、response_head, request_changed 用于判断 out 参数是否落在请求缓冲区上 - 哨兵残留时直接报 abi,不再让上层去猜帧格式 - 新增 CLI --rkp-probe 与隐藏入口 --rkp-ta-probe,输出 JSON 对照表 主机 111 项测试通过(新增帧封装、写入量、请求改写、变体唯一性)。 四 ABI release:arm64 922,984 字节,包 2,104,262 字节。 --- README.md | 13 +- crates/teeforge/src/cli.rs | 34 ++++ crates/teeforge/src/rkp/mod.rs | 39 ++++ crates/teeforge/src/rkp/ta.rs | 303 ++++++++++++++++++++++++++++-- crates/teeforge/src/rkp/worker.rs | 84 +++++++++ docs/maintenance/ROADMAP.md | 1 + docs/project/ARCHITECTURE.md | 1 + 7 files changed, 457 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c3812bc..bd4ad2d 100644 --- a/README.md +++ b/README.md @@ -110,13 +110,14 @@ su -c '/data/adb/modules/teeforge_cd/teeforge --hide-bl' su -c '/data/adb/modules/teeforge_cd/teeforge --keybox --source remote' su -c '/data/adb/modules/teeforge_cd/teeforge --rkp-status' su -c '/data/adb/modules/teeforge_cd/teeforge --rkp-issue' +su -c '/data/adb/modules/teeforge_cd/teeforge --rkp-probe --json' su -c '/data/adb/modules/teeforge_cd/teeforge --keybox --source rkp' su -c '/data/adb/modules/teeforge_cd/teeforge --target-list --json' su -c '/data/adb/modules/teeforge_cd/teeforge --self-update check' su -c '/data/adb/modules/teeforge_cd/teeforge --help' ``` -`--rkp-status` 只做本地检查(SoC、厂商库、`/dev/smcinvoke`、fingerprint),不联网、不调用 TA,用于判断设备端 RKP 是否具备条件。所有查询类命令都支持 `--json`,输出统一的 `{ok,data}` / `{ok,error}` 信封;`--json` 只能搭配一个命令。 +`--rkp-status` 只做本地检查(SoC、厂商库、`/dev/smcinvoke`、fingerprint),不联网、不调用 TA,用于判断设备端 RKP 是否具备条件。`--rkp-probe` 是诊断命令:取一次 challenge 后,把缓冲区初始化、`security_level`、请求封装三组变量拆成 6 个变体,每个变体在独立子进程中调用一次 TA,输出对照数据。所有查询类命令都支持 `--json`,输出统一的 `{ok,data}` / `{ok,error}` 信封;`--json` 只能搭配一个命令。 CLI 任一步失败都会返回非零退出码。完整参数见[项目概览](docs/project/OVERVIEW.md)。 @@ -197,6 +198,16 @@ update_channel=stable - **目标配置没有更新**:确认目标模块使用 `/data/adb/tricky_store/target.txt` 或 `/data/adb/teesim/config.json`,检查 `teeforge` 管理标记和手动运行 `--generate` 的真实退出码。没有任何兼容后端时,命令成功但只记录 warning。 - **Keybox 同步失败**:检查网络和命令输出;失败时程序会保留已有文件,不要把非空响应直接视为有效结果。 - **设备端 RKP 没有生效**:先运行 `--rkp-status` 确认前置条件;日志与 WebUI 会给出失败阶段、原始码与建议,不再有隐式回退。 +- **`--rkp-probe` 的判读方式**:每条结果包含 `status`(`send_cmd` 返回值)、`written`(响应最后一个非零字节位置)、`request_changed`(请求缓冲区是否被厂商改写)与 `response_head`。 + + | 观察 | 结论 | + |---|---| + | 只有 `baseline` 空、`zero-buffer` 有数据 | 前 4 字节的哨兵干扰了厂商逻辑,默认初始化应改为全 0 | + | 全部变体 `written` 均为 0 且 `request_changed` 为 false | 请求没有真正到达 TA,优先查 SELinux 拒绝与 TA 服务状态 | + | 某变体 `request_changed` 为 true | out 参数落在了请求缓冲区上,参数顺序需要调整 | + | `level-0` 有数据 | 该固件要求 `security_level = 0` | + | `length-prefix` 有数据 | 请求需要长度前缀 | + | 多个变体进程崩溃(`crashed: true`) | 厂商对象布局与该固件不匹配 | - **报“无法识别 TA 响应帧”或“TA 未写入响应缓冲区”**:说明厂商 `send_cmd` 的返回封装与预期不同。`--rkp-issue --json` 的 `error.raw.head_hex` 是响应缓冲区开头 64 字节,`written` 是最后一个非零字节的位置: - 前 8 位为 `00000080` 且 `written` 为 0:缓冲区完全未被写入,属于 ABI 或参数约定不匹配。 - 开头是 `82`、`a2`、`d2` 等 CBOR 首字节:厂商直接返回裸 payload。 diff --git a/crates/teeforge/src/cli.rs b/crates/teeforge/src/cli.rs index 14fb172..4f8ada3 100644 --- a/crates/teeforge/src/cli.rs +++ b/crates/teeforge/src/cli.rs @@ -25,6 +25,7 @@ struct Options { rkp_issue: bool, rkp_artifacts: bool, rkp_delete: bool, + rkp_probe: bool, target_list: bool, target_apply: Option, target_mode: Option, @@ -61,6 +62,7 @@ fn parse(args: &[String]) -> Result { "--rkp-issue" => options.rkp_issue = true, "--rkp-artifacts" => options.rkp_artifacts = true, "--rkp-delete" => options.rkp_delete = true, + "--rkp-probe" => options.rkp_probe = true, "--target-list" => options.target_list = true, "--target-apply" => { index += 1; @@ -130,6 +132,7 @@ fn usage(program: &str) { println!(" --rkp-issue 设备端 RKP 签发并保存产物 [Issue RKP artifacts]"); println!(" --rkp-artifacts 列出 RKP 产物 [List RKP artifacts]"); println!(" --rkp-delete 删除 RKP 产物 [Delete RKP artifacts]"); + println!(" --rkp-probe 探测 TA 响应约定(诊断用,消耗一次 challenge) [Probe TA response]"); println!(" --self-update MODE check/install 更新模块 [Check or install updates]"); println!(" --update-channel C stable/dev 更新通道 [Update channel]"); println!(" --bootstrap 输出首屏信息 [Emit bootstrap information]"); @@ -171,6 +174,24 @@ pub fn run(args: Vec) -> i32 { } }; } + // 内部入口:探测变体同样只做一次厂商调用。 + if let Some(index) = args.iter().position(|arg| arg == "--rkp-ta-probe") { + let (Some(directory), Some(variant)) = (args.get(index + 1), args.get(index + 2)) else { + eprintln!("--rkp-ta-probe 需要目录与变体序号 [requires a directory and a variant]"); + return 2; + }; + let Ok(variant) = variant.parse::() else { + eprintln!("变体序号非法 [invalid variant index]"); + return 2; + }; + return match rkp::execute_probe(std::path::Path::new(directory), variant) { + Ok(()) => 0, + Err(error) => { + eprintln!("{error}"); + 1 + } + }; + } let options = match parse(&args) { Ok(options) => options, Err(error) => { @@ -210,6 +231,7 @@ pub fn run(args: Vec) -> i32 { && !options.rkp_issue && !options.rkp_artifacts && !options.rkp_delete + && !options.rkp_probe && !options.target_list && options.target_apply.is_none() && options.target_mode.is_none() @@ -298,6 +320,12 @@ pub fn run(args: Vec) -> i32 { Err(error) => record(Err(error), &mut failed), } } + if options.rkp_probe { + match rkp::probe(&config) { + Ok(value) => println!("{value:#}"), + Err(error) => record(Err(error), &mut failed), + } + } if options.target_list { match target::list_value(&config) { Ok(value) => { @@ -442,6 +470,9 @@ fn run_json(options: &Options, config: &Config, user_config: &Path) -> i32 { if options.rkp_delete { commands.push("rkp-delete"); } + if options.rkp_probe { + commands.push("rkp-probe"); + } if options.target_list { commands.push("target-list"); } @@ -551,6 +582,9 @@ fn run_json(options: &Options, config: &Config, user_config: &Path) -> i32 { if options.rkp_delete { return json::finish(rkp::delete_artifacts(config)); } + if options.rkp_probe { + return json::finish(rkp::probe(config)); + } if options.target_list { return json::finish(target::list_value(config)); } diff --git a/crates/teeforge/src/rkp/mod.rs b/crates/teeforge/src/rkp/mod.rs index 4c49664..99594ef 100644 --- a/crates/teeforge/src/rkp/mod.rs +++ b/crates/teeforge/src/rkp/mod.rs @@ -136,6 +136,40 @@ pub(crate) fn issue(config: &Config) -> Result { }) } +/// 多变异探测:定位「空成功」的成因。 +/// +/// 现场出现过 `send_cmd` 返回 0 但缓冲区零字节的情况:既不是帧格式问题,也不是 +/// 长度字段问题。探测把缓冲区初始化、security_level、请求封装三个变量拆开, +/// 每个变体独立子进程执行,用一次装包换回全部对照数据。 +/// Probes one variable at a time to locate the "empty success" path. +pub(crate) fn probe(config: &Config) -> Result { + let diagnosis = diagnose(); + if let Some((id, detail)) = diagnosis.blockers.first() { + return Err(blocker_error(id, detail)); + } + let key = generate_key()?; + let tmp = work_dir(config); + let challenge = server::fetch_challenge(&config.rkp_server, &diagnosis.fingerprint, &tmp)?; + let cose_key = protocol::cose_key(&key.public_key())?; + let request = protocol::build_request(&challenge, &cose_key)?; + logging::log( + Level::Info, + format!( + "开始探测 TA 响应,共 {} 个变体 [probing TA response with {} variants]", + ta::PROBES.len(), + ta::PROBES.len() + ), + ); + let variants = worker::probe_all(&tmp, &request)?; + Ok(json!({ + "soc": diagnosis.soc_model, + "fingerprint": diagnosis.fingerprint, + "challenge_bytes": challenge.len(), + "request_bytes": request.len(), + "variants": variants, + })) +} + /// 签发并写入 `RKP/`:keybox.xml、cert-chain.pem、meta.json。 pub(crate) fn issue_and_save(config: &Config) -> Result { let issued = issue(config)?; @@ -417,6 +451,11 @@ pub(crate) fn execute_worker(directory: &Path) -> Result<()> { worker::execute(directory) } +/// 探测子进程入口,供 CLI 的隐藏参数转发。 +pub(crate) fn execute_probe(directory: &Path, variant: usize) -> Result<()> { + worker::execute_probe(directory, variant) +} + fn read_meta(config: &Config) -> Value { fs::read_to_string(config.rkp_dir.join("meta.json")) .ok() diff --git a/crates/teeforge/src/rkp/ta.rs b/crates/teeforge/src/rkp/ta.rs index 5066ff0..e20ded0 100644 --- a/crates/teeforge/src/rkp/ta.rs +++ b/crates/teeforge/src/rkp/ta.rs @@ -10,8 +10,13 @@ //! 本层只负责调用,**不解释响应**:返回原始缓冲区,帧布局由 `protocol::decode_frame` //! 结合业务结构判定。早期实现在这里剥帧、上层又剥一次,导致 CBOR 被当成帧头解析。 //! This layer does not interpret the reply; framing is resolved exactly once upstream. +//! +//! 探测模式 [`PROBES`] 用于区分「固件不认命令」「缓冲区约定不同」「参数顺序相反」: +//! 每个变体在独立子进程里执行,崩溃只会丢失该组结果。 +//! Probe variants run one per child process, so a crash only loses that variant. use crate::error::Result; +use crate::rkp::protocol::BUFFER_SIZE; /// 原始响应缓冲区与 `send_cmd` 返回码。 /// @@ -24,17 +29,189 @@ pub(crate) struct RawFrame { pub(crate) status: i32, } +/// 响应缓冲区的初始化方式。 +/// +/// 部分固件会把 out 缓冲区的前几个字节当作「容量」或状态字段读取,预置哨兵反而会 +/// 让它走空响应分支,因此探测时需要区分这三种做法。 +/// Some firmware reads the first bytes of the out buffer as capacity or status. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BufferInit { + /// 前 4 字节写 `i32::MIN`,用于识别「完全未写入」。 + Sentinel, + /// 全 0。 + Zero, + /// 前 4 字节写缓冲区容量。 + Capacity, +} + +/// 请求帧的封装方式。 +/// +/// 主机侧只验证协议与帧构造,厂商调用路径仅在 arm64 Android 编译, +/// 因此非该目标下 `LengthLe` 可能是未使用变体。 +#[cfg_attr( + not(all(target_os = "android", target_arch = "aarch64")), + allow(dead_code) +)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RequestFrame { + /// `[u32 LE cmd][cbor]`,当前实现。 + Plain, + /// `[u32 LE cmd][u32 BE len][cbor]` + LengthBe, + /// `[u32 LE cmd][u32 LE len][cbor]` + /// + /// 保留为候选:大端约定更常见,但小端实现存在,需要时加一行变体即可。 + /// Kept as a candidate; only big-endian is currently probed. + #[allow(dead_code)] + LengthLe, +} + +/// 一次调用的全部可变参数。 +#[cfg_attr( + not(all(target_os = "android", target_arch = "aarch64")), + allow(dead_code) +)] +#[derive(Debug, Clone, Copy)] +pub(crate) struct CallOptions { + pub(crate) level: i32, + pub(crate) buffer: BufferInit, + pub(crate) buffer_len: usize, + pub(crate) frame: RequestFrame, +} + +impl Default for CallOptions { + fn default() -> Self { + Self { + level: 1, + buffer: BufferInit::Sentinel, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + } + } +} + +/// 一次调用的原始观察,包含请求缓冲区被改写的情况。 +pub(crate) struct CallResult { + pub(crate) status: i32, + pub(crate) response: Vec, + pub(crate) request_before: Vec, + pub(crate) request_after: Vec, +} + +/// 探测变体表:标签 + 参数。 +/// +/// 顺序固定,`--rkp-probe` 的输出按这个顺序排列,便于与现场日志对照。 +pub(crate) const PROBES: [(&str, CallOptions); 6] = [ + ( + "baseline", + CallOptions { + level: 1, + buffer: BufferInit::Sentinel, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + }, + ), + ( + "zero-buffer", + CallOptions { + level: 1, + buffer: BufferInit::Zero, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + }, + ), + ( + "capacity-header", + CallOptions { + level: 1, + buffer: BufferInit::Capacity, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + }, + ), + ( + "small-buffer", + CallOptions { + level: 1, + buffer: BufferInit::Zero, + buffer_len: 0x2000, + frame: RequestFrame::Plain, + }, + ), + ( + "level-0", + CallOptions { + level: 0, + buffer: BufferInit::Zero, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + }, + ), + ( + "length-prefix", + CallOptions { + level: 1, + buffer: BufferInit::Zero, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::LengthBe, + }, + ), +]; + +/// 按变体把命令头与 payload 重新封装成实际发送的字节。 +#[cfg_attr( + not(all(target_os = "android", target_arch = "aarch64")), + allow(dead_code) +)] +pub(crate) fn frame_request(request: &[u8], frame: RequestFrame) -> Vec { + let Some((command, payload)) = request.split_at_checked(4) else { + return request.to_vec(); + }; + let length = payload.len().to_le_bytes(); + match frame { + RequestFrame::Plain => request.to_vec(), + RequestFrame::LengthBe => { + let mut out = command.to_vec(); + out.extend_from_slice(&(payload.len() as u32).to_be_bytes()); + out.extend_from_slice(payload); + out + } + RequestFrame::LengthLe => { + let mut out = command.to_vec(); + out.extend_from_slice(&length); + out.extend_from_slice(payload); + out + } + } +} + +/// 缓冲区中最后一个非零字节之后的长度;`0` 表示完全没被写入。 +pub(crate) fn written_len(bytes: &[u8]) -> usize { + bytes + .iter() + .rposition(|byte| *byte != 0) + .map(|index| index + 1) + .unwrap_or(0) +} + +/// 请求前后是否被厂商代码改写;改写说明 out 参数可能落在了请求缓冲区上。 +pub(crate) fn request_changed(before: &[u8], after: &[u8]) -> bool { + before != after +} + #[cfg(all(target_os = "android", target_arch = "aarch64"))] mod imp { use crate::error::{Result, TfError}; use crate::rkp::protocol::BUFFER_SIZE; - use crate::rkp::ta::RawFrame; + use crate::rkp::ta::{BufferInit, CallOptions, CallResult, RawFrame, frame_request}; use std::ffi::c_void; use std::mem::MaybeUninit; use libloading::Library; const LIBRARY_PATH: &str = "/vendor/lib64/libkeymasterdeviceutils.so"; + /// 观察窗口:足够覆盖 CBOR 头部与帧头。 + const OBSERVE_BYTES: usize = 96; type Constructor = unsafe extern "C" fn(*mut c_void, i32); type Destructor = unsafe extern "C" fn(*mut c_void); @@ -60,7 +237,7 @@ mod imp { } impl Session { - fn open() -> Result { + fn open(level: i32) -> Result { // SAFETY: 仅本适配层解析固定厂商 ABI,且只在 arm64 Android 的隔离子进程中调用。 // SAFETY: only this adapter resolves the fixed vendor ABI, in an isolated child. let library: Library = unsafe { @@ -86,9 +263,9 @@ mod imp { } .map_err(|_| fail("缺少 send_cmd 符号 [send_cmd symbol missing]"))?; let mut object = Box::new(VendorObject([MaybeUninit::new(0); 0x40])); - // SAFETY: 对齐的可写存储地址在 Drop 前保持稳定;security_level 1 为 TEE。 - // SAFETY: stable aligned storage; security_level 1 is TRUSTED_ENVIRONMENT. - unsafe { constructor(object.as_mut() as *mut VendorObject as *mut c_void, 1) }; + // SAFETY: 对齐的可写存储地址在 Drop 前保持稳定。 + // SAFETY: stable aligned storage. + unsafe { constructor(object.as_mut() as *mut VendorObject as *mut c_void, level) }; Ok(Self { object, destructor, @@ -97,12 +274,21 @@ mod imp { }) } - fn request(&mut self, request: &[u8]) -> Result { - let mut request = request.to_vec(); - let mut response = vec![0u8; BUFFER_SIZE]; - // 非零哨兵避免未写入的响应被误判为有效。 - // A nonzero sentinel keeps untouched storage from looking valid. - response[..4].copy_from_slice(&i32::MIN.to_le_bytes()); + fn call(&mut self, request: &[u8], options: CallOptions) -> CallResult { + let sent = frame_request(request, options.frame); + let mut request = sent; + let capacity = options.buffer_len.min(BUFFER_SIZE).max(16); + let mut response = vec![0u8; capacity]; + match options.buffer { + BufferInit::Sentinel => { + response[..4].copy_from_slice(&i32::MIN.to_le_bytes()); + } + BufferInit::Zero => {} + BufferInit::Capacity => { + response[..4].copy_from_slice(&(capacity as u32).to_le_bytes()); + } + } + let before = request[..request.len().min(OBSERVE_BYTES)].to_vec(); // SAFETY: 实例存活,两块可写缓冲区在同步调用期间有效且长度匹配。 // SAFETY: live instance, disjoint buffers valid for the synchronous call. let status = unsafe { @@ -114,12 +300,37 @@ mod imp { response.len() as u32, ) }; - if status != 0 { - return Err(crate::rkp::explain::status_error(status)); + let after = request[..request.len().min(OBSERVE_BYTES)].to_vec(); + CallResult { + status, + response, + request_before: before, + request_after: after, + } + } + + fn request(&mut self, request: &[u8]) -> Result { + let result = self.call(request, CallOptions::default()); + if result.status != 0 { + return Err(crate::rkp::explain::status_error(result.status)); + } + // 哨兵仍在说明厂商完全没有写入;此时直接报 ABI 问题,比让上层猜帧格式准确。 + if result.response.starts_with(&i32::MIN.to_le_bytes()) { + return Err(TfError::staged( + "frame", + "abi", + "TA 未写入响应缓冲区 [TA never wrote a response]", + ) + .with_hint("rkp.frame.abi") + .with_raw(serde_json::json!({ + "buffer": result.response.len(), + "request_changed": super::request_changed(&result.request_before, &result.request_after), + "head_hex": crate::rkp::protocol::head_hex(&result.response), + }))); } Ok(RawFrame { - bytes: response, - status, + bytes: result.response, + status: result.status, }) } } @@ -133,22 +344,80 @@ mod imp { } pub(super) fn request(bytes: &[u8]) -> Result { - Session::open()?.request(bytes) + Session::open(1)?.request(bytes) + } + + pub(super) fn probe(bytes: &[u8], options: CallOptions) -> Result { + Ok(Session::open(options.level)?.call(bytes, options)) } } #[cfg(not(all(target_os = "android", target_arch = "aarch64")))] mod imp { use crate::error::{Result, TfError}; - use crate::rkp::ta::RawFrame; + use crate::rkp::ta::{CallOptions, CallResult, RawFrame}; pub(super) fn request(_bytes: &[u8]) -> Result { Err(TfError::new( "设备端 RKP 只能由 Android arm64 触发 [device RKP requires Android arm64]", )) } + + pub(super) fn probe(_bytes: &[u8], _options: CallOptions) -> Result { + Err(TfError::new( + "设备端 RKP 只能由 Android arm64 触发 [device RKP requires Android arm64]", + )) + } } pub(crate) fn request(bytes: &[u8]) -> Result { imp::request(bytes) } + +/// 用指定参数执行一次调用,不做状态判定。 +pub(crate) fn probe(bytes: &[u8], options: CallOptions) -> Result { + imp::probe(bytes, options) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn plain_frame_keeps_bytes() { + let request = [0x22, 0x31, 0x00, 0x00, 0xa1, 0x01]; + assert_eq!( + frame_request(&request, RequestFrame::Plain), + request.to_vec() + ); + } + + #[test] + fn length_prefix_is_inserted_after_command() { + let request = [0x22, 0x31, 0x00, 0x00, 0xa1, 0x01]; + let framed = frame_request(&request, RequestFrame::LengthBe); + assert_eq!(&framed[..4], &[0x22, 0x31, 0x00, 0x00]); + assert_eq!(&framed[4..8], &[0, 0, 0, 2]); + assert_eq!(&framed[8..], &[0xa1, 0x01]); + } + + #[test] + fn written_len_ignores_leading_zeros() { + assert_eq!(written_len(&[0, 0, 0, 0]), 0); + assert_eq!(written_len(&[0, 0, 7, 0]), 3); + } + + #[test] + fn request_change_is_detected() { + assert!(request_changed(&[1, 2], &[1, 3])); + assert!(!request_changed(&[1, 2], &[1, 2])); + } + + #[test] + fn probe_variants_have_unique_labels() { + let mut labels = PROBES.iter().map(|(label, _)| *label).collect::>(); + labels.sort_unstable(); + labels.dedup(); + assert_eq!(labels.len(), PROBES.len()); + } +} diff --git a/crates/teeforge/src/rkp/worker.rs b/crates/teeforge/src/rkp/worker.rs index 63a6b16..62e3cba 100644 --- a/crates/teeforge/src/rkp/worker.rs +++ b/crates/teeforge/src/rkp/worker.rs @@ -90,6 +90,90 @@ pub(crate) fn execute(directory: &Path) -> Result<()> { write_private(&directory.join("reply.json"), &encoded) } +/// 逐个变体在独立子进程中探测。 +/// +/// 每个变体都会重新 dlopen 厂商库并构造对象:这样某一组触发崩溃时只丢失该组, +/// 而不是整轮探测;构造参数(security_level)不同也无法复用同一个对象。 +/// Each variant gets its own child so a crash loses only that variant. +pub(crate) fn probe_all(tmp_root: &Path, request: &[u8]) -> Result> { + let executable = env::current_exe() + .map_err(|error| TfError::new(format!("无法定位当前可执行文件 [current_exe]: {error}")))?; + let mut results = Vec::new(); + for (index, (label, _)) in crate::rkp::ta::PROBES.iter().enumerate() { + let directory = unique_dir(tmp_root)?; + let outcome = (|| -> Result { + write_private(&directory.join("request.bin"), request)?; + let mut command = Command::new(&executable); + command + .arg("--rkp-ta-probe") + .arg(&directory) + .arg(index.to_string()) + .env("LD_LIBRARY_PATH", VENDOR_LIBRARY_PATH); + let output = process::run_bounded( + command, + "teeforge RKP probe", + TIMEOUT, + 4096, + "设备端 RKP 探测 [device RKP probe]", + )?; + if !output.status.success() { + return Ok(json!({ + "variant": label, + "ok": false, + "crashed": true, + "exit": output.status.code(), + "stderr": stderr_detail(&output.stderr), + })); + } + let reply = read_bounded(&directory.join("probe.json"), MAX_REPLY_BYTES)?; + let mut value: Value = serde_json::from_slice(&reply).map_err(|error| { + TfError::new(format!("解析探测回执失败 [decode probe]: {error}")) + })?; + if value.is_object() { + value["variant"] = json!(label); + } + Ok(value) + })(); + let _ = fs::remove_dir_all(&directory); + results.push(match outcome { + Ok(value) => value, + Err(error) => json!({ "variant": label, "ok": false, "error": error.to_string() }), + }); + } + Ok(results) +} + +/// 子进程入口:按变体执行一次调用并写探测回执。 +pub(crate) fn execute_probe(directory: &Path, index: usize) -> Result<()> { + let result = (|| { + let request = read_bounded(&directory.join("request.bin"), protocol::MAX_REQUEST_SIZE)?; + protocol::ta_payload(&request)?; + let (label, options) = crate::rkp::ta::PROBES + .get(index) + .ok_or_else(|| TfError::new("未知探测变体 [unknown probe variant]"))?; + let call = crate::rkp::ta::probe(&request, *options)?; + Ok(json!({ + "ok": true, + "variant": label, + "level": options.level, + "buffer_len": options.buffer_len, + "status": call.status, + "written": crate::rkp::ta::written_len(&call.response), + "response_head": protocol::head_hex(&call.response), + "request_changed": crate::rkp::ta::request_changed(&call.request_before, &call.request_after), + "request_before": protocol::head_hex(&call.request_before), + "request_after": protocol::head_hex(&call.request_after), + })) + })(); + let reply = match result { + Ok(value) => value, + Err(error) => json!({ "ok": false, "error": describe(&error) }), + }; + let encoded = serde_json::to_vec(&reply) + .map_err(|error| TfError::new(format!("编码探测回执失败 [encode probe]: {error}")))?; + write_private(&directory.join("probe.json"), &encoded) +} + /// 把 `TfError` 拆成可跨进程传输的对象。 fn describe(error: &TfError) -> Value { let mut payload = json!({ "message": error.message() }); diff --git a/docs/maintenance/ROADMAP.md b/docs/maintenance/ROADMAP.md index c46dab8..b0349b2 100644 --- a/docs/maintenance/ROADMAP.md +++ b/docs/maintenance/ROADMAP.md @@ -32,6 +32,7 @@ Rust 四 ABI 核心和跨平台打包链已作为默认实现;公开文档正 - arm64-v8a 主力设备安装、升级保留配置、音量键选择和开机服务。 - 设备端 RKP:`--rkp-status` 判定、`--rkp-issue` 真实签发、`-18` 等 TA 错误码的透传与解释、产物落盘/删除、`--keybox --source rkp` 安装;小米 17 Pro 等社区成功机型优先。 - RKP 帧布局:小米 17 Pro 实测 `--rkp-issue` 在旧版报 `invalid payload length`,需确认 `decode_frame` 命中的 `frame_layout`(预期为 `bare` 或 `status`),并提供 `error.raw.head_hex` 用于比对。 +- RKP 空响应:小米 17 Pro 在修复版上 `send_cmd` 返回 0 但缓冲区零字节(`written: 0`),需用 `--rkp-probe --json` 确认是缓冲区初始化、`security_level` 还是命令本身的问题;判读表见 README。 - WebUI v2:五个页面在真机 WebView 下的布局与滚动、`ksu.spawn` 流式日志、`ksu.getPackagesInfo` / `ksu://icon` 缺失时的降级、手动选择重启后保持。 - 视口:小米 17 Pro 上确认四边无溢出、首页不满一屏时不能上下拖动、双指缩放无效、吸顶与吸底正常。 - Action:音量键在 Action 上下文可读、三个分支(RKP / 远程池 / 作用域)、超时取消不写文件、日志文件完整。 diff --git a/docs/project/ARCHITECTURE.md b/docs/project/ARCHITECTURE.md index bf3edf6..459c311 100644 --- a/docs/project/ARCHITECTURE.md +++ b/docs/project/ARCHITECTURE.md @@ -137,6 +137,7 @@ keybox_source=rkp - **帧布局由业务结构判定,不由长度字段判定**:长度字段可以自洽但语义不同,能解出合法 `AuthenticatedRequest` 才是真的;命中布局写入 `meta.json.frame_layout`,便于回溯固件差异。 - 错误按阶段结构化,且**跨 worker 进程保留** `stage` / `code` / `hint` / `raw`。帧类失败额外带 `head_hex`(前 64 字节)、`written`(最后一个非零字节位置)与 `status_le`,用于区分“TA 拒绝”“空成功”“封装不同”“ABI 不匹配”。 - TA 状态码走 `explain` 映射(AOSP `ErrorCode.aidl`);未收录的码保留原始数值。 +- `--rkp-probe` 是诊断入口:取一次 challenge 后,把缓冲区初始化(哨兵 / 全 0 / 容量)、`security_level`(1 / 0)、请求封装(裸 / 长度前缀)拆成 6 个变体,**每个变体一个子进程**,因此某个变体触发崩溃只丢失该组。输出 `status`、`written`、`request_changed`、`response_head`,用于区分「固件不认命令」「参数顺序相反」「缓冲区约定不同」三类原因。 - `rkp` 只在 `aarch64-linux-android` 编译出厂商调用路径,其余 ABI 的 `--rkp-status` 直接给出 `arch` 阻断项。 上游来源:Duck-ToolBox `fb6b1e427f2773ac6139d2eba27068fbdfd089de`,MIT,见 `module/licenses/duck-toolbox.txt`。 From 583b1ce6f1c6c6e75678efd491eb657e1a244cb2 Mon Sep 17 00:00:00 2001 From: mcxiaochenn Date: Sun, 18 Oct 2026 08:26:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(rkp):=20=E6=8E=A2=E6=B5=8B=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E5=AF=B9=E8=B1=A1=E5=A4=A7=E5=B0=8F=E5=8F=98=E4=BD=93?= =?UTF-8?q?=E4=B8=8E=E5=BA=93=E7=AC=A6=E5=8F=B7=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设备侧检查排除了权限:keystore2 正常运行,dmesg 里没有任何涉及 teeforge 或 smcinvoke 的 avc 拒绝。公开代码里也找不到第二份 libkeymasterdeviceutils 的调用示例,上游那套「0x40 对象 + 哨兵 + 参数顺序」只是单点观察。 因此再补两个维度: - CallOptions.object_size:0x40 / 0x80 / 0x100 三个变体。对象存储改用 16 字节对齐的动态缓冲区,并统一预留 0x100 尾部余量——如果固件对象 比 0x40 大,构造会写越界,没有余量就会破坏相邻堆数据,反而让探测 结果失去意义 - 新增 rkp/elf.rs:极简 ELF64 动态符号解析,列出 KeymasterUtils 与 send_cmd 相关导出符号,随探测输出为 library_symbols,用于判断是否 存在必须先调用的初始化入口或其它重载;读取失败不影响探测 变体 6 -> 8,README 与 ARCHITECTURE 的判读表同步。 主机 115 项测试通过(新增 4 项 ELF 解析用例)。 四 ABI release:arm64 927,032 字节,包 2,112,878 字节。 --- README.md | 4 +- crates/teeforge/src/cli.rs | 4 +- crates/teeforge/src/rkp/elf.rs | 172 +++++++++++++++++++++++++++++++++ crates/teeforge/src/rkp/mod.rs | 16 +++ crates/teeforge/src/rkp/ta.rs | 70 +++++++++++--- docs/project/ARCHITECTURE.md | 2 +- 6 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 crates/teeforge/src/rkp/elf.rs diff --git a/README.md b/README.md index bd4ad2d..318b103 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ su -c '/data/adb/modules/teeforge_cd/teeforge --self-update check' su -c '/data/adb/modules/teeforge_cd/teeforge --help' ``` -`--rkp-status` 只做本地检查(SoC、厂商库、`/dev/smcinvoke`、fingerprint),不联网、不调用 TA,用于判断设备端 RKP 是否具备条件。`--rkp-probe` 是诊断命令:取一次 challenge 后,把缓冲区初始化、`security_level`、请求封装三组变量拆成 6 个变体,每个变体在独立子进程中调用一次 TA,输出对照数据。所有查询类命令都支持 `--json`,输出统一的 `{ok,data}` / `{ok,error}` 信封;`--json` 只能搭配一个命令。 +`--rkp-status` 只做本地检查(SoC、厂商库、`/dev/smcinvoke`、fingerprint),不联网、不调用 TA,用于判断设备端 RKP 是否具备条件。`--rkp-probe` 是诊断命令:取一次 challenge 后,把缓冲区初始化、`security_level`、请求封装、厂商对象大小四组变量拆成 8 个变体,每个变体在独立子进程中调用一次 TA,输出对照数据。同时会解析 `/vendor/lib64/libkeymasterdeviceutils.so` 的动态符号表,列出 `KeymasterUtils` 与 `send_cmd` 相关入口——如果固件提供了必须先调用的初始化方法或别的重载,会在 `library_symbols` 里直接暴露。所有查询类命令都支持 `--json`,输出统一的 `{ok,data}` / `{ok,error}` 信封;`--json` 只能搭配一个命令。 CLI 任一步失败都会返回非零退出码。完整参数见[项目概览](docs/project/OVERVIEW.md)。 @@ -207,7 +207,9 @@ update_channel=stable | 某变体 `request_changed` 为 true | out 参数落在了请求缓冲区上,参数顺序需要调整 | | `level-0` 有数据 | 该固件要求 `security_level = 0` | | `length-prefix` 有数据 | 请求需要长度前缀 | + | `object-0x80` / `object-0x100` 有数据 | 厂商对象比 0x40 大,`baseline` 的构造写越界破坏了堆 | | 多个变体进程崩溃(`crashed: true`) | 厂商对象布局与该固件不匹配 | + | `library_symbols` 里有 `init` / `initialize` / `connect` 之类入口 | 调用前需要先完成初始化 | - **报“无法识别 TA 响应帧”或“TA 未写入响应缓冲区”**:说明厂商 `send_cmd` 的返回封装与预期不同。`--rkp-issue --json` 的 `error.raw.head_hex` 是响应缓冲区开头 64 字节,`written` 是最后一个非零字节的位置: - 前 8 位为 `00000080` 且 `written` 为 0:缓冲区完全未被写入,属于 ABI 或参数约定不匹配。 - 开头是 `82`、`a2`、`d2` 等 CBOR 首字节:厂商直接返回裸 payload。 diff --git a/crates/teeforge/src/cli.rs b/crates/teeforge/src/cli.rs index 4f8ada3..b9aefef 100644 --- a/crates/teeforge/src/cli.rs +++ b/crates/teeforge/src/cli.rs @@ -132,7 +132,9 @@ fn usage(program: &str) { println!(" --rkp-issue 设备端 RKP 签发并保存产物 [Issue RKP artifacts]"); println!(" --rkp-artifacts 列出 RKP 产物 [List RKP artifacts]"); println!(" --rkp-delete 删除 RKP 产物 [Delete RKP artifacts]"); - println!(" --rkp-probe 探测 TA 响应约定(诊断用,消耗一次 challenge) [Probe TA response]"); + println!( + " --rkp-probe 探测 TA 响应约定(诊断用,消耗一次 challenge) [Probe TA response]" + ); println!(" --self-update MODE check/install 更新模块 [Check or install updates]"); println!(" --update-channel C stable/dev 更新通道 [Update channel]"); println!(" --bootstrap 输出首屏信息 [Emit bootstrap information]"); diff --git a/crates/teeforge/src/rkp/elf.rs b/crates/teeforge/src/rkp/elf.rs new file mode 100644 index 0000000..8671762 --- /dev/null +++ b/crates/teeforge/src/rkp/elf.rs @@ -0,0 +1,172 @@ +//! 极简 ELF64 动态符号读取 [Minimal ELF64 dynsym reader] +//! +//! 只用于诊断:列出厂商库导出的方法,判断是否存在必须先调用的初始化入口、 +//! `send_cmd` 的其它重载,或构造函数的不同变体。 +//! 不解析重定位、不加载库、不执行任何厂商代码。 +//! +//! Values are read through bounds-checked accessors so a malformed file returns an +//! error instead of panicking. + +use crate::error::{Result, TfError}; +use std::fs; + +const SHT_DYNSYM: u32 = 11; +const SYMBOL_SIZE: usize = 24; +const STT_FUNC: u8 = 2; + +/// 读取 `path` 中名字包含 `needle` 的函数符号,去重排序。 +pub(crate) fn function_symbols(path: &str, needle: &str) -> Result> { + let data = fs::read(path) + .map_err(|error| TfError::new(format!("读取 {path} 失败 [read failed]: {error}")))?; + parse(&data, needle) +} + +fn parse(data: &[u8], needle: &str) -> Result> { + if data.get(..4) != Some(b"\x7fELF") { + return Err(fail("不是 ELF 文件 [not an ELF file]")); + } + if data.get(4) != Some(&2) { + return Err(fail("只支持 64 位 ELF [only ELF64 is supported]")); + } + if data.get(5) != Some(&1) { + return Err(fail("只支持小端 ELF [only little-endian is supported]")); + } + + let section_offset = usize_of(u64_at(data, 0x28)?)?; + // e_shentsize(0x3a) 与 e_shnum(0x3c) 都是 u16,不能按 u64 读。 + let section_size = usize_of(u64::from(u16_at(data, 0x3a).unwrap_or(64)))?; + let section_count = usize_of(u64::from(u16_at(data, 0x3c).unwrap_or(0)))?; + if section_size < 64 || section_count == 0 { + return Err(fail("节表不可用 [section table unavailable]")); + } + + // (类型, 偏移, 长度, 关联字符串表索引) + let mut sections = Vec::with_capacity(section_count); + for index in 0..section_count { + let base = section_offset + .checked_add(index.checked_mul(section_size).ok_or_else(overflow)?) + .ok_or_else(overflow)?; + sections.push(( + u32_at(data, base + 4)?, + usize_of(u64_at(data, base + 24)?)?, + usize_of(u64_at(data, base + 32)?)?, + u32_at(data, base + 40)? as usize, + )); + } + + let mut names = Vec::new(); + for index in 0..sections.len() { + let (kind, offset, size, link) = sections[index]; + if kind != SHT_DYNSYM { + continue; + } + let Some((_, string_offset, string_size, _)) = sections.get(link) else { + continue; + }; + let Some(strings) = data.get(*string_offset..string_offset.saturating_add(*string_size)) + else { + continue; + }; + for symbol in 0..(size / SYMBOL_SIZE) { + let Some(entry) = offset.checked_add(symbol * SYMBOL_SIZE) else { + break; + }; + let Some(name_offset) = u32_at(data, entry) + .ok() + .and_then(|value| usize::try_from(value).ok()) + else { + continue; + }; + let Some(info) = data.get(entry + 4) else { + continue; + }; + if info & 0x0f != STT_FUNC { + continue; + } + if let Some(name) = read_name(strings, name_offset) { + if name.contains(needle) { + names.push(name); + } + } + } + } + names.sort(); + names.dedup(); + Ok(names) +} + +fn read_name(strings: &[u8], offset: usize) -> Option { + let tail = strings.get(offset..)?; + let end = tail.iter().position(|byte| *byte == 0)?; + std::str::from_utf8(&tail[..end]).ok().map(str::to_owned) +} + +fn u16_at(data: &[u8], offset: usize) -> Result { + Ok(u16::from_le_bytes( + data.get(offset..offset + 2) + .ok_or_else(|| fail("ELF 读取越界 [out of bounds]"))? + .try_into() + .map_err(|_| fail("ELF 字段长度错误 [bad field width]"))?, + )) +} + +fn u32_at(data: &[u8], offset: usize) -> Result { + Ok(u32::from_le_bytes( + data.get(offset..offset + 4) + .ok_or_else(|| fail("ELF 读取越界 [out of bounds]"))? + .try_into() + .map_err(|_| fail("ELF 字段长度错误 [bad field width]"))?, + )) +} + +fn u64_at(data: &[u8], offset: usize) -> Result { + Ok(u64::from_le_bytes( + data.get(offset..offset + 8) + .ok_or_else(|| fail("ELF 读取越界 [out of bounds]"))? + .try_into() + .map_err(|_| fail("ELF 字段长度错误 [bad field width]"))?, + )) +} + +/// `u64` 到 `usize`;32 位目标上超出范围视为越界。 +fn usize_of(value: u64) -> Result { + usize::try_from(value).map_err(|_| fail("偏移超出地址空间 [offset exceeds address space]")) +} + +fn overflow() -> TfError { + fail("节表偏移溢出 [section offset overflow]") +} + +fn fail(message: impl Into) -> TfError { + TfError::new(format!("符号表 [symbol table]: {}", message.into())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn non_elf_data_is_rejected() { + assert!(parse(b"not an elf file", "send").is_err()); + } + + #[test] + fn truncated_header_is_rejected() { + assert!(parse(b"\x7fELF\x02\x01", "send").is_err()); + } + + #[test] + fn zero_section_table_is_rejected() { + let mut data = vec![0u8; 64]; + data[..4].copy_from_slice(b"\x7fELF"); + data[4] = 2; + data[5] = 1; + assert!(parse(&data, "send").is_err()); + } + + #[test] + fn name_reader_stops_at_nul() { + assert_eq!(read_name(b"send_cmd\0x", 0).as_deref(), Some("send_cmd")); + assert!(read_name(b"send", 9).is_none()); + } +} diff --git a/crates/teeforge/src/rkp/mod.rs b/crates/teeforge/src/rkp/mod.rs index 99594ef..0efea9a 100644 --- a/crates/teeforge/src/rkp/mod.rs +++ b/crates/teeforge/src/rkp/mod.rs @@ -34,6 +34,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; use std::time::{Instant, SystemTime, UNIX_EPOCH}; +mod elf; mod explain; mod keybox; mod protocol; @@ -161,11 +162,26 @@ pub(crate) fn probe(config: &Config) -> Result { ), ); let variants = worker::probe_all(&tmp, &request)?; + // 符号表能说明固件是否提供初始化入口或其它重载;读不到不影响探测结果。 + let mut library_symbols = elf::function_symbols(VENDOR_LIBRARY, "KeymasterUtils") + .unwrap_or_else(|error| { + logging::log( + Level::Warn, + format!("读取库符号失败 [symbols unavailable]: {error}"), + ); + Vec::new() + }); + if let Ok(more) = elf::function_symbols(VENDOR_LIBRARY, "send_cmd") { + library_symbols.extend(more); + } + library_symbols.sort(); + library_symbols.dedup(); Ok(json!({ "soc": diagnosis.soc_model, "fingerprint": diagnosis.fingerprint, "challenge_bytes": challenge.len(), "request_bytes": request.len(), + "library_symbols": library_symbols, "variants": variants, })) } diff --git a/crates/teeforge/src/rkp/ta.rs b/crates/teeforge/src/rkp/ta.rs index e20ded0..ac9a46a 100644 --- a/crates/teeforge/src/rkp/ta.rs +++ b/crates/teeforge/src/rkp/ta.rs @@ -77,6 +77,12 @@ pub(crate) struct CallOptions { pub(crate) buffer: BufferInit, pub(crate) buffer_len: usize, pub(crate) frame: RequestFrame, + /// 传给厂商构造函数的对象存储大小。 + /// + /// 上游按 0x40 分配,但那只是「观察到的客户端」的值;固件升级后对象可能变大, + /// 构造写越界会破坏堆上相邻数据,表现为后续调用静默失败。 + /// Upstream allocates 0x40 from one observed client; newer firmware may need more. + pub(crate) object_size: usize, } impl Default for CallOptions { @@ -86,6 +92,7 @@ impl Default for CallOptions { buffer: BufferInit::Sentinel, buffer_len: BUFFER_SIZE, frame: RequestFrame::Plain, + object_size: 0x40, } } } @@ -101,7 +108,7 @@ pub(crate) struct CallResult { /// 探测变体表:标签 + 参数。 /// /// 顺序固定,`--rkp-probe` 的输出按这个顺序排列,便于与现场日志对照。 -pub(crate) const PROBES: [(&str, CallOptions); 6] = [ +pub(crate) const PROBES: [(&str, CallOptions); 8] = [ ( "baseline", CallOptions { @@ -109,6 +116,7 @@ pub(crate) const PROBES: [(&str, CallOptions); 6] = [ buffer: BufferInit::Sentinel, buffer_len: BUFFER_SIZE, frame: RequestFrame::Plain, + object_size: 0x40, }, ), ( @@ -118,6 +126,7 @@ pub(crate) const PROBES: [(&str, CallOptions); 6] = [ buffer: BufferInit::Zero, buffer_len: BUFFER_SIZE, frame: RequestFrame::Plain, + object_size: 0x40, }, ), ( @@ -127,6 +136,7 @@ pub(crate) const PROBES: [(&str, CallOptions); 6] = [ buffer: BufferInit::Capacity, buffer_len: BUFFER_SIZE, frame: RequestFrame::Plain, + object_size: 0x40, }, ), ( @@ -136,6 +146,7 @@ pub(crate) const PROBES: [(&str, CallOptions); 6] = [ buffer: BufferInit::Zero, buffer_len: 0x2000, frame: RequestFrame::Plain, + object_size: 0x40, }, ), ( @@ -145,6 +156,7 @@ pub(crate) const PROBES: [(&str, CallOptions); 6] = [ buffer: BufferInit::Zero, buffer_len: BUFFER_SIZE, frame: RequestFrame::Plain, + object_size: 0x40, }, ), ( @@ -154,6 +166,27 @@ pub(crate) const PROBES: [(&str, CallOptions); 6] = [ buffer: BufferInit::Zero, buffer_len: BUFFER_SIZE, frame: RequestFrame::LengthBe, + object_size: 0x40, + }, + ), + ( + "object-0x80", + CallOptions { + level: 1, + buffer: BufferInit::Zero, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + object_size: 0x80, + }, + ), + ( + "object-0x100", + CallOptions { + level: 1, + buffer: BufferInit::Zero, + buffer_len: BUFFER_SIZE, + frame: RequestFrame::Plain, + object_size: 0x100, }, ), ]; @@ -205,7 +238,6 @@ mod imp { use crate::rkp::protocol::BUFFER_SIZE; use crate::rkp::ta::{BufferInit, CallOptions, CallResult, RawFrame, frame_request}; use std::ffi::c_void; - use std::mem::MaybeUninit; use libloading::Library; @@ -217,13 +249,17 @@ mod imp { type Destructor = unsafe extern "C" fn(*mut c_void); type SendCommand = unsafe extern "C" fn(*mut c_void, *mut c_void, u32, *mut c_void, u32) -> i32; - // 参考实现为该厂商类分配 0x40 字节;只经由构造/析构函数持有,不读取字段。 - // The reference client allocates 0x40 bytes; only the vendor constructor owns them. - #[repr(C, align(16))] - struct VendorObject([MaybeUninit; 0x40]); + /// 对象存储的尾部余量。 + /// + /// 探测「对象比预期大」时,若按较小尺寸分配,构造函数写越界会破坏相邻堆数据, + /// 反而改变被测行为。预留余量后越界落在自己的存储内,结果才有意义。 + /// Slack keeps an oversized constructor from corrupting unrelated heap data. + const OBJECT_SLACK: usize = 0x100; struct Session { - object: Box, + /// `u128` 保证 16 字节对齐,与厂商类的要求一致。 + /// `u128` gives the 16-byte alignment the vendor class expects. + object: Vec, destructor: Destructor, send: SendCommand, _library: Library, @@ -237,7 +273,7 @@ mod imp { } impl Session { - fn open(level: i32) -> Result { + fn open(level: i32, object_size: usize) -> Result { // SAFETY: 仅本适配层解析固定厂商 ABI,且只在 arm64 Android 的隔离子进程中调用。 // SAFETY: only this adapter resolves the fixed vendor ABI, in an isolated child. let library: Library = unsafe { @@ -262,10 +298,11 @@ mod imp { .get::(b"_ZN14keymasterutils14KeymasterUtils8send_cmdEPvjS1_j\0") } .map_err(|_| fail("缺少 send_cmd 符号 [send_cmd symbol missing]"))?; - let mut object = Box::new(VendorObject([MaybeUninit::new(0); 0x40])); - // SAFETY: 对齐的可写存储地址在 Drop 前保持稳定。 - // SAFETY: stable aligned storage. - unsafe { constructor(object.as_mut() as *mut VendorObject as *mut c_void, level) }; + let words = (object_size.max(0x10) + OBJECT_SLACK).div_ceil(16); + let mut object = vec![0u128; words]; + // SAFETY: 对齐的可写存储地址在 Drop 前保持稳定;容量含余量。 + // SAFETY: stable aligned storage with slack for oversized objects. + unsafe { constructor(object.as_mut_ptr().cast(), level) }; Ok(Self { object, destructor, @@ -293,7 +330,7 @@ mod imp { // SAFETY: live instance, disjoint buffers valid for the synchronous call. let status = unsafe { (self.send)( - self.object.as_mut() as *mut VendorObject as *mut c_void, + self.object.as_mut_ptr().cast(), request.as_mut_ptr().cast(), request.len() as u32, response.as_mut_ptr().cast(), @@ -339,16 +376,17 @@ mod imp { fn drop(&mut self) { // SAFETY: 析构只执行一次,且对象与库在调用期间均存活。 // SAFETY: the destructor runs once while both the object and library are live. - unsafe { (self.destructor)(self.object.as_mut() as *mut VendorObject as *mut c_void) }; + unsafe { (self.destructor)(self.object.as_mut_ptr().cast()) }; } } pub(super) fn request(bytes: &[u8]) -> Result { - Session::open(1)?.request(bytes) + let options = CallOptions::default(); + Session::open(options.level, options.object_size)?.request(bytes) } pub(super) fn probe(bytes: &[u8], options: CallOptions) -> Result { - Ok(Session::open(options.level)?.call(bytes, options)) + Ok(Session::open(options.level, options.object_size)?.call(bytes, options)) } } diff --git a/docs/project/ARCHITECTURE.md b/docs/project/ARCHITECTURE.md index 459c311..3863d3f 100644 --- a/docs/project/ARCHITECTURE.md +++ b/docs/project/ARCHITECTURE.md @@ -137,7 +137,7 @@ keybox_source=rkp - **帧布局由业务结构判定,不由长度字段判定**:长度字段可以自洽但语义不同,能解出合法 `AuthenticatedRequest` 才是真的;命中布局写入 `meta.json.frame_layout`,便于回溯固件差异。 - 错误按阶段结构化,且**跨 worker 进程保留** `stage` / `code` / `hint` / `raw`。帧类失败额外带 `head_hex`(前 64 字节)、`written`(最后一个非零字节位置)与 `status_le`,用于区分“TA 拒绝”“空成功”“封装不同”“ABI 不匹配”。 - TA 状态码走 `explain` 映射(AOSP `ErrorCode.aidl`);未收录的码保留原始数值。 -- `--rkp-probe` 是诊断入口:取一次 challenge 后,把缓冲区初始化(哨兵 / 全 0 / 容量)、`security_level`(1 / 0)、请求封装(裸 / 长度前缀)拆成 6 个变体,**每个变体一个子进程**,因此某个变体触发崩溃只丢失该组。输出 `status`、`written`、`request_changed`、`response_head`,用于区分「固件不认命令」「参数顺序相反」「缓冲区约定不同」三类原因。 +- `--rkp-probe` 是诊断入口:取一次 challenge 后,把缓冲区初始化(哨兵 / 全 0 / 容量)、`security_level`(1 / 0)、请求封装(裸 / 长度前缀)、厂商对象大小(0x40 / 0x80 / 0x100)拆成 8 个变体,**每个变体一个子进程**,因此某个变体触发崩溃只丢失该组。对象存储统一预留 0x100 尾部余量,避免「对象比预期大」时构造写越界破坏相邻堆数据、反过来改变被测行为。输出 `status`、`written`、`request_changed`、`response_head` 与 `library_symbols`(`libkeymasterdeviceutils.so` 的动态符号表),用于区分「固件不认命令」「参数顺序相反」「缓冲区约定不同」「缺少初始化步骤」四类原因。 - `rkp` 只在 `aarch64-linux-android` 编译出厂商调用路径,其余 ABI 的 `--rkp-status` 直接给出 `arch` 阻断项。 上游来源:Duck-ToolBox `fb6b1e427f2773ac6139d2eba27068fbdfd089de`,MIT,见 `module/licenses/duck-toolbox.txt`。