文章

计划把ykb题库本地化

计划把ykb题库本地化

提取软件

去广告

该死的推广, 每次刷评论区都得看广告 尼玛4个月不看专业知识又忘记了…, 就是这个推广, 先干掉, 顺便再次记住, 渗透压280-310, 哈哈B区国家线左右 分析布局(用的是AutoJS6)看ID idHex(‘0x7f0a00e1’) ### 遁入代码 追踪 先看这个方法吧 大概率就是这里加载的广告, 按下x键查找一下用例 用mt去签名,清空方法安装看一下吧 确实没广告了, 接下来接着分析题目解析接口. 然后置入代码本地化处理. 3年前就想这么搞了, 今天心血来潮, 干他丫的

分析题目控件

显示题目的控件id是titletv

遁入代码

感觉都不是关键的, 对比一下idhex:7f0a1aa5 完蛋, 电脑卡了…这个破软件体积怎么这么大(400+MB)? 多少给它瘦瘦身. 勾选代码才是有效搜索 查看交叉引用 主要是找到对应的model类, 看看它是怎么解析的,肯定有对应的解析方法 这里只是绑定了控件, 应该找this.titletv.setText()这种代码, 才能找到model的来源, 所以看一下交叉引用 哈哈, 很明显第三,四条就是. 区别在于四有题号, 但数据来源都是括号里面的title参数, 点击跳过去看看交叉引用 title只有这2个来源 来源便是这个bean对象.. 接着分析这个对象, 看它构造函数 看看调用处 看来是直接被new出来的… 就得继续看它是在哪儿被赋值的. 但在这个方法种似乎看不出来. 我想通过frida检测这个类的赋值方法. 检测构造函数

1
2
3
4
5
let QuestionInfoBean = Java.use("com.psychiatrygarden.bean.QuestionInfoBean");
QuestionInfoBean["$init"].overload().implementation = function () {
    console.log(`QuestionInfoBean.$init is called`);
    this["$init"]();
};

额, 又忘了frida的孵化参数了.. 问ai吧

frida -U -f com.example.myapp -l hook_script.js

那医考帮的就是

1
frida -U -f com.yikaobang.yixue -l hook_script.js

damn!!! 直接孵化是可以的 那就是加载frida脚本的姿势有问题, 重写试一下

frida -U -f com.yikaobang.yixue -l C:\Users\daily\MyFile\asset\0reverse\ykb\ykb.js

这咋就行了???无语了, 少了个-l..看报错, 没有类??估计是脚本的问题 改一下 能行了, 但是没有打印日志是什么鬼?? 这个方法没有被调用!! 那就换一个方法hook. 我就不信这个方法不会被执行 hook试试, 右键复制frida

1
2
3
4
5
let AnswerNQuestionFragment = Java.use("com.psychiatrygarden.activity.answer.AnswerNQuestionFragment");
AnswerNQuestionFragment["initQuestionType"].implementation = function () {
    console.log(`AnswerNQuestionFragment.initQuestionType is called`);
    this["initQuestionType"]();
};

我的默认终端还是换用wsl吧. 用起来顺手, frida孵化命令就换成

frida -U -f com.yikaobang.yixue -l ykb.js

见鬼了, 还是没有打印日志?? 直接hook活动的onCreate方法! Activity记录显示:

1
com.psychiatrygarden.activity.online.AnswerQuestionActivity

跳到类名!!! onCreate方法没有?? 去父类!! hook!!

1
2
3
4
5
let BaseActivity = Java.use("com.psychiatrygarden.activity.BaseActivity");
BaseActivity["onCreate"].implementation = function (savedInstanceState) {
    console.log(`BaseActivity.onCreate is called: savedInstanceState=${savedInstanceState}`);
    this["onCreate"](savedInstanceState);
};

这回有了 找找父类, 子类里有没有题目模型字段, 发现端倪 看看它里面是什么, 内容还挺详细 我想直到它到底加载了什么东西 大概率这里应该是目录模型, 根据目录模型请求对应题目内容, 我就应该看看onSuccess到底做了什么

1
2
3
4
5
let AnonymousClass3 = Java.use("com.psychiatrygarden.activity.online.AnswerQuestionActivity$3");
AnonymousClass3["onSuccess"].overload('java.lang.Object').implementation = function () {
    console.log(`AnonymousClass3.onSuccess is called`);
    this["onSuccess"]();
};

不打印日志??? 难道是匿名内部类不支持吗, 弄这个方法

1
2
3
4
5
let AnswerQuestionActivity = Java.use("com.psychiatrygarden.activity.online.AnswerQuestionActivity");
AnswerQuestionActivity["cutQuestion"].implementation = function () {
    console.log(`AnswerQuestionActivity.cutQuestion is called`);
    this["cutQuestion"]();
};

还是没用???奇怪了…那我要尝试从点击事件入手, 看看菜单被点击后执行了什么代码. 现重新整理思绪, 这是com.psychiatrygarden.activity.HomePageNewActivity页面 这是com.psychiatrygarden.activity.online.AnswerSheetActivity页面 这是com.psychiatrygarden.activity.online.AnswerQuestionActivity答题页面 那么好, 点击后, 一定会触发网络请求, 一定会有解析返回结果的代码, 我就不行找不到, 我就从

1
com.psychiatrygarden.activity.HomePageNewActivity

开始找, 它里面,一定有一个,负责处理菜单点击后的事件, 找!!! 大致阅读了一下, 这里面进行了版本检查, 广告加载, 权限检查, 多端登陆的逻辑实现.. 重点包括了初始题单的获取!!

1
2
3
4
5
let AnonymousClass12 = Java.use("com.psychiatrygarden.activity.HomePageNewActivity$12");
AnonymousClass12["onSuccess"].overload('java.lang.Object').implementation = function () {
    console.log(`AnonymousClass12.onSuccess is called`);
    this["onSuccess"]();
};

看看你是何方神圣 见鬼了, 还是没有日志!!! 试试往外一层的方法

1
2
3
4
5
let HomePageNewActivity = Java.use("com.psychiatrygarden.activity.HomePageNewActivity");
HomePageNewActivity["getShengYunSets"].implementation = function () {
    console.log(`HomePageNewActivity.getShengYunSets is called`);
    this["getShengYunSets"]();
};

见鬼了??还是没有日志, 估计是缓存的问题, 初次加载后就不再发起联网请求了?跟进大方向: 找点击事件!!!

1
idHex('0x7f0a1188')

分析这些循环布局控件的点击事件!! 额, 似乎找不到, 硬读HomePageNewActivity!!又发现这个YJYHttpUtils.get哪儿都能遇到它 猜测一下, 题目也是这样来的, 那我frida走起!!

1
2
3
4
5
let YJYHttpUtils = Java.use("com.psychiatrygarden.http.YJYHttpUtils");
YJYHttpUtils["get"].overload('android.content.Context', 'java.lang.String', 'net.tsz.afinal.http.AjaxParams', 'net.tsz.afinal.http.AjaxCallBack').implementation = function (context, url, params, icallBack) {
    console.log(`YJYHttpUtils.get is called: context=${context}, url=${url}, params=${params}, icallBack=${icallBack}`);
    this["get"](context, url, params, icallBack);
};

给我软件整崩了..估计是脚本console的问题, 随便打印一条消息看看会不会调用就行

1
2
3
4
5
6
7
Java.perform(function(){
    let YJYHttpUtils = Java.use("com.psychiatrygarden.http.YJYHttpUtils");
    YJYHttpUtils["get"].overload('android.content.Context', 'java.lang.String', 'net.tsz.afinal.http.AjaxParams', 'net.tsz.afinal.http.AjaxCallBack').implementation = function (context, url, params, icallBack) {
        console.log(`YJYHttpUtils.get is called`);
        this["get"](context, url, params, icallBack);
    };
});

确实调用了很多次, 看看请求的url

1
2
3
4
5
6
7
Java.perform(function(){
let YJYHttpUtils = Java.use("com.psychiatrygarden.http.YJYHttpUtils");
YJYHttpUtils["get"].overload('android.content.Context', 'java.lang.String', 'net.tsz.afinal.http.AjaxParams', 'net.tsz.afinal.http.AjaxCallBack').implementation = function (context, url, params, icallBack) {
    console.log(`YJYHttpUtils.get is called: context=url=${url}`);
    this["get"](context, url, params, icallBack);
};
});

总之, 题目就在这几个链接的返回结果中, 很可能是加密的, 而我要做的是, 拿到提单, 主动调用并请求所有题目, 保存到本地, 持久化后, 重写打包一个app, 离线也可以用. 这个需求有点麻烦, 但应该能实现. 那就直接打印返回值看看具体是哪一个链接 先看看这个方法能不能hook

1
2
3
4
5
let AnonymousClass6 = Java.use("com.psychiatrygarden.http.YJYHttpUtils$6");
AnonymousClass6["onSuccess"].implementation = function (t3) {
    console.log(`AnonymousClass6.onSuccess is called: t3=${t3}`);
    this["onSuccess"](t3);
};

看样子似乎有了, 应该就是题目的数据, Unicode!! 额, 似乎不太对, 没有题目.. 这题目到底从那个接口出来的!!!, 静态分析+动态调试都找不到, 那用上reqable!! 嗯, 点击菜单后, 确实出现了一堆数据, 题目应该就在这里面

https://api.yikaobang.com.cn/index.php/allquestion/question/list

妈的,怎么一直在分析用户行为!!!!, 烦, 稍后把你干掉, 现在先搜索关键词question/list 进入分析交叉引用 AnswerSheetFragment有两个onStart方法, 进去一看, 就是展示加载图标的… 屁用没有, 那关于这个url的异步请求到底是哪里发起的呢…哦原来是这里 越来越接近源数据了,frida一下success回调

1
2
3
4
5
let AnonymousClass4 = Java.use("com.psychiatrygarden.http.QuestionDataRequest$4");
AnonymousClass4["onSuccess"].implementation = function (s3) {
    console.log(`AnonymousClass4.onSuccess is called: s3=${s3}`);
    this["onSuccess"](s3);
};

找到咯, 题目就在里面, 我有必要把它解密吗? 似乎没必要, 本地化就行了… 算了, 来都来了, 顺手的事情…我就不喜欢加密的东西!!!! 看看调用处: 看来要对getQuestionList方法大改特该了. 看到第三个参数, 是:isCutQuestion, 还知道数据量大了要分割… 管你呢, 干!!欸嘿, AnswerSheetActivity类中的onSuccess的回调方法不见了? 可是最后一个参数就是this啊 开启展示不一致的代码即可 出来了 而第一个参数str就是题目(加密的), 然后在这里解密 关键代码是找着了, 现在需要搞定的是: 如何保存返回结果? 要不植入代码, 自动主动请求并保存结果到本地的sqlite, 生成一个代理来接管请求…好, 接下来我需要将整个apk里面的classes.dex转换为jar, 作为lib库好作为正向开发…先生成一个新的idea 工程.. 新手建议选groovy, 报错也好排查,初始化完毕后, 在app模块下, 新建一个libs文件夹, 同时在同级目录的build.gradle中添加 表示仅编译阶段用到apk内部代码, 哥们儿我是要手动植入代码滴..接下来进行dex2jar转换. 下载压缩包并解压都某个目录, 添加到环境变量就可以调用了.. 就像这样 把apk安装包以zip格式打开, 解压到libs目录(其他也行, 反正这个文件夹里最终需要有jar文件) AI教我批处理命令行:

1
for /l %i in (2,1,9) do d2j-dex2jar classes%i.dex -o %i.jar

结果如下 随便在工程中创建一个类, 静态方法就是能用invoke-static直接调用的. 就用这个main作为入口吧 我将会在Application中植入smali字节码, 手动调用这个方法, 然后, 我需要在这个方法中实现文件读写权限检查, 权限申请, 有权限后, 再读取/创建本地sqlite文件, 提供对应的方法, 来实现虚拟的网络请求.. 额外要求, 能弹窗提示或Toast提示. 所以要用到反射来读取context, 嗯, 这些都能实现!! 先这样, 让代码跑起来, 搭起简单的调用桥 删除没用的代码, 就能打包安装了(说明能正常编译) 可我已经忘了invoke-static的字节码要怎么写了, 再改一下, 创建一个临时类来模拟调用, 到时候字节码就能直接复制粘贴使用了.. 构建后, 可以直接在这里查看调试版本的apk.. 用jadx打开就能看到smali代码, 就是这行, v0其实我们是不需要的, 再改一下, 把参数去掉, 不然后续很麻烦 再打包看一下

invoke-static {}, Lcn/medmi/ykb_localize/Tool;->main()V

这就是我要(复制并植入到Application类)的代码, 所以, 现在要将apk解包为smali工程, 可以用apktool.jar(我直接在wsl中apt install apktool, 这样方便多了), 为了直接出成品, 我就在去签版本的医考帮上进行修改了, 先反编译

apktool d ykb_kill.apk

开始修改, 先找到Application类, 它在Manifest.xml中注册着呢, 软件运行的第一行代码, 就在这里

1
com.psychiatrygarden.ProjectApp

定位父类运行的第一行代码(静态代码块是最最最早执行的代码) 在这里面植入调用代码, 不要忘了创建对应的文件夹和文件, 我就在smali_classes10中创建吧. 在jadx中把对应内容粘贴过来 回编译

apktool b ykb_kill ykb_kill_localize.apk

好慢啊, 妈蛋, 居然还没有MT管理器快…居然报错了, 看来只能用MT2了 打包安装到手机上, 用MT2提取apk, 找到cn.medmi.ykb_localize包所在的classes.dex, 添加到医考帮中 我这里就把classes3.dex提取出来, 改名classes11.dex, 追加到医考帮安装包里面了.. 接着手动改classes10.dex里面的代码, 和之前一样, 手动植入smali主动调用classes11.dex里面的代码 改完反编译看一下 嗯, 就是这样!! 安装运行看看日志 有效, 接下来就不担心植入代码跑不了的问题咯.后续更新代码, 也只需要打包安装到手机上, 用mt2提取classes3.dex覆盖掉原来的classes11.dex就行了.. 额, 代码还是有点问题, 需要在main方法中传入Application上下文才能正确调用Toast, 改! 此时smali可以快捷用mt2生成, 如下, 先找到你要调用的方法main

然后点击上面的罗盘, 长按该方法名 选择复制invoke代码 , 这回删除原来调用的位置, 因为那里是去签代码, 没有重写onCreate, 回Manifest.xml中找到注册的application实例, 在它的onCreate方法里, invoke-super行后,植入smali,

invoke-static {p0}, Lcn/medmi/ykb_localize/Tool;->main(Landroid/app/Application;)V

运行看看效果
这回尝试打一个Toast 嗯就是这个预期效果.. 顺手预解决UI线程闪退的问题, 用一个handler来post要执行的Toast代码即可 符合预期效果, 添加检查权限的代码 结果符合预期 才发现, 清单中并没有申请该权限, 改一改, 加到医考帮安装包中 结果符合预期, 现在开始创建文件, 符合预期 接下来终于到了我最不熟悉的地方, 接入sqlite, 必须依靠ai了. 我先在医考帮QuestionDataRequest类中接管数据, 把数据岛过来, 看看都请求了哪些东西. 在方法里面的这里接管最好, 因为入参和回调都在这里, 原本是这样的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public <T> void questionList(AjaxParams params, String category, boolean isCutQuestion, final QuestionDataCallBack<T> callBack) {
        final String str = isCutQuestion ? NetworkRequestsURL.sheetCutList : "unit".equals(category) ? NetworkRequestsURL.questionSetsList : NetworkRequestsURL.questionListApi;
        YJYHttpUtils.post(this.mContext, str, params, new AjaxCallBack<T>() { // from class: com.psychiatrygarden.http.QuestionDataRequest.4
            @Override // net.tsz.afinal.http.AjaxCallBack
            public void onFailure(Throwable t3, int errorNo, String strMsg) {
                super.onFailure(t3, errorNo, strMsg);
                callBack.onFailure(t3, errorNo, strMsg, str);
            }

            @Override // net.tsz.afinal.http.AjaxCallBack
            public void onStart() {
                super.onStart();
                callBack.onStart(str);
            }

            @Override // net.tsz.afinal.http.AjaxCallBack
            public void onSuccess(T s3) {
                super.onSuccess(s3);
                callBack.onSuccess(s3, str);
            }
        });
    }

我可以修改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public <T> void questionList(AjaxParams params, String category, boolean isCutQuestion, final QuestionDataCallBack<T> callBack) {
        final String str = isCutQuestion ? NetworkRequestsURL.sheetCutList : "unit".equals(category) ? NetworkRequestsURL.questionSetsList : NetworkRequestsURL.questionListApi;
        Tool.post(this.mContext, str, params, new AjaxCallBack<T>() { // from class: com.psychiatrygarden.http.QuestionDataRequest.4
            @Override // net.tsz.afinal.http.AjaxCallBack
            public void onFailure(Throwable t3, int errorNo, String strMsg) {
                super.onFailure(t3, errorNo, strMsg);
                callBack.onFailure(t3, errorNo, strMsg, str);
            }

            @Override // net.tsz.afinal.http.AjaxCallBack
            public void onStart() {
                super.onStart();
                callBack.onStart(str);
            }

            @Override // net.tsz.afinal.http.AjaxCallBack
            public void onSuccess(T s3) {
                super.onSuccess(s3);
                callBack.onSuccess(s3, str);
            }
        });
    }

经可能的保持参数一样, 以方便移植代码, 构造静态方法来接收收据

1
2
3
4
5
//构造用于替代YJYHttpUtils.post(this.mContext, var2, var1, new AjaxCallBack<T>(this, var4, var2)的方法
    public static <T> void post(Context context, String url, AjaxParams params, final AjaxCallBack<T> icallBack) {
    //在这里面读取sqlite数据, 然后直接调用icallBack.onSuccess, 所以sqlite表的设计也是一件麻烦事,管他呢, 先把数据导进来
        Log.d("Tool", "post() called with: context = [" + context + "], url = [" + url + "], params = [" + params + "], icallBack = [" + icallBack + "]");
    }

再在目标类QuestionDataRequest的questionlist方法中, 插入用于监听的smali(同样用MT快捷生成的)

invoke-static {p3, p2, p1, v0}, Lcn/medmi/ykb_localize/Tool;->post(Landroid/content/Context;Ljava/lang/String;Lnet/tsz/afinal/http/AjaxParams;Lnet/tsz/afinal/http/AjaxCallBack;)V

反编译看一下 没毛病, 安装运行看结果 符合预期. 多次点击目录, 查看返回结果, 仔细观察, 发现不同目录对应的primary_id,每次点击都会重新请求.. 我想直接在YJYHttpUtils处理数据了…干 反编译看看 没毛病, 安排监听方法 植入看效果 一大堆请求, 要不我把params和返回值弄成键值对得了…如果细分表项, 很麻烦啊. 干起来..我可以把请求的url设计成表, 表里面每一个params对应一个响应体.. 可以..我现在植入的代码似乎不能获取到响应体, 应该为收集响应体额外设计配套方法. 用一个布尔开关来控制是否启用数据收集.. 先解决数据收集问题, 在temp中进行模拟匿名内部类调用外包方法参数, 观察参数在smali中是如何传递的 很明显, 代码主要是写在匿名内部类里面的,匿名内部类的构造函数中,往p0的各个字段赋值p1,,pn. 又在内部类的回调方法中, 从p0读取各个字段的值到v0..vn..,而且该字段都是有规律地注册在smali里面的. 要改的地方好多啊啊啊, 这是有史以来我接触到的逆向工程量最大的一个apk…干..难受, 我不想逆向思考如何把smali插进去, 我想直接拷贝YJYHTTpUtils, 反编译出源码后, 直接改源码了…先利用MT工具将YJYHTTpUtils变成我自己包名下的类 源码获取成功, 直接改源码咯 直接改源码真的太爽了, 很快就安装好了数据导流代码, 明天实现sqlite的存储, 现在已经很困了 697 建表

1
CREATE TABLE IF NOT EXISTS upr (url TEXT NOT NULL,params TEXT NOT NULL,response TEXT NOT NULL,PRIMARY KEY (params));

已经能看到sql存入数据了 但毛病是: params里面有些参数是变的, 去掉会变的参数才行, 比如userid, isvip, secret, token之类的. 干起来, 哦哦哦, 原来我把收集代码都放在源代码后面执行了, 这个就可以直接对参数进行操作了 用linkedhashmap处理或提前预防顺序错乱的问题, 符合预期, 不用考虑多次/多人登录导致数据找不到或重复的问题…接下来实现自动拉取数据的功能, 应该是

考研参数

1
params = [subject_id=20201&is_svip=0&module_type=1&identity_id=202012&is_vip=0&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=2&search_where=[]&category=year&unit_id=&app_id=10]

执医参数

1
params = [subject_id=20301&is_svip=0&module_type=1&identity_id=203010&is_vip=1&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=0&search_where=[]&category=chapter&unit_id=&app_id=12]

过度删减信息导致sql忽略插入了, 很明显, subject_id不一样, 为什么放不进去呢, 哦, 是我的眼睛有问题, 这回能看到放进来了.. 接下来解决的是自动化的问题了, 启动一个子线程, 检查数据库是否齐全(手动布尔)的标志, 如果不齐全, 就从sql中抽取chapter,遍历并构造请求, 在sql中查找对应的param, 如果没找到,就主动调用yjyhttputils.最后打一个Toast提示最终结果. 目前确实能从sql查到目录数据了 还是加一个可启动的界面吧, 不然不好调试看参数 还得删除原有的CompatActivity,用原生的Activity才能正常启动 就在自己的app里面模拟解析sql吧. 反反复复移植代码都烦了… 终于是可以动态调试了… 继续抄代码, 看来am_pm与试题分割有关系, 不应该抹除, 加回去 感觉这subject_id就是identity_id的前5位, 是学科分类(考研, 执医这种大类) 手动凑参数真的好麻烦啊, 换条路, 在Tool中保存对应url的请求params, 到时候直接替换关键字段就行了. 先记录一下书本对应的json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{  
  "chapter_id" : "40001",  
  "parent_id" : "40",  
  "title" : "第一章 中医基本特点",  
  "sort" : "1",  
  "activity_id" : "0",  
  "bind_activity_id" : "0",  
  "am_pm" : "0",  
  "deleted_at" : "",  
  "created_at" : "2024-01-08 11:20:49",  
  "updated_at" : "2024-01-22 10:38:31",  
  "app_id" : "12",  
  "school_year" : "0",  
  "identity_id" : "203010",  
  "status" : "1",  
  "b_status" : "1",  
  "have" : "0",  
  "primary_id" : "40001",  
  "count" : "10",  
  "right_count" : "0",  
  "error_count" : "0"  
}

记录一下实际的请求参数是: 中医学基础-第一章

1
apost() called with: context = [com.psychiatrygarden.ProjectApp@9ead118], url = [https://api.yikaobang.com.cn/index.php/allquestion/question/list], params = [subject_id=20301&is_svip=0&module_type=1&identity_id=203010&is_vip=1&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=0&primary_id=40001&category=chapter&app_id=12]

中医学基础-第二章

1
apost() called with: context = [com.psychiatrygarden.ProjectApp@9ead118], url = [https://api.yikaobang.com.cn/index.php/allquestion/question/list], params = [subject_id=20301&is_svip=0&module_type=1&identity_id=203010&is_vip=1&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=0&primary_id=40002&category=chapter&app_id=12]

生理学-第一章

1
apost() called with: context = [com.psychiatrygarden.ProjectApp@9ead118], url = [https://api.yikaobang.com.cn/index.php/allquestion/question/list], params = [subject_id=20301&is_svip=0&module_type=1&identity_id=203010&is_vip=1&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=0&primary_id=11001&category=chapter&app_id=12]

生理学-第二章

1
apost() called with: context = [com.psychiatrygarden.ProjectApp@9ead118], url = [https://api.yikaobang.com.cn/index.php/allquestion/question/list], params = [subject_id=20301&is_svip=0&module_type=1&identity_id=203010&is_vip=1&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=0&primary_id=11002&category=chapter&app_id=12]

总结规律, 需要覆盖的参数是primary_id,就没啦???我还认为有多复杂 奇怪了, 为什么不闪退, 但是代码不执行呢?原来是请求参数模板hashmap没有放入对应模板…终于修好bug了, 大概就是条件判断那一块儿出了点问题, 参数基本构造完毕.. 奇怪了, 看不到数据往sqlite里面灌… 一看就是参数构造有问题… 这个大小才对嘛, 医考帮执医所有题目都在这里咯, 再试试看考研的, 似乎也拿下来了… 解密看看是不是真的,,

1
{"list":[],"search":[{"field":"cut_question","type_str":"\u65a9\u9898","data":[{"title":"\u5168\u90e8","id":"-1"},{"title":"\u5df2\u65a9","id":"1"},{"title":"\u672a\u65a9","id":"2"}]},{"field":"origin_type","type_str":"\u9898\u578b","data":[{"id":"-1","title":"\u5168\u90e8"}]},{"field":"filter_type","type_str":"\u5206\u7c7b","data":[{"title":"\u5168\u90e8","id":"-1"},{"title":"\u505a\u9519\u7684","id":"1"},{"title":"\u6536\u85cf\u7684","id":"2"},{"title":"\u672a\u505a\u7684","id":"3"}]},{"field":"pattern","type_str":"\u6a21\u5f0f","data":[{"title":"\u7ec3\u4e60\u6a21\u5f0f","id":"111"},{"title":"\u5feb\u5237\u6a21\u5f0f","id":"114"},{"title":"\u6d4b\u8bd5\u6a21\u5f0f","id":"112"},{"title":"\u80cc\u9898\u6a21\u5f0f","id":"113"}]}]}

原来是用户数据… 拿不到真题..找学弟去…说什么我也得拿到考研的题目, 明天吧, 今天太困了… 这是考研的请求参数

1
params = [subject_id=20201&is_svip=0&module_type=1&identity_id=202012&is_vip=0&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=2&primary_id=2025&category=year&app_id=10]

这是执医的请求参数

1
params = [subject_id=20301&is_svip=0&module_type=1&identity_id=203010&is_vip=1&secret=a7b9bd1a1782859c98f1ac4421281fa1&type=all&token=be64af6ac88185fd84d6cec566daa36b&user_id=1826749&am_pm=0&primary_id=40001&category=chapter&app_id=12]

发现25年的数据确实是保存下来了的. 看看其他年份的. 没有. 模拟的primary_id不符合预期, 应该是存入的参数模板有问题. 先不管, 想用reqable偷懒看看改参数能成不. 哈哈加了签名验证…那到底为什么模拟参数发不出去涅 这是考研题目的章节信息,而我这里把它视为执医的结构来解析了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
      "chapter_id": "2026",
      "parent_id": "0",
      "title": "2026",
      "sort": "1",
      "activity_id": "1592",
      "bind_activity_id": "0",
      "am_pm": "0",
      "deleted_at": "",
      "created_at": "2026-03-06 17:50:11",
      "updated_at": "2026-03-09 15:41:58",
      "app_id": "10",
      "school_year": "0",
      "identity_id": "202012",
      "status": "1",
      "b_status": "1",
      "primary_id": "2026",
      "have": "0",
      "children": [],
      "pass": "0",
      "count": "165",
      "right_count": "0",
      "error_count": "0"
    }

执医的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{  
  "chapter_id" : "40001",  
  "parent_id" : "40",  
  "title" : "第一章 中医基本特点",  
  "sort" : "1",  
  "activity_id" : "0",  
  "bind_activity_id" : "0",  
  "am_pm" : "0",  
  "deleted_at" : "",  
  "created_at" : "2024-01-08 11:20:49",  
  "updated_at" : "2024-01-22 10:38:31",  
  "app_id" : "12",  
  "school_year" : "0",  
  "identity_id" : "203010",  
  "status" : "1",  
  "b_status" : "1",  
  "have" : "0",  
  "primary_id" : "40001",  
  "count" : "10",  
  "right_count" : "0",  
  "error_count" : "0"  
}

终于找到问题所在了, 在解析json对象时, 应该先判断啊, 要注意, 这里表示没有2级目录了, 不代表没有2级目录的属性了, 所以这里的判断是错的. 应该判断长度是否等于0..不然后面的逻辑走不对.. 终于是修好能看到正常的返回结果了, 不幸的是, 医考帮做了服务器验证, 考研题目并不能返回来.. 我警告你个头!,等我开/借/找/偷/抢个会员…先搞本科题库.. 啊啊, 数据库太大, 闪退了..还得分割数据库咯, 明天再弄吧.. #2026年5月3日 终于记起来还有这样一件事情没有完成, 接着搞. 先开启git仓库, 免得代码不小心删掉没存档. 计划只用sql保存目录信息. 题目直接存文件里. 利用接口设计, 方便换. 阻力好大啊, 不想搞了. 就算我弄成了本地存储请求结果. 但似乎题目答案也是要重新处理. 硬生生要重写app的后端啊, 太糙淡了……我还是花钱开会员吧. 今后逆向ykb就当怡情别趣了…

本文由作者按照 CC BY 4.0 进行授权