某习惯app的脱壳分析与破解

参考了下面的博文学习

某习惯app的Signature分析与vip破解 | twogoat/showmakerの小站

需要app名联系我

几种常见的脱壳相关工具

反编译:

Jdaxhttps://github.com/skylot/jadx

同时支持命令行和图形界面,能以最简便的方式完成apk的反编译操作。

jd-gui:http://jd.benow.ca/

类似jadx

Dex2jar:http://sourceforge.net/projects/dex2jar/files/

类似jadx

ApkTool :https://bitbucket.org/iBotPeaches/apktool/downloads/

ApkTool 的最重要的两个作用是解包和打包 ;

解包 : 拿到 APK 文件 , 如果按照 zip 格式解压出来 , xml 文件都是乱码 ; APK 文件打包时 , 会将 xml 文件进行压缩转为二进制文件 , 以减小体积 ; 解包时 , 必须使用 ApkTool 解包工具 , 将二进制数据格式的 xml 文件转为 文本 xml 文件 , 才能获取刻度的 xml 文件 ;

打包 : 将使用 ApkTool 工具解包后的零散文件 , 再次打包成 APK 文件 ,

Frida脱壳工具

FRIDA-DEXDump: https://github.com/hluwa/FRIDA-DEXDump

脱壳

MT管理器可以看到是有360加固的

image-20251109214311529

公司名称 对应的壳包名
爱加密 libexec.so,libexecmain.so,ijiami.dat
梆梆 libsecexe.so,libsecmain.so , libDexHelper.so libSecShell.so
360 libprotectClass.so,libjiagu.so,libjiagu_art.so,libjiagu_x86.so
百度 libbaiduprotect.so
腾讯 libshellx-2.10.6.0.so,libBugly.so,libtup.so, libexec.so,libshell.so,stub_tengxun
网易易盾 libnesec.so

用查壳工具,发现是360加固

moyuwa/ApkCheckPack: apk加固特征检查工具,汇总收集已知特征和手动收集大家提交的app加固特征,全网最全开源加固特征,支持40个厂商的加固检测,欢迎大家提交无法识别的app

image-20251109214807495

frida-ps -Ua

image-20251109221946043

我用的frida版本是frida16.4.2 用17的高版本 启动frida 会出现白屏

直接frida-dexdump -U -f xxxx -o E:\Desktop\dump\

或者直接frida-dexdump -FU -o E:\Desktop\dump\

1
2
3
指定App的应用名称:frida-dexdump -U -n 保利票务
指定App的应用进程ID:frida-dexdump -U -p 3302
指定App的应用包名:frida-dexdump -U -f com.iCitySuzhou.suzhou001

image-20251110145509746

image-20251110104750933

全部拖入jadx

注意的是,这里需要为no

image-20251110144710025

请求Signature分析

配一下小黄鸟或者burp抓包环境

小黄鸟我这配了有点问题,因此用burp

完整教程:【burp手机真机抓包】Burp Suite 在真机(Android and IOS)抓包手机APP + 微信小程序详细教程 - yangykaifa - 博客园

image-20251117133538530

image-20251117134523148

可以看到一些信息,测试下接口,随便点一些东西找一下post

image-20251117145333383

image-20251117135507377

根据几个接口定位到

image-20251117150503317

image-20251117151231426

跟踪到

image-20251117151722293

image-20251117151914779

其实就是分析activateCode

1
2
3
4
5
6
7
findViewById(R$id.sms1).setOnClickListener(new a());
findViewById(R$id.activationCodeBtn).setOnClickListener(new View.OnClickListener() { // from class: p1.w4
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
GenerateActionCodeActivity.this.O(view);
}
});

调用O

1
2
3
4
5
6
7
8
9
10
11
12
/* JADX INFO: Access modifiers changed from: private */
public /* synthetic */ void O(View view) {
Q();
}
private void Q() {
new MaterialDialog.Builder(this).G("兑换会员").l("请输入激活码", "", new MaterialDialog.f() { // from class: p1.y4
@Override // com.afollestad.materialdialogs.MaterialDialog.f
public final void onInput(MaterialDialog materialDialog, CharSequence charSequence) {
GenerateActionCodeActivity.this.P(materialDialog, charSequence);
}
}).E();
}
1
2
3
4
public /* synthetic */ void P(MaterialDialog materialDialog, CharSequence charSequence) {
if (!TextUtils.isEmpty(charSequence)) {
M(charSequence.toString().trim());
}

接下来

1
2
3
4
private void M(String str) {
String str2 = "{\"activeCode\":\"" + str + "\"}";
i9.a.l(a.e.f22775v).m3292upJson(str2).execute(new c(String.class, str2));
}

调用“兑换激活码”的接口

i9.a.l(a.e.f22775v)是构造请求对象

执行请求(execute),并传入回调,new 就是“创建一个处理器对象”,创建一个回调处理器,网络请求结束后由它处理结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class c extends DailyCallBack<String> {
c(Class cls, String str) {
super(cls, str);
}

@Override // com.itally.base.data.DailyCallBack
public void loadSuccess(DailyResponse<String> dailyResponse) {
if (dailyResponse.isSuccessCode()) {
return;
}
q4.o.a().d(GenerateActionCodeActivity.this, !TextUtils.isEmpty(dailyResponse.getMsg()) ? dailyResponse.getMsg() : "兑换失败~");
}

@Override // m9.a, m9.b
public void onError(u9.e<DailyResponse<String>> eVar) {
super.onError(eVar);
q4.o.a().d(GenerateActionCodeActivity.this, "兑换失败~");
}
}

因此可以跟踪到DailyCallBack

DailyCallBack 是所有网络请求的回调类,负责:

  1. 发请求前自动生成签名 (signature)
  2. 统一设置请求头(时间戳、nonce 等)
  3. 统一 JSON 解析 DailyResponse
  4. 拦截

第一部分:构造函数

1
2
3
4
public DailyCallBack(Class cls, String str) {
this.aClass = cls;
this.content = str;
}

aClass 是返回数据的类型,例如 ActiveCode.classcontent 是你发送的 JSON(请求体)

content 会被写入签名算法中,这是重点!

第二部分:onStart() —— 请求开始前自动生成签名

1
2
@Override
public void onStart(cVar) {

做了几件重要事:

添加时间戳

1
2
String valueOf = String.valueOf(z8.c.a());
cVar.headers("timestamp", valueOf);

生成随机 nonce

1
2
uuid = UUID.randomUUID().toString().replace("-", "");
cVar.headers("nonce", uuid);

准备签名字段

签名字段包括:

  • 请求体 JSON:this.content
  • header 里的 channel
  • deviceinfo
  • platform
  • clientversion
  • deviceid
  • timestamp
  • nonce

按顺序加入 list:

1
2
3
4
5
6
7
8
arrayList.add(this.content);   //请求体
arrayList.add(channel)
arrayList.add(deviceinfo)
arrayList.add(platform)
arrayList.add(clientversion)
arrayList.add(deviceid)
arrayList.add(timestamp)
arrayList.add(nonce)

排序(按字典序)

1
Collections.sort(arrayList);

拼接成大字符串

1
2
3
for (String str : arrayList) {
stringBuffer.append(str);
}

用 KEY”签名”

1
String e10 = d.e(KEY.getBytes(), stringBuffer.toString().getBytes());

这里 KEY 是:

1
06fdrlDr625oTBbW

这就是 签名密钥

签名算法是 d.e()

放入请求头

1
cVar.headers("signature", e10);

看到这里,你应该已经明白:

想伪造 API,就必须伪造 signature,因为服务器一定会验证 signature。

幸运的是:

  • KEY 是硬编码的
  • 签名算法 d.e() 也在 APK 中

完全可以:

  • 逆向 d.e() 算法
  • 自己用 Python/JS 生成 signature
  • 或用 Frida hook d.e() 拿返回值
  • 或直接篡改 signature 验证逻辑(patch)

第三部分:onSuccess

1
2
3
public void onSuccess(e<DailyResponse<T>> eVar) {
loadSuccess(eVar.a());
}

就是把解析结果交给业务层。

第四部分:convertResponse —— JSON 解析器

1
2
DailyResponse<T> dailyResponse2 =
JSON.parseObject(json, new TypeReference<DailyResponse<T>>(this.aClass){});

返回结构是:

1
2
3
4
5
{
"code": 0,
"msg": "success",
"data": { ... }
}

如果 code 是 401:

1
EventBus.post(new Respons401(...));

也可以伪造成功返回

只要你 hook:

1
2
DailyResponse.getCode() 返回 0
DailyResponse.isSuccessCode() 返回 true

整个 APP 就认为激活成功。

题外话,如果不是硬编码的key有什么对抗手段?

方案 1:将签名逻辑放到服务器(完全不在客户端处理)(最安全)

客户端不做加密、不做签名,只做:

1
2
- 身份凭证(token)
- 基本参数

方案 2:使用短时效动态秘钥(临时 key)

流程如下:

  1. 客户端启动时向服务器请求一个临时 key
  2. 服务器返回一个 5 秒有效/一次性 key
  3. 客户端用临时 key 生成 signature
  4. key 过期自动失效

方案 3:使用“服务端签名 + 前端签名混合模式”(抗伪造)

客户端只参与一部分签名,例如:

1
signature = HMAC(server_key, server_data) + MD5(client_data)

或者:

  • 客户端参与“弱签名”
  • 服务器做核心签名

算法追踪到

image-20251117161130977

是一个标准 HMAC-SHA256 签名工具类

这里直接改了下某习惯app的Signature分析与vip破解 | twogoat/showmakerの小站

的代码

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import hmac
import hashlib
import requests
import json
import time
import uuid
# 请求 URL

def getSignature(headers, data):
# 请求体
param_list = []

param_list.append(headers['Channel'])
param_list.append(headers['Deviceinfo'])
param_list.append(headers['Deviceid'])
param_list.append(headers['Clientversion'])
param_list.append(headers['Nonce'])
param_list.append(headers['Platform'])
param_list.append(headers['Timestamp'])
param_list.append(data)
param_list.sort()
param_str = ''.join(str(item) for item in param_list)
key = b"06fdrlDr625oTBbW"
message = (param_str).encode()
hmac_sha256 = hmac.new(key, message, hashlib.sha256)
return hmac_sha256.hexdigest()


# 请求头
headers = {
'Accept-Language': 'zh-CN,zh;q=0.8',
'User-Agent': 'okhttp-okgo/jeasonlzy',
'Channel': '_vivo',
'Deviceinfo': 'Xiaomi|M2102K1AC|13',
'Platform': '1',
'Clientversion': '6.27.7',
'Deviceid': 'ffffffffd3a80c14d3a80c1400000000',
'Token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI4M2RkZTRlY2Y4ZjE0ZGIyYTM2ZjUzODY2NjA1NjdjYSIsImlhdCI6MTc2MzM1NzU5Nn0.1dapuoDHVTwoWNf-EpA8PomGXru3qw2vG1NLp5PuvdPfrqjQG5sTK2cSkNdhwEE2te3s-DA0XjwDWdoa8lp7PA',
'Timestamp': str(int(time.time())),
'Nonce': str(uuid.uuid4()).replace("-", ""),
'Signature': None,
'Content-Type': 'application/json;charset=utf-8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
}

data = {
"activeCode": "1919810"
}

data = json.dumps(data).replace(" ", "")

Signature = getSignature(headers, data)
print(Signature)
headers['Signature'] = Signature
#print(headers)

url = 'https://xianbeikeji.com/daily/app/user/exchangeActiveCode'

response = requests.post(url, headers=headers, data=data)

print(response.status_code)
print(response.text)

image-20251117163815268

Vip破解

image-20251117164210017

image-20251117164218360

image-20251117164315509

image-20251117164421075

我们不应该hook isVip(),为什么在另一篇里讲过了

image-20251117165308868

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_user(){
Java.perform(function(){
var User = Java.use("com.itally.base.data.bean.UserInfo")
var isVip = User.isVip
isVip.implementation = function () {
let res = this.isVip()
this.setVipFlag(1)
this.setVipType(1)
console.log(res)
return true;
};
})
}

ps:frida -U -p 13937 -l xiaoxiguan.js

要用attach去hook而非spwn

because:

  1. Frida 启动 App 的进程
  2. App 还没开始加载 dex
  3. Frida 就执行你的脚本
  4. 此时 Java 层 尚未初始化
  5. 所以 UserInfocom.itally.* 全部都还没加载
  6. 因此 Java.use("xxxx") → ClassNotFound

APP还没加载 Java 类时去 Hook,当然找不到。

8a6342f1c4b8e7cb47db544a9f654041