0x00 工具准备
工具打包下载:https://lanzoui.com/b0eknupng 密码:4vpf
各工具使用方法介绍等详细内容可参考各自文档或。
0x01 发现问题
目标APP:蚂蚁加速器(不提供下载)
打开Fiddler
,模拟器中设置好代{过}{滤}理,安装证书,打开APP,具体步骤自行百度。
Fiddler
中可以看到,所有请求接口发送的数据都是差不多的,看不出发送和返回的数据是什么内容,这次的目标就是将其转为可读的明文,也就是其中的data
和sign
参数生成、返回数据的解密。
0x02 用hook插件简单分析
需提前安装Xposed
框架。
对简单的APP,如果只用了很常用的标准加密算法(如明文字符串拼接后取MD5、使用明文key
进行对称加密),那只要用Inspeckage
、CryptoFucker
或算法助手
其中一个就可以解决了,这三个工具都是用来hook常用加密算法的,如SHA
、MD5
、AES
、DES
等,详见各自文档。
三个工具大同小异,本次测试过程中发现的优缺点如下:
Inspeckage
- 优点
- 各参数会base64编码,不会导致显示乱码。
- 功能很多,加密hook只是其中之一。
- 缺点
- 使用方法比其他两个麻烦一点。
- AES的IV参数有时候会漏掉。
- 有时不显示加密模式。
- sha-256字符串显示错误。
CryptoFucker
- 优点
- 会显示HEX编码和解码后内容,不会导致只显示乱码。
- 加密模式全部能显示。
- 开源,可以自己修改源码。
- 缺点
- 所有加解密都放在同一个文件,看着很乱。
- AES的IV参数有时候会漏掉。
算法助手
- 优点
- AES的IV参数不会漏掉,加密模式全部能显示。
- 加解密参数一一对应,浏览方便。
- 缺点
- 参数显示乱码。
多个工具可以同时开启。配置好一个或多个hook插件后,打开目标APP,随意切换两个界面后,可以在查看各工具结果。本次测试以算法助手
和Inspeckage
为例。
-
从Inspeckage
的Crypto结果中一部分json内容可以确定加密算法是AES,key
是tJb4MRKOT+HeM/S3osRuDdR3JSSkgm/kSG/MB+PiuH8=
(base64编码),没有拿到iv
,解密模式是AES/CFB/NoPadding
,加密模式应该是一样的。
从Inspeckage
的Hash结果中可以看到sign的生成是SHA-256后再取MD5,但是在测试SHA-256加密时结果对不上,暂放。
-
从算法助手
结果中可以看到hook到多种类型加密,点进AES
,可以看到key
和iv
都是乱码,也就是说这两个参数都是不可见字符,不过可以确定加密模式是AES/CFB/NoPadding
。
如果是简单点的APP(像前面说的,明文字符串拼接后取MD5
、使用明文key
进行对称加密),这个时候可能就结束了,直接拿到明文key
、iv
以及MD5
拼接字符串模板,直接去写代码就完事了。
0x03 脱壳
上面用hook插件得到的结果:
- 请求
data
加密和返回数据解密模式为AES/CFB/NoPadding
。
key
是tJb4MRKOT+HeM/S3osRuDdR3JSSkgm/kSG/MB+PiuH8=
(base64编码,可转为HEX编码)。
sign
生成是SHA-256后再取MD5,但是SHA-256结果对不上。
由于没有拿到iv
、SHA-256结果对不上,此时需要进一步反编译分析。
GDA是一个国产免费安卓反编译工具,只有一个几兆的可执行文件,不用配置环境,反编译速度极快,功能比较强大,整体使用体验较好。这里没有选择一些老牌反编译工具,只用GDA就够了。
直接用GDA打开APK,发现提示有360加固,需要脱壳。
GDA中也有拖dex的功能,不过没弄明白怎么用,所以还是用反射大师
来无脑拖dex(步骤:Xposed启用模块/反射大师内选择目标APP并打开/点击六芒星图标/点击当前Activity
/长按写出DEX
)。
此时拿到两个dex,第一个文件比较大,第二个很小,尝试用GDA打开,发现第一个dex无法反编译,而第二个dex内搜不到什么有用信息,所以要用MT管理器或NP管理器修复第一个dex,再用GDA打开。
0x04 用GDA反编译分析
GDA使用比较方便,上方一排工具栏很明确,还有快捷键清单,上手教程见https://zhuanlan.zhihu.com/p/28354064。
因为前面确定了加密模式为AES/CFB/NoPadding
,所以GDA反编译完成后,直接全局搜索(快捷键S
)字符串AES/CFB/NoPadding
或CFB
来定位加密点(也可搜其他字符串/类名/方法名/变量名,一般从请求接口中来寻找搜索的字符串,可以多尝试)。
搜索结果只有两个,双击进入,发现没有混淆,应该比较简单。这里直接定位到了加密、解密的方法中,左侧看方法列表,可以大致确定算法就是这个了。
先稍微读一下Java代码,不会的函数就百度,弄懂这几句还是比较简单的。
public static String decrypt(String p0,String p1){
// p0是明文key,p1是返回数据包的16进制字符串data值(交叉引用查看)
String str = "UTF-8";
byte[] obyteArray = null;
try{
// 待解密data值16进制字符串转字节数组
byte[] bhex2byte = Cfb_256crypt.hex2byte(p1);
// 初始化实例,加密模式为"AES/CFB/NoPadding"
Cipher cInstance = Cipher.getInstance("AES/CFB/NoPadding");
// AES实例参数初始化,key由明文经过EVP_BytesToKey算法生成,iv为data值的前16字节
cInstance.init(2, new SecretKeySpec(Cfb_256crypt.EVP_BytesToKey(32, 16, obyteArray, p0.getBytes(str), 0)[0], "AES"), new IvParameterSpec(Arrays.copyOfRange(bhex2byte, 0, 16)));
// 待解密数据为为data值16字节以后的数据
return new String(cInstance.doFinal(Arrays.copyOfRange(bhex2byte, 16, bhex2byte.length)), str);
}catch(java.lang.Exception e6){
e6.printStackTrace();
return obyteArray;
}
}
public static String encrypt(String p0,String p1){
// p0是明文key,p1是待加密数据(交叉引用查看)
String str = "UTF-8";
byte[] obyteArray = null;
try{
// 初始化实例,加密模式为"AES/CFB/NoPadding"
Cipher cInstance = Cipher.getInstance("AES/CFB/NoPadding");
// AES实例参数初始化,key由明文经过EVP_BytesToKey算法生成,iv未指定,为随机生成
cInstance.init(1, new SecretKeySpec(Cfb_256crypt.EVP_BytesToKey(32, 16, obyteArray, p0.getBytes(str), 0)[0], "AES"));
// 随机生成的iv+加密后的数据,合并后转为16进制字符串,发送给服务器
return Cfb_256crypt.byte2hex(Cfb_256crypt.byteMerger(cInstance.getIV(), cInstance.doFinal(p1.getBytes(str))));
}catch(java.lang.Exception e6){
e6.printStackTrace();
return obyteArray;
}
}
光标放在encryp/decrypt方法上,可以查看是哪里调用的(即交叉引用,快捷键X
),直接定位到了请求接口的封装类,顺便直接拿到了key的明文。
同时在左边发现了sign方法,进去看一下发现果然是接口中的sign(也可以搜索字符来定位,比如搜索前面看到的appVersion=2.1.8
)
public static String sign(String p0,String p1){
// p0是data值,p1是时间戳
return MD5Util.getMD5(Cfb_256crypt.getSHA256StrJava(new StringBuilder()+"appId=android&appVersion=2.1.8&data="+p0+"×tamp="+p1+"2d5f22520633cfd5c44bacc1634a93f2"));
}
分析部分到这里就可以结束了,有key(明文fjeldkb4438b1eb36b7e244b37dhg03j
传入EVP_BytesToKey生成,HEX编码为B496F831128E4FE1DE33F4B7A2C46E0DD4772524A4826FE4486FCC07E3E2B87F
)、有iv(加密iv随机生成,解密iv是数据包前16字节,即前32位字符),可以直接调用AES算法,有SHA-256字符串拼接模板,可以直接拼接调用SHA-256和MD5算法。
注:1byte(字节)=8bit=8位二进制=2位16进制(2的4次方=16,即每4位2进制等于1位16进制)
0x05 验证结果
用数据加解密工具来验证一下,注意key是由算法生成的,base64解码后是乱码,所以这里只能填hex编码转换后的了(工具有网页在线版,GDA内也有,这里用的是Fiddler的一个插件,因为其他的基本不能使用HEX)。
// 发送数据
appId=android&appVersion=2.1.8×tamp=1621007748&data=B5A74CCEA048E1AE91FB4C5BD5A6FA27CCD86F24ABD094763247C15100FA2129469EF559AEA2086A0FA28E78910180580993AE35CCDEBA6C7AD1E07E2508AF9307AD2179B3068D63B2B9260B91E0E79BAE26B36F7CD1824496EB26326AAA76831872EE49998292CDC0D2D7B011C340558D384F01212E9C335BD4CB337E2D72974748B7586050BDB45708930E7512F9AE88BD33FCCFD5257DAFB13D7F5766F5000AA3968DF5ED8434F27384226AE111343DB670A7C7014FD5BB96898E5621E1E0&sign=656c9fb2f0d00b9fcf3fa2d38fa2488d
// 返回数据
{"errcode":0,"timestamp":1621007686,"data":"645328E9B5A19FDCC309EE6067BB3F56E04D05060C268D8B8F0CC850C2DD14117158D6C28D2EAB5C5E27C10690FBAE7729CDE74A20A48CC59BE6FD6C6533DB6D3D120AB160B46A594324F65C2135EB5C9C00FF1EE0CE5682DD62EECDD8BD2E0697DD35AC49DC2735F16C878A46B4C810D1A850A5A80FA85F02833752F7224B9460340B62D20CD1E177CCA878463FA7F76FB0798B2E35B0B75EE1580E0515115670C3E1F504E34F268767BE9A32601D29538724EA6CDDE3FD1D16C605C83A2C30E0AF1F05F6ABEC631CABB3491990FD0623B91466D2E36F4806F7549E839ACF21485AED3EEB768753AF952ED52399A38B1FD4FC42319CA83452F8B76F62B46CEB64E8BB78D3CF28E8A75C045C2D18DE595046584EB37CD3A8FE15831807827ECABB028A3C77334C1FF726B5B075087AE2908A0308188A0D21D604EB11CE00D85FFF8F70C3AF2F4339463D1A93782EB4A7A0AEF880B024419846B207015B4A541E9A57D8F0B0E7FCE055C82DAF90720106862C5DC6F11471E86347AC2B17BC9ED3C60BB9C29043FF838F71F9E8CFA8F9579CF1CFA821F8388A5CE3868C6AE6992FB6D69AD85B8672AE682AEE9A5BD8F2D86640B7167B26BC3B67493B21FAD7D5FA6D670082F9E669B1FDC02FA6007E440453B5FDD0193BB99494B33786407883DFCD881E076205EF8929BD33CEA2357A3F1F02EAFF20FD3D2449011E6A728FE02F4C7109A27A066631D238F5B84BC75977201D36ABDE343D6C7C626987AE66232BB918870FF6F35C4EEEFE5D728D4069B89D6F0099E92F3B1E1D13A4BE9D5DF9EF66B35223105EAC37EEB15E37F7CA77533FCEBF9329983D625BCE2B4690E87EDEAEFC0C1AF145474435A8D322781FFCCE13113189764A65079D281B2FC7066AC5201AA2C191A076C9C55B89E198C1FAEAD690CF4D4F1D441A926EAEBAFC2F6ED1ECECF8571F9C60CBBD0A509EF07CDB3492F9DAB27F6EBE397CA1DB558B8C28A7C6517AEAEA0E57062702D27B4B217A862C80211E92A4B436991707120782E859242A2A95746A5514F0EC1FFFA409300A92D95A3D6B8496EEC86126EF98159AD56A5F45CCE9D0AB7AF582B0B80AB2DC65141B777307E8CA47418F9546C353E422688AB3F3F53C12E10277F8ABAC09ED4E6B3A44C90740A4147488E647E9D7C9105DF3B9D104872112380CC9D46B563ADAB09C9D815FE310084FEBFA253F8A728D3F0CB64CEA1E99FD0EDE2C7FABB7A9C915142E2B6E110C046D838019008B98A66F4CC0142460105C42C407287E629D5A77A1EFBD6CD212BFD1E8D2AC2CB446FF54C8ED111D14D8C6BF060AC9B623154D2793DC4893176C89D25B22BE39231CB3C2915803F76DEA27D828BB95B32E42439415231AC3BEF11B510061F03C60F8DBEB13941C1E3368EC13445A82EC1360EC7FEE82E260F31D1D3D6DA1E59C1ECA4FCF03DCCC7FA787DCD779662F60CE8310021B7A7A2090F9499FE96F6F1A165B4171049220DF2CA60F1A57F7A7EED72F711CB1B1BD823765A2843AB1271BBD452498DBF9614E0E2FBAC4D3222560461259EC2BA22A054B5AAA74715D11C93253F9D27C3A2B530DD3421E5A4809E17F361E05F1BB875EDFA7614743BA1B940934057E0398867992D6F47F7C713C2C927231EDA322118E37C162E3640A914CC13B194502ED8461591946A0F0B2E26D332E27F677B5986C94B06FD5C8ED8B9C0C7CA2AF3E13F00101E83D0C71370626A89D783253CBA409786C8CF1D3AF4AEE2BFC1E47185F37D2A6DAF25AD0BC2D0C60FFE8F2F8BDE3CA61A6D3841A3936B0B8B3EC0F7D2F6F524D4EBF07195AEF8625781A758D47190F14B264ABFD36D9D7151E036D770001F0F4B0E14C6E5AB1D3A69EA9414006B3C8B4AC5F94D4B481DD82CD58DEBFD00896A9266157B31B18C6682ED30119B75BD74B23242BEE09E5E35E3A8721B6CECB2349611858501CEFD8B10335AD8883A85FECDA647379E6F5CB5F7E9C211EE38F4562AF19E79904FE3CCFC7D912B43563EE20ABA37BBD5E0971887511AA2A3218FF12321B3AAD5CA8635FB38A2D59D0FAF03A04747B7DC06E45EB447161099173B0B31B1FCE03B5496797B261183FB6E1C467B64A02B44A78680D7A9DEC731A5A4DE89300F99C8A850D806E13E596A7882D140409E3EA30BD87584BE35594F89109C7F95F30D92B8CDC589DAAF121C5604FD1243911A135336ED7C97CFD0EE1CBDE5997DA3778C72827327FD38556AC417E0C1F4BBD82AC34D8F742C211C472411EA4C16C37176B973419CF85FBCFAFA400D916350ED6BFBD4F5DB5695865010BFB6B4D39DA6DCC4346DA8E2D186E5E633E7B4D57044373105590F8507D897F508604568B055EBF86023A2B273A765104CE1A616BFAAF31560EA96169255644D1A57EC91DBC07A2029FFABFCD910458192C30D1506D79BC383991A2FCB65029CECDE540E70E2734F18DF04A71616A6229BCE80082AA9114C7670F1A126AEFFB30375B99840A65792631106211A352AFCB54FF2EA687D771709FECCA08FF6254FBED743F78E9528D7255AF87EE6276C658D67CBD788460526896DFEE9CCF57EF23F5FC116CFD59414465FA05094AEA80D4687459982C12C5B98BF8A3DF38F13E24F7DAB4D85E7FB4910C0992B9E14285E2CDE40C619244A4ACC1A261A5883C4E504FF549EC43776964ABFF10016D380919CD2AB62FAF312412702E1C9075941BD5791B756B951C539A86871D92C36CA033BF41EF31B21EE35582947B20AA1D00A65A6290E9FAC14CD21B11DE0BB6CDFB906B28F859FBCD9AC2588C5CCD0D06A49DC6DC80603EDE4F6CF709EF7BD4D5F79968664C502282B51F78F1E61FCBB10FC7D7E2747AADA81397C459772936925048E689EF4E681C75F3D6222DA8DC90","sign":"9a0ab084be6f0bc9415b0c204f7fdee5"}
将发送包或返回包data数据分割为两部分,前32位是iv,后面是待解密数据(需要将待解密数据HEX转为base64编码)。
sign字符串拼接模板为"appId=android&appVersion=2.1.8&data="+p0+"×tamp="+p1+"2d5f22520633cfd5c44bacc1634a93f2
,其中p0是data值,p1是时间戳,经SHA-256和MD5加密后结果正确。
0x06 刷邀请
数据包解密完成,就可以写代码了。
这时会意识到,抓包不能看到明文,那怎么写代码?答案还是对照着Inspeckage
等工具查看数据明文。当然也可以是手动hook刚才定位到的加解密方法(Inspeckage
中有自定义hook,但测试无效,估计要重新写个xp插件或者找其他工具替代),实现自动输出数据包明文。再或者根据源码中封装请求包的格式去手动还原。
Python代码
# -*- encoding: utf-8 -*-
'''
@file : main.py
@Time : 2021年05月16日 15:55:51 星期天
@AuThor : erma0
@version : 1.0
@Desc : 蚂蚁加速器刷邀请
'''
import requests
import time
import json
from base64 import b64decode
from hashlib import sha256, md5
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
# from Crypto.Hash import SHA256, MD5 # 和hashlib库一样
class Ant(object):
"""
蚂蚁加速器刷邀请
"""
def __init__(self, aff):
self.aff = aff
self.oauth_id = ''
self.timestamp = ''
self.url = 'http://xxx.xxx.com/api.php'
self.headers = { # 加不加header都可以
# 'User-Agent':
# 'Mozilla/5.0 (Linux; U; Android 7.1.2; zh-cn; E6533 Build/N2G48H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'
}
# 明文key,再经EVP_BytesToKey方法生成最终key,最终HEX为:B496F831128E4FE1DE33F4B7A2C46E0DD4772524A4826FE4486FCC07E3E2B87F
self.key = 'fjeldkb4438b1eb36b7e244b37dhg03j' # 没发现哪个加密库中有EVP_BytesToKey算法
self.hexkey = 'B496F831128E4FE1DE33F4B7A2C46E0DD4772524A4826FE4486FCC07E3E2B87F'
self.b64key = 'tJb4MRKOT+HeM/S3osRuDdR3JSSkgm/kSG/MB+PiuH8='
@staticmethod
def get_timestamp(long=10):
"""
取时间戳,默认10位
"""
return str(time.time_ns())[:long]
def decrypt(self, data: str):
"""
aes解密
"""
ct_iv = bytes.fromhex(data[:32])
ct_bytes = bytes.fromhex(data[32:])
ciper = AES.new(
b64decode(self.b64key), AES.MODE_CFB, iv=ct_iv,
segment_size=128) # CFB模式,iv指定,块大小为128(默认为8,需填8的倍数,貌似AES标准区块大小就是128,和密钥大小128/192/256无关)
plaintext = ciper.decrypt(ct_bytes)
return plaintext.decode()
def encrypt(self, data: str):
"""
aes加密
"""
# cipher = AES.new(bytes.fromhex(self.hexkey), AES.MODE_CFB)
cipher = AES.new(b64decode(self.b64key), AES.MODE_CFB, segment_size=128) # CFB模式,iv自动随机,块大小为128
ct_bytes = cipher.iv + cipher.encrypt(data.encode()) # iv+加密结果合并
return ct_bytes.hex().upper() # hex编码
def get_sign(self):
"""
生成sign
"""
template = 'appId=android&appVersion=2.1.8&data={}×tamp={}2d5f22520633cfd5c44bacc1634a93f2'.format(
self.encrypt_data, self.timestamp)
# sha256
sha = sha256()
sha.update(template.encode())
res = sha.hexdigest()
# nd5
m = md5()
m.update(res.encode())
res = m.hexdigest()
return res
def request(self, d):
"""
请求封包
"""
plaintext = {"version": "2.4.3", "app_type": "ss_proxy", "language": 0, "bundleId": "com.dd.antss"}
d.update(plaintext)
self.timestamp = self.get_timestamp(10)
self.encrypt_data = self.encrypt(json.dumps(d, separators=(',', ':')))
sign = self.get_sign()
data = {
"appId": "android",
"appVersion": "2.1.8",
"timestamp": self.timestamp,
"data": self.encrypt_data,
"sign": sign
}
res = requests.post(url=self.url, data=data, headers=self.headers)
resj = res.json()
res = self.decrypt(resj.get('data'))
print(res)
return res
def get_user(self):
"""
生成新用户
"""
# 取随机md5
m = md5()
m.update(get_random_bytes(16))
oauth_id = m.hexdigest()
data = {"oauth_id": oauth_id, "oauth_type": "android", "mod": "user", "code": "up_sign"}
self.request(data)
self.oauth_id = oauth_id
print(oauth_id)
def invite(self):
"""
刷邀请,邀请码:self.aff
"""
self.get_user()
data = {
"oauth_id": self.oauth_id,
"oauth_type": "android",
"aff": self.aff,
"mod": "user",
"code": "exchangeAFF"
}
self.request(data)
if __name__ == "__main__":
ant = Ant('a6aVx')
ant.invite()