1625-5 王子昂 总结《2017年9月12日》 【连续第345天总结】
A. 问鼎杯题库-逆向两题
B.
C++写的,无壳
运行提示要求掷五次骰子,需要结果为3-1-3-3-7
很明显,是正常运行下不可能出现的结果
除了Enter外不接受输入,观察IDA
发现进行了大量处理,但没有字符串的显示,说明这里要不是混淆要不是加密
Shift+f12查找字符串才发现了真正的main,其实就在main的最后Return的地方调用的WinMain
最后对v89进行了多次处理,flag输出的是v88,那么向上要追踪v88或v89
总共进行了五次类似结构的判断,取其中一层分析
每段随机产生一个值,进行Case结构的判断,如果正确会对v89进行修改;同时通过前后时间比对是否超过两秒来反作弊
关键代码:
v84 = time(0);//取运行前时间
v86 = rand() % 6 + 1;
if ( v86 == 1 )
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v82);
if ( v86 == 2 )
{
v67 = 1;
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v81);
}
if ( v86 == 3 )
{
v67 = 1;
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v80);
}
if ( v86 == 4 )
{
v67 = 1;
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v79);
}
if ( v86 == 5 )
{
v67 = 1;
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v78);
}
if ( v86 == 6 )
{
v67 = 1;
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v77);
}
if ( v86 == 3 )//所需数字
{
v67 = 1;
v10 = std::operator<<<std::char_traits<char>>((int)&std::cout, "[*] You rolled a three! Good!");
v11 = std::ostream::operator<<(v10, std::endl<char,std::char_traits<char>>);
std::ostream::operator<<(v11, std::endl<char,std::char_traits<char>>);
v89 *= 2;//正确时对目标值的操作
std::string::operator=((std::string *)&v88, &byte_444240);
v16 = time(0);//运行后时间
v83 = v16;
v85 = v16 - v84;
if ( v16 - v84 > 2 ) // 作弊检测,通过两次time()函数所取时间是否大于两秒判断
v89 *= 2;//如果作弊则修改目标值
v67 = 1;
v17 = std::operator<<<std::char_traits<char>>(
(int)&std::cout,
"[*] Next you will need to throw a one, press enter to throw a dice!");
直接通过OD/IDA进行爆破,把每次判断时的跳转给转掉,就能得到正确的flag
注意作弊检测的地方没有字符串标识,cmp [ebp-0x30], 2的地方也要爆破过,否则在最后异或的时候会出错
if ( std::string::find((std::string *)&v88, "ebCTF", 0) == -1 )// 作弊检测,如果未出现正确字符串说明被检测到作弊,报错
{
v67 = 1;
v59 = std::ostream::operator<<(&std::cout, std::endl<char,std::char_traits<char>>);
v60 = std::operator<<<std::char_traits<char>>(
v59,
"[!] It seems you did something wrong :( No flag for you.");
跟踪v89被处理的流程,进行还原生成flag
通过OD发现最后flag是用一个内存中的字符串被多次异或处理得到的
在IDA中可以看到这个字符串的指针是v88,在输出之前与v91字符串逐个异或
v88的赋值语句为std::string::operator=((std::string *)&v91, &byte_444240);
向上溯源发现一共有三次对v91字符串的操作,分别是
1.将字符串赋给v91
std::string::operator=((std::string *)&v91, &byte_444309);
2.将字符串与v90逐个异或
v90的赋值在开头处:
if ( isDebuggerPresent() ) // 检查是否被反调试
v90 = 66;
else
v90 = 16;
看来是反调试措施,不过不知道为什么OD没有被触发……
查了一下该函数只针对Ring3(用户级)的调试器,可能是OD有插件搞定它了?
毕竟它只是一个可怜的会被调试器K.O.掉的反调试WinApi……╮(╯_╰)╭
3.将v91字符串与v89逐个异或
for ( i = 0; ; ++i )
{
v67 = 1;
v50 = std::string::size((std::string *)&v91);//取长度
if ( i >= v50 )
break;
v51 = (_BYTE *)std::string::operator[]((std::string *)&v91, i);//取v91的第i个字符
*v51 ^= v89;//逐个异或
}
最后将v88与v91逐个字符异或
得到的v88字符串如果包含ebCTF则说明流程正确,输出flag
还原脚本:
其中middle和flag由IDC脚本从内存中dump下来得到
#v91
middle=[2,55,15,53,15,60,21,7,60,48,42,48,85,18,55,21,30,53,1,81]
#v88
flag=[19,33,56,21,61,51,87,71,45,39,106,115,68,5,38,89,92,121,23,68,69,119,26,117,73,125,5,74,120,116,106,112,66,2,113,5,15,34,8]
#v90
debug = 16
v89 = 6
v89 *= 2
v89 += 4
v89 *= 3
v89 += 2
v89 *= 2
#混淆
# v89 *= 50
# v89 /= 50
# v89 += 65
# v89 -= 65
# v89 *= 42
# v89 /= 42
for i in range(len(middle)):
middle[i] ^= debug
middle[i] ^= v89
for i in range(len(flag)):
flag[i] ^= middle[i%len(middle)]
print(chr(flag[i]),end='')
ebCTF{64ec47ece868ba34a425d90044cd2dec}
下载下来是一个无后缀名的文件,用UltraEdit打开发现文件头是7z
添加zip后缀名,成功拖出一个ELF文件
拖入IDA:
一毛一样的掷骰子套路,连数字都不带变的
一样随机生成,依次检查数字,通过比较时间来反作弊
不过这次检查过程中没有幺蛾子,直接通过一个函数sub_4006B6生成的flag并输出
虽然在Linux中没有OD那么好用的动态调试器了,不过IDA的远程动态调试也勉勉强强可以用,直接修改rip至0x4010d5运行flag生成函数即可
逆向也是可以的,不过这次太麻烦了而且中间有一堆goto,全盘复制下来直接编辑运行就行了应该╮(╯_╰)╭
C. 明日计划
问鼎杯题库逆向