­

某教务管理系统APP逆向分析之协议漏洞

  • 2019 年 10 月 8 日
  • 筆記

0X01前言

样本来源

某大学在使用的一款教务管理系统手机app,为了方便学生查询成绩和选课。我在一次偶然逆向中找到严重漏洞,现在把整个分析流程记录下来。

准备工具

  1. Fiddler
  2. APK改之理
  3. JEB 1.5
  4. PC端安卓模拟器(我用的海马模拟器)
  5. XposedBridgeApi-54.jar
  6. Eclipse

0X02 功能协议分析

 app登录后进入appCenter的界面,内有九个图标

“我的成绩”协议分析

 首先配置好手机和电脑端Fiddler的局域网下连接,等下获取手机的http和https协议。 登录完帐号后,点击“我的成绩”按钮,fd获取到了这个url请求。

  发现这是一个GET包

http://ydjwapp.♦♦♦.edu.cn/login_sso.aspx?procode=002&type=1&choice=XS0205&uid=20162104♦♦♦♦♦&role=XS&key=C3840551F78846DFAABA84C9A81B4F06&time=1520081445

分析url的参数

 仔细观察这个请求构造参数,发现key这个参数格外引人注目,前面的无非是uid不同,choice之类的是成绩查询的代号,time这个参数和现行时间戳很像,但是位数不够,等会分析。

0x03深入APK

首先用APK改之理载入分析我们分析的这个app

参数key的寻找

搜索结果太多了,于是我换个思路,上面既然是GET请求,那么会不会有&key这个常量的保存呢?于是很巧,找到了一处我们要找的关键地方

进入这处的java源码

通过改之理自带的jd-gui可以一键将smali反汇编代码翻译成参考的java源码,进入之后看到"&key"字符串在openURL()函数中,下面贴图部分为openURL()的部分源码,这里可以清楚地看到里面拼接的str1,可以判断基本上就是刚才提交url的地址了。因为里面包含了procode,choice,uid,key,time这些参数正是上面获取的url中get请求参数部分。

openURL()的全部代码

 可以发现有两处相似代码,有点重复?根据if来判断,转折在str1,也就是前面url的get请求进来的procode参数,经过多次尝试,procode一直是"002",也就是上面那处。纵观整个反汇编后的apk文件工程来看,下面的是另一个是OA(Office Automation 办公自动化)系统的请求加密地方,也就是说这个app不只是单独为该高校设计的,而是通用型,或者说换了个包装和切换了下接口就拿来用了,属实有点太随意了吧。

private void openURL()  {    this.time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());    String str3 = this.url + "&" + "time=" + this.time;    Object localObject = getMapParam(this.url);    if (str3.indexOf("?") != -1)    {      str1 = (String)((Map)localObject).get("procode");      String str2 = (String)((Map)localObject).get("choice");      localObject = (String)((Map)localObject).get("uid");      if ((str1 != null) && (str1.equals("002")))      {        str3 = String.valueOf(new Date().getTime()).substring(0, 10);        Log.e("@@@@@@@@@@@@@@@@@@@@", String.valueOf(str3));        str1 = o.a(str1 + str2 + (String)localObject + "DAFF8EA19E6BAC86E040007F01004EA" + str3);        Log.e("@@@@@@@@@@@@@@@@@@@@", str1);        str1 = this.url + "&" + "key=" + str1 + "&" + "time=" + str3;        openUrlWithWebview(this.detailView, str1);        Log.e("WebModuleOaActivity", str1);        return;      }      if ((str1 != null) && (str1.equals("006")))      {        str1 = p.a(str1 + str2 + (String)localObject + this.time + "DAFF8EA19E6BAC86E040007F01004EA");        Log.e("snstimepublickey", str1);        str1 = new StringBuilder(String.valueOf(this.url)).append("&").append("key=").append(str1).append("&").append("time=").append(this.time).toString() + "&tgc=" + t.a(getApplicationContext(), "catgc", "tgc");        openUrlWithWebview(this.detailView, str1);        Log.e("WebModuleOaActivity", str1);        return;      }      str1 = str3 + "&tgc=" + t.a(getApplicationContext(), "catgc", "tgc");      openUrlWithWebview(this.detailView, str1);      Log.e("WebModuleOaActivity", str1);      return;    }    String str1 = this.url + "&tgc=" + t.a(getApplicationContext(), "catgc", "tgc");    openUrlWithWebview(this.detailView, str1);    Log.e("WebModuleOaActivity", str1);  }  

0x04HOOK关键函数

 既然玩安卓逆向,当然少不了Xposed框架了,它可是我们挂钩子的好工具

1.找到关键函数

a()方法

通过上面的图不难发现,第一个箭头指向的o.a()方法是改key参数构造的最直接加密方法,二话不说,点击这个带下划线的a进入a()方法,看看里面长什么样。

 里面调用了内部类的a()方法和重载a()方法以及b()方法,具体我就不贴图了,目的是改变改静态类的一些全局参数,然后经过后面的计算返回解密后的结果赋值给str1,即key的值。

openUrlWithWebview()方法

  判断是发送url请求,其中有最后拼接完整的字符串,即第二个传参paramString,准备下一步拦截进行验证

2.利用Xposed框架拦截O.a()方法的传参和返回值和openUrlWithWebview()的传参

static int i = 1;    @Override  public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {      if(!loadPackageParam.packageName.equals("com.♦♦♦♦♦.jw")){          return;      }      XposedBridge.log("Load app:----->"+loadPackageParam.packageName+" 加载完毕");      findAndHookMethod("com.zfsoft.core.d.o", loadPackageParam.classLoader, "a", String.class,  new XC_MethodHook() {          @Override          protected void afterHookedMethod(MethodHookParam param) throws Throwable {              XposedBridge.log("----第"+(i++)+"次拦截----");              XposedBridge.log("a方法 传入参数:"+param.args[0]);              XposedBridge.log("a方法 返回参数:"+param.getResult());          }      });      findAndHookMethod("com.zfsoft.webmodule.controller.WebModuleOaFun", loadPackageParam.classLoader, "openUrlWithWebview", WebView.class, String.class, new XC_MethodHook() {          @Override          protected void afterHookedMethod(MethodHookParam param) throws Throwable {              XposedBridge.log("openUrlWithWebview方法 传入参数:"+param.args[1]);          }      });    }  

3.在模拟器中测试查看拦截数据

第一次拦截参数分析

参数

str1

002

str1

XS0205

(String)localObject

20162104♦♦♦♦♦

const str

DAFF8EA19E6BAC86E040007F01004EA

time

1520088144

----->com.♦♦♦♦.jw 加载完毕    ----第1次拦截----  a方法 传入参数:002XS020520162104♦♦♦♦♦DAFF8EA19E6BAC86E040007F01004EA1520088144  a方法 返回参数:ECC9D8F62A45091ED3EA35B691105318  openUrlWithWebview方法 传入参数:  http://ydjwapp.♦♦♦.edu.cn/login_sso.aspx?procode=002&type=1&choice=XS0205&uid=20162104♦♦♦♦♦&role=XS&key=ECC9D8F62A45091ED3EA35B691105318&time=1520088144  ----第2次拦截----  a方法 传入参数:002XS020820162104♦♦♦♦♦DAFF8EA19E6BAC86E040007F01004EA1520088165  a方法 返回参数:818348171E471F392C23CBFF99EA3E36  openUrlWithWebview方法 传入参数:  http://ydjwapp.♦♦♦.edu.cn/login_sso.aspx?procode=002&type=1&choice=XS0208&uid=20162104♦♦♦♦♦&role=XS&key=818348171E471F392C23CBFF99EA3E36&time=1520088165  ----第3次拦截----  a方法 传入参数:002XS020420162104♦♦♦♦♦DAFF8EA19E6BAC86E040007F01004EA1520088169  a方法 返回参数:1CE6D9F428DF5294E66D88C0653574BC  openUrlWithWebview方法 传入参数:  http://ydjwapp.♦♦♦.edu.cn/login_sso.aspx?procode=002&type=1&choice=XS0204&uid=20162104♦♦♦♦♦&role=XS&key=1CE6D9F428DF5294E66D88C0653574BC&time=1520088169  

0x05还原核心加密算法

1.明确加密方法a()

借用上面第一次a方法拦截到的参数

002XS020520162104♦♦♦♦♦DAFF8EA19E6BAC86E040007F01004EA1520088144

  可知传入这个方法的参数包含5个字符串的拼接,进入到o这个类中的a()加密运算后返回给str1,后面str1再拼接成完整的GET请求的url。

2.用Elipse模拟a()方法

来到com.zfsoft.core.d.o这个类(这个就是上面a()方法所在类),于是我们在Elipse中新建类,命名为o,拷贝通过APK改之理自带的jd-gui转换过来过来的java源码,会发现很多处不合理的代码,如:(观察如下return位置)

if (paramInt >= j)  {    a(e, paramArrayOfByte, i, 0, j);    a(e);    i = j;    if (i + 63 >= paramInt)    {      j = i;      i = k;    }  }  for (;;)  {    a(e, paramArrayOfByte, i, j, paramInt - j);    return;        //这里return显然不合理,后面的操作会影响结果的。    a(arrayOfByte, paramArrayOfByte, 0, i, 64);    a(arrayOfByte);    i += 64;    break;    j = 0;  }  

这里我是用到了JEB 1.5强大的反编译工具, 上面的代码变成了如下:

if(paramInt >= j) {        o.a(o.e, paramArrayOfByte, i, 0, j);        o.a(o.e);        for(i = j; i + 63 < paramInt; i += 64) {            o.a(arrayOfByte, paramArrayOfByte, 0, i, 64);            o.a(arrayOfByte);        }    }    else {        int v10 = i;        i = 0;        k = v10;    }      o.a(o.e, paramArrayOfByte, k, i, paramInt - i);  

可读性变强了很多,其实要修改的地方还有别的三处,就不一一陈列了,当时调试了一个下午,才改完的。

3.执行Main函数调用O.a()方法

public static void main(String[] args) {    System.out.println(o.a("002XS020520162104♦♦♦♦♦DAFF8EA19E6BAC86E040007F01004EA1520088144"));  }  

前面记录的数据:

a方法 传入参数:002XS020520162104♦♦♦♦♦DAFF8EA19E6BAC86E040007F01004EA1520088144 a方法 返回参数:ECC9D8F62A45091ED3EA35B691105318 测试成功截图:

通过学号和当前时间戳取前十位以及前面的固定参数做一个key的加密计算,拼接url提交网页即可访问到正确的结果。

0x06总结

明确关键位置,Hook拦截传参,以及加密结果返回值,能为我们更好分析部分参数的生成以及提交GET请求参数的拼接。