This page looks best with JavaScript enabled

“红蜻蜓”跑步APP逆向分析与数据伪造思路

 ·  ☕ 5 min read  ·  ✍️ Qfrost · 👀... views

奈何学校太shab,那么多好的体育App不用非要自己整个。经历多次手机锁屏被吞公里数后我决定来看看这垃圾软件到底是怎么写的,因此有了本文。

可行性分析

按道理,别的跑步软件都是跑完直接上传,但我们的跑步软件,跑完会有个提示:“您的跑步记录已经保存,请到我的界面进行上传”。并且在软件注销时会提示“注销后跑步记录将全部清空”。那意味着,它保存的这个跑步记录,一定保存在本地,这就为我们伪造跑步记录提供了可能性。 只要我们知道它将跑步记录如何保存、保存在什么位置、保存了什么数据、使用了什么加密方式,我们就有可能伪造跑步记录。

逆向分析与数据库伪造

先把APK找出来,用dex2jar反编译成jar文件,然后拖入jadx分析。可以看到引用了很多包

packages.png

然后adb连接手机开启监听,打开“浙理体育”APP,确定包名
adb_monitor.png

然后我们就可以将关注点放在com.example.hongqingting这个包了。但是里面还是有很多很多的方法,还需要缩小范围。 当我们跑完步,点击“结束”按钮时,会弹出一个Info “您的跑步记录已经保存,请到我的界面进行上传”,而且根据Info的样式基本可以确定这就是一串字符串而不是一个图像。那直接用jadx搜索功能搜索字符串,确定关键位置
shangchuan.png

因为这个软件有一个选项叫“自动上传跑步记录”,那可以猜测,在弹出这个Info的附近,应该有向本地文件写跑步记录和上传跑步记录的关键代码。
takedata.png

果然,可以看到,在showInfo方法前,调用了 MainActivity.db.insertrundata 向本地数据库写跑步记录,并且根据返回值新建了一条线程调用 UploadRunRecorder 方法上传了跑步记录。

分析出这些信息后,我们还需要知道它向哪个数据库写了哪些信息,有没有在写入前对信息加密,这样我们才能实现伪造。我们跟入 insertrundata 方法
insertrundata.png

可以看到,这里就是单纯的对刚刚传入的参数进行SQL写入,并且根据字段名已经可以猜出各个字段代表的含义了。然后这是一个类方法,那调用这个类方法写入的数据,必然都是写入到同一个数据库的。向上看该类的私有成员,可以看到该类是向stu.db这个数据库写入数据的
database_name.png

然后我在我手机的data目录下找到了这个数据库,提取出来用Sqlite Expert工具解析,可以在rundata table内找到跑步记录,其它表则是一些关于你的个人信息和关于学校的信息,比如运动范围经纬度之类的数据
studb.png

非常直观了,begintime和endtime分别是你跑步起止的时间戳,distance则是公里数,time就是跑步用时,也等于 endtime-begintime ,eventno则表示你这次跑步的跑步计划,801即“区域内运动”,status表示这次跑步记录是否已经上传,pointstatus恒为1
plan.png

然后我直接伪造一条记录试试看
time.png
fakerundata.jpg

可以看到,伪造成功了,并且可以成功上传。

但是还有一个问题,在数据库中我们可以看到,runway这个字段是blob类型的,也就是二进制大对象,点进去也确实看到里面存了很多很多的二进制数据,而且每一条记录都不一样。并且回过头看代码,发现它也并不是通过刚刚的insertrundata这个方法插入进来的,而且这段数据完全是杂乱无章的,而runway这个字段名,应该指的是这个字段会保存跑步路径之类的信息。根据经验判断,这段数据是被加密后从别的方法里插入进来的。
runway.png

回过头接着看代码,搜索“runway”,果然发现了一些有趣的东西
search_runway.png

doInBackground.png

可以看到,这个方法里先调用了 MainActivity.db.serachrunway 方法从数据库中得到了加密后的runway,接着将其用 “ISO-8859-1” 编码格式encode后用 teaUtils.decryptByTea 方法对其做Tea解密。将解密后的数据与其他各个字段的数据封装JSON后用 HttpUtils.HttpPostGzip 方法进行上传。 我们接着跟入 decryptByTea 方法

tea.png

可以看到,这是一个非常标准的Tea加解密算法,甚至连Key都是写死的,是标准的Tea Key。将加密后的runway送入解密函数解密即可得到类似的数据
rundata.png

通过算时间差可以知道,其实就是每隔数秒会记录当前的经纬度和配速写入runway,以这样的方法保存跑步路径信息。对于这个信息的伪造,syc写了个随机生成法向噪声坐标的函数,控制边界随机出跑步路径和配速,然后按Tea加密后写入数据库即可实现伪造。 syc yyds!

分析到这里,其实就已经把关键点分析完了,可以直接写脚本伪造数据库,然后替换原数据库实现伪造记录了,然后用软件的上传跑步记录功能将跑步记录进行上传即可。

模拟上传

在刚刚的分析过程中,其实我们也获得到了软件的上传接口,我们可以直接构造JSON post过去直接上传跑步记录,不需要大费周章的伪造一个数据库出来。因为在上传的时候,runway是上传的明文(开发者属实绝活),我们甚至都可以省去TEA加密runway的过程

但是我们要关注一下上传时别的数据有没有被做过变换

str5.png
可以看到,是否存在跑步记录由str5是否存在,而str5来自于 MainActivity.db.serachrunData() 方法的返回值 跟入这个方法看一眼

searchrunData.png
可以看到,这个方法会循环构造并返回一个列表,这个列表中每一项元素就是每一条跑步记录。而每一条跑步记录中的各项元素都是直接从数据库提取而来的,并没有做过任何变换。那我们就可以直接用前面分析出的上传接口构造post包上传即可

1
String HttpPostGzip = HttpUtils.HttpPostGzip(String.valueOf(MainActivity.db.serachschooladdress()) + "Api/webserver/uploadRunData", "{'begintime':'" + str5.split(",")[0] + "','endtime':'" + str5.split(",")[1] + "','uid':'" + serachruntimes.split(",")[1] + "','schoolno':'" + serachschoolno + "','distance':'" + valueOf + "','speed':'" + valueOf2 + "','studentno':'" + serachruntimes.split(",")[0] + "','atttype':'" + serachatttype + "','eventno':'" + str7 + "','location':'" + serachrunway + "','pointstatus':'" + str10 + "','usetime':'" + str6 + "'}");

已跑公里数查询

这个软件查询自己已经跑过的公里数这个功能都存在问题是我没想到的。但是根据测试发现,旧版的安卓系统是可以正常查询的,分析原因大概是查询后弹出的信息框用了比较老的API,这个API在新版的安卓系统中已经被移除了。那既然软件本身是带有这个查询功能的,意味着我们可以通过逆向的手段将其查询的逻辑扒出来自己模拟app行为进行查询

通过对老APP的正常查询可知,在显示已跑公里数前会有“本学期已锻炼”这六个字。 那直接jadx搜索这六个字,就可以来到关键函数
getRunDataSummary.png

同样的,我们得到了服务器的API接口和发送的数据

1
String HttpPostGzip = HttpUtils.HttpPostGzip(String.valueOf(MainActivity.db.serachschooladdress()) + "Api/webserver/getRunDataSummary", "{'studentno':'" + serachruntimes.split(",")[0] + "','uid':'" + serachruntimes.split(",")[1] + "'}");

那我们就可以依葫芦画瓢,构造一个post包进行查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import requests, gzip, json
url = "http://***.***.***.***:****/DragonFlyServ/Api/webserver/getRunDataSummary"
data = {
    'studentno':'<Your Student ID>',   # 学号
    'uid' :'<Your Cookie>'             # uid  可以在stu.db/school中查到
}
data=str(data)
data=data.encode('utf-8')
data = gzip.compress(data)             # 注意要做Gzip压缩

res = requests.post(url, data=data)
print(json.loads(res.text)["m"])
# 校内定向跑:1
# 区域内运动:9    你已经跑了的公里数
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer