GCTF 2015 WriteUp : Reverse

各位中秋节快乐。

想想大概两年前这个时候,第一次参加这种东西,我还不懂啥叫WriteUp。后来还是咨询了圆神,在圆神的指点下才第一次写了WriteUp(基本上就是一句话)。还是得感谢圆神以及蓝鲸保护协会多年来的指点和帮助!于是我要爆圆神照片:

说实话我还是不想写WriteUp的,自己思路too young sometimes naive,而且那些胜者自然会去写的。但是为了不让博客长草,还是写上点东西。

这次GCTF有三道Reverse,本来我没想过能AK,但是看来出题方侧重Web,Reverse方面还算是比较基础,加壳加花之类的都没有,不过也没有.NET大法。

RE50

第一道还是比较简单的。
先看主函数:
 
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax@2 unsigned int v4; // esi@3 int v5; // eax@5 unsigned int v6; // edx@8 int v7; // [sp+0h] [bp-90h]@0 int v8; // [sp+0h] [bp-90h]@1 int v9; // [sp+8h] [bp-88h]@1 int v10; // [sp+Ch] [bp-84h]@1 int v11; // [sp+10h] [bp-80h]@1 int v12; // [sp+14h] [bp-7Ch]@1 char v13; // [sp+18h] [bp-78h]@1 v9 = dword_409078; //'5C26'
v10 = dword_40907C; //'011J'
v12 = dword_409084; //'L02'
memset(&v13, 0, 120u);
v11 = dword_409080; //'1911'
sub_4013C0((int)aPleaseInputYou, v7); //提示要求输入
gets(&v13); //取得输入
if ( strlen(&v13) <= 20 ) //显然可知输入长度应该在20以内
{
v4 = 0;
*(&v13 + strlen(&v13)) = 0; //确保字符串结尾为\0
if ( strlen(&v13) != 0 )
{
do
{
if ( !isalpha(*(&v13 + v4)) ) //isalpha是标准库的函数,判断是否为字母
{
v5 = (unsigned __int8)*(&v13 + v4);
if ( (_BYTE)v5 != '{' && (_BYTE)v5 != '}' ) //可知正确输入只含字母和左右大括号
{
sub_4013C0((int)aSorryYouReWron, v8);
exit(0);
}
}
++v4;
}
while ( v4 < strlen(&v13) );
}
_strlwr(&v13); //string.ToLower
v6 = 0;
if ( strlen(&v13) != 0 )
{
do
*(&v13 + v6++) -= '1'; //对每个字符减去char‘1’,即31
while ( v6 < strlen(&v13) );
}
if ( !strcmp(&v13, (const char *)&v9) ) //关键,比较对象是v9指向的地址代表的char数组
sub_4013C0((int)aGood, v8);
else
sub_4013C0((int)aSorryYouReWron, v8);
system(aPause);
result = 0;
}
else
{
sub_4013C0((int)aTooLong, v8);
result = 0;
}
return result;
}
可见关键问题就是,v9指向的到底是什么?用IDA查看,可知是十进制数893596214,转为char数组是‘5C26’,但是由于没有\0,strcmp还会继续读下去,从最上面的变量定义可以看出会接着读到v10,v11,v12。所以应该把v9~v12代表的字符全部加起来。但是其实还有个问题, 那就是大小端的转换问题……这里我一开始没有想到,于是又用OD跑了一遍,随便输入全是字母的值,一路往下,在看到输入被处理完(全部减‘1’)之后,真正的对比串就会显示出来:
所以应该是“62C5J110119120L”。
这个程序的主要含义是:将我们的输入的字符每个减‘1’,然后与目标字符串对比。确实很简单,难点只在于能不能正确地找到目标字符串。
于是我们逆推回去,每个字符加‘1’。
可得key:gctf{bbabbjbca}
 

RE100

这个题目挺有意思,然而考验的是算法,这么专业的算法题对于算法弱渣的我来说实在是太不友好了。
我们还是先看主函数,主函数直接调了一个函数:
这个函数一眼看上去,和第一题差不多。不过,首先他在第一行就调用了另一个(真正的)函数sub_401000,接着下一行它隐藏着一个错误,在IDA里已经用红色标注出来,试图向一个不存在的变量存数据。于是实际上它执行的是sub_401000。执行完之后再往下就出错并退出了。
然后看真正的函数:
 
signed int __usercall sub_401000@<eax>(int a1@<esi>)
{
  signed int v2; // edi@5
  signed int v3; // esi@5
  char v4; // al@6
  char v5; // al@11
  int v6; // [sp-4h] [bp-80h]@5
  int v7; // [sp+0h] [bp-7Ch]@0
  int v8; // [sp+0h] [bp-7Ch]@2
  char v9; // [sp+4h] [bp-78h]@2
  if ( dword_40B980 ) //默认是0,并不会退出
    return 1;
  dword_40B980 = 1;
  memset(&v9, 0, 120u);
  sub_4014F0((int)aPleaseInputYou, v7); //提示输入
  gets(&v9); //取得输入
  if ( strlen(&v9) != 25 ) //指明了输入长度必须为25
  {
    sub_4014F0((int)aSorryYouReWron, v8);
    system(aPause);
    return 1;
  }
  v6 = a1;
  v2 = 0;
  v3 = (signed int)asc_409046; //指向一个地址,观察可知这个地址位于一个字符数组的中间。此变量记录“当前位置”
  do //对于每个字符依次处理
  {
    v4 = *(&v9 + v2);
    if ( v4 != 'k' && v4 != 'j' && v4 != 'h' && v4 != 'l' ) //显然输入只能由hjkl四个字母组成
    {
      sub_4014F0((int)aSorryYouReWron, v6);
      system(aPause);
    }
    v5 = *(&v9 + v2);
switch ( v5 ) //对于每个字符... { case 'k': v3 -= 8; //后退8步 if ( v3 < (signed int)&unk_409030 || v3 > 4231279 || *(_BYTE *)v3 == '*' ) //小于最小边界或大于最大边界或“踩雷”,下同 { sub_4014F0((int)aSorryYouReWron, v6); system(aPause); } if ( *(_BYTE *)v3 == '#' ) //抵达目标点,下同 goto LABEL_37; break; case 'j': v3 += 8; //前进8步 if ( v3 < (signed int)&unk_409030 || v3 > 4231279 || *(_BYTE *)v3 == '*' )
{ sub_4014F0((int)aSorryYouReWron, v6); system(aPause); } if ( *(_BYTE *)v3 == '#' )
goto LABEL_37; break; case 'h': --v3; //后退1步 if ( v3 < (signed int)&unk_409030 || v3 > 4231279 || *(_BYTE *)v3 == '*' )
{ sub_4014F0((int)aSorryYouReWron, v6); system(aPause); } if ( *(_BYTE *)v3 == '#' )
goto LABEL_37; break; default: //相当于case 'l': ++v3; //前进1步 if ( v3 < (signed int)&unk_409030 || v3 > 4231279 || *(_BYTE *)v3 == '*' )
{ sub_4014F0((int)aSorryYouReWron, v6); system(aPause); } if ( *(_BYTE *)v3 == '#' ) //抵达‘#’意味着成功
{ LABEL_37:
          sub_4014F0((int)aGood, v6);
          system(aPause);
          break;
        }
        break;
    }
    ++v2;
  }
  while ( v2 < 25 );
  return 1;
}
所以这个程序是什么意思呢?其实是一个“过雷区”问题。
一开始我们位于地雷区中间的某个无地雷的位置,你可以使用kjhl四种指令,分别代表着前进8步、后退8步、前进1步和后退1步。任何时候不能走到有雷(*)的位置,也不能走出左边界或者右边界。最终必须要在第25步走到目标点(#)。
“地图”是这样的(^指向的是起始位置):
**    ***  **  ** **** **^******* *#   ** **** **  **  ***    **                         
所以我们需要写一个搜索来求得这个问题的解……深度优先搜索,我已然忘光……
 
 
        public static int Step = 22; //起始位置
        public static int Round = 0; //步数
        public static StringBuilder Answer = new StringBuilder(); //记录结果
        public static char[] Set = "hjkl".ToCharArray(); //可选指令
        public static char[] Map = "**    ***  **  ** **** ** ******* *#   ** **** **  **  ***    **".ToCharArray(); //地图
        public static void Searcher()
        {
            if (Round > 26) //步数超过25步仍未走到,此路不通
            {
                return;
            }
            //搜索4次
            foreach (char c in Set)
            {
                Answer.Append(c);
                ++Round;
                int save = Step;
                //如果可行(不踩雷不越界)进入下一波
                if (TreeSearch(c))
                {
                    Searcher();
                }
                //尝试过四种指令均不可行,回退
                Step = save;
                --Round;
                Answer.Remove(Answer.Length - 1, 1);
            }
        }
        public static bool TreeSearch(char s) //单次指令可行性判断
        {
            count++;
            switch (s)
            {
                case 'k':
                    Step -= 8;
                    break;
                case 'j':
                    Step += 8;
                    break;
                case 'h':
                    --Step;
                    break;
                case 'l':
                    ++Step;
                    break;
            }
            if (Step > Map.Length - 1 || Step < 0 || Map[Step] == '*') //越界或踩雷
            {
                return false;
            }
            if (Map[Step] == '#' && Answer.Length == 25) //正好25步,正好到目标点
            {
                Console.WriteLine(Answer.ToString()); //打印结果
                Console.WriteLine("SUCCESS");
            }
            return true;
        }
public static void Main()
{
Searcher(); //调用一次,之后自动递归调用
}
 
运行程序跑出结果:khkhhhjhjjjjjljlllklkkhhh。只有这一种结果。
提交key,不对,纠结了我半晚上。后来又做出另一道题,才来了灵感:
key:gctf{khkhhhjhjjjjjljlllklkkhhh}


RE200

RE200在结构上复杂了很多,但是真正找起key来却也不是很麻烦,但是首先你得分析个挺长时间,特别是我这种纯新手,根本不懂如何不看程序直接挖key。。。
 
先看程序:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // edi@0 //别看这么多变量,大部分都只是内部用到
  int v4; // edx@1
  char v5; // al@1
  int v6; // edx@5
  int v7; // ecx@5
  int v8; // eax@5
  int v9; // ecx@5
  int v10; // edx@6
  int v11; // ecx@6
  int v12; // eax@6
  int v13; // ecx@6
  int v14; // edx@7
  int v15; // ecx@7
  int v16; // eax@7
  int v17; // ecx@7
  int v18; // eax@7
  const char *v19; // edi@7
  int v20; // ecx@7
  int v21; // eax@7
  int v22; // ecx@7
  int v23; // edx@9
  int v24; // ecx@9
  int v25; // eax@9
  int v26; // ecx@9
  int v27; // edx@10
  char v28; // cl@10
  char *v29; // eax@10
  int v30; // edx@13
  int v31; // ecx@13
  int v32; // eax@13
  int v33; // ecx@13
  int v34; // edx@16
  int v35; // ecx@16
  int v36; // eax@16
  int v37; // ecx@16
  char *v38; // eax@16
  char v39; // cl@16
  int v41; // [sp-8h] [bp-8h]@2
  v4 = dword_40CC94; //v4相当于PC(程序计数器)
  v5 = byte_409034[dword_40CC94]; //这个byte_409034相当于“专用内存”,v5相当于IR(指令寄存器)
  if ( v5 != '#' ) //做过上一题很容��想到,终点应该还是#
  {
    v41 = v3;
    do
    {
      switch ( v5 )
      {
        case 0x55: //主操作码0x55,包含IO指令
          ++v4;
          switch ( byte_409034[v4] )
          {
            case 0xE0: //次操作码0xE0,取得输入(看做scanf)
              v6 = v4 + 1;
              v7 = (unsigned __int8)byte_409034[v6];
              v8 = (unsigned __int8)byte_409035[v6];
              v6 += 2;
              v9 = (((v7 << 8) | v8) << 8) | (unsigned __int8)byte_409034[v6];
              dword_40CC94 = v6 + 1;
              gets(&byte_409034[(v9 << 8) | (unsigned __int8)byte_409034[v6 + 1]]);
              goto LABEL_18;
            case 0xE2: //次操作码0xE2,取字符串长度(看做strlen)
              v10 = v4 + 1;
              v11 = (unsigned __int8)byte_409034[v10];
              v12 = (unsigned __int8)byte_409035[v10];
              v10 += 2;
              v13 = (((v11 << 8) | v12) << 8) | (unsigned __int8)byte_409034[v10];
              v4 = v10 + 1;
              dword_40CC80 = strlen(&byte_409034[(v13 << 8) | (unsigned __int8)byte_409034[v4]]);
              goto LABEL_19;
            case 0xE1: //次操作码0xE1,比较字符串(看做strcmp)
              v14 = v4 + 1;
              v15 = (unsigned __int8)byte_409034[v14];
              v16 = (unsigned __int8)byte_409035[v14];
              v14 += 2;
              v17 = (((v15 << 8) | v16) << 8) | (unsigned __int8)byte_409034[v14++];
              v18 = (v17 << 8) | (unsigned __int8)byte_409034[v14++];
              v19 = &byte_409034[v18];
              v20 = (v18 << 8) | (unsigned __int8)byte_409034[v14++];
              v21 = (v20 << 8) | (unsigned __int8)byte_409034[v14++];
              v22 = (v21 << 8) | (unsigned __int8)byte_409034[v14];
              v4 = v14 + 1;
              dword_40CC94 = v4;
              if ( !strcmp(v19, &byte_409034[(v22 << 8) | (unsigned __int8)byte_409034[v4]]) )
                dword_40CC80 = 0;
              goto LABEL_19;
            case 0xE3: //次操作码0xE3,输出(看做printf)
              v23 = v4 + 1;
              v24 = (unsigned __int8)byte_409034[v23];
              v25 = (unsigned __int8)byte_409035[v23];
              v23 += 2;
              v26 = (((v24 << 8) | v25) << 8) | (unsigned __int8)byte_409034[v23];
              dword_40CC94 = v23 + 1;
              sub_401466((int)&byte_409034[(v26 << 8) | (unsigned __int8)byte_409034[v23 + 1]], v41);
              goto LABEL_18;
            default:
              goto LABEL_19;
          }
        case 0x66:  //操作码0x66,主要用处是比较字符串长度是否符合要求
          v27 = v4 + 1;
          v28 = byte_409035[v27];
          v29 = &byte_409034[v27];
          v4 = v27 + 1;
          if ( v28 == 0xABu )
          {
            ++v4;
            dword_40CC94 = v4;
            if ( *v29 == (_BYTE)dword_40CC80 )
              dword_409030 = 1;
          }
          break;
        case 0x99: //操作码0x99,主要用处是跳转(调整PC也就是v4)
          v30 = v4 + 1;
          v31 = (unsigned __int8)byte_409034[v30];
          v32 = (unsigned __int8)byte_409035[v30];
          v30 += 2;
          v33 = (((v31 << 8) | v32) << 8) | (unsigned __int8)byte_409034[v30];
          v4 = v30 + 1;
          if ( dword_409030 != 1 )
            v4 = (v33 << 8) | (unsigned __int8)byte_409034[v4];
          dword_409030 = -1;
          break;
        case 0xEE: //操作码0xEE,主要用处是将字符串中的每个字符全部加某个值
          v34 = v4 + 1;
          v35 = (unsigned __int8)byte_409034[v34];
          v36 = (unsigned __int8)byte_409035[v34];
          v34 += 2;
          v37 = (((v35 << 8) | v36) << 8) | (unsigned __int8)byte_409034[v34++];
          v38 = &byte_409034[(v37 << 8) | (unsigned __int8)byte_409034[v34]];
          v4 = v34 + 1;
          dword_40CC94 = v4;
          v39 = *v38;
          if ( *v38 )
          {
            do
            {
              *v38 = v39 + byte_409034[v4];
              v39 = (v38++)[1];
            }
            while ( v39 );
LABEL_18:
            v4 = dword_40CC94;
          }
          break;
        default:
          break;
      }
LABEL_19:
      v5 = byte_409035[v4++]; //IR取PC指向的指令,然后PC指向下一条指令
      dword_40CC94 = v4;
    }
    while ( v5 != '#' );
  }
  system(aPause);
  return 0;
}
 
这个程序,其实是一个非常微型的虚拟机系统。这里说的虚拟机当然是指程序保护领域的虚拟机。
程序运行的时候,PC(v4)指向“内存”即byte_409034的首地址(即地址409034)。内存的开头部分是数据段,目前没有输入,都是0。PC就一直不停地向下走(参见LABEL_19位置),直到走到程序段(位于地址409168,此时v4=308)。
提笔先把程序段内容记下来:
 
这时IR总算取到了第一条有效指令0x55,于是跳转到0x55对应的程序部分开始执行。由于这还是个变长指令集,0x55下面还有几个分指令,于是PC再指下一个地址——取得0xE3,于是跳转到0xE3对应的程序部分。
于是第一条指令:
55 E3   00 00 01 76 作用为输出参数地址(4Byte)处所代表的字符串。这里地址是0x0176(实际地址=409034+0x176=4091AA:“please input your key: ”)。之后程序继续向下执行……
指令 作用
55 E0 取得输入
55 E2 取字符串长度
55 E1 字符串比较
55 E3 输出某个字符串
66 验证字符串长度是否符合某个值
99 跳转
EE 将某个字符串的每一位都加某个值
55 E0   00 00 00 00 取得���入,将输入存到参数(4Byte)位置。前面提到过,0x00是“内存”的首地址,即409034。这里是数据区,不会覆盖掉程序。
 
55 E2   00 00 00 00 取参数地址(4Byte)处字符串的长度。0x00是我们刚输入的字符串的位置。也就是取我们刚输入的字符串的长度。结果会存到某个“寄存器”中,这里叫dword_40CC80(简称CC80)。
 
66        13 AB 00 验证寄存器(CC80)的值(目前是字符串的长度)是否为第一个参数(1Byte),只有当参数2为0xAB时才会实际验证,第三个参数完全用不到。这里验证的是长度是否为0x13(即19位),由此可推断正确的key长度应为19。这时为了取得上一步得到的字符串的长度,就用到了刚才的寄存器CC80。结果会存到某个“标志位”中,这里叫dword_409030(简称9030)。若相等则将标志位置1。
 
99        00 00 01 6E 跳转到参数地址(4Byte),若标志位(9030)为1,则不执行此次跳转,并把标志位重置为-1。这里如果输入正确,则标志位应为1,所以不会跳转,继续顺序执行到下一条指令。若输入不正确,会跳转到地址0x016E,然后由于PC+1,实际地址409034+0x16E+0x1=4091A3,会执行最后一条语句。当然最后一条语句就是输出错误。
 
EE        00 00 00 00 80 取得参数1地址(4Byte)处的字符串,将它的每个字符都加参数2(1Byte)的值。此处取得的是我们输入的字符串,相加的值为0x80。
 
55 E1   00 00 01 8E 00 00 00 00 比较参数1地址(4Byte)和参数2地址(4Byte)处的字符串。这里比较的是0x018E处和我们输入的字符串,结果会存到“寄存器”CC80中,若相等将会存入0(会在66判断语句中用得到)。关键点到了!0x018E显然就是我们要找的目标串的位置(实际位置409034+0x18E=4091C2)!到这里,剩下的语句其实已经不用看了(对于老手来说,恐怕一句没看都已经找到了)。
对这个串的每个值,都减掉0x80。
可得key:gctf{just_add_0x80}
当然接下来会继续把程序看完。
 
 
 
66        00 AB 00 之前刻意说成“验证寄存器的值”,原因就是这里验证的值并不是字符串的长度。目的很明确,就是要验证上面比较的结果。如果上面比较的结果是相同的,则将标志位(9030)置1(下一句跳转指令用)。如果不置1,运行到这里标志位应为-1。
 
99        00 00 01 6E 如果比较的结果不相同,通过这句指令会跳转到0x016E(最后一句指令)。如果我们的输入是正确的,因为标志位为1,所以此句跳转不会执行,成功进入下一个命令。
 
55 E3   00 00 01 BE 输出正确提示。0x01BE的实际地址=409034+0x01BE=4091F2。.data:004091F2 aGood    db 'good',0
 
# 程序退出标志。
 
55 E3   00 00 01 A2 输出错误提示。0x01A2的实际地址=409034+0x01A2=4091D6。.data:004091D6 aSorryYouReWron db 'sorry you',27h,'re wrong',0
 
至此程序段分析完成。
 
其他做出的题目都已经有别人出了更好的WriteUp,现在并不想写了,可能有空会再补一个。
 
_Ulysses
 

 



评论 (2) -

添加评论

Loading