一个月以来,我干了些什么?
摘要生成中
一个月的主线是把三星S24 Ultra国行免解锁root从单机偶然成功打磨成可复现、可审计、可快速适配新固件的流水线。核心结论是载荷按内核构建定标而非按机型,同构建号不同机型可共用,国行与外版不可混用,内核版本不同必须单独定标。定标流程已从下载14GB全包手抄偏移压缩为下载120MB、跑三个脚本、十几分钟完成。DZF2与DZH3同内核线,十万余符号中仅七个静态字符串不同,载荷直接复用;CZB2属另一条线则需单独定标。
— AI生成,仅供参考

题记:废话不说,直接进正题。本月主线一句话:把”三星S24 Ultra国行免解锁root”从一台机子的偶然成功,磨成一套可复现、可审计、可给新固件快速适配的流水线。活儿大部分是配合AI(某自然语言处理大模型)干的,代码说实话我都没完全吃透,但我相信总会理解的——毕竟这代码本身就是大模型写的。

0x00 RootMyS24

先说结论:载荷是按内核构建定标的,不是按机型。机型只是表面,真正决定能不能跑的是那几行偏移和那张滑动指纹。

0x00-1 定标三规则

这一个月反复验证,沉淀成三条规则:

  • 同构建号、不同机型,载荷可共用。S9210/S9260/S9280国行同build,一套载荷通吃(代码段符号、数据段偏移、指纹都一样)。
  • 国行与上版/外版不可混用。哪怕内核版本号一样,kmalloc_caches、nfnetlink_log这类数据段偏移不同,硬套就是全挂。港台同build可互用,国行是单独一条线。
  • 内核版本不同,必须单独定标。最费功夫、最容易翻车,这个月栽过跟头。

0x00-2 把定标流程从”玄学”变成”脚本”

以前适配新固件:下载14GB全包 → 解包 → 反汇编 → 手抄偏移 → 真机试错。现在:下120MB → 跑三个脚本 → 编一次,从头到尾十几分钟。核心脚本逐段看:

0x00-2-1 取 boot:fetch_boot_prefix.sh

VER="${1:?需要4段版本串}"          # 例:S9280ZCS5CZB2/S9280CHC5CZB2/S9280ZCS5CZB1/S9280ZCS5CZB2
OUT="${2:?需要输出目录}"
NEED=${3:-120000000}                # 默认只要文件开头120MB就够覆盖AP里的boot
"$T/samloader" download -m SM-S9280 -r CHC -j 1 -v "$VER" -d "$OUT" &   # -j1单线程,避免分块线程卡死
# 等"文件开头N字节都真正落盘"(稀疏文件里未下载区域是空洞,读出来是全零)
prefix_cov() {   # 用SEEK_DATA/SEEK_HOLE数出[0,第一个空洞)的长度,即已连续下载的字节
  python3 - "$f" <<'PY'
import os, sys
fd=os.open(sys.argv[1], os.O_RDONLY)
try:
    d=os.lseek(fd, 0, os.SEEK_DATA)   # 第一个有数据的偏移(从0开始=0)
    h=os.lseek(fd, d, os.SEEK_HOLE)   # 第一个空洞偏移
    print(h if d==0 else 0)           # 若0处是数据,前缀覆盖到h;否则0
finally: os.close(fd)
PY
}
# 覆盖够120MB后杀掉下载,把zip里第一个tar的boot.img.lz4抠出来解压

妙处:samloader多线程默认把文件分成8段乱序下载,chunk 0那条线程有时会卡死(上次DZH3卡在86MB不动)。单线程顺序写,前缀就能稳定增长,到120MB立刻掐。

0x00-2-2 还原内核:analyze_boot.sh

# 拆boot,得到kernel + ramdisk
magiskboot unpack boot.img
# 内核本身可能是lz4压缩的,解出来;raw的话直接用
magiskboot decompress kernel kernel_raw 2>/dev/null || cp kernel kernel_raw
# 从原始Image还原带符号的ELF——后面所有偏移都从这查
PYTHONPATH=vmlinux-to-elf python3 -m vmlinux_to_elf.scripts.vmlinux_to_elf \
  kernel_raw vmlinux.elf   # 产出43MB ELF,符号可用llvm-nm/objdump查

0x00-2-3 符号身份迁移:gen_target_h.py

把”适配从抄作业变成查符号表”的关键。思路:以DZF2的target.h为模板,对每个偏移取”模板内核里那个地址上的符号名”,在新内核里按同名回查,自动生成新target.h:

def main():
    ...
    for i,line in enumerate(lines):
        m = re.match(r"(\s*#define\s+)([A-Z0-9_]+_OFF)(\s+)(0x[0-9a-fA-F]+)", line)
        if not m: continue
        old = int(m.group(4), 16)
        # 1) 模板内核里,这个偏移落在哪个符号上?
        sym = tpl_addr2sym.get(BASE + old)      # 地址→符号名
        # 2) 新内核里按同名回查,得到新地址
        if sym and sym in new_sym2addr:
            new = new_sym2addr[sym] - BASE
            lines[i] = f"{m.group(1)}{m.group(2)}{m.group(3)}0x{new:09x}\n"
        # 3) 特殊"符号+字段偏移"型:ashmem_miscs+0x10, worker_thread+0xa0 显式处理
    # 顺带把 BUILD_VARIANT_LABEL / BUILD_FINGERPRINT 换成目标型号

实测:DZF2→DZH3迁移了23个偏移全对(符号99.99%相同);CZB2是另一条线,20个偏移0个对得上,只能老老实实单独定标。

0x00-2-4 生成 KASLR 滑动指纹:gen_p0_fingerprint.py

# 32行指纹,每行对应一个可能的slide(0x000000..0x1f0000步进0x10000)
for slide in range(0, 0x200000, 0x10000):
    at = 0x1f0000 - slide              # 该slide下,内核Image的采样基址
    words = [unpack64(img, at + o) for o in (0x000,0x200,...,0xe00)]  # 8个qword
    assert words[0] == unpack64(img, 0)  # 末行(Index[0])=Image[0]的MZ魔数,自检
    # 这8个qword就是P0物理页oracle用来认slide的指纹

0x01-1 DZH3:捡了个便宜,省了整段定标

DZF2和DZH3是同一内核线:6.1.145-android14-11-3254743,只差末尾DZF2→DZH3。比对——103,514个符号里103,507个地址完全相同,就7个静态字符串(f_midi_longname、pty_line_name、trunc_msg那些)不同。所以DZF2载荷对DZH3直接有效,只差p0指纹里1个词(slide 0x1a0000),重生成一份即可。

0x01-2 CZB2:另一条线,偷不得懒

CZB2是6.1.128,构建号2755301,跟DZF2完全不是一根藤上的瓜:20个关键偏移0个相同,10万共有符号里仅0.44%地址一致。只能走完整流水线。实测在S9210ZCS5CZB2上exploit一遍过(attempt 1就triggered=1)——等于给整套流程做了次真机验收。

0x01-3 踩过最大的坑:改vermagic只对相邻build code安全

这月最痛的一课。KernelSU内嵌模块要匹配目标内核vermagic。图省事把S928U模块的vermagic字符串改成DZH3——结果一上真机,late-load后一解锁就卡死重启,毫无征兆。

  • 表象:加载KernelSU后系统极不稳定,解锁(唤醒显示/GPU的work)一碰就崩。
  • 根因:vermagic改串只骗过版本检查,但模块是按S928U内核(构建号33419968)编的,DZH3是3254743——跨内核,运行时的KDP/RKP钩子、cred处理、结构体假设全对不上。
  • 解法:跨内核一律用DDK(ghcr.io/ylarod/ddk-min)按真机内核正式重建,UTS_RELEASE设成目标release,约两分钟编一个,再跑check_symbol + audit_module_against_target.py(209导入/0缺失/0 CRC不匹配全过)。

经验:改串这招只对同一内核线、相邻build code安全(DZF2↔DZH3这种);跨内核版本,省下的时间会加倍赔进去。

0x01-4 真机踩坑记录

  • 坑A 解锁就卡死重启——加载了按错误内核构建的模块(见0x01-3)。解法:DDK按真机内核重建。已解决。
  • 坑B 要么一遍过要么一直失败——fops-slide竞态要多个写线程在精确窗口抢调度;app熄屏/后台把payload压进后台cgroup(Android限CPU)→写线程饿死→triggered=0。解法:跑的时候别让进程进后台cgroup(不熄屏/保持前台),命中率明显提升。
  • 坑C 源码ksud缺–ephemeral——ksud late-load: exit=2 unexpected argument ‘–ephemeral’ found。出厂ksud-selected是定制版自带该参数(加载模块后跳过ksud安装与/data/adb写入),源码重编的没有。解法:把–ephemeral加进cli.rs的LateLoad(ephemeral=true时跳过stage_daemon_from与finish_install)。已补,exit=0。
  • 坑D 管理器显示版本不对——拿3.3.0的ksud测,管理器还是显示v3.2.5(32525-2)。根因:管理器显示的是内核模块版本不是ksud版本;我的模块从v3.2.5源码编,永远报32525。解法:要显示32601,得把模块也从v3.3.0源码+三星补丁重编(需rebase补丁,独立活,还没做)。
  • 坑E CZB2同款坑又踩一遍——CZB2机器exploit一遍过,但late-load后同样卡死。解法:走DDK为CZB2正式重建(vermagic 6.1.128-android14-11-2755301-abS9280ZCS5CZB2),审计全过。

0x01 网站

这个月不全是root。把rms24的日志上报、统计、下载页整到了blog.nanoturtle.cn;地图瓦片换了国内合规底图(高德+可选天地图+无底图,坐标GCJ-02转换);导航、首页卡片重做(PDO直写wp_posts绕开KSES过滤)。顺手修了台本地机的fcitx5拼音——升级addons后ABI对不上(新版addons引用了旧核心没有的fcitx::RegexConstrain符号),换回官方核心5.1.22就好。真的有点难绷。

0x02 总结

一句话:

活都在工具的自动化里,真正难的还是内核侧的运行时假设。

这个月能让我如此忙碌的导火索还是因为我手贱升级OneUI8.5,唉,累了。

就这样。好久没见。

别检查我的网站了喵
本文采用 CC BY-NC-SA 4.0 国际许可协议进行许可。
转载请注明作者及原文链接,并遵循相同方式共享原则。
链接:https://blog.nanoturtle.cn/index.php/2026/09/19/what-i-did-this-month/
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇