小白的《宾果消消消》某定制版逆向笔记
本帖最后由 winding 于 2018-3-17 17:58 编辑https://static.52pojie.cn/static/image/hrline/1.gif
这是来论坛的第2篇实践笔记主题帖,记录学习过程中的一些零散想法。有感身为小白,学习过程中经常遇到困惑的经历,尽量把自己遇到的问题以及尝试的思路办法说详细一些,所以可能啰嗦琐碎,不成体系。涉及的内容也主要是smali的静态分析,有一点log的内容,初始入门级别,没有技术含量,写出来一为自己记录,二与众多小白共勉。作为小白,涉及的一些内容猜测居多,有的可能说得不对,请自行甄别查证;如有牛牛路过欢迎斧正。
https://static.52pojie.cn/static/image/hrline/1.gifhttps://static.52pojie.cn/static/image/hrline/1.gifhttps://static.52pojie.cn/static/image/hrline/1.gif
这次的样本是《宾果消消消》OPPO定制版V5.2.0版本。为毛用定制版?第一次下载下来是这个版本,就它了,没挑剔;也想通过与原版的对比多琢磨点东西。论坛上有很多《宾果消消消》逆向成品,倒还没有贴过程的,就把这次学习的过程贴出来了。
https://static.52pojie.cn/static/image/hrline/1.gifhttps://static.52pojie.cn/static/image/hrline/1.gifhttps://static.52pojie.cn/static/image/hrline/1.gif
相关链接 链接:https://pan.baidu.com/s/1O5iq93pS60qyZRiljLJlmQ 密码:j145 只放了原版样本,成品没放,其实只改了com.nearme.game.sdk.a$8.handleMessage一处,自己改吧;injectlog工具放进去了,原作者不是偶。
一、初识
先扔模拟器和andriodkiller里跑一圈,掌握基本信息。
样本未加壳,有混淆;cocos2工程,lib文件夹下有libBugly.so和libcocos2dlua.so文件,未发现大量lua脚本;assets文件夹下内容丰富,其中assets\china和assets\res下有大量未加密的图片和.json脚本文件,感觉可以自己diy一个版本了,assets\nearme下有oppo_game_service_200604.so文件。so文件就3个,jar文件没有(用windows的文件夹搜索后缀名)。
好像第一次有开屏广告,基本不出现;有内购,微信和支付宝两个渠道;app自带一些LOG信息。
隐藏了一个活动com.mfp.jelly.oppo.wxapi.WXPayEntryActivity,灰色的,不知道藏哪里了。
这里就遇到了小白经常疑惑的第一个问题。疑惑一:灰色的、打不开的那些活动,哪里去了?我也很疑惑,下文讨论。
https://static.52pojie.cn/static/image/hrline/1.gifhttps://static.52pojie.cn/static/image/hrline/1.gifhttps://static.52pojie.cn/static/image/hrline/1.gif
二、动手
(一)去广告
这个版本的开屏广告很好说话,基本不出现。。。都没有逆向的必要。但既然有,还是看一看,大体说下。
程序入口是OppoAdSplashActivity,看名字就在这里了。尝试过一些app,总的来说,去除开屏广告很简单的,基本都在application类或者入口类启动。换位思考,程序猿设计时考虑的都是自己的程序主体,开屏广告大多是在程序主体完工后,后期加上的,与程序主体衔接多数不紧密。对于一些小型的程序,甚至直接把程序入口从原来的入口改成程序主界面即可,不影响程序运转;大型程序设计比较严密,会在入口类初始化主界面的一些东西,或者加了检测,就需要分析修改了。
用《当前activity》这个小程序观察启动过程,发现还经过了不少跳转才到真正的主界面jellyactivity。大型软件需要初始化的东西多,就不尝试直接改入口了,肯定是失败的,从OppoAdSplashActivity开始分析java代码。
因为插入的这些开屏广告,无论如何,肯定都有启动主界面和关闭自身的动作,分析的时候关注startActivity和finish这两个函数(以及类似函数)以及oncreate函数,很简单就能梳理出基本流程。如果不行,再回过头去找application类。
通过startActivity(启动其他活动的)和finish(结束自身的)两个函数定位,可以大体看出样本的启动流程,OppoAdSplashActivity.oncreate>3处startActivity>AppSplashActivity.oncreate>JellyBaseSplashActivity.oncreate>JellyBaseSplashActivity.startGameActivity启动主界面。其中AppSplashActivity.oncreate中是通过调用父类super。。。的方式启动basesplash的。
关键流程是这样,细节的就可以逐一确定了,这里就不详细分析了。修改的话,老办法修改跳转;或者直接添加对startActivity和finish两个函数的调用,如可以直接把OppoAdSplashActivity中next方法中startActivity和finish的smali语句,直接复制到OppoAdSplashActivity的oncreate方法里。需要注意的问题就是插入代码的位置,该做的初始化还得让它做了。因为样本老是不出广告,就不试验贴图了。
https://static.52pojie.cn/static/image/hrline/5.gifhttps://static.52pojie.cn/static/image/hrline/5.gifhttps://static.52pojie.cn/static/image/hrline/5.gif
(二)破解内购
因为看到有支付宝渠道,就从这里入手。
尝试1:上来三板斧,9000,0x2328,onbilling等关键字定位。
支付宝最典型特征找到一处,在Lcom/microfun/onesdk/purchase/c;中
改成这样试试
.sparse-switch
0x1771 -> :sswitch_0
0x1f40 -> :sswitch_0
0x2328 -> :sswitch_0
试验无果,失败了。这里遇到小白经常疑惑的第二个问题。疑惑二:为什么照着教程做的修改不好用?为什么同样的方法,有的时候好用有的时候不好用?
为什么会失败呢?就这次尝试而言,我们并没有分析具体逻辑。看一下关键的java代码:
l();
localPurchaseResult.setState(PurchaseState.Success);
continue;
localPurchaseResult.setState(PurchaseState.Purchasing);
continue;
localPurchaseResult.setState(PurchaseState.Cancel);
可以看到,成功失败和取消3种情况,只是设定了PurchaseResult的支付状态字段,没有任何增加物品的实际操作;成功里还额外执行了 l(),简单追一下代码
protected void l()
{
a("", "CNY", this.m);
}
protected void a(String paramString1, String paramString2, String paramString3)
{
JSONObject localJSONObject = new JSONObject();
try
{
localJSONObject.put("userid", this.n);
localJSONObject.put("level", this.o);
localJSONObject.put("branch", this.p);
localJSONObject.put("currency", paramString2);
localJSONObject.put("revenue", paramString3);
localJSONObject.put("amount", 1);
localJSONObject.put("order_id", this.j);
localJSONObject.put("platform_order_id", paramString1);
localJSONObject.put("purchase_channel", this.g.getChannel());
localJSONObject.put("itemid", this.h);
BIManager.getInstance().recordPurchaseLog(localJSONObject);
return;
}
catch (JSONException paramString1)
{
for (;;)
{
paramString1.printStackTrace();
}
}
看到是构建json对象,然后形成交易记录。那么我们修改的这个地方,并不是最终增加物品的位置,只是中间一个环节,还不一定是必经的环节。
疑惑二解答(讨论):
修改失败有几种可能性。
(1)是不是阻断了程序的原有逻辑。本例来说,只修改了state一个值,我们知道支付宝接口信息传递中还有状态码,还有状态字符串,我们只修改了state,如果支付失败的状态码和状态字符串都照常传递下去,那么的确有可能失败。解决的办法就是顺着本例中锁定的位置,继续分析上下文联系,比如可以发现与Lcom/microfun/onesdk/purchase/c;同目录下还有PurchaseState、PurchaseResult、PurchaseListener等等文件,可以看一下有没有状态码状态字符串,一并改了。这是第一个想到的,真这样做的话也是最累的,因为还有其他可能。
(2)程序是不是只有一种支付逻辑。对于小程序来说,一个渠道一般只有一种逻辑;但大型程序很可能不止一种。原因,一个是加了混淆,加了好多无用的支付代码和逻辑,实际起作用的只有一条路径;另一个是程序正常开发中,难免集成好多sdk,里面都有某渠道(如这里是支付宝)的支付逻辑。本例是oppo定制的,那么程序原来肯定有一种支付逻辑,还有可能加入oppo定制的逻辑,一些统计用的sdk如U盟也同时集成支付逻辑。所以我们需要先判断,我们修改的地方,是不是程序真正执行的那条逻辑,如果不是,累死也白搭。
(3)支付渠道有没有搞错。这个如果搞错了,撞墙吧。。。
对于本例如何解决呢?我们倒着分析,(3)没问题,看(2)。查一下LOG。
看到有一个o_a::支付回调游戏SDK | GC201803170930493800100190000 ,是判断完支付结果回调,后面GC。。。。应该是订单号。这条LOG前(图片上部)、后(图片下部)各有一条LOG,里面regcode=1004,regmsg=支付失败。想到两点,一是我们修改的state不是那么关键,不是关键信息点,二是我们锁定的支付逻辑,很大可能并不是执行的这一条,我们用的特征码是9000。
那么我们需要确定真正执行的支付逻辑。第一个想到的是特征码1004搜索,估计很多,不去试了;第二个想到的是,有好多类似o_a::等的输出LOG,通过LOG关键字不就可以确定关键位置了么?搜一下LOG的位置,没找到。
又一个疑惑冒出来了。疑惑三:为什么看到有LOG输出,在工程里搜不到呢?
尝试2:疑惑先放着,先解决眼前的问题。怎么确定真正的逻辑呢?这么大的游戏,找了找各种关键字一大堆,一点点分析得累死,决定用最流氓、最一劳永逸的办法,把程序所有执行的方法用LOG打印出来!
具体办法借用的这篇帖子里的http://blog.csdn.net/charlessimonyi/article/details/52027563Android应用逆向——分析反编译代码之大神器 作者:charlessimonyi。大体思路是插入一个smali文件,用python遍历所有的smali文件,插入对它的调用,把所有调用它、也就是执行过的方法名打印出来。具体思路请看原贴,实践操作里有点坑,文后介绍。
操作中,我们的目标是把所有执行过的方法打印出来,所有没打印出来的方法,我们能确定它没执行过!
执行一圈遛一遛。。。
找到上次发现的那个回调的地方,上图显示的回调前的信息,我们从回调处往下分析
o_a::支付回调游戏SDK | GC201803171120128420100140000
onEventInTime error is 0
onEventInTime error is 0
notifyGameSdkPayResult::notify game
com.nearme.plugin.framework.LogUtils.log(LogUtils.java)
com.nearme.plugin.framework.LogUtils.log(LogUtils.java)
com.nearme.game.sdk.common.model.biz.ReportParam.<init>(ReportParam.java)
com.nearme.game.sdk.a$8.handleMessage(GCInternal.java)
com.nearme.game.sdk.common.util.LongSparseArray.get(LongSparseArray.java)
com.nearme.game.sdk.common.util.LongSparseArray.get(LongSparseArray.java)
com.nearme.game.sdk.common.util.LongSparseArray$ContainerHelpers.binarySearch(LongSparseArray.java)
com.nearme.game.sdk.GCInternalImpl$3.onFailure(GCInternalImpl.java)
com.microfun.onesdk.purchase.q$1.onFailure(Unknown Source)
很容易锁定com.nearme.game.sdk.a$8.handleMessage就是处理支付组件返回信息的最初关键点。这里是JAVA层接收支付组件返回信息的最初连接点,处理这里的话之后再复杂的逻辑也不用分析,不用去管它了。(为什么这么断定,因为所有在smali文件夹下的smali文件我们都插入了LOG,而回调LOG的TAG不是我们的TAG:injectlog,所以这里就是支付结果返回smali文件夹的最初入口)如果支付失败,分别调用com.nearme.game.sdk.GCInternalImpl$3.onFailure和com.microfun.onesdk.purchase.q$1.onFailure两个支付失败的方法。看a$8.handleMessage的内容
看到有ApiResult;和ApiCallback;两个接口,这个是消息处理类,是一定有的。显眼的一个判断语句,1001与ApiResult实例resultCode的比较。到ApiResult中查一下,
1001代表成功。那么逻辑就很清楚了,如果ApiResult实例的resultCode不等于1001,支付失败,执行paramMessage.onFailure(localApiResult.resultMsg, localApiResult.resultCode);传达失败消息,2个参数是resultMsg和resultCode。如果等于,执行paramMessage.onSuccess(localApiResult.resultMsg);传递成功消息,参数resultMsg。看一下com.nearme.game.sdk.GCInternalImpl$3.onFailure和com.microfun.onesdk.purchase.q$1.onFailure,的确同时也有onsuccess方法,参数也对,o了。
那么动手修改smali
找到关键代码
.line 442
const/16 v2, 0x3e9
iget v3, v0, Lcom/nearme/game/sdk/common/model/ApiResult;->resultCode:I
if-ne v2, v3, :cond_3
.line 443
iget-object v2, v0, Lcom/nearme/game/sdk/common/model/ApiResult;->resultMsg:Ljava/lang/String;
invoke-interface {v1, v2}, Lcom/nearme/game/sdk/callback/ApiCallback;->onSuccess(Ljava/lang/String;)V
这段代码之前还有一大段是判断信息有效性的,之前的LOG里我们看到已经传入信息了,所以前面这段不用考虑,有效性是没问题的。动手改的话,一个要改if-ne v2, v3, :cond_3,直接删掉就可以,另一个要给V2赋值。这里V2的值必须改,因为在前面的LOG里我们看到retmsg=支付取消(前边贴图里有),直接把这个值赋值给onSuccess肯定得出毛病。
resultMsg的类型是Ljava/lang/String;是字符对象,onSuccess(Ljava/lang/String;)的参数也是字符对象。怎么改这个参数偶还卡了一阵子,经人提示,琢磨了一阵子,琢磨出怎么改了,改成下面的样子:
.line 442
iget v3, v0, Lcom/nearme/game/sdk/common/model/ApiResult;->resultCode:I
new-instance v2, Ljava/lang/StringBuilder;
const-string v4, "\u652f\u4ed8\u6210\u529f"
invoke-direct {v2, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
.line 443
#iget-object v2, v0, Lcom/nearme/game/sdk/common/model/ApiResult;->resultMsg:Ljava/lang/String;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v2
invoke-interface {v1, v2}, Lcom/nearme/game/sdk/callback/ApiCallback;->onSuccess(Ljava/lang/String;)V
iget v3, v0, Lcom/nearme/game/sdk/common/model/ApiResult;->resultCode:I这句留着,没仔细看别的地方用不用这个值,取不到的话怕出错;v2不用给他赋值比较了,用这个寄存器新建一个字符串实例new-instance v2, Ljava/lang/StringBuilder;,增加一个v4寄存器,赋值为字符串“支付成功”,对应支付失败猜的,然后把字符串实例初始化,然后调用toString()方法返回Ljava/lang/String;的类型,最后传给onSuccess
我们增加了一个寄存器V4,所以别忘了改方法的声明
.locals 4改成
.locals 5
跑一圈测试一下
成功了,嘻嘻,出名游戏也不是这么可怕么,嘿嘿。看成功的LOG
果然走向成功了。其实试一下,微信支付也同时破解了,那么我们就明白了,为什么传递参数不是CODE,而是“支付成功”字符串,为什么要同时传递给两个onSuccess或是两个onFailure,大概。。。吧
另外,无论成功还是失败的LOG里,都没有发现一开始定位的那个b方法,所以我们改它没用。
(三)存档问题的尝试
本来到这里就结束了,结果找朋友真机测试悲剧了。因为发现这个游戏卸载掉重装的时候,不绑定账号,也会提示已经存在游戏数据,是否恢复(包括关卡、金砖等等),就放心干了。朋友的已经玩到700多关了,拿来测试了一下,结果有个检测手机权限的提示被我否了,结果。。。破解是测试成功了,但是700多关的记录没了,哭死。
没办法,硬着头皮试试能不能改存档吧,寄希望于关卡信息不敏感,没加密。
以下是分析的过程。(1)卸载掉重装提示有数据,说明是网络存档或者本地游戏文件夹以外的地方有记录的存档,游戏卸掉了,存档还在,各种折腾,没找到;(2)拔掉网线,郁闷,不能恢复数据,看来是网络存档,可是抓包实在没发现啥线索,只有一丁点加密数据有可疑,但是不懂加密,无果;(3)尝试利用LOG锁定游戏第一次启动时恢复数据提示的位置,无奈这个时候程序需要复制各类文件,操作太多,COCOS又不懂,放弃;(4)尝试修改现有存档,位置在/data/data/cmo.mfp.jelly.oppo/files下面(咋发现的?逐一找的,db、xml、dat以及其他能用文本文件打开的,都不放过),在断网的情况下进行游戏,会产生bi_cache_play.dat、bi_cache_tutorial.dat、cache.dat以及其他几个dat文件。前两个是记录断网时玩过的关卡记录,包括关卡号、分数等信息。联网后部分文件被删除。尝试修改,不成功,考虑是cache.dat和其他几个dat文件里还有相应记录,但是这几个文件乱码,不知道是什么格式的,放弃了。cache.dat文件的乱码,看着跟AndroidManifest.xml文件很像,无奈AndroidManifest.xml格式不懂。
最后分析,应该是以手机iemi为标识,在服务器存档。
如果有牛牛路过,能指点一下破解存档,感激不尽。
最后实在没办法,想起来assets\res下有很多脚本,改关卡吧。assets\res\Level文件夹下都是各关卡的配置文件,其中map001.json到map3110.json是普通地图,还有其他特殊地图。一开始就看到了,是明文的,随便打开一个:
(好像超长了,二楼继续) 笔记写的很多,没耐心看,况且也不懂编程,看起来很无趣。 昨夜星辰2012 发表于 2018-4-2 13:54
能详细介绍下打印LOG的代码具体怎样使用吗?
1.具体实现的原理,可以看偶贴出来的那个大神的博客原贴。
2.实际使用,偶已经做了简化,做成了一个批处理,应该是比较简单了。
具体步骤:
a.安装python3及以上版本,并添加系统变量,具体操作请百度。检验:在cmd输入“python”,可以成功显示python版本进入python命令行。
b.把偶提供的InjectLog.bat、InjectLog.smali、smalihook.py三个文件,放到andriodkiller根目录,把InjectLog.bat设置成andriodkiller自定义工具。
完成。使用的时候,在andriodkiller自定义工具启动工具,按提示操作即可。
本帖最后由 winding 于 2018-3-17 17:08 编辑
(接一楼)
随便打开一个看看
{
"version":27,
"tile":[["11","11","11","0","0","0","11","11","11"],
["11","0","0","0","0","0","0","0","11"],
["11","0","0","0","0","0","0","0","11"],
["0","0","0","11","0","11","0","0","0"],
["0","0","0","0","0","0","0","0","0"],
["0","0","0","11","0","11","0","0","0"],
["11","0","0","0","0","0","0","0","11"],
["11","0","0","0","0","0","0","0","11"],
["11","11","11","0","0","0","11","11","11"]],
"items":[["0","0","0","0","0","0","0","0","0"],
["0","0","0","0","0","0","0","0","0"],
["0","0","0","0","0","0","0","0","0"],
["0","0","0","0","22","0","0","0","0"],
["0","0","0","22","23","22","0","0","0"],
["0","0","0","0","22","0","0","0","0"],
["0","0","0","0","0","0","0","0","0"],
["0","0","0","0","0","0","0","0","0"],
["0","0","0","0","0","0","0","0","0"]],
"clay":[["0","0","0","2","2","2","0","0","0"],
["0","2","2","2","2","2","2","2","0"],
["0","2","2","2","2","2","2","2","0"],
["2","2","2","0","2","0","2","2","2"],
["2","2","2","2","2","2","2","2","2"],
["2","2","2","0","2","0","2","2","2"],
["0","2","2","2","2","2","2","2","0"],
["0","2","2","2","2","2","2","2","0"],
["0","0","0","2","2","2","0","0","0"]],
"stepLimit":"26",
"targets":[{"type":"3","code":"1","num":"57"}],
"ruleDefinitions":
{
"Initialization":[{"code":"1","weight":"10"},{"code":"501","weight":"5"},{"code":"4","weight":"10"},
{"code":"5","weight":"10"},{"code":"6","weight":"10"}],
"Rule1":[{"code":"1","weight":"10"}],
"Rule2":[{"code":"1","weight":"10"},{"code":"2","weight":"10"},{"code":"3","weight":"10"},
{"code":"5","weight":"10"},{"code":"6","weight":"10"}],
"SweetTimeRule":[{"code":"2","weight":"10"},{"code":"3","weight":"10"},{"code":"1","weight":"10"},
{"code":"4","weight":"10"},{"code":"5","weight":"10"},{"code":"6","weight":"10"}]
},
"spawnTiles":
[
{"x":"0","y":"3","rule":"Initialization"},{"x":"1","y":"1","rule":"Initialization"},{"x":"2","y":"1","rule":"Initialization"},
{"x":"3","y":"0","rule":"Initialization"},{"x":"4","y":"0","rule":"Initialization"},{"x":"5","y":"0","rule":"Initialization"},
{"x":"6","y":"1","rule":"Initialization"},{"x":"7","y":"1","rule":"Initialization"},{"x":"8","y":"3","rule":"Initialization"}
],
"playMode":"0",
"hitPoints":"30",
"starScoresNew":["29720","40407","43880"],
"restStepScoreNew":"3000"
}
"playMode"是游戏模式,0的代表普通消除关卡,"targets":[{"type":"3","code":"1","num":"57"}],这个是目标,正则替换就好了,保留一种类型,数量num=1,或者直接num=0。playMode=1是打到女巫模式,"witchHitPoints":"120",是女巫血量(这时targets表示会对女巫产生打击的种类和点数),改成0。好了,打开即通关,感觉偶已经无敌了。。。。吼吼吼。。。。朋友还需要把前700关逐一点开一遍。。。咳咳咳
(四)账号绑定
游戏里有绑定微信的功能,点击提示只有oppo游戏中心版本可以绑定,可能有检测。试验的时候,先绑定正版,卸载正版再绑定逆向版,竟然成功了,顿时没有逆向的欲望了。BABY,拿着破解版去朋友圈争锋吧,你是无敌的,嘿嘿嘿。
三、扩展
(一)未回答的疑惑
疑惑一的解答(讨论):androidkiller反编出来活动灰色点不开,几种情况。(1)加壳了,先检查有无壳;(2)多个dex,灰色点不开的到第二个第三个smali文件夹里找找;(3)在jar包里,曾经碰到过;(4)在伪装的jar、so文件里,后缀名不一定准确,有时候故意改了;(5)其他地方,本例中的在哪里偶也没找到,或者用文件管理器到模拟器安装目录下找找?
疑惑三的解答(讨论):有时候app保留的LOG,都在一些包里,跟疑惑二类似。本例中,类似o_a等的LOG输出,有时跟nearme牵扯着,可搜不到,nearme包下也没有。想起来assets有个nearme文件夹,下边有个oppo_game_service_200604.so。拖到ida里,竟然直接弹出这个
分明是个apk文件,改文件名,拖进andiodkiller里
果然在这里。那么,这里也可以搞事情了,嘿嘿,我就不测试了。
(二)关于injectlog的使用
强调一下偶是借用,这是别人的成果,原贴在一楼。偶主要和同是小白的坛友们说一下一些坑和改进。
A.首先,需要自己编译和反编,得到InjectLog.smali。原贴没提供,这个是偶反编的。
.class public Lcom/hook/testsmali/InjectLog;
.super Ljava/lang/Object;
.source "InjectLog.java"
# direct methods
.method public constructor <init>()V
.locals 0
.prologue
.line 3
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static PrintFunc()V
.locals 6
.prologue
.line 7
invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread;
move-result-object v0
.line 8
.local v0, "cur_thread":Ljava/lang/Thread;
invoke-virtual {v0}, Ljava/lang/Thread;->getStackTrace()[Ljava/lang/StackTraceElement;
move-result-object v1
.line 9
.local v1, "stack":[Ljava/lang/StackTraceElement;
const-string v2, "InjectLog"
new-instance v3, Ljava/lang/StringBuilder;
invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V
const/4 v4, 0x3
aget-object v4, v1, v4
invoke-virtual {v4}, Ljava/lang/StackTraceElement;->toString()Ljava/lang/String;
move-result-object v4
invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v3
const-string v4, "["
invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v3
invoke-virtual {v0}, Ljava/lang/Thread;->getId()J
move-result-wide v4
invoke-virtual {v3, v4, v5}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder;
move-result-object v3
const-string v4, "]"
invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v3
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
invoke-static {v2, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 10
return-void
.end method
B.用python把对以上print方法的调用插进每个smali方法中,原贴作者提供了py代码,原代码偶就不贴了。里面几个坑:python要用3.0以上版本,不然clear()方法不支持;编码统一为UTF8,并去掉汉字,不懂编码咋这么乱;如果自己修改py文件,不要用tab键,用空格;原作者代码中遇到没有.prologue的错误处理,会中断执行,把错误处理注释掉。
C.把InjectLog.smali文件保存到相应位置。
D.每次操作都麻烦,偶凑了个批处理,作为androidkiller的自定义工具。
@echo off
color 0A
echo ===========injectlog===========
:start
cls
set current_dir=%~dp0
pushd %current_dir%
:start
echo.请输入要注入LOG的逆向工程名称(对于ak,是apk 的文件名)
set /p inputgc=
if not exist .\projects\%inputgc%\ (
echo "工程文件夹不存在"
goto start
)
xcopy smalihook.py /y .\projects\%inputgc%\Project\smali\
set do_dir=.\projects\%inputgc%\Project\smali\
pushd %do_dir%
call python.exe smalihook.py
del smalihook.py
pushd %current_dir%
xcopy InjectLog.smali /y .\projects\%inputgc%\Project\smali\com\hook\testsmali\
if exist .\projects\%inputgc%\Project\smali_classes2\ (
echo "存在dex2,继续处理"
rem goto starttwo
) else (
goto done
)
:done
echo "已处理完毕"
pause
exit
:starttwo
xcopy smalihook.py /y .\projects\%inputgc%\Project\smali_classes2\
set did_dir=.\projects\%inputgc%\Project\smali_classes2\
pushd %did_dir%
call python.exe smalihook.py
del smalihook.py
pushd %current_dir%
xcopy InjectLog.smali /y .\projects\%inputgc%\Project\smali_classes2\com\hook\testsmali\
goto done
是个半成品,对多dex的处理没弄完。
E.原作者的py脚本实际使用的时候还有些问题,做了一些改进。
import os
class ParserError(Exception):
pass
#
def inject_code_to_method_section(method_section):
#
if method_section.find("static constructor") != -1:
return method_section
#
if method_section.find("synthetic") != -1:
return method_section
#
if method_section.find("abstract") != -1:
return method_section
#
inject_code = [
'\n',
' invoke-static {}, Lcom/hook/testsmali/InjectLog;->PrintFunc()V\n',
'\n'
]
#
for i in range(0, len(method_section)):
if method_section.find(".prologue") != -1:
method_section = inject_code
return method_section
#
for ii in range(0, len(method_section)-1):
if len(method_section.strip())!= 0 and len(method_section.strip()) == 0 :
method_section = inject_code
return method_section
return method_section
def inject_log_code(content):
new_content = []
method_section = []
is_method_begin = False
for line in content:
if line[:7] == ".method":
is_method_begin = True
method_section.append(line)
continue
if is_method_begin:
method_section.append(line)
else:
new_content.append(line)
if line[:11] == ".end method":
if not is_method_begin:
raise ParserError(".method error")
is_method_begin = False
new_method_section = inject_code_to_method_section(method_section)
new_content.extend(new_method_section)
method_section.clear()
return new_content
def main():
walker = os.walk("./")
for root, directory, files in walker:
for file_name in files:
if file_name[-6:] != ".smali" or file_name[:5] == "Cocos":
continue
file_path = root + "/" + file_name
print(file_path)
file = open(file_path,'r',encoding='UTF-8')
lines = file.readlines()
file.close()
new_code = inject_log_code(lines)
file = open(file_path, "w")
file.writelines(new_code)
file.close()
if __name__ == '__main__':
main()
主要是1.跳过cocos2文件,应该这些东西即使不做任何操作,也经常调用,LOG会晃眼;2.脚本有的app会做处理,没有.prologue,本例就是这样,这种情况下会原作者的py脚本会跳过去,那么这个方法是否执行就不知道了,如果不能保证100%知道程序执行逻辑,这个神器的作用就打折扣了。偶改了下逻辑判断,如果存在.prologue就插在.prologue后第一行,如果不存在.prologue,就从方法开始,插到第一个空行。smali文件中方法的头部信息,和正式代码之间,都存在一个空行,嘿嘿。
占楼等更新~ 好复杂啊,感谢楼主分享~ 如果有牛牛路过,能指点一下破解存档,感激不尽。 感谢分享,抽空细细读一下! 楼主好6,过来学习下 学习了~{:1_927:}{:1_927:}{:1_927:} 谢谢楼主分享经验!