BUUCTF 刷题笔记——Reverse 1

2023-03-15 14:56:01 浏览数 (3)

BUUCTF 刷题笔记——Reverse 1

easyre

  • 第一道题,题目提示非常简单的逆向并提供一个 zip 压缩包,下载本地解压后是一个 exe 可执行文件。尝试用 IDA 反编译,发现 flag 出来了。

感谢善待新人

reverse1

依然给了一个压缩文件,解压后依然是一个 exe 可执行文件,再次尝试用 IDA 反编译,这次没有一眼看到 flag 了,甚至连主函数都没有。于是 Shift F12 找找特别的字符串,发现了 this is the right flag!:

找到使用该字符串的位置,发现是如下 sub_1400118C0() 函数:

代码语言:javascript复制
__int64 sub_1400118C0()
{
  char *v0; // rdi
  __int64 i; // rcx
  size_t v2; // rax
  char v4[36]; // [rsp 0h] [rbp-20h] BYREF
  int j; // [rsp 24h] [rbp 4h]
  char Str1[224]; // [rsp 48h] [rbp 28h] BYREF
  __int64 v7; // [rsp 128h] [rbp 108h]

  v0 = v4;
  for ( i = 82i64; i; --i )
  {
    *(_DWORD *)v0 = -858993460;
    v0  = 4;
  }
  for ( j = 0; ;   j )
  {
    v7 = j;
    if ( j > j_strlen(Str2) )
      break;
    if ( Str2[j] == 111 )
      Str2[j] = 48;
  }
  sub_1400111D1("input the flag:");
  sub_14001128F(" s", Str1);
  v2 = j_strlen(Str2);
  if ( !strncmp(Str1, Str2, v2) )
    sub_1400111D1("this is the right flag!n");
  else
    sub_1400111D1("wrong flagn");
  sub_14001113B(v4, &unk_140019D00);
  return 0i64;
}

这段程序就是读取用户输入数据,并于内部字符串 Str2 作比较,比较正确说明是正确 flag。值得注意的是用于比较的内部字符串在参与比较前作了以下操作:

代码语言:javascript复制
if ( Str2[j] == 111 )
	Str2[j] = 48;

将 ASCII 码值为 111 的字符替换为码值为 48 的字符,即将 o 替换为 0。

在 IDA 中可以查看字符串 Str2 的值为 {hello_world},替换后即为 {hell0_w0rld}。根据 BUU 的提示,加上 flag 前缀即可提交。有人不看提示直接提交喜提错误我不说是谁。

reverse2

本题的文件没有后缀名,不过因为惯性还是直接 IDA 反编译,本次含有主函数了,并且有 flag 出没:

代码语言:javascript复制
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int stat_loc; // [rsp 4h] [rbp-3Ch] BYREF
  int i; // [rsp 8h] [rbp-38h]
  __pid_t pid; // [rsp Ch] [rbp-34h]
  char s2[24]; // [rsp 10h] [rbp-30h] BYREF
  unsigned __int64 v8; // [rsp 28h] [rbp-18h]

  v8 = __readfsqword(0x28u);
  pid = fork();
  if ( pid )
  {
    waitpid(pid, &stat_loc, 0);
  }
  else
  {
    for ( i = 0; i <= strlen(&flag);   i )
    {
      if ( *(&flag   i) == 105 || *(&flag   i) == 114 )
        *(&flag   i) = 49;
    }
  }
  printf("input the flag:");
  __isoc99_scanf(" s", s2);
  if ( !strcmp(&flag, s2) )
    return puts("this is the right flag!");
  else
    return puts("wrong flag!");
}

与前一关类似,也是通过判断 ASCII 码值来做一些替换,不过本题是将字符 i 与字符 r 都替换为字符 1。查询内部字符串 flag 如下,由于程序是通过首字符地址来访问的 C 类型字符串,反编译时会分隔开来,因此完整字符串为 {hacking_for_fun}。

替换后为 {hack1ng_fo1_fun},因此 flag 为 flag{hack1ng_fo1_fun}。这边每次都要自己组 flag,老是忘记。

内涵的软件

本题给的文件是一个 32 位的可执行文件,因此使用 32 位版的 IDA 打开,主函数仅仅调用了一个 main_0() 函数而已,因此查看该函数:

代码语言:javascript复制
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  char v4[4]; // [esp 4Ch] [ebp-Ch] BYREF
  const char *v5; // [esp 50h] [ebp-8h]
  int v6; // [esp 54h] [ebp-4h]

  v6 = 5;
  v5 = "DBAPP{49d3c93df25caad81232130f3d2ebfad}";
  while ( v6 >= 0 )
  {
    printf(&byte_4250EC, v6);
    sub_40100A();
    --v6;
  }
  printf(asc_425088);
  v4[0] = 1;
  scanf("%c", v4);
  if ( v4[0] == 89 )
  {
    printf(aOd);
    return sub_40100A();
  }
  else
  {
    if ( v4[0] == 78 )
      printf(&byte_425034);
    else
      printf(&byte_42501C);
    return sub_40100A();
  }
}

虽然挺长一段代码,但实测开头的那个长得像 flag 的局部变量 v5 里面便是 flag,不过将 DBAPP 换成 flag 即可。

新年快乐

本题文件为 32 位可执行文件,但是在 IDA 中打开后却发现仅有两个函数,代码也奇奇怪怪找不到啥关键字,而且程序大部分数据所在的段名都含有此前没见过的 UPX。

基本可以确定,碰上个人第一次接触的加壳程序了,即类似压缩文件不过解压过程在执行时在内存中自动完成,因此程序可正常执行但是却无法反编译出多少有效信息,加壳主要用于压缩与加密。不过还好只是入门级的 UPX 压缩壳,可以去 他们官网 下载加壳程序,使用 -d 参数即可完成脱壳:

代码语言:javascript复制
upx -d [文件路径]

看到如下界面即脱壳成功,此时文件就只是一个普通的可执行文件了。当然脱壳的方法有很多,这里暂时不作过多考究,本小白还是慢慢来。

脱壳之后即可在 IDA 反编译出原程序了,主函数如下,有众多关键字 flag 出没:

代码语言:javascript复制
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char Str2[14]; // [esp 12h] [ebp-3Ah] BYREF
  char Str1[44]; // [esp 20h] [ebp-2Ch] BYREF

  __main();
  strcpy(Str2, "HappyNewYear!");
  memset(Str1, 0, 32);
  printf("please input the true flag:");
  scanf("%s", Str1);
  if ( !strncmp(Str1, Str2, strlen(Str2)) )
    return puts("this is true flag!");
  else
    return puts("wrong!");
}

程序依然是老流程,读取数据并与内部字符串 HappyNewYear! 作比较,不过这次没对内部字符串做任何修改,因此组合后的 flag{HappyNewYear!} 便是 flag。

xor

下载文件,解压发现含有 _MACOSX,因此应该是 MAC 来的文件,使用 Exeinfo PE 工具小查一下,确定是 64 位的 MAC 可执行程序,并且没有加壳。

直接 IDA 反编译,发现主函数:

代码语言:javascript复制
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp 2Ch] [rbp-124h]
  char __b[264]; // [rsp 40h] [rbp-110h] BYREF

  memset(__b, 0, 0x100uLL);
  printf("Input your flag:n");
  get_line(__b, 256LL);
  if ( strlen(__b) != 33 )
    goto LABEL_7;
  for ( i = 1; i < 33;   i )
    __b[i] ^= __b[i - 1];
  if ( !strncmp(__b, global, 0x21uLL) )
    printf("Success");
  else
LABEL_7:
    printf("Failed");
  return 0;
}

虽然源码很容易就得到了,不过相比之前,本题对 flag 的处理更加有趣一些,首先读取用户输入,并且长度必须为 33,即 flag 会有 33 个字符。获取用户输入后程序会将输入字符串的后 32 位逐个与前一位作异或运算,计算后的结果与内部字符串相等才是 flag。也就是说 flag 经过一轮异或后会获得内部字符串,又由于对相同的数据异或两次数据就会复原,因此直接对内部字符串作一轮异或操作即可获得 flag。而内部字符串 global 如下,Shift e 获取字符数组,取数值便于计算:

然后写个脚本就可以计算出 flag 了,脚本如下。值得注意的是脚本中的异或运算结果不应存入数组中,因为原计算是基于前字符已经运算完成的情况下进行的,因此复原过程中的每个数据都应保持原样。

代码语言:javascript复制
glb = [0x66,0x0A,0x6B,0x0C,0x77,0x26,0x4F,0x2E,
       0x40,0x11,0x78,0x0D,0x5A,0x3B,0x55,0x11,
       0x70,0x19,0x46,0x1F,0x76,0x22,0x4D,0x23,
       0x44,0x0E,0x67,6,0x68,0x0F,0x47,0x32,0x4F,0]

s = chr(0x66)
for i in range(1,33):
    s  = chr(glb[i] ^ glb[i-1])

print(s)

计算出的 flag 为 flag{QianQiuWanDai_YiTongJiangHu},千秋万代,一统江湖。

helloword

  • 是个 apk 文件!没想到这么快就来到安卓了,直接丢进 IDA 里看看,不过反编译 apk 需要在打开时选择 APK Android Package 才行。

  • 反编译出来相当多东西,没搞过安卓看到真的令人恐惧,直接 Shift F12 查找字符串,字符串也是一大堆,所幸可以使用 Ctrl F 搜索。结果直接就找到 flag,感谢饶命。

reverse3

再次回到普通的 exe 文件,使用 Exeinfo PE 打开查看一下先。是一个 32 位可执行文件,而且没加壳。

那好说,直接丢进 IDA 反编译,主函数依然仅仅调用了 main_0() 函数而已,因此直接查看该函数,有关键词 flag 出没:

代码语言:javascript复制
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  size_t v3; // eax
  const char *v4; // eax
  size_t v5; // eax
  char v7; // [esp 0h] [ebp-188h]
  char v8; // [esp 0h] [ebp-188h]
  signed int j; // [esp DCh] [ebp-ACh]
  int i; // [esp E8h] [ebp-A0h]
  signed int v11; // [esp E8h] [ebp-A0h]
  char Destination[108]; // [esp F4h] [ebp-94h] BYREF
  char Str[28]; // [esp 160h] [ebp-28h] BYREF
  char v14[8]; // [esp 17Ch] [ebp-Ch] BYREF

  for ( i = 0; i < 100;   i )
  {
    if ( (unsigned int)i >= 0x64 )
      j____report_rangecheckfailure();
    Destination[i] = 0;
  }
  sub_41132F("please enter the flag:", v7);
  sub_411375(" s", (char)Str);
  v3 = j_strlen(Str);
  v4 = (const char *)sub_4110BE(Str, v3, v14);
  strncpy(Destination, v4, 0x28u);
  v11 = j_strlen(Destination);
  for ( j = 0; j < v11;   j )
    Destination[j]  = j;
  v5 = j_strlen(Destination);
  if ( !strncmp(Destination, Str2, v5) )
    sub_41132F("rigth flag!n", v8);
  else
    sub_41132F("wrong flag!n", v8);
  return 0;
}

程序在读取用户输入后将输入数据丢进了 sub_4110BE() 函数做运算,然后把运算后数据放进一个 for 循环中逐个字符的码值加上索引,最终与内部字符串 Str2 相同则用户输入数据为 flag。这个 sub_4110BE() 函数令人在意,打开后发现其仅调用了一个 sub_411AB0() 函数,该函数内容实在有些复杂:

代码语言:javascript复制
void *__cdecl sub_411AB0(char *a1, unsigned int a2, int *a3)
{
  int v4; // [esp D4h] [ebp-38h]
  int v5; // [esp D4h] [ebp-38h]
  int v6; // [esp D4h] [ebp-38h]
  int v7; // [esp D4h] [ebp-38h]
  int i; // [esp E0h] [ebp-2Ch]
  unsigned int v9; // [esp ECh] [ebp-20h]
  int v10; // [esp ECh] [ebp-20h]
  int v11; // [esp ECh] [ebp-20h]
  void *v12; // [esp F8h] [ebp-14h]
  char *v13; // [esp 104h] [ebp-8h]

  if ( !a1 || !a2 )
    return 0;
  v9 = a2 / 3;
  if ( (int)(a2 / 3) % 3 )
      v9;
  v10 = 4 * v9;
  *a3 = v10;
  v12 = malloc(v10   1);
  if ( !v12 )
    return 0;
  j_memset(v12, 0, v10   1);
  v13 = a1;
  v11 = a2;
  v4 = 0;
  while ( v11 > 0 )
  {
    byte_41A144[2] = 0;
    byte_41A144[1] = 0;
    byte_41A144[0] = 0;
    for ( i = 0; i < 3 && v11 >= 1;   i )
    {
      byte_41A144[i] = *v13;
      --v11;
        v13;
    }
    if ( !i )
      break;
    switch ( i )
    {
      case 1:
        *((_BYTE *)v12   v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
        v5 = v4   1;
        *((_BYTE *)v12   v5) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
        *((_BYTE *)v12     v5) = aAbcdefghijklmn[64];
        *((_BYTE *)v12     v5) = aAbcdefghijklmn[64];
        v4 = v5   1;
        break;
      case 2:
        *((_BYTE *)v12   v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
        v6 = v4   1;
        *((_BYTE *)v12   v6) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
        *((_BYTE *)v12     v6) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
        *((_BYTE *)v12     v6) = aAbcdefghijklmn[64];
        v4 = v6   1;
        break;
      case 3:
        *((_BYTE *)v12   v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
        v7 = v4   1;
        *((_BYTE *)v12   v7) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
        *((_BYTE *)v12     v7) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
        *((_BYTE *)v12     v7) = aAbcdefghijklmn[byte_41A144[2] & 0x3F];
        v4 = v7   1;
        break;
    }
  }
  *((_BYTE *)v12   v4) = 0;
  return v12;
}

毕竟是伪代码,要直接在这里审计复杂算法还是太难了,这里从一个被反复使用的数组 aAbcdefghijklmn 入手,打开发现其内容为大小写字母、数字以及 、/、= 三个符号,这是 base64 的字符表啊!那就先按 base64 算。

也就是说,将内部字符串 Str2 每一位码值减去索引后再进行 base64 解码结果即为 flag,其中内部字符串 Str2 为 e3nifIH9b_C@n@dH,因此可编写脚本如下:

代码语言:javascript复制
import base64

Str2 = "e3nifIH9b_C@n@dH"
flag = ""

for i in range(len(Str2)):
    flag  = chr(ord(Str2[i]) - i)

print(base64.b64decode(flag))

执行结束后即可获得 b'{i_l0ve_you}',然而要提交到 BUU 平台的话就需要修改成 flag{i_l0ve_you} 才可通过。某人花了三次机会才知道需要这样提交,痛啊!

不一样的flag

先验一下文件,本题文件为 32 位可执行文件,没有加壳,很好。

直接 IDA 反编译,主函数里就有关键词 flag 出没,代码略长,因此大概需要好好审计一下主函数了。

代码语言:javascript复制
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  _BYTE v3[29]; // [esp 17h] [ebp-35h] BYREF
  int v4; // [esp 34h] [ebp-18h]
  int v5; // [esp 38h] [ebp-14h] BYREF
  int i; // [esp 3Ch] [ebp-10h]
  _BYTE v7[12]; // [esp 40h] [ebp-Ch] BYREF

  __main();
  v3[26] = 0;
  *(_WORD *)&v3[27] = 0;
  v4 = 0;
  strcpy(v3, "*11110100001010000101111#");
  while ( 1 )
  {
    puts("you can choose one action to execute");
    puts("1 up");
    puts("2 down");
    puts("3 left");
    printf("4 rightn:");
    scanf("%d", &v5);
    // 列出四个选项并等待用户输入
    if ( v5 == 2 )
    {
        *(_DWORD *)&v3[25];
      // 用户选择 down 则 v3[25] 值减一
    }
    else if ( v5 > 2 )
    {
      if ( v5 == 3 )
      {
        --v4;
        // 用户选择 left 则 v4 值减一
      }
      else
      {
        if ( v5 != 4 )
LABEL_13:
          exit(1);
          v4;
        // 用户选择 right 则 v4 值加一
      }
    }
    else
    {
      if ( v5 != 1 )
        goto LABEL_13;
      --*(_DWORD *)&v3[25];
      // 用户选择 up 则 v3[25] 值加一
    }
    for ( i = 0; i <= 1;   i )
    {
      if ( *(int *)&v3[4 * i   25] < 0 || *(int *)&v3[4 * i   25] > 4 )
        exit(1);
        // v3[25] 取值范围为 [0,4]
    }
    if ( v7[5 * *(_DWORD *)&v3[25] - 41   v4] == 49 )
      exit(1);
      // 指定位置码值不能等于 49,即字符 1
      // 41 为 v7 到 v3 的偏移
    if ( v7[5 * *(_DWORD *)&v3[25] - 41   v4] == 35 )
    {
      puts("nok, the order you enter is the flag!");
      exit(0);
      // 只有指定位置码值为 35 才算成功,即字符 #
    }
  }
}

代码主要逻辑是让用户选择上下左右的一个方向,然后通过用户的选择来对指定值做加减一的操作,由于最终比较字符是将上下移动的值乘 5 后与左右移动的值相加,最后减去到 v3 字符串变量的偏移得到的,可以认为程序将 v3 字符串变量视为每行 5 个元素的矩阵。此外,其中上下移动值 v3[25] 限定区间为 [0,4],而 v3 字符串变量共含有 25 个元素,因此可进一步确定程序将该字符串视为五行五列的矩阵。又由于每一步移动后的值不能为 1 且移动到 # 时才算成功,因此从初始地址开始一步一步移动到终点的路线图大致如下:

只需输入按照上述路线移动的数字序列即为 flag,因此本题 flag 为 flag{222441144222}。独立审计这一段代码可废了我好大劲。

SimpleRev

本题文件没有后缀名,丢进 Exeinfo PE 发现是 Linux 下的 64 位可执行文件,依旧没有加壳。

那还是直接丢进 IDA 中反编译,主函数中没有啥关键词出现,但是其调用了一个 Decry() 函数,点进去发现又是一段很长的代码,且包含一些关于 flag 的关键词。看来又要慢慢审计代码救命。

代码语言:javascript复制
unsigned __int64 Decry()
{
  char v1; // [rsp Fh] [rbp-51h]
  int v2; // [rsp 10h] [rbp-50h]
  int v3; // [rsp 14h] [rbp-4Ch]
  int i; // [rsp 18h] [rbp-48h]
  int v5; // [rsp 1Ch] [rbp-44h]
  char src[8]; // [rsp 20h] [rbp-40h] BYREF
  __int64 v7; // [rsp 28h] [rbp-38h]
  int v8; // [rsp 30h] [rbp-30h]
  __int64 v9[2]; // [rsp 40h] [rbp-20h] BYREF
  int v10; // [rsp 50h] [rbp-10h]
  unsigned __int64 v11; // [rsp 58h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  *(_QWORD *)src = 0x534C43444ELL;
  v7 = 0LL;
  v8 = 0;
  v9[0] = 0x776F646168LL;
  v9[1] = 0LL;
  v10 = 0;
  text = (char *)join(key3, v9);
  // jion() 为自定义函数,连接 key3 与 v9
  // v9 为整型数值,按小端序存储形式为 0x68 0x61 0x64 0x6F 0x77
  // 因此 text 值为 killshadow
  strcpy(key, key1);
  // 将 key1 的值 "ADSFK" 赋给 key
  strcat(key, src);
  // 将 src 拼接在 key 之后
  // src 同样为整形数据,按小端序存储形式为 0x4E 0x44 0x43 0x4C 0x53
  // 拼接后的值为 ADSFKNDCLS
  v2 = 0;
  v3 = 0;
  getchar();
  v5 = strlen(key);
  for ( i = 0; i < v5;   i )
  {
    if ( key[v3 % v5] > 64 && key[v3 % v5] <= 90 )
      key[i] = key[v3 % v5]   32;
    // 遍历 key 的每个字符,若为大写字母则改为小写字母
    // 故 key 值为 adsfkndcls
      v3;
  }
  printf("Please input your flag:");
  while ( 1 )
  {
    v1 = getchar();
    if ( v1 == 10 )
      break;
      // 读取到换行则退出循环
    if ( v1 == 32 )
    {
        v2;
      // 读取到空格则 v2 变量加一
    }
    else
    {
      if ( v1 <= 96 || v1 > 122 )
      {
        if ( v1 > 64 && v1 <= 90 )
        {
          str2[v2] = (v1 - 39 - key[v3 % v5]   97) % 26   97;
            v3;
          // 大写字母与 key 中元素逐个操作,转成某个小写字母
        }
      }
      else
      {
        str2[v2] = (v1 - 39 - key[v3 % v5]   97) % 26   97;
          v3;
        // 小写字母与 key 中元素逐个操作,转成某个小写字母
      }
      if ( !(v3 % v5) )
        putchar(32);
      // 循环使用了一次 key 之后打印一个空格
        v2;
    }
  }
  if ( !strcmp(text, str2) )
    // 处理过后的 str2 与 text 相同则输入值为 flag
    puts("Congratulation!n");
  else
    puts("Try again!n");
  return __readfsqword(0x28u) ^ v11;
}

程序在与用户交互前会处理好内部数据,即后续用于作比较的内部字符串 killshadow 以及配合处理用户输入数据的内部密钥 adsfkndcls,由于这些数据均已知,因此我们只需做找出转换后符合条件的字符串即可。逆运算唯一的复杂之处在于每次都使用了取模运算,由于仅对大小写字母做处理,且模为 26,因此每一位符合条件的字符都应该有大小写各一位。

到这就可以直接写脚本了,直接逐个字母遍历过去,符合条件就是 flag 的重要组分。

代码语言:javascript复制
key = "adsfkndcls"
text = "killshadow"
flag = ""

for i in range(0, len(text)):
    for j in range(65,91):  # 仅取大写字母
    # for j in range(97,123):   # 仅取小写字母
        if ord(text[i]) == (j - 39 - ord(key[i])   97) % 26   97:
            flag  = chr(j)

print(flag)

计算出大写字母序列 KLDQCUDFZO 与小写字母序列 efxkwoxzti,虽然理论上大写版与小写版乃至他们交叉版本均符合条件,但是实测 BUU 仅接受 flag{KLDQCUDFZO}。

Java逆向解密

本题文件直接给了一个 class 文件,这我知道,拉进 idea 就可以反编译了。

代码语言:javascript复制
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.util.ArrayList;
import java.util.Scanner;

public class Reverse {
    public Reverse() {
    }

    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        System.out.println("Please input the flag :");
        String str = s.next();
        System.out.println("Your input is :");
        System.out.println(str);
        char[] stringArr = str.toCharArray();
        Encrypt(stringArr);
    }

    public static void Encrypt(char[] arr) {
        ArrayList<Integer> Resultlist = new ArrayList();

        for(int i = 0; i < arr.length;   i) {
            int result = arr[i]   64 ^ 32;
            Resultlist.add(result);
        }

        int[] KEY = new int[]{180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65};
        ArrayList<Integer> KEYList = new ArrayList();

        for(int j = 0; j < KEY.length;   j) {
            KEYList.add(KEY[j]);
        }

        System.out.println("Result:");
        if (Resultlist.equals(KEYList)) {
            System.out.println("Congratulations!");
        } else {
            System.err.println("Error!");
        }
    }
}

看多了 C 语言伪代码再看这种的就很舒适,程序逻辑非常简单,对输入字符串逐个进行加 64 后与 32 进行异或的操作,值得注意的是,加号的优先级是高于异或运算符的。当计算结果与内部的 KEY 数组内容一样,用户输入的数据即为 flag。

操作比较简单,直接写脚本进行逆操作:

代码语言:javascript复制
KEY = [180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65]
flag = ""

for i in range(len(KEY)):
    flag  = chr((KEY[i] ^ 32) - 64)

print(flag)

运行之后便获得了 flag,提交 flag{This_is_the_flag_!} 即可。

[GXYCTF2019]luck_guy

本题文件为 Linux 系统中的 64 位可执行程序,没有加壳。

丢进 IDA 中反编译,主函数中多半是寒暄,值得注意的是其中的 patch_me() 函数:

代码语言:javascript复制
int __fastcall patch_me(int a1)
{
  if ( a1 % 2 == 1 )
    return puts("just finished");
  else
    return get_flag();
}

该函数在判断输入为偶数时会调用 get_flag() 函数,这显然就是我们的目标函数了。该函数代码依旧很长,因此,又要慢慢审计了:

代码语言:javascript复制
unsigned __int64 get_flag()
{
  unsigned int v0; // eax
  int i; // [rsp 4h] [rbp-3Ch]
  int j; // [rsp 8h] [rbp-38h]
  __int64 s; // [rsp 10h] [rbp-30h] BYREF
  char v5; // [rsp 18h] [rbp-28h]
  unsigned __int64 v6; // [rsp 38h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  v0 = time(0LL);
  srand(v0);
  // 以时间作为随机数种子
  for ( i = 0; i <= 4;   i )
  {
    switch ( rand() % 200 )
        // rand() 以时间为种子,因此几乎无法预测随机序列,不过这不重要
    {
      case 1:
        puts("OK, it's flag:");
        memset(&s, 0, 0x28uLL);
        // 赋 0 值,内存准备工作
        strcat((char *)&s, f1);
        // 将 f1 存入 &s 所指向的内存中
        strcat((char *)&s, &f2);
        // f2 的数据紧随 f1 存入指定内存
        printf("%s", (const char *)&s);
        break;
      case 2:
        printf("Solar not like you");
        break;
      case 3:
        printf("Solar want a girlfriend");
        break;
      case 4:
        s = 0x7F666F6067756369LL;
        v5 = 0;
        strcat(&f2, (const char *)&s);
        // 为 f2 赋值
        break;
      case 5:
        for ( j = 0; j <= 7;   j )
        {
          if ( j % 2 == 1 )
            *(&f2   j) -= 2;
          else
            --*(&f2   j);
        }
        // 对 f2 中的数据做处理
        break;
      default:
        puts("emmm,you can't find flag 23333");
        break;
    }
  }
  return __readfsqword(0x28u) ^ v6;
}

程序虽长,所幸逻辑简单,审计可知 flag 由 f1 与 f2 组成,f1 已知,为 GXY{do_not_,而 f1 则由后续操作完成赋值,且赋值后还需要另外的操作来完成数据处理。也就是说,上述代码中的 switch 语句中只有 1、 4、 5 是有效的,且必须按照 4、 5、 1 的顺序指向才可输出正确的 flag。由于程序执行由随机序列决定,且随机数取模高达 200,因此要靠程序自然输出基本不可能,但是现在毕竟是 Reverse,程序运不运行啥的都不重要,咱自己手工求出来即可。

由 case 4 可知 f2 被赋的值为整形数值 0x7F666F6067756369LL,小端存储的缘故,在内存中的顺序为:0x69、0x63、0x75、0x67、0x60、0x6F、0x66、0x7F,将这些数据按顺序进行 case 5 中的操作后与 f1 拼接即可获得 flag。还是写个脚本来做:

代码语言:javascript复制
f1 = 'GXY{do_not_'
f2 = [0x69, 0x63, 0x75, 0x67, 0x60, 0x6f, 0x66, 0x7f]
flag = ''

for i in range(8):
    if i % 2 == 1:
        flag  = chr(f2[i] - 2)
    else:
        flag  = chr(f2[i] - 1)

print(f1   flag)

运行后即可获得 GXY{do_not_hate_me},不过 BUU 格式问题,因此需要提交 flag{do_not_hate_me}

[BJDCTF2020]JustRE

本题文件为 32 位可执行程序,没有加壳。

丢进 IDA 反编译,主函数中有些复杂且并未发现什么关键词,因此直接 Shift F12 查看字符串,发现一个类似 flag 形式的字符串:

点击进去发现该字符串被 DialogFunc() 函数引用,该函数内容如下:

代码语言:javascript复制
INT_PTR __stdcall DialogFunc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
{
  CHAR String[100]; // [esp 0h] [ebp-64h] BYREF

  if ( a2 != 272 )
  {
    if ( a2 != 273 )
      return 0;
    if ( (_WORD)a3 != 1 && (_WORD)a3 != 2 )
    {
      sprintf(String, &Format,   dword_4099F0);
      if ( dword_4099F0 == 19999 )
      {
        sprintf(String, " BJD{%d�069a45792d233ac}", 19999, 0);
        SetWindowTextA(hWnd, String);
        return 0;
      }
      SetWindowTextA(hWnd, String);
      return 0;
    }
    EndDialog(hWnd, (unsigned __int16)a3);
  }
  return 1;
}

大多为无关代码,仅关注关键字符串所在的 sprintf() 函数即可,可以发现字符串格式化输出,即占位符 %d 在输出时会被替换为其后所跟的整形数据。

因此格式化输出之后的 BJD{1999902069a45792d233ac} 即为 flag,当然 BUU 中需要提交 flag{1999902069a45792d233ac}。值得注意的是,本题文件可双击打开,有个有趣的可视化界面。

刮开有奖

本题文件为 32 位可执行程序,没有加壳。

丢进 IDA 反编译,主函数仅调用了一个 DialogBoxParamA() 函数便退出了,该函数从对话框模板资源创建模式对话框。点进函数发现与其相关的函数都在 DialogFunc() 函数被调用:

而在 DialogFunc() 函数则存在 U g3t 1T! 这样的字符串存在,因此要获取 flag 就得从这个函数入手了。又是一堆代码要审计。

代码语言:javascript复制
INT_PTR __stdcall DialogFunc(HWND hDlg, UINT a2, WPARAM a3, LPARAM a4)
{
  const char *v4; // esi
  const char *v5; // edi
  int v7[2]; // [esp 8h] [ebp-20030h] BYREF
  int v8; // [esp 10h] [ebp-20028h]
  int v9; // [esp 14h] [ebp-20024h]
  int v10; // [esp 18h] [ebp-20020h]
  int v11; // [esp 1Ch] [ebp-2001Ch]
  int v12; // [esp 20h] [ebp-20018h]
  int v13; // [esp 24h] [ebp-20014h]
  int v14; // [esp 28h] [ebp-20010h]
  int v15; // [esp 2Ch] [ebp-2000Ch]
  int v16; // [esp 30h] [ebp-20008h]
  CHAR String[65536]; // [esp 34h] [ebp-20004h] BYREF
  char v18[65536]; // [esp 10034h] [ebp-10004h] BYREF

  if ( a2 == 272 )
    return 1;
  if ( a2 != 273 )
    return 0;
  if ( (_WORD)a3 == 1001 )
  {
    memset(String, 0, 0xFFFFu);
    // 初始化 String 内存
    GetDlgItemTextA(hDlg, 1000, String, 0xFFFF);
    // 从对话框读取信息写入 String 指向的内存中
    if ( strlen(String) == 8 )
    // 当 String 长度为 8 才进入 if,否则退出,因此 flag 长度为 8
    {
      v7[0] = 90;
      v7[1] = 74;
      v8 = 83;
      v9 = 69;
      v10 = 67;
      v11 = 97;
      v12 = 78;
      v13 = 72;
      v14 = 51;
      v15 = 110;
      v16 = 103;
      // 这些变量全部与 v7 在内存中连续,可认为同在 v7 数组中
      sub_4010F0((int)v7, 0, 10);
      memset(v18, 0, 0xFFFFu);
      v18[0] = String[5];
      v18[2] = String[7];
      v18[1] = String[6];
      // v18 初始化并特定位赋初值
      v4 = (const char *)sub_401000(v18, strlen(v18));
      // v18 处理后赋给 v4
      memset(v18, 0, 0xFFFFu);
      v18[1] = String[3];
      v18[0] = String[2];
      v18[2] = String[4];
      // v18 再次初始化并特定位赋初值
      v5 = (const char *)sub_401000(v18, strlen(v18));
      // v18 再次处理后赋给 v5
      if ( String[0] == v7[0]   34
        && String[1] == v10
        && 4 * String[2] - 141 == 3 * v8
        && String[3] / 4 == 2 * (v13 / 9)
        && !strcmp(v4, "ak1w")
        && !strcmp(v5, "V1Ax") )
      // 通过这么些个比较才行
      {
        MessageBoxA(hDlg, "U g3t 1T!", "@_@", 0);
      }
    }
    return 0;
  }
  if ( (_WORD)a3 != 1 && (_WORD)a3 != 2 )
    return 0;
  EndDialog(hDlg, (unsigned __int16)a3);
  return 1;
}

程序在预处理后从对话框读取数据 String,首先限定其长度为 8 位,不符合则退出程序,因此可以判断 flag 长度为 8 位。长度符合的字符串 String 则会与函数的一些已知值的局部变量以及经过 sub_4010F0() 函数处理的 v7 变量做比较,还有特定位参与 sub_401000() 函数处理后与内部字符串常量的比较,而通过所有比较的字符串 String 即为 flag。因此要获取 flag,还有两个函数也需要好好审计审计,救命啊!!!那么就先审审 sub_4010F0() 函数。

代码语言:javascript复制
int __cdecl sub_4010F0(int a1, int a2, int a3)
{
  int result; // eax
  int i; // esi
  int v5; // ecx
  int v6; // edx

  result = a3;
  // 初始化为数组末位,作为后续区间遍历的终点
  for ( i = a2; i <= a3; a2 = i )
  // 未到数组末位则继续循环
  {
    v5 = 4 * i;
    v6 = *(_DWORD *)(4 * i   a1);
    // 4 为整形数据所占用的空间,即 v6 赋值为区间起点处的值
    if ( a2 < result && i < result )
    // 未到区间终点则继续循环
    {
      do
      {
        if ( v6 > *(_DWORD *)(a1   4 * result) )
        // v6 与区间内末尾作比较,大于末位才进入 if 语句
        {
          if ( i >= result )
            break;
            i;
          *(_DWORD *)(v5   a1) = *(_DWORD *)(a1   4 * result);
          // 末位数据存入 v6 数值原本的地址
          if ( i >= result )
            break;
          while ( *(_DWORD *)(a1   4 * i) <= v6 )
          {
            if (   i >= result )
              goto LABEL_13;
            // 若 v6 值仍然不小于其原位后的一位,则继续递归调用直至结束
          }
          if ( i >= result )
            break;
          v5 = 4 * i;
          *(_DWORD *)(a1   4 * result) = *(_DWORD *)(4 * i   a1);
          // 若 v6 跟小了,则把比他大的放在区间末位
        }
        --result;
        // 区间尾部前移,区间缩小
      }
      while ( i < result );
    }
LABEL_13:
    *(_DWORD *)(a1   4 * result) = v6;
    // 若 v6 一直都大,则会一直存在于区间末位
    sub_4010F0(a1, a2, i - 1);
    result = a3;
      i;
  }
  return result;
}

费了好大劲审计完后,发现大的数据总是会往后移,小的数据则前移,很显然这是一个升序的排序函数。参观了网上好多题解发现大家都不会审计这个代码,因为修改一下直接运行就可以出结果了,谁是怨种我不说。因此 v7 数组经过 sub_4010F0() 函数处理后为如下递增序列:

代码语言:javascript复制
51	67	69	72	74	78	83	90	97	103	110
对应字符序列为:
3	C	E	H	J	N	S	Z	a	g	n

最后审计一下 sub_401000() 函数,太长了…

代码语言:javascript复制
_BYTE *__cdecl sub_401000(int a1, int a2)
{
  int v2; // eax
  int v3; // esi
  size_t v4; // ebx
  _BYTE *v5; // eax
  _BYTE *v6; // edi
  int v7; // eax
  _BYTE *v8; // ebx
  int v9; // edi
  int v10; // edx
  int v11; // edi
  int v12; // eax
  int i; // esi
  _BYTE *result; // eax
  _BYTE *v15; // [esp Ch] [ebp-10h]
  _BYTE *v16; // [esp 10h] [ebp-Ch]
  int v17; // [esp 14h] [ebp-8h]
  int v18; // [esp 18h] [ebp-4h]

  v2 = a2 / 3;
  v3 = 0;
  if ( a2 % 3 > 0 )
      v2;
  v4 = 4 * v2   1;
  v5 = malloc(v4);
  v6 = v5;
  v15 = v5;
  if ( !v5 )
    exit(0);
  memset(v5, 0, v4);
  v7 = a2;
  v8 = v6;
  v16 = v6;
  if ( a2 > 0 )
  {
    while ( 1 )
    {
      v9 = 0;
      v10 = 0;
      v18 = 0;
      do
      {
        if ( v3 >= v7 )
          break;
          v10;
        v9 = *(unsigned __int8 *)(v3   a1) | (v9 << 8);
          v3;
      }
      while ( v10 < 3 );
      v11 = v9 << (8 * (3 - v10));
      v12 = 0;
      v17 = v3;
      for ( i = 18; i > -6; i -= 6 )
      {
        if ( v10 >= v12 )
        {
          *((_BYTE *)&v18   v12) = (v11 >> i) & 0x3F;
          v8 = v16;
        }
        else
        {
          *((_BYTE *)&v18   v12) = 64;
        }
        *v8   = byte_407830[*((char *)&v18   v12  )];
        v16 = v8;
      }
      v3 = v17;
      if ( v17 >= a2 )
        break;
      v7 = a2;
    }
    v6 = v15;
  }
  result = v6;
  *v8 = 0;
  return result;
}

万不可死磕审计,太浪费时间了,注意到函数调用了一个字符数组 byte_407830,点开发现,这是老朋友 base64 字符表啊,应该又是 base64 加密计算而已,就不去老实审计了。

代码审完了,接下来照着最终判断条件逐个击破就行了。

代码语言:javascript复制
   String[0] == v7[0]   34 
   // v7[0]=51,故 String[0]='U'(码值为 85)
&& String[1] == v10 
   // v10='J',故 String[1]='J'
&& 4 * String[2] - 141 == 3 * v8 
   // v8=69,故 String[2]='W'(码值为 87)
&& String[3] / 4 == 2 * (v13 / 9) 
   // v13=90,故 String[3]='P'(码值为 80)
&& !strcmp(v4, "ak1w") 
   // ak1w 解密为 jMp,故 String[5,6,7]="jMp"
&& !strcmp(v5, "V1Ax")
   // V1Ax 解密为 WP1,故 String[2,3,4]="WP1"

综上,唯一符合条件的字符串 String 为 UJWP1jMp,因此提交 flag{UJWP1jMp} 即可。

简单注册器

本题文件为安卓的 apk 文件,由于 IDA 反编译处理着实有些看不懂,因此使用 JEB 进行反编译。在字符串一栏搜索到了关键字 flag{,双击即可在右侧看到调用该字符串的代码。

略微审计一下代码,发现 flag 仅由如下代码块生成。

代码语言:javascript复制
{
char[] arr_c = "dd2940c04462b4dd7c450528835cca15".toCharArray();
arr_c[2] = (char)(arr_c[2]   arr_c[3] - 50);
arr_c[4] = (char)(arr_c[2]   arr_c[5] - 0x30);
arr_c[30] = (char)(arr_c[0x1F]   arr_c[9] - 0x30);
arr_c[14] = (char)(arr_c[27]   arr_c[28] - 97);
int i;
for(i = 0; i < 16;   i) {
    char a = arr_c[0x1F - i];
    arr_c[0x1F - i] = arr_c[i];
    arr_c[i] = a;
}

textview.setText("flag{"   String.valueOf(arr_c)   "}");
return;}

虽说这么点代码不难审计,但是毕竟反编译代码十分完善,所以完全可以直接执行该代码块。将 textview.setText 换成 Java 的输出语句即可将 flag 输出,计算结果为 flag{59acc538825054c7de4b26440c0999dd}。

[GWCTF 2019]pyre

本题文件为 Python 编译后的二进制文件,直接丢进 在线工具 反编译一下:

代码语言:javascript复制
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 2.7

print "Welcome to Re World!"
print "Your input1 is your flag~"
l = len(input1)
for i in range(l):
    num = ((input1[i]   i) % 128   128) % 128
    code  = num
for i in range(l - 1):
    code[i] = code[i] ^ code[i   1]
print code
code = [
    "", "", "", "(", "0",
    "4", "", "", "", "4",
    ",", "", "U", "?", "o",
    "6", "*", ":", "", "D",
    ";", "%", "",]

程序读取输入,并且进行逐个取模、相加、异或操作后输出数据,而输出的数据在代码中已经给出,正如提示所言,现在求出用户输入即为 flag。

逆向编写一个脚本即可,脚本如下:

代码语言:javascript复制
code = ["", "", "", "(", "0",
        "4", "", "", "", "4",
        ",", "", "U", "?", "o",
        "6", "*", ":", "", "D",
        ";", "%", ""]
flag = ''

for i in range(len(code) - 2, -1, -1):
    code[i] = chr(ord(code[i]) ^ ord(code[i   1]))
for i in range(len(code)):
    num = chr((ord(code[i]) - i) % 128)
    flag  = num

print(flag)

执行之后即可输出 GWHT{Just_Re_1s_Ha66y!},直接提交 flag{Just_Re_1s_Ha66y!}。

总结

  相比于此前 Web 与 PWN,这边的许多题目笔者都可以独立完成,大概也是因为刚入门吧,这个方向对新手还是蛮友好的。对于 Reverse 这边整体感觉就是,嗯对,逆向。不过,虽说整体毕竟顺利,但是动辄代码审计真的非常痛苦,还老是对伪代码审计。对于笔者这种正着都写不好代码的人来说,还要逆过来分析着实不易。

  到这里 CTF 已经开辟了三个方向了,虽说都只是浅浅的了解了一下,但总归对自己的技术层次有了更深的了解,兴趣也被提上来了!别骂了别骂了,我会好好学的。

0 人点赞