考到我知识盲区的一道题目,但是并不难,如果我知道知识点的话,应该是能拿血的,因为参加这比赛的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个字节。