2018上海CTF“骇极杯”逆向WP
本帖最后由 一筐萝卜 于 2018-11-19 19:10 编辑CPP
[*]题目是一个64位ELF文件
[*]拖入IDA中f5查看main函数伪代码
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char v4; //
char v5; //
char v6; //
unsigned __int64 v7; //
v7 = __readfsqword(0x28u);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v4, a2, a3);
std::operator<<<std::char_traits<char>>(&std::cout, "input flag:");
std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, &v4);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v6, &v4);
sub_4010A2((__int64)&v5, (__int64)&v6, (__int64)&v6);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v6);
sub_401332((__int64)&v5);
sub_40154E((__int64)&v5);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v4);
return 0LL;
}
[*]通过伪代码可以看出来,这是一个用C++写的
[*]这么长一串其实是引用了C++的字符串类,当时我也是没有看懂,也是第一次遇到
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
[*]通过引用字符串"input flag:"我们可以推断出“std::operator<<<std::char_traits<char>>(&std::cout, "input flag:");”是输出字符串的意思,而std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, &v4)是输入字符串的意思
[*]通过动态调试可以发现std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v6, &v4);这个函数是将v4的值复制到v6
[*]然后进入sub_4010A2,通过看汇编代码可以发现,这个函数其实是传入两个参数“sub_4010A2(&v5,&v6)”
void __fastcall sub_4010A2(__int64 a1, __int64 a2, __int64 a3){
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(a1, a2, a3);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(a1, a2);// 把a2的值复制到a1
sub_40111A(a1);
sub_40110E();
}
[*]在函数里面首先是将a2的值传入a1中,也就是将v6的值传入v5
[*]然后进入sub_40111A()
unsigned __int64 __fastcall sub_40111A(__int64 a1)
{
_BYTE *v1; // rbx
int v2; // er12
const char *v3; // rax
int i; //
char s1; //
char v7; //
char v8; //
char v9; //
char v10; //
char v11; //
char v12; //
char v13; //
char v14; //
char v15; //
char v16; //
char v17; //
char v18; //
char v19; //
char v20; //
char v21; //
char v22; //
char v23; //
char v24; //
char v25; //
char v26; //
char v27; //
char v28; //
char v29; //
char v30; //
char v31; //
char v32; //
char v33; //
char v34; //
char v35; //
char v36; //
char v37; //
char v38; //
char v39; //
char v40; //
char v41; //
char v42; //
char v43; //
char v44; //
char v45; //
char v46; //
char v47; //
char v48; //
unsigned __int64 v49; //
v49 = __readfsqword(0x28u);
s1 = 0x99u;
v7 = 0xB0u;
v8 = 0x87u;
v9 = 0x9Eu;
v10 = 0x84u;
v11 = 0xA0u;
v12 = 0xCBu;
v13 = 0xEFu;
v14 = 0x88u;
v15 = 0x90u;
v16 = 0xBBu;
v17 = 0x8Eu;
v18 = 0x91u;
v19 = 0xE0u;
v20 = 0xD2u;
v21 = 0xAEu;
v22 = 0xD4u;
v23 = 0xC5u;
v24 = 0x6F;
v25 = 0xD7u;
v26 = 0xC0u;
v27 = 0x68;
v28 = 0xC6u;
v29 = 0x6A;
v30 = 0x81u;
v31 = 0xC9u;
v32 = 0xB7u;
v33 = 0xD7u;
v34 = 0x61;
v35 = 4;
v36 = 0xDAu;
v37 = 0xCFu;
v38 = 0x3D;
v39 = 0x5C;
v40 = 0xD6u;
v41 = 0xEFu;
v42 = 0xD0u;
v43 = 0x58;
v44 = 0xEFu;
v45 = 0xF2u;
v46 = 0xADu;
v47 = 0xADu;
v48 = 0xDFu;
for ( i = 0;
i < (unsigned __int64)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::length(a1);
++i )
{
v1 = (_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, i);// a1
v2 = 4 * *(char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, i);
*v1 = ((*(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, i) >> 6) | v2) ^ i;
}
v3 = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(a1);
if ( strcmp(&s1, v3) == 0 )
{
sub_4012CE(a1);
exit(0);
}
return __readfsqword(0x28u) ^ v49;
}
[*]然后经过调试发现std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[]的意思是取我们输入的第i个值,即a1
.text:0000000000401234 movzx eax, byte ptr
.text:0000000000401237 movsx eax, al
.text:000000000040123A shl eax, 2
[*]首先是先将a1[\i]进行左移两位
[*]然后将a1的值算术右移6,然后与v2进行或运算,即与“a<<2”进行或运算,随后与i的值进行异或
[*]这一轮下来,总结一下这个循环干了什么:a=(a<<2|a>>6)^i,该循环将a1的每一位都进行这样的操作
[*]当循环结束后,与s1进行比较,所以我们要把s1的数据提取出来,然后根据刚刚的运算进行反推,脚本如下:
s1 =
flag = ""
for x in range(0,len(s1)):
for asc in range(32,128):
if (asc>>6|asc<<2)&0xff==s1^x:
flag+= chr(asc)
break
print flag
[*]我在写脚本的时候也遇到一个错误,就是我之前没遇到过,最开始我的脚本是这样的,“(asc>>6|asc<<2)==s1^x”,然后死活也跑不出来东西,经过查阅之后发现还需要加一个&0xff的操作
[*]该脚本跑出来的是“flag is: flag{7h15_15_4_f4k3_F14G_=3=_rua!}”
[*]当比较成功之后会进入sub_4012CE()函数里面,这个函数的作用就是输出两句话:“good job,but .....”,很显然,题目并没有这个简单,接着往下面分析
[*]std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string,该函数应该是把刚刚分配的变量v6给撤销掉,然后进入sub_401332
unsigned __int64 __fastcall sub_401332(__int64 a1)
{
_BYTE *v1; // r12
int v2; // ebx
char v3; // r13
const char *v4; // rax
signed int i; //
signed int j; //
char s; //
__int64 v9; //
__int64 v10; //
__int64 v11; //
char v12; //
unsigned __int64 v13; //
v13 = __readfsqword(0x28u);
v12 = 0;
s = -103;
s = -80;
s = -121;
s = -98;
s = 112;
s = -24;
s = 65;
s = 68;
v9 = 0x5855BC749A8B0405LL;
v10 = -1920493130842480203LL;
v11 = -3031743088776258207LL;
for ( i = 0; i <= 3; ++i )
{
for ( j = 1; j < strlen(s); ++j )
{
v1 = (_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, j);
v2 = *(unsigned __int8 *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
a1,
j);
v3 = *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, j - 1) | v2;
LOBYTE(v2) = *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
a1,
j);
*v1 = v3 & ~(v2 & *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
a1,
j - 1));
}
}
v4 = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(a1);
if ( strncmp(v4, s, 32uLL) == 0 )
sub_401522();
return __readfsqword(0x28u) ^ v13;
}
[*]进入函数之后,先进行一系列的赋值操作,然后是一个大循环,循环次数为4次,在这个大循环中套着一个小循环,小循环的次数是我们字符串s的长度,经过在上一个check函数跳过的坑,所以我们对这一个check函数直接看汇编代码
[*]程序先将a取出来,再将a|a,当我调试的时候发现第一次或运算的两个数是0xc0和0xc5,这个我就很是纳闷了,我之前输入的是012345....,怎么变成了0xc0、0xc5了呢,然后回头一想,我们的字符串在之前的check函数进行过一次处理
str1 = "0123456789abcdef"
i=0
for x in str1:
print hex(((ord(x)<<2)|(ord(x)>>6))&0xff^i),
i+=1
out:0xc0 0xc5 0xca 0xcf 0xd4 0xd1 0xde 0xdb 0xe8 0xed 0x8f 0x82 0x81 0x9c 0x9b 0x96
[*]emmmmmmm,这就对上了,第一次“0xc5|0xc0=0xc5”,然后将计算的结果和a进行&运算"0xc5 & 0xc0 = 0xc0 ",然后再将结果进去not(按位取反)“~0xc0”,然后和a进行and操作,最后得到的结果为5,综上所述,这个多的运算其实是将a=a^a,验证一下:
print 0xc0^0xc5
out:5
[*]和刚刚的到的结果吻合,所以我们的推测是对的,根据刚刚的调试简化sub_401332这个函数
for i in range(4):
for j in range(32):
a = a^a
[*]当循环结束后,把处理过的字符串和s进行比较,如果正确的话,就会输出“you got it!”,看来这才是真正的flag,脚本如下:
list1 =
for x in range(4):
for x in range(31,0,-1):
list1 = list1^list1
flag = ""
for x in range(len(list1)):
flag+=chr(((list1^x)<<6|(list1^x)>>2)&0xff)
print flag
[*]写这个脚本的时候我还是有点迷,迷在他们之间的关系,check2异或运算是从第2个开始的加密
加密:
a= 4 a = 5 a = 6
a = a^a = 5^4 = 1
a = a^a = 6^1 = 7
解密:
a = a^a = 7^1 = 6
a = a^a = 1^4 = 5
What’s_it
[*]该题目为32位PE文件,使用PEID检测发现没有加壳
[*]拖入IDA中,查看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int8 v4; //
unsigned __int8 v5; //
char v6; //
char v7; //
char Str; //
__int16 v9; //
char v10; //
int k; //
int j; //
int i; //
int v14; //
int v15; //
__main();
*(_DWORD *)Str = 0;
v9 = 0;
v10 = 0;
memset(v6, 0, sizeof(v6));
v7 = 0;
*(_DWORD *)v5 = 0;
*(_DWORD *)v4 = 0;
v15 = 0;
v14 = 0;
printf("Please input your luck string:");
scanf("%s", Str);
if ( strlen(Str) != 6 )
return 0;
for ( i = 0; i <= 5; ++i )
{
if ( Str <= '`' || Str > 'z' ) // 限制string全部为小写
return 0;
}
getMD5(Str, v6); // v6是加密后的ozulmt
for ( j = 0; j <= 31; ++j )
{
if ( v6 == '0' )
{
++v15;
v14 += j;
}
}
if ( 10 * v15 + v14 == 403 )
{
for ( k = 0; k <= 3; ++k )
{
v5 = v6; // v5是md5的前四位
v4 = v6; // v4是md5的后四位
}
decode(v4);
}
check(v5);
return 0;
}
[*]该程序首先让你输入一个luck string,规定长度为6,必须全部是小写字母,并且md5加密后,0的个数和所在的下标满足“ 10 * 个数 + 下标和 == 403”
[*]接下来就是用python写脚本进行爆破
false]
# coding:utf-8
import hashlib
def md5encode(str1):
a = hashlib.md5()
a.update(str1)
return a.hexdigest()
def seek_luckstring():
string = ""
for a in range(97,123):
for b in range(97,123):
for c in range(97,123):
for d in range(97,123):
for e in range(97,123):
for f in range(97,123):
string = chr(a)+chr(b)+chr(c)+chr(d)+chr(e)+chr(f)
md5_string = md5encode(string)
v14=0
v15=0
for x in range(len(md5_string)):
if md5_string=="0":
v15+=1
v14+=x
if 10*v15+v14==403:
print string
exit(0)
seek_luckstring()
[*]最后得出luckstring是“ozulmt”
[*]然后通过循环分别给v4,v5赋值def seek_v4v5():
def seek_v4v5():
luckstring = md5encode("ozulmt")
print luckstring
v4 = [""]*4
v5 = [""]*4
for x in range(4):
v5 = luckstring
v4 = luckstring
print v4 #['0', '1', '0', 'e']后四位
print v5 #['0', 'e', 'c', '4']前四位
[*]然后进入decode函数
// write access to const memory has been detected, the output may be wrong!
signed int __cdecl decode(unsigned __int8 *a1)
{
signed int result; // eax
signed int j; //
signed int i; //
unsigned int Seed; //
Seed = 0;
for ( i = 0; i <= 3; ++i )
Seed += a1; // seed = 246
srand(Seed);
*(_BYTE *)check ^= 0x96u; // check = 0xc3
for ( j = 1; ; ++j )
{
result = j;
if ( j >= 305 )
break;
*((_BYTE *)check + j) ^= rand();
}
return result;
}
[*]这个函数是对check函数的字节码进行操作,将数组v4中的每一个值得ASCII码加起来作为srand的种子,然后将check的每一个字节码都与rand()产生的随机数进行异或
[*]我们知道,当srand()的种子一样的情况下,生成的rand()随机值也是一样的,所以check函数是可以恢复的
[*]我动态调试的时候,check函数确实恢复成功了
[*]然后转成伪C代码:
[*]通过观察,我们可以知道这个函数首先让你输入一个flag,然后进入到checkht函数中
char *__cdecl checkht(char *Dest)
{
int v1; // eax
char Source; //
__int16 v4; //
int v5; //
__int16 v6; //
int j; //
int i; //
int v9; //
v5 = 1734437990;
v6 = 123;
memset(Source, 0, sizeof(Source));
v4 = 0;
v9 = 5;
for ( i = 0; ; ++i )
{
v1 = strlens(Dest);
if ( v1 - 1 <= v9 )
break;
if ( Dest == '-' ) // 去掉我们输入字符串中的"-",新字符串存在source中
--i;
else
Source = Dest;
++v9;
}
for ( j = 0; j <= 4; ++j )
{
if ( Dest != *((_BYTE *)&v5 + j) ) // 前5位位 "flag{"
{
printfs(0);
exit(0);
}
}
if ( Dest != Dest ) //Dest == Dest
{
printfs(0);
exit(0);
}
if ( Dest != Dest )
{
printfs(0);
exit(0);
}
if ( Dest != Dest )
{
printfs(0);
exit(0);
}
if ( Dest != 45 )
{
printfs(0);
exit(0);
}
if ( Dest != '}' )
{
printfs(0);
exit(0);
}
return strcpy(Dest, Source);
}
[*]该函数首先把我们输入的字符串进行去除“-”,然后有几个判断,如果有一个不满足则会退出程序:
条件:
前5位位 "flag{"
Dest == Dest = "-"
Dest == Dest = "-"
Dest == Dest = "-"
Dest == 45 = "-"
最后一个为'}'
[*]最后将处理后的字符串返回,接着看check函数,又是利用随机值来生成一个正确的flag,饭后与我们的输入进行比较
def check2():
# v5 = ['0', 'e', 'c', '4']
# v10 = 0
# for x in range(len(v5)):
# v10 += ord(v5)
# print v10
# v10 = 300
ASCII = "0123456789abcdef"
rand =
flag = ""
for x in range(len(rand)):
flag+=ASCII%16]
print flag
#a197b847709253a47c41bc7d6d52e69d
[*]我们可以得到一串字符串a197b847709253a47c41bc7d6d52e69d,然后根据之前的那几个限制条件,得出最终的flag:flag{a197b847-7092-53a4-7c41-bc7d6d52e69d}
def check2():
# v5 = ['0', 'e', 'c', '4']
# v10 = 0
# for x in range(len(v5)):
# v10 += ord(v5)
# print v10
# v10 = 300
ASCII = "0123456789abcdef"
rand =
flag = ""
for x in range(len(rand)):
flag+=ASCII%16]
print flag
#a197b847709253a47c41bc7d6d52e69d
cyvm
[*]同样拖入IDA中查看伪C代码,发现是循环里套switch,代码非常多,我还以为是算法题,然后我分析了好长时间没分析出来,根据官方WP,这道题是一个VM题
[*]WP上有一句话值得我记住,就是“while循环套switch,不是迷宫就是vm”,这也许就是经验只谈吧,我这个没经验的人看起来这个题是算法题emmmmmmm
[*]判断是VM:1.看到这么多case,那一定是vm了 2.根据传入函数的a2和每次goto前的”+=”运算可以很容易的确定v5是读字节码的操作
[*]根据WP分析出:
s2reg 1
reg2s 2
mov 3
add 4
sub 5
xor 6
and 7
jmp 9
cmp 10
or 11
jnz 12
putc 13
ipt 15
movs 16
adds 17
inc 18
[*]这里贴上官方WP上出的VM指令
[*]总结算法就是:
input=input^i
[*]解题脚本如下:
#coding:utf-8
s1 =
for x in range(31,-1,-1):
s1 ^= s1^x
flag =""
for x in range(len(s1)):
flag+= chr(s1)
print flag
#flag{7h15_15_MY_f1rs7_s1mpl3_Vm}
N0nE_Seana 发表于 2018-11-12 12:21
不知道楼主能否分享以下题目源文件?
https://github.com/hacker-mao/ctf_repo/tree/master/%E7%AC%AC%E5%9B%9B%E5%B1%8A%E4%B8%8A%E6%B5%B7%E5%B8%82%E5%A4%A7%E5%AD%A6%E7%94%9F%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%A4%A7%E8%B5%9B 感觉是大佬 好好学习天天上线 来了就发帖,支持新人,, 我看不懂,给你点赞 想问下lz知道怎么把What’s_it解密后的creck函数dump出来吗 谢谢分享,学习了 我的排班有问题,第一次发,不是太美观,抱歉啦,谢谢支持 超大的橙子 发表于 2018-11-11 22:50
想问下lz知道怎么把What’s_it解密后的creck函数dump出来吗
在od中按照程序的流程走,当走到check函数的时候就会有的,在ida中check函数中是没有的 大家可以去我的博客上看这篇文章,https://luobuming.github.io,博客上比较清晰 谢谢分享,学习了:lol