Android app反编译 攻与防

大概是2020年的时候,有一次,我们的竞争同行有另外一家公司要用我们的安卓软件app,拉了个群,告知他用一个软件多少钱,然后在群里发了一个我打包的apk包。结果就没有下文了。又过了一个月。我同事在那个要买我们apk的人的朋友圈,发现他拍的宣传照片和我们的app界面非常相似。来问我了。我那时候觉得我apk加固的还可以。代码也都做了混淆。应该不会被破解了。排除了这可能,后来公司领导还专门因为这个事开了会。讨论了下,都怀疑是有内鬼,把apk的解密卖了出去。然后这个事就不了了之了。

直到2024年,他们的设备过保了。我们公司又中了他们之前安装的设备的维保项目,然后,我忽然想起来之前发生过的这个事情来。就和那边同时远程解决售后问题的时候。才发现我打包的apk被人套了个壳从新打包了。名字和图标都被改了。但是进去以后的界面和功能都没有变。很明显我的android apk包被人破解了。重新打了包。并且商用了。整个西藏所有的网点都用了这个apk。当时整个人瞬间感觉都不好了。那天心情一直很低落。证明我的加固根本就没有起大的作用。我发现这个事的时候就给老板发了消息。老板没有回我信息。我就胡思乱想是不是老板会质疑我的能力。心里也挺痛恨那个破解我apk并且商用的公司和人。这就像我的孩子换了个衣服让人给拐走了。是我做好安全防护。等上班以后。我就开始研究他是怎么给我破解的。自己把自己做的老版本给破解了。非常简单10分钟都不到就从新打包运行了。

先简单介绍下我的app。第一个界面一个激活界面。激活界面需要我们授权,授权通过激活界面以后。会直接跳入设置界面就行相应的配置。激活后的设备。下次进入软件会直接进到设置界面。他反编译从新打包的我的软件版本是1.9版本。android1.9版本的app下的build.gradle配置如下:

Groovy 复制代码
apply plugin: 'com.android.application'
android {
    compileSdkVersion 26
    defaultConfig {
        applicationId ""
        minSdkVersion 17
        targetSdkVersion 26
        versionCode 1
        versionName "1.7"
        testInstrumentationRunner "androidx.support.test.runner.AndroidJUnitRunner"
        //加载红外的cpp文件
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
        //abiFilter 'armeabi'
        ndk {
            abiFilters 'armeabi'
        }
        // Enabling multidex support.
        multiDexEnabled true
    }
    //签名配置。 配置名release{ //配置内容 }
    signingConfigs {
        release {
            try {
                storeFile file("plbs.jks")
                storePassword KEYSTORE_PASSWORD
                keyAlias "plbs-android"
                keyPassword KEY_PASSWORD
            }
            catch (ex) {
                throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
            }
        }
    }
    buildTypes {
        debug {//允许debug
            debuggable true
            signingConfig signingConfigs.release  //在buildTypes中指定release时的signingConfigs对应的配置名
        }
        release {
            signingConfig signingConfigs.release  //在buildTypes中指定release时的signingConfigs对应的配置名
            zipAlignEnabled true    //Zipalign优化
            shrinkResources true  // 移除无用的resource文件
            minifyEnabled true//是否混淆
            debuggable true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
    //gradle3.01 从新的打包
    android.applicationVariants.all { variant ->
        variant.outputs.all {
            outputFileName = "重庆金融数据播放${defaultConfig.versionName}_${releaseTime()}.apk"
        }
    }
}
def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
dependencies {
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    testImplementation 'junit:junit:4.12'
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.3'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.jiechic.library:xUtils:2.6.14'
    implementation 'com.squareup.picasso:picasso:2.5.2'
    implementation 'com.squareup.okhttp3:okhttp:3.7.0'
    implementation 'com.google.code.gson:gson:2.8.0'
    implementation 'com.squareup.okio:okio:1.13.0'
    implementation 'org.greenrobot:eventbus:3.0.0'
    implementation 'com.github.bumptech.glide:glide:3.7.0'
    implementation 'org.java-websocket:Java-WebSocket:1.4.0'
    //  字体font
    implementation 'uk.co.chrisjenx:calligraphy:2.2.0'
    //粒子动画
    implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2'
    //加载dll 动态文件
    implementation files('libs/jna-3.1.0.jar')
    implementation files('libs/fastjson-1.2.12.jar')
    implementation files('libs/xstream-1.4.7.jar')
    implementation files('libs/zxing.jar')
    implementation files('libs/sun.misc.BASE64Decoder.jar')
    implementation 'com.android.support:multidex:1.0.1'
    //webview加载网页太慢用 第三方的webview加载网页
    implementation 'ren.yale.android:cachewebviewlib:2.1.8'

}

反编译攻:先演示下是如何通过反编译从新打包1.9版本的:

首先从这个网址下载apktool工具,链接如下:iBotPeaches / Apktool / Downloads --- Bitbucket

我下载的是apktool2.4的版本的工具,有需要的自己下载相应的版本。

1.9版本的第一个需要激活的界面如下图:名字叫LoginActivity

通过第一个界面我们授权激活以后就跳转第二个设置界面:名字叫CCBSetActivity。如下图:

将我的1.9版本的apk包放到apktool工具所在的文件夹目录下,如下图:

复制代码

在此目录下打开cmd命令。

在cmd中输入以下命令:

Groovy 复制代码
java -jar apktool.jar d -f 1.9.apk -o out

命令的目的是将1.9.apk反编译到out目录下: 如下图结束运行后,可以看到在目录下多了个out文件夹。

打开out就可以看到反编译后的文件。

在清单文件里就可以修改入口了。

清单文件和android打包前的目录是完全一致的。如下:

XML 复制代码
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.meiaomei.absadplayerrotation">
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.GET_TASKS"/>
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
    <application android:allowBackup="true" android:debuggable="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:name="com.meiaomei.absadplayerrotation.AbsPlbsApplication" android:supportsRtl="true" android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:launchMode="singleTask" android:name="com.meiaomei.absadplayerrotation.activity.CCBSetActivity" android:screenOrientation="unspecified" android:windowSoftInputMode="stateHidden"/>
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:launchMode="singleTask" android:name="com.meiaomei.absadplayerrotation.activity.LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <receiver 
    </application>
</manifest>

可以看到入口文件是LoginActivity。将入口文件的LoginActivity修改为配置界面的CCBSetActivity。这样就可以跳过激活界面直接到配置界面。在这个反编译的文件里也可以修改xml布局文件。我们简单修改下CCBSetActivity的布局文件将系统设置4个字改为 我被狗币反编译了。如果你想修改app的图标还有app的名字在这里都可以改。改完从新打包即可。那个公司的人肯定也是这么反编译我的包的。然后换名字从新打包。

修改完清单文件AndroidManifest.xml的入口直接为系统设置界面CCBSetActivity。并且将设置界面的系统设置4个字改为 我被狗币反编译了。

然后我们从新打包这个修改过的out文件:

cmd里执行以下命令:

XML 复制代码
java -jar apktool.jar b -o 1.9change.apk out

将out重新打包为 1.9change.apk。

cmd执行完成后。我们可以看到已经在这个文件目录下有了一个叫1.9change.apk的包了。

这个重新打包的包拖动到模拟器里安装的时候会显示安装失败。因为这个从新打包的包没有签名。就是平常我们在as里面点击这个Generate Signed Bundle or APK按钮。然后输入app的别名账号密码等的这个按钮,输入完成就打包了。

因为缺少这个签名。我们可以新建一个AS项目。然后快速的打包。会生成一个.jks文件。将这个生产的.jks文件拷贝至这个目录下。用这个.jks去替换原来这个apk的签名文件。

此时在cmd里在次执行:

XML 复制代码
jarsigner -verbose -keystore aqy.jks -storepass youpassword -signedjar 1.9change_signed.apk 1.9change.apk key0

这里的youpassword就是你给新项目打包的密码:替换成自己的即可。这个命令就是用aqy.jks的签名文件去给这个1.9change.apk签名生成一个1.9change_signed.apk的签名的apk包。敲完回车就会看到在重新签名。

签名成功以后。会在该目录下生成一个叫做 1.9change_signed.apk的包。

把这个包拖动到夜神模拟器安装,会显示安装成功。至此完美的绕过了我设置的激活界面。打开以后看下。我在out文件里修改设置界面的布局也生效了。如下图:

我能确定他就是通过这个方法反编译了我的1.9版本。

反编译防

事后我也在思考。我的失误就是我把激活界面放在首页以后在别的界面没有做校验。在首页激活软件以后。应该在每一个界面都去问下软件。是否是激活的?如果不是,就不让他继续使用软件。我的两个界面没有任何联系。导致他直接跳过了我的激活界面。把程序的入口修改了。

因为我们的软件是对接的第三方的接口,接口是对方给的,接口也没有关于安全方面的校验。没办法通过后台去做是否激活的校验。并且软件是运行在内网中的。不涉及到互联网,安装后我们也没法去控制。导致别的公司破解以后就可以直接接入运行。

我后来的修改就是每个界面加了是否激活的校验,如果没有激活就强制退出软件。

在修改的过程中,我发现android的res目录下的valuse下的文件很不安全。被对方反编译以后。这个目录下的所有string都暴漏了。一些重要的数据最好都不要存在这里,如果万不得已非要存,最后采用密文存储。不要用明文。并且把名字都修改下。

之前我把激活界面的3DES对称加密的秘钥都存这里,如下图:(真是够胆大的哈)

XML 复制代码
    <string name="KEY">npiTUAL6InCrYPLA++dbtlQfnqCNoVG5</string>
    <string name="IV">SqHzP3eZlXE+</string>

现在的修改是将他们都隐藏起来。把名字修改成很普通的。很容易混淆视听的名字。

然后将3DES的秘钥字符串用AES的公钥将加密一下。用的时候用私钥先进行解密。然后再去使用。如图所示,涉及到非常重要的数据。从命名到数据都尽量的隐藏起来。让破解的人不知道你这个是做什么的。这样就不好去修改和破解。

XML 复制代码
   <string name="t1">3C8659595676946B94D7F7C7F9A27141FC84A96049349B9116DC2E3BBCADD379E9C9063D1E7DC4013DF0BD5A135B895F3477A00E6B01C7D92D9A04BDC7D4B8623A162C69915AAFAA1CA2F5464EE7C95383174A1450AD265874B37B99812404D9947DEBB57FCA249D5F96E8ED4A5D45A25F7FB84F704D9144280C93638B96F67FB59C2AEB5DD94268613F8508E32423ACB7001E1D2994C799F9B80C8EA7BF229F2F7CA0FB2301580D31A8046F87589279E4191DAE0446B3367E06D064157E4109A2DB0CE0A9DC92422905519327525E343B07D4BBE2F81328127F39B51E067A6D612F7AD703FF75E5DECA1C50B80D79052CA7687CA491FDB288269DFC5BAAB876</string>
   <string name="t2">264578595676946B94D7F7C7F9A27141FC84A96049349B9116DC2E3BBCADD379E9C9063D1E7DC4013DF0BD5A135B895F3477A00E6B01C7D92D9A04BDC7D4B8623A162C69915AAFAA1CA2F5464EE7C95383174A1450AD265874B37B99812404D9947DEBB57FCA249D5F96E8ED4A5D45A25F7FB84F704D9144280C93638B96F67FB59C2AEB5DD94268613F8508E32423ACB7001E1D2994C799F9B80C8EA7BF229F2F7CA0FB2301580D31A8046F87589279E4191DAE0446B3367E06D064157E4109A2DB0CE0A9DC92422905519327525E343B07D4BBE2F81328127F39B51E067A6D612F7AD703FF75E5DECA1C50B80D79052CA7687CA491FDB288269DFC5BAAB876</string>

加密key 和iv的时候可以用以下的RSA加密工具类:

java 复制代码
import android.os.Environment;
import java.io.File;
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import Decoder.BASE64Decoder;
import Decoder.BASE64Encoder;
/**
 * RSA 工具类(生成/保存密钥对、加密、解密)
 */
public class RSAUtils {
    public static void main() {
        // 随机生成一对密钥(包含公钥和私钥)
        KeyPair keyPair = null;
        try {
            keyPair = RSAUtils.generateKeyPair();
            // 获取 公钥 和 私钥
            PublicKey pubKey = keyPair.getPublic();
            PrivateKey priKey = keyPair.getPrivate();
            // 保存 公钥 和 私钥
            RSAUtils.saveKeyForEncodedBase64(pubKey, new File(Environment.getExternalStorageDirectory() + "/"+"pub1.txt"));
            RSAUtils.saveKeyForEncodedBase64(priKey, new File(Environment.getExternalStorageDirectory() + "/"+"pri1.txt"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 算法名称
     */
    private static final String ALGORITHM = "RSA";
    /**
     * 密钥长度
     */
    private static final int KEY_SIZE = 2048;
    /**
     * 加密, 返回加密后的数据
     */
    public static byte[] clientEncrypt(byte[] plainData, File pubFile) {
        // 读取公钥文件, 创建公钥对象
        PublicKey pubKey = null;
        byte[] cipher = {};
        try {
            pubKey = getPublicKey(IOUtils.readFile(pubFile));
            // 用公钥加密数据
            cipher = RSAUtils.encrypt(plainData, pubKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cipher;
    }

    /**
     * 解密, 返回解密后的数据
     */
    public static byte[] serverDecrypt(byte[] cipherData, File priFile) {
        // 读取私钥文件, 创建私钥对象
        PrivateKey priKey = null;
        byte[] plainData = {};
        try {
            priKey = getPrivateKey(IOUtils.readFile(priFile));
            // 用私钥解密数据
            plainData = RSAUtils.decrypt(cipherData, priKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return plainData;
    }

    /**
     * 随机生成密钥对(包含公钥和私钥)
     */
    public static KeyPair generateKeyPair() throws Exception {
        // 获取指定算法的密钥对生成器
        KeyPairGenerator gen = KeyPairGenerator.getInstance(ALGORITHM);
        // 初始化密钥对生成器(指定密钥长度, 使用默认的安全随机数源)
        gen.initialize(KEY_SIZE);
        // 随机生成一对密钥(包含公钥和私钥)
        return gen.generateKeyPair();
    }

    /**
     * 将 公钥/私钥 编码后以 Base64 的格式保存到指定文件
     */
    public static void saveKeyForEncodedBase64(Key key, File keyFile) throws IOException {
        // 获取密钥编码后的格式
        byte[] encBytes = key.getEncoded();
        // 转换为 Base64 文本
        String encBase64 = new BASE64Encoder().encode(encBytes);
        // 保存到文件
        IOUtils.writeFile(encBase64, keyFile);
    }

    /**
     * 根据公钥的 Base64 文本创建公钥对象
     */
    public static PublicKey getPublicKey(String pubKeyBase64) throws Exception {
        // 把 公钥的Base64文本 转换为已编码的 公钥bytes
        byte[] encPubKey = new BASE64Decoder().decodeBuffer(pubKeyBase64);
        // 创建 已编码的公钥规格
        X509EncodedKeySpec encPubKeySpec = new X509EncodedKeySpec(encPubKey);
        // 获取指定算法的密钥工厂, 根据 已编码的公钥规格, 生成公钥对象
        return KeyFactory.getInstance(ALGORITHM).generatePublic(encPubKeySpec);
    }

    /**
     * 根据私钥的 Base64 文本创建私钥对象
     */
    public static PrivateKey getPrivateKey(String priKeyBase64) throws Exception {
        // 把 私钥的Base64文本 转换为已编码的 私钥bytes
        byte[] encPriKey = new BASE64Decoder().decodeBuffer(priKeyBase64);
        // 创建 已编码的私钥规格
        PKCS8EncodedKeySpec encPriKeySpec = new PKCS8EncodedKeySpec(encPriKey);
        // 获取指定算法的密钥工厂, 根据 已编码的私钥规格, 生成私钥对象
        return KeyFactory.getInstance(ALGORITHM).generatePrivate(encPriKeySpec);
    }

    /**
     * 公钥加密数据
     */
    public static byte[] encrypt(byte[] plainData, PublicKey pubKey) throws Exception {
        // 获取指定算法的密码器
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        // 初始化密码器(公钥加密模型)
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
        // 加密数据, 返回加密后的密文
        return cipher.doFinal(plainData);
    }
    /**
     * 私钥解密数据
     */
    public static byte[] decrypt(byte[] cipherData, PrivateKey priKey) throws Exception {
        // 获取指定算法的密码器
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        // 初始化密码器(私钥解密模型)
        cipher.init(Cipher.DECRYPT_MODE, priKey);
        // 解密数据, 返回解密后的明文
        return cipher.doFinal(cipherData);
    }
}
java 复制代码
import java.io.*;
/**
 * IO 工具类, 读写文件
 */
public class IOUtils {
    public static void writeFile(String data, File file) throws IOException {
        OutputStream out = null;
        try {
            out = new FileOutputStream(file);
            out.write(data.getBytes());
            out.flush();
        } finally {
            close(out);
        }
    }
    public static String readFile(File file) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try {
            in = new FileInputStream(file);
            out = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len = -1;
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
            out.flush();
            byte[] data = out.toByteArray();
            return new String(data);
        } finally {
            close(in);
            close(out);
        }
    }
    public static void close(Closeable c) {
        if (c != null) {
            try {
                c.close();
            } catch (IOException e) {
                // nothing
            }
        }
    }
}

在任意一个activity的界面里,生成公钥和私钥,用公钥去加密需要保护的数据:如下图:

需要注意的是公钥加密完后的byte[]数组,转换为string的时候,不可以用new String(byte[]a)直接去转,加密返回的byte[]中存在负值,例如-116 。负数在Ascii码中是没有对应的值。所以直接用你new String()去转byte的时候,采用文件默认字符集UTF-8,处理时遇到解析不了的值,会用 '\uFFFD'代替,显示为 '�'。解密的时候,再次采用string.getbytes()将� 转换回byte的话 ,由于找不到对应的Ascii值。会造成数据丢失。导致解密以后的数据和加密的数据对不上。所以采用HexUtils转换一下。解密的时候也用这个工具类将string转为byte。最后再去new String()。就会拿到原来加密前的字符串。然后我们将这个加密后的秘钥放在string里。这样就算破解了也不能马上就看到这个秘钥。

java 复制代码
    private void aa() {
        RSAUtils.main();//随机生成公钥和私钥
        File pubFile = new File(Environment.getExternalStorageDirectory() + "/"+"pub1.txt");
        byte keyByteEncy[]=clientEncrypt("npiTUAL6InCrYPLA++dbtlQfnqCNoVG5".getBytes(),pubFile);
        File priFile = new File(Environment.getExternalStorageDirectory() + "/"+"pri1.txt");
        String s=HexUtils.bytesToHexString(keyByteEncy);
        Log.e(TAG, "aa1: "+s );
        byte keyDecry[]= serverDecrypt(HexUtils.hexStringToBytes(s),priFile);
        Log.e(TAG, "aa2: "+new String(keyDecry));
    }


//2024-12-18 15:39:16.206 4688-4688/com.hyw.safetyofficertiku E/SecureLife: aa1: 71CD5EE6506C035F749E356495E2C0847F1A38488C45569B24F74414C8EEB566DEF81B6B40F44BDBC5E3D1F5F82365FF84C931E1E34B5D77AA66062B4775D90343018F04931A3A92C401176041E2183EABABAA8DCAE0036E5028119905D44284A3E963DCB3AD4B88702C1396FC85808D5503A947F6EDB0C541BA05E8C26E28094DC56F9BE12E7856D12AC680DFC93B82BA1972CF24AE48097563334186D993508EC24F5A1C334DCF177935D825F257779487740BCDFFF2C84D81CD9C9EDC91D9E87EE25A9CE4DEDB46158E57C428C95EDE70DF372035782B3F45CA3AF962937EEA4967A0EDE6D860176DB737014DB00CCAA9DDFF5A8F902CEBA33FA49032EF70
//2024-12-18 15:39:16.214 4688-4688/com.hyw.safetyofficertiku E/SecureLife: aa2: npiTUAL6InCrYPLA++dbtlQfnqCNoVG5

此处附上HeXUtils类。

java 复制代码
public class HexUtils {
    private static final char[] DIGITS = {
            '0', '1', '2', '3', '4', '5', '6', '7'
            , '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    public static String bytesToHexString(byte[] data) {
        String string = "";
        try {
            char[] chars = new char[data.length << 1];
            //十六进制数一个四位,byte一个八位
            for (int i = 0, j = 0; i < data.length; i++) {
                chars[j++] = DIGITS[(data[i] >> 4) & 0x0F];
                chars[j++] = DIGITS[data[i] & 0x0F];
            }
            string = new String(chars);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return string;
    }

    public static byte[] hexStringToBytes(String data) {
        //两个四位十六进制字符合成一个八位byte
        byte[] bytes = new byte[data.length() / 2];
        char[] chars = data.toCharArray();
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) ((hexCharToByte(chars[i * 2]) << 4) | hexCharToByte(chars[i * 2 + 1]));
        }
        return bytes;
    }
    private static byte hexCharToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }
}

以上都是我们的代码被反编译以后从新打包我们做出的一些防护措施。

如何防止反编译后从新打包:

我也拿我的另外一个程序做了反编译的测试,发现在用apk工具从新打包的时候会报错。这个程序的build.gradle文件是这样配置的。如图:

Groovy 复制代码
apply plugin: 'com.android.application'
//使用greendao
apply plugin: 'org.greenrobot.greendao'
android {
    compileSdkVersion 30
    defaultConfig {
        applicationId ""
        minSdkVersion 22
        targetSdkVersion 30
        versionCode 6
        versionName "6.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    aaptOptions {
        noCompress "pdf"  //表示不让aapt压缩的文件后缀 (不压缩assets下的文件)
    }
    buildTypes {
        release {
            minifyEnabled true
            zipAlignEnabled true    //Zipalign优化
            shrinkResources true  // 移除无用的resource文件
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    greendao {
        //版本号,升级时可配置
        schemaVersion 1
        //这里可以指定编译成功后(DaoMaster、DaoSession、DAOS类)的文件目录,可以不指定
        daoPackage '.greendao'
        targetGenDir 'src/main/java'
    }

    lintOptions {
        checkReleaseBuilds false
    }

    //gradle3.01 从新的打包
    android.applicationVariants.all { variant ->
        variant.outputs.all {
            outputFileName = "唐朝${defaultConfig.versionName}.apk"
        }
    }
}
def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    //  实现沉浸式状态栏
    implementation 'com.gyf.barlibrary:barlibrary:2.3.0'
    implementation'androidx.appcompat:appcompat:1.0.0'
    implementation'androidx.recyclerview:recyclerview:1.0.0'
    implementation'androidx.constraintlayout:constraintlayout:1.1.2'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation files('libs/java_json-1.2.75.jar')
    implementation files('libs/zxing.jar')
    implementation files('libs/sun.misc.BASE64Decoder.jar')
    implementation 'com.github.barteksc:android-pdf-viewer:2.8.2'
    implementation 'commons-io:commons-io:2.8.0'
    //数据库加密类
    implementation 'net.zetetic:android-database-sqlcipher:4.2.0'
    implementation 'org.greenrobot:eventbus:3.0.0'
    //greendao依赖
    api 'org.greenrobot:greendao:3.2.2'
    implementation 'com.google.code.gson:gson:2.8.0'
    implementation 'androidx.multidex:multidex:2.0.0'
}

目标版本都是android sdk版本为30。引入了androidx的包去替换了原来的 support依赖。

继续使用前面的方法去破解先将包解压到out文件夹下。然后修改入口。从新打包。打包的时候会报错。如下代码:

Groovy 复制代码
F:\android反编译\apktool2.4>java -jar apktool.jar d -f androidx.apk -o out
I: Using Apktool 2.2.4 on androidx.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: C:\Users\hyw\AppData\Local\apktool\framework\1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

F:\android反编译\apktool2.4>java -jar apktool.jar b -o androidx_change.apk out
I: Using Apktool 2.2.4
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
W: F:\android反编译\apktool2.4\out\AndroidManifest.xml:1: error: No resource identifier found for attribute 'compileSdkVersion' in package 'android'
W:
W: F:\android反编译\apktool2.4\out\AndroidManifest.xml:1: error: No resource identifier found for attribute 'compileSdkVersionCodename' in package 'android'
W:
W: F:\android反编译\apktool2.4\out\AndroidManifest.xml:14: error: No resource identifier found for attribute 'appComponentFactory' in package 'android'
W:
Exception in thread "main" brut.androlib.AndrolibException: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [C:\Users\hyw\AppData\Local\Temp\brut_util_Jar_5866709263106215219.tmp, p, --forced-package-id, 127, --min-sdk-version, 22, --target-sdk-version, 30, --version-code, 6, --version-name, 6.0, --no-version-vectors, -F, C:\Users\hyw\AppData\Local\Temp\APKTOOL333571415111046532.tmp, -0, arsc, -0, db, -0, txt, -0, arsc, -I, C:\Users\hyw\AppData\Local\apktool\framework\1.apk, -S, F:\android反编译\apktool2.4\out\res, -M, F:\android反编译\apktool2.4\out\AndroidManifest.xml]
        at brut.androlib.Androlib.buildResourcesFull(Androlib.java:496)
        at brut.androlib.Androlib.buildResources(Androlib.java:430)
        at brut.androlib.Androlib.build(Androlib.java:329)
        at brut.androlib.Androlib.build(Androlib.java:267)
        at brut.apktool.Main.cmdBuild(Main.java:230)
        at brut.apktool.Main.main(Main.java:83)
Caused by: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [C:\Users\hyw\AppData\Local\Temp\brut_util_Jar_5866709263106215219.tmp, p, --forced-package-id, 127, --min-sdk-version, 22, --target-sdk-version, 30, --version-code, 6, --version-name, 6.0, --no-version-vectors, -F, C:\Users\hyw\AppData\Local\Temp\APKTOOL333571415111046532.tmp, -0, arsc, -0, db, -0, txt, -0, arsc, -I, C:\Users\hyw\AppData\Local\apktool\framework\1.apk, -S, F:\android反编译\apktool2.4\out\res, -M, F:\android反编译\apktool2.4\out\AndroidManifest.xml]
        at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:441)
        at brut.androlib.Androlib.buildResourcesFull(Androlib.java:482)
        ... 5 more
Caused by: brut.common.BrutException: could not exec (exit code = 1): [C:\Users\hyw\AppData\Local\Temp\brut_util_Jar_5866709263106215219.tmp, p, --forced-package-id, 127, --min-sdk-version, 22, --target-sdk-version, 30, --version-code, 6, --version-name, 6.0, --no-version-vectors, -F, C:\Users\hyw\AppData\Local\Temp\APKTOOL333571415111046532.tmp, -0, arsc, -0, db, -0, txt, -0, arsc, -I, C:\Users\hyw\AppData\Local\apktool\framework\1.apk, -S, F:\android反编译\apktool2.4\out\res, -M, F:\android反编译\apktool2.4\out\AndroidManifest.xml]
        at brut.util.OS.exec(OS.java:95)
        at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:435)
        ... 6 more

反编译从新打包的时候会报错。显示android版本相关的一些信息。我怀疑是apktool工具版本太低。将apktool工具升级到最新的版本2.10。然后去打包。还是会报这个错误。我是不是可以理解为升级到androdx以后。安全性得到了提高呢。也许还是我1.9版本一样。我以为防的还可以结果被轻松绕过了入口。如果有朋友重新打包的时候解决了这个报错。

麻烦也和我分享下。不知攻,焉能防。

总结:重要的数据,采用明文存的尽量都改为密文存储。还有google更新的时候,尽量都跟着最新版本的走。不要偷懒。sdk版本越高,安全性越好。

被人反编译apk的感受还是很不好的。尤其被人商用了。搞了好多钱。让人内心沮丧,心情低落。幸好我去找老板的时候,老板也没说我啥,我义愤填膺的说要去起诉他们。至少给他们一个警告。让他们停止侵害。老板说你怎么证明他们用的咱们的。我说反编译完了对代码行数 。再说我们还有软著。奈何老板不想搞事。我只能把漏洞堵上。然后再写一篇文章记录下。

相关推荐
jiet_h8 分钟前
Android Compose Modifier
android
drebander3 小时前
MySQL EXPLAIN 详解:一眼看懂查询计划
android·数据库·mysql
KpLn_HJL4 小时前
leetcode - 1530. Number of Good Leaf Nodes Pairs
android·java·leetcode
s_nshine5 小时前
AndroidStudio AppInspection和Profiler无法选择进程
android·androidstudio·profiler·appinspection
戴着眼镜看不清5 小时前
从腾讯云的恶意文件查杀学习下PHP的eval函数
android·python·gpt·学习·网络安全·木马·中转api
DexterLien6 小时前
使用 Lambda 创建 Authorizer 对 API Gateway 访问进行鉴权
android·java·gateway
恋猫de小郭7 小时前
Android 16 Baklava 来了,来看看开发者预览版给我们带来了什么
android·前端·flutter
R1cardo8 小时前
android emulator 设置代理
android·emulator
小西米米lxtx10 小时前
Android GO 版本锁屏声音无效问题
android·java·framework·android系统开发