天翼校园安卓客户端算法分析

全国天翼校园的思路应该是通用的,各地算法不同仅供参考

Posted by 浅唱 on November 4, 2021

前言

学校的网是真的差。
历史文章:https://blog.qcnhy.cn/2019/09/20/1/
曾经尝试分析过共享上网,当时启用了防共享的检测,现在也仍有GSwifi的路由器随处可见。现在学校关闭了共享的检测,任意路由器或者移动热点均可共享上网(可能有客户端上的检测,检测到共享就请求下线)
时隔两年,这回我们分析一下他的登录http请求以及damod的加解密算法。
本文涉及android、c、python等语言,使用到的工具有IDA、fridk、unidbg等,数款ide如android studio、vscode、idea等。阅读本文需要一定程度的基础,没有的话自己恶补了(确实太难了555)

鸣谢

开始之前感谢几位在分析过程中对我极大帮助的大佬们

  1. 看雪论坛:原创记掌上大学天翼校园的逆向分析(评论第18楼的小伙纸)(小伙纸很强,静态分析全靠他)
  2. 奋飞,国家高级信息系统项目管理师,独立安全研究员。 c/c++/asm/go/python
  3. 白龙~(给了很多方向性的指导)
  4. 白龙群里的热心群友,昵称:Pluto(远程帮我调试frida主动调用加解密算法,为后面继续分析做了基础铺垫)
  5. 途深
  6. 中国医大CT-YOUNG 闪讯和深澜并存的验证机制研究(文章是对未加密的ctyoung请求的研究,同时作者也提供了许多思路)

感谢对我提供技术支持与帮助的大佬们! ……
花了近两个月,时间有点长,很多人不记得了,在此一并表示感谢!

开始

了解应用发送的请求和对应的作用是最基本的吧,我们使用工具获取一下软件从启动、登录、退出所有流程的http请求。(去除了189.cn等用于测试网络是否被劫持需要登录的无关请求) 第一张.png

加解密

看完请求之后,内容都是加密的,于是我摸索明文的内容。
由于app加360的壳,所以使用xposed的一些工具dump出jar 屏幕截图 2021-11-04 052641.png 大概就能看到格式是xml的请求格式,调用了DaMod的加密算法
继续看DaMod,明写着加密解密。 屏幕截图 2021-11-04 052842.png 使用frida hook就可以获取到加密前后解密前后的内容了。还可以替换加解密的内容。这里上封装好的py代码
登录.py

继续

尝试hook修改传入参数进行自定义加密,然后再自己模拟发送请求到服务器,发现只通过ip来授权登录,mac啥的都是没用的。

ticket的xml请求
<?xml version=\"1.0\" encoding=\"utf-8\"?>
<request>
<host-name>设备名字</host-name>
<user-agent>CCTP/android3/2028</user-agent>
<client-id>设备id</client-id>
<ipv4> ip </ipv4>
<ipv6></ipv6>
<mac></mac>
<ostag>中国电信正在进行网络测试</ostag>
<local-time></local-time>
</request>

备注:ostag只能16个中文 加密自动变成设备ipv4

尝试修改ipv4这个参数,发现加密再解密后ipv4是设备的ip地址无法改变,很容易想到加密的时候对时间和ipv4做了强制替换操作。

傻瓜式修改

我把手机的wifi改为静态,自己手动设置ip地址,当然了不能产生冲突,如果同一个网关有两个相同ip的话,会导致网络错误,两个设备均无法联网。 屏幕截图 2021-11-04 054201.png 此时再hook就是我们想要的ip了。发给服务器拿ticket
用ip跟服务器换ticket,后续的登录、keep、退出操作都使用这个ticket,ticket有有效时间,退出后ticket立即失效

继续摸索

继续尝试auth、term、keep、query等接口。各个接口的作用如下:

  • auth 使用ticket 账号密码 进行登录 登陆失败无返回值,头部有错误代码ERROR_CODE
  • term 退出登录,ticket作废 无返回值 头部有错误代码 0表示成功退出
  • keep 保持登录,如果被其他设备顶掉线,那keep并不能让你恢复上线,keep成功,但是没网。
  • query 查询登陆的设备,客户端接口,利用检测最后一个设备是不是自身,来检测登录
  • state 检测登录,被顶登录检测到还是登陆状态,但是无网络。

分离出加解密的so

从jar代码中我们看见是一个名字为daproxy.so的二进制文件执行加解密操作,我们在lib中看到了这个文件。 同时,发现data/file目录下有damod一个单独的文件,后面发现damod函数load加载了该文件并返回一个int数字,是载入的内存地址(这都是后面才知道的)
尝试在ida中动态调试daproxy,发现有个检测线程,应该是360的壳,检测到调试就直接退出 具体是检测/proc/里面的/statu
尝试unidbg动调该文件也是失败的,报内存错误。
这个无奈僵持了很长时间,途深提示我重新写个apk来调用daproxy,当时没有采纳,因为没有任何android基础,完全从零开始,很久之后上手写了个demo。
需要手动开存储权限,/sdcard/内存存储根目录下放置damod(服务器传来的加密参数文件)
damod
app-debug.apk
这样就可以进行ida调试了。调试daproxy.so如何加解密
调试中我们发现so中的enc函数中v10通过内存地址调用了函数,加密就完成了,于是猜测damod本身是一个二进制的可执行文件。 屏幕截图 2021-11-04 060056.png 小伙纸耐心分析so中的load函数,发现对damod进行异或,在还原成二进制文件,存储在内存中。通过对elf特征的寻找,我们dump出了damod异或还原之后的二进制可执行文件。 屏幕截图 2021-11-09 174424.png 屏幕截图 2021-11-09 174425.png 断点后动调 屏幕截图 2021-11-09 174733.png 双击a1 屏幕截图 2021-11-09 174753.png 长度为a2 屏幕截图 2021-11-09 174821.png 用python导出

import idaapi
fp = open('C:\\dump1.so','wb')
fp.write(idaapi.dbg_read_memory(0xC60CF901,0x9904))
fp.close()

ida打开看到Code、DeCode函数,一目了然了,调用了这个so的加解密。算法还是很复杂,小伙纸猜测是ase、ras等通用算法。 屏幕截图 2021-11-04 060546.png 对了,经过我自己的多次下载多个不同的damod 并dump出来比较发现有三种damod的大小,经过分析有三套算法。
小伙纸成功解出他们学校的算法,他分析我们学校的算法可能是变种的非标准的魔改版算法,初步判断是tea+iv,aes,rsa算法。

调用

解出算法固然牛逼,但是算法太过复杂了,我决定用unidbg调用该算法,调用的过程中,发现了udpsocket的报错,证实了so内部对ipv4进行替换的工作 屏幕截图 2021-11-04 061253.png 所以解密可以成功直接调用,加密却调用失败。
后面发现传入空白进行加密,加密内容会被替换成 屏幕截图 2021-11-04 061622.png 动态调试测试加密,发现替换的函数中只是完成替换,返回值是替换之后的字符串,于是就想到hook该函数 搜索白龙大哥的文章https://reao.io/archives/90/

emulator.attach().addBreakPoint(module.base + 基址 + 1, new BreakPointCallback() {
        final RegisterContext registerContext = emulator.getContext();

        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            //System.out.println("替换函数 verifyApkSign");
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLRPointer().peer);
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, codepoint.hashCode());
            return true;
        }
    });

现学现用了hook,他是一个匿名函数,基址会改变的,猜测同一个算法的基址应该是相同的,还没测试
将返回的字符串hook成我们的字符串,此时code函数的传入参数就没用了,同时跳过了该函数的执行,即把pc寄存器改为lr否则会报错socket相关 ok完美,写了个java的登陆器 从此用网不愁 屏幕截图 2021-11-04 062223.png

进一步优化

初版的unidbg容易崩溃,经龙哥推荐了unidbg-boot-server,尝试同时运行多个so,发现注入偏移这边是不可改变的,也就说同一个java里面没法给一个so断这个位置,另一个so断另一个位置。当然创建多个虚拟机除外,那运行多个就要创建多个虚拟机,那也太麻烦了。
在测试中发现其中两个虚拟机可以同时运行,其他的均因为断点偏移位置错误而崩溃,仔细一看发现?有两个不同的Client-ID有相同的Algo-ID。然后他们的so对应的偏移位置是相同的。
好的我懂了,这两个Client-ID的加密算法是相同的,也就是服务器上有多个已经编译好的so对应多个算法,按照一定的规律分发,并没有随机或者改变。
写个py看看有哪些Client-ID的算法是一致的:

import requests
for x in range(0,100000000001):
	headers = {
		'User-Agent': 'CCTP/android3/2028',
		'Algo-ID': '00000000-0000-0000-0000-000000000000',
		'CDC-Checksum': '9f89c84a559f573636a47ff8daed0d33',
		'Client-ID': '00000000-0000-0000-0000-'+str("%012d" % x),
		'Content-Type': 'text/plain',
		'CDC-SchoolId': '10',
		'CDC-Domain': 'ctyoung',
		'CDC-Area': 'hn',
	}

	data = '00000000-0000-0000-0000-000000000000'

	response = requests.post('http://202.100.244.173:9001/ticket.cgi', headers=headers, data=data)
	#print(response.text)
	xt=response.text.find("$15838E04-05CF-4627-AAAB-8B58037E9A79]")
	if(xt==68):
		#print(response.text)
		print(x)

好的取Client-ID为00000000-0000-0000-0000-000000000005对应的Algo-ID去匹配搜索,
屏幕截图 2021-11-27 151409.png
匹配到了,都能够看到,有多个规律的Client-ID的Algo-ID是相同的。
既然如此我们又有新的思路,把相同Algo-ID的Client-ID取出,他们用的相同的so进行加解密运算,我们就可以只用一个so登录多个客户端ip(比如多拨macvlan+mwan3)。

查缺补漏

查看任务管理器发现程序在运行的过程中占用的内存越来越大直到最后崩溃,检查了代码发现,每次执行任务我就把so载入内存一遍,一直执行一直载入,难怪内存越来越大。自己创建用于注入断点的内存空间也没有释放或者复用,一直运行一直创建。
修改一下代码在初始化对象的时候载入so和创建用于注入的内存空间,并在接下来的代码重复使用,内存能维持在250M以下了。同时把不变的Algo-ID等变量定义为静态,进一步优化代码。
至此程序可以一直运行,测试了一晚上因为网络异常退出了。应该没啥问题了。 屏幕截图 2021-11-27 152839.png