记悠学派APP逆向及利用

本文详细介绍了记悠学派APP的逆向工程过程,包括抓包分析、登录接口逆向和鉴权算法研究,最终实现了自动登录等功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0×00 前言


学校为促进学生们参加活动的积极性,通过学分制度来让学生们提高参加率,并通过该APP进行签到、记录学分,作为一个爱搞事情的好孩子,当然是对他进行一番嘿嘿嘿。
带着这样的想法,我们来进行分析

注:本文所提及到的内容,仅仅为技术讨论,切勿用到非法行为。

0×01 逆向部分

抓包部分

对于APP来说,一般都会通过HTTP协议来进行传输(当然也会有TCP、UDP协议传输的)。
那么第一步就是对APP进行抓包了,我用的抓包工具是Packet Capture,选择好要抓包的程序。从登录、获取我的活动、签到历史查询、签到、评价等几个操作进行抓包。
下面来分析上述所说的登录API的调用。

注:所有API都是通过POST方式进行请求。

登录

请求包

POST /wisdomprovider/router? HTTP/1.1
Content-Length: 267
Host: manage.cisau.com.cn:8080
Connection: Keep-Alive
Accept-Encoding: gzip
content-type: application/x-www-form-urlencoded;charset=utf8

anonymousId=00000000-0000-0000-0000-000000000000&loginId=20*****0&method=wisdom.system.login&sign=D96DBA******0C50B4A&format=json&tenantCode=sxxyzhxy&deviceId=1*****c&password=2****2&v=1.0&appType=2&appKey=00000001&timestamp=1555600646

//loginId为登录账号 password为登录密码 deviceId为设备ID sign为APP产生操作时的鉴权密钥

返回包

HTTP/1.1 200 
Access-Control-Allow-Origin: *
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Content-Length: 799
Date: Thu, 18 Apr 2019 15:17:26 GMT

{"code":"0","data":{"userId":"0*******-****-4***-***4-1*************3","userName":"*********2","studentNo":"2*****0","nickName":"**宇","realName":"**宇","mobile":"","email":"2********1@qq.com","qq":"","wechat":"","address":"北京市朝阳区***","batchCode":"00*","batchName":"201*年级","orgCode":"00*","orgName":"信息工程学院","specialtyName":"计算机科学与技术","remark":"","imageUrl":"http://smartclass.chinaedu.net:8000/sxxyzhxy/image/user/20*****2/f********1-****-****-***c-2b*******6.png","roleTypes":1,"checkinState":2,"userInfoConfirm":1,"checkinNoticeConfirm":1,"appDownloadUrl":"http://smartclass.chinaedu.net:8000/smartcampus/appdown/index.html","isBindCellphone":1},"detailCode":"0"}

注:此版本用的是老版本,该版本没有验证设备ID。

APK逆向

sign鉴权算法

由于开学的时候学过一些Java,我觉得用jadx将APK拖进去先把代码看个究竟。
在这里插入图片描述
看了一大圈也没找到啥有用的东西,还看的是一头汗水,我决定尝试最直接的办法就是直接搜索关键字sign。
在这里插入图片描述
经过长达20分钟的加载,心里一直想着等了这么久要是没搜到心态不得爆炸。但是事实并非我所料,竟然搜到了一个SignUtils.java
在这里插入图片描述
下面上代码,我们来具体分析下到底是怎么计算的这个Sign

public class SignUtils {
	public static String sign(Map<String, String> map, String str) {
        return sign(map, null, str); //递归
    }

    public static String sign(Map<String, String> map, List<String> list, String str) {  //重写方法
        try {
            StringBuilder stringBuilder = new StringBuilder();
            List<String> arrayList = new ArrayList(map.size());
            arrayList.addAll(map.keySet());
            if (list != null && list.size() > 0) {
                for (String remove : list) {
                    arrayList.remove(remove);
                }
            }
            Collections.sort(arrayList);
            stringBuilder.append(str);
            for (String str2 : arrayList) {
                stringBuilder.append(str2);
                stringBuilder.append((String) map.get(str2));
            }
            stringBuilder.append(str); //通过map表排序的方法将未加密的信息存入stringBuilder中
            return byte2hex(getSHA1Digest(stringBuilder.toString())); //返回通过SHA-1加密后的全大写sign
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
 ...
 ...
 ...
 }

通过观察登录时的代码,我们可以清楚的看到会在map表中添加所需的参数

 private void login() {
        if (this.mLoginTenantEntity == null) {
            Toast.makeText(this, getString(R.string.login_check_tenant_tip), 0).show();
        } else if (StringUtil.isEmpty(this.mobile) || StringUtil.isEmpty(this.pwd)) {
            Toast.makeText(this, getString(R.string.username_pwd_not_null), 0).show();
        } else {
            this.preference.save("username", this.mobile);
            CommonTenant commonTenant = (CommonTenant) TenantManager.getInstance().getCurrentTenant();
            commonTenant.setTenantRealCode(this.mLoginTenantEntity.getCode());
            CommonUrl commonUrl = (CommonUrl) commonTenant.getCurrentHttpRoot();
            commonUrl.setAppRootHttpUrl(this.mLoginTenantEntity.getAppUrl());
            commonUrl.setUploadHttpUrl(this.mLoginTenantEntity.getAppUploadUrl());
            commonUrl.setUmengShareHttpUrl(this.mLoginTenantEntity.getAppUmengShareUrl());
            if (!StringUtil.isEmpty(WisdomHttpUtil.getAppRootUrl())) {
                LoadingProgressDialog.showLoadingProgressDialog(this);
                Map hashMap = new HashMap();
                hashMap.put("loginId", this.mobile); //参数loginId
                hashMap.put(PreferenceService.KEY_USER_PWD, this.pwd); //参数password
                hashMap.put("appType", String.valueOf(AppTypeEnum.Android.getValue())); //参数appType
                hashMap.put("deviceBindingId", BindkeyUtil.getBindKey(this)); //参数deviceId
                WisdomHttpUtil.sendAsyncPostRequest(Urls.LOGIN_URI, InterfaceVersionConfig.VERSION_2, hashMap, this.handler, (int) Vars.LOGIN_REQUEST, new TypeToken<User>() {
                });
            }
        }
    }

在继续跟踪POST请求这个方法的时候,还发现了method、timestamp等等。
在这里插入图片描述

public static String SECRET_KEY = "52c203760cf28798a44f6ac4"; //这个是在生成sign时必要的KEY

生成sign时,要注意必要的KEY以及参数的完整性。

0×02 功能实现

sign生成

在这里插入图片描述

package lanqiao;

import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SignUtils {
	public static void main(String[] args) {
		SignUtils s = new SignUtils();
		Map<String, String> map = new HashMap<String, String>();
		Map<String, String> hashMap = new HashMap<String, String>();
		hashMap.put("loginId", "[loginid]");
		hashMap.put("password", "[password]");
	    hashMap.put("appType", "2");
		hashMap.put("appKey", "00000001");
	    hashMap.put("method", "wisdom.system.login");
	    hashMap.put("format", "json");
	    hashMap.put("v", "1.0");
	    hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));//String.valueOf(System.currentTimeMillis() / 1000)


	    hashMap.put("sign", SignUtils.sign(hashMap, "52c203760cf28798a44f6ac4"));

	}
    public static String sign(Map<String, String> map, String str) {
        return sign(map, null, str);
    }

    public static String sign(Map<String, String> map, List<String> list, String str) {
        try {
            StringBuilder stringBuilder = new StringBuilder();
            List<String> arrayList = new ArrayList(map.size());
            arrayList.addAll(map.keySet());
            if (list != null && list.size() > 0) {
                for (String remove : list) {
                    arrayList.remove(remove);
                }
            }
            Collections.sort(arrayList);
            stringBuilder.append(str);
            for (String str2 : arrayList) {
                stringBuilder.append(str2);
                stringBuilder.append((String) map.get(str2));
            }
            stringBuilder.append(str);
            System.out.println(stringBuilder.toString());
            System.out.println(byte2hex(getSHA1Digest(stringBuilder.toString())));
            return byte2hex(getSHA1Digest(stringBuilder.toString()));
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public static String utf8Encoding(String str, String str2) {
        try {
            return new String(str.getBytes(str2), "UTF8");
        } catch (Throwable e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static byte[] getSHA1Digest(String str) throws IOException {
        try {
            return MessageDigest.getInstance("SHA-1").digest(str.getBytes("UTF8"));
        } catch (Throwable e) {
            throw new IOException(e);
        }
    }

    private static byte[] getMD5Digest(String str) throws IOException {
        try {
            return MessageDigest.getInstance("MD5").digest(str.getBytes("UTF8"));
        } catch (Throwable e) {
            throw new IOException(e);
        }
    }

    private static String byte2hex(byte[] bArr) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bArr) {
            String toHexString = Integer.toHexString(b & 255);
            if (toHexString.length() == 1) {
                stringBuilder.append("0");
            }
            stringBuilder.append(toHexString.toUpperCase());
        }
        return stringBuilder.toString();
    }
}

这里我直接复制原包中的代码加以修改后直接利用。
为了方便在易语言中方便使用,我直接生成里stringBuilder中的字符,然后通过直接替换字符来快捷使用。

软件实现

.版本 2
.支持库 iext

.子程序 登陆账号
.局部变量 未加密数据, 文本型
.局部变量 提交数据, 文本型
.局部变量 sign, 文本型
.局部变量 i, 整数型
.局部变量 返回文本, 文本型
.局部变量 username, 文本型
.局部变量 password, 文本型
.局部变量 deviceid, 文本型
.局部变量 timestamp, 文本型

.计次循环首 (账号列表框.取表项数 (), i)
    username = 账号列表框.取标题 (i - 1, 1)
    password = 账号列表框.取标题 (i - 1, 2)
    deviceid = 账号列表框.取标题 (i - 1, 3)
    timestamp = 时间_取现行时间戳 (真)
    提交数据 = “anonymousId=00000000-0000-0000-0000-000000000000&loginId=[loginid]&method=wisdom.system.login&sign=[sign]&format=json&tenantCode=sxxyzhxy&deviceId=[deviceid]&password=[password]&v=1.0&appType=2&appKey=00000001&timestamp=[timestamp]”
    未加密数据 = “52c203760cf28798a44f6ac4anonymousId00000000-0000-0000-0000-000000000000appKey00000001appType2deviceId[deviceid]formatjsonloginId[loginid]methodwisdom.system.loginpassword[password]tenantCodesxxyzhxytimestamp[timestamp]v1.052c203760cf28798a44f6ac4”
    提交数据 = 子文本替换 (提交数据, “[loginid]”, username, , , 真)
    提交数据 = 子文本替换 (提交数据, “[password]”, password, , , 真)
    提交数据 = 子文本替换 (提交数据, “[deviceid]”, deviceid, , , 真)
    提交数据 = 子文本替换 (提交数据, “[timestamp]”, timestamp, , , 真)
    未加密数据 = 子文本替换 (未加密数据, “[loginid]”, username, , , 真)
    未加密数据 = 子文本替换 (未加密数据, “[password]”, password, , , 真)
    未加密数据 = 子文本替换 (未加密数据, “[deviceid]”, deviceid, , , 真)
    未加密数据 = 子文本替换 (未加密数据, “[timestamp]”, timestamp, , , 真)
    sign = 到大写 (校验_取sha1 (到字节集 (未加密数据)))
    提交数据 = 子文本替换 (提交数据, “[sign]”, sign, , , 真)
    ' “application/x-www-form-urlencoded;charset=utf8”
    返回文本 = 编码_utf8到gb2312 (到文本 (网页_访问_对象 (“http://manage.cisau.com.cn:8080/wisdomprovider/router”, 1, 提交数据, , , , , , , , )))
    程序_写日志 (“[” + 时间_格式化 (取现行时间 (), “yy-MM-dd ”, “hh:mm:ss”, 真) + “]  登陆返回信息:” + 返回文本, 取运行目录 () + “\log\return.txt”)
    json.解析 (返回文本)
    .如果 (json.取通用属性 (“data.userId”) ≠ “”)
        ' 登陆成功
        账号列表框.置标题 (i - 1, 4, json.取通用属性 (“data.userId”))
        编辑框1.加入文本 (“[” + 时间_格式化 (取现行时间 (), “yy-MM-dd ”, “hh:mm:ss”, 真) + “] ” + username + “登陆成功...” + #换行符)
    .否则
        ' 登陆失败
        json.取通用属性 (“msg”)
        编辑框1.加入文本 (“[” + 时间_格式化 (取现行时间 (), “yy-MM-dd ”, “hh:mm:ss”, 真) + “] ” + username + “登陆失败:” + json.取通用属性 (“msg”) + #换行符)
    .如果结束

.计次循环尾 ()

为了防止白嫖党,我把签到、评价的代码删除咯,欢迎大家自己来编写代码。
由于技术太渣就不给大家提供其他语言的代码了,这里就用易语言做示例了。
我的代码就随缘写法,如果有可以改进的地方,联系我。

0×03 结论


通过对APP逆向后,提取接口后,实现自动登录、报名、签到、评价等等功能……
同样可以做一个通知机器人来通知你有什么新活动可以参加。

*如果有写的有问题的地方,欢迎留言告知
*本文原创作者:WeiShaos,未经许可禁止转载

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值