• 写一个Android第一代壳

    前言

    在讲解之前,需要了解加壳之前,必须先了解Android动态加载Apk的原理及机制,这次基于这样的机制,实现加壳。

    加壳实例

    被加壳的APP

    随便写个demo的App来,至于为什么这么写,可以回顾一些Android动态加载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
    package com.qqq.myapp;

    import android.app.Activity;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    import android.widget.LinearLayout;
    import android.widget.Toast;

    public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private Activity that;
    public MainActivity()
    {
    super();
    }
    public MainActivity(Activity content)
    {
    super();
    that = content;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    if (that != null)
    {
    LinearLayout lv = new LinearLayout(that);
    that.setContentView(lv);
    Button btn = new Button(that);
    btn.setText("点我");
    lv.addView(btn);
    btn.setOnClickListener(this);
    }else {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    that = this;
    }
    }
    private void getMessage(String str)
    {

    Toast.makeText(that,str,Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onClick(View v) {
    getMessage("加载成功,并点击");
    }
    }

    壳APP

    壳App执行流程就是将被合并的apk数据从classes.dex中提取出来,然后解密,最后加载Activity

    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
    package com.qqq.test.shell;

    import android.app.Application;
    import android.content.Context;

    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.util.zip.ZipEntry;
    import java.util.zip.ZipInputStream;

    /**
    * Created by admin on 2022/7/6.
    */

    public class App extends Application{
    String odexPath,apkFileName;
    @Override
    protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    try
    {
    File odex = this.getDir("odex", MODE_PRIVATE);
    odexPath = odex.getAbsolutePath();
    apkFileName = odexPath + "/myapp.apk";
    File dexFile = new File(apkFileName);
    if (!dexFile.exists())
    {
    dexFile.createNewFile();
    byte[] dexdata = readDexFromApk();
    splitApk(dexdata);
    }
    }catch (Exception e)
    {

    }

    }


    private void splitApk(byte[] dexdata) throws IOException{
    int dexlen = dexdata.length;
    byte[] dexlendata = new byte[4];
    System.arraycopy(dexdata,dexlen - 4, dexlendata,0,4 );
    int apklen = (new DataInputStream(new ByteArrayInputStream(dexlendata))).readInt();
    //得到apk数据长度

    byte[] newDex = new byte[apklen];
    System.arraycopy(dexdata, dexlen - apklen - 4, newDex, 0, apklen);

    newDex = decrpt(newDex);//进行解密处理


    //写入文件
    File file = new File(apkFileName);
    try
    {
    FileOutputStream out = new FileOutputStream(file);
    out.write(newDex);
    out.close();
    } catch (IOException e)
    {
    throw new RuntimeException(e);
    }

    //可添加so文件处理,将apk中的so文件拿出来,放到/data/data/app包名/lib目录下,由于本实例不需要,这里省略

    }

    private byte[] decrpt(byte[] newDex) {
    return newDex;
    }

    private byte[] readDexFromApk() throws IOException{
    ByteArrayOutputStream dex = new ByteArrayOutputStream();
    ZipInputStream local = new ZipInputStream(new BufferedInputStream(new FileInputStream(getApplicationInfo().sourceDir)));
    while (true)
    {
    ZipEntry zipentry = local.getNextEntry();
    if (zipentry == null)
    {
    local.close();
    break;
    }
    if (zipentry.getName().equals("classes.dex"))
    {
    byte[] b = new byte[1024];
    int i = -1;
    while ((i = local.read(b)) != -1)
    {
    dex.write(b, 0, i);
    }
    }
    local.closeEntry();
    }
    local.close();
    return dex.toByteArray();
    }
    }

    下面的代码是实现加载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
    package com.qqq.test.shell;


    import android.annotation.SuppressLint;
    import android.app.Activity;
    import android.content.pm.PackageInfo;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;


    import java.io.File;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Method;

    import dalvik.system.DexClassLoader;

    public class GoodActivity extends AppCompatActivity {
    private String mClss;
    private String dexPath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.dexPath = this.getDir("odex", MODE_PRIVATE).getAbsolutePath() + File.separator +"myapp.apk";
    lanuchTargetActivity();
    }


    @SuppressLint("NewApi")
    protected void lanuchTargetActivity()
    {
    PackageInfo packageinfo = getPackageManager().getPackageArchiveInfo(dexPath, 1);
    if ((packageinfo != null)&&(packageinfo.activities.length>0))
    {
    String activityName = packageinfo.activities[0].name;
    mClss = activityName;
    launchTargetActivity(mClss);
    }
    }
    @SuppressLint("NewApi")
    protected void launchTargetActivity(final String className)
    {
    File dexOutputDir = this.getDir("loadedDexDir", 0);
    final String dexOutputPath = dexOutputDir.getAbsolutePath();
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputPath,null,classLoader);
    try
    {
    Class<?> localClass = dexClassLoader.loadClass(className);//实例化activity
    Constructor<?> localConstructor = localClass.getConstructor(new Class[]{Activity.class});
    Object instense = localConstructor.newInstance(new Object[]{this});

    Method onCreate = localClass.getDeclaredMethod("onCreate", new Class[]{Bundle.class});
    onCreate.setAccessible(true);
    Bundle bundle = new Bundle();
    onCreate.invoke(instense, new Object[]{bundle});
    }
    catch (Exception e)
    {
    e.getStackTrace();
    }
    }

    }

    这里需要替换Application入口,先执行解密代码,然后再正常执行壳APP的正常逻辑,即加载已经解密后的App。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qqq.test.shell">
    <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:name=".App"
    android:theme="@style/AppTheme">
    <activity android:name=".GoodActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    </application>
    </manifest>

    加壳工程

    新建java工程,然后写代码,将壳app(shell.apk)和被加壳app(myapp.apk)放在同一目录下。加壳的流程就是

    • 解压壳APP,读取解压的classes.dex的数据
    • 读取需要加壳的app的数据,并按需加密
    • 将壳的数据和已被加密的数据合并,并在数据结尾用4个字节记录加密数据长度
    • 将合并的新的dex数据进行依次修复file_size、SHA1_signature及checksum数据
    • 把修复好的新dex数据输出,并替换原shell目录下的classes.dex文件
    • 压缩替换classes.dex的文件目录为新的apk
    • 最后对新的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
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    import java.io.*;
    import java.security.MessageDigest;
    import java.util.zip.Adler32;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipFile;
    import java.util.Enumeration;
    import java.util.zip.ZipOutputStream;
    import java.util.zip.ZipInputStream;

    public class enCode {
    public static String path = "E:\\testShell\\";
    public static void main(String[] args) {
    try {
    File srcFile = new File(path + "myapp.apk");
    d("需要加密的apk大小",String.valueOf(srcFile.length()));

    File shellFile = new File(path + "shell.apk");
    d("原始壳apk大小",String.valueOf(shellFile.length()));

    File shellDexFile = getZipFile(shellFile, "classes.dex");
    d("原始壳dex大小",String.valueOf(shellDexFile.length()));

    byte[] shellDex = getFilebyte(shellDexFile);
    byte[] enCodedApk = encrpt(getFilebyte(srcFile));


    int enCodedApkLen = enCodedApk.length;
    d("加密后的apk大小",String.valueOf(enCodedApkLen));

    int shellDexLen = shellDex.length;
    d("原始shellDex大小",String.valueOf(shellDexLen));

    int toatalLen = enCodedApkLen + shellDexLen + 4;
    d("合并后shell-encodedApk.dex大小",String.valueOf(toatalLen));

    byte[] newDex = new byte[toatalLen];
    System.arraycopy(shellDex, 0, newDex, 0, shellDexLen);
    System.arraycopy(enCodedApk, 0, newDex, shellDexLen, enCodedApkLen);
    System.arraycopy(intToByte(enCodedApkLen), 0, newDex, toatalLen - 4, 4);

    fixNewdexHeaderSize(newDex);
    d("修复file_size","完成");

    fixSHA1Header(newDex);
    d("修复signature","完成");

    fixNewdexHeqaderChecksum(newDex);
    d("修复checksum","完成");

    File outNewDexFile = new File(path + "shell-encodedApk.dex");
    outNewDexFile.createNewFile();
    FileOutputStream outStream = new FileOutputStream(outNewDexFile);
    outStream.write(newDex);
    outStream.flush();
    outStream.close();
    d("输出","shell-encodedApk.dex");


    File news = new File(path + "shell/classes.dex");
    news.createNewFile();
    FileOutputStream outSteam = new FileOutputStream(news);
    outSteam.write(newDex);
    outSteam.flush();
    outSteam.close();
    d("输出","shell-encodedApk.dex到shell/classes.dex");

    File newApk = new File(path + "myapp-encoded.apk");
    toZip(path + "shell", new FileOutputStream(newApk), true);
    d("输出","myapp-encoded.apk");

    //签名apk
    apksign(path + "myapp-encoded.apk");

    } catch(Exception e)
    {}
    }

    public static void apksign(String filePath) throws Exception
    {
    String cmd = "java -jar " + path +"signApk/signapk.jar "
    + path + "signApk/testkey.x509.pem "
    + path + "signApk/testkey.pk8 "
    + filePath + " "
    + filePath.replace(".apk", "_signed.apk");
    d("签名中", cmd);
    BufferedReader br = null;
    Process process = Runtime.getRuntime().exec(cmd);
    InputStream is = process.getInputStream();
    br = new BufferedReader(new InputStreamReader(is,"utf-8"));
    StringBuffer sb = new StringBuffer();
    sb.append(br.readLine());
    String content;
    while ((content = br.readLine()) != null) {
    content = br.readLine();
    sb.append(content);
    }
    br.close();
    br = null;
    d("签名信息",content);
    }

    //修改dex头 file_size值
    public static byte[] fixNewdexHeaderSize(byte[] data)
    {
    byte[] newLenData = intToByte(data.length);
    d("修复file_size值为",Integer.toHexString(data.length));
    byte[] refs = new byte[4];
    for (int i = 0; i < 4; i++) {//高位低位转换
    refs[i] = newLenData[newLenData.length - 1 - i];
    d("-修复为",Integer.toHexString(newLenData[i]));
    }
    System.arraycopy(refs, 0, data, 32, 4);//修改32-35
    return data;
    }
    public static byte[] fixSHA1Header(byte[] data) throws Exception
    {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    md.update(data, 32, data.length - 32);
    byte[] newData = md.digest();
    System.arraycopy(newData, 0, data, 12, 20);
    //输出sha-1值,可有可无
    String hexstr = "";
    for (int i = 0; i < newData.length; i++) {
    hexstr += Integer.toString((newData[i] & 0xff) + 0x100, 16).substring(1);
    }
    d("-sha1 signature修改为",hexstr);
    return data;
    }
    public static byte[] fixNewdexHeqaderChecksum(byte[] data)
    {
    Adler32 adler = new Adler32();
    adler.update(data,12,data.length-12);//从12到文件末计算
    long value = adler.getValue();
    byte[] newData = longToByte(value);
    d("修复checkSum值为",Long.toHexString(value));
    byte[] refs = new byte[4];
    for (int i = 0; i < 4; i++) {//高位低位转换
    refs[i] = newData[newData.length - 1 - i];//取低4位
    d("-修复为",Long.toHexString(newData[4 + i]));
    }
    System.arraycopy(refs, 0, data, 8, 4);//修改8-13
    return data;
    }
    public static byte[] longToByte(long num) {
    byte[] b = new byte[8];
    for (int i = 7; i > 0; i--)
    {
    b[i] = (byte)(num%256);
    num >>= 8;
    }
    return b;
    }
    public static byte[] intToByte(int num)
    {
    byte[] b = new byte[4];
    for (int i = 3; i > 0; i--)
    {
    b[i] = (byte)(num%256);
    num >>= 8;
    }
    return b;
    }

    public static byte[] encrpt(byte[] data)
    {
    return data;
    }

    public static byte[] getFilebyte(File file) throws IOException
    {
    byte[] buffer = new byte[1024];
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    FileInputStream fis = new FileInputStream(file);
    while(true)
    {
    int readCount = -1;
    if ((readCount = fis.read(buffer)) != -1)
    {
    byteArrayOutputStream.write(buffer, 0, readCount);
    }else
    {
    break;
    }
    }
    fis.close();
    return byteArrayOutputStream.toByteArray();
    }

    public static File getZipFile(File file,String zipIn) throws Exception
    {
    ZipFile zip = new ZipFile(file);
    Enumeration<? extends ZipEntry> files = zip.entries();
    ZipEntry entry = null;
    File outFile = null;
    while (files.hasMoreElements())
    {
    entry = files.nextElement();
    d("-解压中",entry.getName());

    String outPath = (path +"shell/"+ entry.getName()).replace("\\*", "/");
    File outfile = new File(outPath.substring(0,outPath.lastIndexOf("/")));
    if (!outfile.exists())
    {
    outfile.mkdirs();
    }

    InputStream in = zip.getInputStream(entry);
    OutputStream out = new FileOutputStream(outPath);
    byte[] buf1 = new byte[1024];
    int len;
    while((len=in.read(buf1))>0)
    {
    out.write(buf1,0,len);
    }
    in.close();
    out.close();

    if (entry.getName().equals(zipIn))
    {
    outFile = new File(outPath);
    }
    }
    zip.close();
    return outFile;
    }
    /**
    * @param srcDir 压缩文件夹路径
    * @param out 压缩文件输出流
    * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
    * false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
    * @throws RuntimeException 压缩失败会抛出运行时异常
    */
    public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure)
    throws RuntimeException
    {
    long start = System.currentTimeMillis();
    ZipOutputStream zos = null;
    try {
    zos = new ZipOutputStream(out);
    File sourceFile = new File(srcDir);
    compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
    long end = System.currentTimeMillis();
    d("压缩用时",String.valueOf(end - start) + "ms");
    } catch (Exception e) {
    throw new RuntimeException("zip error from ZipUtils", e);
    } finally {
    if (zos != null) {
    try {
    zos.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }

    /**
    * 递归压缩方法
    * @param sourceFile 源文件
    * @param zos zip输出流
    * @param name 压缩后的名称
    * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
    * false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
    * @throws Exception
    */
    private static void compress(File sourceFile, ZipOutputStream zos, String name,
    boolean KeepDirStructure) throws Exception
    {
    byte[] buf = new byte[2 * 1024];
    d("-压缩中",name);
    if (sourceFile.isFile()) {
    // 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
    zos.putNextEntry(new ZipEntry(name.substring(6)));
    // copy文件到zip输出流中
    int len;
    FileInputStream in = new FileInputStream(sourceFile);
    while ((len = in.read(buf)) != -1) {
    zos.write(buf, 0, len);
    }
    // Complete the entry
    zos.closeEntry();
    in.close();
    } else {
    File[] listFiles = sourceFile.listFiles();
    if ((listFiles == null || listFiles.length == 0) && !name.equals("shell")) {
    // 需要保留原来的文件结构时,需要对空文件夹进行处理
    if (KeepDirStructure) {
    // 空文件夹的处理
    zos.putNextEntry(new ZipEntry(name + "/"));
    // 没有文件,不需要文件的copy
    zos.closeEntry();

    }
    } else {
    for (File file : listFiles) {
    // 判断是否需要保留原来的文件结构
    if (KeepDirStructure) {
    // 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
    // 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
    compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
    } else {
    compress(file, zos, file.getName(), KeepDirStructure);
    }
    }
    }
    }
    }

    public static void d(String str,String str1)
    {
    long time = System.currentTimeMillis();
    System.out.println("["+time+"] "+str+":"+str1);
    }
    }

    运行效果

    image-20220806213231321

    探究

    加壳,就是为了避免反编译直接被读取apk的正常代码,这里实现的加壳基本上实现了这样一个目的,完全看不见被合并的源apk逻辑。在JEB中的分析结果如下:

    image-20220806215751970

    但是这样加壳是有缺陷的,而且挺致命的。因为这里的加壳后运行会进行解密,并且将解密的apk结果放在了/data/data/私有目录下,就会导致已经root了的设备能够直接读取。这样的加载数据形式一般被称为落地加载,而不是在内存中加载。这样,即使落地加载一秒钟,也有可能被读取源apk数据。懂了这点,所以以后研究研究不落地加载的实例吧。

    其次,虽然直接反编译不能直接看见被加密的代码逻辑,但是可以看到壳app的运行逻辑,也并不安全,最好将壳 apk的代码逻辑采用native的方式实现,这种方法也是相对安全的,但是至少能增加逆向者的时间花费。

    参考链接

    下载实例:https://pan.baidu.com/s/15G8eB48ySQk1vkKh9LcGPg (kata)

    Android之Apk加壳:https://blog.csdn.net/LVXIANGAN/article/details/84956476

    Android第一代壳加固原理及实现:https://zhuanlan.zhihu.com/p/373056904

    java压缩zip方法:https://www.cnblogs.com/stromgao/p/16086838.html

    用SignApk.jar对APK进行签名:http://t.zoukankan.com/fengxing999-p-11978037.html

    上一篇:
    SO加固之section加密
    下一篇:
    安洵2022——InvisiableMaze解法
    本文目录
    本文目录