geek-10h-wp-apk
这是Syclover组办的成信校内第十届geek大赛的安卓部分wp,面向新生,主要是两个方向的题四道题,这里记录了自己的wp,前面加
%%
表示是相关的知识点,里面可能有的理解不正确,希望各位发现的及时评论指正。
链接:⬇⬇⬇⬇
链接:https://pan.baidu.com/s/1HvQh0tHPCJcsk_sd0GjmwQ
提取码:tq9o
复制这段内容后打开百度网盘手机App,操作更方便哦
其实安卓基础部分网上已经有很多,可以自行搜索,这里只是出现的在两类题目之间简单说明,
具体关于各层和使用工具的更深层次细节可以自行了解。
%%apk文件分析
最主要一点是安卓文件,即apk文件,内部存在两个资源区,smali储存为代码部分,res储存为数据,
我们的 Android Killer 可以分析整个apk,但是由于这个工具真的比较老了,一般会在分析反汇编卡死,我们这时候直接退出,然后重新打开ak,载入apk文件就好,然后它可以加载到res区u,但是无法加载smali区,我目前就是用它当作查看res层的工具,
smali层通过jdb或者gda来看,平时更喜欢gda的界面一些。然后一般可以看到对应文件目录下出现的MainActivity及后缀$的一系列都是apk的主要运行程序,我们只分析这些即可。
Sign_in
我们看到关键字符,找到了判定是否正确的位置:
然后发现这就是输入的字符串经过base64加密,然后和一串字符对比,这串字符在以0x7f0b0029表示,但我们是找不到的,不管此时使用的是jdb还是gda,因为这串字符 保存在res区,这两个工具只是分析smali区,
我们使用ak中的搜索,我们搜索“0x7f0b0029”然后定位到“sign_in”,再继续搜索就可以找到密文,
我们直接将flag前几位“Syc{”base64加密,取前几位,然后ak中搜索也可以。
然后直接在线工具或者python2中的字符串解密方法的以得到flag:
#python2
st1 = 'U3lje1NpOW5fMW5fSTNfRTRzeSF9'
print(st1.decode("base64"))
#python3
import base64
st1 = 'U3lje1NpOW5fMW5fSTNfRTRzeSF9'
print(base64.b64decode(st1))
#Syc{Si9n_1n_I3_E4sy!}
蒋学姐的秘密
将apk文件载入gda,翻一翻找到关键判定函数:
然后然后发现程序逻辑,p0判定,然后p1是flag,判定时括号内的部分是由p0再函数中生成的。我们先看p0的值,一般运用到的程序参量都会再buildconfig中找到:
然后我们在ak中搜索:
然后得到p0的值为:“Syclover"
然后我们分析生成的函数:
这个程序首先将参数转化为对应十六进制的编码,然后使用md5加密,然后判定是否小于0和大于16,分别执行不同的操作,最后得到一个字符串返回。
我们先得到p0的对应编码;
p2 = 'Syclover'
p3 = ''
for i in range(len(p2)):
p3 += str(hex(ord(p2[i]))).replace('0x','')
print (p3)
#5379636c6f766572
然后我们使用在线工具进行md5加密,得到:73BC1836984E834458C8061527944AEE
然后套上flag格式即可。
%%so层-jni
后面两个安卓题会涉及到so层,我们需要了解一些关于jni的知识:
jni,Java Native Interface;
我们可以简单理解为是java和c/c++之间的转换器
apk的主函数部分是java编写,而可调用的so层是c/c++,两者之间的数据传递和处理函数调用等,我们就需要了解jni
另外一点,apk文件其实是一种压缩包的形式,我们可以使用普通的解压工具解压开,其内部我们可以看到res层,so层在对应的lib文件夹下,注意要打开arm系列的文件夹内的so文件,其中的arm后的后缀表示的是86位/64位,使用对应的ida打开分析即可。
遇到这种代码,即为载入so层文件的意思,某些调用的函数就不会在smali区分析出来,而是需要ida查看so层对应文件。
在ida中查看的函数以Java_xxx开头,如下:
关于jni的逆向,下面是一些小注意点
- 载入函数会有一个参数a1,那并不是参数,而是接口指针,
- 在函数调用时使用的参数a2,是对某个对象的引用,或是对java类的引用,
- 我们将a1设置为对应类型即可,将光标选中a1,单击Y,然后改变类型为:
JNIEnv*
- 在a3开始才是函数真正的参数。
另注函数整理
这里写出几个在以下题目出现的函数或调用的方法:
- CallMethod:从本地方法中调用java实例,中间的类型标识返回值类型,参数a1还是接口指针,参数a2会确定该调用的处理方式,其后就是参数,
- CallMethod (JNIEnv*en v, jobject obj , …)
- GetStringUTFChars:返回指向字符串的utf-8字符数组指针。
- GetStringUTFChars (JNIEnv*env, jstring string, jboolean *isCopy)
- NewObject:构造新的java对象。
- NewObject (JNIEnv *env , jclass clazz, jmethodID methodID, …
正在尝试重新连接
载入gda,翻看MainActivity及相关函数:
这里是判定对错的关键函数,我们看到是将一个获取到的数据和另一串数据一起传入check
函数,这个函数返回1为正确。
获取到的数据,一般来说这应该就是我们输入的数据,即flag。
这里是在在未连接到网络时,会出现的话。
这里出现了一个网址,我们们进入后得到一个字典:
{"key":"Syclover",
"data":["33","28","21","20","93","13","13","96","0","11","66","27","51","42","10","12","1","85","41","38","34","23","60","33","23","86","2","94","2","90","4"]}
然后继续分析并没有其他很有意义的代码,我们去寻找那个判定对错的关键函数,然后发现:
但我们可以看到在此之前:
这是调用了一个so层,其名为native-lib,而我们的关键的这个函数就在这个so层文件内,要打开arm系列的文件然后v8a或v7a是代表64位/86位,我这里分析的是v7a,其实内部的函数都是一致的,我们使用ida打开。
首先定位到,这个函数应该要返回ture,我们找到应该要返回1,则看到了关键的判定位置。
我们点击某个参量高亮后查看之前的赋值,或是单击X查看交叉调用。可以确定判定的两个数组,其中一个为a3异或运算后生成的,一个为a4直接生成的数组,但这个位置有些奇怪:
两个变量分别得到了参数a4的两部分,或者说a4像一个列表或是字典一样a4[0],a4[1]分别转化为object类型赋值给两个变量,
其实到这里就差不多思路清晰了,a3是原本传入的第一个参数,即为我们的flag,第二个参数我们可以想到之前我们得到了一个字典,由两部分,所以是由flag异或后和字典内的列表 (即a4[1]) 一致。
我们在关注异或部分:
其中的v20即为flag,我们查看v19:
其实v19就和a4[0]一致,然后通过函数处理了,我们查看:
这个函数代表了一个倒序处理,将a4[0]进行倒序生成了一个新的数组。即,a4[0]倒序后的数组,和flag异或得到的值,和a4[1]的列表相等,我们可以写出脚本:
arr = [33,28,21,20,93,13,13,96,0,11,66,27,51,42,10,12,1,85,41,38,34,23,60,33,23,86,2,94,2,90,4]
st1 = 'Syclover'[::-1]
for i in range(31):
print(chr(ord(st1[i%8]) ^ arr[i]),end='')
#Syc{1nt3rn4t_Is_s0_INtEre3t1n9}
其实这个算法不难,我们只是需要一些关于jni的知识,
师傅的hint是关于动调so,动态应该是会更加深一些理解,但是过程配置有些复杂,我使用的静态分析的方式。
little case
载入gdb,先查找关键位置:
这个程序流程,
首先是获取输入的字符串,然后载入第一层判定,这里直接输出的是“Wrong",这里应该要返回turn,然后取反是fault,之后将字符串剪切只保留5–20位字符(因为前包后不包应是4–19,而且索引从0开始字符应该是5–20位),然后再载入另一个函数判定,返回值来决定是否打印出“you have sloved it !”,所以这个值应该要返回turn。
我们先分析第一个函数:
首先确定了flag长度为21,然后限制了flag格式为”Syc{“最后为”}“,
然后最为关键的位置是判定flag中间部分,要为数字或者在97–102之间,其实就是为数字或者a–f之间,
然后下一个判定,我们发现在smali区显示为null:
然后我们就考虑是在so层内:
而且我们回看这个函数的参数,是flag的5–20位,即为括号内的部分
首先前面这些都是预先设置,但是要注意func1和func3的调用位置,这两个调用分别代表了加法和乘法。
然后将flag的中间段分隔为4*4,我们为了记住他们的顺序简记为A1–A4,另外要注意一点,这里的更新,不是得到字符串对应的编码,而是直接转化为数,类似int(‘f’,16)这种操作,所以我们的flag也在限制要求在16进制数中,
然后我们就看到了这样一个解密过程,首先定义数字2的位置和我们定义A1–A4位置是一样的,所以那个参数就是2。
而且数组453后面对应的选值,453[0]=4,453[1]=5,453[2] = 3,
然后参数调用的时候参数:
- a1:接口指针
- a2:对类对象或者java库调用,
- a3:设置函数的运算处理
- a4,a5:参数,
想在后面注释的一样,得到后面的运算过程,然后进入下层的判定。
得到四个方程式,我们解出来就可以得到A1–A4,然后接起来就是flag了。有以下脚本:(z3解方程框架:
from z3 import *
a1, a2, a3, a4 = Ints('a1, a2, a3, a4')
solver = Solver()
solver.add()
if solver.check() == sat:
print(solver.model())
else:
print('unsat')
写出脚本得到四个值:
from z3 import *
a1, a2, a3, a4 = Ints('a1, a2, a3, a4')
solver = Solver()
solver.add(a1 + 2 * a2 * a3 - a4 == 0xFA52560D)
solver.add(a1 * a3 + 4 * a2 - 5 * a4 == 0x134C22B5)
solver.add(a1 * a2 + 4 * a4 - a3 == 0x117164E1)
solver.add(a4 * a3 + 3 * a1 - 2 * a2 == 0x36AD4421)
if solver.check() == sat:
print(solver.model())
else:
print('unsat')
然后得到他们对应的十六进制,套上flag格式即可:
a = [6716,43570,48195,19035]
for i in range(4):
print(str(hex(a[i])).replace('0x',''),end='')
#Syc{1a3caa32bc434a5b}