首页
论坛
专栏
课程

[原创]看雪CTF 2019总决赛 第六题 三道八佛

2019-12-15 00:16 1498

[原创]看雪CTF 2019总决赛 第六题 三道八佛

2019-12-15 00:16
1498

目录

脱壳

这次的题目还是用的Q3第十题的壳

壳分析

使用x32dbg调试,依然看到熟悉的操作

 

image-20191214205722093

 

image-20191214205851781

 

在保存完输入的用户名和序列号后便开始解密代码,在解密代码之后依然通过tib重新设置了程序的栈地址(mov dword ptr fs:[4], eaxmov dword ptr fs:[8], ebx),最后通过call eax跳转到解密后的代码

 

值得注意的是这一次壳自解密的过程中执行的代码都一样,因此怀疑验证算法是在壳多次自解密完成后才执行的

 

另外一个和上次题目不同的地方在每次跳转到解密后的代码那里,即call eax这个指令在后面有变化,因此在写脚本定位特征时需要多判断一个特征:E801000000,具体见下图:

 

image-20191214211039531

脱壳脚本

参考了上次比赛@注册LookLook的脚本,因为我上次写的脚本太冗长了
附件:x32dbg.txt

 

通过尝试发现壳一共自解密了0x578次,即1400次(比上次少了很多)。于是断到最后一次,再进行操作。因为最后一次修改fs:[8]后还有一次自解密,所以还需要gocode:这个分支中的操作才能到真正的验证算法

// 程序加载后直接先按空格运行脚本,输入用户名序列号,会自动跑到解密后的代码,并dump
dbh

bph 004014E0
erun
bphc 004014E0

bph fs:[18]+8,w,4

mov $last, 0
mov $cnt, 0
mov $stop_num, 0x577

looop:
    erun
    inc $cnt
    log "Hit {$cnt}"
    find cip, FFD0, 0x100
    mov $next, $result
    cmp $next, 0
    jnz found
    find cip, E801000000, 0x100
    mov $next, $result
    cmp $next, 0
    jz finish
found:
    erun $next
    cmp $cnt, $stop_num
    jz gocode
    jmp looop

gocode:
    sti
    rtr
    sto
    find cip,0F85????????,0x200
    mov $next, $result
    cmp $next,0
    jz finish
    erun $next+6
    find cip, FFD0, 0x100
    mov $next, $result
    cmp $next, 0
    jz finish
    erun $next
    sti
    sto
    erun cip+26
    savedata "D:\dump01.bin",cip,3454

finish:
bphc fs:[18]+8
msg "Done"

脚本执行完后定位到的函数头尾:

 

image-20191214212359171

 

image-20191214213435233

 

然后手动将dump的bin文件末尾一个字节改成c3,即ret,就可以用ida创建函数F5反编译了。附件:dump01.bin dump01.idb

 

image-20191214213555883

 

image-20191214213733124

逻辑分析

抠出来算法,还需要知道这段算法执行的前后干了什么,调试分析一下

 

在进入算法前:

 

image-20191214214433649

 

esi指向的数据:

 

image-20191214214707416

  • 会发现esi指向的数据和上一次的几乎一样,依然是在esi+0x1B0的位置保存序列号的16进制数据。但是后续对算法的分析发现,算法只用到了序列号的16进制数据,其他数据都没用到,又是障眼法。

算法完成后:

 

image-20191214214155208

  • 算法完成后esi+0x1B0的位置保存了解密后的数据,然后将解密后的数据同用户名比较,若相同则打印成功

  • 需要注意这里比较用的是repe cmpsb,判断长度为ecx = 0x11,即判断17字节的数据。然后可以发现若用户名不足16位,则默认有填充数据,也就是用户名KCTF最后验证时验证的用户名数据是:4B 43 54 46 00 1A 19 18 17 16 15 14 13 12 11 10。具体如下所示:

    image-20191214214324168

代码还原

附件:FixCrackMe.cpp,FixCrackMe.exe

 

根据以上逻辑,再将ida反编译的代码优化优化,可以做出一份可独立运行的cpp代码:

#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <string.h>

unsigned char esiData[17] = {0};
#define __ROL2__(x, n)  (((x) << (n)) | ((x) >> (16-(n))))
int check(unsigned char* esiData,WORD *calc_r)
{
    WORD k_E_D; // ST5C_2
    WORD k_A_9; // ST58_2
    WORD k_C_B; // di
    WORD k_0_F; // ST54_2
    WORD k_6_5; // ST60_2
    WORD k_2_1; // edx
    WORD k_8_7; // ebx
    WORD k_4_3; // ST48_4
    WORD v14; // eax
    WORD v17; // edx
    WORD v18; // eax
    WORD v19; // eax
    WORD v20; // ST00_2
    WORD v22; // ecx
    WORD result; // eax

    k_2_1 = esiData[0x1] + (esiData[0x2] << 8);
    k_4_3 = esiData[0x3] + (esiData[0x4] << 8);
    k_6_5 = esiData[0x5] + (esiData[0x6] << 8);
    k_8_7 = esiData[0x7] + (esiData[0x8] << 8);
    k_A_9 = esiData[0x9] + (esiData[0xA] << 8);
    k_C_B = esiData[0xB] + (esiData[0xC] << 8);
    k_E_D = esiData[0xD] + (esiData[0xE] << 8);
    k_0_F = esiData[0xF] + (esiData[0x0] << 8);

    WORD t0 = k_2_1 ^ k_4_3;
    WORD t1 = k_6_5 ^ k_8_7;
    WORD t2 = k_C_B - k_A_9;
    WORD t3 = k_0_F + k_E_D;

    WORD x1 = ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) & 0x3333)     + ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) >> 2) & 0x3333));
    x1 = ((((x1 & 0xF0F) + ((x1 >> 4) & 0xF0F)) >> 8) + ((x1 & 0xF) + ((x1 >> 4) & 0xF)));

    v14 = (t2 & ~t0 | t3 & t0);

    v17 = (t0 * v14 >> x1) + 24;
    v19 = t2 ^ v17;
    v20 = v14 | v17;
    v22 = v14 & v17 | (v19 & v20);

    calc_r[0] = k_4_3 ^ v22;                       // 0 4B 43    434B
    calc_r[1] = k_2_1 ^ v22;                       // 2 54 64    6454
    calc_r[2] = k_0_F + v19;                       // 4 1A 00    001A
    calc_r[3] = k_E_D - v19;                       // 6 19 18    1819
    calc_r[4] = k_C_B + v17;                       // 8 17 16    1617
    calc_r[5] = k_A_9 + v17;                       // A 15 14    1415
    calc_r[6] = __ROL2__(v14 ^ k_8_7, x1 & 0xFF);  // C 13 12    1213  
    calc_r[7] = __ROL2__(v14 ^ k_6_5, x1 & 0xFF);  // E 11 10    1011  

    return 1;                                      //  K  C  T  F  
                                                   // 4B 43 54 46 00 1A 19 18 17 16 15 14 13 12 11 10 

}

//十六进制字符串转换为字节流  
void HexStrToByte(const char* source, unsigned char* dest, int sourceLen)
{
    short i;
    unsigned char highByte, lowByte;

    for (i = 0; i < sourceLen; i += 2)
    {
        highByte = toupper(source[i]);
        lowByte = toupper(source[i + 1]);

        if (highByte > 0x39)
            highByte -= 0x37;
        else
            highByte -= 0x30;

        if (lowByte > 0x39)
            lowByte -= 0x37;
        else
            lowByte -= 0x30;

        dest[i / 2] = (highByte << 4) | lowByte;
    }
    return;
}

// This is an example of an exported function.
int main(void)
{
    /*
    UserName: BE1C6CB1F1616083
    Key: 5E2BA658A0E9C5F1B52C4C3C4C5C161C
    */
    char KCTF[32] = { 0x4B, 0x43, 0x54, 0x46, 0x00, 0x1A, 0x19, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10 };
    char username[32] = { 0x1F, 0x1E, 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10 };
    printf("Input user name: ");
    scanf("%s", username);
    char passwd[64] = { 0 };
    WORD calc_result[9] = { 0 };
    printf("Input password: ");
    scanf("%s", passwd);
    //memcpy(username,"BE1C6CB1F1616083",16);
    //memcpy(passwd, "5E2BA658A0E9C5F1B52C4C3C4C5C161C", 32);
    //memcpy(username, KCTF,16);
    //memcpy(passwd, "6CCDE9D2EC1D469DC67C647E66B4C565", 32);
    int len = strlen(passwd);
    HexStrToByte(passwd, esiData , len);

    int result1 = check(esiData, calc_result);

    int result = memcmp(username, (char*)calc_result, 0x10);
    if (!result)
        printf("Success!\r\n");
    else
        printf("Error!\r\n");

    system("pause");
    return 0;
}

算法分析

优化后的算法看起来就很清晰了,分别将序列号相邻的两个字节拼在一起,然后进行一波看不懂的运算。知道应该输出什么结果后,就可以逆推算法了。

 

可以发现:

  • calc_r[0]^calc_r[1]抵消v22
  • calc_r[2]+calc_r[3]抵消v19
  • calc_r[4]-calc_r[5]抵消v17

所以

WORD t0 = k_2_1 ^ k_4_3;
WORD t1 = k_6_5 ^ k_8_7;
WORD t2 = k_C_B - k_A_9;
WORD t3 = k_0_F + k_E_D;
// 只有 t1 是变量,其他都常数

解法一:爆破

只需要穷举 k_6_5 与 k_8_7,速度就快很多
附件:check_cr.cpp

#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <string.h>

unsigned char esiData[17] = {0};
#define __ROL2__(x, n)  (((x) << (n)) | ((x) >> (16-(n))))

int check_cr(unsigned char* esiData,WORD *calc_r)
{
    WORD k_E_D; // ST5C_2
    WORD k_A_9; // ST58_2
    WORD k_C_B; // di
    WORD k_0_F; // ST54_2
    WORD k_6_5; // ST60_2
    WORD k_2_1; // edx
    WORD k_8_7; // ebx
    WORD k_4_3; // ST48_4
    WORD v14; // eax
    WORD v17; // edx
    WORD v18; // eax
    WORD v19; // eax
    WORD v20; // ST00_2
    WORD v22; // ecx
    WORD result; // eax

    for (k_6_5=0;k_6_5<0xFFFF;k_6_5++)
    {
        for (k_8_7=0;k_8_7<0xFFFF;k_8_7++)
        {
            WORD t0 = 0x4654 ^ 0x434B;
            WORD t1 = k_6_5 ^ k_8_7;
            WORD t2 = 0x1617 - 0x1415;
            WORD t3 = 0x1A00 + 0x1819;

            WORD x1 = ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) & 0x3333)     + ((((t1 & 0x5555) + ((t1 >> 1) & 0x5555)) >> 2) & 0x3333));
            x1 = ((((x1 & 0xF0F) + ((x1 >> 4) & 0xF0F)) >> 8) + ((x1 & 0xF) + ((x1 >> 4) & 0xF)));

            v14 = (t2 & ~t0 | t3 & t0);

            v17 = (t0 * v14 >> x1) + 24;
            v19 = t2 ^ v17;
            v20 = v14 | v17;
            v22 = v14 & v17 | (v19 & v20);

            WORD r6 = __ROL2__(v14 ^ k_8_7, x1 & 0xFF);
            WORD r7 = __ROL2__(v14 ^ k_6_5, x1 & 0xFF);
            if ((r6 == 0x1213) && (r7 == 0x1011))
            {
                printf("t0=%04X\n",t0);
                printf("t1=%04X\n",t1);
                printf("t2=%04X\n",t2);
                printf("t3=%04X\n",t3);
                printf("x1=%04X\n",x1);
                printf("v14=%04X\n",v14);
                printf("v17=%04X\n",v17);
                printf("v19=%04X\n",v19);
                printf("v20=%04X\n",v20);
                printf("v22=%04X\n",v22);

                WORD k_2_1 = v22 ^ 0x4654;
                WORD k_4_3 = v22 ^ 0x434B;
                WORD k_0_F = 0x1A00 - v19;
                WORD k_E_D = 0x1819 + v19;
                WORD k_C_B = 0x1617 - v17;
                WORD k_A_9 = 0x1415 - v17;
                printf("k_2_1=%04X\n",k_2_1);
                printf("k_4_3=%04X\n",k_4_3);
                printf("k_6_5=%04X\n",k_6_5);
                printf("k_8_7=%04X\n",k_8_7);
                printf("k_A_9=%04X\n",k_A_9);
                printf("k_C_B=%04X\n",k_C_B);
                printf("k_E_D=%04X\n",k_E_D);
                printf("k_0_F=%04X\n",k_0_F);
            }
        }
    }
    return 1;
}

// This is an example of an exported function.
int main(void)
{
    WORD calc_result[9] = { 0 };
    check_cr(esiData, calc_result);
    system("pause");
    return 0;
}

解法二:z3解方程

用z3的道路比较曲折,在后面需要注意的地方有说明
附件:key.py

from z3 import *

def __ROL2__(x, n):
    return (((x) << (n&0xF)) | (LShR(x,(16-(n&0xF)))))

k_2_1 = BitVec('k_2_1',32)
k_4_3 = BitVec('k_4_3',32)
k_6_5 = BitVec('k_6_5',32)
k_8_7 = BitVec('k_8_7',32)
k_A_9 = BitVec('k_A_9',32)
k_C_B = BitVec('k_C_B',32)
k_E_D = BitVec('k_E_D',32)
k_0_F = BitVec('k_0_F',32)

s = Solver()

t0 = k_2_1 ^ k_4_3
t1 = k_6_5 ^ k_8_7
t2 = k_C_B - k_A_9
t3 = k_0_F + k_E_D

x0 = (((t1 & 0x5555) + (LShR(t1,1) & 0x5555)) & 0x3333) + (LShR(((t1 & 0x5555) + (LShR(t1,1) & 0x5555)),2) & 0x3333)
x1 = LShR(((x0 & 0xF0F) + (LShR(x0,4) & 0xF0F)), 8) + ((x0 & 0xF) + (LShR(x0,4) & 0xF))
v14 = t2 & ~t0 | t3 & t0

v17 = LShR(t0 * v14, x1) + 24
v19 = t2 ^ v17
v20 = v14 | v17
v22 = (v14 & v17) | (v19 & v20)

s.add(ULT(k_2_1,0x10000))
s.add(ULT(k_4_3,0x10000))
s.add(ULT(k_6_5,0x10000))
s.add(ULT(k_8_7,0x10000))
s.add(ULT(k_A_9,0x10000))
s.add(ULT(k_C_B,0x10000))
s.add(ULT(k_E_D,0x10000))
s.add(ULT(k_0_F,0x10000))

'''
#BE1C6CB1F1616083
s.add(0x4542 == (k_4_3 ^ v22) % 0x10000)
s.add(0x4331 == (k_2_1 ^ v22) % 0x10000)
s.add(0x4336 == (k_0_F + v19) % 0x10000)
s.add(0x3142 == (k_E_D - v19) % 0x10000)
s.add(0x3146 == (k_C_B + v17) % 0x10000)
s.add(0x3136 == (k_A_9 + v17) % 0x10000)
s.add(0x3036 == __ROL2__(v14 ^ k_8_7, x1 & 0xF) % 0x10000)
s.add(0x3338 == __ROL2__(v14 ^ k_6_5, x1 & 0xF) % 0x10000)
'''

#KCTF
s.add(0x434B == (k_4_3 ^ v22) % 0x10000)
s.add(0x4654 == (k_2_1 ^ v22) % 0x10000)
s.add(0x1A00 == (k_0_F + v19) % 0x10000)
s.add(0x1819 == (k_E_D - v19) % 0x10000)
s.add(0x1617 == (k_C_B + v17) % 0x10000)
s.add(0x1415 == (k_A_9 + v17) % 0x10000)
s.add(0x1213 == __ROL2__(v14 ^ k_8_7, x1 & 0xF) % 0x10000)
s.add(0x1011 == __ROL2__(v14 ^ k_6_5, x1 & 0xF) % 0x10000)

if s.check() == sat:
    result = s.model()
    print('%04X' % int(str(result[k_2_1])))
    print('%04X' % int(str(result[k_4_3])))
    print('%04X' % int(str(result[k_6_5])))
    print('%04X' % int(str(result[k_8_7])))
    print('%04X' % int(str(result[k_A_9])))
    print('%04X' % int(str(result[k_C_B])))
    print('%04X' % int(str(result[k_E_D])))
    print('%04X' % int(str(result[k_0_F])))
else:
    print('No')

用z3需要注意的地方:

  1. __ROL2__函数,虽然python的>>是无符号的,但是z3是有符号的,所以需要换成LShR
  2. 虽然数据是16位的,但是实测只能用32位的向量,然后对向量加约束,控制在16位(使用ULT指令约束)
  3. 使用32位向量是因为:算法中的语句v17 = (t0 * v14 >> x1) + 24;中乘法得到16位的值去移位,损失信息了

解题过程记录

首先是脱壳,参考上次的,没有什么特别的,改改脚本就完了

 

脱壳完成后我们先优化代码,然后尝试用z3求解,但是由于对z3的使用不够熟练总是无解

 

一开始也没注意到用户名KCTF\0后面有默认填充的数据,认为全是0,@ccfer就手动逆推算出来了一个序列号,结果不对,一看才知道有填充数据。因为有了填充数据,没办法手动逆推了

 

然后@大帅锅和@ccfer优化代码,最终发现只有 k_6_5 与 k_8_7 是变量,@ccfer就写出了爆破代码

 

到了第二天我们开始研究用z3求解,最终@ccfer找到了错误的原因,完成了用z3解这道题

 

是的,我的队友们就是这么强大!这就是传说中的带躺吧

 

图片描述



2020安全开发者峰会(2020 SDC)议题征集 中国.北京 7月!

最后于 2019-12-17 10:32 被KevinsBobo编辑 ,原因:
上传的附件:
最新回复 (0)
游客
登录 | 注册 方可回帖
返回