Logo

Altstore-wasm移植记录

February 27, 2026

最近刷到砍砍开源的 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”,主要卡在两个点:

  1. unicorn 默认 Rust binding 会把本地的 libunicorn.o 打包进去,但我们要的是 wasm,ELF 的 o 显然不适合。
  2. unicorn 的 wasm 产物怎么打包?比如 libunicorn.a?

最后发现 emscripten 给了 emcmake 这种工具,能把中间产物转成 wasm 虚拟机能用的东西。并且看上去完全兼容原来的编译+链接方法

大致这样配:

PYTHON
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
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

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,再按符号名分发:

RUST
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 强行指定链接地址:

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",
]
TOML
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,但和原版语义不一致:

C++
size_t total = size + align - getpagesize();
return aligned_alloc(align, size);

没考虑 round-up,于是改成这样:

C++
if (!is_power_of_2(align)) return NULL;
size_t rounded = (size + align - 1) & ~(align - 1);
void *p = aligned_alloc(align, rounded);
return p;

https://github.com/lbr77/unicorn/commit/4de7c20ba8a7119405b3e0e8b82862efc70bda45#diff-842456abe9564ae1e7d75ab8f322be6c27ca3c512e445a18e5898dea68ad9799

最后修改后的代码在这里,方便参考:

后面的签名部分还没完全调通,等我折腾完了再更新ww。

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。