[{"data":1,"prerenderedAt":1504},["ShallowReactive",2],{"/articles/self-engine-channel-package-native-patching":3},{"id":4,"title":5,"body":6,"date":1489,"description":1490,"extension":1491,"featured":1492,"meta":1493,"navigation":1339,"path":1494,"report":1492,"seo":1495,"stem":1496,"tags":1497,"tip":1502,"__hash__":1503},"articles/articles/self-engine-channel-package-native-patching.md","自研引擎渠道包 native 补丁全记录",{"type":7,"value":8,"toc":1457},"minimark",[9,18,23,120,136,140,151,173,180,185,198,208,212,267,271,274,309,327,338,342,348,355,399,406,410,416,419,423,429,432,480,484,487,493,497,508,511,554,557,561,619,623,672,676,680,691,698,729,770,774,785,788,797,800,803,807,810,820,835,838,842,857,860,864,867,886,893,897,910,916,946,956,959,963,973,1057,1061,1071,1081,1085,1158,1161,1175,1179,1185,1197,1206,1212,1216,1219,1236,1239,1255,1258,1285,1288,1300,1304,1419,1453],[10,11,12,13,17],"p",{},"测试环境：自有设备、自有账号、离线验证。全文隐去应用与厂商标识，函数偏移与哈希保留（技术内容，非身份信息）。姊妹篇是《Unity IL2CPP 移动端运行时改造技术报告》，两篇的差别本身就是信息：那次的地图是 ",[14,15,16],"code",{},"dump.cs","，这次没有地图。",[19,20,22],"h2",{"id":21},"_0-结论先行","0. 结论先行",[24,25,26,45],"table",{},[27,28,29],"thead",{},[30,31,32,36,39,42],"tr",{},[33,34,35],"th",{},"功能",[33,37,38],{},"手法",[33,40,41],{},"字节",[33,43,44],{},"调用面",[46,47,48,71,93],"tbody",{},[30,49,50,54,65,68],{},[51,52,53],"td",{},"经济货币自动拾取",[51,55,56,57,60,61,64],{},"4 条 ",[14,58,59],{},"bl"," 重定向到 ",[14,62,63],{},"mov w0,#1; ret"," 小 stub",[51,66,67],{},"4 + 2",[51,69,70],{},"31 个调用点共用 1 个 dword",[30,72,73,76,87,90],{},[51,74,75],{},"一次性清屏道具不限量",[51,77,78,79,82,83,86],{},"3 个 ",[14,80,81],{},"cmp w8,#1"," 改 ",[14,84,85],{},"cmp w8,w8","，强制走游戏自己的 unlimited 分支",[51,88,89],{},"3",[51,91,92],{},"4 / 1 / 1",[30,94,95,98,114,117],{},[51,96,97],{},"免广告",[51,99,100,101,82,104,107,108,82,111],{},"出口前 ",[14,102,103],{},"and w0,w9,w8",[14,105,106],{},"mov w0,#0","；每日配额 ",[14,109,110],{},"b.le",[14,112,113],{},"nop",[51,115,116],{},"2",[51,118,119],{},"广告 API 面约 570 个方法",[10,121,122,123,127,128,131,132,135],{},"最终基线 stage3 相对 stage2 恰好改 ",[124,125,126],"strong",{},"11 个指令字","，全部在 ",[14,129,130],{},".text"," 和一个 8 字节空洞里。三条共同点：",[124,133,134],{},"不新增状态","。",[19,137,139],{"id":138},"_1-目标确认与包体拆分","1. 目标确认与包体拆分",[10,141,142,143,146,147,150],{},"APK 约 2.3 GB，",[14,144,145],{},"lib/arm64-v8a/"," 下一个 97 MB 的 C++ 主库（下称 ",[14,148,149],{},"libMain.so","），另有广告、支付、统计 SDK。",[10,152,153,156,157,160,161,164,165,168,169,172],{},[124,154,155],{},"它不是 IL2CPP","：没有 ",[14,158,159],{},"global-metadata.dat","，没有 ",[14,162,163],{},"GameAssembly.dll","/",[14,166,167],{},"libil2cpp.so","，",[14,170,171],{},"assets/bin/Data"," 结构也不同。上一篇的 Il2CppDumper 流水线一步都用不上，工具退回 capstone + IDA + 自写的 AArch64 小解释器。",[10,174,175,176,179],{},"脚本层是 Lua，业务界面大量由 Lua 驱动，C++ 侧只提供带",[124,177,178],{},"反射注册表","的字段与函数。",[181,182,184],"h3",{"id":183},"_11-包体stored-是前提","1.1 包体：STORED 是前提",[10,186,187,168,190,193,194,197],{},[14,188,189],{},"extractNativeLibs=false",[14,191,192],{},".so"," 以 ",[124,195,196],{},"STORED","（未压缩）存在 zip 里，直接从 APK 内存映射执行。于是替换它不需要重压 2.3 GB：新库按原样写回同一个 zip 条目、重写中央目录与 EOCD 即可。",[10,199,200,203,204,207],{},[124,201,202],{},"硬约束：新库字节长度必须与原库完全一致","，否则后面所有条目的偏移整体平移，安装失败或加载到错数据。为此留两个专职校验脚本：一个比对包内解压长度与磁盘长度，一个检查 zip 结构自洽。签名走 ",[14,205,206],{},"apksigner","，v1 到 v4 全开。",[181,209,211],{"id":210},"_12-elf-三个坑","1.2 ELF 三个坑",[213,214,215,234,248],"ul",{},[216,217,218,221,222,225,226,229,230,233],"li",{},[14,219,220],{},"ELF64_R_SYM"," 在 ",[14,223,224],{},"r_info"," 的",[124,227,228],{},"高 32 位","。搞反会误判 ",[14,231,232],{},"dlopen"," 没被导入，从而去做一次本可避免的 Java 层改动。",[216,235,236,239,240,243,244,247],{},[14,237,238],{},"Elf64_Phdr"," 的顺序是 ",[14,241,242],{},"offset, vaddr, paddr, filesz, memsz","。漏掉 ",[14,245,246],{},"paddr"," 会得出「某段大小为 0」。",[216,249,250,251,254,255,258,259,262,263,266],{},"PLT 地址必须由桩自己的 ",[14,252,253],{},"adrp+ldr"," 反查 GOT 再对 ",[14,256,257],{},".rela.plt"," 得到。公式化推导会差 32 字节，从而把 ",[14,260,261],{},"__android_log_print"," 认成 ",[14,264,265],{},"pthread_create","，进而冤枉前面所有探针。",[19,268,270],{"id":269},"_2-反射注册表就是这次的-dumpcs","2. 反射注册表就是这次的 dump.cs",[10,272,273],{},"注册每个成员时都走同一段形状：",[275,276,281],"pre",{"className":277,"code":278,"language":279,"meta":280,"style":280},"language-asm shiki shiki-themes github-light github-dark","adrp x9, \u003Chi> / add x9, x9, \u003Clo>   ; 类型名字符串\nldr  x8, [x8, #0x58]\nmov  w3, #\u003Coffset>                 ; 就是成员的字节偏移\nblr  x8\n","asm","",[14,282,283,291,297,303],{"__ignoreMap":280},[284,285,288],"span",{"class":286,"line":287},"line",1,[284,289,290],{},"adrp x9, \u003Chi> / add x9, x9, \u003Clo>   ; 类型名字符串\n",[284,292,294],{"class":286,"line":293},2,[284,295,296],{},"ldr  x8, [x8, #0x58]\n",[284,298,300],{"class":286,"line":299},3,[284,301,302],{},"mov  w3, #\u003Coffset>                 ; 就是成员的字节偏移\n",[284,304,306],{"class":286,"line":305},4,[284,307,308],{},"blr  x8\n",[10,310,311,312,315,316,319,320,323,324,135],{},"把「地址落在字符串区的 ",[14,313,314],{},"adrp/add","」和紧随其后的 ",[14,317,318],{},"mov w3,#imm"," 配对，一次静态扫描就把主存档对象（下称 ",[14,321,322],{},"StoreObj","）的整张字段表吐出来：",[124,325,326],{},"395 行，无一猜测",[10,328,329,330,333,334,337],{},"这张表当场纠正了一次已经花掉装机的错误：之前有个补丁打的是 ",[14,331,332],{},"StoreObj+0x1a0","，界面毫无反应，表里写着 ",[14,335,336],{},"+0x1a0"," 是收藏图鉴，不是道具数量表。",[19,339,341],{"id":340},"_3-签名校验与暗桩stage1","3. 签名校验与暗桩（stage1）",[10,343,344,345,347],{},"流程：原包解出、只换 ",[14,346,149],{},"、用自建 keystore 重签名、装机看能不能跑。",[10,349,350,351,354],{},"结论：",[124,352,353],{},"没有启动期签名校验","，重签名可以直接安装并正常进游戏。但这一步顺手挖出两个更要命的东西：",[24,356,357,370],{},[27,358,359],{},[30,360,361,364,367],{},[33,362,363],{},"暗桩",[33,365,366],{},"形态",[33,368,369],{},"后果",[46,371,372,383],{},[30,373,374,377,380],{},[51,375,376],{},"签名校验虚函数",[51,378,379],{},"返回常量 0 就放过",[51,381,382],{},"不处理也能跑，但留着是隐患",[30,384,385,390,396],{},[51,386,387],{},[124,388,389],{},"退出总闸",[51,391,392,393],{},"一个 JNI 调用通向 ",[14,394,395],{},"exit",[51,397,398],{},"运行几十秒后随机退出",[10,400,401,402,405],{},"两处共 4 个指令字，构成 stage1。次序因此固定：",[124,403,404],{},"先确认签名这一关要不要处理，再确认二进制里有没有会主动杀死进程或上报的暗桩","，因为后面所有失败归因都依赖这个前提。",[181,407,409],{"id":408},"_31-控制组纪律本次最贵的一条","3.1 控制组纪律（本次最贵的一条）",[10,411,412,415],{},[124,413,414],{},"控制组不许用厂商原版库。"," 原版库带着那两个暗桩，拿它当「未改动基线」去排查崩溃，必然复现一次启动即死，然后得出「原始内容也崩」的假结论。",[10,417,418],{},"控制组的正确形态是 stage1（只做最小改动那一版）配未改动的 dex，签名仍然是自己的。判据是库的 sha256 前缀。",[19,420,422],{"id":421},"_4-离线断言体系把一次装机换成三百次离线断言","4. 离线断言体系：把一次装机换成三百次离线断言",[10,424,425,426,135],{},"真实成本：一次交付要 push 2.3 GB（USB 约 40 MB/s，65 秒）加安装，装机到看到界面 6 到 10 分钟，手机还是唯一的真相来源。这个单价决定策略：",[124,427,428],{},"凡是能在离线证伪的东西，一律不留给装机",[10,430,431],{},"每个补丁都是三件套：",[24,433,434,444],{},[27,435,436],{},[30,437,438,441],{},[33,439,440],{},"件",[33,442,443],{},"职责",[46,445,446,456,470],{},[30,447,448,453],{},[51,449,450],{},[14,451,452],{},"patch_*.py",[51,454,455],{},"从基线字节出发逐字改，改完立刻断言「这条指令原来是我想改的那条」",[30,457,458,463],{},[51,459,460],{},[14,461,462],{},"verify_*.py",[51,464,465,466,469],{},"静态检查 + ",[124,467,468],{},"真跑","（自己的 AArch64 解释器执行被改函数，做收敛比对）+ 变异测试",[30,471,472,477],{},[51,473,474],{},[14,475,476],{},"rebuild_*.sh",[51,478,479],{},"dex 到 lib 到重打包到签名到包内字节审计，一条命令跑完，中间任何一步失败就停",[181,481,483],{"id":482},"_41-变异测试","4.1 变异测试",[10,485,486],{},"每改一处就派生一个「故意改错」的镜像，要求校验器必须点名抓住。错一位、位移一格、两个字对调、守卫从 64 位退成 32 位、答完不弹栈、把共享谓词整体强答，全都要被抓住。",[10,488,489,490,135],{},"最后一个交付版上，11 个字的归属审计配 13 个变异，逐个命名，一个不漏。它防的是同一种失败：",[124,491,492],{},"校验器全绿，但它检查的东西根本不存在",[181,494,496],{"id":495},"_42-函数级收敛比对","4.2 函数级收敛比对",[10,498,499,500,503,504,507],{},"改答案型补丁的判据：把 hook 点的函数",[124,501,502],{},"整个跑完","（从入口到 ",[14,505,506],{},"ret","），基线版和改过版的返回值、寄存器、栈窗口逐项比对，允许有差异的地方必须写清楚是哪个用例、差在哪。",[10,509,510],{},"这条要求顺手暴露了工具自身的四个洞：",[213,512,513,527,533,548],{},[216,514,515,516,519,520,523,524,135],{},"capstone 会把 ",[14,517,518],{},"MOV Xd,Xm"," 渲染成 ",[14,521,522],{},"orr xd,xm,x0","，小解释器没有 ",[14,525,526],{},"orr",[216,528,529,532],{},[14,530,531],{},"add x8,x0,#1, lsl #12"," 按逗号切分会算成加 1。",[216,534,535,164,538,164,541,164,544,547],{},[14,536,537],{},"cbz",[14,539,540],{},"tbz",[14,542,543],{},"asr",[14,545,546],{},"and"," 不支持。",[216,549,550,553],{},[14,551,552],{},"uxtw #3"," 这类寄存器扩展被静默忽略。",[10,555,556],{},"全都是「能反汇编、能装机、结果不对」那一类。",[181,558,560],{"id":559},"_43-工具行为坑","4.3 工具行为坑",[213,562,563,570,588,606,612],{},[216,564,565,566,569],{},"capstone 对不可解码字会",[124,567,568],{},"静默返空","，循环里没断言就等于悄悄漏扫。逐字调用它反汇编 8600 万条要走六分钟，整段丢给一次调用可以。",[216,571,572,575,576,579,580,583,584,587],{},[14,573,574],{},"SystemExit"," 不是 ",[14,577,578],{},"Exception","，变异测试用 ",[14,581,582],{},"except Exception"," 会直接中断整轮，而屏幕上那行 ",[14,585,586],{},"ok"," 看起来一切正常。",[216,589,590,591,594,595,598,599,602,603],{},"只要经过去掉退出码的外壳程序就会骗人：把输出 ",[14,592,593],{},"tail"," 或 ",[14,596,597],{},"sed"," 一截，",[14,600,601],{},"SyntaxError"," 会被藏起来，报出来的退出码是管道下游的。",[124,604,605],{},"判据只用未过滤的运行结果。",[216,607,608,609,135],{},"全量 dump 一千五百行会把 logcat 默认 256 KB 环形缓冲挤掉，症状是「什么都没抓到」，要先 ",[14,610,611],{},"logcat -G 16M",[216,613,614,615,618],{},"Windows 下 adb 的远端路径会被 Git Bash 改写，不设 ",[14,616,617],{},"MSYS_NO_PATHCONV=1"," 时 push 会失败而退出码照旧是 0，机器上留的还是旧文件，测出来的一切都不作数。",[181,620,622],{"id":621},"_44-编码坑","4.4 编码坑",[213,624,625,636,639,661],{},[216,626,627,628,631,632,635],{},"LDR/STR 的 imm7 在 ",[124,629,630],{},"16:10","，LDP/STP 的 imm7 在 ",[124,633,634],{},"21:15","。写成一套掩码扫全库，会得到十五万条假命中，同时漏掉真正要找的那个访问器。",[216,637,638],{},"STP 的立即数既 scaled 又换位的组合最容易写错，错出来的字仍然合法，只是两条保存悄悄落在同一个槽位上。唯一能抓住它的是真的执行一遍。",[216,640,641,644,645,648,649,652,653,656,657,660],{},[14,642,643],{},"MOV Xd, Xm"," 要按 ",[14,646,647],{},"ADD Xd, Xn, #0"," 发；",[14,650,651],{},"ADD"," 的 ",[14,654,655],{},"#1, lsl #12"," 形态是另一个操作数，跟普通 imm12 分开，两条 add 相加才能得到 ",[14,658,659],{},"+0x12e0"," 这种超范围偏移。",[216,662,663,664,667,668,671],{},"字段偏移超过 1016 时 64 位 LDR 无法折叠，超过 4095 时 32 位也无法折叠，编译器会先 ",[14,665,666],{},"add"," 出偏移再访问，此时偏移寄存器是",[124,669,670],{},"基址","不是索引。按「索引寄存器」去匹配会一个都不中。",[19,673,675],{"id":674},"_5-交付三个功能","5. 交付：三个功能",[181,677,679],{"id":678},"_51-经济货币自动拾取","5.1 经济货币自动拾取",[10,681,682,683,686,687,690],{},"原始形态是一个",[124,684,685],{},"共享位图谓词","（按 mask 取位）：全库 31 个调用点共用 ",[14,688,689],{},"player+0x188C"," 这一个 dword 的 4 个位，写它的只有一个来自 Lua 的接口，本账号读出来永远是 0。",[10,692,693,694,697],{},"直觉做法是置位，但那等于给所有共用这几个位的逻辑发特权，属于典型的",[124,695,696],{},"共享总线陷阱","。真做法是逐点重定向：只把「掉落物被收集」这 4 条调用换向，位图一个字节都不动，其他 27 个调用点的行为完全不变。",[275,699,701],{"className":277,"code":700,"language":279,"meta":280,"style":280},"; 4 个调用点统一改成跳到一个 2 指令 stub\nbl   collect_stub      ; 原 bl 判定函数\n; stub:\nmov  w0, #1\nret\n",[14,702,703,708,713,718,723],{"__ignoreMap":280},[284,704,705],{"class":286,"line":287},[284,706,707],{},"; 4 个调用点统一改成跳到一个 2 指令 stub\n",[284,709,710],{"class":286,"line":293},[284,711,712],{},"bl   collect_stub      ; 原 bl 判定函数\n",[284,714,715],{"class":286,"line":299},[284,716,717],{},"; stub:\n",[284,719,720],{"class":286,"line":305},[284,721,722],{},"mov  w0, #1\n",[284,724,726],{"class":286,"line":725},5,[284,727,728],{},"ret\n",[10,730,731,734,735,738,739,742,743,745,746,748,749,752,753,756,757,759,760,763,764,766,767,769],{},[124,732,733],{},"hook 的两个坑","：中途 hook 必须假设标志寄存器不可用，先确认下一条指令是不是重写 flags 的（本项目那两处，一处下一条就是 ",[14,736,737],{},"subs","，另一处是 ",[14,740,741],{},"mov"," 加 ",[14,744,59],{},"）；",[14,747,59],{}," 之后 caller-saved 全废，被 hook 函数的入参（当时就在 ",[14,750,751],{},"w0"," 里）要从自己帧里 reload。函数内部的调用者 LR 不在 ",[14,754,755],{},"x30","（前面的 ",[14,758,59],{}," 已经改写），只能读被 hook 函数 prologue 落盘在 ",[14,761,762],{},"[sp, #8]"," 的值，用 ",[14,765,755],{}," 打日志会指向一些根本不含 ",[14,768,59],{}," 的地址。",[181,771,773],{"id":772},"_52-一次性清屏道具不限量","5.2 一次性清屏道具不限量",[10,775,776,777,780,781,784],{},"字节最少的一项。翻它的时候先假设要造一个「免费」概念，读下去发现游戏",[124,778,779],{},"本来就有","无限概念：道具包里一个标志字节配三个计数访问器，三个访问器是同样的 5 个字、只差读哪个计数，月卡持有者看到的 ",[14,782,783],{},"可用=1、花费=0"," 就是走这个分支。",[10,786,787],{},"于是补丁退化成让比较恒真：",[275,789,791],{"className":277,"code":790,"language":279,"meta":280,"style":280},"cmp  w8, #1      ->   cmp  w8, w8     ; 3 处\n",[14,792,793],{"__ignoreMap":280},[284,794,795],{"class":286,"line":287},[284,796,790],{},[10,798,799],{},"调用面 4/1/1，全在道具自己的 HUD 和消耗路径里。不写存储、不需要挑时机、不可能被服务端按回。",[10,801,802],{},"被排除的两条路：选包的谓词有 71 个调用者（通用的取管理器函数），功能开关谓词有 38 个调用者并且曾经把选关页冻住。",[181,804,806],{"id":805},"_53-免广告","5.3 免广告",[10,808,809],{},"先做归属判定，因为「永久免广告是本地特权还是服务端下发」决定要不要打网络。",[10,811,812,813,164,816,819],{},"答案是本地：整个广告 API 面（约 570 个方法）没有任何一个 ",[14,814,815],{},"S2C_*",[14,817,818],{},"C2S_*"," 名字；网络层只在启动时下发配额数字和一个时钟偏移；两处广告唤醒入口都在 Java 层且自带日志。",[275,821,823],{"className":277,"code":822,"language":279,"meta":280,"style":280},"and  w0, w9, w8     ->   mov  w0, #0     ; 广告开关判定函数出口前\nb.le ...            ->   nop             ; 每日配额那条\n",[14,824,825,830],{"__ignoreMap":280},[284,826,827],{"class":286,"line":287},[284,828,829],{},"and  w0, w9, w8     ->   mov  w0, #0     ; 广告开关判定函数出口前\n",[284,831,832],{"class":286,"line":293},[284,833,834],{},"b.le ...            ->   nop             ; 每日配额那条\n",[10,836,837],{},"改完 2 个字之后顺手去掉每日 20 次限制，用的是同一个函数的另一条分支。",[181,839,841],{"id":840},"_54-归属判定的反面钻石是云端权威","5.4 归属判定的反面：钻石是云端权威",[10,843,844,845,848,849,852,853,856],{},"同一套归属判定在别处给出了相反答案：钻石与内购是",[124,846,847],{},"云端权威","。改完本地字段看到数字变了，再走一次同步就回落到服务端算的值；请求里的 ",[14,850,851],{},"r"," 字段常态是 0，",[14,854,855],{},"e"," 字段加密，客户端不校验响应真伪（那是显示层的信任）。",[10,858,859],{},"这条否证把「刷钻石」这类需求从待办里永久划掉，也顺手解释了另一件事：0 元礼包的界面卡死跟签名改造无关，因为那是服务端在真付款失败时返回的正常状态。",[19,861,863],{"id":862},"_6-id-只存在于运行时的时候怎么办","6. id 只存在于运行时的时候怎么办",[10,865,866],{},"皮肤（装扮）和挂件两项卡在最基础的一步：配置里的 id 全在包里，静态搜不到。三条证据：",[868,869,870,873,879],"ol",{},[216,871,872],{},"注册表的构造函数只写虚表和空串，一条记录都不插。",[216,874,875,878],{},[14,876,877],{},"111029"," 这类数字在整张镜像的立即数里 0 命中。",[216,880,881,882,885],{},"那九个 map 对象本身躺在 ",[14,883,884],{},".data/.bss","，文件里全零。",[10,887,888,889,892],{},"id 是登录后从配置灌进 ",[14,890,891],{},"map\u003Cint, {string name, ...}>"," 的。",[181,894,896],{"id":895},"_61-空间不够用-dlopen","6.1 空间不够，用 dlopen",[10,898,899,900,903,904,907,908,135],{},"手写汇编行不通：整个可执行段里长度不小于 1 KB 的零空洞",[124,901,902],{},"一个都没有","，唯一的洞是 ",[14,905,906],{},".rodata"," 里 1028 字节，扣掉前面几个 stub 只剩三百多，够放指令，不够放「遍历九棵红黑树并解 libc++ 字符串」。而 lib 真的导入了 ",[14,909,232],{},[10,911,912,913,915],{},"做法：那三百字节里放一个一次性加载桩，逻辑放进一个用 NDK 编译的 15 KB 独立 ",[14,914,192],{},"，APK 里多塞一个 zip 条目，dex 与 Java 一行不动。",[275,917,919],{"className":277,"code":918,"language":279,"meta":280,"style":280},"; 一次性加载桩（.rodata 空洞内）\nadr  x0, \u003Cso 路径>\nmov  w1, #2            ; RTLD_NOW\nbl   dlopen\n; .bss 里一个一次性闸门，只跑一次\n",[14,920,921,926,931,936,941],{"__ignoreMap":280},[284,922,923],{"class":286,"line":287},[284,924,925],{},"; 一次性加载桩（.rodata 空洞内）\n",[284,927,928],{"class":286,"line":293},[284,929,930],{},"adr  x0, \u003Cso 路径>\n",[284,932,933],{"class":286,"line":299},[284,934,935],{},"mov  w1, #2            ; RTLD_NOW\n",[284,937,938],{"class":286,"line":305},[284,939,940],{},"bl   dlopen\n",[284,942,943],{"class":286,"line":725},[284,944,945],{},"; .bss 里一个一次性闸门，只跑一次\n",[10,947,948,949,952,953,955],{},"一次性计数器要放 ",[14,950,951],{},".bss","，不能放 ",[14,954,906],{}," 那个洞里，那是 R+X 段。",[10,957,958],{},"这个决定把后面所有问题都变成了免费的。",[181,960,962],{"id":961},"_62-枚举结果","6.2 枚举结果",[10,964,965,966,164,969,972],{},"枚举器把九张注册表按引擎自己的 ",[14,967,968],{},"contains",[14,970,971],{},"id→name"," 两个函数扫一遍，一次跑出一千五百行日志：",[24,974,975,985],{},[27,976,977],{},[30,978,979,982],{},[33,980,981],{},"项",[33,983,984],{},"结果",[46,986,987,1022,1045],{},[30,988,989,992],{},[51,990,991],{},"挂件",[51,993,994,995,998,999,742,1002,1005,1006,1009,1010,1013,1014,1017,1018,1021],{},"注册表",[124,996,997],{},"正好 62 个 id","：",[14,1000,1001],{},"21001..21030",[14,1003,1004],{},"21050..21081","，碎片表是 ",[14,1007,1008],{},"+1000"," 同名。玩家存档里的碎片表按",[124,1011,1012],{},"名字","索引（",[14,1015,1016],{},"{string@0, u32 count@24}","，32 字节一条），62 个里只有 8 个计数非零，唯一那条装备记录的名字指向其中一个特定挂件。原生侧只存在一个数量入口（Lua 导出的 ",[14,1019,1020],{},"GetXxxPieceCount","），它查不到就返回 0。",[30,1023,1024,1027],{},[51,1025,1026],{},"皮肤",[51,1028,1029,1032,1033,1036,1037,1040,1041,1044],{},[14,1030,1031],{},"avatar"," 表 186 条，",[14,1034,1035],{},"plant"," 表 375 条，而 186 条",[124,1038,1039],{},"逐条","满足 ",[14,1042,1043],{},"avatarId = plantId + 200"," 且名字一致，无孤儿无缺口。所以「默认穿上第一套皮肤」这条规则不是猜的。",[30,1046,1047,1050],{},[51,1048,1049],{},"顺带否证",[51,1051,1052,1053,1056],{},"另一套「新皮肤」注册表即使把范围拉到两百万也是 0 条，本渠道根本没下发这套配置。之前照着美术资源名推断出来的 ",[14,1054,1055],{},"_new_avatar_1..4"," 那条思路可以直接埋了。",[181,1058,1060],{"id":1059},"_63-两次免费纠错","6.3 两次免费纠错",[10,1062,1063,1066,1067,1070],{},[124,1064,1065],{},"否定结论","：把装备查询函数强答成「已穿皮肤」，装机后界面毫无变化。翻它的调用点才知道 43 个调用者一律把这个返回值当",[124,1068,1069],{},"条件","使用（跟某个具体皮肤 id 比较，或者在配置结构的两个浮点字段之间二选一），从来没人拿它当美术索引。真正的门在上一层，是「这个账号有没有解锁这件皮肤」，而账号 186 件里只解锁了 2 件。推导全对，下嘴点错了。",[10,1072,1073,1076,1077,1080],{},[124,1074,1075],{},"守卫过严","：枚举器的只读守卫里写的是「指针必须 8 字节对齐」，而按 4 字节步进打印原始 dword 的那条路径正好踩在这条规则上，于是四张表全部在第二行自我判定为不可读。",[124,1078,1079],{},"守卫不许严于它的读者","，这是字节级的事，不是感觉级的事。",[19,1082,1084],{"id":1083},"_7-数据源原则","7. 数据源原则",[24,1086,1087,1102],{},[27,1088,1089],{},[30,1090,1091,1093,1096,1099],{},[33,1092,38],{},[33,1094,1095],{},"游戏持有的状态",[33,1097,1098],{},"重启后",[33,1100,1101],{},"说明",[46,1103,1104,1118,1131,1145],{},[30,1105,1106,1109,1112,1115],{},[51,1107,1108],{},"重定向单点（5.1）",[51,1110,1111],{},"无（不改共享位图）",[51,1113,1114],{},"保持",[51,1116,1117],{},"31 个调用点只动 4 个",[30,1119,1120,1123,1126,1128],{},[51,1121,1122],{},"让比较恒真（5.2）",[51,1124,1125],{},"无（不写存储）",[51,1127,1114],{},[51,1129,1130],{},"游戏本来就有这个分支",[30,1132,1133,1136,1139,1142],{},[51,1134,1135],{},"改本地字段",[51,1137,1138],{},"旧值",[51,1140,1141],{},"失效（服务器重下发）",[51,1143,1144],{},"钻石那条一次否证省下的时间比三条交付都多",[30,1146,1147,1150,1153,1155],{},[51,1148,1149],{},"改协议/配置",[51,1151,1152],{},"无",[51,1154,1114],{},[51,1156,1157],{},"每次解码都改",[10,1159,1160],{},"两条工程结论：",[868,1162,1163,1169],{},[216,1164,1165,1168],{},[124,1166,1167],{},"要跨重启保持，就必须写协议层或配置层","，因为服务器每次下发的数据都会在解码出口被改写。",[216,1170,1171,1174],{},[124,1172,1173],{},"共享谓词只能逐点处理","。调用者数量本身就是危险度量：31 个调用者共用的位图、71 个调用者共用的取管理器，一律用重定向单点解决。",[19,1176,1178],{"id":1177},"_8-防御视角","8. 防御视角",[10,1180,1181,1184],{},[124,1182,1183],{},"服务端权威是唯一真边界。"," 上述改造都只影响本地显示与本地计算。任何服务端会重算的数值，客户端改完只是「看起来变了」。这直接解释了「为什么有些改造重启后失效、有些能持久」。",[10,1186,1187,1190,1191,1193,1194,1196],{},[124,1188,1189],{},"反射注册表是双刃剑。"," 它让 Lua 侧能动态访问任意字段，代价是给逆向者留下了一张完整的字段地图。注册形状固定（",[14,1192,314],{}," + ",[14,1195,318],{},"）的引擎，一次静态扫描就能拿到全表。",[10,1198,1199,1202,1203,1205],{},[124,1200,1201],{},"STORED 打包降低了替换门槛。"," 未压缩的 ",[14,1204,192],{}," 让「只换一个文件、不重压整包」成为可能，也让字节级审计变得可行。",[10,1207,1208,1211],{},[124,1209,1210],{},"离线断言体系是成本杠杆。"," 装机 6 到 10 分钟一次，离线断言毫秒级。变异测试把「校验器全绿但它检查的东西不存在」这类失败提前暴露。",[19,1213,1215],{"id":1214},"_9-判据清单","9. 判据清单",[10,1217,1218],{},"定位阶段：",[868,1220,1221,1227,1230,1233],{},[216,1222,1223,1224,1226],{},"先确认是不是 IL2CPP（有没有 ",[14,1225,159],{},"），不是的话工具链完全不同",[216,1228,1229],{},"反射注册表的注册形状是固定的，一次静态扫描换一张字段全表",[216,1231,1232],{},"下否定结论之前先验前置条件。有一次把某个页面空白归因于「服务端只下发进度」，实际是测试角色根本没推进到那一步",[216,1234,1235],{},"排除法里只能用观察到的事实。某个共享谓词函数有 43 到 71 个调用者，这种数字意味着「不能整体强答」，不意味着「这条功能不走它」",[10,1237,1238],{},"写入阶段：",[868,1240,1241,1244,1247,1250],{"start":725},[216,1242,1243],{},"优先找游戏自己已经实现的状态。清屏道具的 3 个字比任何「新造的免费标志」都稳",[216,1245,1246],{},"共享谓词逐点重定向，不整体置位",[216,1248,1249],{},"中途 hook 假设标志寄存器不可用，LR 从 prologue 落盘处读",[216,1251,1252,1253],{},"一次性计数器放 ",[14,1254,951],{},[10,1256,1257],{},"工具阶段：",[868,1259,1261,1269,1275,1280],{"start":1260},9,[216,1262,1263,1264,164,1266,1268],{},"判据只用未过滤的运行结果（",[14,1265,593],{},[14,1267,597],{}," 会藏错误）",[216,1270,1271,575,1273],{},[14,1272,574],{},[14,1274,578],{},[216,1276,1277,1278],{},"全量 dump 前先 ",[14,1279,611],{},[216,1281,1282,1283],{},"Windows 下 adb 设 ",[14,1284,617],{},[10,1286,1287],{},"交付阶段：",[868,1289,1291,1297],{"start":1290},13,[216,1292,1293,1294,1296],{},"交付链要能回滚：每一级 stage 都有独立目录、README、固定 sha256 与再生脚本，",[14,1295,192],{}," 从不手改，只由脚本产出",[216,1298,1299],{},"控制组不许用厂商原版库（带暗桩）",[19,1301,1303],{"id":1302},"附日常命令","附：日常命令",[275,1305,1309],{"className":1306,"code":1307,"language":1308,"meta":280,"style":280},"language-bash shiki shiki-themes github-light github-dark","cd \u003Cproject>/resign\n\npython patch_lib_stage3.py && python verify_stage3.py   # 只重建主库并验证 11 个字与 13 个变异\nbash rebuild_stage3.sh                                  # dex -> lib -> 重打包 -> 签名 -> 包内字节审计\nbash rebuild_stage3.sh install                          # 再 push 约 2.3 GB 并安装\n\nbash archive/dumper/rebuild_costume.sh again \"\u003Ccmd>\"    # 让已装的枚举器再跑一次（push 一个命令文件，不装机）\nbash archive/dumper/rebuild_costume.sh log             # 抓枚举日志\n","bash",[14,1310,1311,1335,1341,1362,1372,1384,1389,1406],{"__ignoreMap":280},[284,1312,1313,1317,1321,1325,1329,1332],{"class":286,"line":287},[284,1314,1316],{"class":1315},"sj4cs","cd",[284,1318,1320],{"class":1319},"szBVR"," \u003C",[284,1322,1324],{"class":1323},"sZZnC","projec",[284,1326,1328],{"class":1327},"sVt8B","t",[284,1330,1331],{"class":1319},">",[284,1333,1334],{"class":1323},"/resign\n",[284,1336,1337],{"class":286,"line":293},[284,1338,1340],{"emptyLinePlaceholder":1339},true,"\n",[284,1342,1343,1347,1350,1353,1355,1358],{"class":286,"line":299},[284,1344,1346],{"class":1345},"sScJk","python",[284,1348,1349],{"class":1323}," patch_lib_stage3.py",[284,1351,1352],{"class":1327}," && ",[284,1354,1346],{"class":1345},[284,1356,1357],{"class":1323}," verify_stage3.py",[284,1359,1361],{"class":1360},"sJ8bj","   # 只重建主库并验证 11 个字与 13 个变异\n",[284,1363,1364,1366,1369],{"class":286,"line":305},[284,1365,1308],{"class":1345},[284,1367,1368],{"class":1323}," rebuild_stage3.sh",[284,1370,1371],{"class":1360},"                                  # dex -> lib -> 重打包 -> 签名 -> 包内字节审计\n",[284,1373,1374,1376,1378,1381],{"class":286,"line":725},[284,1375,1308],{"class":1345},[284,1377,1368],{"class":1323},[284,1379,1380],{"class":1323}," install",[284,1382,1383],{"class":1360},"                          # 再 push 约 2.3 GB 并安装\n",[284,1385,1387],{"class":286,"line":1386},6,[284,1388,1340],{"emptyLinePlaceholder":1339},[284,1390,1392,1394,1397,1400,1403],{"class":286,"line":1391},7,[284,1393,1308],{"class":1345},[284,1395,1396],{"class":1323}," archive/dumper/rebuild_costume.sh",[284,1398,1399],{"class":1323}," again",[284,1401,1402],{"class":1323}," \"\u003Ccmd>\"",[284,1404,1405],{"class":1360},"    # 让已装的枚举器再跑一次（push 一个命令文件，不装机）\n",[284,1407,1409,1411,1413,1416],{"class":286,"line":1408},8,[284,1410,1308],{"class":1345},[284,1412,1396],{"class":1323},[284,1414,1415],{"class":1323}," log",[284,1417,1418],{"class":1360},"             # 抓枚举日志\n",[10,1420,1421,1422,1425,1426,1429,1430,1433,1434,1436,1437,1440,1441,1444,1445,1448,1449,1452],{},"关键标识（都可在构建里复现）：stage1 ",[14,1423,1424],{},"66ac44fecb18959f","、stage2 ",[14,1427,1428],{},"e50ba0440a4b07ac","、stage3 ",[14,1431,1432],{},"f8218d6e90312022","；主库 97,236,880 字节，",[14,1435,130],{}," 覆盖 ",[14,1438,1439],{},"0x200000"," 到 ",[14,1442,1443],{},"0x5600000","，唯一的可执行空洞在 ",[14,1446,1447],{},"0x17a6864","（1028 字节）；厂商原版库哈希 ",[14,1450,1451],{},"7a99017132c1b78b","（含两处暗桩，不可用作控制组）。",[1454,1455,1456],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":280,"searchDepth":293,"depth":293,"links":1458},[1459,1460,1464,1465,1468,1474,1480,1485,1486,1487,1488],{"id":21,"depth":293,"text":22},{"id":138,"depth":293,"text":139,"children":1461},[1462,1463],{"id":183,"depth":299,"text":184},{"id":210,"depth":299,"text":211},{"id":269,"depth":293,"text":270},{"id":340,"depth":293,"text":341,"children":1466},[1467],{"id":408,"depth":299,"text":409},{"id":421,"depth":293,"text":422,"children":1469},[1470,1471,1472,1473],{"id":482,"depth":299,"text":483},{"id":495,"depth":299,"text":496},{"id":559,"depth":299,"text":560},{"id":621,"depth":299,"text":622},{"id":674,"depth":293,"text":675,"children":1475},[1476,1477,1478,1479],{"id":678,"depth":299,"text":679},{"id":772,"depth":299,"text":773},{"id":805,"depth":299,"text":806},{"id":840,"depth":299,"text":841},{"id":862,"depth":293,"text":863,"children":1481},[1482,1483,1484],{"id":895,"depth":299,"text":896},{"id":961,"depth":299,"text":962},{"id":1059,"depth":299,"text":1060},{"id":1083,"depth":293,"text":1084},{"id":1177,"depth":293,"text":1178},{"id":1214,"depth":293,"text":1215},{"id":1302,"depth":293,"text":1303},"2026-09-27","自研 C++ 引擎 + Lua 脚本 + 反射注册表的塔防渠道包，全程只改一个 .so 里的 11 个指令字：反射注册表扫描、离线断言体系、三个功能交付与运行时 id 枚举。","md",false,{},"/articles/self-engine-channel-package-native-patching",{"title":5,"description":1490},"articles/self-engine-channel-package-native-patching",[1498,1499,1500,1501],"安全","逆向工程","Android","Native","由 LongCat-2.5-Preview with Deepseek-V4.1-Flash 生成。","bAotO-V1RIr-HPy165dd0gizJtrBrqxmTzZKZfP-Blk",1790495474710]