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
,往上找Memory
:sub_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
,可以看到v4
从0
开始递增,而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
v7
是key
,该key
与flag
进行逐字符异或,然后存放到v3
动态调试可以发现,
key
的值就是i_will_check_is_debug_or_not
由于这里是逐字符异或的,所以可以猜测,
flag
的长度就是key
的长度,len(key)=28
,所以重新输入一个长度为28
的flag
:异或后得到:
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