主角:

描述:

  • 湖南省教育局推的一款大学生 App,需要每个学生看完里面的一个课程的视频,共 8 章,每章 10 - 23 个视频(连续播放大约 24 小时),每个视频每隔不定时间就会弹出一个选择题答题界面,题目完成后将继续播放该视频。视频进度条只能拖动至该视频已看的最大位置,上面的视频看完后才能继续向下观看。

思路:

  1. 目标,允许 Android 端直接拖动视频进度条至视频末尾;方案,绕开进度条拖动限制代码。
  2. 目标,视频默认倍速播放,取消显示题目;方案,修改 App 中应用视频播放器的默认设置。
  3. 目标,自动化工具模拟 Android 端操作;方案,抓取接口并调用。

工具:

  • Android Killer v1.3.1.0
  • JEB v2.2.7.201608151620,
  • Android Studio v3.1.2
  • Burp Suite v1.7.37
  • Intellij IDEA v2018.1
  • 有 Root 权限的手机或 ARM 架构的模拟器

其他:

为了方便截手机的图,写了一个下面的 bat 脚本

@echo off
set file_name=/sdcard/sc_%RANDOM%.png
adb shell screencap -p %file_name%
adb pull %file_name% .
adb shell rm %file_name%

过程:

先在 Android Killer 中打开该 APK,打开 Manifest,看到 application 标签里没有 android:debuggable="true" 属性(release 版本默认是没有的),添加上后保存(Android Killer 不会自动保存),然后编译,adb install "C:\Program Files\AndroidKiller\projects\CrackMe\Bin\CrackMe_killer.apk" 将其安装在手机上。

打开应用,使用一下,点击一个视频,尝试拖动,会得到如下信息

主要信息,“不能快进更多”,转 Unicode 后用 Android Killer 搜索,结果如下

很幸运,有且只有一处,VideoView$1.smali,是一个内部类,推断是个拖动的监听器之类的,Android Killer 定位到代码位置,向上看代码,寻找代码分支,首先看到的是

但是调用的参数是 videoisfinish ,用这个去判断最大时长,感觉不大对,以 cond_1 作为线索继续向上, 不难看出来,这是一个或判断,跟到 cond_1 后发现其调用了 invoke-virtual {p1, v0, v1}, Lcn/jzvd/JZMediaInterface;->seekTo(J)V 是进度调整代码无疑了,随便更改或判断的一处即可,这里把 if-le p1, v0, :cond_1 改为 goto :cond_1。

Android Killer 保存,编译,重新安装。

OK,可以拖动了,但是发现课程拖动完后外面的课程总进度并没有变(这可能是犯的第一个大错,已经没有办法再验证了,进度条之所以没变是因为课程数目多,两节课太短所以并没有计入),看来不仅在这里做了校验,打开 JEB,Bytecode/Hierarchay,定位到 VideoView,Decompile。该类扩展自 JZVideoPlayerStandard,双击查看源码,包 cn.jzvd,像是国人做的库,Google 之,果然,GitHub 上的开源项目。速览一遍 VideoView(后来证明在这里犯了第二个大错,主要验证代码以及服务器同步应该都是在这里)没有什么发现,大致认为是对父类方法的重写(有一点可以证明我在这里的推测是错误的,上面对于进制跳转的代码是在这里做的验证)。

类名上 x 查看交叉引用,除了自身就指向 VideoPlayerActivity,现在把主要精力集中在 VideoPlayerActivity,通读代码,其使用 SharedPreference 获取与保存一些数据,网络相关的都是些无关紧要的操作,并没有进度同步的代码,这让我很不解,唯一的一处不知道内部做了什么的就只有 jcVideoPlayerStandard 这个 VideoView 对象(所以为什么不进去看看?)。

第一个思路进行不下去了。

下来看一看 jiaozivideoplayer 的 ReadMe,发现其提供了倍速播放的功能,在于 JZMediaManager.setSpeed(.) 方法,打开 JEB 查看方法列表,发现 JZMediaManager 并没有 setSpeed(.) 方法

推测是版本不一致,打开 GitHub,现版本为 6.x,打开 JZMediaManager,确实有 setSpeed(.) 方法,定位至 5.x 版本,发现 5.x 版本的时候还叫 JCMediaManager,6.2 版本是一年前的,此时也确实没有调速方法。

发现文档里有一句,基于 MediaPlayer。但是 MediaPlayer 6.0+ 起就提供了调速功能,尝试直接修改播放器代码达到倍速效果,使用 Android Killer 搜索 MediaPlayer,与 JZVideoPlayer 有关的如下

很遗憾,JZMediaPlayer 并没有创建MediaPlayer 对象,惊喜的是 JZMediaSystem 里有 MediaPlayer 对象

在 JZMediaSystem 的 prepare(.) 开始处添加代码倍速播放代码

    invoke-virtual {v0}, Landroid/media/MediaPlayer;->getPlaybackParams()Landroid/media/PlaybackParams;

    move-result-object v1

    const/high16 v2, 0x41200000    # 10.0f

    invoke-virtual {v1, v2}, Landroid/media/PlaybackParams;->setSpeed(F)Landroid/media/PlaybackParams;

    move-result-object v1

    invoke-virtual {v0, v1}, Landroid/media/MediaPlayer;->setPlaybackParams(Landroid/media/PlaybackParams;)V

保存,编译,安装,发现并没有用。用 Android Studio 动态调试一下 Smali 代码,给 prepare() 打上断点,发现该方法根本没有调用

方案二失败。

Burp Suite 抓包,发现接口参数都异常简单,决定自己写一个简单的工具直接模拟 Android 端调用服务器接口,这个方法比较简单。

需要格外注意这里的请求头,个人不喜欢用 OkHttp,所以自己的封装请求类如下,主要是模拟请求头的添加(失败重试机制没有放在这里是一个很大的失误)

package com.seliote.crackcjyykt.network;

import com.seliote.crackcjyykt.exception.SetCookieParseException;
import com.seliote.crackcjyykt.util.Constants;
import com.seliote.crackcjyykt.util.HttpUtils;
import com.seliote.crackcjyykt.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream; /**
* Package: com.seliote.crackcjyykt.network
* FileName: CjyyktHttpUtils
* Describe: CJYYKT 专用 Http 请求工具,自动设置请求头与 Cookie
* Author: seliote
* Email: seliote@hotmail.com
* Date: 2018/12/30 14:48
*/
@SuppressWarnings("WeakerAccess")
public class CjyyktHttpUtils { private final Map<String, String> COOKIE_MAP; public CjyyktHttpUtils() {
COOKIE_MAP = new LinkedHashMap<>();
} /**
* 获取已存储的所有 Cookie
* @return 已存储的所有 Cookie
*/
@SuppressWarnings("unused")
public Map<String, String> getCookies() {
return COOKIE_MAP;
} /**
* 以请求头中字符串形式返回所有 Cookie 的字符串表示
* @return 请求头中字符串形式返回所有 Cookie 的字符串表示
*/
public String getCookieString() {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : COOKIE_MAP.entrySet()) {
//noinspection StringConcatenationInsideStringBufferAppend
stringBuilder.append(entry.getKey() + "=" + entry.getValue() + ";");
}
if (stringBuilder.length() > 1) {
return stringBuilder.substring(0, stringBuilder.length() -1);
}
return stringBuilder.toString();
} /**
* 添加 Cookie
*
* @param aKey Cookie 键值
* @param aValue Cookie 值
*/
public void addCookie(@NotNull String aKey, @NotNull String aValue) {
if (aValue.equals("") || aValue.equals("\"\"")) {
deleteCookie(aKey);
return;
} COOKIE_MAP.put(aKey, aValue);
} /**
* 删除 Cookie
*
* @param aKey 要删除的 Cookie 名
* @return 已删除的 Cookie 的值,如果不存在,返回 null
*/
@SuppressWarnings({"UnusedReturnValue"})
@Nullable
public String deleteCookie(@NotNull String aKey) {
return COOKIE_MAP.remove(aKey);
} /**
* 模拟 CJYYKT Android 端 Get 方式请求
*
* @param aUrl 请求的 URL
* @return 服务器的返回值
* @throws IOException 连接或读取异常
*/
public String get(@NotNull String aUrl) throws IOException { Pair<Map<String, List<String>>, byte[]> pair = HttpUtils.get(
aUrl,
10000,
10000,
generateHeader("GET")
); handleHeader(pair.getFirst()); return new String(unGzip(pair.getSecond()), StandardCharsets.UTF_8);
} /**
* 模拟 CJYYKT Android 端 Post 方式请求
*
* @param aUrl 请求的 URL
* @param aPostBody 请求体
* @return 服务器的返回值字符串形式
* @throws IOException 连接或读取异常
*/
public String post(@NotNull String aUrl, @Nullable String aPostBody) throws IOException {
Pair<Map<String, List<String>>, byte[]> pair = HttpUtils.post(
aUrl,
10000,
10000,
generateHeader("POST"),
aPostBody,
StandardCharsets.UTF_8
); handleHeader(pair.getFirst()); return new String(unGzip(pair.getSecond()), StandardCharsets.UTF_8);
} public String post(@NotNull String aUrl, @Nullable Map<String, String> aPostBodyMap) throws IOException {
return post(aUrl, mapToPostBody(aPostBodyMap));
} /**
* Map 生成 key1=value1&key2=value2 类似形式的字符串,转换完成后清空参数 Map
* @param aPostBody 需要格式化的参数 Map
* @return 格式化后的字符串
*/
public String mapToPostBody(Map<String, String> aPostBody) {
StringBuilder result = new StringBuilder(); for (Map.Entry<String, String> entry : aPostBody.entrySet()) {
result.append(entry.getKey());
result.append("=");
result.append(entry.getValue());
result.append("&");
} // 可千万别放下面了
aPostBody.clear(); if (result.length() > 1) {
// substring(..) 返回的是一个 String...
return result.substring(0, result.length() - 1);
} return result.toString();
} /**
* 生成请求头
*
* @param aMethod POST 或 GET
* @return 生成的请求头
*/
public Map<String, String> generateHeader(String aMethod) {
// 这个值和 Cookie 里的 SESSION 相等,但是不知道他为什么要单独写一个请求头?
String headerSession = COOKIE_MAP.getOrDefault(Constants.Api.Header.SESSION, "");
String headerCookie = Constants.Api.Header.COOKIE_JLXCKID
+ "="
+ COOKIE_MAP.getOrDefault(Constants.Api.Header.COOKIE_JLXCKID, "")
+ ";SESSION="
+ COOKIE_MAP.getOrDefault(Constants.Api.Header.COOKIE_SESSION, ""); Map<String, String> map = new LinkedHashMap<>();
map.put(Constants.Api.Header.SESSION, headerSession);
map.put(Constants.Api.Header.COOKIE, headerCookie);
map.put(Constants.Api.Header.IP, Constants.Api.Header.IP_VALUE);
map.put(Constants.Api.Header.VERSION, Constants.Api.Header.VERSION_VALUE);
map.put(Constants.Api.Header.CONNECTION, Constants.Api.Header.CONNECTION_VALUE);
// OkHttp 自动设置 Content-Encoding: GZIP 并解压返回值,现在需要自己去解压返回值
map.put(Constants.Api.Header.ACCEPT_ENCODING, Constants.Api.Header.ACCEPT_ENCODING_VALUE);
map.put(Constants.Api.Header.USER_AGENT, Constants.Api.Header.USER_AGENT_VALUE);
// POST 请求的话需要多一个 Content-Type 请求头
if (aMethod.equalsIgnoreCase("POST")) {
map.put(Constants.Api.Header.CONTENT_TYPE, Constants.Api.Header.CONTENT_TYPE_VALUE);
}
return map;
} /**
* 处理请求头,读取并添加 Cookie
*
* @param aHeader HttpUtils.get(...) 或 HttpUtils.post(...) 返回的 Pair 的请求头部分
*/
@SuppressWarnings("UnnecessaryContinue")
public void handleHeader(Map<String, List<String>> aHeader) {
// 没有直接取出 Set-Cookie List,因为之后可能还要做其他处理
for (Map.Entry<String, List<String>> entry : aHeader.entrySet()) {
// HTTP 响应的第一行响应头信息被存在了 null => ${value} 中
if (entry.getKey() == null) {
continue;
} else if (entry.getKey().equalsIgnoreCase("Set-Cookie")) {
String regex = "^([^=]*)=([^;]*).*$";
Pattern pattern = Pattern.compile(regex);
for (String setCookie : entry.getValue()) {
Matcher matcher = pattern.matcher(setCookie.trim());
if (matcher.matches()) {
addCookie(matcher.group(1).trim(), matcher.group(2).trim());
} else {
throw new SetCookieParseException("Set-Cookie 匹配出错:" + setCookie);
}
}
}
}
} /**
* 解压 GZIP 压缩的数据
*
* @param aBytes 需要解压的 byte[]
* @return 解压后的 byte[]
* @throws IOException 读取出错时抛出
*/
public byte[] unGzip(byte[] aBytes) throws IOException {
// 如果是在抓包,BurpSuite 会自动解压缩 Gzip 流
if (Constants.Config.DEBUG) {
return aBytes;
} ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(aBytes);
GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);
byte[] buffer = new byte[1024];
int length;
while ((length = gzipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
} byte[] result = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.close();
byteArrayInputStream.close();
gzipInputStream.close(); return result;
}
}

当然这也是需要抓包的,启动类还需要加上这个才行

if (Constants.Config.DEBUG) {
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "8888");
System.setProperty("https.proxyPort", "8888");
}

然后就是获取数据循环调用接口,除了发现接口命名混乱外基本没有什么其他问题了,实际测试也是正常的,可以刷课了。

总结:

本来简简单单的 Android 逆向硬是被自己的几个重大失误搞成了这样,以后一定耐心看完代码再下结论。还有就是要结合抓包和逆向,单搞一个信息不完全

记一次 Android 客户端(CJYYKT)的逆向的更多相关文章

  1. 接入新浪、腾讯微博和人人网的Android客户端实例 接入新浪、腾讯微博和人人网的Android客户端实例

    做了个Android项目,需要接入新浪微博,实现时也顺带着研究了下腾讯微博和人人网的Android客户端接入,本文就跟大家分享下三者的Android客户端接入方法. 一.实例概述 说白了,接入微博就是 ...

  2. Android客户端和服务器端数据交互

    网上有很多例子来演示Android客户端和服务器端数据如何实现交互不过这些例子大多比较繁杂,对于初学者来说这是不利的,现在介绍几种代码简单.逻辑清晰的交互例子,本篇博客介绍第四种: 一.服务器端: 代 ...

  3. appium 自动化测试之知乎Android客户端

    appium是一个开源框架,相对来说还不算很稳定.转载请注明出处!!!! 前些日子,配置好了appium测试环境,至于环境怎么搭建,参考:http://www.cnblogs.com/tobecraz ...

  4. 仿优酷Android客户端图片左右滑动(自动滑动)

    最终效果: 页面布局main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayou ...

  5. 【原创】轻量级即时通讯技术MobileIMSDK:Android客户端开发指南

    申明:MobileIMSDK 目前为个人维护的原创开源工程,现陆续整理了一些资料,希望对需要的人有用.如需与作者交流,见文章底签名处,互相学习. MobileIMSDK开源工程的代码托管地址请进入 G ...

  6. 基于SuperSocket的IIS主动推送消息给android客户端

    在上一篇文章<基于mina框架的GPS设备与服务器之间的交互>中,提到之前一直使用superwebsocket框架做为IIS和APP通信的媒介,经常出现无法通信的问题,必须一天几次的手动回 ...

  7. Android客户端性能优化(魅族资深工程师毫无保留奉献)

    本文由魅族科技有限公司资深Android开发工程师degao(嵌入式企鹅圈原创团队成员)撰写,是degao在嵌入式企鹅圈发表的第一篇原创文章,毫无保留地总结分享其在领导魅族多个项目开发中的Androi ...

  8. 微信Android客户端架构演进之路

    这是一个典型的Android应用在从小到大的成长过程中的“踩坑”与“填坑”的历史.互联网的变化速度如此之快,1年的时间里,可以发生翻天覆地的变化.今天在这里,重新和大家回顾微信客户端架构的演进过程,以 ...

  9. Android 客户端设计之解决方案

    解决方案,是正对与需求来谈的.一个抽象的需求,需要一个较为上层抽象的解决方案来处理,这是病和药的关系.但是一个解决方案,可能会包含多个功能,每个功能都是解决方案上的一个节点.一个优秀的解决方案必然需要 ...

随机推荐

  1. July 25th 2017 Week 30th Tuesday

    Everything is always more beautiful reflected in your eyes. 一切事物映在你的眼里都会变得更美. Looking in your eyes, ...

  2. CodeForces 91A Newspaper Headline

    题目链接:CodeForces - 91A  Newspaper Headline 官方题解: In this problem letters from s1 should be taken gree ...

  3. Map使用方法

    转:https://www.cnblogs.com/lzq198754/p/5780165.html Java map 详解 - 用法.遍历.排序.常用API等 概要: java.util 中的集合类 ...

  4. Codeforces Round #535 (Div. 3) E2. Array and Segments (Hard version) 【区间更新 线段树】

    传送门:http://codeforces.com/contest/1108/problem/E2 E2. Array and Segments (Hard version) time limit p ...

  5. spring bean中构造函数,afterPropertiesSet和init-method的执行顺序

    http://blog.csdn.net/super_ccc/article/details/50728529 1.xml文件 <bean id="aaa" class=&q ...

  6. 使用jmeter进行简单的压测

    安装下载 前往官网下载,[地址] 环境 需要java环境,此处略 最好对jmeter配置下环境变量,方便打开,此处略 运行 启动jmeter 进入到bin目录,输入   ./jmeter    启动 ...

  7. 使用Analyze 和Instruments-Leaks分析解决iOS内存泄露

    版权声明:本文为博主原创文章,未经博主允许不得转载. 使用Analyze和Instruments-Leaks分析解决iOS内存泄露   实验的开发环境:Xcode 7   一.使用Product-An ...

  8. 01-Python学习笔记-基础语法

    Python标识符 -d           在解析时显示调试信息 -O           生成优化代码 ( .pyo 文件 ) -S           启动时不引入查找Python路径的位置 - ...

  9. 用javascript编写简单银行取钱存钱流程(函数)

    const readline = require('readline-sync')//引用readline-sync let arr = [[], []]; //登陆 let add = functi ...

  10. OC - ARC(自动引用计数)

    1.什么是自动引用计数? 顾明思义,自动引用计数(ARC,Automatic Reference Counting)是指内存管理中对引用采取自动计数的技术. 在OC中采用ARC机制,让编译器来进行内存 ...