[第二届数信杯南区决赛]easy-decode
本文最后更新于84 天前,其中的信息可能已经过时,如有错误可以直接在文章下留言

考到我知识盲区的一道题目,但是并不难,如果我知道知识点的话,应该是能拿血的,因为参加这比赛的Re手好像不是很多,我看了很久,三血才最后被别人拿到。

拿到题目打开可能会因为dll的原因报错

后面发现这通常是因为题目使用了OpenSSL 库中的加密函数,但是题目附加没有包含相应的dll导致的,我们直接在自己电脑上查找该dll,复制一份就可以正常运行和动态调试了。

主函数代码如下

int __fastcall main(int argc, const char **argv, const char **envp)
{
  FILE *v3; // rax
  FILE *v4; // rbx
  size_t v6; // rbp
  void *v7; // rax
  void *v8; // rsi
  void *v9; // rbx
  __int64 v10; // rdi
  __int64 v11; // rax
  char *v12; // rdi
  __int64 v13; // rbx
  __int64 v14; // rax
  int v15; // ebp
  int v16; // ebp
  FILE *v17; // rax
  FILE *v18; // rbx
  int v19; // [rsp+30h] [rbp-48h] BYREF
  char v20[16]; // [rsp+38h] [rbp-40h] BYREF
  __int128 v21[2]; // [rsp+48h] [rbp-30h] BYREF

  v3 = fopen("flag.txt", "rb");
  v4 = v3;
  if ( v3 )
  {
    fseek(v3, 0, 2);
    v6 = ftell(v4);
    fseek(v4, 0, 0);
    v7 = j__malloc_base(v6);
    v8 = v7;
    if ( v7 )
    {
      fread(v7, 1ui64, v6, v4);
      fclose(v4);
      v9 = j__malloc_base(0x7D0ui64);
      memmove(v9, main, 0x7D0ui64);
      v10 = EVP_MD_CTX_new();
      v11 = EVP_md5();
      EVP_DigestInit_ex(v10, v11, 0i64);
      EVP_DigestUpdate(v10, v9, 2000i64);
      EVP_DigestFinal_ex(v10, v20, 0i64);
      EVP_MD_CTX_free(v10);
      memset(v21, 0, sizeof(v21));
      v12 = (char *)j__malloc_base((int)((((((int)v6 + 32) >> 31) & 0x1F) + v6 + 32) & 0xFFFFFFE0));
      if ( v12 )
      {
        v13 = EVP_CIPHER_CTX_new();
        v14 = EVP_aes_128_cbc();
        EVP_EncryptInit_ex(v13, v14, 0i64, v20, v21);
        EVP_EncryptUpdate(v13, v12, &v19, v8, v6);
        v15 = v19;
        EVP_EncryptFinal_ex(v13, &v12[v19], &v19);
        v16 = v19 + v15;
        EVP_CIPHER_CTX_free(v13);
        v17 = fopen("flag_enc.txt", "wb");
        v18 = v17;
        if ( v17 )
        {
          fwrite(v12, 1ui64, v16, v17);
          fclose(v18);
          free(v8);
          free(v12);
          return 0;
        }
        else
        {
          free(v8);
          free(v12);
          return 1;
        }
      }
      else
      {
        free(v8);
        return 1;
      }
    }
    else
    {
      fclose(v4);
      return 1;
    }
  }
  else
  {
    perror("无法打开 flag.txt");
    return 1;
  }
}

分析了一下,逻辑并不复杂,就是简单的将整个main函数进行MD5加密之后的值,作为密钥,再对flag进行AES加密,模式为CBC,128位,IV全为0。最后写入flag_enc.txt文件当中

我们在同目录下创建一个flag.txt文件,随便写一点什么,第一想法当然是打断点动态调试拿到AES加密的密钥,即对main函数进行MD5加密后的值。问了一下AI,MD5加密后的值应该存储在变量v20当中

MD5加密后的长度为32位,如下图所示(并不是说图中结果就是加密结果),头部和尾部有多余的数据。

但是拿着结果去解密却发现并不能得到flag,然后就卡在这里了,再去看代码是否还有其他处理,在这疑惑了接近两个小时,期间操作竟然还发现v20存储的MD5加密后的值还会变,直接懵逼了,找了很久也没发现对v20的其他处理,误打误撞发现原理打断点的位置会影响v20的值。

然后再去查看了一下打断点的原理,为啥会影响v20的值,瞬间就清晰了。

深入理解逆向工程之断点 – 先知社区

调试程序时,设置断点的原理是什么? – 知乎

我这我也想拿WinDbg来调试,验证软件断点的实现,但是WinDbg调试好麻烦,有空再学习一下。但是我用Cheat engine简单做了一下实验,如图

打了断点之后,该地址处的0x68确实被替换成了0xCC。

总之,我们平常随手打的断点就软件断点,它是基础的断点类型。软件断点在X86系统中就是指令INT 3,它的二进制代码opcode是0xCC。当程序执行到INT 3指令时,会引发软件中断,就会抛出一个断点异常,程序的执行也就跳转到调试器中。

当我们打下断点,并运行程序时,断点地址处的opcode的第一个字节会被调试器更换为0xCC,但是OllyDbg和IDA等未修改,是为了我们方便观察原内存地址的值,方便调试。

所以我们前面打断点的位置的不同,和断点的数量都会影响main函数的opcode,就会让对main函数进行MD5加密后的值改变。

那么我们就不能下软件断点了,我们可以通过下硬件断点来截取v20种的值。

硬件断点依靠CPU中的调试寄存器。硬件断点使用 1 号中断(INT1)实现,INT1 一般被用于硬件断点和单步事件。总之,硬件断点不会改变程序的opcode,这里的原理我就不再贴上来了。

取出v20的值,然后Cyberchef解密即可得到flag为

flag{85e66dbb-540b-4a49-90d3-baf2092f55b1}

当然,我也有看到其他战队有手动dumpmain函数opcode的做法

这里拷贝的字节数为2000,所以要从main函数的开头opcode往后dumo2000个字节,然后放到Cyberchef里面一样也可以得到MD5加密后的值。

后面再仔细看了一下程序的opcode,并不是只对main函数的opcode进行MD5加密,因为main函数的opcode并没有那么多,而是一直往后面算直到2000个字节。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇