首页
社区
课程
招聘
安卓壳学习记录
发表于: 2025-3-5 14:05 4187

安卓壳学习记录

2025-3-5 14:05
4187

前言:

电脑为window11系统,用到工具有IDA,Androidstudio,JEB,jadx,frida等,这段时间学习了一下安卓壳的相关知识,将途中所学记录了下来,学习很多大佬的文章所以难免有雷同之处,请见谅。

第一代壳Dex整体加固

第一代壳主要是对dex/apk文件整体加密,然后自定义类加载器动态加载dex/apk文件并执行。在动态加载dex/apk文件的时候有落地加载和不落地加载,落地加载就是通过DexClassLoader从磁盘加载dex/apk文件,不落地加载就是通过InMemoryDexClassLoader从内存中加载dex/apk文件。

Dex文件结构

一代壳实现原理

app的启动流程

通过将原apk进行加密后保存在加壳apk的dex文件尾部,在加壳apk运行时将dex文件尾部加密的原apk文件解密后进行动态加载。
壳代码需要最早获取到加壳apk的执行时机,所以加壳apk的Application实现了attachContextApplication函数,此函数在handleBindApplication函数中通过调用makeApplicaton进行调用,是一个APP进程最早的执行入口。加壳apk需要进行如下操作

  • attachContextApplication解密原apk保存到磁盘文件中(不落地加载可以不保存磁盘文件),动态加载解密后的apk并替换掉mClassLoader
  • Application::onCreate重定位Application,调用原apk的Application的onCreate函数

一代壳实践

步骤:

一个源程序,即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
package com.example.pack_test_one;
 
import androidx.appcompat.app.AppCompatActivity;
 
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
 
import com.example.pack_test_one.databinding.ActivityMainBinding;
 
public class MainActivity extends AppCompatActivity {
 
    // Used to load the 'pack_test_one' library on application startup.
    static {
        System.loadLibrary("pack_test_one");
    }
 
    private ActivityMainBinding binding;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        Log.i("test", "rea111111111:"+getApplicationContext());
        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());
    }
 
    /**
     * A native method that is implemented by the 'pack_test_one' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

加壳工具

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
import hashlib
import os
import shutil
import sys
import zlib
import zipfile
import subprocess
def process_apk_data(apk_data: bytes):
    """
    用于处理apk数据,比如加密,压缩等,都可以放在这里。
    :param apk_data:
    :return:
    """
    return apk_data
 
# 使用前需要修改的部分
keystore_path = 'D:/Android/Key.jks'
keystore_password = '######'
key0_password="########"
src_apk_file_path = 'D:/Android/Projection/pack_test_one/app/debug/app-debug.apk'
shell_apk_file_path = 'D:/Android/Projection/unshell/app/debug/app-debug.apk'
buildtools_path = 'D:/Android/SDK/build-tools/34.0.0'
 
# 承载apk的文件名
carrier_file_name= 'classes.dex'
# 中间文件夹
intermediate_dir= 'intermediates'
intermediate_apk_name='app-debug.apk'
intermediate_aligned_apk_name='app-debug-aligned.apk'
intermediate_apk_path="intermediates/app-debug.apk"
intermediate_carrier_path=os.path.join(intermediate_dir, carrier_file_name)
intermediate_aligned_apk_path=os.path.join(intermediate_dir,intermediate_aligned_apk_name)
if os.path.exists(intermediate_dir):
    shutil.rmtree(intermediate_dir)
os.mkdir(intermediate_dir)
 
# 解压apk
shell_apk_file=zipfile.ZipFile(shell_apk_file_path)
shell_apk_file.extract(carrier_file_name,intermediate_dir)
 
# 查找dex
if not os.path.exists(os.path.join(intermediate_dir, carrier_file_name)):
    raise FileNotFoundError(f'{carrier_file_name} not found')
 
src_dex_file_path= os.path.join(intermediate_dir, carrier_file_name)
 
#读取
src_apk_file=open(src_apk_file_path, 'rb')
src_dex_file=open(src_dex_file_path, 'rb')
 
src_apk_data=src_apk_file.read()
src_dex_data=src_dex_file.read()
 
# 处理apk数据
processed_apk_data=process_apk_data(src_apk_data)
processed_apk_size=len(processed_apk_data)
 
# 构建新dex数据
new_dex_data=src_dex_data+processed_apk_data+int.to_bytes(processed_apk_size,8,'little')
 
# 更新文件大小
file_size=len(processed_apk_data)+len(src_dex_data)+8
new_dex_data=new_dex_data[:32]+int.to_bytes(file_size,4,'little')+new_dex_data[36:]
 
# 更新sha1摘要
signature=hashlib.sha1().digest()
new_dex_data=new_dex_data[:12]+signature+new_dex_data[32:]
 
# 更新checksum
checksum=zlib.adler32(new_dex_data[12:])
new_dex_data=new_dex_data[:8]+int.to_bytes(checksum,4,'little')+new_dex_data[12:]
 
# 写入新dex
intermediate_carrier_file= open(intermediate_carrier_path, 'wb')
intermediate_carrier_file.write(new_dex_data)
intermediate_carrier_file.close()
src_apk_file.close()
src_dex_file.close()
 
os.environ.update({'PATH': os.environ.get('PATH') + f';{buildtools_path}'})
 
# 复制文件,Windows 下使用 copy 命令
shell_apk_file_path = r"D:/Android/Projection/unshell/app/debug/app-debug.apk"
intermediate_apk_path = r"intermediates/app-debug.apk"
 
# 检查源文件是否存在
if os.path.exists(shell_apk_file_path):
    try:
        # 使用 shutil.copy 进行文件复制
        shutil.copy(shell_apk_file_path, intermediate_apk_path)
        print(f"文件已成功复制到 {intermediate_apk_path}")
    except Exception as e:
        print(f"复制文件时出错: {e}")
else:
    print(f"源文件不存在: {shell_apk_file_path}")
   
# 切换到 intermediate_dir 目录
os.chdir(intermediate_dir)
 
# 使用 7z 压缩文件,替代 zip 命令
zip_command = f'"D://tool//7-Zip//7z.exe" a -tzip "{intermediate_apk_name}" "{carrier_file_name}"'
print( f'"D://tool//7-Zip//7z.exe" a -tzip "{intermediate_apk_name}" "{carrier_file_name}"')
r = subprocess.getoutput(zip_command)
print(r)
 
# 切换回上一级目录
os.chdir('../')
 
# 对齐 APK,使用 Android SDK 的 zipalign 工具
zipalign_command = f'zipalign 4 "{intermediate_apk_path}" "{intermediate_aligned_apk_path}"'
r = subprocess.getoutput(zipalign_command)
print(r)
 
# 使用 apksigner 工具进行签名
 
apksigner_command = f'apksigner sign -ks "{keystore_path}" --ks-pass pass:{keystore_password} "{intermediate_aligned_apk_path}"'
 
# 使用 subprocess.run 直接执行命令
result = subprocess.run(apksigner_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,input=f"{key0_password}\n")
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
 
# 复制对齐并签名后的 APK 到输出目录
copy_final_command = f'copy "{intermediate_aligned_apk_path}" "./app-out.apk"'
r = subprocess.getoutput(copy_final_command)
print(r)
 
print('Success')

看到打包这块想起自己之前每次都懒得写脚本,每次都自己手动重打包。。。

这个脚本的话,是linux下的,我改写一下命令后,os.popen无法很完美的实现copy等一些命令,copy也很奇怪,推测是无法对和自身所在盘符不同的文件操作,很难受。将文件都复制到了脚本这里,改用了subprocess,签名的时候需要二次输入密码交互所以用run,其他getoutput即可。

具体的作用就是将源程序apk和脱壳程序构建成了下面这个

脱壳程序

脱壳程序要做的就是在启动流程调用Application的attachBaseContext时点释放我们的源程序并将其替换为我们的application实例。

代理application

添加android:name=".ProxyApplication"还有替换活动为我们脱壳后实际会运行的活动。

提取apk文件

先将apk文件的dex提出,然后根据之前加壳时在文件中留下的大小信息,将源apk提取出来。

然后将apk存储起来。

dexclassloader加载apk

so需要手动提取一下到目标路径。

之后需要通过反射替换一下classloader。这里替换classloader的原因需要了解一下双亲委派模式。

这样子在之后加载我们的源程序的活动时,可以正常找到类。

主要代码

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
package com.example.unshell;
 
import android.app.*// 导入 android.app 包中的所有类
import android.content.*// 导入 android.content 包中的所有类
import android.os.Build;
import android.util.*// 导入 android.util 包中的所有类
 
import java.io.*// 导入 java.io 包中的所有类
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.*// 导入 java.nio 包中的所有类
import java.util.*;
import java.util.zip.*// 导入 java.util.zip 包中的所有类
 
import dalvik.system.DexClassLoader;
 
public class ProxyApplication extends Application {
    private String srcApkPath;
    private String optDirPath;
    private String libDirPath;
 
    private ZipFile getApkZip() throws IOException {
        Log.i("demo", this.getApplicationInfo().sourceDir);
        ZipFile apkZipFile = new ZipFile(this.getApplicationInfo().sourceDir);
        return apkZipFile;
    }
 
    private byte[] readDexFileFromApk() throws IOException {
        /* 从本体apk中获取dex文件 */
        ZipFile apkZip = this.getApkZip();
        ZipEntry zipEntry = apkZip.getEntry("classes.dex");
        InputStream inputStream = apkZip.getInputStream(zipEntry);
        byte[] buffer = new byte[1024];
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int length;
        while ((length = inputStream.read(buffer)) > 0) {
            baos.write(buffer, 0, length);
        }
        return baos.toByteArray();
    }
 
    private byte[] reverseProcessApkData(byte[] data) { //解密函数
        for (int i = 0; i < data.length; i++) {
            data[i] = data[i];
        }
        return data;
    }
 
    private byte[] splitSrcApkFromDex(byte[] dexFileData) {
        /* 从dex文件中分离源apk文件 */
        int length = dexFileData.length;
        ByteBuffer bb = ByteBuffer.wrap(Arrays.copyOfRange(dexFileData, length - 8, length));
        bb.order(java.nio.ByteOrder.LITTLE_ENDIAN); // 设置为小端模式
        long processedSrcApkDataSize = bb.getLong(); // 读取这8个字节作为long类型的值
        byte[] processedSrcApkData = Arrays.copyOfRange(dexFileData, (int) (length - 8 - processedSrcApkDataSize), length - 8);
        byte[] srcApkData = reverseProcessApkData(processedSrcApkData);
        return srcApkData;
    }
 
    public static void replaceClassLoader1(Context context, DexClassLoader dexClassLoader) {
        ClassLoader pathClassLoader = ProxyApplication.class.getClassLoader();
        try {
            // 1.通过currentActivityThread方法获取ActivityThread实例
            Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread");
            Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");
            Object activityThreadObj = currentActivityThread.invoke(null);
            // 2.拿到mPackagesObj
            Field mPackagesField = ActivityThread.getDeclaredField("mPackages");
            mPackagesField.setAccessible(true);
            ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);
            // 3.拿到LoadedApk
            String packageName = context.getPackageName();
            WeakReference wr = (WeakReference) mPackagesObj.get(packageName);
            Object LoadApkObj = wr.get();
            // 4.拿到mClassLoader
            Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
            Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
            mClassLoaderField.setAccessible(true);
            Object mClassLoader = mClassLoaderField.get(LoadApkObj);
            Log.i("mClassLoader", mClassLoader.toString());
            // 5.将系统组件ClassLoader给替换
            mClassLoaderField.set(LoadApkObj, dexClassLoader);
        } catch (Exception e) {
            Log.i("demo", "error:" + Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }
 
    private void extractSoFiles(File srcApkFile, File libDir) throws IOException {
        // 获取当前设备的架构
        String abi = Build.CPU_ABI; // 或者使用 Build.SUPPORTED_ABIS 来获取更全面的信息
 
        // 提取 APK 中的 lib 文件夹并复制到 libDir 目录中
        ZipFile apkZip = new ZipFile(srcApkFile);
        ZipEntry libDirEntry = null;
        Enumeration entries = apkZip.entries();
 
        while (entries.hasMoreElements()) {
            ZipEntry entry = entries.nextElement();
            // 找到 lib 文件夹并提取里面的 .so 文件
            if (entry.getName().startsWith("lib/") && entry.getName().endsWith(".so")) {
                // 检查 .so 文件是否属于当前设备架构
                if (entry.getName().contains("lib/" + abi + "/")) {
                    File destSoFile = new File(libDir, entry.getName().substring(entry.getName().lastIndexOf("/") + 1));
                    // 创建目标文件夹
                    if (!destSoFile.getParentFile().exists()) {
                        destSoFile.getParentFile().mkdirs();
                    }
                    // 复制 .so 文件
                    try (InputStream inputStream = apkZip.getInputStream(entry);
                         FileOutputStream fos = new FileOutputStream(destSoFile)) {
                        byte[] buffer = new byte[1024];
                        int length;
                        while ((length = inputStream.read(buffer)) > 0) {
                            fos.write(buffer, 0, length);
                        }
                    }
                }
            }
        }
    }
 
 
    @Override
    protected void attachBaseContext(Context base) {
        Log.i("demo", "attachBaseContext");
        super.attachBaseContext(base);
        try {
            byte[] dexFileData = this.readDexFileFromApk();
            byte[] srcApkData = this.splitSrcApkFromDex(dexFileData);
            // 创建储存apk的文件夹,写入src.apk
            File apkDir = base.getDir("apk_out", MODE_PRIVATE);
            srcApkPath = apkDir.getAbsolutePath() + "/src.apk";
            File srcApkFile = new File(srcApkPath);
            srcApkFile.setWritable(true);
            try (FileOutputStream fos = new FileOutputStream(srcApkFile)) {
                Log.i("demo", String.format("%d", srcApkData.length));
                fos.write(srcApkData);
            }
            srcApkFile.setReadOnly(); // 受安卓安全策略影响,dex必须为只读
            Log.i("demo", "Write src.apk into " + srcApkPath);
 
            // 新建加载器
            File optDir = base.getDir("opt_dex", MODE_PRIVATE);
            File libDir = base.getDir("lib_dex", MODE_PRIVATE);
            optDirPath = optDir.getAbsolutePath();
            libDirPath = libDir.getAbsolutePath();
 
            // 提取 .so 文件到 lib_dex 目录
            extractSoFiles(srcApkFile, libDir);
 
            ClassLoader pathClassLoader = ProxyApplication.class.getClassLoader();
            DexClassLoader dexClassLoader = new DexClassLoader(srcApkPath, optDirPath, libDirPath, pathClassLoader);
            Log.i("demo", "Successfully initiate DexClassLoader.");
            // 修正加载器
            replaceClassLoader1(base, dexClassLoader);
            Log.i("demo", "ClassLoader replaced.");
        } catch (Exception e) {
            Log.i("demo", "error:" + Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }
}

安装时如果报这个错的话是因为zipaligned工具自身的问题,这里需要添加在application下添加**android:extractNativeLibs="true"**安装时才不会报错。

启动的是我们的源程序。

jadx里显示的是我们的壳。

一代壳脱壳

一代壳的防护性并不高,frida-dump应该几乎是秒杀一代壳,不多赘述。

二代壳—函数抽取壳

二代壳完全复现的话感觉时间会花费较多,所以我会以这个项目的代码作为学习。

ac9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3L8s2g2G2P5h3g2K6K9i4q4A6N6g2)9J5c8Y4m8Q4x3V1k6V1M7s2c8Q4x3X3g2Z5N6r3#2D9

该项目的代码有两个部分,proccessor和shell

proccessor是对apk进行字节提取然后重新打包处理成加壳apk的模块,shell模块最终生成的dex和so集成到加壳的apk中。

不过在开始前对dex的文件结构有些了解比较好,因为函数抽取,抽取的就是dex文件中的codeitem,具体是codeItem的insns,它保存了函数的实际字节码。

processor的工作流程:

shell模块工作流程:

处理Androidmanifest.xml

Androidmainfest.xml在编译为apk时,会以axml格式存放,不是普通可读的形式,该项目采取的ManifestEditor进行解析。

对于Androidmanifest.xml,我们主要的操作是,备份原Application的类名和写入壳的代理application类名。也就是在代理Application中实现一些加载dex或者HOOK等操作。

提取xml的代码段:

以及还有相应的写入Application类的操作。

1
2
3
4
5
6
7
public static void writeApplicationName(String inManifestFile, String outManifestFile, String newApplicationName){
    ModificationProperty property = new ModificationProperty();
    property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME,newApplicationName));
 
    FileProcesser.processManifestFile(inManifestFile, outManifestFile, property);
 
}

提取CodeItem

我们可以随便找一个dex放010里看一下。

然后我们具体提取的是CodeItem的insns,insns是函数实现的具体字节码。

提取所用工具为安卓提供的dx工具,同样存放在lib目录下。

该项目实现的函数是extractAllMethods,具体的实现先不谈了,结果就是读取所有的classdef(一个类的定义)的所有函数,并是否进行函数抽取。提取的数据怎么存储比较好呢,之前遇到的一道题是直接提取出来作为静态值最后填回去了。该项目的话是存储到了instruction结构体里,再看下这个结构体。

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
public class Instruction {
 
    public int getOffsetOfDex() {
        return offsetOfDex;
    }
 
    public void setOffsetOfDex(int offsetOfDex) {
        this.offsetOfDex = offsetOfDex;
    }
 
    public int getMethodIndex() {
        return methodIndex;
    }
 
    public void setMethodIndex(int methodIndex) {
        this.methodIndex = methodIndex;
    }
 
    public int getInstructionDataSize() {
        return instructionDataSize;
    }
 
    public void setInstructionDataSize(int instructionDataSize) {
        this.instructionDataSize = instructionDataSize;
    }
 
    public byte[] getInstructionsData() {
        return instructionsData;
    }
 
    public void setInstructionsData(byte[] instructionsData) {
        this.instructionsData = instructionsData;
    }
 
    @Override
    public String toString() {
        return "Instruction{" +
        "offsetOfDex=" + offsetOfDex +
        ", methodIndex=" + methodIndex +
        ", instructionDataSize=" + instructionDataSize +
        ", instructionsData=" + Arrays.toString(instructionsData) +
        '}';
    }
 
    //Offset in dex
    private int offsetOfDex;
    //Corresponding method_idx in dex
    private int methodIndex;
    //instructionsData size
    private int instructionDataSize;
    //insns data
    private byte[] instructionsData;
}

这个结构体是专门用来存储方法相关的东西的,如偏移,字节码。

Shell模块

这个部分是壳的主要逻辑。

Hook函数

mmap

Hook这个函数是为了使得我们加载dex时修改dex的属性,使其可写,才能将字节码回填。https://bbs.kanxue.com/thread-266527.htm

具体是给__prot参数追加PROT_WRITE属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void* fake_mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset){
    BYTEHOOK_STACK_SCOPE();
    int hasRead = (__prot & PROT_READ) == PROT_READ;
    int hasWrite = (__prot & PROT_WRITE) == PROT_WRITE;
    int prot = __prot;
 
    if(hasRead && !hasWrite) {
        prot = prot | PROT_WRITE;
        DLOGD("fake_mmap call fd = %p,size = %d, prot = %d,flag = %d",__fd,__size, prot,__flags);
    }
 
    void *addr = BYTEHOOK_CALL_PREV(fake_mmap,__addr,  __size, prot,  __flags,  __fd,  __offset);
    return addr;
}

LoadMethod

类加载的调用链

1
ClassLoader.java::loadClass -> DexPathList.java::findClass -> DexFile.java::defineClass -> class_linker.cc::LoadClass -> class_linker.cc::LoadClassMembers -> class_linker.cc::LoadMethod
  • **ClassLoader.java::loadClass** — 被调用时,检查类是否已加载。如果未加载,调用 findClass 来继续加载。
  • **DexPathList.java::findClass** — 查找指定的类,如果找不到,返回 ClassNotFoundException
  • **DexFile.java::defineClass** — 通过 DexFile 查找类的字节码,并在内存中创建一个 Class 对象。
  • **class_linker.cc::LoadClass** — 在 native 层解析类字节码,进行验证,并在内存中创建类的表示。
  • **class_linker.cc::LoadClassMembers** — 加载类的所有成员(字段、方法等)。
  • **class_linker.cc::LoadMethod** — 加载类的方法,处理方法的符号信息。
1
2
3
4
void ClassLinker::LoadMethod(const DexFile& dex_file,
                             const ClassDataItemIterator& it,
                             Handle klass,
                             ArtMethod* dst);

LoadMethod中有两个关键参数DexFile和ClassDataItemIterator

ClassDataItemIterator结构体的**code_off **是加载的函数的CodeItem相对于DexFile的偏移。所以LoadMethod是一个绝佳的HOOK点。

还有就是LoadMethod不同的安卓版本函数声明可能不同,所以需要做一个版本的不同处理,这个也确实在二代壳的ctf题中有看到。

填充insns

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
void LoadMethod(void *thiz, void *self, const void *dex_file, const void *it, const void *method,
                void *klass, void *dst) {
 
    if (g_originLoadMethod25 != nullptr
        || g_originLoadMethod28 != nullptr
        || g_originLoadMethod29 != nullptr) {
        uint32_t location_offset = getDexFileLocationOffset();
        uint32_t begin_offset = getDataItemCodeItemOffset();
        callOriginLoadMethod(thiz, self, dex_file, it, method, klass, dst);
 
        ClassDataItemReader *classDataItemReader = getClassDataItemReader(it,method);
 
 
        uint8_t **begin_ptr = (uint8_t **) ((uint8_t *) dex_file + begin_offset);
        uint8_t *begin = *begin_ptr;
        // vtable(4|8) + prev_fields_size
        std::string *location = (reinterpret_cast((uint8_t *) dex_file +
                                                                 location_offset));
        if (location->find("base.apk") != std::string::npos) {
 
            //code_item_offset == 0说明是native方法或者没有代码
            if (classDataItemReader->GetMethodCodeItemOffset() == 0) {
                DLOGW("native method? = %s code_item_offset = 0x%x",
                      classDataItemReader->MemberIsNative() ? "true" : "false",
                      classDataItemReader->GetMethodCodeItemOffset());
                return;
            }
 
            uint16_t firstDvmCode = *((uint16_t*)(begin + classDataItemReader->GetMethodCodeItemOffset() + 16));
            if(firstDvmCode != 0x0012 && firstDvmCode != 0x0016 && firstDvmCode != 0x000e){
                NLOG("this method has code no need to patch");
                return;
            }
 
            uint32_t dexSize = *((uint32_t*)(begin + 0x20));
 
            int dexIndex = dexNumber(location);
            auto dexIt = dexMap.find(dexIndex - 1);
            if (dexIt != dexMap.end()) {
 
                auto dexMemIt = dexMemMap.find(dexIndex);
                if(dexMemIt == dexMemMap.end()){
                    changeDexProtect(begin,location->c_str(),dexSize,dexIndex);
                }
 
 
                auto codeItemMap = dexIt->second;
                int methodIdx = classDataItemReader->GetMemberIndex();
                auto codeItemIt = codeItemMap->find(methodIdx);
 
                if (codeItemIt != codeItemMap->end()) {
                    CodeItem* codeItem = codeItemIt->second;
                    uint8_t  *realCodeItemPtr = (uint8_t*)(begin +
                                                classDataItemReader->GetMethodCodeItemOffset() +
                                                16);
 
                    memcpy(realCodeItemPtr,codeItem->getInsns(),codeItem->getInsnsSize());
                }
            }
        }
    }
}

codeitem的传递是assets/OoooooOooo。

加载dex

重新加载dex并替换dexElements,使ClassLoader从我们新加载的dex文件中加载类。这里还是之前那个双亲委派机制的原因。

这就是二代壳的大致过程,但是详细的具体实现没有过多去分析,后续有需求实现的话会再补充一下,

这里有一篇dpt-shell分析文章,比我分析的要详细很多https://tttang.com/archive/1728/

二代壳脱壳

最老实的方法就是找到字节码自己填充回去再分析,这是我之前ctf一道题这样干的。。。。

大体思路是找到函数填充回去的时机,如HOOK loadmethod来dump出已经填充回去的dex。

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
function Hook_Invoke() {
    var InvokeFunc = Module.findExportByName("libart.so", "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc")
    Interceptor.attach(InvokeFunc, {
        onEnter: function (args) {
            console.log(args[1])
            // args[0].add(8).readInt().toString(16) == 0
            console.log("Method Idx -> " + args[0].add(8).readInt().toString(16) + " " + args[0].add(12).readInt().toString(16))
            dump_memory(dex_begin, dex_size)
            //dump_memory(dex_begin, dex_size)
        },
        onLeave: function (retval) {}
    })
}
var dex_begin = 0
var dex_size = 0
  
function dump_memory(base, size) {
    Java.perform(function () {
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var dir = "/sdcard/Download/";
        var file_path = dir + "/mydumpp";
        var file_handle = new File(file_path, "w+");
        if (file_handle && file_handle != null) {
            Memory.protect(ptr(base), size, 'rwx');
            var libso_buffer = ptr(base).readByteArray(size);
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();
            console.log("[dump]:", file_path);
        }
    });
}
  
function getDexFile() {
  
    Hook_Invoke()
    Java.perform(function () {
  
        var ActivityThread = Java.use("android.app.ActivityThread")
        ActivityThread["performLaunchActivity"].implementation = function (args) {
            var env = Java.vm.getEnv()
            var classloader = this.mInitialApplication.value.getClassLoader()
            var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader")
            var elementsClass = Java.use("dalvik.system.DexPathList$Element")
            classloader = Java.cast(classloader, BaseDexClassLoader)
            var pathList = classloader.pathList.value
            var elements = pathList.dexElements.value
            console.log(elements)
            //console.log(elements.value)
            for (var i in elements) {
                var element;
                try {
                    element = Java.cast(elements[i], elementsClass);
                } catch (e) {
                    console.log(e)
                }
                //之后需要获取DexFile
                var dexFile = element.dexFile.value
                //getMethod(dexFile, classloader)
                var mCookie = dexFile.mCookie.value
                //$h获取句柄
                var length = env.getArrayLength(mCookie.$h, 0)
                //console.log(length)
                var Array = env.getLongArrayElements(mCookie.$h, 0)
                //第一个不是DexFile
                for (var i = 1; i < length; ++i) {
                    var DexFilePtr = Memory.readPointer(ptr(Array).add(i * 8))
                    var DexFileBegin = ptr(DexFilePtr).add(Process.pointerSize).readPointer()
                    var DexFileSize = ptr(DexFilePtr).add(Process.pointerSize * 2).readU32()
                    console.log(hexdump(DexFileBegin))
                    console.log("Size => " + DexFileSize.toString(16))
                    dex_begin = DexFileBegin
                    dex_size = DexFileSize
                }
            }
            return this.performLaunchActivity(arguments[0], arguments[1])
        }
    })
}
function main() {
   getDexFile()
  
}
setImmediate(main)

三代壳——VM

这个就更没有通解了,之后遇到再说吧。

36X加固免费版分析

基于oacia大佬的文章分析复现的,有很多重合部分,也有一些我个人在分析中的体悟和疑问。

https://bbs.kanxue.com/thread-280609.htm

加固入口。

额,这些字符串应该是加密状态的,但是我这里的jeb自动解密了。

大致分析下可以看出来这里的做的是根据不同的手机架构加载不同的libjiagu.so,位于assert目录下。

这里还有一个Dctloader的init,但是不知道为什么JEB并没有反编译出来。

这里调用的init虽然是空的,但是Dctloader的静态代码块会在加载到JVM时运行。

也就是加载了libjgdtc.so,加载失败会在/data/data/com.oacia.apk_protect/lib/下加载,但是在/data/data/com.oacia.apk_protect/lib/下并没有该so。

所以先分析libjiagu.so。

壳ELF导入导出表修复

1
frida -H 127.0.0.1:1234 -l .\hook.js -f "com.oacia.apk_protect"
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
function my_hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    dump_so("libjiagu_64.so");
                }
            }
        }
    );
}
  
function dump_so(so_name) {
    var libso = Process.getModuleByName(so_name);
    console.log("[name]:", libso.name);
    console.log("[base]:", libso.base);
    console.log("[size]:", ptr(libso.size));
    console.log("[path]:", libso.path);
    var file_path = "/data/data/com.oacia.apk_protect/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
    var file_handle = new File(file_path, "wb");
    if (file_handle && file_handle != null) {
        Memory.protect(ptr(libso.base), libso.size, 'rwx');
        var libso_buffer = ptr(libso.base).readByteArray(libso.size);
        file_handle.write(libso_buffer);
        file_handle.flush();
        file_handle.close();
        console.log("[dump]:", file_path);
    }
}
  
setImmediate(my_hook_dlopen("libjiagu_64.so"));

Sofixer修复一下。

1
SoFixer-Windows-64.exe -s libjiagu_64.so_0x7569f17000_0x274000.so -o fix.so -m 0x7569f17000 -d

加固壳反调试分析

这里在dump之后就意外退出了,想必存在反调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook_dlopen() {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    console.log("load " + path);
                }
            }
        }
    );
}
  
setImmediate(hook_dlopen)

通过hook dlopen函数查看中断前so被加载的顺序可以猜测应该存在于libjiagu_64中。

HOOK open函数

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
function my_hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    hook_open();
                }
            }
        }
    );
}
function hook_open(){
    var pth = Module.findExportByName(null,"open");
  Interceptor.attach(ptr(pth),{
      onEnter:function(args){
          this.filename = args[0];
          console.log("",this.filename.readCString())
      },onLeave:function(retval){
      }
  })
}
setImmediate(my_hook_dlopen,"libjiagu");

/proc/self/maps反调试,frida注入时会在maps留下re.frida.server之类的特征。

尝试了多种常见的HOOK,如将相关字段替换,但是仍会被检测退出,所以先采取作者的做法,读取一个不存在的文件或者直接return -1。

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
function my_hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    hook_proc_self_maps();
                }
            }
        }
    );
}
  
function hook_proc_self_maps() {
    const openPtr = Module.getExportByName(null, 'open');
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    var fakePath = "/data/data/com.oacia.apk_protect/maps";
    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open",pathname);
        if (pathname.indexOf("maps") >= 0) {
            console.log("find",pathname,",redirect to",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open(filename, flag);
        }
        var fd = open(pathnameptr, flag);
        return fd;
    }, 'int', ['pointer', 'int']));
}
  
  
setImmediate(my_hook_dlopen,"libjiagu");

classes.dex没有魔术头,dex处于加密状态。

查看释放时的堆栈调用

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
function addr_in_so(addr){
    var process_Obj_Module_Arr = Process.enumerateModules();
    for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
        if(addr>process_Obj_Module_Arr[i].base && addr
            console.log(addr.toString(16),"is in",process_Obj_Module_Arr[i].name,"offset: 0x"+(addr-process_Obj_Module_Arr[i].base).toString(16));
        }
    }
}
function hook_proc_self_maps() {
    const openPtr = Module.getExportByName(null, 'open');
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent";
    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open",pathname);//,Process.getCurrentThreadId()
        if (pathname.indexOf("maps") >= 0) {
            console.log("find",pathname+", redirect to",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open(filename, flag);
        }
        if (pathname.indexOf("dex") >= 0) {
            Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);
        }
        var fd = open(pathnameptr, flag);
        return fd;
    }, 'int', ['pointer', 'int']));
}
  
function my_hook_dlopen(soName='') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    //console.log(path);
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    hook_proc_self_maps();
                }
            }
        }
    );
}
setImmediate(my_hook_dlopen,'libjiagu');

可以看出来classes的堆栈调用和classes2几乎是一样的,所以处理classes的代码应该是一个循环。

从最近的堆栈调用查看,也就是0x19b780,是空的。

data段,可以猜到应该是将这段作为可执行内存然后执行了代码。

再次dump so文件,以open的路径中包含dex为HOOK时点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function my_hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    hook_proc_self_maps();
                     
                }
            }
        }
    );
}
var dump_once=false;
function hook_proc_self_maps() {
    const openPtr = Module.getExportByName(null, 'open');
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent";
    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open",pathname);//,Process.getCurrentThreadId()
        if (pathname.indexOf("maps") >= 0) {
            console.log("find",pathname+", redirect to",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open(filename, flag);
        }
        if (pathname.indexOf("dex") >= 0) {
            if(!dump_once){
                dump_once = true;
                dump_so("libjiagu_64.so");
            }
        }
        var fd = open(pathnameptr, flag);
        return fd;
    }, 'int', ['pointer', 'int']));
}
function dump_so(so_name) {
    var libso = Process.getModuleByName(so_name);
    console.log("[name]:", libso.name);
    console.log("[base]:", libso.base);
    console.log("[size]:", ptr(libso.size));
    console.log("[path]:", libso.path);
    var file_path = "/data/data/com.oacia.apk_protect/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
    var file_handle = new File(file_path, "wb");
    if (file_handle && file_handle != null) {
        Memory.protect(ptr(libso.base), libso.size, 'rwx');
        var libso_buffer = ptr(libso.base).readByteArray(libso.size);
        file_handle.write(libso_buffer);
        file_handle.flush();
        file_handle.close();
        console.log("[dump]:", file_path);
    }
}
  
setImmediate(my_hook_dlopen("libjiagu_64.so"));

重新用Sofixer修复so。

使用beyond compare进行一下比对。

填充的开始是elf的魔术头,program header table被加密了。

python脚本将其中的elf文件提取一下。

1
2
3
4
with open('libjiagu_64.so_0x7a69829000_0x274000_open_classes.dex.so','rb') as f:
    s=f.read()
with open('libjiagu_0xe7000.so','wb') as f:
    f.write(s[0xe7000::])

对dlopen交叉引用查看调用点。

作者真是神了,如果是我的话肯定不可能仅从这段循环和case就看出来是自实现linker。

导入相关定义,ida中依次点击View->Open subviews->Local Types,然后按下键盘上的Insert将下面的结构体添加到对话框中。

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
//IMPORTANT
//ELF64启用该宏
#define __LP64__  1
//ELF32启用该宏
//#define __work_around_b_24465209__  1
  
/*
//https://android.googlesource.com/platform/bionic/+/master/linker/Android.bp
架构为32位 定义__work_around_b_24465209__宏
arch: {
        arm: {cflags: ["-D__work_around_b_24465209__"],},
        x86: {cflags: ["-D__work_around_b_24465209__"],},
    }
*/
  
//android-platform\bionic\libc\include\link.h
#if defined(__LP64__)
#define ElfW(type) Elf64_ ## type
#else
#define ElfW(type) Elf32_ ## type
#endif
  
//android-platform\bionic\linker\linker_common_types.h
// Android uses RELA for LP64.
#if defined(__LP64__)
#define USE_RELA 1
#endif
  
//android-platform\bionic\libc\kernel\uapi\asm-generic\int-ll64.h
//__signed__-->signed
typedef signed char __s8;
typedef unsigned char __u8;
typedef signed short __s16;
typedef unsigned short __u16;
typedef signed int __s32;
typedef unsigned int __u32;
typedef signed long long __s64;
typedef unsigned long long __u64;
  
//A12-src\msm-google\include\uapi\linux\elf.h
/* 32-bit ELF base types. */
typedef __u32   Elf32_Addr;
typedef __u16   Elf32_Half;
typedef __u32   Elf32_Off;
typedef __s32   Elf32_Sword;
typedef __u32   Elf32_Word;
  
/* 64-bit ELF base types. */
typedef __u64   Elf64_Addr;
typedef __u16   Elf64_Half;
typedef __s16   Elf64_SHalf;
typedef __u64   Elf64_Off;
typedef __s32   Elf64_Sword;
typedef __u32   Elf64_Word;
typedef __u64   Elf64_Xword;
typedef __s64   Elf64_Sxword;
  
typedef struct dynamic{
  Elf32_Sword d_tag;
  union{
    Elf32_Sword d_val;
    Elf32_Addr  d_ptr;
  } d_un;
} Elf32_Dyn;
  
typedef struct {
  Elf64_Sxword d_tag;       /* entry tag value */
  union {
    Elf64_Xword d_val;
    Elf64_Addr d_ptr;
  } d_un;
} Elf64_Dyn;
  
typedef struct elf32_rel {
  Elf32_Addr    r_offset;
  Elf32_Word    r_info;
} Elf32_Rel;
  
typedef struct elf64_rel {
  Elf64_Addr r_offset;  /* Location at which to apply the action */
  Elf64_Xword r_info;   /* index and type of relocation */
} Elf64_Rel;
  
typedef struct elf32_rela{
  Elf32_Addr    r_offset;
  Elf32_Word    r_info;
  Elf32_Sword   r_addend;
} Elf32_Rela;
  
typedef struct elf64_rela {
  Elf64_Addr r_offset;  /* Location at which to apply the action */
  Elf64_Xword r_info;   /* index and type of relocation */
  Elf64_Sxword r_addend;    /* Constant addend used to compute value */
} Elf64_Rela;
  
typedef struct elf32_sym{
  Elf32_Word    st_name;
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info;
  unsigned char st_other;
  Elf32_Half    st_shndx;
} Elf32_Sym;
  
typedef struct elf64_sym {
  Elf64_Word st_name;       /* Symbol name, index in string tbl */
  unsigned char st_info;    /* Type and binding attributes */
  unsigned char st_other;   /* No defined meaning, 0 */
  Elf64_Half st_shndx;      /* Associated section index */
  Elf64_Addr st_value;      /* Value of the symbol */
  Elf64_Xword st_size;      /* Associated symbol size */
} Elf64_Sym;
  
  
#define EI_NIDENT   16
  
typedef struct elf32_hdr{
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half    e_type;
  Elf32_Half    e_machine;
  Elf32_Word    e_version;
  Elf32_Addr    e_entry;  /* Entry point */
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word    e_flags;
  Elf32_Half    e_ehsize;
  Elf32_Half    e_phentsize;
  Elf32_Half    e_phnum;
  Elf32_Half    e_shentsize;
  Elf32_Half    e_shnum;
  Elf32_Half    e_shstrndx;
} Elf32_Ehdr;
  
typedef struct elf64_hdr {
  unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
  Elf64_Half e_type;
  Elf64_Half e_machine;
  Elf64_Word e_version;
  Elf64_Addr e_entry;       /* Entry point virtual address */
  Elf64_Off e_phoff;        /* Program header table file offset */
  Elf64_Off e_shoff;        /* Section header table file offset */
  Elf64_Word e_flags;
  Elf64_Half e_ehsize;
  Elf64_Half e_phentsize;
  Elf64_Half e_phnum;
  Elf64_Half e_shentsize;
  Elf64_Half e_shnum;
  Elf64_Half e_shstrndx;
} Elf64_Ehdr;
  
/* These constants define the permissions on sections in the program
   header, p_flags. */
#define PF_R        0x4
#define PF_W        0x2
#define PF_X        0x1
  
typedef struct elf32_phdr{
  Elf32_Word    p_type;
  Elf32_Off p_offset;
  Elf32_Addr    p_vaddr;
  Elf32_Addr    p_paddr;
  Elf32_Word    p_filesz;
  Elf32_Word    p_memsz;
  Elf32_Word    p_flags;
  Elf32_Word    p_align;
} Elf32_Phdr;
  
typedef struct elf64_phdr {
  Elf64_Word p_type;
  Elf64_Word p_flags;
  Elf64_Off p_offset;       /* Segment file offset */
  Elf64_Addr p_vaddr;       /* Segment virtual address */
  Elf64_Addr p_paddr;       /* Segment physical address */
  Elf64_Xword p_filesz;     /* Segment size in file */
  Elf64_Xword p_memsz;      /* Segment size in memory */
  Elf64_Xword p_align;      /* Segment alignment, file & memory */
} Elf64_Phdr;
  
typedef struct elf32_shdr {
  Elf32_Word    sh_name;
  Elf32_Word    sh_type;
  Elf32_Word    sh_flags;
  Elf32_Addr    sh_addr;
  Elf32_Off sh_offset;
  Elf32_Word    sh_size;
  Elf32_Word    sh_link;
  Elf32_Word    sh_info;
  Elf32_Word    sh_addralign;
  Elf32_Word    sh_entsize;
} Elf32_Shdr;
  
typedef struct elf64_shdr {
  Elf64_Word sh_name;       /* Section name, index in string tbl */
  Elf64_Word sh_type;       /* Type of section */
  Elf64_Xword sh_flags;     /* Miscellaneous section attributes */
  Elf64_Addr sh_addr;       /* Section virtual addr at execution */
  Elf64_Off sh_offset;      /* Section file offset */
  Elf64_Xword sh_size;      /* Size of section in bytes */
  Elf64_Word sh_link;       /* Index of another section */
  Elf64_Word sh_info;       /* Additional section information */
  Elf64_Xword sh_addralign; /* Section alignment */
  Elf64_Xword sh_entsize;   /* Entry size if section holds table */
} Elf64_Shdr;
  
  
//android-platform\bionic\linker\linker_soinfo.h
typedef void (*linker_dtor_function_t)();
typedef void (*linker_ctor_function_t)(int, char**, char**);
  
#if defined(__work_around_b_24465209__)
#define SOINFO_NAME_LEN 128
#endif
  
struct soinfo {
#if defined(__work_around_b_24465209__)
  char old_name_[SOINFO_NAME_LEN];
#endif
  const ElfW(Phdr)* phdr;
  size_t phnum;
#if defined(__work_around_b_24465209__)
  ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility.
#endif
  ElfW(Addr) base;
  size_t size;
  
#if defined(__work_around_b_24465209__)
  uint32_t unused1;  // DO NOT USE, maintained for compatibility.
#endif
  
  ElfW(Dyn)* dynamic;
  
#if defined(__work_around_b_24465209__)
  uint32_t unused2; // DO NOT USE, maintained for compatibility
  uint32_t unused3; // DO NOT USE, maintained for compatibility
#endif
  
  soinfo* next;
  uint32_t flags_;
  
  const char* strtab_;
  ElfW(Sym)* symtab_;
  
  size_t nbucket_;
  size_t nchain_;
  uint32_t* bucket_;
  uint32_t* chain_;
  
#if !defined(__LP64__)
  ElfW(Addr)** unused4; // DO NOT USE, maintained for compatibility
#endif
  
#if defined(USE_RELA)
  ElfW(Rela)* plt_rela_;
  size_t plt_rela_count_;
  
  ElfW(Rela)* rela_;
  size_t rela_count_;
#else
  ElfW(Rel)* plt_rel_;
  size_t plt_rel_count_;
  
  ElfW(Rel)* rel_;
  size_t rel_count_;
#endif
  
  linker_ctor_function_t* preinit_array_;
  size_t preinit_array_count_;
  
  linker_ctor_function_t* init_array_;
  size_t init_array_count_;
  linker_dtor_function_t* fini_array_;
  size_t fini_array_count_;
  
  linker_ctor_function_t init_func_;
  linker_dtor_function_t fini_func_;
  
/*
#if defined(__arm__)
  // ARM EABI section used for stack unwinding.
  uint32_t* ARM_exidx;
  size_t ARM_exidx_count;
#endif
  size_t ref_count_;
//怎么找不到link_map这个类型的声明...
  link_map link_map_head;
  
  bool constructors_called;
  
  // When you read a virtual address from the ELF file, add this
  // value to get the corresponding address in the process' address space.
  ElfW(Addr) load_bias;
  
#if !defined(__LP64__)
  bool has_text_relocations;
#endif
  bool has_DT_SYMBOLIC;
*/
};

a1即是soinfo结构体指针,但是还是存在一些未知偏移,所以有理由怀疑这个结构体可能经过魔改。

sub_3C94被调用的地方开始分析,也就是sub_49F0.

0x38是循环的步长,v6为结束的地址,而0x38刚好是程序头表的大小,所以v5应该是ELF64_Phdr*类型。

所以我们只要将a1参数输出就可以得到解密后的phdr表。

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
function my_hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                         
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    hook_proc_self_maps();
                    hook_native();//在so加载后再进行hook
                }
            }
        }
    );
}
function hook_native(){
    var module=Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x5E6C),{
        onEnter: function(args){
            console.log(hexdump(args[0],{
                offset:0,
                length:0x38*0x6+0x20,//这里加0x20应该是为了保险多dump了一点,实际长度应该只有0x38*6
                header: true,
                ansi:true
 
            }));
            console.log(args[1])
            console.log(args[2])
            console.log(`base = ${module.base}`)
        }
 
    }
)
}
var dump_once=false;
function hook_proc_self_maps() {
    const openPtr = Module.getExportByName(null, 'open');
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent";
    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open",pathname);//,Process.getCurrentThreadId()
        if (pathname.indexOf("maps") >= 0) {
            console.log("find",pathname+", redirect to",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open(filename, flag);
        }
        if (pathname.indexOf("dex") >= 0) {
            if(!dump_once){
                dump_once = true;             
            }
        }
        var fd = open(pathnameptr, flag);
        return fd;
    }, 'int', ['pointer', 'int']));
}
setImmediate(my_hook_dlopen("libjiagu_64.so"));

cyberchef处理一下dump的数据。

010中按insert切换为覆盖模式,然后ctrl+shift+v覆盖数据。

但是这里phdr在soinfo结构体里是偏移0x0的地方,但是sub_5E6C传入的参数一却是a1+232偏移。

重新对soinfo结构体进行编辑

现在的传参就很正确,这里也佐证了soinfo被魔改的猜测。

使用作者的ida插件来跟踪一下函数的调用stalker_trace_so

获得函数调用链:

ELF解密流程

从之前dlopen被调用的地方sub_3C94开始分析。sub_4B54->sub_49F0->sub_3C94

sub_4B54有两个调用点,看函数调用链sub_8000应该是调用它的函数。

sub_8000只有data段存放着函数指针,静态分析下没有调用,但是看到很有意思的东西。

如果这块存的都是函数指针的话,那上面附近的大概率也是,并且都被sub_96E0调用,恐怕有蹊跷。

跟进sub_6DBC

接着跟到sub_6ADC

有一个很扎眼的异或,这里的v8就是参数一+v3,v3的值和传入的a2参数有关,a2是个常数,可以试着去计算v3的值,不过这里直接大胆假设好了,这里是循环异或解密数据。

1
2
3
4
5
6
import idaapi
import idc
for addr in range(0x2E125,0x2E125+0x2D):
    a=idc.get_wide_byte(addr)
    idc.patch_byte(addr,a^0xA5)
print("done")

确实解密出来一个正常的函数名,但是就算全部解密出来也不知道如果跟进下一步,还是跟回作者的思路,去分析函数调用链好了。

到sub_5F20时,这里有很明显的RC4算法特征

但是从函数调用链上来说的话,上几个函数都没有调用该函数的代码,那它是怎么调用的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function get_call_ptr(){
    var module=Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x5F20), {
        onEnter: function(args) {
            // 获取当前调用栈的信息
            var backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE);
            console.log('Backtrace:');
            backtrace.forEach(function(addr) {
                console.log(hexdump(addr, { length: 16, header: false, ansi: true }));
            });
     
            // 获取调用者的地址(即上层调用栈)
            var callerAddress = backtrace[1]; // 通常 backtrace[0] 是当前函数的地址,backtrace[1] 是调用者
            console.log('[+] sub_5F20 Caller address: ' + (callerAddress-module.base).toString(16)+'\n');
        },
        onLeave: function(retval) {
            // 在函数执行完后可以做进一步的处理
        }
    }
        );
     
}

将该函数在安置在该位置进行hook

调用地址在17710

是通过函数指针调用的。最早的调用函数是sub_C918,后面有call_SYSV的记录,这里先大概知道是使用的动态调用的方式。

我们再回过头来看rc4_init,HOOK一下参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function hook_5f20_guess_rc4(){//像是RC4的样子,hook看看
    var module = Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x5f20), {
        // fd, buff, len
        onEnter: function (args) {
            console.log(hexdump(args[0], {
              offset: 0,// 相对偏移
              length: 0x10,//dump 的大小
              header: true,
              ansi: true
            }));
            console.log(args[1])
            console.log(hexdump(args[2], {
              offset: 0,// 相对偏移
              length: 256,//dump 的大小
              header: true,
              ansi: true
            }));
        },
        onLeave: function (ret) {
  
        }
    });
}

密钥:vUV4#.#SVt

之后查看函数调用链的下一个函数sub_6044

很明显的RC4加密过程。

再HOOK一下sub_6044函数

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
function hook_rc4_enc(){
    var module = Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x6044), {
        // fd, buff, len
        onEnter: function (args) {
            rc4_enc_text_addr = args[0];
            rc4_enc_size = args[1];
            console.log(hexdump(args[0], {
              offset: 0,// 相对偏移
              length: 0x30,//dump 的大小
              header: true,
              ansi: true
            }));
            console.log(args[1])
  
        },
        onLeave: function (ret) {
            console.log(hexdump(rc4_enc_text_addr, {
              offset: 0,// 相对偏移
              length: 0x30,//dump 的大小
              header: true,
              ansi: true
            }));
        }
    });
}

是之前在sub_8000看到过的0xb8010,从该函数的参数使用上来说,这个值是处理的数据的大小。

再看一下v5[0]的值

和解密的数据是一致的,这段数据就是RC4加密过的。

接着看函数调用链,接下来是uncompress,解压缩函数。

函数定义:

int uncompress(
    unsigned char *dest,        // 解压后的数据存放位置
    uLongf *destLen,            // 解压后的数据长度(输入输出参数)
    const unsigned char *source, // 压缩数据源
    uLong sourceLen             // 压缩数据源的长度
);
function hook_uncompress_res(){
  var module = Process.findModuleByName("libjiagu_64.so");
  Interceptor.attach(Module.findExportByName(null, "uncompress"), {
  onEnter: function (args) {
  console.log("hook uncompress")
  console.log(hexdump(args[2], {
  offset: 0,// 相对偏移
  length: 0x30,//dump 的大小
  header: true,
  ansi: true
}));
console.log(args[3])
dump_memory(args[2],args[3],`uncompress_${args[2]}_${args[3]}`)
},
onLeave: function (ret) {

}
});
}

解压缩的数据就是RC4解密后的数据

不包括前四个字节,根据加壳的一些做法可以猜测是大小。

有ELF的数据及解密方式,直接手动解密

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
import zlib
import struct
def RC4(data, key):
    S = list(range(256))
    j = 0
    out = []
  
    # KSA Phase
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
  
    # PRGA Phase
    i = j = 0
    for ch in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(ch ^ S[(S[i] + S[j]) % 256])
  
    return out
  
def RC4decrypt(ciphertext, key):
    return RC4(ciphertext, key)
  
  
wrap_elf_start = 0x2e270
wrap_elf_size = 0xb8010
key = b"vUV4#\x91#SVt"
with open('D:/Android-reverse-tool/so_fixer/fix.so','rb') as f:
    wrap_elf = f.read()
  
  
# 对密文进行解密
dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start+wrap_elf_size], key)
dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))
with open('wrap_elf','wb') as f:
    f.write(dec_elf)

可以找到elf标志位,但是前面有一堆D3数据。

将elf这两部分切割

1
2
3
4
5
6
7
8
9
10
11
with open('wrap_elf','rb') as f:
    wrap_elf=f.read()
    ELF_magic=bytes([0x7F,0x45,0x4C,0x46])
    for i in range(len(wrap_elf)-len(ELF_magic)+1):
        if(wrap_elf[i:i+len(ELF_magic)]==ELF_magic):
            print(hex(i))
            with open('wrap_elf_part1','wb') as f:
                f.write(wrap_elf[0:i])
            with open('wrap_elf_part2','wb') as f:
                f.write(wrap_elf[i::])
            break

之后再接着函数调用链往下看elf的解密,sub_5B08

又是0x38,program_header_table的大小,可以猜测又是通过解析elf结构来进行一些关键操作。

HOOK下进行异或的v5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function hook_5B08(){
    var module = Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x5B08), {
        onEnter: function (args) {
            console.log("[+] All arguments:");
            // 遍历并打印前 3 个参数
            for (var i = 0; i < 3; i++) {
                console.log("[+] Argument " + i + ": " + args[i]);
            }
 
            // 获取指向 v4 的指针,假设 args[1] 是一个指针
            
            var v5=ptr(args[1]).add(16).readPointer();
            console.log("[+] Address:",v5);
            console.log("[+] v5:\n" + hexdump(v5, {
                offset: 0, // 相对偏移
                length: 0x30, // dump 的大小
                header: true,
                ansi: true
            }));
        }
    });
}

和010中看到的不明数据吻合

这个也可以找到他解密完的位置去进行dump,但是我觉得分析一下解密流程也算学习的一部分了。

首先只是从表面来看的话,似乎设计到加密的部分只有异或,包括vdupq_n_s8和veorq_s8似乎也只是实现另类的异或。

先从第一个循环来看,v5异或的值,重点应该聚焦于异或的数据,也就是v18

v18是v10+v12,而v12在进入该循环时被赋值为0,也就是说v18就是v10,而v10——》result——》strtol,

说真的这个strtol的使用在这里真的很迷,搞不太懂这里的逻辑,所以我选择了用frida的stalker来直接跟踪一下返回值。

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
function trace_entry(baseAddr, tatgetAddr){
    // 使用 Interceptor.attach 来 hook 目标函数
    Interceptor.attach(tatgetAddr, {
         
        // 在函数调用进入时触发的钩子
        onEnter: function(args){
            console.log("enter tatgetAddr====================================================================");
            // 获取当前线程的线程 ID
            this.pid = Process.getCurrentThreadId();
           
            // 使用 Stalker API 跟踪当前线程的执行
            Stalker.follow(this.pid, {
                events: {
                    // 设置哪些事件需要被追踪,这里暂时不追踪调用、返回、执行等事件
                    call: false, // 不追踪函数调用
                    ret: false, // 不追踪函数返回
                    exec: false, // 不追踪每一条执行的指令
                    block: false, // 不追踪执行的内存块
                    compile: false // 不追踪已编译的代码
                },
 
                // 当事件被接收到时执行的钩子,这里未使用
                onReceive: function(events){
                    // 目前没有处理接收到的事件
                },
                transform: function(iterator) {
                    var instruction = iterator.next(); // 获取下一条指令的迭代器
                    const startAddress = instruction.address; // 获取当前指令的地址
                    var isModule=startAddress.compare(baseAddr.add(0x5B08))>=0&&startAddress.compare(baseAddr.add(0x5B9C))<0;                                                 
                    do {
                        if(isModule) {
                            console.log(instruction.address.sub(baseAddr) + "\t:\t" + instruction.toString());
                            //var is_x0=instruction.toString().includes('x0');
                            var now_address=instruction.address.sub(baseAddr);
                            // 将一个回调注入到当前的指令(putCallout)中
                            if(now_address==0x5B74){
                            iterator.putCallout((context) => {
                                // 从 x0 寄存器读取指向的字符串
                                var x0_ptr = ptr(context["x0"]);
                                // 打印字符串,假设这是从内存读取的返回值 
                                if(x0_ptr!=null)                                                        
                                console.log("[+] "+now_address+": x0: \n"+context['x0']+hexdump(x0_ptr,{
                                    offset: 0, // 相对偏移
                                    length: 0x30, // dump 的大小
                                    header: true,
                                    ansi: true
                            }));
                                
                            });
                        }
                        }
                        // 保持继续迭代
                        iterator.keep();
                    } while ((instruction = iterator.next()) !== null); // 继续迭代,直到没有更多指令
                },
                onCallSummary:function(summary){
                    // 目前未使用该回调
                }
            });
        },
        onLeave: function(retval){
            // 停止跟踪当前线程的执行
            Stalker.unfollow(this.pid);
            // 打印返回值
            console.log("retval:" + retval);
            console.log("leave tatgetAddr====================================================================");
        }
    });
}

该函数的定义来说返回的应该是个整数,所以就是b40000704205f130,尝试几次没有变化,似乎是固定的值。

但是不管是从参数还是返回值来说,都相当奇怪,不排除可能自己对该函数进行了hook吧。

这里有一个既视感很强的操作

prctl的第二个参数正好跳过了前面赋值给其他变量的五个字节,同时四字节作为第三个参数,而参数1 result在前面被赋值给了v10,v10又赋值给v18,v18正好是要进行异或的数据。

所以这里的prtcl像是memcpy的操作。

改一下前面的frida-stlaker中输出x0的地址然后HOOK,不出所料,该函数操作后result指向的地址存储了010中看到的五字节之后的数据


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 24
支持
分享
最新回复 (10)
雪    币: 2731
活跃值: (3162)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
确实 我也遇到与你一样的问题,后面我用魔改的frida(软件能正常打开) 甚至hook不了  linker 加载的so 中的方法
2025-3-5 14:41
1
雪    币: 233
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
看雪精华帖预定
2025-3-5 16:04
1
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
xianyuuuan 看雪精华帖预定
排版有问题,估计得改改有个优把
2025-3-5 20:29
0
雪    币: 2835
活跃值: (3130)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
5
逆天而行 确实 我也遇到与你一样的问题,后面我用魔改的frida(软件能正常打开) 甚至hook不了 linker 加载的so 中的方法
这个原因是基地址问题,需要你自己计算linker上去之后的地址
5天前
0
雪    币: 4851
活跃值: (7154)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
真棒
5天前
0
雪    币: 2731
活跃值: (3162)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
Shangwendada 这个原因是基地址问题,需要你自己计算linker上去之后的地址
我打印了字节码 与我ida 里看到的字节码 是一样  的  所以问题就很奇怪 
5天前
0
雪    币: 244
活跃值: (2172)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
分析的非常棒
4天前
0
雪    币: 528
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
饱饱你太强了
4天前
0
雪    币: 105
活跃值: (71)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
相当 nice 的分析文章,从中有学到知识!
3天前
0
雪    币: 1284
活跃值: (2040)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
学习了,总结的很到位
3天前
0
游客
登录 | 注册 方可回帖
返回