GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

逆向工程 --- lab1

https://github.com/lucideus-repo/cybergym/tree/master/CyberGym%202/mobile

 

源码

package com.moksh.lab1;

// ... (导入相关的Android和第三方库类)
import com.goodiebag.pinview.Pinview; // 一个自定义的PIN码输入视图
import net.sqlcipher.Cursor; // SQLCipher的游标
import net.sqlcipher.database.SQLiteDatabase; // SQLCipher的加密数据库类

import java.io.File;

public class MainActivity extends AppCompatActivity {
    int i; // 尝试次数计数器
    Cursor cursor; // 数据库游标
    SQLiteDatabase secureDB; // SQLCipher加密数据库对象
    Context context; // 应用上下文
    int max_tries = 2; // 最大允许尝试次数为2次(总共3次机会)

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // 设置界面布局
        SQLiteDatabase.loadLibs(this); // **关键点**: 加载SQLCipher所需的本地库(.so文件)
        context = this.getApplicationContext(); // 获取应用上下文
        final Pinview pin1 = findViewById(R.id.pinview1); // 获取PIN码输入视图
        final TextView tv1 = findViewById(R.id.textView1); // 获取用于显示Flag的文本视图
        final Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); // 获取震动服务

        int d1 = b.checkAppSignature(this); // 调用前面分析的签名检查函数
        if (d1 < 1) { // 检查签名结果
            // 由于checkAppSignature总是返回1,这个分支通常不会执行,除非发生异常
            Toast.makeText(context, "Application Tampered", Toast.LENGTH_LONG).show(); // 提示应用被篡改
            this.finishAffinity(); // 关闭整个应用
        }

        try {
            String bb = new String("123456"); // 硬编码的数据库密码
            File dbFile = this.getDatabasePath("q.db"); // 获取加密数据库的文件路径
            dbFile.getParentFile().mkdir(); // 创建数据库所在的目录(如果不存在)
            this.deleteDatabase("q.db"); // **关键点**: 每次启动应用时都删除旧的数据库。这意味着PIN码每次启动都会重新生成。
            // 使用密码'123456'创建或打开一个名为'q.db'的加密数据库
            secureDB = SQLiteDatabase.openOrCreateDatabase(this.getDatabasePath("q.db"), bb, null);
            secureDB.execSQL("CREATE TABLE IF NOT EXISTS a(z VARCHAR,a VARCHAR);"); // 在加密数据库中创建一个表'a'

            // 这部分代码创建了另一个非加密数据库'kkk.db'并插入数据。
            // 这很可能是一个“红鲱鱼”(Red Herring),即与主线任务无关的干扰信息。
            SQLiteDatabase dbdb = SQLiteDatabase.openOrCreateDatabase(this.getDatabasePath("kkk.db"), "12345678", null);
            dbdb.execSQL("CREATE TABLE IF NOT EXISTS name(user VARCHAR, pass VARCHAR)");
            dbdb.execSQL("INSERT INTO name VALUES('moksh','password')");
            dbdb.close();
        } catch (Exception e) {
            // 数据库操作的异常处理
            Toast.makeText(MainActivity.this, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            Log.e("PinView", e.getMessage());
            e.printStackTrace();
        }

        // 查询加密数据库中的表'a'
        cursor = secureDB.rawQuery("SELECT * from a;", null);
        cursor.moveToFirst();
        if (cursor.getCount() < 1) { // 因为数据库每次都重建,所以这个条件永远为真
            // 如果表中没有数据,就创建新的PIN码
            String pinx1 = createPin(); // 生成一个随机4位PIN
            String pinx2 = createPin(); // 生成另一个随机4位PIN
            // 将两个PIN码插入数据库。pinx1在'z'列,pinx2在'a'列。后续验证只用'z'列的PIN。
            secureDB.execSQL("INSERT INTO a(z,a) VALUES('" + pinx1 + "','" + pinx2 + "');");
            cursor.close(); // 关闭游标
        } else {
            // 这个分支理论上不会执行
            cursor.close();
        }

        // 使用SharedPreferences来存储PIN码尝试次数
        final SharedPreferences sharedPref = getSharedPreferences("a", Context.MODE_PRIVATE);
        final SharedPreferences.Editor editor = sharedPref.edit();
        if (!sharedPref.contains("val")) { // 如果没有存储过尝试次数
            editor.putInt("val", 0); // 初始化为0
            editor.commit(); // 提交更改
        }

        // 为PIN码输入框设置监听器,当用户输入完PIN时触发
        pin1.setPinViewEventListener(new Pinview.PinViewEventListener() {
            @Override
            public void onDataEntered(Pinview pinview, boolean fromUser) {
                i = sharedPref.getInt("val", 0); // 读取当前尝试次数
                if (i >= max_tries) { // 如果尝试次数超过上限
                    Toast.makeText(MainActivity.this, "Attempts made! Wait 30 seconds", Toast.LENGTH_SHORT).show();
                    pin1.clearValue(); // 清空输入框
                    if (v.hasVibrator()) {
                        v.vibrate(400); // 震动提示
                    }
                    
                    // 延迟30秒后重置尝试次数并重新启用输入框
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            i = 0; // 重置计数器
                            editor.putInt("val", i);
                            editor.commit();
                            Toast.makeText(MainActivity.this, "Enabled, please try again.", Toast.LENGTH_SHORT).show();
                        }
                    }, 30 * 1000);

                } else {
                    // 如果尝试次数未达上限
                    i = i + 1; // 尝试次数加1
                    editor.putInt("val", i);
                    editor.commit();
                    String pp = pinview.getValue(); // 获取用户输入的PIN码
                    cursor = secureDB.rawQuery("SELECT * FROM a;", null); // 再次查询数据库
                    cursor.moveToFirst(); // 定位到第一行
                    // **关键点**: 比较用户输入的PIN (pp) 和数据库中存储的第一个PIN (cursor.getString(0) 对应 'z'列)
                    if (pp.equalsIgnoreCase(cursor.getString(0))) {
                        // 如果PIN码正确
                        Toast.makeText(MainActivity.this, "Right Pin, Congratulations", Toast.LENGTH_SHORT).show();

                        // --- Flag 计算过程 ---
                        // 1. 从资源文件(res/values/strings.xml)中获取名为'google_api_key'的字符串
                        String xo = getResources().getString(R.string.google_api_key);
                        a mo = new a();
                        // 2. 调用func1。我们知道func1什么都不做,直接返回xo。所以xo不变。
                        xo = mo.func1(xo, xo.substring(4)); 
                        // 3. 调用func2。将xo作为AES加密的密文进行解密。
                        xo = a.func2(xo);
                        // 4. 调用func3。将上一步解密的结果再进行Base64解码。
                        xo = a.func3(xo.substring(1), xo);
                        // 5. 调用func4。将上一步解码的结果作为十六进制字符串,转换回普通字符串。
                        xo = a.func4(xo, xo, xo.substring(2));
                        // 6. 将最终结果显示在TextView中
                        tv1.setText("Flag: " + xo);
                    } else {
                        // 如果PIN码错误
                        Toast.makeText(MainActivity.this, "Incorrect Pin, " + (max_tries + 1 - i) + " attempts remaining", Toast.LENGTH_SHORT).show();
                    }
                    cursor.close(); // 关闭游标
                }
            }
        });
    }

    // 一个简单的函数,生成一个1000到9999之间的随机数作为PIN
    private String createPin() {
        int randomPIN = (int) (Math.random() * 9000) + 1000;
        return String.valueOf(randomPIN);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        secureDB.close(); // 在Activity销毁时关闭数据库连接,防止内存泄漏
    }
}


package com.moksh.lab1;

import android.util.Base64; // 导入Android的Base64工具类,用于编码和解码

import java.security.MessageDigest; // 导入消息摘要类,用于哈希计算(如SHA-1)
import java.util.Arrays; // 导入数组操作工具类

import javax.crypto.Cipher; // 导入加密/解密核心类
import javax.crypto.spec.SecretKeySpec; // 导入用于生成密钥的类

public class a {
    // 这个类名为'a',通常在安全挑战中用于混淆,使其不具有描述性

    public String func1(String val1, String val2) {
        // 注释:这个函数声称会做一些数学运算,但实际上它的行为非常简单。
        // val1: 原始加密文本
        // val2: 虚拟值
        try {
            MessageDigest sha = null; // 声明一个MessageDigest对象
            sha = MessageDigest.getInstance("SHA-1"); // 初始化为SHA-1哈希算法
            byte[] k = val1.getBytes("UTF-8"); // 将val1转换为字节数组,存入k。但这行代码是无用的,因为k马上被覆盖。
            k = sha.digest(val2.getBytes("UTF-8")); // 计算val2的SHA-1哈希值,结果存入k。但变量k在此之后从未被使用,所以这行也是无用功。
            val2 = str2hex(val2); // 将val2转换为十六进制字符串。但这个结果也未被使用。
            return val1; // **关键点**: 函数忽略了所有中间计算,直接返回了原始的第一个输入参数val1。
        } catch (Exception e) {
            e.printStackTrace(); // 打印异常堆栈信息
            return "123"; // 发生异常时返回一个硬编码的字符串
        }
    }

    public static String func2(String val1) {
        // 这个函数看起来是一个AES解密函数
        try {
            MessageDigest sha = null; // 声明MessageDigest对象
            byte[] key = "mysecret".getBytes("UTF-8"); // 定义一个硬编码的密钥字符串 "mysecret"
            sha = MessageDigest.getInstance("SHA-1"); // 初始化SHA-1算法
            key = sha.digest(key); // 对"mysecret"字符串进行SHA-1哈希,生成一个20字节的哈希值作为密钥
            key = Arrays.copyOf(key, 16); // **关键点**: AES-128需要16字节的密钥。此行代码将20字节的SHA-1哈希值截断为前16字节。
            SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); // 使用截断后的16字节密钥创建一个AES密钥规范
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 获取一个AES加密器实例。模式为ECB,填充方式为PKCS5Padding。
            cipher.init(2, secretKey); // 初始化加密器为解密模式 (mode 2)
            // **关键点**: 对输入字符串val1进行解密。流程是:先Base64解码,然后AES解密。
            return new String(cipher.doFinal(Base64.decode(val1, Base64.DEFAULT)));
        } catch (Exception e) {
            e.printStackTrace(); // 打印异常
            return "error"; // 发生异常时返回"error"
        }
    }

    public static String func3(String val1, String val2) {
        // 注释:val2是主要值
        try {
            val1 = str2hex(val1); // 将val1转换为十六进制。但这行是无用代码,因为val1马上被覆盖。
            val1 = val2; // 将val2的值赋给val1,实际上只关心第二个参数。
            // **关键点**: 将第二个参数val2作为Base64编码的字符串进行解码。
            return new String(Base64.decode(val1, 0)); // 0等同于Base64.DEFAULT
        } catch (Exception e) {
            e.printStackTrace();
            return "123";
        }
    }

    public static String func4(String s1, String s2, String s3) {
        // 注释:s2是主要值
        try {
            s1 = str2hex(s1); // 无用代码,s1马上被覆盖。
            s2 = hex2str(s2); // **关键点**: 将第二个参数s2从十六进制字符串转换回普通字符串。
            s3 = s2; // 将转换后的结果赋给s3。
            return s3; // 返回转换后的结果。这个函数实际上只处理了第二个参数s2。
        } catch (Exception e) {
            return "123";
        }
    }

    // 辅助函数:将普通字符串转换为十六进制表示的字符串
    public static String str2hex(String str) {
        StringBuffer sb = new StringBuffer(); // 用于构建结果字符串
        char ch[] = str.toCharArray(); // 将输入字符串转为字符数组
        for (int i = 0; i < ch.length; i++) { // 遍历每个字符
            String hexString = Integer.toHexString(ch[i]); // 将字符的ASCII码/Unicode码转换为十六进制字符串
            sb.append(hexString); // 追加到结果中
        }
        return sb.toString(); // 返回最终的十六进制字符串
    }

    // 辅助函数:将十六进制表示的字符串转换回普通字符串
    public static String hex2str(String str) {
        String result = new String(); // 用于构建结果字符串
        char[] charArray = str.toCharArray(); // 将十六进制字符串转为字符数组
        for (int i = 0; i < charArray.length; i = i + 2) { // 每次处理2个字符(例如 "61" 代表 'a')
            String st = "" + charArray[i] + "" + charArray[i + 1]; // 组合成一个两位十六进制数
            char ch = (char) Integer.parseInt(st, 16); // 将这个十六进制字符串解析成一个整数(ASCII/Unicode码),然后强制转换为字符
            result = result + ch; // 追加到结果字符串
        }
        return result; // 返回最终的普通字符串
    }
}




package com.moksh.lab1;

// ... (导入相关的Android类)
import android.content.pm.Signature;
import android.util.Base64;
// ...

public class b {
    // 一个硬编码的字符串,设计用来存放正确的应用签名哈希值
    public static final String p0 = "<Add Signature Here>"; 

    // 静态方法,检查当前应用的签名是否合法
    public static int checkAppSignature(Context context) {
        try {
            // 获取当前应用的包信息,特别是签名信息
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
            for (Signature signature : packageInfo.signatures) { // 遍历所有签名(通常只有一个)
                byte[] signatureBytes = signature.toByteArray(); // 获取签名的原始字节。这行代码未被使用。
                MessageDigest md = MessageDigest.getInstance("SHA"); // 创建一个SHA(通常是SHA-1)哈希实例
                md.update(signature.toByteArray()); // 将签名信息喂给哈希算法
                // 计算哈希值,并将其编码为Base64字符串,以便比较和打印
                final String currentSignature = Base64.encodeToString(md.digest(), Base64.DEFAULT);
                
                // 这是一个被注释掉的日志,开发者可能用它来获取正确的签名字符串
                //Log.e("Sherlock","Sig: "+currentSignature); 
                
                /* 这是真正的签名比较逻辑,但它被注释掉了!
                if(p0.trim().equalsIgnoreCase(currentSignature.trim())){
                    return 1; // 如果签名匹配,返回1(成功)
                }else{
                    return 0; // 如果不匹配,返回0(失败)
                }
                */
                // **关键点**: 由于真正的检查逻辑被注释掉了,这个函数现在总是返回1。
                // 这意味着反篡改检查实际上是无效的。
                return 1;
            }
        } catch (Exception e) {
            // 如果在获取或处理签名时发生任何异常
            return 0; // 返回0(失败)
        }
        return 0; // 如果没有找到任何签名,也返回0(失败)
    }
}

 

 

我们使用jadx导出解析所有java和资源的解析结果,可惜不能导出so的源码

打开trae自动分析项目

 下面是解析的结果和ctf解密脚本

APK逆向工程与CTF Flag解密分析报告
概述
这是一个Android APK的逆向工程分析,该应用实现了PIN码验证功能,输入正确PIN后会显示CTF flag。

应用结构分析
主要组件
包名: com.moksh.lab1
主活动: MainActivity.java
加密工具类: defpackage.ja
PIN视图: 使用com.goodiebag.pinview.Pinview库
核心逻辑流程
PIN码生成与存储

应用启动时生成随机4位PIN码
使用SQLCipher加密数据库存储PIN码
数据库密码: "123456"
PIN码验证机制

用户最多有3次尝试机会
失败3次后锁定30秒
验证成功后触发flag解密流程
Flag解密流程

String string = MainActivity.this.getResources().getString(R.string.google_api_key);
new ja();
String a3 = ja.a(ja.a(string, string.substring(4)));
String b = ja.b(a3.substring(1), a3);
b.substring(2);
textView.setText("Flag: ".concat(String.valueOf(ja.c(b, b))));
加密算法分析
ja类方法详解
1. ja.a(String str) - AES解密
public static String a(String str) {
    SecretKeySpec secretKeySpec = new SecretKeySpec(
        Arrays.copyOf(
            MessageDigest.getInstance("SHA-1").digest("mysecret".getBytes("UTF-8")), 
            16
        ), 
        "AES"
    );
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(2, secretKeySpec); // 2 = DECRYPT_MODE
    return new String(cipher.doFinal(Base64.decode(str, 0)));
}
密钥生成: SHA-1("mysecret")的前16字节
加密模式: AES/ECB/PKCS5Padding
操作: Base64解码后AES解密
2. ja.a(String str, String str2) - 伪操作
public static String a(String str, String str2) {
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
    str.getBytes("UTF-8");
    messageDigest.digest(str2.getBytes("UTF-8"));
    b(str2);
    return str; // 实际只返回第一个参数
}
实际作用: 只是返回第一个参数,其他操作无实际意义
3. ja.b(String str, String str2) - Base64解码
public static String b(String str, String str2) {
    b(str); // 调用私有方法但不使用结果
    return new String(Base64.decode(str2, 0));
}
实际作用: 对第二个参数进行Base64解码
4. ja.c(String str, String str2) - 十六进制转ASCII
public static String c(String str, String str2) {
    b(str); // 调用私有方法但不使用结果
    String str3 = new String();
    char[] charArray = str2.toCharArray();
    for (int i = 0; i < charArray.length; i += 2) {
        StringBuilder sb = new StringBuilder();
        sb.append(charArray[i]);
        sb.append(charArray[i + 1]);
        str3 = str3 + ((char) Integer.parseInt(sb.toString(), 16));
    }
    return str3;
}
实际作用: 将十六进制字符串转换为ASCII字符
解密过程详解
输入数据
google_api_key = "R4f/mz5cIi2NHsrnAUGqYWThCTg60fHF1xYUZt73KXxS/mHJwYl41hcJ8R3rvAzuu9MUguemAhc8ydjifc+WiY9oVKZyN9xfscoD95b9BDI="
解密步骤
步骤1: ja.a(string, string.substring(4))

输入: (google_api_key, google_api_key[4:])
输出: google_api_key (原值不变)
步骤2: ja.a(step1) - AES解密

输入: google_api_key
AES解密后输出: "NjM3OTY3Nzk2ZDMzN2I1OTZmNzU1ZjY0Njk2NDVmNjk3NDVmNGQ2ZjcyNzQ3OTdk"
步骤3: ja.b(step2.substring(1), step2) - Base64解码

输入: step2
Base64解码后输出: "637967796d337b596f755f6469645f69745f4d6f7274797d"
步骤4: step3.substring(2)

去掉前2个字符: "7967796d337b596f755f6469645f69745f4d6f7274797d"
步骤5: ja.c(step4, step4) - 十六进制转ASCII

将十六进制字符串转换为ASCII
最终输出: "ygym3{You_did_it_Morty}"
最终结果
CTF Flag: ygym3{You_did_it_Morty}

安全分析
漏洞点
硬编码密钥: AES密钥基于硬编码字符串"mysecret"
弱加密模式: 使用ECB模式,存在安全隐患
资源文件暴露: 加密数据直接存储在资源文件中
逻辑混淆不足: 加密逻辑相对简单,容易被逆向
防护建议
使用动态密钥生成
采用更安全的加密模式(如CBC、GCM)
增加代码混淆和反调试机制
使用服务器端验证
工具和技术
反编译工具: jadx
分析语言: Java
解密实现: Python + pycryptodome
数据库: SQLCipher
分析完成时间: 2024年
分析工具: Trae AI + 人工逆向工程






 

 

import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def ja_a_single_param(s):
    """ja.a(String str) - AES解密函数"""
    try:
        # 生成密钥:SHA-1("mysecret")的前16字节
        key = hashlib.sha1("mysecret".encode('utf-8')).digest()[:16]
        
        # AES/ECB/PKCS5Padding解密
        cipher = AES.new(key, AES.MODE_ECB)
        encrypted_data = base64.b64decode(s)
        decrypted = cipher.decrypt(encrypted_data)
        
        # 移除PKCS5填充
        decrypted = unpad(decrypted, AES.block_size)
        return decrypted.decode('utf-8')
    except Exception as e:
        print(f"Error in ja_a_single_param: {e}")
        return "error"

def ja_a_two_params(str1, str2):
    """ja.a(String str, String str2) - 这个函数实际上只是返回str1"""
    try:
        # 根据代码分析,这个函数只是做了一些无用的操作,最后返回str1
        return str1
    except Exception as e:
        print(f"Error in ja_a_two_params: {e}")
        return "123"

def ja_b_helper(s):
    """私有方法b(String str) - 将字符转换为十六进制"""
    result = ""
    for c in s:
        result += hex(ord(c))[2:]  # 去掉'0x'前缀
    return result

def ja_b(str1, str2):
    """ja.b(String str, String str2) - Base64解码"""
    try:
        ja_b_helper(str1)  # 调用但不使用结果
        return base64.b64decode(str2).decode('utf-8')
    except Exception as e:
        print(f"Error in ja_b: {e}")
        return "123"

def ja_c(str1, str2):
    """ja.c(String str, String str2) - 十六进制字符串转ASCII"""
    try:
        ja_b_helper(str1)  # 调用但不使用结果
        result = ""
        for i in range(0, len(str2), 2):
            if i + 1 < len(str2):
                hex_pair = str2[i:i+2]
                result += chr(int(hex_pair, 16))
        return result
    except Exception as e:
        print(f"Error in ja_c: {e}")
        return "123"

# 主要解密逻辑
def decrypt_ctf():
    # google_api_key的值
    google_api_key = "R4f/mz5cIi2NHsrnAUGqYWThCTg60fHF1xYUZt73KXxS/mHJwYl41hcJ8R3rvAzuu9MUguemAhc8ydjifc+WiY9oVKZyN9xfscoD95b9BDI="
    
    print(f"原始google_api_key: {google_api_key}")
    print(f"google_api_key.substring(4): {google_api_key[4:]}")
    
    # 步骤1: ja.a(string, string.substring(4))
    step1 = ja_a_two_params(google_api_key, google_api_key[4:])
    print(f"步骤1结果: {step1}")
    
    # 步骤2: ja.a(step1) - AES解密
    step2 = ja_a_single_param(step1)
    print(f"步骤2结果 (AES解密): {step2}")
    
    # 步骤3: ja.b(step2.substring(1), step2)
    if len(step2) > 1:
        step3 = ja_b(step2[1:], step2)
        print(f"步骤3结果 (Base64解码): {step3}")
        
        # 步骤4: step3.substring(2)
        if len(step3) > 2:
            step4 = step3[2:]
            print(f"步骤4结果 (substring(2)): {step4}")
            
            # 步骤5: ja.c(step4, step4) - 最终flag
            final_flag = ja_c(step4, step4)
            print(f"\n最终CTF Flag: {final_flag}")
            return final_flag
    
    return None

if __name__ == "__main__":
    print("开始解密CTF flag...\n")
    decrypt_ctf()

 

我们发现ai提示使用了公共的加密库libsqlcipher.so。因此我们使用库识别工具,libchecker

 

posted on 2025-07-21 23:30  GKLBB  阅读(28)  评论(0)    收藏  举报