319次浏览

jeb3.0 动态调试app寻找登录加密全过程 (一)

准备好工具: JEB调试器, 逍遥模拟器, apk文件。

首先打开JEB调试器, 拖入要进行调试的apk文件, 双击JEB软件右侧工程浏览器中的Manifest xml文件, 然后ctrl+f搜索一下字符串 debug , 看看是否能搜索得到, 要想apk可调试那么这个xml配置文件中的application标签下会有字符串 android:debuggable=”true”。

如图:

很好, 什么也没搜索到, 这样的话我们就得手动在application标签处开始添加。

添加 android:debuggable=”true”

要添加或者更改数据就要反编译apk, 现在打开工具ApkToolBox打开apk文件然后点击反编译, 这时候选择好输出路径后会弹出一个提示: 是否忽略res资源文件? 这里选择取消,然后就开始反编译了。

可以看到apk被解包到文件夹了, 文件夹中的 AndroidManifest.xml 文件就是前面在JEB中看到的Manifest xml文件, 现在我们只需修改它然后再重新打包。

如上图我已经改好了,保存好, 再把 天天手游 这个文件夹摁住拖进ApkToolBox后, 点击回编译apk, 按照提示走一下, 最后生成了一个新的apk, 这工具会自动签名非常方便, 至此我们就已经修改好 apk配置文件中的是否可调试状态为true了。

开始调试

JEB调试器加载重新打包的apk文件, 检查一下是否成功添加了 android:debuggable=”true”, 没有的话是调试不了的。

很好, 没有任何问题! 打开逍遥模拟器安装重新打包的apk并且运行。

打开Fiddler 4抓取app登录包, 这里逍遥模拟器内的代理以及证书我都配置好了, 不会的话请自行百度, 还有个事, 这个app不允许http代理, 所以我安装了xp框架中的一个 JustTrusMe模块来杀死证书验证, 解除限制。

现在捕捉app 的登录包

登录POST提交:

POST https://ttsy-apid2ddw.tgcloud2.com:5869/lottery-api-tg0152/api/v2/user/login HTTP/1.1
deviceId: 863535273811746
packageName: com.tg9.ttsy
sign: ae62347c9297ffc6791add746f3f576e
key: MwFF1KliHIHTjDKodVuC+AhOToWjogI8jz5YUCkTKwu0kIl+qxh8dWNUM+/56KCSWwnVZStq0Z370eMB/BCSbUiGdF1bXoWKGUUMHp6br/Wg9ZEYGVsfbb2Uzxa03TJt9K2NDUcWuNy4V9CMLuSDJywQdVu9TV72hGWbjiP6dic=
version: V2.1.1
timestamp: 1598645771
deviceName: OPPO PCRT00
User-Agent: Android
Content-Type: application/json; charset=UTF-8
Content-Length: 109
Host: ttsy-apid2ddw.tgcloud2.com:5869
Connection: Keep-Alive
Accept-Encoding: gzip

{"body":"ZlNPqh96KIqBVT//BYdzThiyPI8KAx+0PzWY3adpfIpw53JeJy6AXEqRS4R2S9Zu55sOWLWEUTPMrNTHi+9S0Q\u003d\u003d"}

服务器返回:

HTTP/1.1 200 OK
Server: openresty
Date: Fri, 28 Aug 2020 20:16:11 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 110
Connection: keep-alive
X-RateLimit-Remaining: 299
X-RateLimit-Burst-Capacity: 300
X-RateLimit-Replenish-Rate: 20
X-Response-Default-Foo: Default-Bar
X-RateLimit-Remaining: 298
X-RateLimit-Burst-Capacity: 300
X-RateLimit-Replenish-Rate: 20

{"code":200,"data":"{\"msg\":\"账号不存在,请联系客服!\",\"code\":100}","message":"请求成功"}

寻找Sign加密

可以看到登录提交包中的内容很多是加密的, 我们先来找sign的生成方式 , 回到 JEB搜索字符串deviceName, 为啥要搜索它? 因为它在提交数据中是个协议头, 添加这些参数的时候想必都会在一个代码块, 所以sign也可能在附近

搜索到一个结果, 双击结果就会跳转到 java代码

可以看到一些java代码, 大致看了一下, 这几部分代码都是在添加协议头, 而且这里边的协议头和登录包中的协议头都一样还有sign生成和key的生成, 所以很可疑!我要在这个代码的smali文件下几个断点, 目前是在java代码中, 鼠标点到要下断点的函数位置然后按下tab就会来到前面鼠标所在位置函数的smali代码处了, 再按ctrl+B下断

断点下好之后点击 JEB工具栏中 “虫子按钮” 进行进行调试, 点击后会弹出对话框出现模拟器地址以及模拟器内的进程一些信息, 我们选中 com.tg9ttsy 进程后点击附加。

注意: 如果附加和调试对话框中没有模拟器的话, 你需要将模拟器目录下的adb.exe路径添加进系统环境变量path中, 如果已经添加却无法搜索到, 打开任务管理器结束adb.exe进程, 重启模拟器应该就可以了

附加完毕后, 现在回到模拟器中点击登录按钮, 看下断点是否会断下

可以看到成功断下来了, 说明我们找对了, 开始分析, 目前我们在smali代码处, 按f6单步走到sign下面, 再按 tab可以回到java代码

如图, 代码操作了v1变量, 这里可以把v1类型int改为string就可以看到文本信息了, v1=”1598648931″ , 为什么要改string类型呢? 因为上行代码定义的v1就是String类型的

再看上方代码 String v1 = String.valueOf(System.currentTimeMillis() / 1000L);

得出v1是当前时间戳,v0.put(“sign”, vq.a(np.a(v1))); 这里是添加协议头sign

vq.a(np.a(v1)) 这里先是调用了np.a方法传入了v1时间戳, 我们双击a进入到源码处看看做了什么操作

np.a方法源码:

package com.jianshu.test;

public class np {
    public static String a(String arg1) {
        return arg1 + "##Lottery2017$$";
    }

    public static String b(String arg1, String arg2) {
        return arg1 + "##Lottery2017$$" + arg2;
    }
}

很简单就是传入时间戳字符串, 返回传入的时间戳+”##Lottery2017$$” , 一个字符串拼接操作罢了, 也就是 vq.a(时间戳+”##Lottery2017$$”)

好了现在点击JEB工具栏的左箭头返回到sign处, 继续分析 vq.a方法, 双击vq.a 的a来到a方法查看

vq.a方法源码:

public class vq {
    public static String a(String arg7) {
        int v2;
        if(TextUtils.isEmpty(arg7)) {
            return "";
        }

        try {
            byte[] v7_1 = MessageDigest.getInstance("MD5").digest(arg7.getBytes());
            v2 = 0;
            String v3 = "";
            while(true) {
            label_11:
                if(v2 >= v7_1.length) {
                    return v3;
                }

                String v4 = Integer.toHexString(v7_1[v2] & 0xFF);
                if(v4.length() == 1) {
                    v4 = "0" + v4;
                }

                v3 = v3 + v4;
                break;
            }
        }
        catch(NoSuchAlgorithmException v7) {
            v7.printStackTrace();
            return "";
        }

        ++v2;
        goto label_11;
    }
}

可以看出这是一个MD5加密, 最后sign的生成方式就是 MD5 (时间戳+”##Lottery2017$$”)

再看看提交协议头的主要参数

deviceId: 863535273811746 //这个可以随机
sign: ae62347c9297ffc6791add746f3f576e //MD5 (时间戳+"##Lottery2017$$")
key: MwFF1KliHIHTjDKodVuC+AhOToWjogI8jz5YUCkTKwu0kIl+qxh8dWNUM+/56KCSWwnVZStq0Z370eMB/BCSbUiGdF1bXoWKGUUMHp6br/Wg9ZEYGVsfbb2Uzxa03TJt9K2NDUcWuNy4V9CMLuSDJywQdVu9TV72hGWbjiP6dic= //还没有找
version: V2.1.1
 //app版本
timestamp: 1598645771
 //时间戳
deviceName: OPPO PCRT00 //手机型号

sign的加密的时间戳就是协议头中的 timestamp: 1598645771

加密方法也就是 MD5 (“1598645771″+”##Lottery2017$$”)

拿工具算了下确实是ae62347c9297ffc6791add746f3f576e

到这里sign就成功找到了, 打开笔记本记录下来。

寻找key加密

回到sign java代码处可以看到key的生成就在下边

v0.put(“key”, ar.a(op.a().b()).trim()) //key的生成

可以看到代码 ar.a(op.a().b()).trim() 先是执行了op.a().b(), 就看看它到底干了什么, 把鼠标点到最里边的那个函数, 再按下tab来到函数对应的smail代码下个断点就行了

回到模拟器点击登录后成功断下来了, 先f6单步配合变量查看分析

000000E4  invoke-virtual      op->b()String, v1
000000EA  move-result-object  v1
000000EC  invoke-static       ar->a(String)String, v1

把v1变量的值(4edf5e3495b84983) 赋值给了 op类中的b变量, 先点中函数b, 按下tab来到java代码处

部分代码:


      try {
            v0.put("key", ar.a(op.a().b()).trim());
        }
        catch(Exception v5) {
            tq.a(v5.toString());
        }

        return v0;
    

再双击函数op.a().b()就可以来到函数java源码处了

public String b() {
        if(TextUtils.isEmpty(op.b)) {
            op.b = "598b8227b2114f81";
        }

        return op.b;
    }

可以看到就是判断刚刚赋值的op.b是否为空, 如果是空的话就给op.b赋值 598b8227b2114f81 不为空的话就直接返回op.b, 前面已经赋值过了所以不是空的, 那么op.a().b() 函数返回就是 4edf5e3495b84983

记录一下 ar.a(“4edf5e3495b84983”).trim() , 现在里边这个函数已经分析完毕了, 再来看看ar.a这个函数执行了什么, 和前面一样来到ar.a函数的smail处打个断点继续调试

又把v1变量的值(4edf5e3495b84983) 传递ar.a函数了, 继续单步执行完ar.a函数后返回了v1, 此时的v1变量值变成了一串gDEM8mhOHwOaKI+2oX8qCVzAI4x/PVKCLEFJdo++tZwygcv5OK9eH0OShFkiuulaJ59orwAHHM2JrfVSh2QjVJEqPDjrEEKlOGW1QRiZlYyQmgfXO2pXcyaLjqQFJ4IXFHMQA0uqoVVNly7wxZIywMHrf03HQ/9F0dxB82lF618=

很好! 现在就可以确定这个ar.a函数就是把 v1变量的值 4edf5e3495b84983 进行了一个加密, 返回了这样一串密文, 现在我们要分析ar.a这个函数的内部, 目前在smail代码处, 双击ar->a() 的a来到它的smail代码处

00000000  invoke-virtual      String->getBytes()[B, p0
00000006  move-result-object  p0
00000008  const-string        v0, "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRQZ5O/AOAjeYAaSFf6Rjhqovws78I716I9oGF7WxCIPmcaUa1YuyLOncCCuPsaw69+RMWjdbOBp8hd4PPM/d4mKTOVEYUE0SfxhhDTZaM5CzQEUXUyXy7icQTGR5wBjrbjU1yHCKOf5PJJZZQWB06husSFZ40TdL7FdlBpZ1u1QIDAQAB"
0000000C  invoke-static       fq->a(String)[B, v0
00000012  move-result-object  v0

这里其实就是一个RSA加密, 根据经验得出这一串base64字符是public key也就是RSA的密码, 把RSA公钥赋值给了 v0, 我们先按tab进去看看java代码

此时的arg9就是我们前面传递进来的v1, 然后用v9字节数组保存arg9的字节

后面就是初始化rsa加密, 设置密钥

 byte[] v9 = arg9.getBytes();
        X509EncodedKeySpec v1 = new X509EncodedKeySpec(new byte[]{0x30, 0x81, -97, 0x30, 13, 6, 9, 42, 0x86, 72, 0x86, -9, 13, 1, 1, 1, 5, 0, 3, 0x81, 0x8D, 0, 0x30, 0x81, 0x89, 2, 0x81, 0x81, 0, 0x91, 65, -98, 78, -4, 3, 0x80, 0x8D, -26, 0, 105, 33, 0x5F, -23, 24, 0xE1, -86, 0x8B, -16, -77, -65, 8, -17, 94, -120, -10, 0x81, 0x85, -19, 108, 66, 0x20, -7, -100, 105, 70, -75, 98, -20, 0x8B, 58, 0x77, 2, 10, -29, -20, 107, 14, -67, -7, 19, 22, 0x8D, -42, -50, 6, -97, 33, 0x77, 0x83, -49, 51, -9, 120, -104, -92, -50, 84, 70, 20, 19, 68, -97, -58, 24, 67, 77, -106, 0x8C, -28, 44, 0xD0, 17, 69, -44, -55, 0x7C, -69, 0x89, -60, 19, 25, 30, 0x70, 6, 58, -37, 0x8D, 77, 0x72, 28, 34, 0x8E, 0x7F, -109, -55, 37, -106, 80, 88, 29, 58, 0x86, -21, 18, 21, -98, 52, 77, -46, -5, 21, -39, 65, -91, -99, 110, -43, 2, 3, 1, 0, 1});
        PublicKey v0 = KeyFactory.getInstance("RSA").generatePublic(v1);
        Cipher v1_1 = Cipher.getInstance("RSA/ECB/PKCS1Padding");

X509EncodedKeySpec()这个函数的参数就是一个密钥, 可以百度查查, byte字节数组内容进行base64编码后

完美对上, 前面的猜想完全正确, 现在已经知道协议头中key的加密方式和密钥了, 记录一下

public key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRQZ5O/AOAjeYAaSFf6Rjhqovws78I716I9oGF7WxCIPmcaUa1YuyLOncCCuPsaw69+RMWjdbOBp8hd4PPM/d4mKTOVEYUE0SfxhhDTZaM5CzQEUXUyXy7icQTGR5wBjrbjU1yHCKOf5PJJZZQWB06husSFZ40TdL7FdlBpZ1u1QIDAQAB
填充方式: PKCS1Padding

好了, 现在ar.a分析完毕了, ar.a(“4edf5e3495b84983”).trim() 这里边的trim()函数是起到什么作用呢?百度了一下这个就是把加密后的密文首尾空格删除, 哦对了还没有去找这个4edf5e3495b84983是怎么生成的? 实话说当时我没找着, 暂且认为他是固定的。

寻找body加密

前面协议头中的所有参数都已经生成添加好了,按理说我继续单步调试下去应该会到提交内容那部分代码吧? 可惜单步不过去, 不知道是JEB的bug还是我太菜了,不管了老方法搜索一下body字符串

无数次尝试告诉我这个setbody就是设置body内容的地方, 双击进不去, 只能切换到smail代码界面ctrl+f搜索setBody了

搜到以后就在setbody函数内下个断, 回到模拟器点击登录后成功断下

我猜这个string@8522:”xGXQWoulsTSAyMngZuXLGFpzEDt48UbGl6qPoqkuNjFu4tSyptqgp1k/TkwUl6iV98FgJtScYxQT1X1H1qbK6C6+sH4g5TmmACxhAHUbY2s=” 就是加密后的body值, 点击jeb的工具栏的运行按钮看看登录提交包中的body内容是不是这个

POST https://ttsy-apid2ddw.tgcloud2.com:5869/lottery-api-tg0152/api/v2/user/login HTTP/1.1
deviceId: 863535273811746
packageName: com.tg9.ttsy
sign: bd488eb194bea6760835bf52804b419e
key: TCbT6Z6X5hnOt7TfuGKEPxidvr1P1xagNaqjI3HcewGgD6lKCPSe5wao+6ExYH3zwfh1tgyRhFLzbEAhUA1M81I0dRzjXmeuPlj8blkYeL0hzoptp+71M4hS/Xea/7YG+k+JTUzIY5DgIAsKpUdxpwmNrs5sqgavU8/qen+lYew=
version: V2.1.1
timestamp: 1598694458
deviceName: OPPO PCRT00
User-Agent: Android
Content-Type: application/json; charset=UTF-8
Content-Length: 124
Host: ttsy-apid2ddw.tgcloud2.com:5869
Connection: Keep-Alive
Accept-Encoding: gzip

{"body":"xGXQWoulsTSAyMngZuXLGFpzEDt48UbGl6qPoqkuNjFu4tSyptqgp1k/TkwUl6iV98FgJtScYxQT1X1H1qbK6C6+sH4g5TmmACxhAHUbY2s\u003d"}

运气真好果然如此, 那么现在要怎么找这个body加密的地方呢? 目前我们是在setbody函数处,函数前面代码应该就是加密的过程,再次调试点击登录断下来了

我按下tab进入java代码后发现这是在setbody函数的地方, 前面并没有加密的过程, 所以只能继续f6单步直至setbody返回, 可以看到上图, 我们只需要f6单步两下就可以返回出这个setbody函数了

如上图成功返回出来了,按下tab查看java代码是否有加密过程?

内部源码:

   private sh1 b(sh1 arg4) throws IOException {
        th1 v0 = arg4.a();
        if(v0 != null) {
            bl1 v1 = new bl1();
            v0.k(v1);
            String v0_1 = v1.j(lp.b.c());
            String v0_2 = bq.b(op.a().b(), v0_1);
            la v1_1 = new la();
            if(TextUtils.isEmpty(v0_2)) {
                v0_2 = "";
            }

            v1_1.setBody(v0_2);
            th1 v0_3 = th1.c(this.a.r(v1_1), lp.b);
            com.jianshu.test.sh1.a v4 = arg4.h();
            v4.g("enc");
            v4.f(v0_3);
            return v4.b();
        }

        return arg4;
    }

v1_1.setBody(v0_2); 刚才我们就在这个函数内,这里是传递进入了一个v0_2, 显然这个v0_2就是加密后的body

再看v0_2是怎么生成的

String v0_2 = bq.b(op.a().b(), v0_1); 这里跟前面的rsa加密很像, 我猜测这里就是传入数据进行加密,

先确定一下v0_1是个什么值, 现在点中v0_1按下tab来到smail代码处下个断点, 断下后f6单步几下, 再查看变量界面的变化

ok, 找着点了它的值是: {“password”:”c4ca4238a0b923820dcc509a6f75849b”,”userCode”:”test”}

这不是我登录输入的账号和密码吗? 一个json格式的字符串? password为什么看不懂呢? 猜测这是密码的md5,app中我输入的账号是test 密码是1 我们来看看1的md5是多少

猜对了诶, 那么现在知道了 String v0_2 = bq.b(op.a().b(), v0_1); 的v0_1值, 目前是在smail代码处, 按下tab来到java代码处,再来看看op.a().b()是个什么东西? 双击跳转到函数java代码处


    public String b() {
        if(TextUtils.isEmpty(op.b)) {
            op.b = "598b8227b2114f81";
        }

        return op.b;
    }

这不是前面寻找key加密中找不到成方式的那个东西吗? 不管, 先记录一下 bq.b(16位英文数字字符串,{“password”:”c4ca4238a0b923820dcc509a6f75849b”,”userCode”:”test”} )

现在点击jeb工具栏的左箭头返回到上次的地方

再来看看bq.b()函数是什么加密? 双击bq.b的b来到函数内部代码处

真香啊, 这不就是一个AES加密吗? CBC模式 pkcss5填充, iv偏移是 Cp99-!qazXSw2-88, 这里arg3参数就是AES密码, arg4参数就是 {“password”:”c4ca4238a0b923820dcc509a6f75849b”,”userCode”:”test”} , 最后加密后的数据就是我们想要的body了

原来前面不知道怎么生成的那个16位英数字符串是用到了这里, 作为了aes的密钥, 那我们这样加密后的数据,服务器是怎么知道aes密码并且解密的呢? 这个密码就通过RSA公钥加密后放在了协议头中的key里边, 服务器有RSA私钥直接解密就得到了aes的密码, 从而解密我们发送的数据。

总结

总之全凭运气!!

提交方式 POST
提交地址 https://ttsy-apid2ddw.tgcloud2.com:5869/lottery-api-tg0152/api/v2/user/login

deviceId: 863535273811746
 //自己随机
packageName: com.tg9.ttsy
sign: 7bf3f6f9600d57c6138683384f61385f //sign就是
 md5加密(时间戳+”##Lottery2017$$”)
key:Ntul3oL9KDCrPCaGDFeIENIVzHkYN4QQw+bjSVlnWO4YnS9FzNP2GcCIzF0ijax7lGpVn0I6g/njTfIo5oWjY8iYuJbeKo0Bv1mo2Ajefi8DXWdYzHQnOETfOY7T29ETFDWPatiIo4sfPat4EfLwO/ENdMkcQ2cfkrWwBaAlOPM=
//这里是rsa加密aes的key, key可以自己定义只需16位英数即可
version: V2.1.1
timestamp: 1598605741
 //时间戳
deviceName: OPPO PCRT00
User-Agent: Android
Content-Type: application/json; charset=UTF-8
Host: ttsy-apid2ddw.tgcloud2.com:5869
Connection: Keep-Alive

提交内容
{"body":"v0YxfpXm6Qy/r4KgyP/u/3ly2m2NuHm2vPWUniWtUEWkMeZ6piBvzV+M7ggdgp/4qQihvRIYDrviojkXwIxO4OqPsWgh+xtCDrCemrKhsKA\u003d"}
这里边的body密文解密后就是
{"password":"c4ca4238a0b923820dcc509a6f75849b","userCode":"test"}
password这个字段的加密是密码的md5, 最后用aes加密整个字符串, 注意这里aes的密码是前面rsa加密的那个16位英数字符串, 要不然服务器解密不了, 当你提交成功后返回的数据是aes加密的, 解密密码是你加密body时的密码。

需要注意AES加密解密都是UTF8字符集的哦, 下篇文章写代码来实现这整个过程。

发表评论

电子邮件地址不会被公开。 必填项已用*标注