一个抽奖应用的逆向破解全流程

东窗事发

偶然见到一个应用内有抽奖的活动(应用具体名称就不便告知),而且是每天都可以抽。同时抽奖之前也不需要用户登录什么的,但限定了用户一天(自然天)只能抽奖一次。那么应用的服务端在用户没有登录的情况下是依据什么来判定当前用户今天是否已抽过奖了呢?这当中判断的依据是否可靠,能否被伪造然后实现一天多次抽奖呢?带着这些问题,让我们来剥开应用的层层外纱。

初识庐山

首先在PC端打开LogCat,然后手机连接PC,最后手机上打开该应用的抽奖界面。这时我们在LogCat中发现了该应用输出的如下日志:

image

里面有onPageStart并且包含了被加载网页的url。由此可以得出结论,抽奖页是用WebView加载网页实现的。那加载这个WebView的Activity又是什么呢?

在PC端打开Android DDMS,在进程列表中找到该应用包对应的进程(如下图的进程ID为21641),然后查看Activity Manager State。

image

这时我们得到的Activity Stack就像这样。

image

由此我们就可以大概知道它Java内部的实现结构是怎样的了。首先创建的是ActivityToolsExplorer,然后在其中嵌入了一个名为FragmentToolsExplorer的Fragment,最后由FragmentToolsExplorer在onCreate里实现WebView加载网页的过程。

顺水推舟

由上面我们知道了抽奖页的Url。好了,我们首先在浏览器上打开这个Url,看看它的抽奖是怎样一步步实现的。

这个页面打开后,在页面正中间有一个用来作抽奖操作的按钮,嵌套样式名为.lucky-btn .lucky-cell-conn。当然在浏览器上这个程序是运行不起来的,我们关心的只是开始抽奖这个动作是怎样发起的。查看源码打开main.js。js文件竟然没有压缩,就更方便我们逆向跟踪了。我们找到对这个按钮绑定点击事件的位置:

main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function init(info) {
...
var deviceID=info.deviceId;
checkDrawEnable(drawingObject,deviceID);
$(".lucky-btn .lucky-cell-conn").on("click",function(e){
...
window.setTimeout(function(){
..
getJSONP(BASE_URL+"draw/"+deviceID,function(data){
drawResult(drawingObject,data);
});
},Math.random()*1000+1000);
});
}

ok,在这里我们就直接发现了,服务端用来判断用户当天已抽奖的依据是DeviceId(设备ID)。现在我们需要的是一步步逆向跟踪,找到是谁调用了init函数并带入的info参数是怎样构成的,这样似乎整个程序逻辑就会变得清晰起来。

这个网页中bridge对象为java层注入的对象,而脚本中一些初始化信息也是通过调用java方法获得的。那么我们要跟踪整个代码流程,就必须对java代码也进行逆向分析。首先拿到应用的apk包,unzip后使用baksmali将classes.dex解为smali代码包。结合脚本代码,对代码调用层次一步步追踪,这个过程需要你对smali代码也比较熟悉。最后得到如下图所示的调用流程图(具体追踪过程就不在这展开):

image

由此,我们就知道deviceId串是主要就由Lcom/xx/util/e;->a(Landroid/content/Context;)Ljava/lang/String;方法负责生成。这个方法中获取到AndroidID与DeviceId后用”_”符连接。

Lcom/xx/util/e;->j(Landroid/content/Context;)Ljava/lang/String;就是获取DeviceId的方法,具体的smali代码为:

Lcom/xx/util/e;->j(Landroid/content/Context;)Ljava/lang/String;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.method public static j(Landroid/content/Context;)Ljava/lang/String;
.registers 2
const-string v0, "phone"
invoke-virtual {p0, v0}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Landroid/telephony/TelephonyManager;
invoke-virtual {v0}, Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;
move-result-object v0
return-object v0
.end method

至此找到获取DeviceId真正的位置,我们就破案了。

偷梁换柱

上面我们知道了,服务端用来判断用户当天已抽奖的依据是DeviceId,同时我们也追踪到了代码中获取DeviceId具体位置。这样,程序每次获取DeviceId时我们就可以伪造返回值(每次伪造的结果都不能相同),就实现一天无限次抽奖了。那怎样伪造这样的返回值呢,下面的简单一句Java代码就可以实现了。

1
return String.valueOf(System.currentTimeMillis() ^ 537400335373457L);

把上面这句翻译成smali代码后,替换掉j(Landroid/content/Context;)Ljava/lang/String;方法内原来的全部smali代码,偷梁换柱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.registers 9
invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
move-result-wide v0
const-wide v2, 0x1e8c344179491L
xor-long/2addr v0, v2
invoke-static {v0, v1}, Ljava/lang/String;->valueOf(J)Ljava/lang/String;
move-result-object v0
return-object v0

最后用smali.jar打包回去,签名后安装。运行起来后应用在抽奖页面已经没有了次数限制。

魔高一尺,道高一丈

学习破解的目的不是为了走更多的捷径。有个道理大家都知道,最好的防守就是进攻。我们只有了解怎样去破解一个应用,才知道应该如何去加固自己的应用。就比如这个应用中,有下面这些问题可以改进来加大各方面破解的难度:

  • Debug日志不应该存在于发布版本,Error日志也尽量少暴露敏感信息。
  • 线上网页的脚本代码及样式应该压缩,压缩后可以增加逆向时阅读及追踪上的难度。
  • 不能以客户端上任何固有参数作为服务端上的关键凭据,比如这里的抽奖凭据依赖了客户端的DeviceID。
  • Java代码混淆还是需要的,虽然代码仍可以被一步步逆向跟踪,但语义上的隔离会加大追踪的时间成本。

    上面内容如有侵权,请告知便作以删除!

欢迎转载,请注明出处链接!!!