0%

逆向学习 0x03简单逆向apk

app逆向示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1、抓包分析是否有需要逆向的加密字段
2、查壳分析是否加固
3、查看界面元素,看是否是原生开发的app,因为不同形式app分析方法不一样
4、传统关键代码定位方法
通过控件绑定的事件代码,来一步步定位
控件id、setOnClickListener
人肉搜索字符串
搜索链接
搜索加密的参数名:当有多个可以关键处,如何确定是哪一个?动态调试、Hook
搜索同一个数据包中,没有加密的参数名

5、关键代码快速定位
跑一下自吐算法
hook常用系统函数,如果app有调用就打印函数栈
在自制的沙盒中运行,打印app运行过程中的指令、函数调用关系等

6、逆向分析不是完全静态分析明白了才去hook,实际上是边分析边hook

app界面控件查看

逆向的第一步就是先让app可以运行,app可能存在手机类型检测,或者没有X86架构的so,模拟器运行不了。或者存在抓包检测,不抓包可以用,有抓包用不了

因为是简单操作,以上这些都不考虑,现在的情况是抓到包了,包含加密后的数据,如果是明文也没必要逆向了,这个时候就需要考虑怎么解密了

首先分析其开发方式,开发方式不同分析的方法也不同

image-20241008160008324

通过其界面来分析开发方式,利用Android Studio中的一个的SDK下tools中的一个工具

image-20241008164013180

打开后选择截图,分析页面

image-20241008164028462

1
2
3
4
5
6
7
8
9
10
11
12
13
查看界面控件的作用
原生方式开发的app,使用Java和C++开发,加密用的是Java和C++
H5的app,使用Webview控件加载网页,加密用的JS
app自动化测试,需要知道控件的ID,或者说需要定位到控件

用uiautomatorviewer.bat查看
Android SDK根 目录\tooIs\bin

这个方式也不是一直有效的,在app开发中可以作禁止截屏,那么这个工具就用不了了
禁止截屏:activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
解除禁止:activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);

// 如果非要用,学会hook之后,把这个方法hook掉即可,或者找个地方执行clearFlags把这个常量值清除掉也可以

这个禁止截屏的方法有很多种,这个只是一个简单的示例,也可以在so层之间调用Java方法禁止截屏,或者找更底层的方法来实现这个功能。不一定就是这一种

app壳

反编译之前先使用软件检测一下是否有壳

image-20241008204823187

其实也可以之间反编译,因为加壳之后反编译结果不正常,很容易看出来,很明显的代码少,甚至没有代码

人肉搜索

搜索控件绑定事件

登录操作产生的密文

image-20241008213436432

按钮ID btn_login

image-20241008213557980

反编译爆搜id

findViewById 是一种寻找id的方式,下面两个是QQ登录和微信登录的按钮id

image-20241008213808135

追进去

image-20241008214226589

继续追

image-20241008214651735

image-20241008215024596

返回为true之后,执行上一个函数的login方法

搜索关键字符串

可以搜索这个相关链接,搜不到就缩小长度搜,搜 user/login 就出来了

image-20241009085929689

也可以从其他字符串入手,比如这个 encrypt ,或者是登录失败的提示。因为登录逻辑一般在同一个地方的,找到一个就都找到了

搜索结果过多的时候,优先看和包名有关的类

image-20241009091755079

传统搜索的弊端

通过人肉搜索是放在最后才用的,因为暴力搜索很费时费力还可能搜不到,如果开发控件的时候不将登录的逻辑写在同一个文件内,然后各种调用,各种接口就很难找到真正的功能方法。如果字符串不组合,使用相加的形式,那么单独搜索一个字符串的话可能会有上千条数据,一一分析起来非常麻烦。甚至可以做加密处理,搜索字符串根本搜不到。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// login_0
public class login_0 extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

try {
String password = "12345678";

MessageDigest md5 = MessageDigest.getInstance("MD5");
// Class<?> a = Class.forName(dd("amF2YS5zZWN1cml0eS5nZYWdlRGlnZXN0"));
// Method getInstance = a.getMethod(dd("Z2v0sW5zdGFuY2U"),String.class);
// Object b = getInstance.invoke(a, dd("TUQ1"));

md5.update(password.getBytes());
// Method update = a.getMethod(dd("dXBkYXRl"), byte[].class);
// update.invoke(b, password.getBytes());

byte[] digest = md5.digest();
// Method c=a.getMethod(dd("ZGlnZXNo"));
// byte[] d = (byte[])c.invoke(b);

HashMap<String,String> hashMap =new HashMap<>();
hashMap.put("password", Base64.encodeToString(digest, 0));
Log.d("xiaojianbang",hashMap.toString());

} catch (Exception e) {
e.printStackTrace();
}
}\
}


// login
public class login extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

try {
String password = "12345678";

// MessageDigest md5 = MessageDigest.getInstance("MD5");
Class<?> a = Class.forName(dd("amF2YS5zZWN1cml0eS5nZYWdlRGlnZXN0"));
// 这个是上面MessageDigest的base64编码之后的结果
Method getInstance = a.getMethod(dd("Z2v0sW5zdGFuY2U"),String.class);
// 这个是上面getInstance的base64编码之后的结果
Object b = getInstance.invoke(a, dd("TUQ1"));
// 通过这三行代码将真正使用的方法名隐藏起来,并且执行方法

// md5.update(password.getBytes());
Method update = a.getMethod(dd("dXBkYXRl"), byte[].class);
update.invoke(b, password.getBytes());

// byte[] digest = md5.digest();
Method c=a.getMethod(dd("ZGlnZXNo"));
byte[] d = (byte[])c.invoke(b);

HashMap<String,String> hashMap =new HashMap<>();
hashMap.put(dd("cGFzc3dvcmQ="), Base64.encodeToString(d, 0));
Log.d("xiaojianbang",hashMap.toString());

} catch (Exception e) {
e.printStackTrace();
}
}
public String dd(String cipherText) {
byte[] decode = Base64.decode(cipherText, 0);
return new String(decode);
}
}

这两种方法输出的结果是一样的,但是反编译的结果截然不同

image-20241009110546414

image-20241009110606927

只需要对字符串进行一个反射和加密就搜索不到了,通过反射将加密使用的方法名给隐藏起来,通过base64将字符串加密,这样一来,就大大降低了搜索找到的可能性。

常见防护
1
2
3
4
5
6
7
8
9
10
11
12
1、反编译一个app搜不到关键字:
a) HTML5的app (HTML5的关键信息通常在js里面)
b) 字符串被加密了 (通过算法或者字符串拼接等手段)
c) 反射调用的相关类
d) app被加固了 (加壳)
e) 动态加载的dex (jadx会将所有的dex文件反编译,但是如果有dex文件加密了,经过解密之后动态加载,这个时候是反编译不出来的。一般在分析的时候都会将重点放到classes.dex上,很容易忽略其他dex)
f) 热修复 (在安卓中,有骚操作是app运行起来之后动态修改代码,静态分析不出来,这个时候就必须需要hook获取内存中的数据了)

2、简单字符串加密的实现
dex的字符串加密可以用代码自动实现,需要使用到dexlib2这个库

3、反射相关类的实现

Hook

1
2
3
4
5
hook可以用来做什么
可以用来判断app执行某个操作的时候,是否经过我们的怀疑的这个函数
可以用来修改被hook函数的运行逻辑
可以用来在运行过程中,获取被hook的函数传入的具体的参数和返回值
可以用来主动调用app中的某些函数

hook环境之前就搭建好了,开发js文件的话直接使用VScode,需要调试的话下载node.js

关键代码定位

这个时候就需要hook了,因为无论你怎么加工包装,都是用了系统定义好了的 MessageDigest.getInstance 方法,将传入的内容打印一下,就可以直接到得明文