一筐萝卜 发表于 2018-11-11 21:22

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}

一筐萝卜 发表于 2018-11-12 18:25

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

Q火流星 发表于 2018-11-20 22:22

感觉是大佬

lbwb2 发表于 2018-11-11 21:52

好好学习天天上线

HasaH 发表于 2018-11-11 22:18

来了就发帖,支持新人,,

GLSakura 发表于 2018-11-11 22:47

我看不懂,给你点赞

超大的橙子 发表于 2018-11-11 22:50

想问下lz知道怎么把What’s_it解密后的creck函数dump出来吗

壹万叁 发表于 2018-11-11 23:00

谢谢分享,学习了

一筐萝卜 发表于 2018-11-11 23:35

我的排班有问题,第一次发,不是太美观,抱歉啦,谢谢支持

一筐萝卜 发表于 2018-11-11 23:42

超大的橙子 发表于 2018-11-11 22:50
想问下lz知道怎么把What’s_it解密后的creck函数dump出来吗

在od中按照程序的流程走,当走到check函数的时候就会有的,在ida中check函数中是没有的

一筐萝卜 发表于 2018-11-11 23:48

大家可以去我的博客上看这篇文章,https://luobuming.github.io,博客上比较清晰

btctw 发表于 2018-11-12 00:28

谢谢分享,学习了:lol
页: [1] 2 3 4 5 6 7 8
查看完整版本: 2018上海CTF“骇极杯”逆向WP