Benx1 发表于 2024-8-1 20:39

记录一下一个.Net阅读器的两种破解方式

## 声明

本文章中所有内容仅供研究、学习交流使用,不能用作其他任何目的,严禁用于商业用途和非法用途,否则一切后果自负,与作者无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请发送邮件Service@52pojie.cn联系论坛管理员删除文章

> XX阅读器133,下载见 aHR0cDovL3JlYWQuaG9tZTEzMy5jb20v
>
> 原分析帖链接:https://www.52pojie.cn/thread-1949654-1-1.html

# 1、检查软件

## 运行起来看看

软件运行起来是个框框,右键这个框框有菜单可以注册,注册之后会提示快到期了。



右键系统->会员可以看到会员信息



大概的会员信息都在这里了,下一步

## 拖到DIT里面查壳

一眼顶针,C# .Net 有混淆,先不管,直接丢到dnspy看代码



## 代码分析

直接在dnspy里面搜索 “会员”



可以看到有4个结果,一个个打开看,其实第一条就是我们要找的结果,仔细观察这个函数可以得到两个很关键的点,一个会员状态,一个过期时间,会员状态本身就是个 enum 枚举有3个状态,会员(永久会员),非会员(期限会员),过期。



根据代码可以看到,这个状态是联网获取的并且请求和响应没有加密,那这里就有两个方式破解了:
1. 抓包改相应,直接修改
2. patch程序

# 2、破解方式一、抓包破解
## 分析请求和响应

掏出我们的老朋友HttpDbgPro开始分析,先直接开软件,可以看到登录请求,是个GET请求,请求头里面带上了UID,这个UID就是软件界面显示的UID,实际就是用这个UID判断用户的,响应可以看到是明文的



我们把json格式化一下



### 伪造响应

重要数据都出来了,我们直接在HttpDbg里面修改响应的status为1



不对了,现在怎么提示离线版本了,右键菜单也没会员选项了

刚开始以为有校验,仔细看了下伪造的响应,发现回复的数据比之前的数据小



是被自动回复截断了,改成用文件回复



然后在文件里面改下数据,就可以正常回复,这时候我们再重新打开软件,可以显示会员信息了,但是我们打开会员信息发现和之前的没有变化

### 伪造获取用户信息响应



但是在httpdbg上可以看到有一条新的请求



和登陆请求差不多,也是获取信息的,那我们把这条请求也进行伪造



伪造完了点一下刷新用户信息



可以看到我们已经变成终生会员了,至此,会员部分就分析完了。

## 小应用部分

刚开始一直不知道这个积分有什么作用,但是他放在这里就肯定有放在这里的原因,仔细去找了找,发现了个好玩的地方---应用中心



这里有一些其他的小软件,需要消耗积分下载,我们点击公考行测两万题下载,提示了积分不足,看看请求和响应

响应如下:

```{"status":402,"msg":"\u60a8\u7684\u5206\u4eab\u503c\u4e0d\u8db3\uff0c\u65e0\u6cd5\u4e0b\u8f7d\r\n\u8bf7\u524d\u5f80\u3010\u7cfb\u7edf\u3011-\u3010\u6ce8\u518c\u3011\u6a21\u5757\u83b7\u53d6\u66f4\u591a\u7684\u5206\u4eab\u503c"}```

根据前面的响应我们就能猜到这个正确响应的状态码应该是200,我们改成200试试

> 这里不赘述怎么修改响应了,和之前的一样方法,只是需要注意自动回复设置成“含有”URL,因为每个App的ID是不一样的



可以看到开始下载了,下载完点击打开又出问题了



提示你妹购买,看来还有二次检测。

同样的方式,在HttpDbg里面看请求,有一条 “checkApp” 的请求,把他的状态码也改成200



现在就能正常打开了,答题功能也能用,其他的APP也是同理了,但是我们退出之后再进来应用中心,发现又变成下载了,虽然每次都能正常打开,但是每次都要下载还是挺麻烦的。这个状态每次重开应用中心就会被重置,那肯定有网络请求。

和前面的一样,仔细看就会有个getAppList请求,分析请求就可以发现,你已有的应用的status是1,没有的就是0



手动把这些状态全部改成1,就可以看到我们已经下载过的APP就不会需要再次重新下载了。



至此,伪造响应破解的分析就完成了,要写成代码很简单,就留给评论区的大牛们去做了。我们开始下一步,Patch程序。

# 3、破解方式二、修改软件

## 分析程序

前面已经说了,判断是不是会员的关键点在BindRegister()这个函数里面,关键点就在Bis.LoRes.UserStatus = regResult.Status;在这里设置了用户的会员状态。

## 修改
### 修改会员

我们已知永久会员的状态是1,直接右键编辑IL指令



我们先把这两句选中,右键用NOP替换,然后在15行填上idc.i4.1(常数1),然后点确定



可以看到代码里面赋值的地方已经被我们替换成一个常量了。注意看下面的switch语句,判断的是服务器返回值的status,所以还需要修改一下,和上面的同理。



上面的这个修改了之后switch就变成判断1了,其实也可以改成本地的数据判断,就是改Call,但是我懒得改了,有兴趣的话自己研究一下,不难。

然后 左上角-》文件-》全部保存,可以加点后缀,比如破解会员,避免自己搞混了,然后再运行

> 要注意,dnspy的调试必须先保存再调试才有效果,我们暂时不需要调试,所以保存了之后直接去打开这个exe看效果



可以看到我们已经变成终身会员了,会员功能也有效果

### 修改应用中心
#### 修改下载

应用中心和之前的一样,下载会判断,所以需要修改判断点,直接在dnspy搜索“需要消耗”,搜出来的第一个就是弹窗提示的内容



我们点进去可以看到很明显的get请求,判断状态是不是等于200



直接右键-》编辑IL指令,这里有几种改法,一个是改成0不执行,跳过这段代码,另一个是取反,让他等于200的时候才提示不足,或者直接删除return等等,我们这里直接取反



选择这4条语句,右键反转分支即可,因为我们没有积分,返回值必定不可能是200。

> 这里修改完之后再保存一下,然后看效果。最好做一步保存一步,这样即使有问题闪退了,也能在上次修改保存好的文件基础上再改

这里已经可以下载了,但是打开和之前的抓包一样,有二次检测会提示没有购买,所以要找一下检测的点并且修改。
#### 修改二次检测

二次检测稍微麻烦一点,因为提示的内容是服务器返回的,搜不到字符串,所以要跟一下流程。

我们先返回上一步修改下载的位置,可以看到这一块是打开App的,但是没有调用函数,只是改了状态



所以我们这一步要右键这个this.start然后点击分析,可以看到读取和赋值都是那个函数调用了,追一下可以追到下面这个函数


代码精简了,分开看就知道是在干嘛了

```
// 动态加载指定路径的DLL文件
Assembly assembly = Assembly.LoadFile(Util.GetAppPath("/app/" + app.Path) + "/" + app.Path + ".dll");

// 获取assembly中指定类名的Type对象
Type type = assembly.GetType(app.ClassName);

// 使用反射创建type类型的实例
object obj = Activator.CreateInstance(type);

// 如果obj实现了IACSetter接口,将调用其SetIAC方法
if (obj is IACSetter setter)
{
    // 创建一个AppController实例并传给SetIAC方法
    setter.SetIAC(new AppController(app));
}
```

进这个类就可以看到


这个检测函数了,但是这个检测函数值负责返回,不判断。分析也没有发现调用,那说明是小App的DLL调用了这个方法,小APP有点多,不可能一个个去改,这个函数留给我们的操作空间也不是很多,因为没有声明临时变量,所以修改ApiResult的内容还挺麻烦的,那我们换个思路,让他请求一个返回status返回200的地址。

打开抓包软件,看看软件请求的哪个数据一直都是200返回值的



找到了一个符合要求的,这个是每次退出都会调用的,我们吧参数改一下看看是不是还返回200

经过测试修改参数之后返回的还是200,说明没对传入的参数检测



那我们修改一下访问的路径



直接右键编辑C#方法,把这个改成active.php或者修改获取的改成获取active的。

保存,测试



OK,可以打开了,但是我们还是遇到了和抓包破解一样的问题,每次都要重新下载。

#### 处理状态检测

我们再去找状态检测的地方,继续返回到修改应用中心的地方,在这个类里面可以看到有添加item到列表的操作,那我们就看看在哪里设置了按钮的状态,往上翻翻可以看到一个private void SetActionStyle(ReApp **app**) 的函数设置了按钮的状态,

这里是用switch语句判断添加的



我们点击这个成员变量,找到他定义的地方(这里直接点分析是不行的,因为他是初始化过的一个Map(字典)),要找往字典添加的方法调用,所以先跳转到它定义的地方,因为是个私有变量,所以肯定在这个类内部添加的数据,在代码搜索btnActions



很容易就能找到赋值的地方,关键点就是这个**rApp**.Status == 1,那我们就把这个判断去掉,右键,编辑IL指令



修改后的C#代码如下


这样就没有状态判断了,默认都是已经购买了的。

我们保存打开看看效果(和上面图一样,我就不贴了),可以看到下载了的软件默认就是打开按钮,不需要再次下载了,至此,分析结束。

# 4、后语

> 具体的IL指令代表的什么意思就不解释了,可以百度或者问GPT都可以得到很好地回答。

这个软件分析没花多久,我个人认为这个软件比较简单,很适合新手练手,不管是Patch技巧还是抓包姿势都很适合。但是还是那句话,只做分析讨论,不提供成品,如有需求请支持正版。

RCKLV 发表于 2024-8-16 12:03

学习了,感谢楼主。恭喜荣登“吾爱破解”公众号文章,特地过来收藏点赞!

jha334201553 发表于 2024-8-16 22:10

本帖最后由 jha334201553 于 2024-11-7 23:05 编辑

还有方法三:
hosts 文件加入
127.0.0.1 read.home133.com
然后运行本地服务注册
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import json
from flask import Flask

app = Flask(__name__)

# http://read.home133.com/s.php?u=3616926
@app.route("/",methods=['GET','POST'])
def home_page():
    return "home123 register server"

# 启动获取更新信息等
@app.route("/apii/getPreRegInfo.php",methods=['GET'])
def pre_reg():
    pre_register = {}
    pre_register['status'] = 200
    pre_register['msg'] = 'success'
    pre_register['data'] = {}
    pre_register['data']['member_flag'] = '0'
    pre_register['data']['share_flag'] = "1"
    pre_register['data']['share_code'] = ''
    pre_register['data']['last_version'] = '1.2.0.5'
    pre_register['data']['update_version'] = '1.2.0.0'
    return json.dumps(pre_register)

# 用户登录
@app.route("/apii/userLogin.php",methods=['GET'])
def login():
    user_info = {}
    user_info['status'] = 200
    user_info['msg'] = 'success'
    user_info['data'] = '3616926'
    return json.dumps(user_info)

# 第二次自动登录
@app.route("/apii/autoLogin_2.php",methods=['GET'])
def auto_login():
    user_info = {}
    user_info['status'] = 200
    user_info['msg'] = 'success'
    user_info['data'] = {}
    user_info['data']['last_version'] = '1.2.0.5'
    user_info['data']['update_version'] = '1.2.0.0'
    user_info['data']['chapter_ai_url'] = 'http://read.home133.com/apii/chapter_ai_url.php'
    user_info['data']['member_share_cnt'] = 999999
    user_info['data']['point_day'] = 999999
    user_info['data']['qq_group'] = '760414837'
    user_info['data']['user_name'] = '13800000000'
    user_info['data']['user_create_time'] = '2000-01-01 00:00:00'
    user_info['data']['phone'] = '13800000000'
    user_info['data']['nick_name'] = '本地注册用户'
    user_info['data']['nick_flag'] = 0
    user_info['data']['sex'] = 0
    user_info['data']['head_img'] = None
    user_info['data']['channel'] = ''
    # user_info['data']['append'] = []
    # user_info['data']['site'] = []
    # user_info['data']['app'] = []
    user_info['data']['status'] = '0'
    user_info['data']['expire_date'] = '2099-12-30'
    user_info['data']['msg'] = {}
    user_info['data']['msg']['msg'] = '本地终身会员'
    user_info['data']['msg']['cap'] = '极简单行阅读器提示'
    user_info['data']['msg']['url'] = ''
    user_info['data']['msg']['close'] = False
    user_info['data']['active_id'] = '676875C1DC56B4C0F1DC6F30BB7BE753'
    return json.dumps(user_info)

# 获取注册到期时间
@app.route("/apii/getRegInfo.php",methods=['GET'])
def reg_info():
    register_info = {}
    register_info['status'] = 200
    register_info['msg'] = 'success'
    register_info['data'] = {}
    register_info['data']['status'] = '0'
    register_info['data']['expire_date'] = "2099-12-30"
    register_info['data']['total_share'] = 999999
    register_info['data']['share'] = 999999
    register_info['data']['share_ucnt'] = 999999
    register_info['data']['pay_point'] = 999999
    return json.dumps(register_info)

# GET ?activeId= HTTP/1.1\r\n
@app.route("/apii/active.php",methods=['GET'])
def active_last():
    active_info = {}
    active_info['status'] = 200
    active_info['msg'] = 'success'
    return json.dumps(active_info)

if __name__=='__main__':    app.run(host='127.0.0.1', port=80, debug=True)

登录信息存储在   
%ALLUSERSPROFILE%\Read133

luxingyu329 发表于 2024-8-2 13:52

高手,也不知道怎么样才能达到这个水平?

pizazzboy 发表于 2024-8-2 14:42

楼主很出色,谢谢。

dglbh 发表于 2024-8-2 16:09

HttpDbgPro能按不同的请求的方法返回不同的数据吗?

Benx1 发表于 2024-8-2 16:48

dglbh 发表于 2024-8-2 16:09
HttpDbgPro能按不同的请求的方法返回不同的数据吗?

同一个URL的GET和POST吗,应该是不能的,HttpDbgPro是根据URL来自动回复的,不能判断请求方式。
而且市面上应该不会有GET POST 使用同一个URL的这种设计

user52pj 发表于 2024-8-2 17:33

牛逼啊,真牛逼!(不要判我灌水啊,由衷的由感而发,发自肺腑地)

lancelot623 发表于 2024-8-2 19:23

很详细,感谢分享,学习一下

zhaohainuo 发表于 2024-8-2 19:24

感谢分享,学习一下

dglbh 发表于 2024-8-2 22:36

Benx1 发表于 2024-8-2 16:48
同一个URL的GET和POST吗,应该是不能的,HttpDbgPro是根据URL来自动回复的,不能判断请求方式。
而且市 ...

是post 不同的方法

msmvc 发表于 2024-8-3 11:09

思路清晰流畅
页: [1] 2 3 4 5 6
查看完整版本: 记录一下一个.Net阅读器的两种破解方式