0x0 前言
此软件不同于普通的Android软件, 是由 Mono for Android
编写的, 主逻辑的语言为C#, 在尝试破解这款软件的时候花费了我大量功夫, 特此记录
0x1 界面分析
首先打开软件有一个输入注册码的输入框, 随便输入一串注册码点击验证
随后显示: 操作失败, 验证码不正确, 尝试抓包, 发现请求
说明此软件为网络验证, 有了这些信息准备开始逆向
0x2 反编译
首先将apk扔进 Android Killer
(下面简称AK)里进行分析, 发现此软件并不同于普通的软件
发现其只有一个Activity, 且都以 md5
命名, 怀疑加壳, 使用查壳工具进行验证
发现并没有加壳, 只能手动进行分析, 只有一个Activity肯定不正常, 首先检查 assets
和 lib
查看是否有线索
在 lib
文件里发现猫腻, 有几个 libmono
开头的.so文件, 还有 libmonodroid_bundle_app.so
, 经过百度, 此Android程序并非传统的Android程序, 而是 Mono for Android
, 使用C#写的.
检查 MainActivity
代码, 发现大量native代码, 说明Java层全是代理方法, 没有主逻辑, 现在应该寻找C#代码藏在哪里
网上查询得知, 所有的C#代码都藏在 libmonodroid_bundle_app.so
内, 且是个千层饼结构, 尝试使用 binwalk
检查, 发现大量压缩包
不过一个一个解压是要死的, 于是上GitHub寻找工具, 找到mono_unbundle这款工具
使用命令 mono_unbundle libmonodroid_bundle_app.so dlls/
解包, 获得dll文件一堆
根据 mono for android
结构, 主逻辑在 <appname>.dll
内, 使用 dnSpy
进行反编译
查找其页面控制, 根据Android开发习惯, 重点寻找 ViewModel
, 于是在 AutoR.ViewModel
内寻找到重点函数 OnCDKCommandExecuted()
分析其代码, 可以梳理出其逻辑大概是这样的
- 将注册码发送出去, 返回数据分为三段, 分别为
登录成功|剩余时间|CDKToken
- 然后CDKToken拿去进行验证, 拼接字符串
CDKToken|时间戳
, 然后与 123C7E5E875FBF0EEE2583F8AF3DDFF9
进行循环异或运算 - 将加密的数据发送给服务器, 服务器进行某种运算后返回一个Base64
- 客户端对Base64与时间戳进行运算, 最终能变成字符串PASS, 说明验证成功
0x3 破解
这里有两种思路
- 直接修改C#代码并回编译
- 通过代理拦截验证函数, 使其返回正常
由于选择第一种后我不会将dll打包回so文件, 我这里选择第二种方法
首先通过 Fiddler
进行拦截, 返回固定数据, 伪造第一个数据包
可以发现软件发出了第二个数据包, 数据为 NTA0MDMyOzo1NjY6OjIzMTIzfzIxMDc2NTQ7OjM=
, base64解码后为 504032;:566::231232107654;:3
返回的数据包经过一定的运算后, 最终变成 PASS
, 尝试进行反向运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import base64
def login4(data_in: str): input_bytes = list(base64.b64decode(data_in)) key_byte = list("123C7E5E875FBF0EEE2583F8AF3DDFF9".encode()) for i in range(len(input_bytes) - 1, -1, -1): for j in range(len(key_byte) - 1, -1, -1): input_bytes[i] ^= key_byte[j] ok, token = bytes(input_bytes).decode().split("|") print(ok, token) ok_bytes = list(ok.encode()) final_data = list("PASS".encode()) for i in range(len(final_data) - 1, -1, -1): for j in range(len(ok) - 1, -1, -1): final_data[i] ^= ok_bytes[j] return base64.b64encode(bytes(final_data)).decode()
def verify(): ok = "637301896559910210" array2 = list(base64.b64decode(login4("NTA0MDMyOzo1NjY6OjIzMTIzfzIxMDc2NTQ7OjM="))) ok_bytes = list(ok.encode())
for i in range(len(array2)): for j in range(len(ok_bytes)): array2[i] ^= ok_bytes[j] return bytes(array2).decode()
if __name__ == "__main__": print(verify())
|
其中 login4()
是接受软件请求验证的数据, verify()
模拟软件发送验证, 随后编写代理服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| from flask import Flask, request import base64
app = Flask(__name__)
@app.route('/api/cd694e62ba74089c8df7aefb324c7910') def hello_world(): data = request.args.get("login4") if data: return login4(data) else: return "登录成功|999999|1234567890"
def login4(data_in: str): input_bytes = list(base64.b64decode(data_in)) key_byte = list("123C7E5E875FBF0EEE2583F8AF3DDFF9".encode()) for i in range(len(input_bytes) - 1, -1, -1): for j in range(len(key_byte) - 1, -1, -1): input_bytes[i] ^= key_byte[j] ok, token = bytes(input_bytes).decode().split("|") print(ok, token) ok_bytes = list(ok.encode()) final_data = list("PASS".encode()) for i in range(len(final_data) - 1, -1, -1): for j in range(len(ok) - 1, -1, -1): final_data[i] ^= ok_bytes[j] return base64.b64encode(bytes(final_data)).decode()
app.run()
|
然后通过 Fiddler
的重定向功能进行测试
验证结果
这个软件就此就破解完成了