当前位置:首页>iOSAPP>iOS 内存修改工具箱:扫描、定位、修改

iOS 内存修改工具箱:扫描、定位、修改

  • 2026-08-25 05:28:51
iOS 内存修改工具箱:扫描、定位、修改

从 g_gold = 1250 出发,点击一个按钮,用 mt_scan_u32 扫描内存找到地址,mach_vm_write 写入 8888,同一行代码都没动,UI 上 1250 变成了 8888。完整源码见文末获取方式。


1. 什么是内存修改,能解决什么问题

内存修改,就是绕过正常代码逻辑,直接读写进程的虚拟内存。
App 里的任何变量最终都存在于进程地址空间的某个地址上。正常改 g_gold = 8888 要经过编译器生成的 store 指令。但如果你能直接往 g_gold 所在的内存地址写 8888——什么 setter、KVC、属性观察者、所有上层逻辑全部跳过。你直接改了物理事实。
它能做什么:
场景
做什么
游戏修改
定位金币、血量、等级的内存地址,直接改成目标值
越狱检测绕过
把 check_jailbreak() 的入口指令 patch 成 return 0
安全研究
观察某段内存区域在运行时如何变化
逆向分析
修改函数参数、返回值来观察程序行为变化

内存修改 vs Hook vs 注入,对抗检测的能力差别

方式
做了什么
留下什么痕迹
对抗检测能力
GOT Hook(fishhook)
改 __la_symbol_ptr 函数指针
指针指向非预期 image,dladdr 立即可查
最低
Inline Hook(Dobby)
改写函数入口指令为跳板
__TEXT
 页哈希变化 + rwx 权限标记
中
动态库注入
往进程塞 Mach-O
dyld_all_image_infos
 多一条记录
中
内存修改
mach_vm_write
 直接写值
什么都不加,只改已有数据
最高
GOT Hook 改指针,dladdr 一查就暴露。Inline Hook 改指令,代码段哈希会变。注入多一个 image,dyld 有记录。内存修改不进 dyld、不改符号表、不改指令——你只改了数据段里一个值,或者临时改了代码段一个字节。安全 SDK 可以扫描 rwx 页面来发现代码段权限异常,但对数据段的修改几乎无迹可寻——除非服务端发现了行为异常(金币从 1250 突然变成 999999),客户端拿不出直接证据。
内存修改的前提:同进程操作用 mach_task_self() 即可,跨进程操作需要越狱或 task_for_pid-allow。

2. Demo:一个按钮,让 1250 变成 8888

2.1 场景

ViewController 里有一个全局变量 g_gold,初始值 1250。界面上一个 Label 显示这个值,一个按钮。点击按钮 → 扫描内存找 1250 → 确认地址 → 写入 8888 → Label 刷新。

2.2 完整代码

// 全局变量static uint32_t g_gold = 1250;- (void)onTap {    if (self.hasRun) return;    self.hasRun = YES;    [self.actionBtn setTitle:@”执行中...” forState:UIControlStateNormal];    [self.actionBtn setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];    self.actionBtn.enabled = NO;    self.stepLabel.text = @”扫描中...”;    self.stepLabel.textColor = [UIColor orangeColor];    dispatch_async(dispatch_get_global_queue(0, 0), ^{        mt_ctx_t ctx;        mt_init(&ctx, mach_task_self());          // 初始化,task port = 自身        // 步骤 1:扫描值为 1250 的 4 字节整数        dispatch_async(dispatch_get_main_queue(), ^{            self.stepLabel.text = @”1/3  扫描内存中...  搜索值 = 1250”;        });        mach_vm_address_t addr = 0;        bool found = mt_scan_u32(&ctx, 1250, &addr);  // 核心:内存扫描        if (!found) {            dispatch_async(dispatch_get_main_queue(), ^{                self.stepLabel.text = @”扫描失败:未找到值 1250”;                self.stepLabel.textColor = [UIColor redColor];            });            return;        }        // 步骤 2:定位到地址        dispatch_async(dispatch_get_main_queue(), ^{            self.stepLabel.text = [NSString stringWithFormat:                @”2/3  已定位到地址 0x%llx”, addr];        });        usleep(400000);        // 步骤 3:写入新值        dispatch_async(dispatch_get_main_queue(), ^{            self.stepLabel.text = @”3/3  写入新值 8888...”;        });        mt_write_u32(&ctx, addr, 8888);           // 核心:直接写内存        // 验证        uint32_t verify = 0;        mt_read_u32(&ctx, addr, &verify);        dispatch_async(dispatch_get_main_queue(), ^{            self.valueLabel.text = [NSString stringWithFormat:@”%u”, g_gold];            if (verify == 8888 && g_gold == 8888) {                self.valueLabel.textColor = [UIColor greenColor];                self.stepLabel.text = @”✅ 验证通过:1250 → 8888”;                self.stepLabel.textColor = [UIColor greenColor];                [self.actionBtn setTitle:@”✅ 修改成功” forState:UIControlStateNormal];            } else {                self.valueLabel.textColor = [UIColor redColor];                self.stepLabel.text = [NSString stringWithFormat:                    @”❌ 失败  verify=%u  g_gold=%u”, verify, g_gold];                self.stepLabel.textColor = [UIColor redColor];            }        });    });}

2.3 运行效果

左侧 1250(黄色)、右侧 8888(绿色)。g_gold = 1250 这一行代码从来没执行过 g_gold = 8888——值是被 mach_vm_write 从外部直接改的。
下面拆解 onTap 里调用的四个核心函数,每一步在 Mach 内核里发生了什么。

3. 步骤一:mt_init — 拿到 task port

mt_ctx_t ctx;mt_init(&ctx, mach_task_self());
一切 Mach VM 操作的前提是 task port。同进程用 mach_task_self(),跨进程(外部工具改其他 App)需要越狱后通过 task_for_pid 拿目标 task port。

4. 步骤二:mt_scan_u32 — 扫描内存找到 1250

mach_vm_address_t addr = 0;bool found = mt_scan_u32(&ctx, 1250, &addr);
从输入(值 1250)到输出(地址 addr),内部做了三步:

4.1 枚举所有可读写区域

mach_vm_region 遍历进程的虚拟地址空间,过滤出 VM_PROT_READ | VM_PROT_WRITE 的区域——全局变量和堆在这里。系统库在 dyld_shared_cache 里(地址 0x180000000+),数据不会存在那里,直接过滤掉。
kern_return_t kr = mach_vm_region(task, &addr, &size,    VM_REGION_BASIC_INFO_64,    (vm_region_info_t)&info, &info_count, &object_name);// 过滤条件if ((info.protection & VM_PROT_READ) && (info.protection & VM_PROT_WRITE)    && !is_in_shared_cache(addr)) {    // 这个区域值得扫描}
一个坑:某些内核区域返回 size=0,不手动跳过就是无限循环。

4.2 分块读取并匹配

对每个可读写区域,用 mach_vm_read_overwrite 逐块拉数据到本地缓冲区,然后搜索字节序列 0xE2 0x04 0x00 0x00(1250 的小端表示):
#define CHUNK (1024 * 1024)  // 1MB 块uint8_t buf[CHUNK];while (cur < region_end) {    mach_vm_read_overwrite(task, cur, CHUNK, (mach_vm_address_t)buf, &bytes_read);    // 在 buf 中搜索 1250 的 4 字节编码    for (size_t i = 0; i <= bytes_read - 4; i++) {        if (*(uint32_t *)(buf + i) == 1250) {            *out_addr = cur + i;   // 命中!返回地址            return true;        }    }    cur += bytes_read - 3;  // 回退 3 字节,防止跨块漏匹配}
mach_vm_read_overwrite 比 mach_vm_read 高效——前者的数据直接写到调用方缓冲区,后者需要内核分配新内存再映射回来,多了两次上下文切换。

4.3 更多搜索方式

除了精确值扫描,工具还支持:
// 带通配符的 AoB(Array of Bytes)mt_scan_aob(&ctx, pattern, mask, pat_len, &addr);// 多结果收集(所有命中地址)mt_result_set_t rs;mt_result_init(&rs);mt_scan_u32_all(&ctx, 1250, &rs);// 然后在结果集中缩小范围(经典游戏修改器流程)mt_narrow_search_u32(&ctx, &rs, 1180);  // 只保留变成 1180 的// 各种宽度mt_scan_u8(&ctx, 42, &addr);mt_scan_f32(&ctx, 3.14f, &addr);  // 游戏坐标常用mt_scan_u64(&ctx, 0xDEAD, &addr);// 指针链追踪(多级间接引用)uint64_t offsets[] = {0x10, 0x24, 0x08};mt_follow_pointer(&ctx, game_manager_base, offsets, 3, &gold_addr);

5. 步骤三:mt_write_u32 — 直接写入 8888

mt_write_u32(&ctx, addr, 8888);
底层就是 Mach 内核的 mach_vm_write:
kern_return_t kr = mach_vm_write(task, addr, (vm_offset_t)&value, sizeof(value));// kr == KERN_SUCCESS
这一行执行完,addr 处的物理内存已经被改写。需要特别注意的是:mach_vm_write 直接写内存,完全绕过了任何语言运行时。没有 setter、没有 KVO、没有 willChange/didChange 回调——ObjC runtime 不知道这件事发生过。只有当你重新读出这个地址的值时,才会发现它已经变了。

6. 步骤四:mt_read_u32 — 验证写入

uint32_t verify = 0;mt_read_u32(&ctx, addr, &verify);// verify == 8888  ✓
通过 mach_vm_read_overwrite 读回确认。在 onTap 的最后一步,同时检查了 verify == 8888 和 g_gold == 8888——两者是同一个物理地址,读出来的结果一致,证明了 mach_vm_write 确实改到了 g_gold 的内存。

7. 完整源码获取

ios_mem_toolkit.h — 全部 API 声明
ios_mem_toolkit.c — 完整实现,含分块扫描、缩小搜索、指针链、代码段 Patch、平台兼容层
获取方式:关注后私信回复「内存工具箱」即可获取完整源码。

8. 参考资料

[1] Apple Mach VM API — mach_vm_region, mach_vm_read_overwrite, mach_vm_write, mach_vm_protect
[2] XNU 源码 — vm_map.c,https://github.com/apple-oss-distributions/xnu
[3] ARM Architecture Reference Manual — ARM64 指令编码(MOV/CBZ/B/SUBS)
[4]  — segment_command_64, max_protection 字段

技术交流

对 iOS 内存操作、Mach VM API 或逆向工程感兴趣,欢迎深入探讨。
读完本文,如果你希望把零散的知识点串成完整的攻防体系,可以系统学习《iOS逆向安全从入门到攻防实战》:
阶段
内容
入门篇
逆向基础、越狱、环境搭建、Hook 入门,动手修改 IDFA / IDFV
基础篇
MachOView / Hopper / IDA Pro 工具链、Mach-O 格式精讲、脱壳、反编译分析
中级篇
Method Swizzling / FishHook / Dobby / Frida 四套 Hook 方案,覆盖 OC 方法、C 函数、符号表、运行时注入
高级篇
非 MonkeyDev 重打包、注入动态库、越狱 Tweak 插件开发、Tweak 与 App 通信
实战篇
虚拟相机(替换摄像头帧 + 预览视频)、虚拟定位(开发者方式 / Hook / 注入三类方案)
防护篇
Method Swizzling 检测、Got 表 Hook 检测、InlineHook 检测、重打包检测
设备指纹
概念、稳定性实现、抹机不变、篡改三方指纹
整套课程攻防双视角,从工具使用讲到工程落地。

最新文章

随机文章