前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >BUUCTF 刷题笔记——Reverse 1

BUUCTF 刷题笔记——Reverse 1

作者头像
h-t-m
发布2023-03-15 14:56:01
1.9K0
发布2023-03-15 14:56:01
举报
文章被收录于专栏:h-t-m

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("%20s", Str1);
  v2 = j_strlen(Str2);
  if ( !strncmp(Str1, Str2, v2) )
    sub_1400111D1("this is the right flag!\n");
  else
    sub_1400111D1("wrong flag\n");
  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("%20s", 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("%20s", (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 right\n:");
    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%d2069a45792d233ac}", 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 = [
    "%1f", "%12", "%1d", "(", "0",
    "4", "%01", "%06", "%14", "4",
    ",", "%1b", "U", "?", "o",
    "6", "*", ":", "%01", "D",
    ";", "%", "%13",]

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

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

代码语言:javascript
复制
code = ["%1f", "%12", "%1d", "(", "0",
        "4", "%01", "%06", "%14", "4",
        ",", "%1b", "U", "?", "o",
        "6", "*", ":", "%01", "D",
        ";", "%", "%13"]
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 已经开辟了三个方向了,虽说都只是浅浅的了解了一下,但总归对自己的技术层次有了更深的了解,兴趣也被提上来了!别骂了别骂了,我会好好学的。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • BUUCTF 刷题笔记——Reverse 1
    • easyre
      • reverse1
        • reverse2
          • 内涵的软件
            • 新年快乐
              • xor
                • helloword
                  • reverse3
                    • 不一样的flag
                      • SimpleRev
                        • Java逆向解密
                          • [GXYCTF2019]luck_guy
                            • [BJDCTF2020]JustRE
                              • 刮开有奖
                                • 简单注册器
                                  • [GWCTF 2019]pyre
                                    • 总结
                                    相关产品与服务
                                    代码审计
                                    代码审计(Code Audit,CA)提供通过自动化分析工具和人工审查的组合审计方式,对程序源代码逐条进行检查、分析,发现其中的错误信息、安全隐患和规范性缺陷问题,以及由这些问题引发的安全漏洞,提供代码修订措施和建议。支持脚本类语言源码以及有内存控制类源码。
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档