某鱼App逆向
请求参数分析
1 | |
| 字段 | 值 | 备注 |
|---|---|---|
| x-sgext | 变化 | |
| umid | 固定 | |
| x-sign | 变化 | |
| x-mini-wua | 变化 | |
| oaid | 固定 | |
| x-t | 秒级时间戳 | |
| x-extdata | openappkey%3DDEFAULT_AUTH | 固定 |
| x-ttid | 231200%40fleamarket_android_7.24.50 | 固定 |
| x-c-traceid | null + 毫秒级时间戳 + 4位数字(当前请求次数?) +131963 | |
| x-location | 经纬度 | |
| x-umt | 固定 | |
| x-utdid | 固定 | |
| c-launch-info | 3,0,1767621947276,1767621723839,3 | 3,0,相比x-c-traceid更早的毫秒级时间戳, app启动还是安装的时间戳? |
| x-falco-id | 变化 | |
| user-agent | sdk+版本 + 设备信息 |
同一次搜索,请求前10个商品的参数
1 | |
同一次搜索,下拉加载更多
1 | |
搜索请求RPC
请求与响应的hook 点定位
根据同一关键字的多次请求,可以发现resultListLastIndex字段的值发生改变,以及增加了sqiControlFieldsJson字段。jadx中搜索这两个关键字

定位关键方法resetPageNumber以及updatePageNumberAndIndex方法。查看该方法的用例,定位到refresh以及loadMore方法。

根据方法名以及代码逻辑,可以确定refresh和loadMore方法首先更新参数(刚才所提及的字段),然后调用requestMtop方法,该方法对searchResultReq填充字段,然后发送请求,主要代码部分如下

请求成功调用onSuccess方法,该方法调用access$100方法

ThreadUtils.runOnUI方法顾名思义就是通过线程更新搜索结果展示的UI界面,trackRefreshAndMore方法则可能是日志记录。
很显然,我们可以利用这个hook点从searchResultResp对象中获取到响应结果。至于SearchResultResp中各个字段的含义,可以与响应结果进行对比,很容易知道这些字段都是什么内容。

通过对比,知道了商品信息在resultList中。
响应结果获取的hook 点找到了,接下来就是找请求构造
hook refresh和loadMore方法
1 | |
根据测试,可以确认refresh是重新加载与搜索关键字相关的商品,loadMore是加载更多与搜索关键字相关的商品。
1 | |
它们的回调函数都是com.taobao.idlefish.search_implement.mvp.presenter.SearchResultPresenter$1匿名函数。
通过堆栈可以发现,refresh和loadMore方法分别由SearchResultPresenter类中的refreshAll和loadMore方法调用而来的。再往前溯源,则是SearchResultActivity类中的方法了,很显然,这是搜索结果展示页面。它对应的onCreate()方法如下

其中initRequestParams顾名思义就是初始化请求参数,这里很可能就有我们的搜索关键字。

frida hook该方法,打印routerParams字段
1 | |
结果如下
1 | |
此时我们的搜索关键字(”phone”)已经存在了。
梳理一下搜索功能的代码逻辑,首先会通过SearchResultActivity.onCreate()方法创建搜索结果展示页面,该方法里面会填充基本的请求参数(此时还未生成x-系列),创建一个searchResultPresenter实例,然后调用refreshAll()方法进行首次网络请求,该方法进一步调用SearchResultModel.refresh()方法,设置页码以及sqicontrol字段的值,最终通过requestMtop()方法完成请求的最后组装,最终发送请求。如果是加载更多商品,则是调用SearchResultPresenter.loadMore()方法,后续步骤类似,不在讲述。
RPC制作
通过内存搜索SearchResultActivity、SearchResultPresenter、SearchResultModel三个实例,发现只有在搜索页面出现的情况下才在内存中存在唯一实例,因此可以借助这些实例来完成自己的任意关键字搜索。
思路如下:
选取的hook点为searchResultModel的refresh和loadMore方法,因为符合功能最小化原则(防止后续调用过多无关函数),且这两个hook点会自动处理参数生成(如x-系列),以及自动根据响应结果更新请求(updatePageNumberAndIndex、updateSqiControlFieldsJson)。
最主要是搜索关键字的修改,需要注意的是修改routerParams无效!因为routerParams虽然在requestMtop方法中存在使用,但并没有修改request的keyword,因此我们需要直接获取request实例然后修改keyword字段。最后需在access$100处解析响应结果,但是不要调用原方法,以防止回调函数触发导致一系列可能的报错。
rpc脚本如下
1 | |
python端代码如下
1 | |
以及导出成服务端API
1 | |
网络访问/search API的果如下

x-系列参数生成定位
native层定位
以x-sgext为例, hook HashMap过滤x-sgext并打印函数调用堆栈
1 | |
结果如下
1 | |
com.taobao.wireless.security.adapter.JNICLibrary.doCommandNative在jadx中搜索不到,显然这个类是动态加载的(通过.jar或.dex形式动态加载)。使用 frida 来枚举一下 classloader 来定位这个类在哪个文件中。
1 | |
结果如下:
1 | |
classloader里的第一个文件就是我们要找的,名为libsgmain.so。看似是so文件,其实是jar文件,将其从对应文件夹中提取出来,放入jadx中进行分析。

接下来就定位doCommandNative函数在哪个so库中,由于它可能是静态加载也可能是动态加载,因此我们要 hook JNI 的 RegisterNatives 函数以及 libdl.so 的 dlsym 函数,代码如下(通过spawn方式启动):
1 | |
打印如下
1 | |
ida分析该函数 0x280c4 ,发现都是BR间接跳转。
间接跳转去混淆
对应BR间接跳转混淆的修复,可以借助frida stalker将doCommandNative的指令轨迹记录下来,同时记录br\blr指令的跳转地址。代码如下:
1 | |
然后再从日志中提取br\blr指令中的地址,对于固定跳转地址的指令,进行间接跳转指令的修复。
1 | |
修复脚本如下
1 | |
然后再借助AI修复,最终展示效果如下:
!
其中sub_25B78是根据cmdId选取待执行的函数,sub_109B94则是抛SecException的地方。

参数7012的确定
尝试打印doCommandNative的入参,发现该函数一直被调用,因此需要通过第一个参数进行过滤。如何寻找第一个参数呢,可以根据doCommandNative的调用堆栈
1 | |
com.alibaba.wireless.security.middletierplugin.d.d.a.a方法也不在apk中,通过定位确定在
1 | |
libsgmiddletier.so其实是个.jar文件,对应方法如下

可以确认第一个参数位70102
1 | |
结果如下
1 | |
生成的参数分别为:x-sgext、x-umt、x-mini-wua、x-sign
Unidbg模拟执行
如果仅是简单直接的模拟执行doCommandNative方法,则回抛出SecExpection 701029904异常,通过网上查阅,发现该方法存在初始化函数。
确定初始化函数
1 | |
部分打印如下:
1 | |
提取关键方法如下:
1 | |
按顺序依次调用doCommandNative,最后调用doCommandNative生成x-系列参数。
Unidbg补环境
初始代码
1 | |
报错
1 | |
1 | |
报错
1 | |
通过ActivityThread获取当前Application实例,然后调用相应方法获取返回值。
1 | |
报错
1 | |
可能需要文件路径重定向?毕竟new File是电脑端执行的,映射的是本地路径。
1 | |
接下来就是这个文件的方法
1 | |
不能直接获取File对象并执行相应方法,刚才说过,路径是本地的
1 | |
报错
1 | |
1 | |
报错
1 | |
1 | |
报错
1 | |
遍历的classloader并没有找到该类,取值参考了https://bbs.kanxue.com/thread-268927-1.htm
1 | |
报错
1 | |
静态方法,那就使用frida主动调用获取具体值
1 | |
报错
1 | |
1 | |
重新运行后,发现跑通了10101 app_SGLib以及10102 libsgmainso-6.6.231201.33656539.so。之后就存在一个致命报错
1 | |
slot值没有设置初始值就直接获取,尝试如下修复
1 | |
然后就出现了libsgsecuritybodyso dlopen失败的报错(一度怀疑slot跟libsgsecuritybodyso的加载有关,卡了好久)
1 | |
经查询,该文件确实存在于rootfs目录下,且文件路径没有问题。发给gpt,告诉我可以手动加载该库。
1 | |
没想到居然成功了!接下来报错
1 | |
这次参考刚才的给出的文章链接
1 | |
报错
1 | |
返回对应类的空对象进行占位
1 | |
报错
1 | |
1 | |
接下来终于到了slot的SetStaticLongField报错
1 | |
1 | |
报错
1 | |
获取类名参数,通过vm.resolveClass让模拟器自己去找
1 | |
报错,看样子来到了设备信息采集环节了
1 | |
通过枚举类加载器,确认该类在/data/app/com.taobao.idlefish-Mk5qkEh32zCm7x9fbp9IBw==/lib/arm64/libsgmain.so文件中

静态方法的话,在加载libsgmain.so后hook 该函数并主动调用。
1 | |
报错
1 | |
同刚才所述方法以获取返回值
1 | |
重新运行后,终于跑通了10102 libsgsecuritybodyso-6.6.231201.33656539.so和 10102 libsgmiddletierso-6.6.231201.33656539.so。接下来就是与70102 x-系列参数生成相关的报错了。
非常可惜!!!补了这么久的环境,结果到头来还是抛出 SecException 701029904报错!这说明初始化工作还是未完成!
根据日志实现了其他早于70102的调用号,然而还是没有用。以下是相关调用号的实现,比较杂乱。
继续根据日志查找早于70102的调用号,在他之上是
1 | |
unidbg里调用发现此处出现同70102的SecException 702019904。同样也走到了SecException处。根据之前的分析,很可能是marjor_cmd(7)模块还未挂载
1 | |
继续往前追溯,找不是7的
1 | |
实现了但是没什么用
然后发现唯一调用号13806
1 | |
也没用
1 | |
再次证明主模块7未加载
然后是22302调用号
1 | |
添加后,提示补环境
1 | |
1 | |
然后发现aWTitjAtFNcDALD1RXzM/mEk的值是调用号70102的参数的一部分
继续补环境报错
1 | |
通过frida hook获取该值
1 | |
最终发现模拟执行的返回值和真机的不一样!!!
1 | |
这说明环境没补对。
然后是10401调用号
1 | |
报错
1 | |
1 | |
报错
1 | |
1 | |
报错
1 | |
1 | |
补完之后,出现SecException报错
1 | |
搜索发现是参数不正确。
后续待补充…..
参考:
unidbg调用sgmain的doCommandNative函数生成某酷encryptR_client参数
关于unidbg调试某app的libsgmainso文件出现的SecException(1910)问题-Android安全-看雪安全社区|专业技术交流与安全研究论坛