背景:客户的一个小程序在XX宝上,由于请求与返回的数据包存在加密,随后学习记录了一次从加解密到成功获取成果的过程。

抓包&逆向过程

抓XX宝小程序,需要使用Reqable(小黄鸟),VPN模式抓包,此处我使用协同抓包模式,方便查看数据包:

并且使用二级代理,方便把数据包传入BURP中:

此处抓到数据包为请求包和返回包均为密文

从网上得知XX宝数据目录在/d⭐at⭐a/ d⭐at⭐a⭐/ com.eg.andr⭐oid.A⭐li⭐pay⭐G⭐phone⭐/files,(此处去掉⭐号为路径),数据为0.data,但研究后发现最新的存储格式是0.dc文件

此处需要一台ROOT的安卓机,进入一次小程序后,进入以上目录,将files文件夹整个压缩后带到电脑上,解压后,有一个Sandbox文件夹:

进入Sandbox:

一个一个搜索,一般最大的那个文件夹就是:

此处存在一个0.dc文件

使用AI分析:


但此时还不能提取文件,使用AI写一个提取工具:

# -*- coding: utf-8 -*-
"""
XX宝小程序缓存 .dc 文件通用提取工具
========================================

格式逆向结果 (0.dc):

0.dc 是一个"外层 tar 容器", 内部条目:
.signature 342B 签名/哈希 (base64)
.meta 14B 元数据
.manifest 887B protobuf 文件索引
每条目: 0A <文件名> 12 { 08 <序号> 10 <流内偏移> 18 <大小> } 30 <CRC32>
<appid>.tar ~6MB 嵌套小程序包 (appId 如 2021001190xxxxxx)

嵌套小程序包 <appid>.tar 内部是标准 ustar tar 流 (21 个文件):
meta.json appConfig.json api_permission tabBar.json manifest.json
index.worker.js index.js splitchunk~N/index.js resources/images/...

两种提取方式:
A) 直接解析 ustar tar 流 (自包含, 文件名/大小在 tar 头里)
B) 按 .manifest 的 protobuf 索引偏移提取, 并用 CRC32(v6) 校验

用法:
python dc_extractor.py <0.dc> [-o 输出目录]
无参数进入交互模式
"""
import argparse
import os
import sys
import zlib

BLOCK = 512
TAR_MAGIC = b'ustar\x00'

# ---------------- protobuf 工具 ----------------
def varint(buf, pos):
result = 0
shift = 0
while True:
b = buf[pos]
pos += 1
result |= (b & 0x7F) << shift
if not (b & 0x80):
break
shift += 7
return result, pos

def parse_msg(buf, start, end):
fields = {}
pos = start
while pos < end:
t = buf[pos]
pos += 1
fnum = t >> 3
wtype = t & 7
if wtype == 0:
v, pos = varint(buf, pos)
fields.setdefault(fnum, []).append(('v', v))
elif wtype == 2:
ln, pos = varint(buf, pos)
fields.setdefault(fnum, []).append(('b', buf[pos:pos + ln]))
pos += ln
else:
break
return fields

# ---------------- tar 流解析 ----------------
def find_tar_start(data):
"""扫描 512 对齐位置, 定位 ustar tar 流起点"""
for pos in range(0, len(data) - BLOCK, BLOCK):
if data[pos + 257:pos + 263] == TAR_MAGIC:
size_raw = data[pos + 124:pos + 136].rstrip(b'\x00').rstrip(b' ')
if size_raw.isdigit() and len(size_raw) <= 11:
return pos
return None

def parse_tar_stream(data, start):
"""解析 tar 流, 返回 [(name, size, content_off, mtime, typeflag, valid)]"""
entries = []
pos = start
n = len(data)
while pos + BLOCK <= n:
block = data[pos:pos + BLOCK]
if block == b'\x00' * BLOCK:
break
# GNU 长文件名
if block[156] == ord('L'):
ln = int(block[124:136].rstrip(b'\x00').rstrip(b' ') or b'0', 8)
longname = data[pos + BLOCK:pos + BLOCK + ln].split(b'\x00')[0].decode('utf-8', 'replace')
pos += BLOCK + ((ln + BLOCK - 1) // BLOCK) * BLOCK
block = data[pos:pos + BLOCK]
name = longname
else:
name = block[0:100].split(b'\x00')[0].decode('utf-8', 'replace')
size_raw = block[124:136].rstrip(b'\x00').rstrip(b' ')
try:
size = int(size_raw or b'0', 8)
except ValueError:
break
mtime = int((block[136:148].rstrip(b'\x00').rstrip(b' ') or b'0'), 8)
typeflag = chr(block[156]) if block[156] else '0'
coff = pos + BLOCK
entries.append((name, size, coff, mtime, typeflag, coff + size <= n))
pos = coff + ((size + BLOCK - 1) // BLOCK) * BLOCK
return entries

# ---------------- protobuf 索引解析 (.manifest) ----------------
def parse_manifest_index(manifest_bytes):
"""解析 .manifest 内容为索引: [(name, id, offset, size, crc32)]"""
rows = []
pos = 0
n = len(manifest_bytes)
while pos + 2 < n:
if manifest_bytes[pos] != 0x1A:
pos += 1
continue
ln, q = varint(manifest_bytes, pos + 1)
end = q + ln
if end > n:
break
f = parse_msg(manifest_bytes, q, end)
if 1 in f and 2 in f:
try:
name = f[1][0][1].decode('utf-8', 'replace')
except Exception:
name = '<bin>'
sub = parse_msg(f[2][0][1], 0, len(f[2][0][1]))
idv = sub.get(1, [('v', 0)])[0][1]
v2 = sub.get(2, [('v', 0)])[0][1]
v3 = sub.get(3, [('v', 0)])[0][1]
v6 = f.get(6, [('v', None)])[0][1]
rows.append((name, idv, v2, v3, v6))
pos = end
return rows

# ---------------- 主流程 ----------------
def extract_tar_entries(data, start, out_dir, crc_map=None, depth=0):
"""解析并写出 tar 流, 返回 (ok_count, bad_list)"""
entries = parse_tar_stream(data, start)
if not entries:
return 0, []

pad = ' ' * depth
print('%s%s[tar 流起点 0x%X, %d 个条目]' % (pad, '├─ ' if depth else '', start, len(entries)))
ok = bad = 0
bad_list = []
nested = []
for name, size, coff, mtime, typeflag, valid in entries:
rel = name.lstrip('/')
if typeflag == '5':
os.makedirs(os.path.join(out_dir, rel), exist_ok=True)
continue
if rel.endswith('/'):
continue
content = data[coff:coff + size] if valid else b''
target = os.path.join(out_dir, rel)
os.makedirs(os.path.dirname(target), exist_ok=True) if os.path.dirname(target) else None
with open(target, 'wb') as fp:
fp.write(content)

status = ''
if crc_map and name in crc_map:
if zlib.crc32(content) == crc_map[name]:
status = 'OK'; ok += 1
else:
status = 'CRC MISMATCH!'; bad += 1; bad_list.append(name)
print('%s %-36s %10d %s' % (pad, name, size, status))

# 检测嵌套 tar (ustar magic 且扩展名 .tar/.pkg2)
if content[:512].find(TAR_MAGIC) != -1 and (rel.endswith('.tar') or rel.endswith('.pkg2') or rel.endswith('.tbp')):
nested.append((rel, content))
return ok, bad, nested

def extract(dc_path, out_dir=None):
data = open(dc_path, 'rb').read()
start = find_tar_start(data)
if start is None:
print('[错误] 未找到 ustar tar 流, 不是有效的 .dc 缓存文件')
return 1

if not out_dir:
base = os.path.splitext(os.path.basename(dc_path))[0] or 'dc'
out_dir = base + '_extracted'
os.makedirs(out_dir, exist_ok=True)

print('文件: %s (%d 字节)' % (dc_path, len(data)))
print('外层 tar 流起点: 0x%X' % start)
print()

# 第一轮: 提取外层条目
entries = parse_tar_stream(data, start)
crc_map = {}
nested = []
for name, size, coff, mtime, typeflag, valid in entries:
rel = name.lstrip('/')
if typeflag == '5' or rel.endswith('/'):
continue
content = data[coff:coff + size] if valid else b''
target = os.path.join(out_dir, rel)
os.makedirs(os.path.dirname(target), exist_ok=True) if os.path.dirname(target) else None
with open(target, 'wb') as fp:
fp.write(content)
# .manifest 解析为索引
if rel == '.manifest':
crc_map = {r[0]: r[4] for r in parse_manifest_index(content) if r[4] is not None}
print('解析 .manifest: %d 个文件索引 (含 CRC32)' % len(crc_map))
# 嵌套包
if content[:512].find(TAR_MAGIC) != -1 and rel.endswith('.tar'):
nested.append((rel, content))

print()
print('外层 tar 条目:')
for name, size, coff, mtime, typeflag, valid in entries:
print(' %-36s %10d' % (name, size))
print()

# 递归提取嵌套小程序包
for rel, content in nested:
print('检测到嵌套小程序包: %s (%d 字节)' % (rel, len(content)))
sub_dir = os.path.join(out_dir, rel + '_extracted')
os.makedirs(sub_dir, exist_ok=True)
ns = find_tar_start(content)
if ns is not None:
print()
ok, bad, _ = extract_tar_entries(content, ns, sub_dir, crc_map=crc_map, depth=1)
print(' [嵌套包校验] CRC32 通过 %d 个, 异常 %d 个' % (ok, bad))
else:
print(' [警告] 嵌套包内未找到 tar 流')
print()
print('完成: 所有文件已提取到 %s' % out_dir)
return 0

def interactive():
print('=' * 56)
print(' 支付宝小程序缓存 .dc 文件通用提取工具')
print(' 格式: 外层tar容器 → 嵌套appid.tar → ustar文件流')
print('=' * 56)
while True:
print()
path = input(' 输入 .dc 文件路径 (回车退出): ').strip().strip('"')
if not path:
break
if not os.path.isfile(path):
print(' [错误] 文件不存在')
continue
out = input(' 输出目录 (直接回车用默认): ').strip().strip('"')
extract(path, out or None)
print('已退出')

def main():
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ('utf-8', 'utf8'):
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
parser = argparse.ArgumentParser(description='支付宝 .dc 缓存文件通用提取工具')
parser.add_argument('dc_file', nargs='?', help='.dc 文件路径')
parser.add_argument('-o', '--outdir', help='输出目录 (默认 <文件名>_extracted)')
args = parser.parse_args()
if not args.dc_file:
interactive()
else:
sys.exit(extract(args.dc_file, args.outdir))

if __name__ == '__main__':
main()

最终可写出加解密脚本

越权漏洞

此处发现一个接口可查看个人信息,但存在三个参数,此时使用最小参数法,先保留第一个参数尝试:

此处发现没有报缺少参数的错误,只是无匹配记录,而刚好此参数值有规律可循,直接编写一个遍历脚本:

成功遍历获取其他用户数据

题外话

这里可以写出双mitm自动处理加解密脚本,实现数据包同时存在密文和密文双字段,修改明文字段自动处理成密文后发送到对方服务器,这样方便直接修改参数进行测试

流量路径:

客户端→8080(本脚本)→Burp(8081)→8085(mitm_to_server)→服务器

请求(客户端→BP):解密密文, 注入__plain__明文,让BP可直接查看/篡改

响应(BP→客户端):若BP篡改了__plain__,用明文重加密还原裸base64,客户端正常解析

Burp关掉默认监听的8080,监听8081,上游监听8085:

可以愉快的渗透了~