BUUCTF 刷题笔记——Reverse 1

easyre

  • 第一道题,题目提示非常简单的逆向并提供一个 zip 压缩包,下载本地解压后是一个 exe 可执行文件。尝试用 IDA 反编译,发现 flag 出来了。

    感谢善待新人

reverse1

  • 依然给了一个压缩文件,解压后依然是一个 exe 可执行文件,再次尝试用 IDA 反编译,这次没有一眼看到 flag 了,甚至连主函数都没有。于是 Shift + F12 找找特别的字符串,发现了 this is the right flag!

    找到使用该字符串的位置,发现是如下 sub_1400118C0() 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    __int64 sub_1400118C0()
    {
    char *v0; // rdi
    __int64 i; // rcx
    size_t v2; // rax
    char v4[36]; // [rsp+0h] [rbp-20h] BYREF
    int j; // [rsp+24h] [rbp+4h]
    char Str1[224]; // [rsp+48h] [rbp+28h] BYREF
    __int64 v7; // [rsp+128h] [rbp+108h]

    v0 = v4;
    for ( i = 82i64; i; --i )
    {
    *(_DWORD *)v0 = -858993460;
    v0 += 4;
    }
    for ( j = 0; ; ++j )
    {
    v7 = j;
    if ( j > j_strlen(Str2) )
    break;
    if ( Str2[j] == 111 )
    Str2[j] = 48;
    }
    sub_1400111D1("input the flag:");
    sub_14001128F("%20s", Str1);
    v2 = j_strlen(Str2);
    if ( !strncmp(Str1, Str2, v2) )
    sub_1400111D1("this is the right flag!\n");
    else
    sub_1400111D1("wrong flag\n");
    sub_14001113B(v4, &unk_140019D00);
    return 0i64;
    }
  • 这段程序就是读取用户输入数据,并于内部字符串 Str2 作比较,比较正确说明是正确 flag。值得注意的是用于比较的内部字符串在参与比较前作了以下操作:

    1
    2
    if ( Str2[j] == 111 )
    Str2[j] = 48;

    将 ASCII 码值为 111 的字符替换为码值为 48 的字符,即将 o 替换为 0

  • IDA 中可以查看字符串 Str2 的值为 {hello_world},替换后即为 {hell0_w0rld}。根据 BUU 的提示,加上 flag 前缀即可提交。有人不看提示直接提交喜提错误我不说是谁。

reverse2

  • 本题的文件没有后缀名,不过因为惯性还是直接 IDA 反编译,本次含有主函数了,并且有 flag 出没:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    int __cdecl main(int argc, const char **argv, const char **envp)
    {
    int stat_loc; // [rsp+4h] [rbp-3Ch] BYREF
    int i; // [rsp+8h] [rbp-38h]
    __pid_t pid; // [rsp+Ch] [rbp-34h]
    char s2[24]; // [rsp+10h] [rbp-30h] BYREF
    unsigned __int64 v8; // [rsp+28h] [rbp-18h]

    v8 = __readfsqword(0x28u);
    pid = fork();
    if ( pid )
    {
    waitpid(pid, &stat_loc, 0);
    }
    else
    {
    for ( i = 0; i <= strlen(&flag); ++i )
    {
    if ( *(&flag + i) == 105 || *(&flag + i) == 114 )
    *(&flag + i) = 49;
    }
    }
    printf("input the flag:");
    __isoc99_scanf("%20s", s2);
    if ( !strcmp(&flag, s2) )
    return puts("this is the right flag!");
    else
    return puts("wrong flag!");
    }
  • 与前一关类似,也是通过判断 ASCII 码值来做一些替换,不过本题是将字符 i 与字符 r 都替换为字符 1。查询内部字符串 flag 如下,由于程序是通过首字符地址来访问的 C 类型字符串,反编译时会分隔开来,因此完整字符串为 {hacking_for_fun}

    替换后为 {hack1ng_fo1_fun},因此 flagflag{hack1ng_fo1_fun}这边每次都要自己组 flag,老是忘记。

内涵的软件

  • 本题给的文件是一个 32 位的可执行文件,因此使用 32 位版的 IDA 打开,主函数仅仅调用了一个 main_0() 函数而已,因此查看该函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    int __cdecl main_0(int argc, const char **argv, const char **envp)
    {
    char v4[4]; // [esp+4Ch] [ebp-Ch] BYREF
    const char *v5; // [esp+50h] [ebp-8h]
    int v6; // [esp+54h] [ebp-4h]

    v6 = 5;
    v5 = "DBAPP{49d3c93df25caad81232130f3d2ebfad}";
    while ( v6 >= 0 )
    {
    printf(&byte_4250EC, v6);
    sub_40100A();
    --v6;
    }
    printf(asc_425088);
    v4[0] = 1;
    scanf("%c", v4);
    if ( v4[0] == 89 )
    {
    printf(aOd);
    return sub_40100A();
    }
    else
    {
    if ( v4[0] == 78 )
    printf(&byte_425034);
    else
    printf(&byte_42501C);
    return sub_40100A();
    }
    }

    虽然挺长一段代码,但实测开头的那个长得像 flag 的局部变量 v5 里面便是 flag,不过将 DBAPP 换成 flag 即可。

新年快乐

  • 本题文件为 32 位可执行文件,但是在 IDA 中打开后却发现仅有两个函数,代码也奇奇怪怪找不到啥关键字,而且程序大部分数据所在的段名都含有此前没见过的 UPX

  • 基本可以确定,碰上个人第一次接触的加壳程序了,即类似压缩文件不过解压过程在执行时在内存中自动完成,因此程序可正常执行但是却无法反编译出多少有效信息,加壳主要用于压缩与加密。不过还好只是入门级的 UPX 压缩壳,可以去 他们官网 下载加壳程序,使用 -d 参数即可完成脱壳:

    1
    upx -d [文件路径]

    看到如下界面即脱壳成功,此时文件就只是一个普通的可执行文件了。当然脱壳的方法有很多,这里暂时不作过多考究,本小白还是慢慢来

  • 脱壳之后即可在 IDA 反编译出原程序了,主函数如下,有众多关键字 flag 出没:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int __cdecl main(int argc, const char **argv, const char **envp)
    {
    char Str2[14]; // [esp+12h] [ebp-3Ah] BYREF
    char Str1[44]; // [esp+20h] [ebp-2Ch] BYREF

    __main();
    strcpy(Str2, "HappyNewYear!");
    memset(Str1, 0, 32);
    printf("please input the true flag:");
    scanf("%s", Str1);
    if ( !strncmp(Str1, Str2, strlen(Str2)) )
    return puts("this is true flag!");
    else
    return puts("wrong!");
    }

    程序依然是老流程,读取数据并与内部字符串 HappyNewYear! 作比较,不过这次没对内部字符串做任何修改,因此组合后的 flag{HappyNewYear!} 便是 flag

xor

  • 下载文件,解压发现含有 _MACOSX,因此应该是 MAC 来的文件,使用 Exeinfo PE 工具小查一下,确定是 64 位的 MAC 可执行程序,并且没有加壳。

    直接 IDA 反编译,发现主函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int __cdecl main(int argc, const char **argv, const char **envp)
    {
    int i; // [rsp+2Ch] [rbp-124h]
    char __b[264]; // [rsp+40h] [rbp-110h] BYREF

    memset(__b, 0, 0x100uLL);
    printf("Input your flag:\n");
    get_line(__b, 256LL);
    if ( strlen(__b) != 33 )
    goto LABEL_7;
    for ( i = 1; i < 33; ++i )
    __b[i] ^= __b[i - 1];
    if ( !strncmp(__b, global, 0x21uLL) )
    printf("Success");
    else
    LABEL_7:
    printf("Failed");
    return 0;
    }
  • 虽然源码很容易就得到了,不过相比之前,本题对 flag 的处理更加有趣一些,首先读取用户输入,并且长度必须为 33,即 flag 会有 33 个字符。获取用户输入后程序会将输入字符串的后 32 位逐个与前一位作异或运算,计算后的结果与内部字符串相等才是 flag。也就是说 flag 经过一轮异或后会获得内部字符串,又由于对相同的数据异或两次数据就会复原,因此直接对内部字符串作一轮异或操作即可获得 flag。而内部字符串 global 如下,Shift + e 获取字符数组,取数值便于计算:

  • 然后写个脚本就可以计算出 flag 了,脚本如下。值得注意的是脚本中的异或运算结果不应存入数组中,因为原计算是基于前字符已经运算完成的情况下进行的,因此复原过程中的每个数据都应保持原样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    glb = [0x66,0x0A,0x6B,0x0C,0x77,0x26,0x4F,0x2E,
    0x40,0x11,0x78,0x0D,0x5A,0x3B,0x55,0x11,
    0x70,0x19,0x46,0x1F,0x76,0x22,0x4D,0x23,
    0x44,0x0E,0x67,6,0x68,0x0F,0x47,0x32,0x4F,0]

    s = chr(0x66)
    for i in range(1,33):
    s += chr(glb[i] ^ glb[i-1])

    print(s)

    计算出的 flagflag{QianQiuWanDai_YiTongJiangHu}千秋万代,一统江湖

helloword

  • 是个 apk 文件!没想到这么快就来到安卓了,直接丢进 IDA 里看看,不过反编译 apk 需要在打开时选择 APK Android Package 才行。

  • 反编译出来相当多东西,没搞过安卓看到真的令人恐惧,直接 Shift + F12 查找字符串,字符串也是一大堆,所幸可以使用 Ctrl + F 搜索。结果直接就找到 flag,感谢饶命。

reverse3

  • 再次回到普通的 exe 文件,使用 Exeinfo PE 打开查看一下先。是一个 32 位可执行文件,而且没加壳。

  • 那好说,直接丢进 IDA 反编译,主函数依然仅仅调用了 main_0() 函数而已,因此直接查看该函数,有关键词 flag 出没:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    int __cdecl main_0(int argc, const char **argv, const char **envp)
    {
    size_t v3; // eax
    const char *v4; // eax
    size_t v5; // eax
    char v7; // [esp+0h] [ebp-188h]
    char v8; // [esp+0h] [ebp-188h]
    signed int j; // [esp+DCh] [ebp-ACh]
    int i; // [esp+E8h] [ebp-A0h]
    signed int v11; // [esp+E8h] [ebp-A0h]
    char Destination[108]; // [esp+F4h] [ebp-94h] BYREF
    char Str[28]; // [esp+160h] [ebp-28h] BYREF
    char v14[8]; // [esp+17Ch] [ebp-Ch] BYREF

    for ( i = 0; i < 100; ++i )
    {
    if ( (unsigned int)i >= 0x64 )
    j____report_rangecheckfailure();
    Destination[i] = 0;
    }
    sub_41132F("please enter the flag:", v7);
    sub_411375("%20s", (char)Str);
    v3 = j_strlen(Str);
    v4 = (const char *)sub_4110BE(Str, v3, v14);
    strncpy(Destination, v4, 0x28u);
    v11 = j_strlen(Destination);
    for ( j = 0; j < v11; ++j )
    Destination[j] += j;
    v5 = j_strlen(Destination);
    if ( !strncmp(Destination, Str2, v5) )
    sub_41132F("rigth flag!\n", v8);
    else
    sub_41132F("wrong flag!\n", v8);
    return 0;
    }

    程序在读取用户输入后将输入数据丢进了 sub_4110BE() 函数做运算,然后把运算后数据放进一个 for 循环中逐个字符的码值加上索引,最终与内部字符串 Str2 相同则用户输入数据为 flag。这个 sub_4110BE() 函数令人在意,打开后发现其仅调用了一个 sub_411AB0() 函数,该函数内容实在有些复杂:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    void *__cdecl sub_411AB0(char *a1, unsigned int a2, int *a3)
    {
    int v4; // [esp+D4h] [ebp-38h]
    int v5; // [esp+D4h] [ebp-38h]
    int v6; // [esp+D4h] [ebp-38h]
    int v7; // [esp+D4h] [ebp-38h]
    int i; // [esp+E0h] [ebp-2Ch]
    unsigned int v9; // [esp+ECh] [ebp-20h]
    int v10; // [esp+ECh] [ebp-20h]
    int v11; // [esp+ECh] [ebp-20h]
    void *v12; // [esp+F8h] [ebp-14h]
    char *v13; // [esp+104h] [ebp-8h]

    if ( !a1 || !a2 )
    return 0;
    v9 = a2 / 3;
    if ( (int)(a2 / 3) % 3 )
    ++v9;
    v10 = 4 * v9;
    *a3 = v10;
    v12 = malloc(v10 + 1);
    if ( !v12 )
    return 0;
    j_memset(v12, 0, v10 + 1);
    v13 = a1;
    v11 = a2;
    v4 = 0;
    while ( v11 > 0 )
    {
    byte_41A144[2] = 0;
    byte_41A144[1] = 0;
    byte_41A144[0] = 0;
    for ( i = 0; i < 3 && v11 >= 1; ++i )
    {
    byte_41A144[i] = *v13;
    --v11;
    ++v13;
    }
    if ( !i )
    break;
    switch ( i )
    {
    case 1:
    *((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
    v5 = v4 + 1;
    *((_BYTE *)v12 + v5) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
    *((_BYTE *)v12 + ++v5) = aAbcdefghijklmn[64];
    *((_BYTE *)v12 + ++v5) = aAbcdefghijklmn[64];
    v4 = v5 + 1;
    break;
    case 2:
    *((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
    v6 = v4 + 1;
    *((_BYTE *)v12 + v6) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
    *((_BYTE *)v12 + ++v6) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
    *((_BYTE *)v12 + ++v6) = aAbcdefghijklmn[64];
    v4 = v6 + 1;
    break;
    case 3:
    *((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
    v7 = v4 + 1;
    *((_BYTE *)v12 + v7) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
    *((_BYTE *)v12 + ++v7) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
    *((_BYTE *)v12 + ++v7) = aAbcdefghijklmn[byte_41A144[2] & 0x3F];
    v4 = v7 + 1;
    break;
    }
    }
    *((_BYTE *)v12 + v4) = 0;
    return v12;
    }

    毕竟是伪代码,要直接在这里审计复杂算法还是太难了,这里从一个被反复使用的数组 aAbcdefghijklmn 入手,打开发现其内容为大小写字母、数字以及 +/= 三个符号,这是 base64 的字符表啊!那就先按 base64 算。

  • 也就是说,将内部字符串 Str2 每一位码值减去索引后再进行 base64 解码结果即为 flag,其中内部字符串 Str2e3nifIH9b_C@n@dH,因此可编写脚本如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import base64

    Str2 = "e3nifIH9b_C@n@dH"
    flag = ""

    for i in range(len(Str2)):
    flag += chr(ord(Str2[i]) - i)

    print(base64.b64decode(flag))

    执行结束后即可获得 b'{i_l0ve_you}',然而要提交到 BUU 平台的话就需要修改成 flag{i_l0ve_you} 才可通过。某人花了三次机会才知道需要这样提交,痛啊!

不一样的flag

  • 先验一下文件,本题文件为 32 位可执行文件,没有加壳,很好。

  • 直接 IDA 反编译,主函数里就有关键词 flag 出没,代码略长,因此大概需要好好审计一下主函数了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
    {
    _BYTE v3[29]; // [esp+17h] [ebp-35h] BYREF
    int v4; // [esp+34h] [ebp-18h]
    int v5; // [esp+38h] [ebp-14h] BYREF
    int i; // [esp+3Ch] [ebp-10h]
    _BYTE v7[12]; // [esp+40h] [ebp-Ch] BYREF

    __main();
    v3[26] = 0;
    *(_WORD *)&v3[27] = 0;
    v4 = 0;
    strcpy(v3, "*11110100001010000101111#");
    while ( 1 )
    {
    puts("you can choose one action to execute");
    puts("1 up");
    puts("2 down");
    puts("3 left");
    printf("4 right\n:");
    scanf("%d", &v5);
    // 列出四个选项并等待用户输入
    if ( v5 == 2 )
    {
    ++*(_DWORD *)&v3[25];
    // 用户选择 down 则 v3[25] 值减一
    }
    else if ( v5 > 2 )
    {
    if ( v5 == 3 )
    {
    --v4;
    // 用户选择 left 则 v4 值减一
    }
    else
    {
    if ( v5 != 4 )
    LABEL_13:
    exit(1);
    ++v4;
    // 用户选择 right 则 v4 值加一
    }
    }
    else
    {
    if ( v5 != 1 )
    goto LABEL_13;
    --*(_DWORD *)&v3[25];
    // 用户选择 up 则 v3[25] 值加一
    }
    for ( i = 0; i <= 1; ++i )
    {
    if ( *(int *)&v3[4 * i + 25] < 0 || *(int *)&v3[4 * i + 25] > 4 )
    exit(1);
    // v3[25] 取值范围为 [0,4]
    }
    if ( v7[5 * *(_DWORD *)&v3[25] - 41 + v4] == 49 )
    exit(1);
    // 指定位置码值不能等于 49,即字符 1
    // 41 为 v7 到 v3 的偏移
    if ( v7[5 * *(_DWORD *)&v3[25] - 41 + v4] == 35 )
    {
    puts("\nok, the order you enter is the flag!");
    exit(0);
    // 只有指定位置码值为 35 才算成功,即字符 #
    }
    }
    }

    代码主要逻辑是让用户选择上下左右的一个方向,然后通过用户的选择来对指定值做加减一的操作,由于最终比较字符是将上下移动的值乘 5 后与左右移动的值相加,最后减去到 v3 字符串变量的偏移得到的,可以认为程序将 v3 字符串变量视为每行 5 个元素的矩阵。此外,其中上下移动值 v3[25] 限定区间为 [0,4],而 v3 字符串变量共含有 25 个元素,因此可进一步确定程序将该字符串视为五行五列的矩阵。又由于每一步移动后的值不能为 1 且移动到 # 时才算成功,因此从初始地址开始一步一步移动到终点的路线图大致如下:

  • 只需输入按照上述路线移动的数字序列即为 flag,因此本题 flagflag{222441144222}独立审计这一段代码可废了我好大劲。

SimpleRev

  • 本题文件没有后缀名,丢进 Exeinfo PE 发现是 Linux 下的 64 位可执行文件,依旧没有加壳。

  • 那还是直接丢进 IDA 中反编译,主函数中没有啥关键词出现,但是其调用了一个 Decry() 函数,点进去发现又是一段很长的代码,且包含一些关于 flag 的关键词。看来又要慢慢审计代码救命。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    unsigned __int64 Decry()
    {
    char v1; // [rsp+Fh] [rbp-51h]
    int v2; // [rsp+10h] [rbp-50h]
    int v3; // [rsp+14h] [rbp-4Ch]
    int i; // [rsp+18h] [rbp-48h]
    int v5; // [rsp+1Ch] [rbp-44h]
    char src[8]; // [rsp+20h] [rbp-40h] BYREF
    __int64 v7; // [rsp+28h] [rbp-38h]
    int v8; // [rsp+30h] [rbp-30h]
    __int64 v9[2]; // [rsp+40h] [rbp-20h] BYREF
    int v10; // [rsp+50h] [rbp-10h]
    unsigned __int64 v11; // [rsp+58h] [rbp-8h]

    v11 = __readfsqword(0x28u);
    *(_QWORD *)src = 0x534C43444ELL;
    v7 = 0LL;
    v8 = 0;
    v9[0] = 0x776F646168LL;
    v9[1] = 0LL;
    v10 = 0;
    text = (char *)join(key3, v9);
    // jion() 为自定义函数,连接 key3 与 v9
    // v9 为整型数值,按小端序存储形式为 0x68 0x61 0x64 0x6F 0x77
    // 因此 text 值为 killshadow
    strcpy(key, key1);
    // 将 key1 的值 "ADSFK" 赋给 key
    strcat(key, src);
    // 将 src 拼接在 key 之后
    // src 同样为整形数据,按小端序存储形式为 0x4E 0x44 0x43 0x4C 0x53
    // 拼接后的值为 ADSFKNDCLS
    v2 = 0;
    v3 = 0;
    getchar();
    v5 = strlen(key);
    for ( i = 0; i < v5; ++i )
    {
    if ( key[v3 % v5] > 64 && key[v3 % v5] <= 90 )
    key[i] = key[v3 % v5] + 32;
    // 遍历 key 的每个字符,若为大写字母则改为小写字母
    // 故 key 值为 adsfkndcls
    ++v3;
    }
    printf("Please input your flag:");
    while ( 1 )
    {
    v1 = getchar();
    if ( v1 == 10 )
    break;
    // 读取到换行则退出循环
    if ( v1 == 32 )
    {
    ++v2;
    // 读取到空格则 v2 变量加一
    }
    else
    {
    if ( v1 <= 96 || v1 > 122 )
    {
    if ( v1 > 64 && v1 <= 90 )
    {
    str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
    ++v3;
    // 大写字母与 key 中元素逐个操作,转成某个小写字母
    }
    }
    else
    {
    str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
    ++v3;
    // 小写字母与 key 中元素逐个操作,转成某个小写字母
    }
    if ( !(v3 % v5) )
    putchar(32);
    // 循环使用了一次 key 之后打印一个空格
    ++v2;
    }
    }
    if ( !strcmp(text, str2) )
    // 处理过后的 str2 与 text 相同则输入值为 flag
    puts("Congratulation!\n");
    else
    puts("Try again!\n");
    return __readfsqword(0x28u) ^ v11;
    }

    程序在与用户交互前会处理好内部数据,即后续用于作比较的内部字符串 killshadow 以及配合处理用户输入数据的内部密钥 adsfkndcls,由于这些数据均已知,因此我们只需做找出转换后符合条件的字符串即可。逆运算唯一的复杂之处在于每次都使用了取模运算,由于仅对大小写字母做处理,且模为 26,因此每一位符合条件的字符都应该有大小写各一位。

  • 到这就可以直接写脚本了,直接逐个字母遍历过去,符合条件就是 flag 的重要组分。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    key = "adsfkndcls"
    text = "killshadow"
    flag = ""

    for i in range(0, len(text)):
    for j in range(65,91): # 仅取大写字母
    # for j in range(97,123): # 仅取小写字母
    if ord(text[i]) == (j - 39 - ord(key[i]) + 97) % 26 + 97:
    flag += chr(j)

    print(flag)

    计算出大写字母序列 KLDQCUDFZO 与小写字母序列 efxkwoxzti,虽然理论上大写版与小写版乃至他们交叉版本均符合条件,但是实测 BUU 仅接受 flag{KLDQCUDFZO}

Java逆向解密

  • 本题文件直接给了一个 class 文件,这我知道,拉进 idea 就可以反编译了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //

    import java.util.ArrayList;
    import java.util.Scanner;

    public class Reverse {
    public Reverse() {
    }

    public static void main(String[] args) {
    Scanner s = new Scanner(System.in);
    System.out.println("Please input the flag :");
    String str = s.next();
    System.out.println("Your input is :");
    System.out.println(str);
    char[] stringArr = str.toCharArray();
    Encrypt(stringArr);
    }

    public static void Encrypt(char[] arr) {
    ArrayList<Integer> Resultlist = new ArrayList();

    for(int i = 0; i < arr.length; ++i) {
    int result = arr[i] + 64 ^ 32;
    Resultlist.add(result);
    }

    int[] KEY = new int[]{180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65};
    ArrayList<Integer> KEYList = new ArrayList();

    for(int j = 0; j < KEY.length; ++j) {
    KEYList.add(KEY[j]);
    }

    System.out.println("Result:");
    if (Resultlist.equals(KEYList)) {
    System.out.println("Congratulations!");
    } else {
    System.err.println("Error!");
    }
    }
    }

    看多了 C 语言伪代码再看这种的就很舒适,程序逻辑非常简单,对输入字符串逐个进行加 64 后与 32 进行异或的操作,值得注意的是,加号的优先级是高于异或运算符的。当计算结果与内部的 KEY 数组内容一样,用户输入的数据即为 flag

  • 操作比较简单,直接写脚本进行逆操作:

    1
    2
    3
    4
    5
    6
    7
    KEY = [180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65]
    flag = ""

    for i in range(len(KEY)):
    flag += chr((KEY[i] ^ 32) - 64)

    print(flag)

    运行之后便获得了 flag,提交 flag{This_is_the_flag_!} 即可。

[GXYCTF2019]luck_guy

  • 本题文件为 Linux 系统中的 64 位可执行程序,没有加壳。

  • 丢进 IDA 中反编译,主函数中多半是寒暄,值得注意的是其中的 patch_me() 函数:

    1
    2
    3
    4
    5
    6
    7
    int __fastcall patch_me(int a1)
    {
    if ( a1 % 2 == 1 )
    return puts("just finished");
    else
    return get_flag();
    }

    该函数在判断输入为偶数时会调用 get_flag() 函数,这显然就是我们的目标函数了。该函数代码依旧很长,因此,又要慢慢审计了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    unsigned __int64 get_flag()
    {
    unsigned int v0; // eax
    int i; // [rsp+4h] [rbp-3Ch]
    int j; // [rsp+8h] [rbp-38h]
    __int64 s; // [rsp+10h] [rbp-30h] BYREF
    char v5; // [rsp+18h] [rbp-28h]
    unsigned __int64 v6; // [rsp+38h] [rbp-8h]

    v6 = __readfsqword(0x28u);
    v0 = time(0LL);
    srand(v0);
    // 以时间作为随机数种子
    for ( i = 0; i <= 4; ++i )
    {
    switch ( rand() % 200 )
    // rand() 以时间为种子,因此几乎无法预测随机序列,不过这不重要
    {
    case 1:
    puts("OK, it's flag:");
    memset(&s, 0, 0x28uLL);
    // 赋 0 值,内存准备工作
    strcat((char *)&s, f1);
    // 将 f1 存入 &s 所指向的内存中
    strcat((char *)&s, &f2);
    // f2 的数据紧随 f1 存入指定内存
    printf("%s", (const char *)&s);
    break;
    case 2:
    printf("Solar not like you");
    break;
    case 3:
    printf("Solar want a girlfriend");
    break;
    case 4:
    s = 0x7F666F6067756369LL;
    v5 = 0;
    strcat(&f2, (const char *)&s);
    // 为 f2 赋值
    break;
    case 5:
    for ( j = 0; j <= 7; ++j )
    {
    if ( j % 2 == 1 )
    *(&f2 + j) -= 2;
    else
    --*(&f2 + j);
    }
    // 对 f2 中的数据做处理
    break;
    default:
    puts("emmm,you can't find flag 23333");
    break;
    }
    }
    return __readfsqword(0x28u) ^ v6;
    }

    程序虽长,所幸逻辑简单,审计可知 flagf1f2 组成,f1 已知,为 GXY{do_not_,而 f1 则由后续操作完成赋值,且赋值后还需要另外的操作来完成数据处理。也就是说,上述代码中的 switch 语句中只有 145 是有效的,且必须按照 451 的顺序指向才可输出正确的 flag。由于程序执行由随机序列决定,且随机数取模高达 200,因此要靠程序自然输出基本不可能,但是现在毕竟是 Reverse,程序运不运行啥的都不重要,咱自己手工求出来即可。

  • case 4 可知 f2 被赋的值为整形数值 0x7F666F6067756369LL,小端存储的缘故,在内存中的顺序为:0x690x630x750x670x600x6F0x660x7F,将这些数据按顺序进行 case 5 中的操作后与 f1 拼接即可获得 flag。还是写个脚本来做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    f1 = 'GXY{do_not_'
    f2 = [0x69, 0x63, 0x75, 0x67, 0x60, 0x6f, 0x66, 0x7f]
    flag = ''

    for i in range(8):
    if i % 2 == 1:
    flag += chr(f2[i] - 2)
    else:
    flag += chr(f2[i] - 1)

    print(f1 + flag)

    运行后即可获得 GXY{do_not_hate_me},不过 BUU 格式问题,因此需要提交 flag{do_not_hate_me}

[BJDCTF2020]JustRE

  • 本题文件为 32 位可执行程序,没有加壳。

  • 丢进 IDA 反编译,主函数中有些复杂且并未发现什么关键词,因此直接 Shift + F12 查看字符串,发现一个类似 flag 形式的字符串:

    点击进去发现该字符串被 DialogFunc() 函数引用,该函数内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    INT_PTR __stdcall DialogFunc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
    {
    CHAR String[100]; // [esp+0h] [ebp-64h] BYREF

    if ( a2 != 272 )
    {
    if ( a2 != 273 )
    return 0;
    if ( (_WORD)a3 != 1 && (_WORD)a3 != 2 )
    {
    sprintf(String, &Format, ++dword_4099F0);
    if ( dword_4099F0 == 19999 )
    {
    sprintf(String, " BJD{%d%d2069a45792d233ac}", 19999, 0);
    SetWindowTextA(hWnd, String);
    return 0;
    }
    SetWindowTextA(hWnd, String);
    return 0;
    }
    EndDialog(hWnd, (unsigned __int16)a3);
    }
    return 1;
    }

    大多为无关代码,仅关注关键字符串所在的 sprintf() 函数即可,可以发现字符串格式化输出,即占位符 %d 在输出时会被替换为其后所跟的整形数据。

  • 因此格式化输出之后的 BJD{1999902069a45792d233ac} 即为 flag,当然 BUU 中需要提交 flag{1999902069a45792d233ac}值得注意的是,本题文件可双击打开,有个有趣的可视化界面。

刮开有奖

  • 本题文件为 32 位可执行程序,没有加壳。

  • 丢进 IDA 反编译,主函数仅调用了一个 DialogBoxParamA() 函数便退出了,该函数从对话框模板资源创建模式对话框。点进函数发现与其相关的函数都在 DialogFunc() 函数被调用:

    而在 DialogFunc() 函数则存在 U g3t 1T! 这样的字符串存在,因此要获取 flag 就得从这个函数入手了。又是一堆代码要审计。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    INT_PTR __stdcall DialogFunc(HWND hDlg, UINT a2, WPARAM a3, LPARAM a4)
    {
    const char *v4; // esi
    const char *v5; // edi
    int v7[2]; // [esp+8h] [ebp-20030h] BYREF
    int v8; // [esp+10h] [ebp-20028h]
    int v9; // [esp+14h] [ebp-20024h]
    int v10; // [esp+18h] [ebp-20020h]
    int v11; // [esp+1Ch] [ebp-2001Ch]
    int v12; // [esp+20h] [ebp-20018h]
    int v13; // [esp+24h] [ebp-20014h]
    int v14; // [esp+28h] [ebp-20010h]
    int v15; // [esp+2Ch] [ebp-2000Ch]
    int v16; // [esp+30h] [ebp-20008h]
    CHAR String[65536]; // [esp+34h] [ebp-20004h] BYREF
    char v18[65536]; // [esp+10034h] [ebp-10004h] BYREF

    if ( a2 == 272 )
    return 1;
    if ( a2 != 273 )
    return 0;
    if ( (_WORD)a3 == 1001 )
    {
    memset(String, 0, 0xFFFFu);
    // 初始化 String 内存
    GetDlgItemTextA(hDlg, 1000, String, 0xFFFF);
    // 从对话框读取信息写入 String 指向的内存中
    if ( strlen(String) == 8 )
    // 当 String 长度为 8 才进入 if,否则退出,因此 flag 长度为 8
    {
    v7[0] = 90;
    v7[1] = 74;
    v8 = 83;
    v9 = 69;
    v10 = 67;
    v11 = 97;
    v12 = 78;
    v13 = 72;
    v14 = 51;
    v15 = 110;
    v16 = 103;
    // 这些变量全部与 v7 在内存中连续,可认为同在 v7 数组中
    sub_4010F0((int)v7, 0, 10);
    memset(v18, 0, 0xFFFFu);
    v18[0] = String[5];
    v18[2] = String[7];
    v18[1] = String[6];
    // v18 初始化并特定位赋初值
    v4 = (const char *)sub_401000(v18, strlen(v18));
    // v18 处理后赋给 v4
    memset(v18, 0, 0xFFFFu);
    v18[1] = String[3];
    v18[0] = String[2];
    v18[2] = String[4];
    // v18 再次初始化并特定位赋初值
    v5 = (const char *)sub_401000(v18, strlen(v18));
    // v18 再次处理后赋给 v5
    if ( String[0] == v7[0] + 34
    && String[1] == v10
    && 4 * String[2] - 141 == 3 * v8
    && String[3] / 4 == 2 * (v13 / 9)
    && !strcmp(v4, "ak1w")
    && !strcmp(v5, "V1Ax") )
    // 通过这么些个比较才行
    {
    MessageBoxA(hDlg, "U g3t 1T!", "@_@", 0);
    }
    }
    return 0;
    }
    if ( (_WORD)a3 != 1 && (_WORD)a3 != 2 )
    return 0;
    EndDialog(hDlg, (unsigned __int16)a3);
    return 1;
    }

    程序在预处理后从对话框读取数据 String,首先限定其长度为 8 位,不符合则退出程序,因此可以判断 flag 长度为 8 位。长度符合的字符串 String 则会与函数的一些已知值的局部变量以及经过 sub_4010F0() 函数处理的 v7 变量做比较,还有特定位参与 sub_401000() 函数处理后与内部字符串常量的比较,而通过所有比较的字符串 String 即为 flag。因此要获取 flag,还有两个函数也需要好好审计审计,救命啊!!!那么就先审审 sub_4010F0() 函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    int __cdecl sub_4010F0(int a1, int a2, int a3)
    {
    int result; // eax
    int i; // esi
    int v5; // ecx
    int v6; // edx

    result = a3;
    // 初始化为数组末位,作为后续区间遍历的终点
    for ( i = a2; i <= a3; a2 = i )
    // 未到数组末位则继续循环
    {
    v5 = 4 * i;
    v6 = *(_DWORD *)(4 * i + a1);
    // 4 为整形数据所占用的空间,即 v6 赋值为区间起点处的值
    if ( a2 < result && i < result )
    // 未到区间终点则继续循环
    {
    do
    {
    if ( v6 > *(_DWORD *)(a1 + 4 * result) )
    // v6 与区间内末尾作比较,大于末位才进入 if 语句
    {
    if ( i >= result )
    break;
    ++i;
    *(_DWORD *)(v5 + a1) = *(_DWORD *)(a1 + 4 * result);
    // 末位数据存入 v6 数值原本的地址
    if ( i >= result )
    break;
    while ( *(_DWORD *)(a1 + 4 * i) <= v6 )
    {
    if ( ++i >= result )
    goto LABEL_13;
    // 若 v6 值仍然不小于其原位后的一位,则继续递归调用直至结束
    }
    if ( i >= result )
    break;
    v5 = 4 * i;
    *(_DWORD *)(a1 + 4 * result) = *(_DWORD *)(4 * i + a1);
    // 若 v6 跟小了,则把比他大的放在区间末位
    }
    --result;
    // 区间尾部前移,区间缩小
    }
    while ( i < result );
    }
    LABEL_13:
    *(_DWORD *)(a1 + 4 * result) = v6;
    // 若 v6 一直都大,则会一直存在于区间末位
    sub_4010F0(a1, a2, i - 1);
    result = a3;
    ++i;
    }
    return result;
    }

    费了好大劲审计完后,发现大的数据总是会往后移,小的数据则前移,很显然这是一个升序的排序函数。参观了网上好多题解发现大家都不会审计这个代码,因为修改一下直接运行就可以出结果了,谁是怨种我不说。因此 v7 数组经过 sub_4010F0() 函数处理后为如下递增序列:

    1
    2
    3
    51	67	69	72	74	78	83	90	97	103	110
    对应字符序列为:
    3 C E H J N S Z a g n

    最后审计一下 sub_401000() 函数,太长了…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    _BYTE *__cdecl sub_401000(int a1, int a2)
    {
    int v2; // eax
    int v3; // esi
    size_t v4; // ebx
    _BYTE *v5; // eax
    _BYTE *v6; // edi
    int v7; // eax
    _BYTE *v8; // ebx
    int v9; // edi
    int v10; // edx
    int v11; // edi
    int v12; // eax
    int i; // esi
    _BYTE *result; // eax
    _BYTE *v15; // [esp+Ch] [ebp-10h]
    _BYTE *v16; // [esp+10h] [ebp-Ch]
    int v17; // [esp+14h] [ebp-8h]
    int v18; // [esp+18h] [ebp-4h]

    v2 = a2 / 3;
    v3 = 0;
    if ( a2 % 3 > 0 )
    ++v2;
    v4 = 4 * v2 + 1;
    v5 = malloc(v4);
    v6 = v5;
    v15 = v5;
    if ( !v5 )
    exit(0);
    memset(v5, 0, v4);
    v7 = a2;
    v8 = v6;
    v16 = v6;
    if ( a2 > 0 )
    {
    while ( 1 )
    {
    v9 = 0;
    v10 = 0;
    v18 = 0;
    do
    {
    if ( v3 >= v7 )
    break;
    ++v10;
    v9 = *(unsigned __int8 *)(v3 + a1) | (v9 << 8);
    ++v3;
    }
    while ( v10 < 3 );
    v11 = v9 << (8 * (3 - v10));
    v12 = 0;
    v17 = v3;
    for ( i = 18; i > -6; i -= 6 )
    {
    if ( v10 >= v12 )
    {
    *((_BYTE *)&v18 + v12) = (v11 >> i) & 0x3F;
    v8 = v16;
    }
    else
    {
    *((_BYTE *)&v18 + v12) = 64;
    }
    *v8++ = byte_407830[*((char *)&v18 + v12++)];
    v16 = v8;
    }
    v3 = v17;
    if ( v17 >= a2 )
    break;
    v7 = a2;
    }
    v6 = v15;
    }
    result = v6;
    *v8 = 0;
    return result;
    }

    万不可死磕审计,太浪费时间了,注意到函数调用了一个字符数组 byte_407830,点开发现,这是老朋友 base64 字符表啊,应该又是 base64 加密计算而已,就不去老实审计了。

  • 代码审完了,接下来照着最终判断条件逐个击破就行了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
       String[0] == v7[0] + 34 
    // v7[0]=51,故 String[0]='U'(码值为 85)
    && String[1] == v10
    // v10='J',故 String[1]='J'
    && 4 * String[2] - 141 == 3 * v8
    // v8=69,故 String[2]='W'(码值为 87)
    && String[3] / 4 == 2 * (v13 / 9)
    // v13=90,故 String[3]='P'(码值为 80)
    && !strcmp(v4, "ak1w")
    // ak1w 解密为 jMp,故 String[5,6,7]="jMp"
    && !strcmp(v5, "V1Ax")
    // V1Ax 解密为 WP1,故 String[2,3,4]="WP1"

    综上,唯一符合条件的字符串 StringUJWP1jMp,因此提交 flag{UJWP1jMp} 即可。

简单注册器

  • 本题文件为安卓的 apk 文件,由于 IDA 反编译处理着实有些看不懂,因此使用 JEB 进行反编译。在字符串一栏搜索到了关键字 flag{,双击即可在右侧看到调用该字符串的代码。

    略微审计一下代码,发现 flag 仅由如下代码块生成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    char[] arr_c = "dd2940c04462b4dd7c450528835cca15".toCharArray();
    arr_c[2] = (char)(arr_c[2] + arr_c[3] - 50);
    arr_c[4] = (char)(arr_c[2] + arr_c[5] - 0x30);
    arr_c[30] = (char)(arr_c[0x1F] + arr_c[9] - 0x30);
    arr_c[14] = (char)(arr_c[27] + arr_c[28] - 97);
    int i;
    for(i = 0; i < 16; ++i) {
    char a = arr_c[0x1F - i];
    arr_c[0x1F - i] = arr_c[i];
    arr_c[i] = a;
    }

    textview.setText("flag{" + String.valueOf(arr_c) + "}");
    return;}

    虽说这么点代码不难审计,但是毕竟反编译代码十分完善,所以完全可以直接执行该代码块。将 textview.setText 换成 Java 的输出语句即可将 flag 输出,计算结果为 flag{59acc538825054c7de4b26440c0999dd}

[GWCTF 2019]pyre

  • 本题文件为 Python 编译后的二进制文件,直接丢进 在线工具 反编译一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #!/usr/bin/env python
    # visit https://tool.lu/pyc/ for more information
    # Version: Python 2.7

    print "Welcome to Re World!"
    print "Your input1 is your flag~"
    l = len(input1)
    for i in range(l):
    num = ((input1[i] + i) % 128 + 128) % 128
    code += num
    for i in range(l - 1):
    code[i] = code[i] ^ code[i + 1]
    print code
    code = [
    "%1f", "%12", "%1d", "(", "0",
    "4", "%01", "%06", "%14", "4",
    ",", "%1b", "U", "?", "o",
    "6", "*", ":", "%01", "D",
    ";", "%", "%13",]

    程序读取输入,并且进行逐个取模、相加、异或操作后输出数据,而输出的数据在代码中已经给出,正如提示所言,现在求出用户输入即为 flag

  • 逆向编写一个脚本即可,脚本如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    code = ["%1f", "%12", "%1d", "(", "0",
    "4", "%01", "%06", "%14", "4",
    ",", "%1b", "U", "?", "o",
    "6", "*", ":", "%01", "D",
    ";", "%", "%13"]
    flag = ''

    for i in range(len(code) - 2, -1, -1):
    code[i] = chr(ord(code[i]) ^ ord(code[i + 1]))
    for i in range(len(code)):
    num = chr((ord(code[i]) - i) % 128)
    flag += num

    print(flag)

    执行之后即可输出 GWHT{Just_Re_1s_Ha66y!},直接提交 flag{Just_Re_1s_Ha66y!}

总结

  相比于此前 WebPWN,这边的许多题目笔者都可以独立完成,大概也是因为刚入门吧,这个方向对新手还是蛮友好的。对于 Reverse 这边整体感觉就是,嗯对,逆向。不过,虽说整体毕竟顺利,但是动辄代码审计真的非常痛苦,还老是对伪代码审计。对于笔者这种正着都写不好代码的人来说,还要逆过来分析着实不易。

  到这里 CTF 已经开辟了三个方向了,虽说都只是浅浅的了解了一下,但总归对自己的技术层次有了更深的了解,兴趣也被提上来了!别骂了别骂了,我会好好学的。