项目环境:
提示:不提供成品,不贴地址。
目标Apk:某通V2.2.12
Frida :12.8.0
其他工具:
NP管理器、JADX、Fiddler
正文:
一.分析&动态修改
1.抓包
使用Fiddler对目标进行抓包,发现请求体存在加密部分,以4kA
开头。
初步判断为base64加密。使用Frida对目标进行Hook。
1
2
3
4
5
6
7
8
9
10
11
12
|
var Base64Class
=
Java.use(
"android.util.Base64"
);
Base64Class.encodeToString.overload(
"[B"
,
"int"
).implementation
=
function(a,b){
var StrCls
=
Java.use(
'java.lang.String'
);
var OutStr
=
StrCls.$new(a);
console.log(
"OutStr:"
+
OutStr);
var re
=
this.encodeToString(a,b);
console.log(
">>> Base64: "
+
re);
return
re;
}
|
运行后,发现没有结果。说明该字段加密是另有方法。
没办法,直接Hook StringBuilder toString方法,通过字符串去定位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var strCls
=
Java.use(
"java.lang.StringBuilder"
);
strCls.toString.implementation
=
function(){
var result
=
this.toString();
if
(result.toString().indexOf(
"4kA"
) >
=
0
)
{
console.log(result.toString());
/
/
打印堆栈
var stack
=
threadinstance.currentThread().getStackTrace();
console.log(
"Re call stack:"
+
Where(stack));
}
return
result;
}
|
打印结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
4kA1n8n
-
W0cpJ_YTL3FUVz2lLz2PFHL7uDipswFoFrCTLqj0LeFPFHLBuDMX2r1pJpA3zpFPFrn
-
2eFonHiP20cPFrnq2rL
-
S0TpJ_Y0V_WPFHldSH2pJpAsweFPFHi7s8nq2pFoJw7puDq1F_I75eAkWDM1F_IpL3WgLzFp5eA
-
ueFoVzW0V_YTJz10Vw7pWDVpJ_FPFrQZnkFoFHLBSwMZu8fB5rn7SpFPFrL
-
nkFoF17x90uV41nMS8
-
fshnomDqH4qqr9_in9inTDDtHDclInqq
-
sDnbVqdE4Hnnn_VlzclnSDtD43iTzNdDViWxchlV8UuZ9rIlLUnoWUfEDhLTnqRGnclnJD9H4ri
-
nqjkDinoSzuD80UP9q1MshU_SDlD8Xiq9_i
-
nc
-
TDzuEV0LrwrIk9eAR
Re call stack:dalvik.system.VMStack.getThreadStackTrace(Native Method)
java.lang.Thread.getStackTrace(Thread.java:
1566
)
java.lang.StringBuilder.toString(Native Method)
i3.x.b(:
6
)
b8.a.b(:
8
)
z8.c.h(:
14
)
m8.d$r.onClick(:
2
)
android.view.View.performClick(View.java:
5637
)
android.view.View$PerformClick.run(View.java:
22429
)
android.os.Handler.handleCallback(Handler.java:
751
)
android.os.Handler.dispatchMessage(Handler.java:
95
)
android.os.Looper.loop(Looper.java:
154
)
android.app.ActivityThread.main(ActivityThread.java:
6121
)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:
889
)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:
779
)
|
定位到i3.x.b
,用Jadx反编译目标apk,搜索到该方法,可以发现入参是字符串,返回值是字符串。
用Frida objection插件,对i3.x.b
进行Hook,App发送加密请求则触发。
1
|
android hooking watch class_method i3.x.b
-
-
dump
-
args
-
-
dump
-
backtrace
-
-
dump
-
return
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
(agent) [
1e9vrk1qy8w
] Called i3.x.b(java.lang.String)
(agent) [
1e9vrk1qy8w
] Backtrace:
i3.x.b(Native Method)
b8.a.b(:
8
)
z8.c.h(:
14
)
m8.d$r.onClick(:
2
)
android.view.View.performClick(View.java:
5637
)
android.view.View$PerformClick.run(View.java:
22429
)
android.os.Handler.handleCallback(Handler.java:
751
)
android.os.Handler.dispatchMessage(Handler.java:
95
)
android.os.Looper.loop(Looper.java:
154
)
android.app.ActivityThread.main(ActivityThread.java:
6121
)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:
889
)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:
779
)
(agent) [
1e9vrk1qy8w
] Arguments i3.x.b({
"device"
:
18xxxxxx57
,
"cpuabi"
:
"x86_64"
,
"country"
:
"CN"
,
"vip"
:false,
"version"
:
1626
,
"lang"
:
"ZH"
,
"apiver"
:
9
,
"uid"
:
0
,
"rand"
:
"48155"
,
"it"
:
1662189961
,
"ac"
:
2
,
"pkg"
:
"com.xxxx.vpn"
,
"sig"
:
"L3ClLzcymzQhfzAifzYgV1YTVxYkfYLhfYiif_2XJzfYf31MLYmkVx1xLxV0V3PlL_WkVz1lVxWMJXcxf_neLY9efzqif_2XVzm7V_mlVY9hmcmlV_AeV1ieJxY7J3cgJz2T"
})
(agent) [
1e9vrk1qy8w
] Return Value:
4kA1n8n
-
W0cpJ_YTL3FUVz2lLz2PFHL7uDipswFoFrCTLqj0LeFPFHLBuDMX2r1pJpA3zpFPFrn
-
2eFonHiP20cPFrnq2rL
-
S0TpJ_Y0V_WPFHldSH2pJpAsweFPFHi7s8nq2pFoJw7puDq1F_I75eAkWDM1F_IpL3ClLzcp5eA
-
ueFoVzW0V_YTJz10Vw7pWDVpJ_FPFrQZnkFoFHLBSwMZu8fB5rn7SpFPFrL
-
nkFoF17xm0lV4HLMS8
-
fshnomDqH4qqr9_in9inTDDtHDclInqq
-
sDnbVqdE4Hnnn_VlzclnSDtD43iTzNdDViWxchlV8UuZ9rIlSinT9XUEDhLTnqRGnclnJD9H4ri
-
nqjkDinoSzuD80UP9q1MshU_SDlD8Xiq9_i
-
nc
-
TDzuEV0LrwrIk9eAR
|
可以发现与抓包结果相一致。
2.展示Vip列表
在上一步Hook i3.x.b
过程中时,是点击app刷新按钮。通过注册免费赠送的一天Vip,可以发现刷新按钮,是用来请求Vip服务器列表的。
通过抓包可以发现,请求Vip服务器列表,响应体中也是以4kA
开头,本地apk肯定需要解密它。
既然加密在i3.x.b
中,那么解密是不是也在i3.x
这个类呢?
直接用objection hook i3.x
。
1
|
android hooking watch
class
i3.x
|
1
2
3
|
(agent) [
1gv2xiqsb6h
] Called i3.x.b(java.lang.String)
(agent) [
1gv2xiqsb6h
] Called i3.x.b(java.lang.String)
(agent) [
1gv2xiqsb6h
] Called i3.x.a(java.lang.String)
|
发现除了调用了i3.x.b
,还调用了i3.x.a
。
那就hook i3.x.a
,看看它的参数值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
(agent) [e5k5hzxxs5v] Called i3.x.a(java.lang.String)
(agent) [e5k5hzxxs5v] Backtrace:
i3.x.a(Native Method)
b8.a.d()
b8.a$c.invoke(:
2
)
p7.a.f(:
3
)
p7.a.e(:
11
)
p7.a$c.run()
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:
1133
)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:
607
)
java.lang.Thread.run(Thread.java:
761
)
(agent) [e5k5hzxxs5v] Arguments i3.x.a(
4kAbWg9k2H9GuiR0n8
...[不展开了])
(agent) [e5k5hzxxs5v] Return Value: [
object
Object
]
|
可以发现参数值,与抓包结果相一致。
那么重点来了,在没有登录的情况下,通过将带有vip列表的返回值,修改为i3.x.a
的参数值,就可以实现展现vip列表了。
3.修改Vip天数
在展现Vip列表后,我们随机点击一个服务器,发现会自动跳转到登录页面,一定是在检测是否是Vip。
通过查找字符串资源,可以找到显示vip天数的字符串。
1
|
<string name
=
"iw"
>
%
d天<
/
string>
|
Jadx搜索R.string.iw
,可以发现该文本的%d
,取之于r8.b.a
这个方法。
通过对r8.b
这个类进行hook,可以发现r8.b.a
是返回vip天数,r8.b.b
是判断是否为vip。
然后用Frida,对r8.b.a
返回值修改为1,即可。
以上为动态修改方法。
二.静态修改
1.去签名校验
使用NP管理器的超强去除签名校验2.0
,即可去除校验。
这里有一个巨坑,在后面踩到了。原因是lib文件夹中会多出一个libfuck.so
文件,而发送获取vip服务器列表请求,会对lib文件夹的文件数量进行判断,导致请求一直发送不出去。
2.修改Vip天数
直接在r8.b.a
中的smali代码,返回值永远返回1,即可。
3.修改请求地址
因为新安装打开的App,没有Vip列表,获取Vip服务器列表,需要从对方服务器获取。所以将带有Vip列表的响应保存起来,自己写个接口,将请求发送给自己的服务器即可。
通过Hook字符串定位,找到原获取Vip服务器列表请求的Url。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var strCls
=
Java.use(
"java.lang.StringBuilder"
);
strCls.toString.implementation
=
function(){
var result
=
this.toString();
if
(result.toString().indexOf(
"get_config_v9"
) >
=
0
)
{
console.log(result.toString());
var stack
=
threadinstance.currentThread().getStackTrace();
console.log(
"Re call stack:"
+
Where(stack));
}
return
result;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
https:
/
/
xxxxx.xxxxxxxxxx.com
/
get_config_v9
Re call stack:dalvik.system.VMStack.getThreadStackTrace(Native Method)
java.lang.Thread.getStackTrace(Thread.java:
1566
)
java.lang.StringBuilder.toString(Native Method)
java.util.Formatter.toString(Formatter.java:
2326
)
java.lang.String.
format
(String.java:
2670
)
b8.a.b(:
2
)
z8.c.h(:
14
) a8.a.a .b
m8.d$r.onClick(:
2
)
android.view.View.performClick(View.java:
5637
)
android.view.View$PerformClick.run(View.java:
22429
)
android.os.Handler.handleCallback(Handler.java:
751
)
android.os.Handler.dispatchMessage(Handler.java:
95
)
android.os.Looper.loop(Looper.java:
154
)
android.app.ActivityThread.main(ActivityThread.java:
6121
)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:
889
)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:
779
)
|
发现该Url在b8.a.b
的String.format
方法中生成。
查看smali代码,将v1
的值改为修改的请求地址即可。
1
2
3
4
5
6
7
|
new
-
instance v7, Lp7
/
d;
const
-
string v1,
"http://xxxxxx.com/xxx"
invoke
-
direct {v7, v0, v1}, Lp7
/
d;
-
><init>(Ljava
/
lang
/
Class;Ljava
/
lang
/
String;)V
sget
-
object
v0, Lm7
/
a;
-
>d:Lm7
/
a;
|
按道理来说,应该就可以了,结果我在这里卡了很长时间,就是遇到了之前的那个坑,请求发不出去。
那就看看原请求是怎么发送出去的。翻到之前打印的堆栈。
1
2
3
4
5
6
7
8
9
10
|
https:
/
/
xxxxx.xxxxxxxx.com
/
get_config_v9
Rc Full call stack:dalvik.system.VMStack.getThreadStackTrace(Native Method)
java.lang.Thread.getStackTrace(Thread.java:
1566
)
java.lang.StringBuilder.toString(Native Method)
java.util.Formatter.toString(Formatter.java:
2326
)
java.lang.String.
format
(String.java:
2670
)
b8.a.b(:
2
)
z8.c.h(:
14
)
m8.d$r.onClick(:
2
)
...
|
通过一系列的hook发现,修改后的请求,可以触发z8.c.h
,却触发不到b8.a.b
,说明在z8.c.h
遇到了某条件判断。
Jadx没有反编译出来,可以通过NP管理器查看。
通过hook可以发现l7.d.f
是获取lib文件夹地址的。在下面的文件个数判断中,要将listFiles.length == 6
改为listFiles.length == 7
。
发现下面有一个熟悉的方法,r8.b.b
,这不是判断是否vip的方法嘛。
不管,先打包好,发送请求… … 失败… …
把!r8.b.b()
的smali改为r8.b.b()
,发现成功发送请求。
最终完成了apk的破解!
三.总结
破解过程中总感觉有点晕,但是最后写文章整理了一下,发现其实很简单… …
这可能就是当局者迷,破局者清吧。
最后感谢奋飞大佬的指点,还有大佬的文章~
期间遇到的其他问题:
1
2
3
|
Q1:破解了半天破不出来,换个版本?
A1:换个版本发现更晕。
|
后记
我真的很菜。学习Android逆向,看了一下IDM下载Frida和Jadx的时间,到破解完这个app,花了整整一个月。
很多知识其实并不完备。不过还是贴一下自己的学习过程,希望可以帮助到其他人。
1.:我大概过了一下,毕竟本身就是学计算机的,操作系统啥的要熟悉一丢丢
2.《Android软件安全权威指南》,非虫大佬的书:我也是大概过了一下,基本就是Android逆向的全面概括和简单实操。
3.:这个必看,大佬整理的很全,感谢大佬!
4.:非常方便的Frida Hook工具,减少很多代码量。
5.:分析 Java 类/对象结构,而且还能dump出实例中的各个属性值,6 。
6.公众号-奋飞安全 :奋飞大佬已经把sign解密玩出花了,实操文章很多,对我帮助很大,也很感谢有不懂的时候,问大佬,也一直回我。
7.抓包涉及的Fiddler、WireShark工具,还有SSL Pining等等。(一般我用两种方法,和,基本上就能过大部分抓包了,这两个都需要root,后者需要真机)
8.:我自己的博客,刚做,记录学习和一些实战。
基本上一个月就搞了这些,连Xposed也没装过,主要Frida比较适合我分析,又快又好。以后有时间可以去看看。