本文最初发表在CSDN上:
https://blog.csdn.net/weixin_43632667/article/details/105008506
https://blog.csdn.net/weixin_43632667/article/details/104394222
Android应用当中,很多隐私信息都是以字符串的形式存在的。这些隐私信息是明文,对于软件来说是想当不安全的,如果我们能在打包时对Dex中的字符串加密替换,并在运行时调用解密,这样就能够避免字符串明文存在于Dex中。虽然,无法完全避免被破解,但是加大了逆向提取信息的难度,安全性无疑提高了很多。
目前市面上主要存在两种字符串加密方式:
(1)在开发阶段开发者使用加密后的字符串然后手动调用解密,这种方法工程量太大了,缺点很明显。
(2)编译后修改字节码,然后再动态植入加密后的字符串,最后让其自动调用进行解密,这里重点分析的是第二种。
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,总而言之,这只是一个比较普通的混淆方法
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
现在的反编译工具都太先进了,很多纯粹的对抗反编译技术都不在适应了,基本上,我们都可以通过jeb这个强大的反编译工具查看到
但是我们可以将类名进行混淆:
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); }
这个之前已经写过了:
文档:函数混淆(JNI_Onload).note
链接:http://note.youdao.com/noteshare?id=f6add1ff6a6c4b78aac4a9e27fe390ed&sub=04FF4E2B1F504587A06C69F39C4DF22C
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>
在处于调试状态时,Linux会向/proc/pid/status写入一些进程状态信息,比如TracerPid字段会写入调试进程的pid,因此我们可以自己ptrace自己,然后让android_server不能调试
代码如下:
#include<sys/ptrace.h> //这个头文件很重要 void anti_debug01() { ptrace(PTRACE_TRACEME,0,0,0); }
jint JNI_Onload(JavaVM* vm,void* reserved) { anti_debug01(); }
一旦开始调试,就会出现
反反调试思路:nop掉anti_debug01()函数调用
根据第一种分析得出Tracepid的值只要不为0 就能说明进程正在被调试,因此我们只需要检测Tracepid的值是不是0,如果不为0,直接退出就行了
void anti_debug02() { try { const int bufsize =1024; char filename[bufsize]; char line[bufsize]; int pid=getpid(); sprintf(filename,"/proc/%d/status",pid); FILE* fd=fopen(filename,"r"); if(fd!=NULL) { while(fgets(line,bufsize,fd)) { if(strncmp(line,"TracerPid",9)==0) { int statue = atoi(&line[10]); //atoi,将字符串转化为int if(statue !=0) { fclose(fd); int ret =kill(pid,SIGKILL); } break; } } } } }
#include <pthread.h> #include <unistd.h> #include <stdio.h> void* thread_function(void *arg){ int pid = getpid(); char file_name[20] = {'\0'}; sprintf(file_name,"proc/%d/status",pid); char line_str[256]; int i = 0,traceid; FILE *fp; while(1){ i = 0; fp = fopen(file_name,"r"); if(fp == NULL){ break; } while(!feof(fp)){ fgets(line_str,256,fp); if(i == 5){ // traceid = getnumberfor_str(line_str); traceid = atoi(&line_str[10]); if(traceid > 0){ exit(0); } break; } i++; } fclose(fp); sleep(5); } } void create_thread_check_traceid(){ pthread_t thread_id; int err = pthread_create(&thread_id,NULL,thread_function,NULL); if(err != 0){ } }
反反调试思路:使用IDA动态调试在函数调用前下断,对比当前TracerPid为4591,将TracerPid对应的寄存器修改为0,达到“0==0”的效果,绕开反调试。
1.需要加密的APK(源APK)
2.壳程序APK
3.加密工具(负责将源APK进行加密和壳DEX合并成新的DEX)
这里面需要注意的字段:
1.checksum文件校验码,使alder32算法校验文件除去magic,checksum外余下的所有文件区域,用于检查文件错误。
2.signature使用SHA-1算法hash除去magic,checksum和signature外余下的所有文件区域,用于唯一识别本文件。
3.file_Size Dex文件的大小。
4.在文件的最后,我们需要标注被加密的apk大小,因此需要增加4个字节。
关注的原因如下:
因为我们需要将一个文件(加密之后的源Apk)写入到Dex中,那么我们肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。还有就是需要修改dex文件的大小。
不过这里还需要一个操作,就是标注一下我们加密的Apk的大小,因为我们在脱壳的时候,需要知道Apk的大小,才能正确的得到Apk。那么这个值放到哪呢?这个值直接放到文件的末尾就可以了。
总的来说:我们需要做:修改Dex的三个文件头,将源Apk的大小追加到壳dex的末尾就可以了
根据上述修改后的dex文件样式如下
由原理可知这里需要进行三个步骤了:
1、自己编写一个源程序项目(需要加密的APK)
2、脱壳项目(解密源APK和加载APK)
3、对源APK进行加密和脱壳项目的DEX合并
命名为:SourceApk
MyApplication:
package com.example.sourceapk; public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Log.i("demo", "source apk onCreate:" + this); } }
ps:MyApplication的作用:MyApplication类继承Application,查看源码我们知道,Application中有一个attachBaseContext方法,它在Application的onCreate方法执行前就会执行了,这个关键点为后面的加密程序做铺垫。
MainActivity:
package com.example.sourceapk; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView content = new TextView(this); content.setText("I am Source Apk"); content.setOnClickListener(new OnClickListener(){ @Override public void onClick(View arg0) { Intent intent = new Intent(MainActivity.this, SubActivity.class); startActivity(intent); }}); setContentView(content); Log.i("demo", "app:"+getApplicationContext()); } }
就是源程序的一个主要类
SubActivity:
package com.example.sourceapk; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class SubActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView content = new TextView(this); content.setText("I am Source Apk SubMainActivity"); setContentView(content); Log.i("demo", "app:"+getApplicationContext()); } }
同MainActivity
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.sourceapk"> <application android:allowBackup="true" android:label="@string/app_name" android:name="com.example.sourceapk.MyApplication" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".SubActivity" /> </application> </manifest>
注意一定要加上android:name="com.example.sourceapk.MyApplication"
5.1.3.2加密APK(对加密后的源apk进行解密加载的apk)
ProxyActivity:
```
package com.example.packapk;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;
public class ProxyApplication extends Application{
private static final String appkey = "APPLICATION_CLASS_NAME";
private String apkFileName;
private String odexPath;
private String libPath;
// 这是context赋值 @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); try { // 创建两个文件夹payload_odex、payload_lib,私有的,可写的文件目录 File odex = this.getDir("payload_odex", MODE_PRIVATE); File libs = this.getDir("payload_lib", MODE_PRIVATE); odexPath = odex.getAbsolutePath(); libPath = libs.getAbsolutePath(); apkFileName = odex.getAbsolutePath() + "/payload.apk"; File dexFile = new File(apkFileName); Log.i("demo", "apk size:"+dexFile.length()); if (!dexFile.exists()) { dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk // 读取程序classes.dex文件 byte[] dexdata = this.readDexFileFromApk(); // 分离出解壳后的apk文件已用于动态加载 this.splitPayLoadFromDex(dexdata); } // 配置动态加载环境 Object currentActivityThread = RefInvoke.invokeStaticMethod( "android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});//获取主线程对象 String packageName = this.getPackageName();//当前apk的包名 ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mPackages"); WeakReference wr = (WeakReference) mPackages.get(packageName); // 创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码) DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath, libPath, (ClassLoader) RefInvoke.getFieldOjbect( "android.app.LoadedApk", wr.get(), "mClassLoader")); //把当前进程的mClassLoader设置成了被加壳apk的DexClassLoader RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader); Log.i("demo","classloader:"+dLoader); try{ Object actObj = dLoader.loadClass("com.example.sourceapk.MainActivity"); Log.i("demo", "actObj:"+actObj); }catch(Exception e){ Log.i("demo", "activity:"+Log.getStackTraceString(e)); } } catch (Exception e) { Log.i("demo", "error:"+Log.getStackTraceString(e)); e.printStackTrace(); } } @Override public void onCreate() { { //loadResources(apkFileName); Log.i("demo", "onCreate"); // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。 String appClassName = null; try { ApplicationInfo ai = this.getPackageManager() .getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = ai.metaData; if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) { appClassName = bundle.getString("APPLICATION_CLASS_NAME"); // className 是配置在xml文件中的。 } else { Log.i("demo", "have no application class name"); return; } } catch (NameNotFoundException e) { Log.i("demo", "error:"+Log.getStackTraceString(e)); e.printStackTrace(); } //有值的话调用该Applicaiton Object currentActivityThread = RefInvoke.invokeStaticMethod( "android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {}); Object mBoundApplication = RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mBoundApplication"); Object loadedApkInfo = RefInvoke.getFieldOjbect( "android.app.ActivityThread$AppBindData", mBoundApplication, "info"); //把当前进程的mApplication 设置成了null RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, null); Object oldApplication = RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mInitialApplication"); // http://www.codeceo.com/article/android-context.html ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke .getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications"); mAllApplications.remove(oldApplication); // 删除oldApplication ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke .getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplicationInfo"); ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke .getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "appInfo"); appinfo_In_LoadedApk.className = appClassName; appinfo_In_AppBindData.className = appClassName; Application app = (Application) RefInvoke.invokeMethod( "android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null }); // 执行 makeApplication(false,null) RefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app); ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mProviderMap"); Iterator it = mProviderMap.values().iterator(); while (it.hasNext()) { Object providerClientRecord = it.next(); Object localProvider = RefInvoke.getFieldOjbect( "android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider"); RefInvoke.setFieldOjbect("android.content.ContentProvider", "mContext", localProvider, app); } Log.i("demo", "app:"+app); app.onCreate(); } } /** * 释放被加壳的apk文件,so文件 * @param data * @throws IOException */ private void splitPayLoadFromDex(byte[] apkdata) throws IOException { int ablen = apkdata.length; //取被加壳apk的长度 这里的长度取值,对应加壳时长度的赋值都可以做些简化 byte[] dexlen = new byte[4]; System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4); ByteArrayInputStream bais = new ByteArrayInputStream(dexlen); DataInputStream in = new DataInputStream(bais); int readInt = in.readInt(); System.out.println(Integer.toHexString(readInt)); byte[] newdex = new byte[readInt]; //把被加壳的源程序apk内容拷贝到newdex中 System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt); //这里应该加上对于apk的解密操作,若加壳是加密处理的话 // 对源程序Apk进行解密 newdex = decrypt(newdex); // 写入apk文件 File file = new File(apkFileName); try { FileOutputStream localFileOutputStream = new FileOutputStream(file); localFileOutputStream.write(newdex); localFileOutputStream.close(); } catch (IOException localIOException) { throw new RuntimeException(localIOException); } // 分析被加壳的apk文件 ZipInputStream localZipInputStream = new ZipInputStream( new BufferedInputStream(new FileInputStream(file))); while (true) { ZipEntry localZipEntry = localZipInputStream.getNextEntry(); // 这个也遍历子目录 if (localZipEntry == null) { localZipInputStream.close(); break; } // 取出被加壳apk用到的so文件,放到libPath中(data/data/包名/payload_lib) String name = localZipEntry.getName(); if (name.startsWith("lib/") && name.endsWith(".so")) { File storeFile = new File(libPath + "/" + name.substring(name.lastIndexOf('/'))); storeFile.createNewFile(); FileOutputStream fos = new FileOutputStream(storeFile); byte[] arrayOfByte = new byte[1024]; while (true) { int i = localZipInputStream.read(arrayOfByte); if (i == -1) break; fos.write(arrayOfByte, 0, i); } fos.flush(); fos.close(); } localZipInputStream.closeEntry(); } localZipInputStream.close(); } /** * 从apk包里面获取dex文件内容(byte) * @return * @throws IOException */ private byte[] readDexFileFromApk() throws IOException { ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream(); ZipInputStream localZipInputStream = new ZipInputStream( new BufferedInputStream(new FileInputStream( this.getApplicationInfo().sourceDir))); while (true) { ZipEntry localZipEntry = localZipInputStream.getNextEntry(); if (localZipEntry == null) { localZipInputStream.close(); break; } if (localZipEntry.getName().equals("classes.dex")) { byte[] arrayOfByte = new byte[1024]; while (true) { int i = localZipInputStream.read(arrayOfByte); if (i == -1) break; dexByteArrayOutputStream.write(arrayOfByte, 0, i); } } localZipInputStream.closeEntry(); } localZipInputStream.close(); return dexByteArrayOutputStream.toByteArray(); } //直接返回数据,读者可以添加自己解密方法 private byte[] decrypt(byte[] srcdata) { for(int i=0;i<srcdata.length;i++){ srcdata[i] = (byte)(0xFF ^ srcdata[i]); } return srcdata; } //以下是加载资源 protected AssetManager mAssetManager;//资源管理器 protected Resources mResources;//资源 protected Theme mTheme;//主题 protected void loadResources(String dexPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); mAssetManager = assetManager; } catch (Exception e) { Log.i("inject", "loadResource error:"+Log.getStackTraceString(e)); e.printStackTrace(); } Resources superRes = super.getResources(); superRes.getDisplayMetrics(); superRes.getConfiguration(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); mTheme = mResources.newTheme(); mTheme.setTo(super.getTheme()); } @Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; } @Override public Theme getTheme() { return mTheme == null ? super.getTheme() : mTheme; }
}
RelInvoke(反射工具类):
package com.example.packapk; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Field; public class RefInvoke { /** * 反射执行类的静态函数(public) * @param class_name 类名 * @param method_name 函数名 * @param pareTyple 函数的参数类型 * @param pareVaules 调用函数时传入的参数 * @return */ public static Object invokeStaticMethod(String class_name, String method_name, Class[] pareTyple, Object[] pareVaules){ try { Class obj_class = Class.forName(class_name); Method method = obj_class.getMethod(method_name,pareTyple); return method.invoke(null, pareVaules); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchMethodException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * 反射执行类的函数(public) * @param class_name * @param method_name * @param obj * @param pareTyple * @param pareVaules * @return */ public static Object invokeMethod(String class_name, String method_name, Object obj ,Class[] pareTyple, Object[] pareVaules){ try { Class obj_class = Class.forName(class_name); Method method = obj_class.getMethod(method_name,pareTyple); return method.invoke(obj, pareVaules); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchMethodException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * 反射得到类的属性(包括私有和保护) * @param class_name * @param obj * @param filedName * @return */ public static Object getFieldOjbect(String class_name,Object obj, String filedName){ try { Class obj_class = Class.forName(class_name); Field field = obj_class.getDeclaredField(filedName); field.setAccessible(true); return field.get(obj); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchFieldException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * 反射得到类的静态属性(包括私有和保护) * @param class_name * @param filedName * @return */ public static Object getStaticFieldOjbect(String class_name, String filedName){ try { Class obj_class = Class.forName(class_name); Field field = obj_class.getDeclaredField(filedName); field.setAccessible(true); return field.get(null); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchFieldException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * 设置类的属性(包括私有和保护) * @param classname * @param filedName * @param obj * @param filedVaule */ public static void setFieldOjbect(String classname, String filedName, Object obj, Object filedVaule){ try { Class obj_class = Class.forName(classname); Field field = obj_class.getDeclaredField(filedName); field.setAccessible(true); field.set(obj, filedVaule); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchFieldException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 设置类的静态属性(包括私有和保护) * @param class_name * @param filedName * @param filedVaule */ public static void setStaticOjbect(String class_name, String filedName, Object filedVaule){ try { Class obj_class = Class.forName(class_name); Field field = obj_class.getDeclaredField(filedName); field.setAccessible(true); field.set(null, filedVaule); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchFieldException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
分析:
1.首先通过java的反射,置换掉android.app.ActivityTread中的mClassLoader,作为加载解密出的APK的DexClassLoader,,该DexClassloader既加载了源程序,还以mClassLoader作为其父类,使得资源文件和系统代码能正确的被加载
代码解析:
加密程序的AndroidManifest文件也一定要添加上:
<meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.example.sourceapk.MyApplication"/>
且其他的activity必须和源程序保持一致性
例如:源程序为:
<activity android:name="com.example.sourceapk.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.example.sourceapk.SubActivity"></activity>
那么加密程序必须和其一模一样
需要源程序apk和加密程序apk的dex文件
package com.example.packdex; public class mymain { public static void main(String[] args) { try { File payloadSrcFile = new File("files/SourceApk.apk"); // 需要加壳的源程序 System.out.println("apk size:"+payloadSrcFile.length()); File packDexFile = new File("files/SourceApk.dex"); // 壳程序dex byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile)); // 以二进制形式读出源apk,并进行加密处理 byte[] packDexArray = readFileBytes(packDexFile); // 以二进制形式读出dex /* 合并文件 */ int payloadLen = payloadArray.length; int packDexLen = packDexArray.length; int totalLen = payloadLen + packDexLen + 4; // 多出4字节是存放长度的 byte[] newdex = new byte[totalLen]; // 申请了新的长度 // 添加解壳代码 System.arraycopy(packDexArray, 0, newdex, 0, packDexLen); // 先拷贝dex内容 // 添加加密后的解壳数据 System.arraycopy(payloadArray, 0, newdex, packDexLen, payloadLen); // 再在dex内容后面拷贝apk的内容 // 添加解壳数据长度 System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4); // 最后4字节为长度 // 修改DEX file size文件头 fixFileSizeHeader(newdex); // 修改DEX SHA1 文件头 fixSHA1Header(newdex); // 修改DEX CheckSum文件头 fixCheckSumHeader(newdex); String str = "files/classes.dex"; // 创建一个新文件 File file = new File(str); if (!file.exists()) { file.createNewFile(); } FileOutputStream localFileOutputStream = new FileOutputStream(str); localFileOutputStream.write(newdex); // 将新计算出的二进制dex数据写入文件 localFileOutputStream.flush(); localFileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } } // 直接返回数据,读者可以添加自己加密方法 private static byte[] encrpt(byte[] srcdata){ for (int i = 0; i < srcdata.length; i++) { srcdata[i] = (byte)(0xFF ^ srcdata[i]); } return srcdata; } ... }
完成所有的操作后还需要把新的apk进行签名操作,否则会导致错误发生
本项目的地址 :
github地址:https://github.com/lzh18972615051/AndroidShellCode