最近刷到砍砍开源的 AssppWeb,里面用 libcurl.js 把 HTTPS fetch 加密再转发,这样ZeroTrust的设计真是很有意思啊。
那既然这样,AltStore 能不能也丢到浏览器里跑?这样可是解决了一个大难题啊
简单查了一圈,发现真正难的是 “anisette” 凭据生成。
anisette是什么?苹果为了验证请求发送者是一个真实的系统,在各种平台上封装了一系列库,使用系统调用来判断系统真实性,并生成一个otp用来发送。
哼哼,那这就到我们网安小子最喜欢的地方了,你用系统调用那我反过来hook掉你的调用不就好了~
普遍的anisette做法是从 Apple Music 里抠两个 so,然后本地加载+hook一些函数来做票据。问题来了:浏览器端怎么搞?
这时候就想到 unicorn。翻 issue 发现官方还没做 wasm 支持(说白了就是 qemu‑tci 没接上),但某个 PR 的评论里有人提到这个分支:
https://github.com/petabyt/unicorn/tree/tci-emscripten
那看起来是有人实现过了!
起初还想用 unicorn.js 的维护版,结果 SIMD 指令不全、版本又老,直接劝退。
折腾到后面我发现:JS 对接 C 原生真是邪门,错误一堆一堆,于是决定——全部用 Rust 写,暴露几个简单又不易出错的接口。底层逻辑参考了 pyprovision-uc。(还真是个抖M呢)
Rust + wasm 移植的第一步很朴素:先写一个正常的库,example 里调用一下,确认本地能跑通。事实证明,传说中的“AI 会写 Rust”是真的。
到了 “Rust + unicorn + wasm”,主要卡在两个点:
- unicorn 默认 Rust binding 会把本地的 libunicorn.o 打包进去,但我们要的是 wasm,ELF 的 o 显然不适合。
- unicorn 的 wasm 产物怎么打包?比如 libunicorn.a?
最后发现 emscripten 给了 emcmake 这种工具,能把中间产物转成 wasm 虚拟机能用的东西。并且看上去完全兼容原来的编译+链接方法
大致这样配:
emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DUNICORN_BUILD_TESTS=OFF \
-DUNICORN_INSTALL=OFF \
-DUNICORN_LEGACY_STATIC_ARCHIVE=ON \
-DUNICORN_INTERPRETER=ON \
-DUNICORN_ARCH="arm;aarch64" \
-DCMAKE_C_COMPILER=emcc 但 CMakeLists 并没有支持 emcc,需要补 patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e6b60aa8..03501788 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -284,6 +284,11 @@ else()
set(UNICORN_TARGET_ARCH "loongarch64")
break()
endif()
+ string(FIND ${UC_COMPILER_MACRO} "__wasm__" UC_RET)
+ if (${UC_RET} GREATER "0")
+ set(UNICORN_TARGET_ARCH "tci")
+ break()
+ endif()
message(FATAL_ERROR "Unknown host compiler: ${CMAKE_C_COMPILER}.")
endwhile(TRUE)
endif()
@@ -395,10 +400,15 @@ else()
else()
set(UNICORN_EXECUTION_MODE "--disable-interpreter")
endif()
+ if(EMSCRIPTEN)
+ set(EXTRA_EXTRA_CFLAGS --cpu=i386 --disable-stack-protector)
+ endif()
+ set (EXTRA_CFLAGS "${EXTRA_CFLAGS} -g")
execute_process(COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/qemu/configure
--cc=${CMAKE_C_COMPILER}
${EXTRA_CFLAGS}
${TARGET_LIST}
+ ${EXTRA_EXTRA_CFLAGS}
${UNICORN_EXECUTION_MODE}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
@@ -508,8 +518,8 @@ else()
-D_GNU_SOURCE
-D_FILE_OFFSET_BITS=64
-D_LARGEFILE_SOURCE
- -Wall
- -fPIC
+ -Wall -O2
+ -fPIC -fpic -fvisibility=hidden
)
if (ATOMIC_LINKAGE_FIX)
add_compile_options( qemu/configure 也得补一下:
diff --git a/qemu/configure b/qemu/configure
index 30854676..378ffecb 100755
--- a/qemu/configure
+++ b/qemu/configure
@@ -5,6 +5,7 @@
# Unset some variables known to interfere with behavior of common tools,
# just as autoconf does.
+set -x
CLICOLOR_FORCE= GREP_OPTIONS=
unset CLICOLOR_FORCE GREP_OPTIONS
@@ -402,6 +403,8 @@ EOF
if check_define __linux__ ; then
targetos="Linux"
+elif check_define __wasm__ ; then
+ targetos='Linux'
elif check_define _WIN32 ; then
targetos='MINGW32'
elif check_define __OpenBSD__ ; then 接下来就是“正常逻辑先跑通,再换 wasm target”。在 wasm 里几乎要 hook 掉所有 syscall(其实是 libc call 或类似接口)去伪造环境。我这边是在 got.plt 的跳转点加 code_hook,再按符号名分发:
for i in 0..IMPORT_LIBRARY_COUNT {
let base = IMPORT_ADDRESS + (i as u64) * IMPORT_LIBRARY_STRIDE;
uc.mem_map(base, as_usize(IMPORT_SIZE)?, Permission::ALL)?;
let mut stubs = vec![0_u8; IMPORT_SIZE as usize];
for chunk in stubs.chunks_mut(4) {
chunk.copy_from_slice(&RET_AARCH64);
}
uc.mem_write(base, &stubs)?;
uc.add_code_hook(base, base + IMPORT_SIZE - 1, |uc, address, _| {
if let Err(err) = dispatch_import_stub(uc, address) {
debug_print(format!("import hook failed at 0x{address:X}: {err}"));
let _ = uc.emu_stop();
}
})?;
}
pub fn dispatch_import_stub(uc: &mut Unicorn<'_, RuntimeState>, address: u64) -> Result<(), VmError> {
if address < IMPORT_ADDRESS {
return Err(VmError::InvalidImportAddress(address));
}
let offset = address - IMPORT_ADDRESS;
let library_index = (offset / IMPORT_LIBRARY_STRIDE) as usize;
let symbol_index = ((offset % IMPORT_LIBRARY_STRIDE) / 4) as usize;
let symbol_name = {
let state = uc.get_data();
let library = state
.loaded_libraries
.get(library_index)
.ok_or(VmError::LibraryNotLoaded(library_index))?;
let symbol = library
.symbols
.get(symbol_index)
.ok_or_else(|| VmError::SymbolIndexOutOfRange {
library: library.name.clone(),
index: symbol_index,
})?;
symbol.name.clone()
};
handle_stub_by_name(uc, &symbol_name)
}
fn handle_stub_by_name(
uc: &mut Unicorn<'_, RuntimeState>,
symbol_name: &str,
) -> Result<(), VmError> {
match symbol_name {
"malloc" => stub_malloc(uc),
"free" => stub_free(uc),
"strncpy" => stub_strncpy(uc),
"mkdir" => stub_mkdir(uc),
"umask" => stub_umask(uc),
"chmod" => stub_chmod(uc),
"lstat" => stub_lstat(uc),
"fstat" => stub_fstat(uc),
"open" => stub_open(uc),
"ftruncate" => stub_ftruncate(uc),
"read" => stub_read(uc),
"write" => stub_write(uc),
"close" => stub_close(uc),
"dlopen" => stub_dlopen(uc),
"dlsym" => stub_dlsym(uc),
"dlclose" => stub_dlclose(uc),
"pthread_once" => stub_return_zero(uc),
"pthread_create" => stub_return_zero(uc),
"pthread_mutex_lock" => stub_return_zero(uc),
"pthread_rwlock_unlock" => stub_return_zero(uc),
"pthread_rwlock_destroy" => stub_return_zero(uc),
"pthread_rwlock_wrlock" => stub_return_zero(uc),
"pthread_rwlock_init" => stub_return_zero(uc),
"pthread_mutex_unlock" => stub_return_zero(uc),
"pthread_rwlock_rdlock" => stub_return_zero(uc),
"gettimeofday" => stub_gettimeofday(uc),
"__errno" => stub_errno_location(uc),
"__system_property_get" => stub_system_property_get(uc),
"arc4random" => stub_arc4random(uc),
other => {
debug_print(other);
Err(VmError::UnhandledImport(other.to_string()))
}
}
} Hook 出来之后,剩下就是“按需补 stub”。
顺带一提:hook 里只有时间是变量,如果时间不变,生成的 OTP 也会稳定一致。
另外,cargo 编译这块我用 .cargo/pkg-config.toml 强行指定链接地址:
[env]
PKG_CONFIG_ALLOW_CROSS = "1"
PKG_CONFIG_PATH = { value = ".cargo/pkgconfig", relative = true }
[target.wasm32-unknown-emscripten]
rustflags = [
"-C",
"link-arg=--no-entry",
"-C",
"link-arg=-sSTANDALONE_WASM=1",
"-C",
"link-arg=-sALLOW_MEMORY_GROWTH=1",
"-C",
"link-arg=-sINITIAL_MEMORY=268435456",
] prefix=${pcfiledir}/../../../unicorn
exec_prefix=${prefix}
libdir=${exec_prefix}/build
includedir=${exec_prefix}/include
Name: unicorn
Description: Unicorn engine static library (wasm build)
Version: 2.1.1
Libs: -L${libdir} -lunicorn -lunicorn-common -laarch64-softmmu -larm-softmmu
Cflags: -I${includedir} 经过一天愉快的调试,本地rust跑通了!
接下来就build emscripten runtime的wasm了!
然后就遇到了程序员经典玄学问题之:为什么它能跑?为什么它不能跑?是 wasm 环境里“同样代码跑不通”。先确认一个前提:问题不在 unicorn 本体上,只可能是 wasm 相关改动。于是用对拍 + 手动 hook register 的方式确认逻辑能完整跑通,然后再把这些调试逻辑清理掉。
做法也挺朴素:固定所有输入(时间 + device token),每 1k 次输出一次寄存器快照,用脚本找差异,缩小区间。原来这就是 Baby Step Giant Step 对拍啊 XD。
第一个坑是 UMULH:应该做 (x1 * x2) >> 64,结果不对。把问题丢给 codex 读了一通,发现是 TCI 的 wasm ABI helper 参数截断。修掉后 UMULH 正常。
第二个坑在内存读取:某些地址读不对。于是我在 Python 版和 Rust wasm 版同一位置加 code hook / mem read hook,定位读地址,再回溯到上一次写地址,dump 一下内存,问题就清楚了。
最后发现是 mmap 的 wasm 实现用了 aligned_alloc,但和原版语义不一致:
size_t total = size + align - getpagesize();
return aligned_alloc(align, size); 没考虑 round-up,于是改成这样:
if (!is_power_of_2(align)) return NULL;
size_t rounded = (size + align - 1) & ~(align - 1);
void *p = aligned_alloc(align, rounded);
return p; 最后修改后的代码在这里,方便参考:
后面的签名部分还没完全调通,等我折腾完了再更新ww。