吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3214|回复: 37
上一主题 下一主题
收起左侧

[Android 原创] 【多栈实战】某黑产软件全链路逆向实录 (上)

  [复制链接]
跳转到指定楼层
楼主
JiGuro 发表于 2026-5-31 19:29 回帖奖励
【多栈实战】
某黑产软件全链路逆向实录 (上) ——
Signing Block V2 隐藏邀请码实现逻辑详析


开篇严正声明:本文仅用于学习逆向工程与网络安全相关技术,未掺杂任何不良目的。文中的软件已对其名字、图标及相关敏感信息做了模糊处理,本人也不会提供软件原包样品,内容仅作学习交流使用!同时,本人并未对该软件样本进行任何分享,下载仅做技术研究,均在个人设备上和虚拟设备中进行分析,并已在分析完后删除!

之前的文章分析了 HLS加密 ,并利用穷举方法获取了完整视频内容,反响挺不错的,这说明大家可能喜欢看这方面内容,所以这次我又找到了一个逆向实例。
由于分析内容过多,且每一篇内容都有一个主题,所以我决定把其拆成三个部分,分别为上、中、下,可能间隔几天之后才会发其他内容,请各位耐心等待

同时,这系列文章较以前文章技术难度有一定提升,全是干货,希望各位能够耐心看完,感谢大家的支持与理解!

又是一天夜晚,我拿到了软件样本,发现仍然需要会员才能观看完整视频。



仔细观察软件,我们可以发现软件个人界面有一个邀请朋友得 VIP 的功能。老带新,拉人头送会员,这类软件的典型套路。



我扫了扫软件生成的邀请二维码,跳转到了这样一个 Center 页面:



我们可以看到网页当中有一个 “轻量版下载” 的按钮,点击下载可以下载到一个 APK 包。
我们尝试在模拟器中安装,打开后和我手中的样本页面一模一样,我当时想这应该就是普通的软件下载渠道,而且下载的软件包应该都是一致的。
但当我回到最初安装的软件页面时,我惊奇地发现我竟然得到了一天 VIP ,且软件提示我分享成功。

这就有些奇怪了,我在模拟器中安装软件后,打开并没有登录账号 (软件会自动生成账号) ,更没有输入之前页面中的邀请码,但服务器却能够判定我已经分享给了“朋友”。也就是说,服务器能精准识别“这是谁邀请来的用户”,然后自动给邀请人发放 VIP 。这是怎么一回事呢?我准备对其进行深入研究。

首先,我们最容易怀疑的就是网页本身,因为我们是通过中转页进行下载的。
我们查看中转网页网址:
https://xxx.cloudfront.net/?p=eyJ2IjoxLCJpIjoiNzE5N0Q5MTYifQ

经典 AWS CDN 服务,眼尖的同学应该一眼就看出来了,这 p 参数长得就不像正经明文。拖进解码器一解码:
{"v":1,"i":"7197D916"}
好家伙,Base64 编码的 JSON,里面藏着一个编码 “7197D916” ,版本号 ”v=1“ ,这不就是我们的邀请码吗?
原来,邀请码是通过 p 参数的形式传递给网页的。

既然网页已经知道分享码,那么有可能我们下载的文件本身就和原包存在差异,通过某种特殊方式标记了分享码,于是我使用 MT 管理器提取两个安装包,使用 APK 对比功能进行对比,发现管理器提示 “未找到任何差异” 。安装包内部的 600 个文件,全部逐字节相同。




为了以防万一,我还进行了安装包的哈希值对比,不对比不知道,一对比吓一跳,这种诡异的事还真发生了,两个安装包的哈希值不一样!



这就很反直觉了。APK 整体哈希不同,但解包后所有文件哈希完全相同?那差异到底藏在哪?

相信很多同学分析到这里就直接想上手操作了,但是我们可以先慢下来,先梳理一下现在可能的判断思路,再进行下一步验证,来验证我们的思路是否正确,这样可以极大的提升我们的逆向能力:

1、时间拟合
虽说我们已经定位到安装包存在差异,但我们仍不能排除服务器记录这一选项。众所周知,当我们向服务器发起一个下载请求时,服务器可以记录该下载请求的发生时间,从而根据这个时间以及在这段时间临近范围内新登入的用户来大致拟合用户,从而发放奖励。
不过这种方式极不稳定,而且在实际情况中,无论用户何时下载、何时打开,奖励都能正确发放,所以排除该方案。

2、文件名辨识
我们可以观察到,不同邀请码下载好的安装包拥有不同的文件名,特别是文件名后面的一串字符显得较为可疑,且同一邀请码对应的文件名是唯一且恒定的,这给我们怀疑软件通过读取安卓下载目录中的文件名来识别邀请码的动机。
但事实证明,在并未给软件读取文件权限的情况下,服务器仍能成功发放奖励;同时,该方案也很不稳定,所以排除该方案。

3、时间戳辨识
APK 的本质是一个 ZIP 压缩包,而压缩包内部的所有文件都会记录一个文件修改时间 (即打包时间) ,这个时间是可以修改和读取的,可能用于辨识邀请码。
但由于 APK 内部时间戳的记录采用的是 MS-DOS 格式,不记录时区,读取时很容易产生混乱,且可能使用不同工具读取到的时间戳也不同,极不稳定;同时,可以注意到两个安装包内的文件时间戳一致,所以排除该方案。

4、签名(V1/V2/V3)辨识
该软件采用 V1+V2+V3 签名形式,我起初怀疑可能存在服务器自动根据时间生成不同的签名,然后软件再通过读取签名发送至服务器,再由签名信息判断邀请码并发放奖励。但前文已经提到,这两个 APK 文件内所有文件都相同,签名也完全一致,所以排除该方案。

5、ZIP 注释辨识
我能想到的最完美的方案。既然 APK 本质是 ZIP 文件,那么它也当然支持 ZIP 注释。ZIP 注释是 ZIP 压缩格式提供的一种附加文本信息功能,直接写在文件的元数据中,可直接读写,主要用于为整个压缩包或包内的单个文件添加说明文字。所以软件很可能将一些特征信息隐藏在注释中。

根据当前信息综合判断,我们现在只剩方案5没有被排除了,方案5的可能性理论上也是最大的。

那么,我们现在就可以验证我们的猜想。要验证方案5的正确性,只需要将两个 APK 文件进行16进制比较即可。
我们直接使用 Windows 端的老朋友 Beyond Compare 5 ,对两个 APK 文件进行比较。



果然,我们发现两个 APK 文件中仅有一个差异,而这个差异所对应的 ASCII 明文也十分可疑!
“7197D916” 和 “20F7247C”,这不就是邀请码吗!
而且它们的存储格式和网页中的一模一样!

但是,不对!
仔细观察这段二进制标识所在的位置,它并不位于我们常说的 ZIP注释段 ,而是位于其他的位置。那么这个位置对应的是什么呢?
在标准 APK 结构中,这个区域正是 V2/V3 签名块 的所在位置。

为什么这样能工作?难道签名中还可以插入其他东西吗?这样做不会破坏签名吗?
这也就来到了今天这篇文章的重点,就让我带大家一步一步深入理解这种渠道包制作方式的底层逻辑。

首先,要理解这种工作方式,先得复习一下 APK 结构。
就像前文所述,APK 本质上是一个 ZIP 文件:



其中,APK Signing Block 位于 ZIP 内容和 Central Directory 之间。标准的 ZIP 解析器会忽略这个区域(ZIP 规范允许在内容和 Central Directory 之间存在额外数据),而 Android 系统则会专门解析它来验证签名。APK Signing Block 详细结构如下:



我们注意到,其中 ID-Value Pairs 是可以包含多个 Pairs 的,这给我们可以操作的空间。ID-Value 对每个条目由一个 32 位整数 ID 和任意字节的 Value 组成。也就是开发者可以把自定义信息包装成一个 ID-Value 对,在 Value 区域写入自定义信息,然后打包进安装包。而且这种打包方式速度非常快,不需要修改任何文件,只需要在原始签名的基础上注入 ID-Value 对即可。

既然已经注入了 ID-Value 对,那么为了获取分享码,就一定要进行读取操作。为了验证我们的猜想,我们可以在 APK 中寻找类似操作。
使用 MT 管理器 搜索功能,在 APK 的 DEX 字符串中搜索与读取 Signing Block 相关的关键词:channel、signatures
很快,我们就找到了这个读取 Signing Block 的工具类 LT/h(T.h), 通过 Jadx 反编译混淆类 T.h 的方法 r(Context, String) (省略代码):
// LT/h — APK Signing Block 读取工具类
// 这是整个邀请码读取链路中最核心的底层函数
// 函数名 'r' 是混淆后的方法名
public static String readSigningBlock(Context context, String key) {
    // 获取 APK 文件路径
    // context.getApplicationInfo().sourceDir 返回 APK 文件的绝对路径
    String apkPath = context.getApplicationInfo().sourceDir;
   
    // 以 RandomAccessFile 打开 APK 文件
    // 使用 "r"(只读)模式打开
    RandomAccessFile raf = new RandomAccessFile(apkPath, "r");
    FileChannel channel = raf.getChannel();

    // 定位 APK Signing Block(方法 m)
    // APK Signing Block 位于 ZIP Central Directory 之前。
    // 具体定位方式:从文件尾读取 Central Directory 的偏移量,然后反向搜索找到 Signing Block 的起始位置。

    // 关键常量:
    //   "APK Signing Block sizes in header and footer do not match"
    //   "APK Signing Block offset out of range"
    //   "No APK Signing Block before ZIP Central Directory"
    ByteBuffer block = locateApkSigningBlock(channel);
   
    // 解析 ID-Value 对(方法 n)
    // 我们要找的 ID 是 0x71777777
    LinkedHashMap<Integer, byte[]> entries = parseIdValuePairs(block);
   
    // 提取 ID 0x71777777 的数据
    // 这个 ID 存储的就是我们的 JSON 数据
    byte[] jsonBytes = entries.get(0x71777777);
   
    // 解析 JSON 并返回指定 key 的值
    JSONObject json = new JSONObject(new String(jsonBytes, "UTF-8"));
    return json.optString(key, "");
}
这完全验证了我们的猜测,分享码确实是用这种方式保存的!

我们可以写一个 Python 脚本,模仿伪 Java 代码的实现方式,对其中一个安装包的所有 ID-Value 对进行解析,来验证我们的猜想,这里给出主要实现:
import struct

def u32(f): return struct.unpack('<I', f.read(4))[0]
def u64(f): return struct.unpack('<Q', f.read(8))[0]

def find_eocd(f):
    # 从文件尾向前搜索 EOCD 签名 0x06054b50(小端 50 4B 05 06)。
    # EOCD 固定 22 字节,注释最长 65535 字节,故最大搜索 65557 字节。
    sz = f.seek(0, 2)
    f.seek(max(0, sz - 65557))
    b = f.read(min(sz, 65557))
    for i in range(len(b) - 22, -1, -1):
        if b[i:i+4] == b'\x50\x4b\x05\x06': return sz - len(b) + i
    return -1

def find_signing_block(f, cd):
    # 验证魔数 "APK Sig Block 42"(cd 前 16 字节),读取 end_size(cd 前 24 字节)。
    # 标准结构含 front_size,非标准可能缺失或为 0,需兼容判断。
    f.seek(cd - 16)
    if f.read(16) != b'APK Sig Block 42': return None
    f.seek(cd - 24)
    end = u64(f); pe = cd - 24; st = pe - end
    f.seek(st)
    return (st + 8, pe) if u64(f) == end else (st, pe)

def parse_pairs(f, st, pe):
    # 遍历 ID-Value 区域:每项 = 8 字节长度 + 4 字节 ID + Value。
    # 长度为 0 视为 padding 跳过;长度非法则逐字节滑动重新同步。
    r, o = {}, st
    while o + 12 <= pe:
        f.seek(o); ln = u64(f)
        if ln == 0: o += 8; continue
        if ln < 4 or o + 8 + ln > pe: o += 1; continue
        r[u32(f)] = f.read(ln - 4); o += 8 + ln
    return r


可以看到,已经成功解析出四条 ID-Value 对:
[ID: 0x7109871a] ef 05 00 00 eb 05 00 00 ad 03 00 00 2c 00 00 00 28 00 00 00 03 01 00 00 20 00 00 00 c7 54 df 79 ... 
用途:APK Signature Scheme V2 Block
来源:Android 官方
[ID: 0xf05368c0] ef 05 00 00 eb 05 00 00 a5 03 00 00 2c 00 00 00 28 00 00 00 03 01 00 00 20 00 00 00 c7 54 df 79 ...
用途:APK Signature Scheme V3 Block
来源:Android 官方
[ID: 0x42726577] ...
用途:Padding Block(Verity 填充)
来源:Android 官方
[ID: 0x71777777] {"p":"eyJ2IjoxLCJpIjoiNzE5N0Q5MTYifQ","channel":"default"}
最后一条,就是开发者自定义用于识别邀请人的邀请码的标识了。

那么,为什么可以在不破坏签名的情况下写入渠道?
由于V2 签名计算摘要时,覆盖的数据范围是:
ZIP Entry Contents(所有文件内容)
ZIP Central Directory
End of Central Directory

我们可以发现,V2 签名计算不覆盖 APK Signing Block 中的其他 ID-Value 对!

我们从 AOSP 中可以还原 V2 签名计算方式:
// 使用静态字段的方式,写死在代码中
// 验证时只关心 ID = 0x7109871A 的签名块 (APK Signature Scheme V2 Block)
// 其他 ID 的块被忽略,不参与摘要计算
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
当开发者写入渠道时,就像以下图片示例的这样:



最后,我们可以在重新签名安装包后,用以下这个注入器重新注入 ID-Value 对,将其中的分享码改成其他用户的值:
https://share.weiyun.com/ki2kuvms

打开修改软件后,对应分享码用户果然收到了 VIP 奖励。这说明我们成功复现了



现在整个机制已经水落石出了。让我帮各位理解这套"免登录邀请追踪"的完整链路:
用户A生成邀请链接
    ↓
服务器根据邀请码 (如 7197D916) 生成专属 APK
    ↓
在 APK 文件末尾、ZIP Central Directory 之前
插入一段 JSON 明文: {"p":"Base64(邀请码JSON)","channel":"default"}
    ↓
用户B下载并安装该 APK
    ↓
App 首次启动时,读取自身 APK 文件 (sourceDir)
    ↓
搜索上下模块魔数 → 解析 JSON → 提取邀请码
    ↓
将邀请码作为设备标识,随心跳/注册请求上报服务器
    ↓
服务器识别: "这是 7197D916 邀请来的用户" → 发放 VIP

准确来说,其实这套方案就是前面被我们否掉的 方案4 的变相版本,并不是直接存储信息在签名文件中,而是利用了签名块之间的 Pair 来实现这一点。

这套方案有几个精妙之处:

1、对 ZIP 结构零侵入
数据插在文件数据区末尾、Central Directory 之前,不修改任何 ZIP 条目。所以解包后所有文件的哈希完全相同,常规的 APK  diff 工具根本发现不了。

2、不需要签名重签
因为数据插在 V2 签名块之前(或者说,利用了签名块和 CD 之间的间隙),同一个签名密钥签出来的包,只需要在签名前注入不同数据即可。签名值不同是因为 RSA 随机填充,但证书不变。

3、运行时读取极其隐蔽
App 不需要申请任何特殊权限,只需要用包管理器读取签名信息,找到上下区块对应的魔数位置,解析签名对找到对应的 ID-Value 即可。
这比在 “AndroidManifest.xml” 里写 “meta-data”、比在 “assets” 里放配置文件隐蔽得多——后者解包一眼就能看到,而前者藏在 ZIP 结构之外,绝大多数人根本不会往这个方向想。

4、免登录绑定
因为邀请码是跟着 APK 走的,不是跟着账号走的。用户 B 甚至不需要注册登录,服务器只要看到来自这个 APK 的首个请求,就知道这是 7197D916 拉来的,直接给 A 发奖励。

5、修改即毁
如果重新签名安装包,ID-Value 对又会被恢复成原来的样子,没有一丝痕迹。所以这种方案也可能会被用来作为签名校验的一种方式,因为可能有些过签工具并未考虑到自定义 ID-Value 对的情况。

实际上,在分析完这一切后,我就尝试上网搜索相关信息,发现这一思路已经被 美团 Walle 、腾讯 VasDolly 等大厂工具利用了 (本来以为是我最先发现的说),说不定该黑产软件就是使用这些大厂服务来做到这一点的。
这些工具专门用于制作渠道包,以便在用户无感知的情况下统计用户信息。
不得不说,能想出这个方案的人还是太聪明了。

那么,如何利用这个方式来获取无限会员?在后续逆向中又会遇到什么问题?如何逆向 Flutter ?如何伪造请求?
这些问题可能只能留到下几篇文章再说了

就是因为这个软件,启发我做了之前的 Crackme ,它的关键信息其实就是使用这种方式来隐藏的,各位可以使用这篇文章的思路,挑战一下自己:
【CrackMe】破解游戏——你敢挑战吗?(https://www.52pojie.cn/thread-2108082-1-1.html)
这里顺便提一下,至于 Crackme 的解答,已经有大佬在帖子下发文了,我就不去做解答了,毕竟难也只是难在第二步 Signing Block 比较难被想到,获取假 flag 还是相当简单的
如果还是不懂,可以去 吾爱论坛 搜索相关信息,已经有相当多的大佬给出了分析,甚至有人发了分析帖。

最后送给各位一句话:
在字节级精度面前,没有什么是安全的。

码字不易,点赞可有?
帖子内所有工具都已放在下面,可供大家练手,评论自取。
https://jiguro.lanzouw.com/iI7Xy3qt8p7e

免费评分

参与人数 26吾爱币 +25 热心值 +22 收起 理由
yinyiniao + 1 + 1 谢谢@Thanks!
greendays + 1 + 1 谢谢@Thanks!
BrutusScipio + 1 + 1 用心讨论,共获提升!
Hobo酱 + 1 + 1 用心讨论,共获提升!
N0NeckKing + 1 + 1 用心讨论,共获提升!
zjqfm + 1 热心回复!
SKA + 1 + 1 用心讨论,共获提升!
IcePlume + 1 + 1 谢谢@Thanks!
lnxiaoguo + 1 我很赞同!
Ruomeng + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
111mz + 1 + 1 热心回复!
tiantangniaone3 + 1 谢谢@Thanks!
jaffa + 1 谢谢@Thanks!
palec + 1 + 1 用心讨论,共获提升!
aihetianshui + 1 + 1 我很赞同!
FZZZP + 1 + 1 我很赞同!
yulai3230 + 1 + 1 写的太好了,什么时候开启第二篇
jayhang + 1 + 1 我很赞同!
chengdragon + 1 + 1 感谢分享
Toast + 1 + 1 我很赞同!
buluo533 + 1 + 1 谢谢@Thanks!
surepj + 1 + 1 用心讨论,共获提升!
laozhang4201 + 1 + 1 热心回复!
mynccs + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
aigc + 1 我很赞同!
W9046P + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
紫翼精灵 发表于 2026-6-5 17:12
不错, 一种设备埋点拉新的全新视角; 相比于原来的,一鱼多吃
原来方案,依赖第三方服务,通过在H5页面采集设备快照,下载后与服务端匹配来传递邀请关系
推荐
sapin 发表于 2026-6-1 17:46
解包后hash完全相同,但是实际藏了数据。。。原来这么简单就可以办到。。。估计是大厂先想出来的技术被拿去利用了

确实太精明了
沙发
dork 发表于 2026-5-31 20:12
看完之后想到的第一个类似APP应该就是PDD

免费评分

参与人数 1吾爱币 +1 收起 理由
xiaofeiTM233 + 1 哈哈哈但是国产软件随便用别人邀请码好像没啥用

查看全部评分

3#
douyacai 发表于 2026-5-31 20:17
厉害,分析的透彻
4#
nulla2011 发表于 2026-5-31 21:20
学到了,还可以这样藏信息
5#
blyizhi 发表于 2026-5-31 22:00
看了lz的帖子,学习了不少,多谢lz的分享。
6#
13714550928 发表于 2026-5-31 22:11
无敌,深奥
7#
罗婷 发表于 2026-5-31 23:53
难道你不好奇,他是如何轻量化修改签名的吗?如果用户量比较大,那不是分分钟爆硬盘
8#
TaoyaoX 发表于 2026-5-31 23:58
期待更新!
9#
xhd7908491 发表于 2026-6-1 00:03
优秀的大神学习
10#
qzhidong 发表于 2026-6-1 00:18
学习学习大神厉害
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-6-18 00:12

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表