[GXYCTF2019]simple CPP

知识点

  • C++逆向
  • z3脚本
  • Debug绕过

解题

首先F12+shift查看字符串, 找到关键代码,

if ( (v23 | v22 | v28 & v29) == (~*v18 & v29 | 864693332579200012i64) && v3 )// 判断flag
{
v32 = print(std::cout, (__int64)"Congratulations!flag is GXY{");
v33 = flag;
if ( v41 >= 0x10 )
v33 = (void **)flag[0];
v34 = sub_140001FD0(v32, (__int64)v33, v40);
print(v34, (__int64)"}");
j_j_free(v6);
}
else
{
print(std::cout, (__int64)"Wrong answer!try again");
j_j_free(v6);
}

经过分析可知,易知v33为flag并最后输出,上翻flag,找到输入sub_140001DE0(std::cin, flag);

接下来一步步分析flag的字符串是如何加密的

第一步

if ( v40 )
{
v8 = 0i64;
do // 第一个循环
{
v9 = flag;
if ( v41 >= 0x10 )
v9 = (void **)flag[0];
v10 = &i_will;
if ( (unsigned __int64)qword_140006060 >= 0x10 )
v10 = (void **)i_will;
v6[v8] = *((_BYTE *)v9 + v8) ^ *((_BYTE *)v10 + v7 % 27);// i_will和flag异或
++v7;
++v8;
}
while ( v7 < v40 );

发现 flag与i_will变量异或赋值给v6.

i_will变量的值获取不到直接试试动调

1

输入11111111111111

经过调试得到i_will的变量为i_will_check_is_debug_or_not

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

‘1’*28

第二步

do                                            // 第二个循环
{
v17 = *v16 + v11;
++v15;
++v16;
switch ( v15 )
{
case 8:
v14 = v17;
goto LABEL_23;
case 16:
v13 = v17;
goto LABEL_23;
case 24:
v12 = v17;
LABEL_23:
v17 = 0i64;
break;
case 32:
print(std::cout, (__int64)"ERRO,out of range");
exit(1);
}
v11 = v17 << 8;
}
while ( v15 < (int)v40 );

经过动调上面的程序得,每八个一组,逆序,最后一组进行了一个字节的偏移。

静态分析也可得到类似的结果,如下

*v18 = v14;
v18[1] = v13;
v18[2] = v12;
v18[3] = v11;

第三步

f3 = v18[2];
f2 = v18[1];
f1 = *v18;
v21 = (__int64 *)operator new(0x20ui64);
if (IsDebuggerPresent())
{
print(std::cout, (__int64)"Hi , DO not debug me !");
Sleep(0x7D0u);
exit(0);
}

这里绕过IsDebuggerPresent很简单,改EAX为0或者修改je为jmp都可以,具体看绕过IsDebuggerPresent,但之后好像不需要动调了

第四步

下面进行了复杂的运算

可对v18分成的四部分分别赋值为f1,f2,f3,f4,对以下代码做整理

v22 = f2 & f1;
*v21 = f2 & f1;
v23 = f3 & ~f1;
v21[1] = v23;
v24 = ~f2;
v25 = f3 & v24;
v21[2] = f3 & v24;
v26 = f1 & v24;
v21[3] = v26;
if ( v23 != 0x11204161012i64 )
{
v21[1] = 0i64;
v23 = 0i64;
}
v27 = v23 | v22 | v25 | v26;
v28 = v18[1];
v29 = v18[2];
v30 = v25 & *v18 | v29 & (v22 | v28 & ~*v18 | ~(v28 | *v18));
v31 = 0;
if ( v30 == 0x8020717153E3013i64 )
v31 = v27 == 0x3E3A4717373E7F1Fi64;
if ( (v27 ^ v18[3]) == 0x3E3A4717050F791Fi64 )
v3 = v31;
if ( (v23 | v22 | v28 & v29) == (~*v18 & v29 | 864693332579200012i64) && v3 )// 判断flag

条件大概如下

f3 & ~f1 == 0x11204161012 # 1
(f3 & ~f2) & f1 | f3 & (f2 & f1 | f2 & ~f1 | ~(f2 | f1))== 0x8020717153E3013 # 2
(f3 & ~f1)| (f2 & f1) | (f3 & ~f2) | (f1 & ~f2) == 0x3E3A4717373E7F1F # 3
(((f3 & ~f1)| (f2 & f1) | (f3 & ~f2) | (f1 & ~f2)) ^ f4) == 0x3E3A4717050F791F # 4
((f3 & ~f1) | (f2 & f1) | f2 & f3) == (~f1 & f3 | 0xC00020130082C0C) # 5

写出z3脚本即可计算出各自的值,然后逆向写出前几步加密

第五步

所有都分析完了,怎么还有。

问题就在于该方程的不具有唯一解,所以发现当时比赛的时候是给了第二部分的提示的也就是deajoa_S需要替换成e!P0or_a

脚本

from z3 import *

d2 = []
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()
d = s.model()
flag1 = hex(d[f1].as_long())[2:].rjust(16, "0")
flag2 = hex(d[f2].as_long())[2:].rjust(16, "0")
flag3 = hex(d[f3].as_long())[2:].rjust(16, "0")
flag4 = hex(d[f4].as_long())[2:-2]


def decode(flag, d): # 第二步
le = len(flag)
for i in range(0, le, 2):
if flag[i:i+2] != "": # 不加也行
d.append(int(flag[i:i+2], 16)) # 将str转成int


decode(flag1, d2)
decode(flag2, d2)
decode(flag3, d2)
decode(flag4, d2)
key = 'i_will_check_is_debug_or_not' # 第一个
fake_flag = ""
for i in range(len(d2)):
fake_flag += chr(ord(key[i]) ^ d2[i])
print(fake_flag)
# We1l_D0ndeajoa_Slgebra_am_i

right_flag = ''
right_flag += fake_flag[:8]
right_flag += 'e!P0or_a' # 多解
right_flag += fake_flag[16:]
print(right_flag)
# We1l_D0ne!P0or_algebra_am_i