返回

文章详情

我逆向工程了一款桌游

Hacker News2026年10月21日 13:40

我最近收到了一个名为 "Hitster "的游戏作为礼物。这是一款桌游,玩家需要识别歌曲,并猜测它们的发行时间、艺术家或乐队等信息。每张卡片上都有一个 QR 码,使用 Hitster 应用程序扫描后,会通过 Spotify 或 Apple Music 播放音乐。可惜的是,应用程序需要 Apple Music 或 Spotify 订阅。在这篇博客文章中,我将深入探讨逆向工程 Android 应用程序并绕过他们非常糟糕的隐藏数据的方法。 第 0 部分:它是如何工作的 首先,我们需要了解游戏的工作原理。在每张卡片的正面,你会找到艺术家或乐队的名字、歌曲发布的年份以及歌曲标题。背面则有一个 QR 码。扫描后会进入一个 URL,其路径中包含两个内容:游戏的地区(在我的情况下是 DE)和卡片 ID。然后,音乐会在应用程序或通过 Spotify 播放。这意味着有一个从卡片 ID 到 Spotify 曲目的映射。每个地区也有多个卡片版本,因此仅凭卡片 ID 不能唯一识别一首歌。卡片 ID 1 在英国可能是 "Wet Dream",而在德国则可能是 "Tage wie diese"。 第 1 部分:Android 应用程序 我从一个可疑的 Android APK 下载网站获取了应用程序的 APK 文件,使用 jadx 获取了反编译后的 Java 代码。经过一番探查,我在 res/raw 下找到了一些 JSON 文件。其中一个名为 gameset_database_4.json,似乎是加密的。我运行了一个工具来识别格式,结果为 Base64。这是一种编码,因此我首先对其进行了解码。结果仍然不可读,我猜测它是用 AES 或类似的方式加密的。我之前从未逆向工程过 Android 应用程序,因此我只是在全局搜索 AES。结果怎么样?有一个名为 k43.java 的文件,其中详细列出了整个解密过程: public static String f (String str, String str2) throws o21 { str.getClass(); str2.getClass(); try { byte [] bArrDecode = Base64.decode (str, 0); bArrDecode.getClass(); p (bArrDecode); return o ( or.m (bArrDecode, 0 , 12), bArrDecode, m (bArrDecode.length - 28 , str2)); } catch (o21 e) { throw e; } catch (Exception e2) { Napier.e$default (Napier.INSTANCE, e2, (String) null , new s11 (24), 2 , (Object) null); throw new o21 (pv0.o ("解密失败:" , e2.getMessage ()), e2); } } public static SecretKeySpec m (int i, String str) throws NoSuchAlgorithmException { String str2 = i + str; MessageDigest messageDigest = MessageDigest.getInstance ("SHA-256"); byte [] bytes = str2.getBytes (yj0.a); bytes.getClass(); return new SecretKeySpec (messageDigest.digest (bytes), "AES"); } public static String o (byte [] bArr, byte [] bArr2, SecretKeySpec secretKeySpec) throws BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException { Cipher cipher = Cipher.getInstance ("AES/GCM/NoPadding"); cipher.init (2 , secretKeySpec, new GCMParameterSpec (SemanticState.Focused, bArr)); byte [] bArrDoFinal = cipher.doFinal (bArr2, 12 , bArr2.length - 12); bArrDoFinal.getClass(); return new String (bArrDoFinal, yj0.a); } 那么让我们一步步走过这个过程。函数 f 接受两个字符串作为输入。第一个 str 包含 Base64 编码的数据,它将其解码:byte [] bArrDecode = Base64.decode (str, 0); bArrDecode.getClass(); p (bArrDecode); 然后它检查解码后的数据是否至少为 28 字节长: public static void p ( byte [] bArr) throws o21 { if (bArr.length < 28 ) { throw new o21 ( gg3.g ( bArr.length, "加密数据过小:" , " 字节(最小:28 字节)" ), null ); } } 接下来,它将一堆数据传递给函数 o,该函数处理解密的最终阶段: return o ( or.m (bArrDecode, 0 , 12), bArrDecode, m (bArrDecode.length - 28 , str2)); 第一个参数是解码数据的前 12 个字节。这是我们的 IV 或 nonce。第二个参数是整个解码后的字节数组。第三个参数就变得有趣了。它取总长度减去 28,并将其与 str2 一起传递给函数 m。这 28 个字节是为 12 字节的 nonce 和 16 字节的身份验证标签保留的。在 m 中,剩余长度被转换为字符串并与 str2 连接。然后使用 SHA-256 对这个组合字符串进行哈希,生成 AES 密钥。因此,str2 并不是 AES 密钥本身。它是用来推导密钥的秘密字符串,而我们尚未找到它。最后,o 执行实际的解密: public static String o (byte [] bArr, byte [] bArr2, SecretKeySpec secretKeySpec) throws BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException { Cipher cipher = Cipher.getInstance ("AES/GCM/NoPadding"); cipher.init (2 , secretKeySpec, new GCMParameterSpec (SemanticState.Focused, bArr)); byte [] bArrDoFinal = cipher.doFinal (bArr2, 12 , bArr2.length - 12);

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

☕请我喝杯咖啡