writeup:DASCTF一道逆向题的小诡计

逆向过程

直接切入主题,通过字符串引用查找到关键函数sub_401300,如下:

加密部分

加密算法基本原理就是源字符串3个字符通过算法转化成4个字符,有点类似base64算法的变形。加密算法还原:

const char *bytes = "pc$LEQz=`J&x}Z1H[_#WY?7(d+beh9Rg%U.omaADIn/^r2qj\"X8*SPiVT~,!@0lO";

/**
* 加密算法
*/
void encode(char * src)
{
    size_t len = strlen(src);
    size_t bufsize = 4 * (len / 3) + 4;
    size_t padding = (3 * (len / 3 + 1) - len) % 3; // len % 3;
    if (!padding) {
        bufsize = 4 * (len / 3);
    }

    char *buf = (char *)malloc((bufsize+1) * sizeof(char));

    char *pSrcStart = src + 2;
    char *pBufEnd = bufsize + buf;
    char *pBufStart = buf + 2;

    *pBufEnd = 0; // 结尾反杠零

    size_t i = ((bufsize - 3) >> 2) + 1; //循环计数
    do {
        char c1 = *(pSrcStart - 2);
        pSrcStart += 3;
        char c2 = *(pSrcStart - 4);
        pBufStart += 4;
        *(pBufStart - 6) = bytes[c1 >> 2]; // 标注(1)
        int v9 = 0x10 * (c1 & 0x3) | (c2 >> 4); // 标注(2)
        char c3 = *(pSrcStart - 3);

        *(pBufStart - 5) = bytes[v9];  // 标注(3)
        *(pBufStart - 4) = bytes[4 * (c2 & 0xF) | (c3 >> 6)];  // 标注(4)
        *(pBufStart - 3) = bytes[c3 & 0x3F];  // 标注(5)
    } while (i-- != 1);

    if (padding == 1) {
        *(pBufEnd - 1) = '=';
    }
    else if (padding == 2) {
        *(pBufEnd - 1) = '=';
        *(pBufEnd - 2) = '=';
    }

    printf("%s", buf);
    free(buf);
}

为了保留原汁原味,上面的代码并没有优化,而是采用IDA分析后的结果直接还原代码。看懂了关键的加密算法,接下来分析一下如何写出解密算法。代码标注(1)(2)(3)(4)(5),可以看到用了&这种不可逆的算法,只能穷举,由于字符0x00~0xFF范围很小,如果排除掉不可见字符,范围就更小了,所以穷举的办法可行,直接上代码:


int getindex(char ch)
{
    const char *p = bytes;
    while (*p != '\0') {
        if (*p == ch) {
            return (p - bytes);
        }
        p++;
    }

    return -1;
}

bool findch(char t0, char t1, char t2, char t3, char &c1, char &c2, char & c3)
{
    bool result = false;
    char temp = t0 << 2;

    printf("findch2 start\n");

    for (int k = 0; k <= 3; k++) {
        char guess = temp + k;

        for (int i = 1; i <= 0xFF; i++) {
            //printf("findch2:i->%d\n", i);

            if ((((guess & 3) << 4) | (i >> 4)) == t1) {
                printf("findch2:%c\n", i);

                for (int j = 1; j <= 0xFF; j++) {
                    if (((4 * (i & 0xF) | (j >> 6)) == t2) && ((j & 0x3F) == t3)) {
                        printf("findch1-3:%c%c%c\n", guess,i, j);
                        c1 = guess;
                        c2 = i;
                        c3 = j;
                        result = true;
                        //break;
                    }
                }
            }

        }

    }

    return result;
}

/**
* 解密算法
*/
void decode(char *src)
{
    size_t len = strlen(src);
    size_t bufsize = len / 4 * 3;
    char *buf = (char *)malloc((bufsize+1) * sizeof(char));
    memset(buf, 0, bufsize + 1);

    char *psrc = src;
    char *pbuf = buf;

    do {
        //int idx = getindex(*psrc);
        //char c1 = idx << 2; // 1

        //16 * (c1 & 3) | (c2 >> 4);
        char c1, c2, c3;

        int t0 = getindex(*psrc);
        int t1 = getindex(*(psrc + 1));
        int t2 = getindex(*(psrc + 2));
        int t3 = getindex(*(psrc + 3));

        if (findch(t0, t1, t2, t3, c1, c2, c3)) {
            printf("char1-3:%c%c%c\n", c1, c2, c3);
        }
        else {
            printf("!!!!!!!!!!!!!!!!!!!????????");
        }

        *pbuf = c1;
        *(pbuf+1) = c2;
        *(pbuf+2) = c3;

        psrc += 4;
        pbuf += 3;

    } while (*psrc != '\0');

    printf("%s", buf);
    free(buf);
}

到这里你可能会以为已经结束了,并不是如此,接着看关键函数sub_401300后半部分:

比对结果

为了方便取出v21字符串,载入OD动态调试,在00401488处下断点:

寻找下段处

OD中下断点

得到9zUnhP0nhP0U(i+Ubi?g+AXU+P0Vbz?8+?0nhP0Sbz?g9=JP+W@=,使用我们的decode函数解密结果为:this_is_a_fake_flag_where_is_the_true?,很明显这还不是真正的flag,被耍了哈哈。。说明还有其它地方判断,返回字符串窗口,仔细观察能看到DASCTF{%s}被引用了两次:

image.png

sub_401300已经跟过,这次进入sub_4010E0:

函数sub_4010E0

这个函数就很有意思了,故意触发int3的异常,进入异常处理函数:

sub_4010E0异常处理函数

异常处理函数逻辑也很简单,取出真正的加密字符串与之前的输入后的加密字符串对比,相同就输出真正的flag。同样使用OD载入下断点,得到真正的加密字符串为:bDYP9Q0S}Vag}(_gZz~m(Vm"9?0V}7Xr(ih*9Q0Ybz?g+AXp+8E=,解密后明文为:ju5t_t3y_1t_4nd_y0u_w1ll_g3t_The_fl@g!

最后输入验证:


完工

延伸与思考

上面通过了很明显的字符串才查找到真正的加密字符串,并且调用也相当隐蔽,这里其实用到了_initterm和atexit函数结合的方式。_initterm是CRT的内部函数,可以利用下面代码给程序增加隐蔽的初始化代码,再利用atexit函数在程序退出时增加回调函数:

#include 
using namespace std;

#define SECNAME ".CRT$XCV"
#pragma section(SECNAME, long, read)

void exit()
{
    cout << "exit~" << endl;
}

void init()
{
    cout << "init~" << endl;
    atexit(exit);
}

typedef void(__cdecl * _PVFV)();
__declspec(allocate(SECNAME)) _PVFV dummy[] = { init, };

int main()
{
    return 0;
}

另外一点,利用int3异常来打乱IDA的F5分析:

try {
  __debugbreak();
}
catch(...)
{
  cout << "int3 exception" << endl;
  //代码...
}

这段代码在VS高版本中无法被catch到,上述代码要在debug模式下编译。

题目及代码

链接: https://pan.baidu.com/s/17hst6TLgjQnxjHhPBaJdFg?pwd=f3na 提取码: f3na

更多参考:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2012/ff797097(v=vs.110)

你可能感兴趣的:(writeup:DASCTF一道逆向题的小诡计)