Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`、请求封装、厂商对象大小四组变量拆成 8 个变体,每个变体在独立子进程中调用一次 TA,输出对照数据。同时会解析 `/vendor/lib64/libkeymasterdeviceutils.so` 的动态符号表,列出 `KeymasterUtils` 与 `send_cmd` 相关入口——如果固件提供了必须先调用的初始化方法或别的重载,会在 `library_symbols` 里直接暴露。所有查询类命令都支持 `--json`,输出统一的 `{ok,data}` / `{ok,error}` 信封;`--json` 只能搭配一个命令。

CLI 任一步失败都会返回非零退出码。完整参数见[项目概览](docs/project/OVERVIEW.md)。

Expand Down Expand Up @@ -197,6 +198,18 @@ 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` 有数据 | 请求需要长度前缀 |
| `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。
Expand Down
36 changes: 36 additions & 0 deletions crates/teeforge/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct Options {
rkp_issue: bool,
rkp_artifacts: bool,
rkp_delete: bool,
rkp_probe: bool,
target_list: bool,
target_apply: Option<PathBuf>,
target_mode: Option<String>,
Expand Down Expand Up @@ -61,6 +62,7 @@ fn parse(args: &[String]) -> Result<Options> {
"--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;
Expand Down Expand Up @@ -130,6 +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!(" --self-update MODE check/install 更新模块 [Check or install updates]");
println!(" --update-channel C stable/dev 更新通道 [Update channel]");
println!(" --bootstrap 输出首屏信息 [Emit bootstrap information]");
Expand Down Expand Up @@ -171,6 +176,24 @@ pub fn run(args: Vec<String>) -> 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::<usize>() 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) => {
Expand Down Expand Up @@ -210,6 +233,7 @@ pub fn run(args: Vec<String>) -> 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()
Expand Down Expand Up @@ -298,6 +322,12 @@ pub fn run(args: Vec<String>) -> 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) => {
Expand Down Expand Up @@ -442,6 +472,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");
}
Expand Down Expand Up @@ -551,6 +584,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));
}
Expand Down
172 changes: 172 additions & 0 deletions crates/teeforge/src/rkp/elf.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<String>> {
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<Vec<String>> {
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<String> {
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<u16> {
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<u32> {
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<u64> {
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> {
usize::try_from(value).map_err(|_| fail("偏移超出地址空间 [offset exceeds address space]"))
}

fn overflow() -> TfError {
fail("节表偏移溢出 [section offset overflow]")
}

fn fail(message: impl Into<String>) -> 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());
}
}
55 changes: 55 additions & 0 deletions crates/teeforge/src/rkp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -136,6 +137,55 @@ pub(crate) fn issue(config: &Config) -> Result<Issued> {
})
}

/// 多变异探测:定位「空成功」的成因。
///
/// 现场出现过 `send_cmd` 返回 0 但缓冲区零字节的情况:既不是帧格式问题,也不是
/// 长度字段问题。探测把缓冲区初始化、security_level、请求封装三个变量拆开,
/// 每个变体独立子进程执行,用一次装包换回全部对照数据。
/// Probes one variable at a time to locate the "empty success" path.
pub(crate) fn probe(config: &Config) -> Result<Value> {
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)?;
// 符号表能说明固件是否提供初始化入口或其它重载;读不到不影响探测结果。
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,
}))
}

/// 签发并写入 `RKP/`:keybox.xml、cert-chain.pem、meta.json。
pub(crate) fn issue_and_save(config: &Config) -> Result<SavedArtifacts> {
let issued = issue(config)?;
Expand Down Expand Up @@ -417,6 +467,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()
Expand Down
Loading
Loading