0x00 工具筹备

  • Fiddler(代理抓包)
  • Xposed(hook框架)

    • Inspeckage(通用hook插件)
    • CryptoFucker/算法助手(常见加密算法hook插件,用处同上)
    • 反射巨匠(APP脱壳插件)
  • HEX编码转换/数据加解密工具(测试加解密与后果对照)
  • MT管理器/NP管理器(脱壳后dex修复)
  • GDA(反编译工具)
工具打包下载:https://lanzoui.com/b0eknupng 明码:4vpf
各工具应用办法介绍等具体内容可参考各自文档或。

0x01 发现问题

指标APP:蚂蚁加速器(不提供下载)

因为指标APP没有公开下载渠道,接口域名也无奈间接拜访,所以全文未打码。

关上Fiddler,模拟器中设置好代理,装置证书,关上APP,具体步骤自行百度。

Fiddler中能够看到,所有申请接口发送的数据都是差不多的,看不出发送和返回的数据是什么内容,这次的指标就是将其转为可读的明文,也就是其中的datasign参数生成、返回数据的解密。

0x02 用hook插件简略剖析

需提前装置Xposed框架。

对简略的APP,如果只用了很罕用的规范加密算法(如明文字符串拼接后取MD5、应用明文key进行对称加密),那只有用InspeckageCryptoFucker算法助手其中一个就能够解决了,这三个工具都是用来hook罕用加密算法的,如SHAMD5AESDES等,详见各自文档。

三个工具大同小异,本次测试过程中发现的优缺点如下:

  • Inspeckage

    • 长处
    1. 各参数会base64编码,不会导致显示乱码。
    2. 性能很多,加密hook只是其中之一。

      • 毛病
    3. 应用办法比其余两个麻烦一点。
    4. AES的IV参数有时候会漏掉。
    5. 有时不显示加密模式。
    6. sha-256字符串显示谬误。
  • CryptoFucker

    • 长处
    1. 会显示HEX编码和解码后内容,不会导致只显示乱码。
    2. 加密模式全副能显示。
    3. 开源,能够本人批改源码。

      • 毛病
    4. 所有加解密都放在同一个文件,看着很乱。
    5. AES的IV参数有时候会漏掉。
  • 算法助手

    • 长处
    1. AES的IV参数不会漏掉,加密模式全副能显示。
    2. 加解密参数一一对应,浏览不便。

      • 毛病
    3. 参数显示乱码。

多个工具能够同时开启。配置好一个或多个hook插件后,关上指标APP,随便切换两个界面后,能够在查看各工具后果。本次测试以算法助手Inspeckage为例。

  • Inspeckage的Crypto后果中一部分json内容能够确定加密算法是AES,keytJb4MRKOT+HeM/S3osRuDdR3JSSkgm/kSG/MB+PiuH8=(base64编码),没有拿到iv,解密模式是AES/CFB/NoPadding,加密模式应该是一样的。


Inspeckage的Hash后果中能够看到sign的生成是SHA-256后再取MD5,然而在测试SHA-256加密时后果对不上,暂放。

  • 算法助手后果中能够看到hook到多种类型加密,点进AES,能够看到keyiv都是乱码,也就是说这两个参数都是不可见字符,不过能够确定加密模式是AES/CFB/NoPadding

如果是简略点的APP(像后面说的,明文字符串拼接后取MD5、应用明文key进行对称加密),这个时候可能就完结了,间接拿到明文keyiv以及MD5拼接字符串模板,间接去写代码就完事了。

0x03 脱壳

下面用hook插件失去的后果:

  • 申请data加密和返回数据解密模式为AES/CFB/NoPadding
  • keytJb4MRKOT+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/...。

因为后面确定了加密模式为AES/CFB/NoPadding,所以GDA反编译实现后,间接全局搜寻(快捷键S)字符串AES/CFB/NoPaddingCFB来定位加密点(也可搜其余字符串/类名/办法名/变量名,个别从申请接口中来寻找搜寻的字符串,能够多尝试)。

搜寻后果只有两个,双击进入,发现没有混同,应该比较简单。这里间接定位到了加密、解密的办法中,左侧看办法列表,能够大抵确定算法就是这个了。

先略微读一下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+"&timestamp="+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&timestamp=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+"&timestamp="+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@Link    :   https://erma0.cn@Desc    :   蚂蚁加速器刷邀请'''import requestsimport timeimport jsonfrom base64 import b64decodefrom hashlib import sha256, md5from Crypto.Cipher import AESfrom 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://ant.hyysapi.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={}&timestamp={}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()