2016 LCTF EASY EASY 200

2016年LCTF的第二道Mobile题,分值200,有言在先,本百分之九十九靠着自己多年酱油选手的灵感

Jadx载入样本,先判断了输入字符串的长度,然后对字符串进行处理,最后调用函数进行校验

调用Format类的函数进行处理,这里写了这么多个,肯定在后面会用到,不然出题人吃饱撑着写这么多

check()函数里显示检测了模拟器,所以直接运行在真机上,然后调用native函数进行校验

IDA载入so,先看checkEmulator()函数,这个函数并没有做其它操作,只是根据传入的值返回

同时,我们看到还有一个checkPasswd()函数

我们进行简单的分析,可以看出来中间用到了某种加密操作

在对整个校验函数有了简单的认识后,进行深入分析

sub_91C0(&v23, &unk_19D7F, &v22);
sub_7118(&v23, input_chars_new, num_33);
sub_8740(*v21 - 12);
sub_8740(v23 - 12);

在翻了几下之后,发现这几个函数并没有太大关系,我们需要关注的是

j_j_j__Z7encryptPKcj(v20, v22, *(v22 - 12));

找到v22 - 12的定义就是vInput_,v20在后面会有操作,所以应该是输出

-00000028 vInput_         DCD ?
-00000024 var_24          DCB 4 dup(?)
-00000020 var_20          DCB ?
-0000001F                 DCB ? ; undefined
-0000001E                 DCB ? ; undefined
-0000001D                 DCB ? ; undefined
-0000001C var_1C 

跟进去

这明显的Base64算法,那么分析到这里就可以回去了

最后的对比

v14 = *v20;
v15 = sub_7B10(&secret, v14);

这里调用sub_7B10,可以跟进去

int __fastcall sub_7B10(const void **a1, const char *a2)
{
    const void *v2; // r7@1
    const char *v3; // r6@1
    size_t v4; // r5@1
    size_t v5; // r4@1
    size_t v6; // r2@1
    int result; // r0@3

    v2 = *a1;
    v3 = a2;
    v4 = *(*a1 - 3);
    v5 = j_j_strlen(a2);
    v6 = v5;
    if ( v5 > v4 )
        v6 = v4;
    result = j_j_memcmp(v2, v3, v6);
    if ( !result )
        result = v4 - v5;
    return result;
}

调用memcpy()函数进行对比,那么现在关键的就是secret

使用交叉引用,找到一处对secret的调用

int sub_4C54()
{
    int result; // r0@1
    int v1; // [sp+0h] [bp-18h]@1
    char v2; // [sp+4h] [bp-14h]@1
    int v3; // [sp+8h] [bp-10h]@1

    v3 = _stack_chk_guard;
    My_create(&secret, "dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.", &v1);
    j_j___cxa_atexit(sub_6BE4, &secret, &unk_1E000);
    My_create(&dword_1E09C, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", &v2);
    j_j___cxa_atexit(sub_6BE4, &dword_1E09C, &unk_1E000);
    result = _stack_chk_guard - v3;
    if ( _stack_chk_guard != v3 )
      j_j___stack_chk_fail();
    return result;
}

从第二个字符串来看,这是Base64算法无误了

那么分析到这,我们可以逆推一下,首先将dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.用Base64还原,然后倒序

>>> temp = "ttttievahtnodiesuacebllehsatnawi!"
>>> temp[::-1]
'!iwantashellbecauseidonthaveitttt'

鉴于没有参赛我也不知道flag到底是什么

网上流传的flag:iwantashellbecauseidonthaveitttt

可是我记得前面是有调用一个Format类的一个函数做substring()操作,应该是补上固定长度的任意字符串

不过还有一点点其它的东西可以跟大家聊一聊

我们注意到

>>> temp = "iwantashellbecauseidonthaveitttt"
>>> print len(temp)
32

而我们Java层传进来的字符串应该是33位,再来翻so文件,蓦然发现有一个JNI_OnLoad()函数

粗略一看,这里面的东西还是很多的,先是做初始化,猜测这里是要获取dex文件

通过/proc/self/maps找dex文件

寻找dex的Magic Number

接下来看到了比较关键的代码,这通常用于Dex文件自篡改,一大堆乱七八糟的东西没看懂是啥

if(v91)
{
    if(!j_j_mprotect(v75, v71, 7))
    {
        *v86 = *filenamed;
        v92 = *(filenamed + 3);
        if (v92)
        {
            v93 = filenamed + 16;
            v94 = v86 + 8;
            do
            {
                *v94 = *v93;
                ++v93;
                ++v94;
                --v92;
            }while(v92);
        }
        j_j_mprotect(v75, v71, 5);
    }
}

这时候开始瞎猜,一定有篡改,而且截取的长度由33变成32,获取的是Lcom/example/ring/wantashell/Format;

再回头看该类的四个函数,主办方还是很仁慈的,并没有换三个长度都是32的函数

public class Format {
    protected String form(String input) {
        return input.substring(5, 38);
    }

    protected String fo1m(String input) {
        return input.substring(5, 36);
    }

    protected String forn(String input) {
        return input.substring(5, 39);
    }

    protected String f0rm(String input) {
        return input.substring(5, 37);
    }
}

大胆的猜测这里是将form()自篡改成f0rm()

那么现在flag是iwantashellbecauseidonthaveitttt就没什么问题了

至于反调试,可以直接patch so的调用反调试函数指令,也可以动态调试的时候下断点,patch一下内存

如果有同学感觉好的,在看secret赋值那里,就应该猜到这是Base64算法,感觉再好一点的,直接就解出了flag

Last updated