CTF逆向 [GXYCTF2019] simple CPP

Start

无壳,查字符串:

通过xref交叉引用定位关键函数:

if ( (v20 | v19 | v25 & v26) != (~*(_QWORD *)v15 & v26 | 0xC00020130082C0Ci64) || v0 != 1 )
{
    sub_1400019C0(std::cout, (__int64)"Wrong answer!try again");
    j_j_free(v3);
}
else
{
    v29 = sub_1400019C0(std::cout, (__int64)"Congratulations!flag is GXY{");
    v30 = &Memory;
    if ( v40 >= 0x10 )
        v30 = Memory;
    LODWORD(v31) = sub_140001FD0(v29, v30, v39);
    sub_1400019C0(v31, (__int64)"}");
    j_j_free(v3);
}

判断相等后,输出flag,往上找Memorysub_140001DE0(std::cin, &Memory);可以发现输入的flag存到Memory

0x01 第一部分

v2 = sub_1400024C8(0x20ui64);
v3 = (_BYTE *)v2;
if ( v2 )
{
    *(_QWORD *)v2 = 0i64;
    *(_QWORD *)(v2 + 8) = 0i64;
    *(_QWORD *)(v2 + 16) = 0i64;
    *(_QWORD *)(v2 + 24) = 0i64;
}
else
{
    v3 = 0i64;
}
v4 = 0;
if ( v39 > 0 )
{
    v5 = 0i64;
    do
    {
        v6 = &Memory;
        if ( v40 >= 0x10 )
            v6 = Memory;
        v7 = &Dst;
        if ( (unsigned __int64)qword_140006060 >= 0x10 )
            v7 = (void **)Dst;
        v3[v5] = v6[v5] ^ *((_BYTE *)v7 + v4++ % 27);
        ++v5;
    }
    while ( v4 < v39 );
}

看到do-while循环,观察判断条件,v4 < v39,可以看到v40开始递增,而v39第一次调用在这个位置,可以发现输入的flag不需要包含GXY{}

if ( v39 - 5 > 0x19 )
{
    v32 = sub_1400019C0(std::cout, (__int64)"Wrong input ,no GXY{} in input words");
    std::basic_ostream<char,std::char_traits<char>>::operator<<(v32, sub_140001B90);
    goto LABEL_45;
}

动态调试一下:


可以发现这里rax就是v39,里面存放的就是输入的flag长度
随后进入循环体中:
v6就是输入的flag
v7key,该keyflag进行逐字符异或,然后存放到v3
动态调试可以发现,key的值就是i_will_check_is_debug_or_not

由于这里是逐字符异或的,所以可以猜测,flag的长度就是key的长度,len(key)=28,所以重新输入一个长度为28flag:
异或后得到:
2021010621311628

0x02 第二部分

第二个do-while循环:

do
{
    v14 = *v13 + v8;
    ++v12;
    ++v13;
    if ( v12 == 8 )
    {
        v11 = v14;
        goto LABEL_24;
    }
    if ( v12 == 16 )
    {
        v10 = v14;
        goto LABEL_24;
    }
    if ( v12 == 24 )
    {
        v9 = v14;
        LABEL_24:
        v14 = 0i64;
        goto LABEL_25;
    }
    if ( v12 == 32 )
    {
        sub_1400019C0(std::cout, "ERRO,out of range");
        exit(1);
    }
    LABEL_25:
    v8 = v14 << 8;
}
while ( v12 < (signed int)v39 );

这里将异或后的字符以8个字节为一组放入寄存器中,最终得到:


随后将其放进内存,得到:

对比最开始异或后的结果,可以发现这里数据进行了逆序(按组逆序,8字节为1组),并且最后一组进行了一个字节的偏移。
到这里可以简单的看出前面的几个操作就是将flag与key异或后分成四组,对每组以字节为单位进行逆序,最后一组逆序后进行一个字节的偏移。

0x03 第三部分

上面说到的放进内存的操作在IDA反编译后的代码体现为:

*(_QWORD *)v15 = v11;
*(_QWORD *)(v15 + 8) = v10;
*(_QWORD *)(v15 + 16) = v9;
*(_QWORD *)(v15 + 24) = v8;

随后,将值赋给v36、v16、v17,并且调用IsDebuggerPresent反调试函数,这里直接:

v36 = *(_QWORD *)(v15 + 16);
v16 = *(_QWORD *)(v15 + 8);
v17 = *(_QWORD *)v15;
v18 = sub_14000223C(0x20ui64);
v37 = v18;
if ( IsDebuggerPresent() )
{
  sub_1400019C0(std::cout, "Hi , DO not debug me !");
  Sleep(0x7D0u);
  exit(0);
}

这里绕过IsDebuggerPresent很简单,改EAX为0或者修改je为jmp都可以,具体看绕过IsDebuggerPresent


随后进入最关键的部分:

v19 = v16 & v17;
*(_QWORD *)v18 = v16 & v17;
v20 = v36 & ~v17;
*(_QWORD *)(v18 + 8) = v20;
v21 = ~v16;
v22 = v36 & v21;
*(_QWORD *)(v18 + 16) = v36 & v21;
v23 = v17 & v21;
*(_QWORD *)(v18 + 24) = v23;
if ( v20 != 0x11204161012i64 )
{
    *(_QWORD *)(v18 + 8) = 0i64;
    v20 = 0i64;
}
v24 = v20 | v19 | v22 | v23;
v25 = *(_QWORD *)(v15 + 8);
v26 = *(_QWORD *)(v15 + 16);
v27 = v22 & *(_QWORD *)v15 | v26 & (v19 | v25 & ~*(_QWORD *)v15 | ~(v25 | *(_QWORD *)v15));
v28 = 0;
if ( v27 == 577031497978884115i64 )
    v28 = v24 == 4483974544037412639i64;
if ( (v24 ^ *(_QWORD *)(v15 + 24)) == 4483974543195470111i64 )
    v0 = v28;
if ( (v20 | v19 | v25 & v26) != (~*(_QWORD *)v15 & v26 | 864693332579200012i64) || v0 != 1 )
{
    sub_1400019C0(std::cout, "Wrong answer!try again");
    j_j_free(v3);
}

这里给了五个条件判断,又已知是四段flag,所以可以直接写成一个四元一次方程组进行解未知数,程序给出的东西看来很复杂,很多变量,这里通过数学上的代入法,简化变量,令:

f4 = *(_QWORD *)(v15 + 24);
f3 = *(_QWORD *)(v15 + 16);
f2 = *(_QWORD *)(v15 + 8);
f1 = *(_QWORD *)v15;

最终简化得到:(注意括号)

f3&(~f1)!= 0x11204161012
(f3&(~f2))&f1|f3&((f2&f1)|f2&(~f1)|~(f2|f1))==577031497978884115
(f3&(~f1))|(f2&f1)|(f3&(~f2))|(f1&(~f2))==4483974544037412639
(((f3&(~f1))|(f2&f1)|(f3&(~f2))|(f1&(~f2)))^f4)==4483974543195470111
((f3&(~f1))|(f2&f1)|f2&f3)!=((~f1)&f3|864693332579200012)

通过python中的z3进行解方程组:

from z3 import *

f1, f2, f3, f4 = BitVecs('f1 f2 f3 f4', 64)
s=Solver()
s.add(f3&(~f1)==0x11204161012)
s.add((f3&(~f2))&f1|f3&((f2&f1)|f2&(~f1)|~(f2|f1))==577031497978884115)
s.add((f3&(~f1))|(f2&f1)|(f3&(~f2))|(f1&(~f2))==4483974544037412639)
s.add((((f3&(~f1))|(f2&f1)|(f3&(~f2))|(f1&(~f2)))^f4)==4483974543195470111)
s.add(((f3&(~f1))|(f2&f1)|f2&f3)==((~f1)&f3|864693332579200012))
s.check()

decrypted1 = s.model()
print(decrypted1)

得到:

[f2 = 864693332579200012,
 f1 = 4483973367147818765,
 f3 = 577031497978884115,
 f4 = 842073600]

根据第一和第二部分的分析,直接反解,得到:

We1l_D0ndeajoa_Slgebra_am_i

这里flag还是错误的,后面查了一下别人的writeup,发现当时比赛的时候是给了第二部分的提示的也就是deajoa_S需要替换成e!P0or_a,原因是该方程的不具有唯一解,所以会出现第二部分的解会不太一样,不过我尝试多次通过z3解该方程组,得到的结果都是跟上面解出来的结果一样。
最终拿到flag:

We1l_D0ne!P0or_algebra_am_i

exp.py

from z3 import *

# reverse the third encrypt part by z3
f1, f2, f3, f4 = BitVecs('f1 f2 f3 f4', 64)
s=Solver()
s.add(f3&(~f1)==0x11204161012)
s.add((f3&(~f2))&f1|f3&((f2&f1)|f2&(~f1)|~(f2|f1))==577031497978884115)
s.add((f3&(~f1))|(f2&f1)|(f3&(~f2))|(f1&(~f2))==4483974544037412639)
s.add((((f3&(~f1))|(f2&f1)|(f3&(~f2))|(f1&(~f2)))^f4)==4483974543195470111)
s.add(((f3&(~f1))|(f2&f1)|f2&f3)==((~f1)&f3|864693332579200012))
s.check()
decrypted1 = s.model()

# add encrypted flag to one string
flag1 = hex(decrypted1[f1].as_long())[2:].rjust(16,"0")
flag2 = hex(decrypted1[f2].as_long())[2:].rjust(16,"0") 
flag3 = hex(decrypted1[f3].as_long())[2:].rjust(16,"0")
flag4 = hex(decrypted1[f4].as_long())[2:-2]

decrypted2 = []

def decrypt(sub_flag, d):
    for i in range(len(sub_flag)):
        if sub_flag[i*2:i*2+2] != '':
            d.append(int(sub_flag[i*2:i*2+2],16))
        else:
            break

decrypt(flag1, decrypted2)
decrypt(flag2, decrypted2)
decrypt(flag3, decrypted2)
decrypt(flag4, decrypted2)

# decrypt the flag by xor
flag = ''
key = 'i_will_check_is_debug_or_not'
for i in range(len(decrypted2)):
    flag += chr(ord(key[i]) ^ decrypted2[i])

# By the Tips, change the second part
right_flag = ''
right_flag += flag[:8]
right_flag += 'e!P0or_a'
right_flag += flag[16:]
print(right_flag)

参考文章:
[GXYCTF2019]simple CPP

End

2 个赞