总体来说逆向的题目质量挺高~感觉学到了不少东西=-=
就是第三题放题时间有点晚233没有公网的情况下做题难度确实比较大
欢迎各位师傅指出疏漏错误和交流~
用JEB查看反编译代码,JAVA层做了如下操作
有点绕,不知道是出题人故意还是无意地,这个处理写的很蛋疼
除了上述列出的字符以外都是保留不变的
output的范围是彼此分开的,因此可以写出该处理的反函数
def decode(x):
if(x>48 and x<57):
return x+1
elif(x>97 and x<97+26):
return 123-x+97
return
elif(x>=65 and x<=65+26):
return 91-x+65
else:
return x
接下来将处理过后的输入和this.b()
送入native函数this.b(String, String)
中
this.b()
通过getPackageInfo.signatures
来获得应用签名,是一个固定值
最简单的方法当然就是直接JEB动态调试挂上去,但是运行起来发现根本没有输入框,仅有一个Check按钮
查看代码感觉没有什么隐藏的,于是找来res/layout/activity_main.xml
发现
把它删掉再重打包就可以看到输入框了,当然这样重打包会导致签名不一样233
但是不重打包的话,又会由于输入内容为空而不会进入到b方法的调用中,所以动态调试就陷入一个死循环的尴尬了233
除了动态调试以外,还有两个方法在不进行重打包的情况下拿到签名:
Hook对我来说比较熟悉,所以比赛的时候用的是Xposed:
XposedHelpers.findAndHookMethod(
"com.example.crackme.MainActivity",
lpparam.classLoader,
"onClick",
View.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
String md5 = (String)XposedHelpers.callMethod(param.thisObject, "b");
Log.e("test", "get md5: " + md5);
}
}
);
这样在点击按钮的时候直接调用b方法即可
代码则是直接照抄,将getPacakagename()
的调用改成包名即可(相关的md5及hex_decode也直接照抄,就不另附了)
try {
String sig = this.b(v0.getPackageInfo("com.example.crackme", PackageManager.GET_SIGNATURES).signatures[0].toByteArray());
Log.e("test", sig);
}
catch(Exception e) {
return ;
}
通过adb logcat -s test
即可获得签名
然后下一步拖lib文件夹中的so库来查看native函数
native函数的动态链接库so通常有ARM和x86两种架构,常见的ARM架构安卓平台为手机,常见的x86架构安卓平台则为平板和PC端模拟器
一般情况下,IDA反编译ARM的链接库的可读性要更好一些,这是以前被某位大佬指导过的,因此最好选用armeabi-v7a
文件夹下的so库来反编译
然后就掉到出题人的坑里去了_(:з」∠)_
首先在左侧函数列表中寻找JAVA_com_example_crackme_b
函数,这是因为导入函数是有命名规则的
然而一无所获,那么只可能是在JNI_OnLoad
函数中动态注册了
j_ptrace(0, 0, 0, 0);
v6 = 0;
v2 = 65540;
if ( ((int (__fastcall *)(JNIEnv *, JNIEnv **, signed int))(*v1)->FindClass)(v1, &v6, 65540)
|| (v3 = v6,
(v4 = ((int (__fastcall *)(JNIEnv *, const char *))(*v6)->FindClass)(v6, "com/example/crackme/MainActivity")) == 0)
|| ((int (__fastcall *)(JNIEnv *, int, char **, signed int))(*v3)->RegisterNatives)(v3, v4, off_4004, 1) )
这里能识别到FindClass和RegisterNatives是因为我将参数a1的类型修正为了JNIEnv*
类型,对其右击选择set lvar type
即可
注意上面有个ptrace
反调试调用,如果之后想要动态调试的话需要把这里处理掉–最简单的方法就是直接NOP掉,然后放入/data/data/com.example.crackme/lib
中替换掉原来的lib(需要具有root权限)
另:x86架构模拟器不能调试ARM的动态链接库
大多数的模拟器都是x86架构的,目前我仅知道AndroidStudio下可以创建ARM架构的模拟器,并且速度奇慢无比
这里想要详细了解动态注册Native函数的话可以自己百度一波资料,我们直接找到结构体off_4004的第三个参数过去即可
这里的参数同样要修改为JNIEnv*
类型,可以看到一些去参数的JNI方法
后面的反编译没啥好说的,慢慢阅读即可
主逻辑为检查格式:YM-AAAABBBB-CCCCDDDD
将AAAABBBBCCCCDDDD
保存到一个16字节的数组中
注意这里A和B是顺序传入,而C和D是逆序传入的
然后将参数1–也就是直接传入的签名的md5做一个hexdecode,即32字节的字符串解码成16字节的数组
最后用四个8字节数组进行运算,2个数组负责前8字节、2个数组负责后8字节:
result[i]==~((~(input[i]^md5[i]))^data[i])
其中~
表示按位取反运算,和!
、^0xff
效果在python中相同(!
表示非,但python中对int使用与~
作用一样。而C语言中!一个非零数都会得到0,因为所有整数都可以看做布尔型)
公式很简单,data和result都可以直接提取出来,之前也拿到了签名,因此直接逆运算即可
def decode(x):
if(x>48 and x<57):
return x+1
elif(x>123-25 and x<123):
return 123-x+97
elif(x>=91-25 and x<=91):
return 91-x+65
else:
return x
md5 = bytes.fromhex("5F01C8622AA0A69B8ABEC3AD9070BA56")
data1 = [ 56, 116, 176, 23, 86, 221, 209, 188]
data2 = [ 156, 211, 171, 215, 150, 37, 196, 68]
data1_result = [ 85, 65, 79, 70, 76, 79, 65, 113]
data2_result = [ 67, 56, 81, 74, 49, 103, 73, 75]
flag = [0 for i in range(20)]
flag[0] = ord('Y')
flag[1] = ord('O')
flag[2] = ord('-')
flag[11] = ord('-')
for i in range(8):
flag[3+i] = 0xff&((~(data1[i]^(0xff^data1_result[i])))^md5[i])
for i in range(8):
flag[19-i] = 0xff&((~(data2[i]^(0xff^data2_result[i])))^md5[i+8])
print([chr(i) for i in flag])
output = [chr(decode(i)) for i in flag]
print("".join(output))
这里的难点在于数据处理的时候ARM似乎很喜欢使用负值来作为下标,而IDA对局部变量的切分都是根据base来的,也就是说经常会出现倒着使用数组的情况
例如对于a[20],a[i]在C语言及其反编译中通常表示为*(a+i*1)
,而ARM总是会使用*(&a[20] - i)
的方式来调用,由于符号丢失,所以IDA往往会将&a[20]
作为一个变量进而增加理解难度
最恐怖的就是这一段
do
{
((_BYTE *)&v60 - v13)[3] = input2[v13 + 12];
++v13;
}
while ( v13 != 8 );
这里的v60从栈帧可以看到
int int1; // [sp+64h] [bp-30h]
int int2; // [sp+68h] [bp-2Ch]
int v59; // [sp+6Ch] [bp-28h]
int v60; // [sp+70h] [bp-24h]
正好是int1后面12个字节的地方,然后[3]表示+3即第四个字节
即int1[12-v13+3]
,随着v13的自增,将input2的后8个字节逆序写入int1的7-15个字节
比赛的时候我没发现逆序,所以搞了半天都不对
最后回头去看了一眼x86的so,发现真简单–简单到我甚至以为这是两个算法,直到最后发现算出来的结果仅仅是后半截逆序的区别而已orz
打开x86就会发现,各种日狗的负数下标、按位取反都没有了
取而代之的就是一堆逐字节异或,将数据细心地整理一下拿出来即可
hint中x86就是用来说明这一点的阿23333真是良苦用心
md5 = bytes.fromhex("5F01C8622AA0A69B8ABEC3AD9070BA56")
data1 = [0xc7, 0x8b, 0x4f,0xe8, 0xa9, 0x22, 0x2e, 0x43]
data2 = [0xBB, 0x3b, 0xda, 0x69, 0x28, 0x54, 0x2c, 0x63]
result = [-86, -66, -1^0x4f, -71, -77, -80, -66, -114, -68, -57, -82, -75, -50, -104, -74, -76]
flag = [0 for i in range(20)]
flag[0] = ord('Y')
flag[1] = ord('O')
flag[2] = ord('-')
flag[11] = ord('-')
for i in range(8):
flag[3+i] = (result[i]^data1[i]^md5[i])&0xff
for i in range(8):
flag[i+12] = (result[15-i]^data2[i]^md5[15-i])&0xff
print(flag)
print("".join([chr(decode(i)) for i in flag]))
这里的第二段其实也有一个逆序操作,但是在整理的过程中毫无意外地注意到了,然后修正一下即可得到flag
关于调试的步骤网上有很多教程、之前的博客里也有,就不赘述了
如果需要在模拟器中调试so,那么需要使用x86的so,并将兼容模式关掉
否则会报B747B9D2: got unknown signal #33 (exc.code 21
的错误,这个signal #33查了一下好像是线程切换相关的异常,猜测是兼容模式中通过x86来模拟ARM啥的,不是很懂_(:з」∠)_
另外由于输入内容需要重打包,而校验flag需要正确的签名,因此可以重打包之后通过Hook或者Patch来修改b方法的返回值从而正确校验check
比方说Xposed
XposedHelpers.findAndHookMethod(
"com.example.crackme.MainActivity",
lpparam.classLoader,
"b",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
param.setResult("5F01C8622AA0A69B8ABEC3AD9070BA56");
}
}
);
这题的代码量小了很多,Java层操作了一个AES解密然后比较前8个字节
懒得折腾CBC、IV向量啥的,直接动调拿到结果
“A1B8E6PJ”
然后又是Native方法check
与上题一样是动态注册,找到结构体的第三个内容跳过去,发现不可读
刚开始以为是壳,但是看了一波JNI_OnLoad和.init_array都没有可疑的函数
最后注意到了地址是奇数,即最低位为1
这说明这里的指令为Thumb状态
Thumb是ARM体系结构中一种16位的指令集。Thumb指令集可以看作是ARM指令压缩形式的子集,它是为减小代码量而提出,具有16bit的代码密度。Thumb指令体系并不完整,只支持通用功能,必要时仍需要使用ARM指令,如进入异常时。其指令的格式与使用方式与ARM指令集类似,而且使用并不频繁,Thumb指令集作一般了解。
简单来说就是ARM架构中的另一种解释方式,在IDA中可以通过快捷键Alt+G将低位修改成1来切换
这里默认情况下是CODE32,即ARM指令集
所以修改为CODE16,即Thumb指令集试试,发现可读了
再重新按P来CreateFunction即可,这一点绕过以后算法很简单,只是一个Table的累加和而已
不再多说,直接附上代码
s = "6U8DPTKtVHSOYE8IFwpl1T8OCrqfog17"
t = "0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm"
sum = 0
indexs = []
for i in s[::-1]:
indexs.append(t.index(i))
print(indexs)
flag = ""
for i in indexs:
for x in range(32, 127):
if((sum+x)%62==i):
sum += x
break
flag += chr(x)
print(flag[::-1])
至于RE3_(:з」∠)很容易看出来是Lua_JIT的打包版本,需要从中Dump出JIT的字节码然后通过工具反编译
跟直接做过的JIT的格式不一致,于是没有在断网的情况下做出来(:з」∠)_
过两天有空再搞他