记悠学派APP逆向及利用
- 0×00 前言
- 0×01 逆向部分
- 抓包部分
- 登录
- APK逆向
- sign鉴权算法
- 0×02 功能实现
- sign生成
- 软件实现
- 0×03 结论
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=utf8anonymousId=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×tamp=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); //参数loginIdhashMap.put(PreferenceService.KEY_USER_PWD, this.pwd); //参数passwordhashMap.put("appType", String.valueOf(AppTypeEnum.Android.getValue())); //参数appTypehashMap.put("deviceBindingId", BindkeyUtil.getBindKey(this)); //参数deviceIdWisdomHttpUtil.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×tamp=[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,未经许可禁止转载