安卓逆向入门-安全基线

安卓OWASP10

组件安全

Android有Activity、Service、ContentProvider、Broadcast Receiver、Indent五大组件,不需要进行跨应用运行的组件需要在AndroidManifest.xml中将属性android:exported设为false,否则该组件可被任意应用启动,被恶意调用。实在需要设为true的组件应用android:permission指定自定义权限,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<permission
android:name="example.permission.USESERVICE"
android:protectionLevel="normal"
/>
<service
android:name="com.example.haohai.helloWorldService"
android:exported="true"
android:label="@string/app_name"
android:permission="example.permission.USESERVICE"
</service>

<!--需要调用HelloWorldActivity的需要声明权限 -->
<uses-permission
android:name="example.permission.USESERVICE"
/>

ContentProvider为外部提供统一数据存储和读取接口,赢对其设置的腹泻访问控制权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 针对APK某文件添加读取权限 -->
<provider
android:name="com.example.haohai.HelloWorldProvider"
android:authorities="com.example.haohai.HelloWorldProvider">
<path-permission
android:pathPattern="/Apk/.*"
android:readPermission="com.example.haohai.permission.READ"
android:protectionLevel="normal"
/>
</provider>

<!--设置全局刻度权限-->
<provider
android:name="com.example.haohai.HelloWorldProvider"
android:authorities="com.example.haohai.HelloWorldProvider"
android:readPermission="com.example.haohai.permission.READ">
</provider>

Intent进行组件间跳转有两种方法,一种显式调用,即通过指定组件名,用setComponentsetClassNamesetClassnew Intent(A.class,b.class)等。另一种是隐式调用,配置文件中设置Intent Filter,系统根据action、category、数据等隐式意图进行组件跳转,可能存贷判断失误而导致数据泄露。显式调用方法:

1
2
Intent intent=new Intent(HelloWolrdActivity.this,TargetActivity.class);
startActivity(intent)

数据存储安全

SharedPreference内部用键值对方式存储,XML结构存储在/data/data/packageName/shared_prefs下。MODE_PRIVAT4E表示当前文件私有化存储模式,只能被应用本身访问,写入时覆盖原文件。反之MODE_APPEND模式将追加内容,应设置为MODE_PRIVATE以防被恶意修改或泄露:

1
SharedPreferences mySharedPreferences=getSharedPreferences("HelloWorld",Activity.MODE_PRIVATE);

认证安全

WebView自带记住密码功能,密码以明文保存在/data/data/com.package.name/databases/webview.db中,Root后可读,关闭自动保存功能为:

1
myWebView.getSettings().setSavePassword(false);

数据加密

敏感信息建议用SHA-256或SHA-3加密,不建议MD5、SHA-1、PIPEMD。

授权安全

绑定设备时,客户端检测用户是否有权限访问数据或执行操作时,可能出现信息伪造、数据替换等风险。为每个用户分配一个唯一ID,可对所有访问关键数据和敏感操作进行追溯,且该唯一ID应当不可伪造。IMEI国际移动设备识别码在移动电话网络中唯一标识了每台独立的移动通信设备,但模拟器可能会模拟或伪造。为避免该风险,建议用DEVICE_ID、MAC ADDRESS、Sim Serial Number、IMEI进行组装生成哈希后使用。

1
2
3
4
5
6
7
8
9
10
11
TelephonyManager tm=(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
String DEVICE_ID=tm.getDevice();

WifiManager wifi=(WifiManager)getSystemService(Context.WIFI_SERVICE);
WifiInfo info=wifi.getConnectionInfo();
String macAddress=info.getMacAddress();

TelephonyManager tm=(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
String SimSerialNumber=tm.getSimSerialNumber();

String IMEI=((TelephonyManager)getSystemService(TELEPHONY_SERVICE)).getDeviceId();

其他

SSL通信服务/客户端检测信任任意整数:开发者重写了checkServerTrusted但没做任何校验,使得可用中间人攻击获取加密内容。

WebView忽略SSL证书错误:WebView调用onReceivedSslError时直接执行handler.proceed()来忽略证书错误,可引起中间人攻击。

HTTPS关闭主机名验证:构造HttpClient时设置HostnameVerifier使用ALLOW_ALL_HOSTNAME_VERIFIER或空,可导致中间人攻击。修复应用STRICT_HOSTNAME_VERIFIER并自定义实现。

Intent Scheme URL攻击:AndroidManifest.xml设置Scheme协议后可用浏览器打开对应Activity,攻击者可构造Intent唤起应用相应组件,引起拒绝服务或提权漏洞。修复应配置category filter以添加android.intent.category.BROWSABLE。

AES弱加密:使用AES/ECB/NoPadding或AES/ECB/PKCS5padding模式。修复应指定CBC或CFB模式,填充PKCS5padding填充,密钥至少128位,推荐256位。

Provider文件目录便利:Provider被导出且复写openFile时没有对Content Query Uri进行判断或过滤,导致文件遍历。

WebView启用访问文件数据:WebView使用默认的setAllowFileAccess(true)时,则在File域下可执行任意JS代码,绕过同源策略对私有文件目录访问,造成隐私泄露。

Unzip解压缩:getName获取文件名后未对名称进行校验,被解压文件可进行目录跳转而被解压到其他目录,覆盖相应文件造成任意代码执行。

未使用编译器堆栈保护技术:Stack Canaries、PIE,修复在Android.mk中添加:

1
2
LOCAL_CFLAGS:=-Wall-O2-U_FORTIFY_SOURCE-fstack-protector-all
LOCAL_CFLAGS:=-fpie -pie

随机数不安全使用:用SecureRandom类setSeed时生成随机数有确定性,应用/dev/urandom或/dev/random来初始化伪随机数生成器

AES/DES硬编码密钥。

权限安全

PackageManagerService中有ApplicationInfo.FLAG_SYSTEM标记,或位于特定目录如/vendor/overlay、/system/framework、/system/priv-app、/system/app、/vendor/app、/oem/app等,为系统应用。

PackageManagerService中有ApplicationInfo.PRIVATE_FLAG_PRIVILEGED标记的系统应用为特权应用,可使用protectionLevel为signatureOrSystem或signature|privileged权限。

权限分为正常权限、危险权限、signature、signatureOrSystem。危险权限如下:

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
CALENDAR:
READ_CALENDAR
WRITE_CALENDAR

CAMERA:
CAMERA

CONTACTS:
READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS

LOCATION:
ACCESS_FINE_LOCATION
ACCESE_COARSE_LOCATION

MICROPHONE:
RECORD_AUDIO

PHONE:
READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS

SENSORS:
BODY_SENSORS

SMS:
SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS

STORAGE:
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_SOTRAGE

signature权限只有当请求该权限应用具有与声明权限的应用相同签名时,不通知用户并授予权限。signatureOrSystem权限时请求和声明权限的应用签名相同或请求权限的为系统应用。

MobSF

安装:

1
2
docker pull opensecurity/mobile-security-framework-mobsf:latest
docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:latest

默认登陆账密mobsf。

IoT入门

IoT安卓应用常见高风险权限:

1
2
3
4
5
6
7
8
9
10
11
ACCESS_COARSE_LOCATION CellID或WiFi 接收到基站服务信号便可获得位置信息
ACCESS_FINE_LOCATION 精良位置GPS
CAMERA
GET_TASKS
MOUNT_UNMOUNT_FILESYSTEMS SD卡内创建删除文件
READ_EXTERNAL_STORAGE
READ_PHONE_STATE 获取手机号码、IMEI、IMSI等
REQUEST_INSTALL_PACKAGES 安装未知来源应用
SYSTEM_ALERT_WINDOW 全局弹出系统弹出框
WRITE_EXTERNAL_STORAGE 向SD卡写入
WRITE_SETTINGS 修改系统设置数据

Android数组存储位置有:

1
2
3
4
5
6
/data/data/包名/
/data/data/包名/databases
/data/data/包名/shared_prefs
/data/data/包名/files/*.realm
/data/data/包名/app_webview
/sdcard/Android/data/包名

常见攻击方法

重签名绕过

例如签名校验:

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
//MainActivity
package com.demo.repackage;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import com.demo.tools.ToolUtils;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ToolUtils.getSignature2(getApplicationContext());
if (ToolUtils.checkSignature(getApplicationContext())){
Log.i("checkSignature", "True");
}else {
Log.i("checkSignature", "False");
}
}
}

//ToolUtils
package com.demo.tools;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Log;
import java.security.MessageDigest;
public class ToolUtils {
static String sign = "8b34e97425e0e682e3a73bd55830fc28ce34a4e8";
public static boolean checkSignature(Context context){
if (sign.equals(getAppSignature(context))){
return true;
}else {
return false;
}
}
public static String getAppSignature(Context context) {
try {
return sha1(getSignature(context));
} catch (Exception e) {}
return null;
}
public static String sha1(byte[] bytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(bytes);
byte[] b = md.digest();
int i;
StringBuilder sb = new StringBuilder();
for (byte value : b) {
i = value;
if (i < 0)
i += 256;
if (i < 16)
sb.append("0");
sb.append(Integer.toHexString(i));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static byte[] getSignature(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;
if (signatures != null){
return signatures[0].toByteArray();
}
} catch (Exception e) {}
return null;
}
}

这里通过PacakgeManagerService(PMS)服务获取应用签名信息,通过反射结合动态代理方式控制PMS返回值,达到替换签名的目的。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//HookServiceWraper 替换PMS
package com.verify.signature;
import android.content.Context;
import android.content.pm.PackageManager;
import com.verify.signature.ProxyHookPMS;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class HookServiceWraper {
public static void startHookPMS(Context context, String signData, String packageName){
try{
// 获取全局ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取原始sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 准备代理对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),new Class<?>[] { iPackageManagerInterface },new ProxyHookPMS(sPackageManager, signData, packageName, 0));
// 替换掉sPackageManager
sPackageManagerField.set(currentActivityThread, proxy);
// 替换 ApplicationPackageManager中 mPM对象
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
}catch (Exception e){
e.printStackTrace();
}
}
}

//ProxyHookPMS 用官方签名信息替换伪造的签名信息
package com.verify.signature;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Log;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyHookPMS implements InvocationHandler{
private Object base;
private String SIGN;
private String appPkgName = "";
public ProxyHookPMS(Object base, String sign, String appPkgName, int hashCode) {
try {
this.base = base;
this.SIGN = sign;
this.appPkgName = appPkgName;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("getPackageInfo".equals(method.getName())){
String pkgName = (String)args[0];
Integer flag = (Integer)args[1];
if(flag == PackageManager.GET_SIGNATURES && appPkgName.equals(pkgName)){
Signature sign = new Signature(SIGN);
PackageInfo info = (PackageInfo) method.invoke(base, args);
info.signatures[0] = sign;
return info;
}
}
return method.invoke(base, args);
}
}

//MyApplication
package com.verify.signature;
import android.app.Application;
import android.content.Context;
import com.verify.signature.HookServiceWraper;
public class MyApplication extends Application{
@Override
protected void attachBaseContext(Context baseContext) {
String sign = "3082038f30820277a00302010202045ad85776300d06092a864886f70d01010b05003078311230100603550406130952657061636b616765311230100603550408130952657061636b616765311230100603550407130952657061636b61676531123010060355040a130952657061636b61676531123010060355040b130952657061636b616765311230100603550403130952657061636b616765301e170d3232303431323031353032315a170d3437303430363031353032315a3078311230100603550406130952657061636b616765311230100603550408130952657061636b616765311230100603550407130952657061636b61676531123010060355040a130952657061636b61676531123010060355040b130952657061636b616765311230100603550403130952657061636b61676530820122300d06092a864886f70d01010105000382010f003082010a0282010100a207868100beed73e1f04f36435af22054e4ef1eed443c87e73844ece9c1269f40d1ec652528f7c0375cb81ad2edc9acc57136ac2e43252b158d5e8a1118d2d0772aa3b9f0ef6f34906e185eedee4e5dacc45c78b57e1d5b42a031261e3f05cce8055033cfdf4ace0ecdb2ff4c704b0c7d05ef45590040d3921393e15d38b782d1349156155a69461745b83325fed5c4dc8bb2f6c0a74969c1f35e19a5f7ba568e17245360de5c26718454e9a165b857fa236840635b9048bcccf620ea5083cec6b875ee6a541db17d3a8da6a328b237dba73f2d66191593fe76aa9a133cd5dcdafa33d697052b2599c7440689b74434702b6df163152631628985c7eb7120fb0203010001a321301f301d0603551d0e0416041407eaf98b249478511f997b2bb8eea40c236000c4300d06092a864886f70d01010b050003820101004708ad73716e350908dd2bf4fde35667885aa8c0bd3ac5805d9fcee40e1fd70d2cb8db75cb80d9db2bb0e9876f5e7c4bf057816d26592e2608f24ff3b5d9fbe8080ff6f8bfa7ae5bfb48d9a497474005ef51fc41af1d805e9f2bf33786c42b95c488db6c69a201bee659023509ea6182a435330e94f0c1c5ac51a37d3b4785189b66354e9403d6bd72af99b0dd30ae66bfc3a9fc07e8a3c84121d3f6d2dc3941b710b84f46df1604a13c42598dfdc1a6da6fde94a47abf16e042657fd361e8e5cce789b6078b604b0df20abe0d11734fa671fe41eb5c563e27e6568de367fc0c786c90f298cb1783a6881891b04005b0c84079103283b404cdfe7c91d319bf20";
String packageNeme = "com.demo.repackage";
HookServiceWraper.startHookPMS(baseContext, sign, packageNeme);
super.attachBaseContext(baseContext);
}
@Override
public void onCreate() {
super.onCreate();
}
}

使用方法是创建一个和目标应用包名相同的新应用,将上述Hook代码编译为APK再解包,获取Smali代码文件。将目标应用也解包,将Hook用的Smali塞到目标应用的Smali文件下,并重打包。

安全加固

ProGuard

build.gradle中添加:

1
2
3
4
5
6
buildTypes{
release{
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-optimize.txt'),'proguard-rules.pro'
}
}

签名校验

上文说PMS返回值可能被替换,这里直接绕过系统API,直接读取data/app/包名/base.apk中签名文件,获取原始签名信息。系统PackageParser服务对APK进行解析并获取文件信息,但该系统不对外暴露,需要用反射调用:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//MainActivity
package com.verify.signature;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import com.verify.signature.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Context context = getApplicationContext();
Log.i("Current sign ===>", Tools.getAppSignature(context));
Log.i("Reflect sign ===>", Tools.getReflectSignature(context));
Log.i("Native sign ===>", Tools.getSignByJni(context));
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Tools.getSignatureFileCrc(context);
if (!Tools.checkSignature(context)){
new AlertDialog.Builder(MainActivity.this).setTitle("安全提醒")//设置对话框标题
.setMessage("当前使用的客户端为非官方版本,存在极大的安全隐患。为了您的财产安全请在官方渠道下载重新安装!")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.exit(0);
}
}).show();
}
}
});
}
}

//Tools
package com.verify.signature;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Build;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Tools {
static{
try {
System.loadLibrary("test");
} catch (Exception e) {
e.printStackTrace();
}
}
public static native String getSignByJni(Context mContext);
public static boolean checkSignature(Context context){
String sign = "8b34e97425e0e682e3a73bd55830fc28ce34a4e8";
if (sign.equals(getReflectSignature(context)))
return true;
else
return false;
}
public static String getSignatureFileCrc(Context context){
try {
ZipFile zf;
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
zf = new ZipFile(path);
ZipEntry ze = zf.getEntry("META-INF/MANIFEST.MF");
String crcValue = String.valueOf(ze.getCrc());
Log.i("Test MANIFEST Crc ====>",crcValue);
return crcValue;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static String getAppSignature(Context context) {
try {
return sha1(getSignature(context));
} catch (Exception e) {}
return null;
}
public static byte[] getSignature(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;
if (signatures != null) {
Log.i("Test ===>", signatures[0].toCharsString());
return signatures[0].toByteArray();
}
} catch (Exception e) {}
return null;
}
public static String getReflectSignature(Context context) {
try {
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
return sha1(getApkSignatureByReflect(path));
} catch (Exception e) {}
return null;
}
public static String sha1(byte[] bytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(bytes);
byte[] b = md.digest();
int i;
StringBuilder sb = new StringBuilder();
for (byte value : b) {
i = value;
if (i < 0)
i += 256;
if (i < 16)
sb.append("0");
sb.append(Integer.toHexString(i));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static byte[] getApkSignatureByReflect(String apkPath) {
String fullPackageParserPath = "android.content.pm.PackageParser";
try {
Class packageParserClass = Class.forName(fullPackageParserPath);
Constructor pkgParserConstructor = null;
Object pkgParserIns = null;
if (Build.VERSION.SDK_INT > 20){
pkgParserConstructor = packageParserClass.getConstructor();
pkgParserIns = pkgParserConstructor.newInstance();
Class[] args = {File.class, Integer.TYPE};;
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", args);
Object[] valueArgs = new Object[2];
valueArgs[0] = new File(apkPath);
valueArgs[1] = PackageManager.GET_SIGNATURES;
Object parserPackage = parsePackageMethod.invoke(pkgParserIns, valueArgs);
if (Build.VERSION.SDK_INT>=28){
Class[] typeArgs = {parserPackage.getClass(),Boolean.TYPE};
Method collectCertificatesMethod = packageParserClass.getDeclaredMethod("collectCertificates", typeArgs);
Object[] valueArgs2 = {parserPackage, Build.VERSION.SDK_INT>28};
collectCertificatesMethod.invoke(pkgParserIns, valueArgs2);
Field mSignatures = null;
if(Build.VERSION.SDK_INT>=29)
mSignatures = parserPackage.getClass().getDeclaredField("mSigningDetails");
else
mSignatures = parserPackage.getClass().getDeclaredField("mSignatures");
mSignatures.setAccessible(true);
Object mSigningDetails = mSignatures.get(parserPackage);
Field infoField = mSigningDetails.getClass().getDeclaredField("signatures");
infoField.setAccessible(true);
Signature[] info = (Signature[]) infoField.get(mSigningDetails);
Log.i("ReflectSignature ===>", info[0].toCharsString());
return info[0].toByteArray();
}
Class[] typeArgs = {parserPackage.getClass(),Integer.TYPE};
Method collectCertificatesMethod = packageParserClass.getDeclaredMethod("collectCertificates", typeArgs);
Object[] valueArgs2 = {parserPackage, PackageManager.GET_SIGNATURES};
collectCertificatesMethod.invoke(pkgParserIns, valueArgs2);
// 应用程序信息包, 这个公开的, 不过有些函数, 变量没公开
Field packageInfoFld = parserPackage.getClass().getDeclaredField("mSignatures");
Signature[] info = (Signature[]) packageInfoFld.get(parserPackage);
Log.i("ReflectSignature ===>", info[0].toCharsString());
return info[0].toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

JNI层:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//signature
#include <jni.h>
#include <string.h>
#include <android/log.h>
#include <malloc.h>
#define TAG "Test"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
const char *APP_SIGNATURE = "10645EA8A12BE7A2C04B1F81DF3B4D90";
void ByteToHexStr(const char *source, char *dest, int sourceLen) {
short i;
char highByte, lowByte;
for (i = 0; i < sourceLen; i++) {
highByte = source[i] >> 4;
lowByte = source[i] & 0x0f;
highByte += 0x30;
if (highByte > 0x39)
dest[i * 2] = highByte + 0x07;
else
dest[i * 2] = highByte;
lowByte += 0x30;
if (lowByte > 0x39)
dest[i * 2 + 1] = lowByte + 0x07;
else
dest[i * 2 + 1] = lowByte;
}
}
jstring sha1(JNIEnv *env, jbyteArray source) {
// MessageDigest类
jclass classMessageDigest = env->FindClass("java/security/MessageDigest");
// MessageDigest.getInstance()静态方法
jmethodID midGetInstance = env->GetStaticMethodID(classMessageDigest, "getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
// MessageDigest object
jobject objMessageDigest = env->CallStaticObjectMethod(classMessageDigest,midGetInstance,env->NewStringUTF("sha1"));
// update方法,这个函数的返回值是void,写V
jmethodID midUpdate = env->GetMethodID(classMessageDigest, "update", "([B)V");
env->CallVoidMethod(objMessageDigest, midUpdate, source);
// digest方法
jmethodID midDigest = env->GetMethodID(classMessageDigest, "digest", "()[B");
jbyteArray objArraySign = (jbyteArray) env->CallObjectMethod(objMessageDigest, midDigest);
jsize intArrayLength = env->GetArrayLength(objArraySign);
jbyte *byte_array_elements = env->GetByteArrayElements(objArraySign, NULL);
size_t length = (size_t) intArrayLength * 2 + 1;
char *char_result = (char *) malloc(length);
memset(char_result, 0, length);
// 将byte数组转换成16进制字符串,发现这里不用强转,jbyte和unsigned char应该字节数是一样的
ByteToHexStr((const char *) byte_array_elements, char_result, intArrayLength);
// 在末尾补\0
*(char_result + intArrayLength * 2) = '\0';
jstring stringResult = env->NewStringUTF(char_result);
// release
env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT);
// 释放指针使用free
free(char_result);
env->DeleteLocalRef(classMessageDigest);
env->DeleteLocalRef(objMessageDigest);
return stringResult;
}
static jobject getContext(JNIEnv *env) {
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
jmethodID currentApplication = env->GetStaticMethodID(activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL)
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
env->DeleteLocalRef(activity_thread_clz);
}
return application;
}
JNIEXPORT jstring JNICALL getSignByJni(JNIEnv *env, jobject thiz, jobject context) {
// 获得Context类
jclass cls = env->GetObjectClass(context);
// 得到getPackageManager方法的ID
jmethodID mid = env->GetMethodID(cls, "getPackageManager","()Landroid/content/pm/PackageManager;");
// 获得应用包的管理器
jobject pm = env->CallObjectMethod(context, mid);
// 得到getPackageName方法的ID
mid = env->GetMethodID(cls, "getPackageName", "()Ljava/lang/String;");
// 获得当前应用包名
jstring packageName = (jstring) env->CallObjectMethod(context, mid);
// 获得PackageManager类
cls = env->GetObjectClass(pm);
// 得到getPackageInfo方法的ID
mid = env->GetMethodID(cls, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
// 获得应用包的信息
jobject packageInfo = env->CallObjectMethod(pm, mid, packageName, 0x40); //GET_SIGNATURES = 64;
// 获得PackageInfo 类
cls = env->GetObjectClass(packageInfo);
// 获得签名数组属性的ID
jfieldID fid = env->GetFieldID(cls, "signatures", "[Landroid/content/pm/Signature;");
// 得到签名数组
jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid);
// 得到签名
jobject signature = env->GetObjectArrayElement(signatures, 0);
// 获得Signature类
cls = env->GetObjectClass(signature);
// 得到toCharsString方法的ID
mid = env->GetMethodID(cls, "toByteArray", "()[B");
// 返回当前应用签名信息
jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);
return sha1(env, signatureByteArray);
}

SO防护

对不想对外暴露的函数进行隐藏:

1
2
3
__attribute__((visibility("hidden"))) int test(int a,int b){
return a+b;
};

反调试

调试工具附加到目标进程时需要用ptrace系统调用操作,以建立其与目标程序的跟踪关系。正常情况下每个进程同一时间只能被附加一次,若应用程序启动后fork一个子进程抢先一步附加到自身进程,则调试器将无法正常附加,达到调试目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <time.h>
void check_debug_by_ptrace() {
int pid = fork();
if (pid == 0) {
int ppid = getppid();
if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0) {
waitpid(ppid, NULL, 0);
ptrace(PTRACE_CONT, NULL, NULL);
}
}
}

也可直接用PTRACE_TRACEME参数,在进程启动时主动请求被监控跟踪:

1
2
3
void anti_debug_by_ptrace(){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
}

当攻击者使用调试器进行动态分析时,需要设置断点将应用挂起,此时调试器发出SIGTRAP信号。可以在应用中主动添加SIGTRAP信号监听代码:

1
2
3
4
5
6
7
8
9
void signal_handle(int sig){ 
// 自定义信号处理函数
exit(0);
}
void check_debug_by_signal(){
// 设置SIGTRAP信号的处理函数为signal_handle
long ret = (long)signal(SIGTRAP, signal_handle);
raise(SIGTRAP); // 主动发送SIGTRAP信号
}

进程被附加后有些属性值被改变:

特征文件名 特征值
/proc/pid/status和/proc/pid/task/pid/status 进程status值中TracePid值非0时处于调试状态
/proc/pid/stat和/proc/pid/task/pid/stat 进程stat值中括号后跟随的第一个字母为t时处于调试状态
/proc/pid/wchan和/proc/pid/task/pid/wchan 进程wchan状态值为ptrace_stop时处于调试状态

提取特征值为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void check_debug_by_tracerpid() {
char debug_file[56]={0};
int state = 0;
int pid = getpid();
sprintf(debug_file, "/proc/%d/status", pid);
FILE* fp = fopen(debug_file, "r");
char line[1024]={0};
while(fgets(line, 1024, fp)){
if (strncmp(line, "TracerPid", 9) == 0){
state = atoi(&line[10]);
if (state != 0){
fclose(fp);
exit(0);
}
break;
}
memset(line, 0, 1024);
}
fclose(fp);
}

还可监测代码执行时间,可能出现单步调试现象:

1
2
3
4
5
6
7
8
9
10
void check_debug_by_time() {
time_t start_time;
time_t end_time;
time(&start_time);
// 此处添加需要监控的核心代码
time(&end_time);
// 时间差根据实际情况变动
if(end_time - start_time > 10)
exit(0);
}

进行调试时需要开启并监听指定端口,若设备开启这些端口则说明在和调试器通信,处于调试状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* IDA调试的默认端口23946,对应的十六进制值5D8A
* Frida默认会占用的两个端口27402、27403,对应的十六进制值6B0A、6B0B
*/
void check_debug_by_port() {
char buff[BUFF_LEN];
char line[BUFF_LEN];
const char* dir = "/proc/net/tcp";
FILE *fp = fopen(dir, "r");
while (fgets(buff, BUFF_LEN, fp) != NULL)
if (strstr(buff, "5D8A") != NULL || strstr(buff, "6B0A") != NULL || strstr(buff, "6B0B") != NULL) {
fclose(fp);
exit(0);
}
fclose(fp);
FILE *fd = popen("netstat -apn", "r");
while (fgets(line, sizeof(line), fd) != NULL)
if (strstr(line, "23946") != NULL || strstr(line, "27402") != NULL || strstr(line, "27403") != NULL){
fclose(fd);
exit(0);
}
pclose(fd);
}

为防Hook,可建立动态库黑名单机制,将动态库来源路径中非系统目录或其他不可信路径收集整理为黑名单。

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
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/ptrace.h>
/*
hook注入检测
返回值为0 说明没有异常库信息
正常返回异常库的个数。
根据type的值不同 检测hook框架和注入
*/
void check_inject()
{
char pFilePath[32];
char pLibInfo[256];
char *pLibPath = NULL;
char *savePtr = NULL;
int pid = getpid();
sprintf(pFilePath, "/proc/%d/maps", pid);
FILE *fp = fopen(pFilePath, "r");
while (fgets(pLibInfo, sizeof(pLibInfo), fp) != NULL)
{
strtok_r(pLibInfo, " \t", &savePtr); // 地址信息
strtok_r(NULL, " \t", &savePtr); // 权限信息
strtok_r(NULL, " \t", &savePtr); // 偏移信息
strtok_r(NULL, " \t", &savePtr); // 设备信息
strtok_r(NULL, " \t", &savePtr); // 节点信息
pLibPath = strtok_r(NULL, " \t", &savePtr); // 路径信息
if (pLibPath != NULL) // 判断是否有路径信息
{
if (check_block_list(pLibPath) == -1)
{
exit(0);
}
}
memset(pLibInfo, 0, 256);
}
fclose(fp);
}

完整性校验

对应用安装包中文件计算CRC值,与原始CRC值对比,其中CRC值应保存到服务器中。

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
76
77
78
79
80
81
82
83
84
//MainActivity
package com.test.integritycheck_android;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
Button btn = null;
Context mContext = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = getApplicationContext();
btn = findViewById(R.id.check);
btn.setOnClickListener(new MyClick());
}
class MyClick implements View.OnClickListener{
@Override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
UtilTools.getFileCrc(mContext);
UtilTools.getAllFileCrc(mContext);
}
}).start();
}
}
}

//UtilTools
package com.test.integritycheck_android;
import android.content.Context;
import android.util.Log;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class UtilTools {
private static final String[] fileArray = {"META-INF/MANIFEST.MF","classes.dex","resources.arsc","AndroidManifest.xml"};
public static Map getFileCrc(Context context){
Map result = new HashMap();
try {
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
ZipFile zf = new ZipFile(path);
ZipEntry ze;
for(String item:fileArray){
ze = zf.getEntry(item);
if (ze != null){
String crcValue = String.valueOf(ze.getCrc());
result.put(item, crcValue);
Log.i(item+ " CRC ======>",crcValue);
}
}
return result;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static Map getAllFileCrc(Context context){
Map result = new HashMap();
try {
String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).publicSourceDir;
ZipFile zf = new ZipFile(path);
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
// get the zip entry
ZipEntry entry = entries.nextElement();
String crcValue = String.valueOf(entry.getCrc());
result.put(entry.getName(), crcValue);
Log.i(entry.getName()+ " CRC ======>",crcValue);
}
return result;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}