BUUCTF--CrackRTF
测试文件:https://www.lanzous.com/iane9hi
代码分析
直接IDA打开,找到主函数之后
1 int __cdecl main_0() 2 { 3 DWORD v0; // eax 4 DWORD v1; // eax 5 CHAR String; // [esp+4Ch] [ebp-310h] 6 int v4; // [esp+150h] [ebp-20Ch] 7 CHAR String1; // [esp+154h] [ebp-208h] 8 BYTE pbData; // [esp+258h] [ebp-104h] 9 10 memset(&pbData, 0, 0x104u); 11 memset(&String1, 0, 0x104u); 12 v4 = 0; 13 printf("pls input the first passwd(1): "); 14 scanf("%s", &pbData); // 输入 15 if ( strlen((const char *)&pbData) != 6 ) 16 { 17 printf("Must be 6 characters!\n"); 18 ExitProcess(0); 19 } 20 v4 = atoi((const char *)&pbData); // 将数字字符串转换为整型 21 if ( v4 < 100000 ) 22 ExitProcess(0); 23 strcat((char *)&pbData, "@DBApp"); // 末尾加上 @DBApp 24 v0 = strlen((const char *)&pbData); 25 sub_40100A(&pbData, v0, &String1); 26 if ( !_strcmpi(&String1, "6E32D0943418C2C33385BC35A1470250DD8923A9") )// MD5解密得到123321@DBApp 27 { 28 printf("continue...\n\n"); 29 printf("pls input the first passwd(2): "); 30 memset(&String, 0, 0x104u); 31 scanf("%s", &String); 32 if ( strlen(&String) != 6 ) 33 { 34 printf("Must be 6 characters!\n"); 35 ExitProcess(0); 36 } 37 strcat(&String, (const char *)&pbData); // 将123321@DBApp放到第二次输入字符串后面 38 memset(&String1, 0, 0x104u); 39 v1 = strlen(&String); 40 sub_401019((BYTE *)&String, v1, &String1); 41 if ( !_strcmpi("27019e688a4e62a649fd99cadaafdb4e", &String1) ) 42 { 43 if ( !sub_40100F(&String) ) 44 { 45 printf("Error!!\n"); 46 ExitProcess(0); 47 } 48 printf("bye ~~\n"); 49 } 50 } 51 return 0; 52 }
需要我们输入两次密码。
第一次密码
首先我们知道密码长度为6,并且通过atoi的操作,可以看出应该是纯数字的字符串(后面使用星号*表示)。
第一部分将******@DBApp经过sub_40100A函数加密之后,得到6E32D0943418C2C33385BC35A1470250DD8923A9。因此我们只需要将密文解密,就能得到第一段的密码。
打开sub_40100A函数后
猜测是hash类的函数,测试后发现是MD5加密(https://www.somd5.com/),解密得到123321@DBApp
第二次密码
这次将上次的结果放到这次输入之后,即******123321@DBApp,用的是同一种加密方法得到27019e688a4e62a649fd99cadaafdb4e,但是解密不出来。
因此我们向下看sub_40100F函数
1 char __cdecl sub_4014D0(LPCSTR lpString) 2 { 3 LPCVOID lpBuffer; // [esp+50h] [ebp-1Ch] 4 DWORD NumberOfBytesWritten; // [esp+58h] [ebp-14h] 5 DWORD nNumberOfBytesToWrite; // [esp+5Ch] [ebp-10h] 6 HGLOBAL hResData; // [esp+60h] [ebp-Ch] 7 HRSRC hResInfo; // [esp+64h] [ebp-8h] 8 HANDLE hFile; // [esp+68h] [ebp-4h] 9 10 hFile = 0; 11 hResData = 0; 12 nNumberOfBytesToWrite = 0; 13 NumberOfBytesWritten = 0; 14 hResInfo = FindResourceA(0, (LPCSTR)0x65, "AAA"); 15 if ( !hResInfo ) 16 return 0; 17 nNumberOfBytesToWrite = SizeofResource(0, hResInfo); 18 hResData = LoadResource(0, hResInfo); 19 if ( !hResData ) 20 return 0; 21 lpBuffer = LockResource(hResData); 22 sub_401005(lpString, (int)lpBuffer, nNumberOfBytesToWrite); 23 hFile = CreateFileA("dbapp.rtf", 0x10000000u, 0, 0, 2u, 0x80u, 0); 24 if ( hFile == (HANDLE)-1 ) 25 return 0; 26 if ( !WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) ) 27 return 0; 28 CloseHandle(hFile); 29 return 1; 30 }
- FindResourceA:该函数确定指定模块中指定类型和名称的资源所在位置。(可以看看这篇:https://blog.csdn.net/singleyellow/article/details/80308789)
- SizeofResource:表示该函数返回指定资源的字节数大小。
- LoadResource 以前返回值类似于记录可移动的指针的 HANDLE,需要传递给 LockResource 得到一个固定的指针地址以供读写。而现在,LoadResource 直接返回固定的可读写内存地址,不再需要 LockResource。
这个函数我看成了三个部分:
- 第1~21行:查找类型“AAA”,名称0x65的资源。
- 第22行:将第二段密码与查找到的资源进行某种变换。
- 第23行~最后:将变换得到的信息写入dbapp.rtf文件。
第一部分中的信息,可以使用Resource Hacker查看
我们重点看第二部分的函数
unsigned int __cdecl sub_401420(LPCSTR lpString, int a2, int a3) { unsigned int result; // eax unsigned int i; // [esp+4Ch] [ebp-Ch] unsigned int v5; // [esp+54h] [ebp-4h] v5 = lstrlenA(lpString); for ( i = 0; ; ++i ) { result = i; if ( i >= a3 ) break; *(_BYTE *)(i + a2) ^= lpString[i % v5]; } return result; }
这段实际就是将资源文件的信息与输入的密码进行异或操作。因为密码的后12位我们都知道了,因此我们只需要知道密码的前6位。
这里需要一点脑洞,因为生成的是.rtf文件,我们可以了解到它的标识符前6位为{\rtf1(https://blog.csdn.net/dream_dt/article/details/79215798)。有结果,有异或值,不难解出原值。
脚本
# -*- coding:utf-8 -*- List = [0x05,0x7D,0x41,0x15,0x26,0x01] enc = '{\\rtf1' flag = '' for i,val in enumerate(List): flag += chr(ord(enc[i]) ^ val) print (flag)
~!3a@0
输入密码~!3a@0得到dbapp.rtf文件