Base64 魔改为什么流行
Base64 本身不是加密(公开编码),但换表 Base64 是最便宜的"伪加密":把标准索引表打乱,解码结果立刻变成乱码。成本一行代码,效果是没见过的人对着乱码发呆半天。逆向遇到"看着像 base64 但解不开"的数据,九成是换表。
识别:三个特征
- 字符集是 base64 风格(64 个字符 + 可能的填充符
=),但标准解码出乱码 - 长度符合 base64 规律(4 的倍数)
- 统计特征:字符分布接近均匀(base64 的典型分布)
import base64
data = "k3j4h5..." # 待识别数据
try:
decoded = base64.b64decode(data)
print(decoded) # 乱码 = 可能换表;可读 = 标准 base64
except:
pass # 含非标准字符 = 可能自定义字符集还原方法一:找到自定义表(最快)
换表 Base64 的表必须存在代码里。jadx/IDA 搜 64 字符长度的字符串字面量:
标准表: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
自定义表: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/ ← 大小写互换
自定义表: ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/ ← 逆序找到表后还原就是查表替换:
import base64
CUSTOM = "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/"
STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
def custom_b64decode(s):
trans = str.maketrans(CUSTOM, STD)
return base64.b64decode(s.translate(trans))还原方法二:已知明文反推表(找不到表时)
表被加密存储或运行时生成时,用已知明文对拍反推:
- hook 编码函数的输入输出,拿到一对"明文 → 换表编码"
- 明文做标准 base64 得到标准编码
- 逐字符对比标准编码和换表编码,建立映射表
def recover_table(plaintext, custom_encoded):
std_encoded = base64.b64encode(plaintext).decode()
table = {}
for std_char, custom_char in zip(std_encoded, custom_encoded):
table[custom_char] = std_char # 自定义字符 → 标准字符
return table
# 多对样本补全 64 个字符的映射一对样本通常覆盖不完 64 个字符,多抓几组(明文越长覆盖越全)。
还原方法三:不只是换表——索引打乱
更狠的魔改是编码后做位置置换(比如每 4 字符反转、奇偶位互换)。识别:换表还原后还是乱码,但乱码的"结构"有规律(比如能看出局部片段)。
对策:hook 编码函数,喂递增特征输入(\x00\x01\x02\x03...),观察输出的位置变化规律,置换模式立刻现形。
精髓:Base64 魔改在协议里的真实角色
- 它通常是最后一层包装。协议参数的加密链经常是
AES 加密 → 换表 base64 编码——base64 魔改解决后,里面还有真加密。别解开 base64 看到二进制就以为失败了,那是密文 - 变体字符集要留意。URL 安全 base64(
-_替代+/)、自定义填充符(!替代=)都是常见变体,还原时一并处理 - hook 编码函数优于一切静态分析。
android.util.Base64是标准实现,魔改的一定是自写函数——搜encodeToString的自定义封装,或 hook 输出点直接拿映射关系
总结
- 换表 base64 识别:字符集像 base64 但标准解码乱码
- 还原三法:找自定义表 → 已知明文反推 → 特征输入识别置换
- 它通常是加密链的最后一层包装,解开是密文不是终点
- hook 编码函数是最短路径
交流微信:run1255