Unity 游戏逆向:从内存中获取未保护的 IL2Cpp 可执行文件
Unity 游戏逆向:从内存中获取未保护的 IL2Cpp 可执行文件

Unity 游戏逆向:从内存中获取未保护的 IL2Cpp 可执行文件

阅读时长 ≈33 分, 49 秒

Loading

书接上回,在逆向元气骑士的时候,遇到了 ERROR: This file may be protected. 报错(传送门)。

根据 Il2CppDumper 的文档提示,libil2cpp.so 被保护了,可以使用 GameGuardian 从游戏内存中来获取未保护的可执行文件。

GameGuardian 固然强大,但问题是 我 不 会 用 啊。作者还提到了他的另一个项目 Zygisk-IL2CppDumper,但这个项目局限性非常大,只能取出 dump.cs,无法从 global-metadata.dat 中生成 stringliteral.json(IL2Cpp 的字符串保存在 global-metadata.dat 中,如果不使用脚本应用字符串,IDA 是反编译不出来的),也无法生成 script.json 来辅助 IDA Pro 分析代码。

而且 Magisk 自 23.0 以后就已经实质性停更,最重要的 MagiskHide 功能也被去掉了,后面版本用 Zygisk 实现的隐藏模块都是假隐藏,所以我一直停留在 23.0 版本,自然也就无缘 Zygisk-IL2CppDumper。


既然是从内存中取出内容,那就必然少不了内存操作,让我们先来了解一下一些基础的 Linux 内核进程机制:

  1. Linux 进程的信息保存在 /proc 虚拟文件系统中,/proc/<pid> 文件夹内是进程 <pid> 的信息
  2. 进程的 UID、PID 等信息可以通过 ps -ef 命令获取
  3. /proc/<pid>/mem 是进程所占用的虚拟内存空间,不可直接读取或复制,需要使用 open、read 或 lseek 等系统调用来使用
  4. /proc/<pid>/maps 是一个列表,记录了进程或线程中的连续虚拟内存区域
  5. 进程的内存空间在一般情况下只能由自己读写,读写其他进程的内存空间需要足够的权限(一般是 root)

以上不是全部,但这些信息已经足够我们完成当前的工作了。

我们还是以元气骑士为例,既然是从内存中获取,进程肯定要在后台运行。首先启动游戏,看到加载界面时,我们需要的东西就已经加载到内存了,可以退回到桌面了。(奔跑的小猫真可爱)

读写应用时,建议将应用挂在后台,这是因为,应用可能会对 /proc/self/mem 挂文件监测,识别到意外读写时杀死自身,这会对我们的分析造成不利影响。

读写内存需要权限,这里我们执行了 su,后续将默认以 root 用户执行命令。先使用 ps -ef 命令获取进程 PID:

TB371FC:/ $ su
TB371FC:/ # ps -ef
UID            PID  PPID C STIME TTY          TIME CMD
root             1     0 1 16:36 ?        00:00:02 init second_stage
root             2     0 0 16:36 ?        00:00:00 [kthreadd]
root             3     2 0 16:36 ?        00:00:00 [rcu_gp]
root             4     2 0 16:36 ?        00:00:00 [rcu_par_gp]
root             5     2 0 16:36 ?        00:00:00 [kworker/0:0-events]
root             6     2 0 16:36 ?        00:00:00 [kworker/0:0H-events_highpri]
root             7     2 0 16:36 ?        00:00:00 [kworker/u16:0+NPU_CNTL]
root             8     2 0 16:36 ?        00:00:00 [mm_percpu_wq]
root             9     2 0 16:36 ?        00:00:00 [ksoftirqd/0]
root            10     2 0 16:36 ?        00:00:00 [rcu_preempt]
root            11     2 0 16:36 ?        00:00:00 [rcu_sched]
root            12     2 0 16:36 ?        00:00:00 [rcu_bh]
root            13     2 0 16:36 ?        00:00:00 [rcuop/0]
root            14     2 0 16:36 ?        00:00:00 [rcuos/0]
# ... 此处省略无数行 ...
u0_a281       5957   755 9 17:12 ?        00:00:21 com.ChillyRoom.DungeonShooter
# ... 此处省略无数行 ...
root          9079  9052 8 21:06 /debug_+ 00:00:00 ps -ef
root          9090   755 3 21:06 ?        00:00:00 zygote64

不出意外的,后台运行着很多进程,想要找到我们需要的进程并不容易,我们知道应用的包名是 com.ChillyRoom.DungeonShooter,可以使用 grep 精准找到该进程:

TB371FC:/ # ps -ef | grep com.ChillyRoom.DungeonShooter
u0_a281       5957   755 4 08:17:11 ?     00:00:22 com.ChillyRoom.DungeonShooter
root         10483  9052 10 08:26:09 /debug_ramdisk/.magisk/pts/0 00:00:00 grep com.ChillyRoom.DungeonShooter

Android 有多用户机制,如果有其他用户也在运行该进程(如应用多开等),输出结果会对我们造成干扰。

输出的第一列代表进程用户,Android 的 Linux 用户是有规律的,u0 表示 user 0,a281 表示 app 281,我们可以使用 am get-current-user 命令获取前台用户:

TB371FC:/ # am get-current-user
0

使用 grep 命令套一层:

TB371FC:/ # ps -ef | grep com.ChillyRoom.DungeonShooter | grep u$(am get-current-user)
u0_a281       5957   755 3 08:17:12 ?     00:00:24 com.ChillyRoom.DungeonShooter

输出的第二列就是我们需要的 PID,也可以再套一层 awk 命令实现自动化获取:

TB371FC:/ # ps -ef | grep com.ChillyRoom.DungeonShooter | grep u$(am get-current-user) | awk '{print $2}'
5957

cat 一下 maps 看看:

TB371FC:/ # cat /proc/5957/maps
12c00000-32c00000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space (region space)]
32c00000-32c01000 rw-p 00000000 00:00 0                                  [anon:libc_malloc]
6ffb0000-70240000 rw-p 00000000 00:00 0                                  [anon:dalvik-/system/framework/boot.art]
70240000-70282000 rw-p 00000000 00:00 0                                  [anon:dalvik-/system/framework/boot-core-libart.art]
70282000-702ab000 rw-p 00000000 00:00 0                                  [anon:dalvik-/system/framework/boot-okhttp.art]
702ab000-702ed000 rw-p 00000000 00:00 0                                  [anon:dalvik-/system/framework/boot-bouncycastle.art]
702ed000-702ee000 rw-p 00000000 00:00 0                                  [anon:dalvik-/system/framework/boot-apache-xml.art]
702ee000-70381000 r--p 00000000 fc:05 2867                               /system/framework/arm64/boot.oat
70381000-70665000 r-xp 00093000 fc:05 2867                               /system/framework/arm64/boot.oat
70665000-70666000 rw-p 00000000 00:00 0
# ... 此处省略无数行 ...
74113e6000-7411c78000 r--s 00000000 fc:05 2741                           /system/fonts/ZUKChinese.ttf
7411c78000-7412940000 ---p 00000000 00:00 0
7412940000-7412942000 rw-p 00000000 00:00 0
7412942000-7413a8a000 ---p 00000000 00:00 0
7413a8a000-7413a8c000 rw-p 00000000 00:00 0
7413a8c000-7413ce6000 ---p 00000000 00:00 0
7413ce6000-7413ce8000 rw-p 00000000 00:00 0
7413ce8000-7414c8c000 ---p 00000000 00:00 0
7414c8c000-7414c8e000 rw-p 00000000 00:00 0
7414c8e000-7415c78000 ---p 00000000 00:00 0
7415cc7000-7415cdd000 r--p 00000000 fc:0a 58226                          /data/user_de/0/com.google.android.gms/app_chimera/m/00000023/oat/arm64/DynamiteLoader.odex
# ... 此处省略无数行 ...
77d9aab000-77d9ab3000 rw-p 00000000 00:00 0
77d9ab3000-77d9ab4000 ---p 00000000 00:00 0
77d9ab4000-77d9ad4000 r--s 00000000 00:11 13673                          /dev/__properties__/properties_serial
77d9ad4000-77d9ad8000 rw-p 00000000 00:00 0                              [anon:System property context nodes]
77d9ad8000-77d9af0000 r--s 00000000 00:11 11302                          /dev/__properties__/property_info
77d9af0000-77d9b54000 r--p 00000000 00:00 0                              [anon:linker_alloc]
77d9b54000-77d9b56000 rw-p 00000000 00:00 0                              [anon:bionic_alloc_small_objects]
77d9b56000-77d9b57000 r--p 00000000 00:00 0                              [anon:atexit handlers]
77d9b57000-77d9e9e000 ---p 00000000 00:00 0
# ... 此处省略无数行 ...

接下来是找规律时间。不难理解,第一列是当前连续内存区域的起止位置(起-止);第二列是内存权限;第三列是类似偏移之类的地址;第四列是文件所在设备前四位的十六进制;第五列是文件的 Inode 值;最后一列则是内存区域名称或对应的文件名。

其中第四、第五列的含义是猜的,和对应文件 stat 的结果可以对应上,如果不对烦请赐教。

TB371FC:/ # stat /system/framework/arm64/boot.oat
  File: /system/framework/arm64/boot.oat
  Size: 3777592  Blocks: 7384    IO Blocks: 512  regular file
Device: fc05h/64517d     Inode: 2867     Links: 1        Device type: 0,0 # <- 702ee000-70381000 r--p 00000000 fc:05 2867
Access: (0644/-rw-r--r--)       Uid: (    0/    root)   Gid: (    0/    root)
Access: 2009-01-01 08:00:00.000000000 +0800
Modify: 2009-01-01 08:00:00.000000000 +0800
Change: 2009-01-01 08:00:00.000000000 +0800
TB371FC:/ # stat /data/user_de/0/com.google.android.gms/app_chimera/m/00000023/oat/arm64/DynamiteLoader.odex
  File: /data/user_de/0/com.google.android.gms/app_chimera/m/00000023/oat/arm64/DynamiteLoader.odex
  Size: 480880   Blocks: 944     IO Blocks: 512  regular file
Device: fc0ah/64522d     Inode: 58226    Links: 1        Device type: 0,0 # <- 7415cc7000-7415cdd000 r--p 00000000 fc:0a 58226
Access: (0444/-r--r--r--)       Uid: (10149/ u0_a149)   Gid: (10149/ u0_a149)
Access: 2023-10-09 19:29:42.674133462 +0800
Modify: 2023-10-09 19:29:42.878133462 +0800
Change: 2023-10-12 02:27:00.179999995 +0800
TB371FC:/ # stat /dev/__properties__/properties_serial
  File: /dev/__properties__/properties_serial
  Size: 131072   Blocks: 8       IO Blocks: 512  regular file
Device: 11h/17d  Inode: 13673    Links: 1        Device type: 0,0 # <- 77d9ab4000-77d9ad4000 r--s 00000000 00:11 13673
Access: (0444/-r--r--r--)       Uid: (    0/    root)   Gid: (    0/    root)
Access: 2023-10-15 08:16:40.956000000 +0800
Modify: 1970-02-01 23:04:31.907999999 +0800
Change: 1970-02-01 23:04:31.907999999 +0800

既然有文件名的信息,我们找一下 il2cpp 的内存:

TB371FC:/ # cat /proc/5957/maps | grep il2cpp
73c71c2000-73c8a39000 r--s 00000000 fc:0a 57632                          /data/media/0/Android/data/com.ChillyRoom.DungeonShooter/files/il2cpp/Metadata/global-metadata.dat
7417c25000-7417c78000 r--s 00000000 fc:0a 51745                          /data/media/0/Android/data/com.ChillyRoom.DungeonShooter/files/il2cpp/Resources/mscorlib.dll-resources.dat

奇怪,居然没有 libil2cpp.so,难道没加载进内存?但这很明显是不可能的,我们看一下应用安装时解压出来的 libraries:

TB371FC:/ # pm path com.ChillyRoom.DungeonShooter
package:/data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/base.apk
package:/data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_base_assets.apk
package:/data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
TB371FC:/ # cd /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/
TB371FC:/data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg== # ls lib/arm64/

惊不惊喜,意不意外,空的~ 运行库压根就没解压~


看到 split_xxx.apk 就知道,这是一个以 App Bundle 打包方式分发的应用,到这里就没头猪了,怎么办,查!

在 Stack Overflow 上找到了一个高赞回答(传送门),大概意思就是 App Bundle 打包的应用如果不设置 android.bundle.enableUncompressedNativeLibs=false,在安装时是不会解压的,应用直接从 APK 中读取运行库,这样做的好处是减少安装后的存储占用,也优化了 I/O 性能。

扯远了,既然应用直接从 APK 读取运行库,那么在内存中的文件名也应该是存储运行库的那个 APK,也就是 split_config.arm64_v8a.apk,找一下看看:

TB371FC:/ # cat /proc/5957/maps | grep split_config.arm64_v8a.apk
729caba000-729cd62000 r-xp 00142000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
729cd62000-729cd6a000 r--p 003e9000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
729cd6a000-729cdb2000 rw-p 003f1000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
72c0c80000-72c9a18000 r--p 00a00000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
72c9a18000-72caf02000 r--p 09fe7000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
72e9d18000-72eaa62000 r-xp 0b4d1000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
72eaa71000-72eaac9000 rw-p 0c21a000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73e3a0c000-73ebd62000 r-xp 00a00000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ebd63000-73ec790000 rw-p 08d56000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d0000-73ec9d3000 r-xp 09783000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d4000-73ec9d6000 rw-p 09786000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d8000-73ec9ec000 rw-p 09784000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7404a1b000-7405e9b000 r-xp 09fe7000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7405e9c000-7405eea000 r--p 0b467000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7405eea000-7405f05000 rw-p 0b4b5000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7417bd6000-7417bd7000 r-xp 09fb3000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7417bd8000-7417bd9000 r--p 09fb4000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7417bd9000-7417bda000 rw-p 09fb5000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7419f06000-7419f33000 r-xp 09fb6000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7419f34000-7419f37000 r--p 09fe3000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
7419f37000-7419f38000 rw-p 09fe6000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
741f4c8000-741f69f000 r-xp 00439000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
741f69f000-741f6a2000 r--p 0060f000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
741f6a2000-741f6ce000 rw-p 00612000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
77cdcfc000-77cdcfd000 r--s 0f6ab000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
77d4c08000-77d4c09000 r--s 0f6ab000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk

有了,还挺多,只看 maps 也看不出来哪个是 libil2cpp.so,怎么办?愣着啊,全 dump 出来干嘛?还准备慢慢看嘛?

说干就干,和已知一样,mem 不能被 cat,会报 I/O error 错误:

TB371FC:/ # cat /proc/5957/mem
cat: /proc/5957/mem: I/O error

不过我们可以用 dd 命令,来读写指定范围的内存,命令格式如下:

dd if="输入文件" of="输出文件" bs=块大小 skip=起始偏移块 count=块数量
# 从 if 中的第 skip 块后开始读取 count 个块,保存到 of 中,每个块大小为 bs 字节

(我已经不认识“块”这个字了)

取出速度取决于块大小,过小会非常慢,过大会丢失精度,同时也会降低速度。你想一下,dd 就像是在搬砖的你(不是我!是你!正在读这篇文章的你!),count 就像是每次搬的砖数量,一次搬一个,那得要搬多少次啊,一次搬太多反而会把自己累的搬不动,一不小心还超出需求,白搬了,所以选一个合适的快大小是很有必要的。

在读取内存时一般与系统的内存页大小(Pagesize)相同,可以用系统命令读取:

TB371FC:/ # getconf PAGESIZE
4096

起始地址除以块大小做 skip;用结束地址减去起始地址,再除以块大小,得到 count。(以第一行的为例)

TB371FC:/ # dd if="/proc/5957/mem" of="/sdcard/729caba000.bin" bs=$(getconf PAGESIZE) skip=120179386 count=680
680+0 records in
680+0 records out
2785280 bytes (2.6 M) copied, 0.135125 s, 20 M/s

不错,是 ELF 文件头,之前的思路是对的。但大小一看就不是 libil2cpp.so,这样一个一个提取太麻烦了,集合一下上面的思路,写成脚本自动化。

#!/system/bin/sh

package=com.ChillyRoom.DungeonShooter
pid=$(ps -ef | grep $package | grep u$(am get-current-user) | awk '{print $2}')
mem_list=$(grep "split_config.arm64_v8a.apk" /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}') # 把分隔符替换成"|",方便循环提取
SYS_PAGESIZE=$(getconf PAGESIZE)
HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE) # 取一下十六进制,计算用
for memory in $mem_list; do
	local range=$(echo $memory | awk -F'|' '{print $1}')
	local offset=$(echo $range | awk -F'-' '{print toupper($1)}') # 转换成大写
	
	dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;($(echo $range | awk -F'-' '{print toupper($2)}')-${offset})/$HEX_PAGESIZE" | bc) of="/sdcard/${offset}.bin" # 使用 bc 命令计算表达式
done

值得注意的是,由于 shell 的数字范围是 Int 32,最大只有 2147483647,直接计算会数值溢出,所以要使用 bc 命令计算表达式。bc 可以接受十六进制,但是表达式的值必须是大写。

执行脚本后,出现了一堆文件,找出来两个最大的(APK 里的 libil2cpp.so 有 140MB),尝试用 IL2CppDumper 解析。

两个都解析失败,看来还需要在其他文件上面下功夫。


so 文件有固定的文件头 7F 45 4C 46,猜测需要将其他内容加到 so 后面,补一下脚本。

#!/system/bin/sh

package=com.ChillyRoom.DungeonShooter
pid=$(ps -ef | grep $package | grep u$(am get-current-user) | awk '{print $2}')
mem_list=$(grep "split_config.arm64_v8a.apk" /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}')

SYS_PAGESIZE=$(getconf PAGESIZE)
HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE)

lastFile= # 保存一下最后的 ELF 文件名

for memory in $mem_list; do
	local range=$(echo $memory | awk -F'|' '{print $1}')
	local offset=$(echo $range | awk -F'-' '{print toupper($1)}')
	
	dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;$offset" | bc) count=4 of="/data/local/tmp/mem_temp" # 取前四位
	
	if [[ $(cat "/data/local/tmp/mem_temp") == $(echo -ne "\x7F\x45\x4C\x46") ]]; then
		fileExt="so" # 检测到 ELF 文件头
	else
		fileExt="bin"
	fi
	
	local fileOut="/sdcard/${offset}.${fileExt}"
	
	dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;($(echo $range | awk -F'-' '{print toupper($2)}')-${offset})/$HEX_PAGESIZE" | bc) of="$fileOut"
	
	if [[ $fileExt == "so" ]]; then
		lastFile=$fileOut
	else
		if [[ $lastFile != "" ]]; then
		  cat "$fileOut">>"$lastFile" # 将文件合并到上一个 ELF 中
			rm -f "$fileOut"
		else
			lastFile=$fileOut
		fi
	fi
	
	rm -f "/data/local/tmp/mem_temp"
done

老规矩,拉出来大小合适的出来跑 IL2CppDumper,发现还是报错,但之前识别不出来信息的那个 ELF 文件大小变大了不少,也可以识别了,算是有进步。


接下来就从这个文件下手,起始地址为 73E3A0C000,再看一眼 maps。

73e3a0c000-73ebd62000 r-xp 00a00000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ebd62000-73ebd63000 ---p 00000000 00:00 0 
73ebd63000-73ec790000 rw-p 08d56000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec790000-73ec9cf000 rw-p 00000000 00:00 0                              [anon:.bss]
73ec9cf000-73ec9d0000 ---p 00000000 00:00 0 
73ec9d0000-73ec9d3000 r-xp 09783000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d3000-73ec9d4000 ---p 00000000 00:00 0 
73ec9d4000-73ec9d6000 rw-p 09786000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d6000-73ec9d8000 ---p 00000000 00:00 0 
73ec9d8000-73ec9ec000 rw-p 09784000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk

原来在两个 APK 之间,还有一到两个非文件内存,我们在这里将其称为间隙块(Gap blocks),补全脚本把这段也加上。

#!/system/bin/sh

package=com.ChillyRoom.DungeonShooter
pid=$(ps -ef | grep $package | grep u$(am get-current-user) | awk '{print $2}')
mem_list=$(grep "split_config.arm64_v8a.apk" /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}')

SYS_PAGESIZE=$(getconf PAGESIZE)
HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE)

lastFile=
lastEnd= # 保存上个内存区域的结束偏移

for memory in $mem_list; do
	local range=$(echo $memory | awk -F'|' '{print $1}')
	local offset=$(echo $range | awk -F'-' '{print toupper($1)}')
	local end=$(echo $range | awk -F'-' '{print toupper($2)}')
	
	dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;$offset" | bc) count=4 of="/data/local/tmp/mem_temp"
	
	if [[ $(cat "/data/local/tmp/mem_temp") == $(echo -ne "\x7F\x45\x4C\x46") ]]; then
		fileExt="so"
	else
		fileExt="dump" # 我改个后缀不过分吧(
	fi
	
	local fileOut="/sdcard/${offset}.${fileExt}"
	
	dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${end}-${offset})/$HEX_PAGESIZE" | bc) of="$fileOut"
	
	if [[ $fileExt == "so" ]]; then
		lastFile=$fileOut
	else
		if [[ $lastFile != "" ]]; then
			if [[ $lastEnd != $offset ]]; then
			  dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${lastEnd}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc) of="/data/local/tmp/mem_temp" # 发现不一样啦,来把间隙块补全吧
				cat "/data/local/tmp/mem_temp">>"$lastFile"
			fi
		  cat "$fileOut">>"$lastFile"
			rm -f "$fileOut"
		else
			lastFile=$fileOut
		fi
	fi
	
	lastEnd=$end
	
	rm -f "/data/local/tmp/mem_temp"
done

正当我准备运行的时候,我发现了一个问题,在偏移稍微靠后的位置,有一些间隙块非常的大,可以达到几万甚至几百万块,这很明显不是同一个连续的内存区域,我们不能把他们强行合并到一起。

#!/system/bin/sh

package=com.ChillyRoom.DungeonShooter
pid=$(ps -ef | grep $package | grep u$(am get-current-user) | awk '{print $2}')
mem_list=$(grep "split_config.arm64_v8a.apk" /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}')

SYS_PAGESIZE=$(getconf PAGESIZE)
HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE)

lastFile=
lastEnd=

for memory in $mem_list; do
	local range=$(echo $memory | awk -F'|' '{print $1}')
	local offset=$(echo $range | awk -F'-' '{print toupper($1)}')
	local end=$(echo $range | awk -F'-' '{print toupper($2)}')
	
	dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;$offset" | bc) count=4 of="/data/local/tmp/mem_temp"
	
	if [[ $(cat "/data/local/tmp/mem_temp") == $(echo -ne "\x7F\x45\x4C\x46") ]]; then
		fileExt="so"
	else
		fileExt="dump"
	fi
	
	local fileOut="/sdcard/${offset}.${fileExt}"
	
	dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${end}-${offset})/$HEX_PAGESIZE" | bc) of="$fileOut"
	
	if [[ $fileExt == "so" ]]; then
		lastFile=$fileOut
	else
		if [[ $lastFile != "" ]]; then
			skipMerge=false
			if [[ $lastEnd != $offset ]]; then
				gap_block=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc)
				if [[ $gap_block -gt 32 ]]; then # 我也不知道应该把上限设置为多少合适,先随手写一个 32 吧,应该不会有间隙块超过 32*4096 字节的...吧...
					skipMerge=true # 间隙块过大,不合并到上个文件
					lastFile=$fileOut # 让后面的文件合并到当前文件
				else
			    dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${lastEnd}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc) of="/data/local/tmp/mem_temp"
				  cat "/data/local/tmp/mem_temp">>"$lastFile"
				fi
			fi
			if [[ $skipMerge == "false" ]]; then
		    cat "$fileOut">>"$lastFile"
			  rm -f "$fileOut"
			fi
		else
			lastFile=$fileOut
		fi
	fi
	
	lastEnd=$end
	
	rm -f "/data/local/tmp/mem_temp"
done

运行脚本,取出文件大小与原 libil2cpp.so 相似的文件,扔给 IL2CppDumper 解析,这次有一个可以正常解析了。


换一款游戏再来测试一下,想起了之前逆向过的开罗游戏创意糕点部(传送门),就拿它来开这第二刀了。

这款游戏最新版也用了 IL2Cpp 打包,并且加了 so 保护,但没有使用 App Bundle 发布。这意味着我们的脚本需要做一些改动,是时候直接在内存中搜索 libil2cpp.so 了,顺手再一起搜索一下设备支持的其他架构。

Android 设备支持的架构可以通过命令 getprop ro.product.cpu.abilist 获取,以半角逗号 , 分割。

#!/system/bin/sh

package=$1 # 通过命令行传入目标包名
pid=$(ps -ef | grep $package | grep u$(am get-current-user) | awk '{print $2}')

targets="libil2cpp.so "$(getprop ro.product.cpu.abilist | awk -F',' '{for (i = 1; i <= NF; i++) {gsub(/-/, "_"); print "split_config."$i".apk"}}') # 把可用架构的 "-" 替换成 "_" 以适配文件名

for target in $targets; do
	local maps=$(grep $target /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}')
	if [[ $maps != "" ]]; then
		if [[ $mem_list != "" ]]; then
			mem_list="${mem_list} ${maps}"
		else
			mem_list=$maps
		fi
	fi
done

SYS_PAGESIZE=$(getconf PAGESIZE)
HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE)

lastFile=
lastEnd=

for memory in $mem_list; do
	local range=$(echo $memory | awk -F'|' '{print $1}')
	local offset=$(echo $range | awk -F'-' '{print toupper($1)}')
	local end=$(echo $range | awk -F'-' '{print toupper($2)}')
	local memName=$(echo $memory | awk -F'|' '{print $6}' | awk -F'/' '{print $NF}') # 记录文件名以供区分
	
	dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;$offset" | bc) count=4 of="/data/local/tmp/mem_temp"
	
	if [[ $(cat "/data/local/tmp/mem_temp") == $(echo -ne "\x7F\x45\x4C\x46") ]]; then
		fileExt="so"
	else
		fileExt="dump"
	fi
	
	local fileOut="/sdcard/${offset}_${package}_${memName}.${fileExt}" # 输出文件名记录包名
	
	dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${end}-${offset})/$HEX_PAGESIZE" | bc) of="$fileOut"
	
	if [[ $fileExt == "so" ]]; then
		lastFile=$fileOut
	else
		if [[ $lastFile != "" ]]; then
			skipMerge=false
			if [[ $lastEnd != $offset ]]; then
				gap_block=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc)
				if [[ $gap_block -gt 32 ]]; then
					skipMerge=true
					lastFile=$fileOut
				else
			    dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${lastEnd}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc) of="/data/local/tmp/mem_temp"
				  cat "/data/local/tmp/mem_temp">>"$lastFile"
				fi
			fi
			if [[ $skipMerge == "false" ]]; then
		    cat "$fileOut">>"$lastFile"
			  rm -f "$fileOut"
			fi
		else
			lastFile=$fileOut
		fi
	fi
	
	lastEnd=$end
	
	rm -f "/data/local/tmp/mem_temp"
done

老规矩,运行代码,找到合适大小的文件,拉出来喂工具解析,成功解析。

扔进 IDA Pro 反编译(注:不能使用直接从 APK 提取出的so,因为解析出的偏移是根据 dump 出来的 so 计算的),弹了两个报错,可以忽略掉。跑脚本报了一堆错,凑合用。

可能 dump 出来的 so 需要修复,不过超出这篇文章的范畴了。(其实主要问题是我也不会 x)


脚本是没什么问题了(大概),但我们总不能每次都靠猜测来判断哪个是正确的 libil2cpp.so。那么能不能用脚本来代替我们来“猜测”呢,答案是肯定的。当然,不是根据大小来判断。

通过把 App Bundle 合并安装测试、将几个 maps 进行比对,我们发现内存中有一个 global-metadata.dat 文件,而在这个文件之后的一个 ELF 文件,通常就是我们要找的 libil2cpp.so

# 合并 App Bundle 后的元气骑士
7977268000-7980000000 r--p 00000000 fc:0a 49763                          /data/app/~~6kC6XFUULhHulM01DwI6fA==/com.ChillyRoom.DungeonShooter-_E4Bf_EJNotbVBZxg5eWXQ==/lib/arm64/libil2cpp.so
# ... 此处省略无数行 ...
79cd789000-79cf000000 r--s 00000000 fc:0a 50772                          /data/media/0/Android/data/com.ChillyRoom.DungeonShooter/files/il2cpp/Metadata/global-metadata.dat
# ... 此处省略无数行 ... 下面就是我们要找的
7aa5415000-7aad76b000 r-xp 00000000 fc:0a 49763                          /data/app/~~6kC6XFUULhHulM01DwI6fA==/com.ChillyRoom.DungeonShooter-_E4Bf_EJNotbVBZxg5eWXQ==/lib/arm64/libil2cpp.so
7aad76b000-7aad76c000 ---p 00000000 00:00 0 
7aad76c000-7aae199000 rw-p 08356000 fc:0a 49763                          /data/app/~~6kC6XFUULhHulM01DwI6fA==/com.ChillyRoom.DungeonShooter-_E4Bf_EJNotbVBZxg5eWXQ==/lib/arm64/libil2cpp.so
7aae199000-7aae3d8000 rw-p 00000000 00:00 0                              [anon:.bss]
7aae3d8000-7aae3d9000 ---p 00000000 00:00 0 
7aae3d9000-7aae3dc000 r-xp 08d83000 fc:0a 49763                          /data/app/~~6kC6XFUULhHulM01DwI6fA==/com.ChillyRoom.DungeonShooter-_E4Bf_EJNotbVBZxg5eWXQ==/lib/arm64/libil2cpp.so
7aae3dc000-7aae3dd000 ---p 00000000 00:00 0 
7aae3dd000-7aae3df000 rw-p 08d86000 fc:0a 49763                          /data/app/~~6kC6XFUULhHulM01DwI6fA==/com.ChillyRoom.DungeonShooter-_E4Bf_EJNotbVBZxg5eWXQ==/lib/arm64/libil2cpp.so
7aae3df000-7aae3e1000 ---p 00000000 00:00 0 
7aae3e1000-7aae3f5000 rw-p 08d84000 fc:0a 49763                          /data/app/~~6kC6XFUULhHulM01DwI6fA==/com.ChillyRoom.DungeonShooter-_E4Bf_EJNotbVBZxg5eWXQ==/lib/arm64/libil2cpp.so
# ... 此处省略无数行 ...

# 原版元气骑士
# ... 此处省略无数行 ...
729caba000-729cd62000 r-xp 00142000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
729cd62000-729cd6a000 r--p 003e9000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
729cd6a000-729cdb2000 rw-p 003f1000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
# ... 此处省略无数行 ...
73c71c2000-73c8a39000 r--s 00000000 fc:0a 57632                          /data/media/0/Android/data/com.ChillyRoom.DungeonShooter/files/il2cpp/Metadata/global-metadata.dat
# ... 此处省略无数行 ... 下面就是我们要找的
73e3a0c000-73ebd62000 r-xp 00a00000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ebd62000-73ebd63000 ---p 00000000 00:00 0 
73ebd63000-73ec790000 rw-p 08d56000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec790000-73ec9cf000 rw-p 00000000 00:00 0                              [anon:.bss]
73ec9cf000-73ec9d0000 ---p 00000000 00:00 0 
73ec9d0000-73ec9d3000 r-xp 09783000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d3000-73ec9d4000 ---p 00000000 00:00 0 
73ec9d4000-73ec9d6000 rw-p 09786000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
73ec9d6000-73ec9d8000 ---p 00000000 00:00 0 
73ec9d8000-73ec9ec000 rw-p 09784000 fc:0a 73347                          /data/app/~~wwI8fTI34usd9DBbBqpP4A==/com.ChillyRoom.DungeonShooter-olvDyD0tf9exDoyTM7Hwgg==/split_config.arm64_v8a.apk
# ... 此处省略无数行 ...

# 创意糕点部
# ... 此处省略无数行 ...
731780a000-7319fb3000 r--p 00000000 fc:0a 68394                          /data/app/~~EeYFpmSrnO1pEm-dVmp9rQ==/net.kairosoft.android.okashi_en-AWdoF0iBkO1WbE3SF5a1qA==/lib/arm64/libil2cpp.so
# ... 此处省略无数行 ...
734a86e000-734b000000 r--s 00000000 fc:0a 69978                          /data/media/0/Android/data/net.kairosoft.android.okashi_en/files/il2cpp/Metadata/global-metadata.dat
# ... 此处省略无数行 ... 下面就是我们要找的
7410624000-7412b11000 r-xp 00000000 fc:0a 68394                          /data/app/~~EeYFpmSrnO1pEm-dVmp9rQ==/net.kairosoft.android.okashi_en-AWdoF0iBkO1WbE3SF5a1qA==/lib/arm64/libil2cpp.so
7412b11000-7412c5b000 r--p 024ec000 fc:0a 68394                          /data/app/~~EeYFpmSrnO1pEm-dVmp9rQ==/net.kairosoft.android.okashi_en-AWdoF0iBkO1WbE3SF5a1qA==/lib/arm64/libil2cpp.so
7412c5b000-7412dce000 rw-p 02636000 fc:0a 68394                          /data/app/~~EeYFpmSrnO1pEm-dVmp9rQ==/net.kairosoft.android.okashi_en-AWdoF0iBkO1WbE3SF5a1qA==/lib/arm64/libil2cpp.so
# ... 此处省略无数行 ...

找到了这个规律,我们就可以将其写入脚本中了。顺便呢,再完善一下输入输出,方便调试。

#!/system/bin/sh

echo "########################"
echo "# IL2Cpp Memory Dumper #"
echo "# by NekoYuzu neko.ink #"
echo "########################"

if [[ $1 == "" ]]; then
	echo "* Usage: $0 <package> [out]"
	exit
fi

package=$1

if [[ $2 == "" ]]; then
	out=/sdcard/dump
else
	out=$2
fi

echo "- Target package: $package"
echo "- Output directory: $out"

mkdir -p "$out"

user=$(am get-current-user)
pid=$(ps -ef | grep $package | grep u$user | awk '{print $2}')

if [[ $pid == "" ]]; then
	echo "! Target package of current user ($user) not found, is process running?"
	exit
fi

echo "- Found target process: $pid"

targets="global-metadata.dat libil2cpp.so "$(getprop ro.product.cpu.abilist | awk -F',' '{for (i = 1; i <= NF; i++) {gsub(/-/, "_"); print "split_config."$i".apk"}}') # 加入 global-metadata.dat 查找

for target in $targets; do
	local maps=$(grep $target /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}')
	if [[ $maps != "" ]]; then
		if [[ $mem_list != "" ]]; then
			mem_list="${mem_list} ${maps}"
		else
			mem_list=$maps
		fi
	fi
done

cp /proc/$pid/maps "$out/${package}_maps.txt"

SYS_PAGESIZE=$(getconf PAGESIZE)
HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE)

echo "- Starting dump process..."

lastFile=
lastEnd=
metadataOffset=

for memory in $mem_list; do
	local range=$(echo $memory | awk -F'|' '{print $1}')
	local offset=$(echo $range | awk -F'-' '{print toupper($1)}')
	local end=$(echo $range | awk -F'-' '{print toupper($2)}')
	local memName=$(echo $memory | awk -F'|' '{print $6}' | awk -F'/' '{print $NF}')
	
	if [[ $memName == "global-metadata.dat" ]]; then
		metadataOffset=$offset # 记录 global-metadata.dat 的偏移
		continue
	fi
	
	dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;$offset" | bc) count=4 of="${out}/tmp" 2>/dev/null
	
	if [[ $(cat "${out}/tmp") == $(echo -ne "\x7F\x45\x4C\x46") ]]; then
		fileExt="so"
	else
		fileExt="dump"
	fi
	
	local fileOut="${out}/${offset}_${package}_${memName}.${fileExt}"
	
	if [[ $metadataOffset != "" ]] && [[ $(echo "ibase=16;(${metadataOffset}-${offset})<0" | bc) -ne 0 ]]; then
		echo "- Dumping [$memName] $range... <- This might be the correct libil2cpp.so" # metadata 偏移的后一个匹配通常是我们要找的 so
		metadataOffset=
	else
		echo "- Dumping [$memName] $range..."
	fi
	
	dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${end}-${offset})/$HEX_PAGESIZE" | bc) of="$fileOut" 2>/dev/null
	
	if [[ $fileExt == "so" ]]; then
		lastFile=$fileOut
	else
		if [[ $lastFile != "" ]]; then
			echo "- Merging memory..."
			skipMerge=false
			if [[ $lastEnd != $offset ]]; then
				gap_block=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc)
				if [[ $gap_block -gt 32 ]]; then
					echo "- Gap block(s) $gap_block is too large, skipping merge..."
					skipMerge=true
					lastFile=$fileOut
				else
					echo "- Adding $gap_block gap block(s)..."
					dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${lastEnd}/$HEX_PAGESIZE" | bc) count=$gap_block of="$out/tmp" 2>/dev/null
					cat "$out/tmp">>"$lastFile"
				fi
			fi
			if [[ $skipMerge == "false" ]]; then
				cat "$fileOut">>"$lastFile"
				rm -f "$fileOut"
			fi
		else
			echo "- No ELF header found, but nothing to merge. Treating as a raw dump..."
			lastFile=$fileOut
		fi
	fi
	
	lastEnd=$end

	rm -f "${out}/tmp"
done

echo "- Done!"

再进行最后一次测试,no problem~


后话:

脚本只能辅助,却不能代替我们去分析代码。

此外,脚本还有些不足之处,例如:so 文件可能需要修复;内存结束偏移后面还可能有一些有用的信息未 dump;等等。

希望有大佬可以帮着完善一下,有哪些说的不对的地方也欢迎指出~

脚本已开源在 GitHub,后续完善将在 GitHub 更新:https://github.com/MlgmXyysd/IL2CppMemoryDumper

发表回复