181124 逆向-2018“柏鹭杯”厦大邀请赛初赛(Re1、2)

总体来说逆向的题目质量挺高~感觉学到了不少东西=-=
就是第三题放题时间有点晚233没有公网的情况下做题难度确实比较大
欢迎各位师傅指出疏漏错误和交流~

Re1

JAVA层

用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

除了动态调试以外,还有两个方法在不进行重打包的情况下拿到签名:

  1. 编写应用取出“第三方”APK的签名
  2. Hook

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库来反编译
然后就掉到出题人的坑里去了_(:з」∠)_

Native层(ARM)

首先在左侧函数列表中寻找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

Native层(X86)

打开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");
                        }
                    }
            );

Re2

这题的代码量小了很多,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的格式不一致,于是没有在断网的情况下做出来
(:з」∠)_
过两天有空再搞他

你可能感兴趣的:(CTF,CrackMe,Android)