关于逆向工程:记一次某加速器APP算法解密实现刷邀请

610次阅读

共计 12125 个字符,预计需要花费 31 分钟才能阅读完成。

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 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://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()

正文完
 0