JDRead1破解小记

咸鱼上300收了个JDRead1,买之前就做过调研,系统比较封闭,不能安装第三方软件,也一直没人成功破解,就抱着试一试的心态收了一个,体验还行,ppi挺高的,很细腻,观感很不错,系统其实也还行,但是不能换字体,不能换锁屏壁纸这两点略难受,于是我就开始对它下手了(其实还是手痒)。

信息收集  

这机器是18年发售的,这两年各种修修补补各种漏洞已经封的差不多了,到处点了点试了半天连adb都没连上。 然后就想着抓包看看能不能解包固件然后分析一下,因为系统过于封闭,不能在通过设置代理然后fiddler抓包,苦思冥想想起来数年前(真的是数年前了)玩过的arp欺骗,由此可见我的姿势水平不仅在这数年间没有丝毫长进甚至还略有退步:(
上网搜了搜,ettercap + wireshark就能搞定,但是我没有找到ettercap的windows版,于是我不得不给尘封已久的老台式通上电,启动许久未见的kde,然后使用我最爱的pacman安装ettercap,可惜因为太久没有滚动更新装不上,只能等待几十g的更新安装完成,然后重启、安装、启动软件、配置,轻松搞定。 通过抓包找到了以下两个请求:

固件更新历史  

请求:

http://IP_ADDRESS/api/firmware/history?where={"brand":"Onyx","buildNumber":1548,"buildType":"user","deviceMAC":"","fingerprint":"Onyx/JDRead/JDRead:4.4.2/2020-08-26_17-55_jd_a2dd4ac/1548:user/dev-keys","force":false,"fwType":"release","heightPixels":1448,"id":-1,"lang":"zh_CN","model":"JDRead","size":0,"submodel":"","widthPixels":1072}

返回就是一堆历史固件的信息,但是固件是upx格式,搜了一下是加密过的,解不开,就放弃了从固件找突破口。

JDRead APP 更新检查  

请求:

http://IP_ADDRESS/api/app/batchUpdate?where=[{"changeLogList":[],"channel":"ONYX","id":-1,"macAddress":"","model":"JDRead","packageName":"com.onyx.jdread","platform":"ONYX","size":50482020,"type":"RELEASE","versionCode":10876,"versionName":"10877-e3308261751"}]

返回是[]
然后修改请求中的versionCode,不知道为什么只找到了一个2018年06月13号的更新,成功下载了apk,准备反编译看看。

JDRead APP 反编译及关键代码定位  

不得不说,反编译apk还是mt管理器舒服,虽然apktool+jadx也能用但是体验一般。 首先尝试搜索关键词"adb",在com.onyx.jdread.setting.g.d里找到一段:

private void a(boolean z) {
  Settings.Secure.putInt(getContext().getContentResolver(), "adb_enabled", z ? 1 : 0);
}

很容易看出来这个函数是在往系统设置里写adb_enabled的值,此时正常人应该继续追踪哪里调用了这个函数,但我是一个注意力比较容易分散的人,看到上面不远处有这样一个函数:

private boolean p() {
  String r = r();
  if (StringUtils.isNotBlank(r)) {
    return r.equals(q.j("0423"));
  }
  return false;
}

虽然不知道q.j是个什么函数,但是很容易看出来这个函数是在判断r()是否等于"0423",在下面可以找到r()的代码:

private String r() {
  return g.a(0x7f080437, "");
}

然后切回smali,找到g的具体位置:Lcom/onyx/jdread/main/common/g;,找到了上一步调用的函数:

private static Context a;
public static String a(int paramInt, String paramString) {
  return a(a, paramInt, paramString);
}

在这个类里没找到符合String a(Context, int, String)的函数,找了半天终于发现这个类继承了com.onyx.android.sdk.utils.ad,在这个类里找到了如下函数:

public static String a(Context context, int i, String str) {
    return a(context, context.getResources().getString(i), str);
}

这个混淆让人很难受,但是作为一名坚韧不拔的人,当然还是要继续,形式为String a(Context, String, String)的函数就在下面一点:

public static String a(Context context, String str, String str2) {
  return a.getString(str, str2);
}

在上面可以找到:

SharedPreferences a = PreferenceManager.getDefaultSharedPreferences(context);

由此可见最上面的函数r()返回的字符串就是SharedPreferences里的一个value,而key是上一个a里面传过来的:

context.getResources().getString(i)

i的值是0x7f080437,随后在resource.arsc中找到id为0x7f080437的值为"password_key",继续搜索0x7f080437,找到一段代码:

private void c() {
  g.b(0x7f080438, this.a.phone);
  g.b(0x7f080437, q.j(this.a.password));
}

联想到设置锁屏密码的时候需要设置密码和手机号,猜测password_key为锁屏密码。 综上可以推测出把锁屏密码设置为0423会发生一些神奇的事情,然后回到最开始的com.onyx.jdread.setting.g.d,找到如下代码:

@Subscribe
public void onDeviceModelEvent(p pVar) {
  if (this.k && p()) {
    a(1, !w());
  }
}

看到函数名onDeviceModelEvent推测是点击设备型号的事件,于是放下代码,拿起机器,设置锁屏密码为0423,狂点设备型号,没用,然后试试狂点下一行的系统版本,然后toast飘出来了,提示已开启开发者权限,发现能够成功连上adb,我留下了感动的泪水,至于为什么devicemodel是系统版本我也懒得管了,混淆过的破代码我是看不下去了。

提权  

连上adb我以为万事大吉,于是下了一个eink桌面(强烈谴责酷安网页版不提供apk下载的行为),但是adb installpm install均无法安装,提示INSTALL_FAILED_USER_RESTRICTED,后来发现应该是PackageManager.class被魔改了。 然后我去搜索了android 4.4.2的CVE,企图提权,主要在尝试dirtyc0w(CVE-2016-5195)修改系统文件,期间还学会了ndk的使用,但是折腾了半天也没成功。 后来在群友的提醒下下载了五年前用过的kingroot,几分钟后成功root,虽然还是无法通过常规手段安装app,但是可以通过比较dirty的方式安装app(把apk扔到/data/app/,并改好权限),顺便还从自带的脚本里学到了/system/挂载读写的方法:mount -o rw,remount /dev/block/mmcblk0p5 /system,从而可以随意安装系统级app。 至此,jdread1的破解已基本完成,我也快乐地用上了新锁屏和新阅读软件。

感想  

说实话,成功破解也是误打误撞,如果不是纯java的apk我很可能就完蛋了,但是亲手设置完锁屏密码然后连上adb的那一刻的快乐真的是无与伦比的,作为一名pwn苦手,连栈溢出堆溢出都不会的究极菜鸡,第一次体会到了pwn的快乐,一步一步定位代码的过程虽然痛苦但现在回想起来也挺快乐,以至于我现在有了购买奇奇怪怪的安卓设备然后破解的冲动(当然是不可能的,贫穷)。 最后秀一下我分析的时候用鼠标强行画的流程图: 分析