蚂蚁森林为支付宝内的一个小应用,一年前通过xposed hook的方式实现了静默式的自动偷取好友能量,具体的分析过程此处略去,可见尼古拉斯_赵四的帖子:Android支付宝蚂蚁森林能量自动收取插件开发原理解析。xposed拥有十分强大的功能,但是也有一定的缺点。首先很难24小时运行,由于支付宝不可能一直在前台,而手机会在特定情况下清理或者冻结掉后台应用。每次都得运行支付宝才行,这是一个很头疼的问题。最近趁着周围的人又在疯狂偷能量,于是想着能不能通过爬虫的方式来实现偷能量呢?经过几天的分析,终于实现了功能,对其中的一些坑以及难点进行一下记录。
首先通过前面的分析已经知道,蚂蚁森林的数据发送调用了
com.alipay.mobile.nebulaappproxy.api.rpc.H5RpcUtil.rpcCall(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/alibaba/fastjson/JSONObject;Ljava/lang/String;ZLcom/alipay/mobile/h5container/api/H5Page;ILjava/lang/String;ZILjava/lang/String;)Lcom/alipay/mobile/nebulaappproxy/api/rpc/H5Response;
通过xposed hook可以得到rpcCall的参数内容并且自己构造这部分参数来实现自动偷能量。跟踪rpcCall函数有一定难度,于是继续采用DDMS跟踪堆栈以及日志来分析。对H5Log以及LogCatUtil两个日志类进行hook,手机上操作好友列表时进行跟踪方法调用。在方法调用中发现了:
com.alipay.mobile.common.transport.http.HttpWorker.call()Lcom/alipay/mobile/common/transport/Response;
com.alipay.mobile.common.transport.http.HttpWorker.executeHttpClientRequest(Lorg/apache/http/HttpHost;Lorg/apache/http/HttpRequest;Lorg/apache/http/protocol/HttpContext;)Lorg/apache/http/HttpResponse;
这两个可能相关的函数,同时纵览一遍日志,也发现了可能相关的信息(对关键信息进行了删除):
HttpWorker:printHeaderLog. : visibleflag:1, miniwua:{"w":"HHnB_..."}, x-mgs-productversion:8f5..., AppId:Android-container, Version:2, Did:WSv..., Operation-Type:alipay.mobileaix.fetchAppList, Ts:MtfX9ar, Content-Type:application/protobuf, Sign:647..., signType:0, clientVersion:10.1.75.7000, Cookie:ALIPAYJSESSIONID=RZ4...; zone=RZ4..., Accept-Language:zh-Hans, Accept-Encoding:gzip, Connection:Keep-Alive, Retryable2:0,
在附近也发现了url,body,cookie等信息,猜想可能就是关键点:
url:https://mobilegw.alipay.com/mgw.htm
body:[{"ct":"android","av":"5","startPoint":"1","pageSize":20}]
cookie:ALIPAYJSESSIONID=RZ4...; zone=RZ4...
header:
visibleflag: 1
miniwua: {"w":"..."}
x-mgs-productversion: 8f5...
AppId: Android-container
Version: 2
Did: WSvmO...,
Operation-Type: alipay.antmember.forest.h5.queryEnergyRanking,
Ts: MtfX9ar,
Content-Type: application/protobuf,
Sign: 647...,
signType: 0,
clientVersion: 10.1.75.7000
Accept-Language: zh-Hans
Connection: Keep-Alive
Retryable2: 0
自己写一遍发送,发现成功,说明蚂蚁森林数据确实采用http协议并且数据包可以重放。
其中body时在前面制作hook插件时已经知道,是主要发送的数据。cookie也不必多说。对协议头部分进行分析,其中miniwua经过字符串查找,找到了定位函数com.alipay.rdssecuritysdk.v2.face.RDSClient.getMiniWuaData
,对该函数进行跟踪调用,发现生成主要与设备有关,因此可以固定。x-mgs-productversion应该为产品版本,直接固定。Did由httpworker.call中分析
httpUrlRequest.setHeader(new BasicHeader("Did", DeviceInfoUtil.getDeviceId()));
因此为设备ID,可固定。唯一难点在于sign函数。在httpworker.call中可以看到
SignData signData = getSignData();
继续跟踪到达此处(删掉部分无用代码):
public SignResult signature(SignRequest signRequest) {
try {
SecurityGuardManager instance = SecurityGuardManager.getInstance(EnvUtil.getContext());
ISecureSignatureComponent secureSignatureComp = instance.getSecureSignatureComp();
SignResult signResult = new SignResult();
Object hashMap = new HashMap();
hashMap.put("INPUT", signRequest.content);
SecurityGuardParamContext securityGuardParamContext = new SecurityGuardParamContext();
securityGuardParamContext.paramMap = hashMap;
securityGuardParamContext.appKey = signRequest.appkey;
if (signRequest.isSignTypeMD5()) {
securityGuardParamContext.requestType = 4;
}
signResult.sign = secureSignatureComp.signRequest(securityGuardParamContext, "");
signResult.setSuccess(true);
LoggerFactory.getTraceLogger().warn("SecurityManager", "[doSignature] Get security signed string: " + signResult.sign + ", requestType: " + securityGuardParamContext.requestType + ", appKey: " + securityGuardParamContext.appKey);
return signResult;
}
}
看到
signResult.sign = secureSignatureComp.signRequest(securityGuardParamContext, "");
最后跟踪落在了此处:
@InterfacePluginInfo(pluginName = "main")
public interface ISecureSignatureComponent extends IComponent {
String getSafeCookie(String str, String str2, String str3);
String signRequest(SecurityGuardParamContext securityGuardParamContext, String str);
}
emmmmm遇到了这种插件就很难受,但是可以继续在DDMS中跟踪堆栈调用,在后续的堆栈调用了发现了关键函数:
com.alibaba.wireless.security.open.securesignature.a.signRequest(Lcom/alibaba/wireless/security/open/SecurityGuardParamContext;Ljava/lang/String;)Ljava/lang/String;
最终找到了:
com.taobao.wireless.security.adapter.JNICLibrary.doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;
在apk反编译中并没有找到com.taobao.wireless.security.adapter.JNICLibrary
类。实际上,支付宝apk解压,在lib目录下发现libsgmain.so
,该文件为jar文件,解压得到插件dex以及lib目录下的核心so库:libsgmainso-6.4.11174435.so
,JNICLibrary
类就在该dex中,doCommandNative为native函数。尝试分析该so库,难度太大,放弃。但是既然sign签名涉及到这儿,还是得想办法搞定。想到最近很多人研究unicorn,看雪上有一篇帖子介绍了基于unicorn开发的模拟器,可以模拟执行ARM指令,可以基于此项目进行直接调用so库的加密函数。调用函数得知道参数,尝试hook参数。既然该dex为动态加载的,那么不能直接hook,得hook类加载器ClassLoader的loadClass函数:
XposedHelpers.findAndHookMethod(ClassLoader.class,
"loadClass",
String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
if (param.hasThrowable()) return;
Class> cl = (Class>)param.getResult();
String clsname = cl.getName();
......
}
});
这样写对于别的app动态加载dex是没有问题的,但是对于支付宝,只要执行到Class> cl = (Class>)param.getResult();
这一句就会报错,jni error,本地引用溢出。这儿卡了我很久,后来才想起来,不如打印一下loadClass的参数,看看到底是加载什么类才会报错,经过分析得知,当加载到java.lang.String
类时会报错,因此首先应该检查参数,过滤掉java.lang.String
才hook正常并且得到调用参数。
doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object
表明参数1为整数,参数2为一个Object数组。
signature_0:10401
signature_1[0]:Operation-Type=alipay.antmember.forest.h5.queryEnergyRanking&Request-Data=Cgx...&Ts=MtfZKyp
signature_1[1]:rpc-sdk-online
signature_1[2]:0
signature_1[3]:null
其中,Request-Data为base64编码过的body,Ts为:
private static String get64Time(){
long ts = System.currentTimeMillis();
return c10to64(ts);
}
private static final String c10to64(long j) {
char[] a = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '+', '/'};
int pow = (int) Math.pow(2.0d, 6.0d);
char[] cArr = new char[pow];
int i = pow;
do {
i--;
cArr[i] = a[(int) (63 & j)];
j >>>= 6;
} while (j != 0);
return new String(cArr, i, pow - i);
}
接下来尝试使用unidbg模拟执行so库中的doCommandNative
函数。根据demo,我们可以让模拟器加载指定的apk文件并且加载apk内部相应的so文件。在测试中发现,如果直接加载支付宝apk,由于libsgmainso-6.4.11174435.so
在插件内部,因此模拟器加载不到这个so,没办法调用。如果直接加载libsgmain.so
,模拟器可以加载到该jar文件内部的so库,但是在调用过程中会出现某个类出错,经过检查,该类并不是在插件内,而是在支付宝apk中出现。为了解决这个问题,可以手动将libsgmainso-6.4.11174435.so
放入支付宝apk/lib目录下,然后加载支付宝apk,测试加载so库,发现测试正常。
给出关键性的代码:
//创建模拟器
public void initSign() throws IOException{
emulator = createARMEmulator();
emulator.getSyscallHandler().addIOResolver(this);
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(createLibraryResolver());
memory.setCallInitFunction();
vm = emulator.createDalvikVM(APK_FILE);
vm.setJni(this);
DalvikModule dm = vm.loadLibrary("sgmainso-6.4.11174435", false);
dm.callJNI_OnLoad(emulator);
Native = vm.resolveClass("com.taobao.wireless.security.adapter.JNICLibrary".replace(".", "/"));
}
//获取签名值
public String getSign(String str) {
Number ret = Native.callStaticJniMethod(emulator, "doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;",
10401,
new ArrayObject(new ArrayObject(new StringObject(vm, str)),
new StringObject(vm, "rpc-sdk-online"), DvmInteger.valueOf(vm, 0), new StringObject(vm, "")));
long hash = ret.intValue() & 0xffffffffL;
DvmObject dvmObject = vm.getObject(hash);
vm.deleteLocalRefs();
return dvmObject.toString();
}
基于以上分析过程,便可以开发出蚂蚁森林爬虫程序,不再依赖手机以及支付宝。让你的支付宝随时随地都在偷能量!
回顾一下分析过程,找到协议的发送信息以及定位sign并不是难点,单纯的是一个体力活。之所以浪费了比较多的时间,首先在hook动态加载的类时,由于没有对loadClass加载的类做过滤导致报错,百思不得其解,随后采用frida进行hook也是报错。最后突然开窍尝试过滤才搞定。其次,吹爆unidbg,支付宝的libsgmain做的确实到位,直接破解很难搞定,但是利用该项目,我们可以不去分析加密的具体实现细节,只要将其当成一个黑盒进行直接调用即可。对于动态加载插件中的so库,可以手动放在apk的lib目录中进行加载。通过本次的分析,对android、frida以及unidbg有了更加深入的了解。但是也有问题没有解决,当参数为object[]{String[] str, String str1, int i, String str2}时,frida如何输出该参数的所有值?!需要进一步看看frida的接口深入分析一下frida才行。
github项目地址:https://github.com/WithHades/forest/