安卓加固基礎(一)
1.Dex字符串加密
1.1 前序
Android應用當中,很多隱私信息都是以字符串的形式存在的。這些隱私信息是明文,對於軟件來說是想當不安全的,如果我們能在打包時對Dex中的字符串加密替換,並在運行時調用解密,這樣就能夠避免字符串明文存在於Dex中。雖然,無法完全避免被破解,但是加大了逆向提取信息的難度,安全性無疑提高了很多。
1.2 字符串加密方法簡介
目前市面上主要存在兩種字符串加密方式:
(1)在開發階段開發者使用加密後的字符串然後手動調用解密,這種方法工程量太大了,缺點很明顯。
(2)編譯後修改字節碼,然後再動態植入加密後的字符串,最後讓其自動調用進行解密,這裏重點分析的是第二種。
1.3 強大的工具字符串加密工具StringFog
1.3.1 環境配置和運行效果
工具:
(1)AS版本3.5.0
(2)StringFog配置方法:https://github.com/MegatronKing/StringFog
(3)反編譯工具 jeb3
代碼(MainActivity.java):
package com.example.testdex;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_main);
String str ="Stringfog"; //這是加密字符串
}
}
jeb3反編譯的效果(MainActivity.java):
package com.example.testdex;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
public MainActivity() {
super();
}
protected void onCreate(Bundle arg2) {
super.onCreate(arg2);
this.setContentView(0x7F09001C);
StringFog.decrypt("GxEeBQFHMQAV"); //顯然"StringFog"被加密了
}
}
1.3.2 StringFog加密原理分析
首先看加密的方法:StringFog採用的是base64+xor(異或)算法
import java.util.Base64;
public class StringFog {
private static byte[] xor(byte[] data, String key) { //異或算法
int len = data.length;
int lenKey = key.length();
int i = 0;
int j = 0;
while (i < len) {
if (j >= lenKey) {
j = 0;
}
data[i] = (byte) (data[i] ^ key.charAt(j));
i++;
j++;
}
return data;
}
public static String encode(String data, String key) {
return new String(Base64.getEncoder().encode(xor(data.getBytes(), key)));
//調用base64加密包
}
public static String decode(String data, String key) {
return new String(xor(Base64.getDecoder().decode(data), key));
//調用base64解密包
}
按照安卓程序加密的字符串測試:
public static void main(String[] args) {
String test = encode("Stringfog","Hello World");
System.out.println(test);
}
輸出結果爲:
顯然跟jeb3裏面看到的一樣。
ps:Base64 packge 下載地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi 下載後導入commons-codec-1.14.jar包
1.3.2 StringFog加密原理分析
StringFog實際上是操作了class文件,編譯class文件的字節碼文件,發現如果都是字符串常量的話,指令都爲LDC
****************************************************************************
[root@iZbp1dubkpj5g938jakcouZ ~]# javac Hello.java
[root@iZbp1dubkpj5g938jakcouZ ~]# javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String This is a String!
2: astore_1
3: return
}
那麼我們可以藉助asm庫攔截方法中的每條LDC指令,然後插入該指令即可
總結:字符串加密方法能夠較爲有效的阻止他人通過字符串搜素定位代碼,但是不能防止Hook,總而言之,這只是一個比較普通的混淆方法
2.資源加密
java代碼將png圖片加密:
package cn.zzh;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class pngEncrypt {
public static void main(String[] args){
//調用加密方法
KMD.encrypt("f://1.png");
}
//加密後,會在原圖片的路徑下生成加密後的圖片
public static void encrypt(String filePath){
byte[] tempbytes = new byte[5000];
try {
InputStream in = new FileInputStream(filePath);
OutputStream out = new FileOutputStream(filePath.subSequence(0, filePath.lastIndexOf("."))+"2.png");
while (in.read(tempbytes) != -1) {
byte a = tempbytes[0];
tempbytes[0] = tempbytes[1]; //將第一個字符和第二個字符交換
tempbytes[1] = a;
out.write(tempbytes);//寫文件
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用winhex查看發現:
變成了:
然後我們只需要在AS中將編寫解密代碼即可:
下面是一個簡單的測試demo:
(1)建立asserts文件,將加密的12.png文件放進去
(2)代碼編寫:
package com.example.testdex;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_main);
Bitmap bitmap= getImageFromAssets(this,"12.png");//圖片資源加密
if(bitmap != null) {
//imageView.setImage(ImageSource.bitmap(bitmap));
Toast.makeText (this, "圖片解密成功", Toast.LENGTH_SHORT).show ();
} else {
// Log.i(TAG,"圖片爲空");
Toast.makeText (this, "圖片解密失敗", Toast.LENGTH_SHORT).show ();
}
}
public Bitmap getImageFromAssets(Context context, String fileName) {
Bitmap image = null;
AssetManager am = context.getResources().getAssets();
try {
InputStream is = am.open(fileName);
byte[] buffer = new byte[1500000];//記得要足夠大
is.read(buffer);
for(int i=0; i<buffer.length; i+= 5000){//和加密相同
byte temp = buffer[i];
buffer[i] = buffer[i+1];
buffer[i+1] = temp;
}
image = BitmapFactory.decodeByteArray(buffer, 0, buffer.length);
if (is!=null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return image;
}
}
測試結果:
此時,我們打開apk查看assert文件時依然無法看到有用的文件
ps:同時我們還可以進行資源路徑混淆,詳情見:https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md
3.對抗反編譯
3.1類名混淆
現在的反編譯工具都太先進了,很多純粹的對抗反編譯技術都不在適應了,
但是我們可以將類名進行混淆:
gradle版本在3.4以下時我們使用proguard-rules.pro進行混淆,達到3.4以上時我們使用R8穿插一些proguard規則進行混淆
下面重點研究的是3.4以上版本的情況:
官方文檔:https://developer.android.com/studio/build/shrink-code?hl=zh-cn
通用教程:https://www.jianshu.com/p/65027e18c2fe
混淆規則(如下):
#############################################
#
# 對於一些基本指令的添加
#
#############################################
# 代碼混淆壓縮比,在0~7之間,默認爲5,一般不做修改
-optimizationpasses 5
# 混合時不使用大小寫混合,混合後的類名爲小寫
-dontusemixedcaseclassnames
# 指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses
# 這句話能夠使我們的項目混淆後產生映射文件
# 包含有類名->混淆後類名的映射關係
-verbose
# 指定不去忽略非公共庫的類成員
-dontskipnonpubliclibraryclassmembers
# 不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify
# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
# 避免混淆泛型
-keepattributes Signature
# 拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable
# 指定混淆是採用的算法,後面的參數是一個過濾器
# 這個過濾器是谷歌推薦的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*
#############################################
#
# Android開發中一些需要保留的公共部分
#
#############################################
# 保留我們使用的四大組件,自定義的Application等等這些類不被混淆
# 因爲這些子類都有可能被外部調用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
# 保留support下的所有類及其內部類
-keep class android.support.** {*;}
# 保留繼承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
# 保留R下面的資源
-keep class **.R$* {*;}
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留在Activity中的方法參數是view的方法,
# 這樣以來我們在layout中寫的onClick就不會被影響
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
# 保留枚舉類不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留我們自定義控件(繼承自View)不被混淆
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
# 保留Parcelable序列化類不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保留Serializable序列化的類不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 對於帶有回調函數的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}
# webView處理,項目中沒有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, jav.lang.String);
}
3.2 so層混淆
這個之前已經寫過了:
文檔:函數混淆(JNI_Onload).note
鏈接:http://note.youdao.com/noteshare?id=f6add1ff6a6c4b78aac4a9e27fe390ed&sub=04FF4E2B1F504587A06C69F39C4DF22C
3.3 簽名驗證
3.3.1 在MainActivity.java中進行簽名驗證
package com.example.signatureverify;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Bundle;
import android.widget.Toast;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_main);
Context context =getApplicationContext ();
Toast.makeText (context, "我是正版", Toast.LENGTH_SHORT).show ();
String cert_sha1="59F8A6B86A367F0586F1A15DDDB63D75263C5D62"; // 通過調試提前獲取apk的sha1簽名
boolean is_org_app = false;
try {
is_org_app = isOrgApp(context,cert_sha1);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace ();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace ();
}
if(!is_org_app){
android.os.Process.killProcess ((android.os.Process.myPid ()));
//如果簽名不一致,說明程序被修改了,直接退出
}
}
//比較簽名
private boolean isOrgApp(Context context, String cert_sha1) throws PackageManager.NameNotFoundException, NoSuchAlgorithmException {
String current_sha1=getAppSha1(context);
current_sha1=current_sha1.replace (":","");
return cert_sha1.equals (current_sha1);
}
//生成sha1的簽名
private String getAppSha1(Context context) throws PackageManager.NameNotFoundException, NoSuchAlgorithmException {
PackageInfo info=context.getPackageManager ().getPackageInfo (context.getPackageName (),PackageManager.GET_SIGNATURES);
byte[] cert =info.signatures[0].toByteArray ();
MessageDigest md =MessageDigest.getInstance ("SHA1");
byte[] publicKey=md.digest (cert);
StringBuffer hexString =new StringBuffer ();
for(int i=0;i<publicKey.length;i++){
String appendString=Integer.toHexString (0xFF&publicKey[i]).toUpperCase(Locale.US);
if(appendString.length ()==1){
hexString.append("0");
}
hexString.append(appendString);//簽名的格式是11:22,所以需要加上":"
hexString.append (":");
}
String result=hexString.toString ();
return result.substring (0,result.length ()-1);
}
}
完成程序後,使用AndroidKiller進行修改時,將"我是正版"字符串修改成"我是盜版",在運行程序時,程序會直接閃退
3.3.2 在So層中進行簽名驗證
MainActivity:
package com.example.jnisignatureverify;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.security.MessageDigest;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
protected TextView appSignaturesTv;
protected TextView jniSignaturesTv;
protected Button checkBtn;
protected Button tokenBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setContentView(R.layout.activity_main);
initView();
appSignaturesTv.setText(getSha1Value(MainActivity.this));
jniSignaturesTv.setText(getSignaturesSha1(MainActivity.this));
}
private View.OnClickListener clickListener = new View.OnClickListener(){
@Override
public void onClick(View v) {
boolean result = checkSha1(MainActivity.this);
if(result){
Toast.makeText(getApplicationContext(),"驗證通過",Toast.LENGTH_LONG).show();
}else{
Toast.makeText(getApplicationContext(),"驗證不通過,請檢查valid.cpp文件配置的sha1值",Toast.LENGTH_LONG).show();
}
}
};
private View.OnClickListener tokenClickListener = new View.OnClickListener(){
@Override
public void onClick(View v) {
String result = getToken(MainActivity.this,"12345");
Toast.makeText(getApplicationContext(),result,Toast.LENGTH_LONG).show();
}
};
private void initView() {
appSignaturesTv = (TextView) findViewById(R.id.app_signatures_tv);
jniSignaturesTv = (TextView) findViewById(R.id.jni_signatures_tv);
checkBtn = (Button) findViewById(R.id.check_btn);
tokenBtn = (Button) findViewById(R.id.token_btn);
checkBtn.setOnClickListener(clickListener);
tokenBtn.setOnClickListener(tokenClickListener);
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String getSignaturesSha1(Context context);
public native boolean checkSha1(Context context);
public native String getToken(Context context,String userId);
//獲取apk當前的簽名
public String getSha1Value(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), PackageManager.GET_SIGNATURES);
byte[] cert = info.signatures[0].toByteArray();
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] publicKey = md.digest(cert);
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < publicKey.length; i++) {
String appendString = Integer.toHexString(0xFF & publicKey[i])
.toUpperCase(Locale.US);
if (appendString.length() == 1)
hexString.append("0");
hexString.append(appendString);
}
String result = hexString.toString();
return result.substring(0, result.length());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
native-lib.cpp:
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
#include <android/log.h>
#include <cstring>
#define TAG "jni-log"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__)
//簽名信息
const char *app_sha1="59F8A6B86A367F0586F1A15DDDB63D75263C5D62";
const char hexcode[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char* getSha1(JNIEnv *env, jobject context_object){
//上下文對象
jclass context_class = env->GetObjectClass(context_object);
//反射獲取PackageManager
jmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject package_manager = env->CallObjectMethod(context_object, methodId);
if (package_manager == NULL) {
//LOGD("package_manager is NULL!!!");
return NULL;
}
//反射獲取包名
methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;");
jstring package_name = (jstring)env->CallObjectMethod(context_object, methodId);
if (package_name == NULL) {
//LOGD("package_name is NULL!!!");
return NULL;
}
env->DeleteLocalRef(context_class);
//獲取PackageInfo對象
jclass pack_manager_class = env->GetObjectClass(package_manager);
methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
env->DeleteLocalRef(pack_manager_class);
jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, 0x40);
if (package_info == NULL) {
LOGD("getPackageInfo() is NULL!!!");
return NULL;
}
env->DeleteLocalRef(package_manager);
//獲取簽名信息
jclass package_info_class = env->GetObjectClass(package_info);
jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;");
env->DeleteLocalRef(package_info_class);
jobjectArray signature_object_array = (jobjectArray)env->GetObjectField(package_info, fieldId);
if (signature_object_array == NULL) {
LOGD("signature is NULL!!!");
return NULL;
}
jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0);
env->DeleteLocalRef(package_info);
//簽名信息轉換成sha1值
jclass signature_class = env->GetObjectClass(signature_object);
methodId = env->GetMethodID(signature_class, "toByteArray", "()[B");
env->DeleteLocalRef(signature_class);
jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId);
jclass byte_array_input_class=env->FindClass("java/io/ByteArrayInputStream");
methodId=env->GetMethodID(byte_array_input_class,"<init>","([B)V");
jobject byte_array_input=env->NewObject(byte_array_input_class,methodId,signature_byte);
jclass certificate_factory_class=env->FindClass("java/security/cert/CertificateFactory");
methodId=env->GetStaticMethodID(certificate_factory_class,"getInstance","(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");
jstring x_509_jstring=env->NewStringUTF("X.509");
jobject cert_factory=env->CallStaticObjectMethod(certificate_factory_class,methodId,x_509_jstring);
methodId=env->GetMethodID(certificate_factory_class,"generateCertificate",("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"));
jobject x509_cert=env->CallObjectMethod(cert_factory,methodId,byte_array_input);
env->DeleteLocalRef(certificate_factory_class);
jclass x509_cert_class=env->GetObjectClass(x509_cert);
methodId=env->GetMethodID(x509_cert_class,"getEncoded","()[B");
jbyteArray cert_byte=(jbyteArray)env->CallObjectMethod(x509_cert,methodId);
env->DeleteLocalRef(x509_cert_class);
jclass message_digest_class=env->FindClass("java/security/MessageDigest");
methodId=env->GetStaticMethodID(message_digest_class,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
jstring sha1_jstring=env->NewStringUTF("SHA1");
jobject sha1_digest=env->CallStaticObjectMethod(message_digest_class,methodId,sha1_jstring);
methodId=env->GetMethodID(message_digest_class,"digest","([B)[B");
jbyteArray sha1_byte=(jbyteArray)env->CallObjectMethod(sha1_digest,methodId,cert_byte);
env->DeleteLocalRef(message_digest_class);
//轉換成char
jsize array_size=env->GetArrayLength(sha1_byte);
jbyte* sha1 =env->GetByteArrayElements(sha1_byte,NULL);
char *hex_sha=new char[array_size*2+1];
for (int i = 0; i <array_size ; ++i) {
hex_sha[2*i]=hexcode[((unsigned char)sha1[i])/16];
hex_sha[2*i+1]=hexcode[((unsigned char)sha1[i])%16];
}
hex_sha[array_size*2]='\0';
LOGD("hex_sha %s ",hex_sha);
return hex_sha;
}
jboolean checkValidity(JNIEnv *env,char *sha1){
//比較簽名
if (strcmp(sha1,app_sha1)==0)
{
LOGD("驗證成功");
return true;
}
LOGD("驗證失敗");
return false;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_jnisignatureverify_MainActivity_getSignaturesSha1(JNIEnv *env, jobject thiz,
jobject context) {
// TODO: implement getSignaturesSha1()
return env->NewStringUTF(app_sha1);
}extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_jnisignatureverify_MainActivity_checkSha1(JNIEnv *env, jobject thiz,
jobject contextObject) {
// TODO: implement checkSha1()
char *sha1 = getSha1(env,contextObject);
jboolean result = checkValidity(env,sha1);
return result;
}extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_jnisignatureverify_MainActivity_getToken(JNIEnv *env, jobject thiz,
jobject contextObject, jstring user_id) {
// TODO: implement getToken()
char *sha1 = getSha1(env,contextObject);
jboolean result = checkValidity(env,sha1);
if(result){
return env->NewStringUTF("獲取Token成功");
}else{
return env->NewStringUTF("獲取失敗,請檢查native-lib.cpp文件配置的sha1值");
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_margin="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.jnisignatureverify.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="APP簽名信息:"/>
<TextView
android:id="@+id/app_signatures_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_marginTop="16dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="jni配置的簽名信息:"/>
<TextView
android:id="@+id/jni_signatures_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<Button
android:id="@+id/check_btn"
android:layout_marginTop="16dp"
android:text="驗證簽名是否正確"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/token_btn"
android:layout_marginTop="16dp"
android:text="獲取token"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>