Start
常规逆向题
ELF64文件,拖进IDA,日常搜索字符串
直接跟进去
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int128 v4; // [rsp+0h] [rbp-D8h]
__int128 v5; // [rsp+10h] [rbp-C8h]
__int64 v6; // [rsp+20h] [rbp-B8h]
char v7; // [rsp+28h] [rbp-B0h]
__int16 v8; // [rsp+58h] [rbp-80h]
__m128i v9; // [rsp+60h] [rbp-78h]
__m128i v10; // [rsp+70h] [rbp-68h]
__int64 v11; // [rsp+80h] [rbp-58h]
__int64 v12; // [rsp+88h] [rbp-50h]
__int128 v13; // [rsp+90h] [rbp-48h]
__int128 v14; // [rsp+A0h] [rbp-38h]
__int64 v15; // [rsp+B0h] [rbp-28h]
__int16 v16; // [rsp+B8h] [rbp-20h]
unsigned __int64 v17; // [rsp+C8h] [rbp-10h]
__asm { endbr64 }
v17 = __readfsqword(0x28u);
v12 = 0LL;
v9 = _mm_load_si128((const __m128i *)&xmmword_2050);
v11 = 4927612398162959439LL;
v10 = _mm_load_si128((const __m128i *)&xmmword_2060);
v13 = 0LL;
v14 = 0LL;
v15 = 0LL;
v16 = 0;
sub_1080("Input the flag: ", a2);
memset(&v4, 0, 0x58uLL);
v8 = 0;
sub_10B0(&v4);
sub_12F0(&v4);
if ( (v4 ^ *(_OWORD *)&v9) != 0 || (v5 ^ *(_OWORD *)&v10) != 0 || v11 != v6 || (_BYTE)v12 != v7 )
sub_1080("oh no,you are wrong", a2);
else
sub_1080("right,you get the flag!!", a2);
if ( __readfsqword(0x28u) != v17 )
{
sub_10A0();
start();
}
return 0LL;
}
这里的sub_1080
很容易看出应该就是printf
然后对v4
进行内存的初始化,之后sub_10B0
和sub_12F0
都传入了v4
的指针
这里先跟进sub_10B0
看看,
继续跟
这里是JUMPOUT
,说明是调用了外部的一个函数,其实不难猜测出,v4
应该就是我们输入的flag
的首地址,这里只传入v4
的指针,所以,可以得出sub_10B0
应该就是gets()
函数,这里我们验证一下
回到汇编窗口,找到最开始传入v4
的参数的call
的函数偏移地址为0x1163
,
随后利用gdb动态调试看看这个函数做了啥
start
发现OEP
在0x555555555200
,猜测基址应该是0x555555554000
在gdb下尝试下断点b gets
,成功下断点
run
,此时函数就停在了gets
这个位置,说明程序正常的运行的时候就会调用这个函数
往下单步到0x7ffff7e52b68
的位置
再往下单步时,程序就会停留在这个输入这个位置
这里随便输入点东西,之后继续单步,会发现函数返回到0x5555555555168
减去基址,得到0x1168
,发现刚好就是调用完sub_10B0
后的返回地址
其实上面这一步大概猜测出是gets函数就行了,没必要大费周章的去分析,这里我只是为了熟悉熟悉gdb。
往下来到sub_12F0
,这里才是真正对flag进行处理的位置
signed __int64 __fastcall sub_12F0(_BYTE *a1)
{
signed __int64 result; // rax
int v2; // esi
__int64 v3; // r8
__int64 v4; // rdx
__int64 v5; // rax
int v6; // ecx
char v7; // r8
char v8; // dl
__asm { endbr64 }
result = sub_1090();
if ( (signed int)result > 0 )
{
v2 = result;
v3 = (unsigned int)(result - 1);
v4 = 0LL;
do
{
a1[v4] = (a1[v4] ^ byte_4010[(unsigned int)v4 % 7]) + 65;
v5 = v4++;
}
while ( v3 != v5 );
v6 = 0;
do
{
v7 = a1[3];
result = (signed __int64)(a1 + 3);
do
{
v8 = *(_BYTE *)(result-- - 1);
*(_BYTE *)(result + 1) = v8;
}
while ( a1 != (_BYTE *)result );
v6 += 4;
*a1 = v7;
a1 += 4;
}
while ( v2 > v6 );
}
return result;
}
用gdb跟可以发现sub_1090
就是strlen
之后看到第一个do-while
循环,遍历flag
以七个为一组与byte_4010
进行异或再加上65
do
{
a1[v4] = (a1[v4] ^ byte_4010[(unsigned int)v4 % 7]) + 65;
v5 = v4++;
} while ( v3 != v5 );
byte_4010
: 51h, 57h, 42h, 6Ch, 6Fh, 67h, 73h
这里测试时我输入的全都是a
,测试数据越简单越容易看出规律,可以先异或一组看结果 qwdNOGS
来到第二个循环
do
{
v7 = a1[3];
v8 = a1 + 3;
do
{
v9 = *(v8-- - 1);
v8[1] = v9;
}
while ( a1 != v8 );
v6 += 4;
*a1 = v7;
a1 += 4;
} while ( v2 > v6 );
可以发现这个循环中v7 = a1[3]
,在执行完中间的do-while
循环后,令*a1 = v7
,之后a1 += 4
,不难发现第二个加密是以四个为一组进行加密,并且先将每一组中最后一个存到v7
中,最后放到了每一组第一的位置,这里v6
是跳出循环的条件,可以不管。
重点是中间的循环,这里比较可能比较难理解,首先第一句:
v9 = *(v8-- - 1);
我们注意到v8 = a1 + 3;
也就是说最开始v8
应该指向每一组的最后一个字符,第一次进入循环,将当前组的第三个字符(从左往右数)存到v9
,当该运算完成后,v8指针偏移,指向第三个字符。
第二句:
v8[1] = v9;
通过第一句可以发现,此时v8的指针指向的是第三个字符(从左往右),所以v8[1]
是第四个字符
总结出来就是,将当前组的第三个字符,移动到第四个字符的位置。
最后看到循环的条件,a1 != v8
,也就是当v8
指向第一个字符的时候,循环结束。再通过之前v7
的操作,不难发现这里其实就是对字符进行了移位
假设经过第一轮异或后的字符串为qwdN
,那么经过此轮移位后,得到的就是Nqwd
这里通过gdb验证之后,推断正确
读完处理函数后,往下来到最终的判断
(v4 ^ *(_OWORD *)&v9) != 0 || (v5 ^ *(_OWORD *)&v10) != 0 || v11 != v6 || (_BYTE)v12 != v7
首先是v4
和v9
进行异或之后看是否等于0
,这里其实就是看v4
是否等于v9
,一个数异或0
之后还是它本身,后面的也如此,这里需要注意的是_OWORD
是16个字节
v9 = 4F4243684E76495C65775554647C784Ch
v10 = 78796974435A466D497D57664E44714Ch
v11 = 4927612398162959439LL
最后还有个判断v12
是否等于0
的,这个先不管
根据v9
,v10
,v11
就可以写出exp
解出flag
了,这里需要注意的是,小端存储,所以要先把v9
,v10
和v11
翻转之后再进行拼接,得到
['0x4c', '0x78', '0x7c', '0x64', '0x54', '0x55', '0x77', '0x65', '0x5c', '0x49', '0x76', '0x4e', '0x68', '0x43', '0x42', '0x4f', '0x4c', '0x71', '0x44', '0x4e', '0x66', '0x57', '0x7d', '0x49', '0x6d', '0x46', '0x5a', '0x43', '0x74', '0x69', '0x79', '0x78', '0x4f', '0x5c', '0x50', '0x57', '0x5e', '0x65', '0x62', '0x44']
之后每四个为一组,将每组第一个字符提取出来,第2-4个统一提取出来,进行移位操作
tmp = []
index = 0
while index < 40:
tmp1 = encrypt[index]
tmp2 = encrypt[index+1:index+4]
print(tmp1, tmp2)
for i in tmp2:
tmp.append(i)
tmp.append(tmp1)
index += 4
print(tmp)
最后将tmp-65
与byte_4010
进行异或,便可得到最终的flag
exp.py:
v10 = [0x4F, 0x42, 0x43, 0x68, 0x4E, 0x76, 0x49, 0x5C, 0x65, 0x77, 0x55, 0x54, 0x64, 0x7C, 0x78, 0x4C]
v11 = [0x78,0x79,0x69,0x74,0x43,0x5A,0x46,0x6D,0x49,0x7D,0x57,0x66,0x4E,0x44,0x71,0x4C]
byte_4010 = [0x51, 0x57, 0x42, 0x6C, 0x6F, 0x67, 0x73]
v12 = [0x44, 0x62, 0x65, 0x5E, 0x57, 0x50, 0x5C, 0x4F]
v10.reverse()
v11.reverse()
v12.reverse()
# v10 = 'OBChNvI\ewUTd|xL'
# v11 = 'xyitCZFmI}WfNDqL'
# v12 = 'Dbe^WP\\O'
# v13 = 0
encrypt = []
for i in v10:
encrypt.append(hex(i))
for i in v11:
encrypt.append(hex(i))
for i in v12:
encrypt.append(hex(i))
print(encrypt,len(encrypt))
tmp = []
index = 0
while index < 40:
tmp1 = encrypt[index]
tmp2 = encrypt[index+1:index+4]
print(tmp1, tmp2)
for i in tmp2:
tmp.append(i)
tmp.append(tmp1)
index += 4
print(tmp)
flag = ''
for i in range(len(tmp)):
flag += chr((tmp[i] - 65) ^ byte_4010[i % 7])
print(flag)
得到flag:flag{QWB_water_problem_give_you_the_scor
发现少了点什么东西,这里不难发现是少了e}
,添上即是完整的flag,而e
的位置就是前面提到的v12 != v7
最终flag:flag{QWB_water_problem_give_you_the_score}
总结:这道题其实难度还是偏向简单的,比赛的时候解出来的也有几百支队伍,我感觉难的地方就在于初始化v4后又有个外部函数以v4作为参数,最开始看到这个就劝退了,等到比赛第二天才把这道题解了。这次的强网杯水题是挺多的,但是难的题也不少,刚刚接触逆向,还得慢慢学习。学习,是无止境的。