逆向工程 --- 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

浙公网安备 33010602011771号