1.引言
随着移动互联网技术和网络经济的迅捷发展,越来越多的网络犯罪活动已经由线下转移到线上实施,移动智能终端的普及,又开始涌现出大量钓鱼、诈骗、赌博、色情等各种类型的应用APP,利用这类应用APP实施电信诈骗、网络传销、网络赌博、黑灰产、套路贷、暗网交易等,可见移动应用程序作为新型犯罪的主要手段,取证民警在获取到涉案AP后,应该如何从中获取到关键侦办线索,本文以笔者办案过程中,遇到的M3U8涉黄视频APP为例,通过M3U8格式文件的概念切入,使用APP取证通常的手段,即动态抓包、静态动态逆向分析、Python代码模拟客户端与涉案服务器通信,从而快速获取相关证据数据信息。
关键词:安卓APP取证、手机取证、m3u8视频、m3u8解密、APP逆向分析
2. M3U8文件是什么
HTTP实时流(也称为HLS)是一种基于HTTP的媒体流通信协议,由Apple Inc.制定。它的工作原理是将整个流分解为一系列基于HTTP的小文件下载,每次下载都加载一个分片。当播放流时,客户端可以从多个不同备选流中选择,这些备选流包含不同编码速率的码流,从而允许流会话适应可用的数据速率。在流会话开始时,HLS下载一个扩展的M3U(m3u8) PlayList文件,其中包含各种可用子流的元数据。M3U8视频格格式也是一种M3U,只是它的编码格式是UTF-8格式,M3U用Latin-1字符集编码。M3U8格式特点是带有一个目录信息或文件。M3U8文件实质是一个播放列表(playlist)。
HLS是如何对视频文件进行加密的,一般步骤为用户将本地视频(未加密)上传到服务器,服务器使用ffmpeg(ffmpeg是一个免费的开源程序库,一个命令行工具软件,专门用来编辑处理各种音视频或图像) 对视频进行切片处理,将生成的m3u8播放列表文件存储在数据库,后端编写接口将m3u8文件以自定义加密的形式传递给客户端,客户端使用key文件解密后放入播放器进行播放。加解密处理流程图如下:
图1 m3u8视频加解密过程
2.1 m3u8加密过程
HLS允许视频流加密和不加密,针对不加密的HLS,那么生成的m3u8索引文件中的视频片段,可以直接播放,仅仅是对原始视频切片成多个小视频片段而已,常见的有腾讯视频、爱奇艺视频等均是采用未加密切片成m3u8索引播放列表,如果要下载这类视频,只需要抓包找到m3u8文件即可下载所有视频片段,在本地合成后及可与原视频一样播放。而加密视频的方式通常有三种aes-128, sample-aes, drm 等。不同的加密方式原理不一样,生成的方式也不一样,目前,市场上几乎清一色的采用aes-128加密方式生成视频片段,比如腾讯课堂、慕课网等等均采用 aes-128加密方式。而该加密方式的原理为加密是对分片进行整个加密,加密得到的分片流是检查不出媒体信息的,即相当于将整个分片文件进行加密,不区分媒体头之类的定义。对视频采用 aes-128加密方法实操过程如下:
1、准备加密密钥:随机生成一个16字节(128位)的数据作为密钥,保存到enc.key中,加密的key的名字可以随意命名,key的关键是16个字节的密文。笔者使用 openssl 命令生成的 key 及以16进制查看key的内容如下图。
图2 生成16进制key文件
2、生成IV(Initialization Vector 可选):在实际解密过程中,该值一般固定在m3u8文件中,解密过程无需关注该值。
3、创建enc.info文件:创建一个文本文件来记录key的信息,文件名可以起随意名字,内容由两部分组成如下:Key URI 以及IV (optional)。其中,Key URI: 指的 key 的位置,即放置 enc.key 的路径,一般为服务器的路径,这个路径会在m3u8里。这个url在测试时,需要填写正确的路径,本地测试时,可以不用服务器的路径。IV (optional):可选,将之前生成的值填到这里,一个完整的enc.info 内容如下图。
图3 enc.info 文件
4、使用ffmpeg 命令对视频进行加密切片,具体指令含义如下。
ffmpeg -y -i test.mp4 -hls_time 9 -hls_key_info_file enc.info -hls_playlist_type vod -hls_segment_filename “index%d.ts” playlist.m3u8
• -i:输入视频
• -hls_time: 分成几个分片,这里是 9
• -hls_key_info_file:加密信息文件
• -hls_playlist_type:这个是可选的,有 VOD, EVENT, LIVE, 点播一般是 vod
• -hls_segment_filename:分片名字
命令执行后,输出结果为playlist.m3u8,可以是其他名字,操作如下图所示。
图4 加密后生成mu38文件及ts加密流文件
图5 m3u8文件内幕
对于m3u8文件, 需要注意,文件中的key前面如果没有网址,那么就相对地址,一般是和 m3u8 同样的地址,除了文件名不一样,前面的网址是一样的,m3u8文件中的视频流ts文件,没有前面没有网址,同样是相对地址,在解密处理相对地址时,记得添加基础网址即可。
2.2 m3u8解密过程
通过实操一遍m3u8文件加密过程,得知一个未加密视频,使用ffmpeg工具传入16进制大小的key文件进行加密以后,生成了m3u8文件播放列表,以及若干个ts流视频文件,这些ts视频片段是整体加密的,本地根本无法播放,而且m3u8播放列表中,包括了key的路径以及ts视频片段的路径,只要下载到m3u8文件播放列表文件,即可在m3u8文件的内容中看到key文件的路径以及ts流视频片段的路径。
那么对于m3u8的解密,只需要拿个两个文件,即m3u8文件播放列表和16进制的ke文件,然后通过第三方开源工具进行下载并解密合并ts视频流片段即可。常见的第三方m3u8解密下载工具有开源的N_m3u8DL-CLI或者吾爱破解论坛中用户逍遥一仙的M3U8批量下载器。使用编程语python的话,也有第三方的开源包m3u8download_hecoter。
所以,解密的关键为一通过抓包获取到 m3u8 文件,二通过抓包获取key文件,或者在APP终端通过逆向调试,找到key的解密算法,从而得到16进制的key字符串。比如腾讯课堂的key即是在用户的浏览器端通过JavaScript计算得出的一个16进制大小的字符串,m3u8解密的流程图如下图。
图6 m3u8 解密过程
3. APP取证流程
拿到一个涉黄视频类APP以后,首先计算它的hash值,然后以副本进行分析,一般是对APP程序进行抓包,APP的运行一般使用雷电等模拟器、有条件的使用真机环境,抓包可以使用fiddler或者httpcanary(黄鸟),fiddler是电脑端进行代理抓包,而httpcanary(黄鸟)本身就是安卓apk程序,直接在模拟器或手机端进行安装抓包查看。
在实际工作中,建议两个抓包工具都使用一下,然后对抓包数据进行对比分析,防止仅仅使用一个工具出现漏包的情况。对抓到的域名数据包进行IP地址核实,视频类APP一般关注登录数据包,确实登录服务器IP,支付数据包,确定支付调用接口,视频源数据包,确定视频服务器IP,如果IP位于国内的及时进行采取调证措施。
抓包分析以后,大致明白了该APP有哪些功能以及出现了哪些数据包的关键字,然后使用逆向工具 JEB、GDA 对关键字进行搜索逆向,获取关键数据的加密算法,从而使用 Python 等编程语言与服务器通信,并得到服务器的拦截认可,从而实现批量下载固定M3U8视频,并解密到本地。M3U8涉黄视频类APP取证分析流程如下图。
图7 APP取证一般流程
4. APP抓包分析
4.1 抓包数据分析
将涉案APP安装到雷电模拟器或者真机(最好是ROOT版)中,然后配置好代理,笔者一般先使用 Fiddler 进行抓包,后再使用 httpcanary(黄鸟)进行抓包,一般关注涉案APP的首页数据包、登录数据包、视频流数据包、支付数据包,对这几类数据包进行分析,查看每种的域名是否一样,IP服务器是否为同一个,然后再多点击几个页面,分析域名及IP服务器是否有变化,确认后,对涉案的IP服务器进行调证操作。下面以httpcanary(黄鸟)抓包数据为例,部分关键数据包截图如下。
图8 登录及首页域名IP
图9 视频流服务器
因为服务器信息作为APP破案的核心资源,一直是案件办理的核心要点。通过域名,我们可以获取到服务器的IP,进而获取服务器物理位置。同时,域名涉及的注册人信息、DSN信息、CDN信息、甚至后端管理平台都可通过相应的服务商调取业务委托人的相关信息,从而形成有价值的线索。下面是笔者在实际工作中,常用的网络查询网站,对于抓到的所有域名及IP,都需要经过表1中的六个网址进行进一步查询核实,从而更好的掌握破案关键线索。
表1 常用查询网址
网址名称 |
网址链接地址 |
站长之家 |
https://tool.chinaz.com |
whois查询 |
https://whois.chinaz.com |
IP地址查询 |
https://ip138.com |
SOJSON |
https://tool.chinaz.com |
端口查询 |
http://coolaf.com/tool/port |
工信部备案信息查询 |
ttps://beian.miit.gov.cn/#/Integrated/recordQuery |
通过上述六个网址查询核实后,及时将相关线索交由案侦民警开展调证工作。取证民警仍需进一步深入分析,上面为了截图更为直观,笔者使用了 httpcanary(黄鸟)来展示,在实际工作中,fiddler也是非常强大的代理抓包工具,而且该工具本身就在电脑端,方便编写 Python 脚本与服务器进行通信,因为该涉案APP为M3U8视频类的APP,那么最好能将该APP的涉黄视频固定到本地,固定的方法一般由两种,第一为传统方式,即录屏,然后点击视频播放,这种方法处理速度较慢,费时费力又费电,不推荐使用。那么第二种方式就是用Python脚本模拟APP的身份与服务器通信,从而批量固定涉案视频文件到本地。
通过 fiddler 抓包发现,涉案APP打开以后,会首选发送一个checkAPi的数据包到服务器,而服务器返回了一Json消息数据,并且有个字段 msg=ok,该数据包详情如下图。
图10 服务器验证身份数据包
通过数据包的名字,大概猜测是验证客户端身份证的一条数据包,在本地模拟该数据包发送给服务器,得到的消息为”非法请求” ,服务器的成功的识别出该数据包不是涉案APP发出的。Python发送数据包的语句及结果如下图。
图11 Python发送数据包请求
服务器只关心接受的数据包信息,通过对接受的数据包信息进行计算判断是否为真正的涉案APP发送的请求,笔者通过多次抓包,多次收集该 checkAPi 的数据包字段信息,从而找出不同点如表2所示。
表2 数据包字段解读
5. APP逆向分析
对于涉案APP的逆向,可以使用 JEB、GDA等逆向工具打开,首先查看该APP的 AndroidManifest.xml 文件,该文件记录了APP的包名,使用的android系统版本信息,描述APP本身的版本信息,描述应用对外暴露的组件、以及使用了哪家的消息推送API等等。对于消息推送的API,也是调证的一个重点关注信息,用于查询该APP的开发者在消息推送厂商的注册信息。
5.1 加壳检测及脱壳
通过查壳工具查看,发现并未识别到加固行为。
常见的加固特征都是去识别lib目录下的so文件,主要是识别文件特征。但是查壳能力取决于加固文件特征的完善程度,有的加固比较小众,市面的工具可能都无法识别。
实际上分析代码时,无法定位到程序入口其实程序就应该是使用了未知的加固方式,其实判断一个程序是够加固,有一个很好的判断方法。就是看能否定位程序入口。这个最准确。接下来以反编译JEB工具为例,先反编译一个不加固的程序。
#!MainActivity:com.ljhhr.mobile.ui.login.splash.SplashActivity (SplashActivity)
这个Main Activity其实就是程序入口。或者叫做程序启动页,主界面等等。没有加固的我们直接点击图中蓝色的(SplashActivity),就会跳转过去。
只要能正常调转的说明没有加固。再试试目标程序。
首先左下部分可以看出来代码很少,其实中间那块区域也没有发现Main Activity,实际上程序入口是去查看Manifest,当反编译工具无法直接识别到这个主界面的时候,其实就可以判断程序一定加固了。发现了这个加固行为,就去尝试脱壳。
这里使用frida配合dexdump.js来进行脱壳,首先运行frida-server。
这里我已经重命名了frida-server为fs,并且赋予了 777的权限,然后运行
输入frida指令来完成脱壳frida -Uf sk8bo.rlr1q9.e1na5g –no-pause -l dexDump.js
然后用adb pull /data/data/sk8bo.rlr1q9.e1na5g/8664560.dex 本地路径
这里因为这个8664560.dex出现多次,我们就优先分析它就好,实际上我们也可以把所有dump下来的dex一起分析,但是因为测试过目标代码在8664560.dex中,这里就不一一pull出来了。脱壳使用的方法蛮多的,只需要掌握任意一种能dump出内存中的dex就行,不限于frida。
5.2 核心算法解密
通过前面抓包及数据包分析得知,要想使用Python 脚本与服务器通信,需要获得服务器的信任认可,认可数据包的关键字段为noise和 sign,这两个字段是如何计算出来的。使用 JEB 3.19对脱壳的dex文件进行逆向分析,既然知道了要分析的字段名称,并且为http请求,可以搜索 “sign=”、”noise”、”http” 等关键字,或者根据每次抓包发现 Referer 的值都为 “www.hahaha.hehe” ,也可以作为逆向的关键词搜索,关键操作步骤如下图。
图14 字符串关键词搜索
图15 sign算法
图16 sign算法
通过上面的分析,得知sign为加盐后的md5值,noise为16位的随机数,既然是随机数那么写死也属于随机数,而sign传递的参数为 token、timestamp、secret、uid、noise 的字符串值拼接后再拼接固定值”JauE^Xl@6upwx1Ov4SCPLOW=8w4J0naI”后再计算MD5即可,使用 Python实现sign的算法后,传入图10中的值,计算出sign与 图10的sign值一样,从而确定了sign字段的计算逻辑。如下图所示。
图17 字段 sign 的算法
实际上sign的还原还有一个小困难,这个困难要看反编译工具。上图的代码来自jeb 3.19.1。如果使用的是别的版本会得到下图的代码。这里会发现字符串被混淆了。
fuu6zl.vs420[]这个方法我们扣出来调用一下。我们处理一下bHZ4cQ。方法名我改成了a,直接调用运行,发现解密后是sign。看着很像是base64编码,但是我尝试了用base64解码,发现不是标准的。所以这个方法应该是混淆字符串的。所以工具其实也很影响分析代码,建议分析的时候多装几个版本的工具。目前发现JEB 3.19.1还原能力应该在JEB中最强大。
6. 批量固证解密M3U8
解决了字段sign以后,Python脚本已经可以得到服务器的信任,进行通信。继续分析m3u8的数据包,发现当点击一个具体视频时,会向服务器的”/index/Get/videoInfo”这个API发送请求,在服务器返回的响应数据包中,包含了该视频的m3u8链接,涉案APP接受到该m3u8链接后,会继续发送Get请求该链接,服务器会继续返回m3u8文件的内容,里面包含了相对地址的key文件以及相对地址的ts视频流文件,涉案APP接受到key文件以后,使用key文件和m3u8文件对后续下载的ts视频流文件进行解密,从而可以在涉案APP的视频窗口观看解密后的ts视频流文件。m3u8视频响应数据包如下图。
图18 m3u8数据包来源
通过观察m3u8数据包的来源,得知每个视频有唯一的id字段,比如上图中的视频的id为23310,而通过首页的相应数据包可知,每个专题有专题的唯一id,点击专题,那么下面的所有视频的id都会展示出来,那么只要会下载一个视频到本地,外层循环加上专题的id就是批量下载该专题的所有视频文件到本地。如果在首页的相应数据中加上循环即可下载该涉案APP的全部视频文件到本地。
所以,批量下载的关键是能成功下载一个视频,下面以上图的id为23310为例下载该并解密合并该视频流文件集到本地。单个视频下载的全部 Python 脚本代码如下,视频下载过程和下载后查看如下图所示。如果需要批量固证,在下面的代码基础上,加上两层遍历循环即可。
import requests
import time, os
import hashlib
import base64
from m3u8download_hecoter import m3u8download
token = ''
def now_to_timestamp(digits=13):
time_stamp = time.time()
digits = 10**(digits - 10)
time_stamp = int(round(time_stamp * digits))
return str(time_stamp)
def get_sign_md5(token, timestamp, secret, uid, noise): #secret:1非vip
md5str = token + timestamp + secret + uid + noise[8:] + "JauE^Xl@6upwx1Ov4SCPLOW=8w4J0naI"
md5res = hashlib.md5(md5str.encode("utf-8"))
return md5res.hexdigest()
def http_Login(token):
headers = {
'User-Agent': 'android-aoteman',
'and_ver': '31',
'Referer': 'https://www.hahaha.hehe/',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
}
timestamp = now_to_timestamp()
secret = '1'
uid = '0'
noise = "Z9603VS2P14ETLAA"
sign = get_sign_md5(token, timestamp, secret, uid, noise)
data = "sign=" + sign + "&client_type=android&secret=" + secret + "&time=" + timestamp + "&token=" + token + "&other_info=%7B%22agent_code%22%3A2369%7D&device_code=ffffffff-da92-eed5-0033-c5870033c587&and_ver=31&uid=" + uid + "&noise=" + noise + "&"
url = 'http://api.tangnvshi.com/index/Login/doLogin111'
token = requests.post(url, data=data, headers=headers).json()['token']
return token
#根据视频ID,获取视频的M3U8链接
def get_videoInfo(token, video_id):
headers = {
'User-Agent': 'android-aoteman',
'and_ver': '31',
'Referer': 'https://www.hahaha.hehe/',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
}
timestamp = now_to_timestamp()
secret = '1'
uid = '4405531'
noise = "UA9J789K872Q5C9K"
sign = get_sign_md5(token, timestamp, secret, uid, noise)
data = "sign=" + sign + "&secret=" + secret + "&time=" + now_to_timestamp() + "&token=" + token + "&id=" + video_id + "&and_ver=31&uid=" + uid + "&noise=" + noise + "&"
#print("data:", data)
url = 'http://api.tangnvshi.com/index/Get/videoInfo'
m3u8url = requests.post(url, data=data, headers=headers).json()['data']['video']['link']
return m3u8url
#下载 m3u8文件到本地
def down_m3u8(token, m3u8_url):
timestamp = now_to_timestamp()
isvip = '0'
uid = '4405531'
noise = "RNQ21F69F1698337"
sign = get_sign_md5(token, timestamp, isvip, uid, noise)
url_start = m3u8_url.rindex('/')
base_url = m3u8_url[:url_start + 1]
#print(base_url)
headers = {
'User-Agent': 'android-aoteman',
'Accept': '*/*',
'Range': 'bytes=0-',
'Icy-MetaData': '1',
'Referer': 'https://www.hahaha.hehe/',
'sign': sign,
'time': timestamp,
'vip': '0',
'token': token,
'and_ver': '31',
'uid': uid,
'noise': noise
}
work_dir = "123"
title = "test"
m3u8_key = requests.get(url=base_url + 'v-crypt.key', headers=headers).content
if not os.path.exists(work_dir):
os.makedirs(work_dir)
temp_dir = work_dir + '/' + title
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
with open(f'{work_dir}/{title}/v-crypt.key', mode='ab') as f:
f.write(m3u8_key)
m3u8download(m3u8url=m3u8_url,
title=title,
base_uri_parse=base_url,
work_dir=work_dir,
threads=1,
key=base64.b64encode(m3u8_key).decode(),
headers=headers,
enable_del=False,
merge_mode=1)
# 模拟登录,获取到登录凭证 token数据
token = http_Login(token)
#获取视频的m3u8视频链接地址
m3u8_url = get_videoInfo(token, '23310')
#下载 M3U8 视频到本地
down_m3u8(token, m3u8_url)
图19 视频下载解密后
7. 总结
通过对M3U8涉黄APP案件的取证分析,笔者认为,在取证过程中,对于陌生的软件、程序不要有畏难心理。只要能了解安卓APP取证的一般流程,即抓包、逆向、Python脚本模拟请求,就能一步一步按部就班分析逆向,最后编写脚本与服务器模拟通信,批量固定涉案视频数据。希望能通过该涉黄APP的固证实战,以此开阔取证思路,达到抛砖引玉的目的。
安全为先,洞鉴未来,奇安信盘古石取证团队竭诚为您提供电子数据取证专业的解决方案与服务。如需试用,请联系奇安信各区域销售代表,或致电95015,期待您的来电!
原文始发于微信公众号(盘古石取证):M3U8涉黄APP取证分析实战