mlgmxyysd 发表于 2023-10-15 13:46

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

本帖最后由 mlgmxyysd 于 2023-10-15 23:15 编辑

书接上回,在逆向元气骑士的时候,遇到了 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 内核进程机制:


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

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

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



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

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

TB371FC:/ $ su
TB371FC:/ # ps -ef
UID            PIDPPID 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
root             3   2 0 16:36 ?      00:00:00
root             4   2 0 16:36 ?      00:00:00
root             5   2 0 16:36 ?      00:00:00
root             6   2 0 16:36 ?      00:00:00
root             7   2 0 16:36 ?      00:00:00
root             8   2 0 16:36 ?      00:00:00
root             9   2 0 16:36 ?      00:00:00
root            10   2 0 16:36 ?      00:00:00
root            11   2 0 16:36 ?      00:00:00
root            12   2 0 16:36 ?      00:00:00
root            13   2 0 16:36 ?      00:00:00
root            14   2 0 16:36 ?      00:00:00
# ... 此处省略无数行 ...
u0_a281       5957   755 9 17:12 ?      00:00:21 com.ChillyRoom.DungeonShooter
# ... 此处省略无数行 ...
root          90799052 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         104839052 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                                 
32c00000-32c01000 rw-p 00000000 00:00 0                                 
6ffb0000-70240000 rw-p 00000000 00:00 0                                 
70240000-70282000 rw-p 00000000 00:00 0                                 
70282000-702ab000 rw-p 00000000 00:00 0                                 
702ab000-702ed000 rw-p 00000000 00:00 0                                 
702ed000-702ee000 rw-p 00000000 00:00 0                                 
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                              
77d9ad8000-77d9af0000 r--s 00000000 00:11 11302                        /dev/__properties__/property_info
77d9af0000-77d9b54000 r--p 00000000 00:00 0                              
77d9b54000-77d9b56000 rw-p 00000000 00:00 0                              
77d9b56000-77d9b57000 r--p 00000000 00:00 0                              
77d9b57000-77d9e9e000 ---p 00000000 00:00 0
# ... 此处省略无数行 ...
接下来是找规律时间。不难理解,第一列是当前连续内存区域的起止位置(起-止);第二列是内存权限;第三列是类似偏移之类的地址;第四列是文件所在设备前四位的十六进制;第五列是文件的 Inode 值;最后一列则是内存区域名称或对应的文件名。

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

TB371FC:/ # stat /system/framework/arm64/boot.oat
File: /system/framework/arm64/boot.oat
Size: 3777592Blocks: 7384    IO Blocks: 512regular 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: 512regular 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: 512regular file
Device: 11h/17dInode: 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                              
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                              
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                              
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> "
      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

mlgmxyysd 发表于 2023-10-15 23:20

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

修复 ELF 可以尝试使用:https://github.com/F8LEFT/SoFixer

XhyEax 发表于 2023-10-15 16:31

楼主可以试试https://github.com/BryanGIG/PADumper

lyl610abc 发表于 2023-10-16 14:07

本帖最后由 lyl610abc 于 2023-10-16 14:16 编辑

意想不到的思路
我通常都是使用 frida 执行脚本直接 dump 出 so 文件:https://github.com/lasting-yang/frida_dump (PS:会自动进行 so-fix 修正,超好用)
然后对于 global-meta.dat 有 2 种办法:
1.直接搜索特征码(针对在内存中解密后 global-meta.dat 头部特征不变的):https://github.com/350030173/global-metadata_dump/tree/master
2.分析 MetadataLoader::LoadMetadataFile , hook 后获取加载完 global-meta.dat 的内存返回值,然后再 dump 出来 :https://raw.githubusercontent.co ... -metadata-finder.js
参考链接:
1.https://github.com/IroniaTheMast ... tadata-First-Method
2.https://github.com/IroniaTheMast ... adata-Second-Method
可以直接使用 frida dump 出 il2cpp.so 和 global-meta.dat

PS: Zygisk-il2cppDumper 走的是另一个路子,通过 il2cpp 导出函数以及反射解析出 dump.cs

BonnieRan 发表于 2023-10-15 17:14

看完了 学到了新的dump姿势,感谢楼主这么详细的贴{:301_1003:}

罗萨 发表于 2023-10-15 17:29

{:1_921:}牛,不过我选择使用更简单的frida

rain1986 发表于 2023-10-15 22:05

好帖子,学习啦

mlgmxyysd 发表于 2023-10-15 22:45

XhyEax 发表于 2023-10-15 16:31
楼主可以试试https://github.com/BryanGIG/PADumper

收藏了,之前没搜到相关软件,就自己写了&#129315;

xyz989 发表于 2023-10-15 23:03

本帖最后由 xyz989 于 2023-10-15 23:11 编辑

大佬 有没有提取素材(PNG)后转动态化(GIF)的教程
是网页游戏(农场3.0采用Unity重制游戏)


https://i.im.ge/2023/02/10/ay4emL.2.gif

人物素材链接(原文件)
https://xyz989.lanzouo.com/ir00j0n5vmah


提取工具AssetStudioGUI

https://s1.ax1x.com/2023/02/10/pShuR5n.png


导出的资源目录如下
https://s1.ax1x.com/2023/02/10/pShu5vT.png

mlgmxyysd 发表于 2023-10-15 23:17

xyz989 发表于 2023-10-15 23:03
大佬 有没有提取素材(PNG)后转动态化(GIF)的教程
是网页游戏(农场3.0采用Unity重制游戏)



我记得之前有个东西可以支持导入Unity的导出模型来着,让我找找看

xyz989 发表于 2023-10-15 23:21

mlgmxyysd 发表于 2023-10-15 23:17
我记得之前有个东西可以支持导入Unity的导出模型来着,让我找找看

收到 有劳了 {:1_893:}
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: Unity 游戏逆向:从内存中获取未保护的 IL2Cpp 可执行文件