一道预期解是爆破的题
这道题来自西湖论剑,关键代码是splay平衡树,俺不会

image-20201008213916560

从栈里可以看见格式是%25s

随便输入之后看ret返回之后的代码

image-20201008214053666

外层逻辑很清晰

只要经过4022B7 call 004020C2之后 al!=0就行了

image-20201008214333209

进入之后呢入眼就是一个strlen函数 想走到这里看看能不能知道所需求的长度

然而并不能 这里只是根据长度来进行下面的循环而已

再往下跟一下可以知道是将输入的每一位与0x4C和0x52相比 这两位分别是’L’和’R’

然后再往下走就直接跳到上面的循环并且计数器加一,说明输入只能是’L’和’R’

image-20201008214737470

粗略分析中间的代码,并且经过调试,可以看出是将每一位’L’或’R’传入call中,然后检查经过4个call之后的eax返回值,如果返回0会直接return

直接return肯定是不能的

但是分析中间各个的call,即使是用上了IDA的反编译来分析,也并没有理清程序的逻辑

后来想着 这个只需要输入’L’和’R’ 长度为25位

就尝试着在4个call下面那一句判断下断点,

每次多加一位,不是L就是R,都不是就回到上一位,

不过这种方法实在是难受

于是我把这4个地方都hook了一下

image-20201009215121982

image-20201009215357994

这样的结果是如果返回0就打印当前的flag索引值然后退出

然后用了管道进行输入输出交互 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#pragma warning(disable:4996)
#include <iostream>
#include <windows.h>
#define BUFSIZE 4096

int main()
{
HANDLE hReadPipe1, hWritePipe1, hReadPipe2, hWritePipe2; //定义句柄
SECURITY_ATTRIBUTES sa{ sizeof(sa),NULL,true }; //初始化安全属性
CreatePipe(&hReadPipe1, &hWritePipe1, &sa, 0);
CreatePipe(&hReadPipe2, &hWritePipe2, &sa, 0);

STARTUPINFO si = {}; //设置进程属性
si.cb = sizeof(STARTUPINFO);
GetStartupInfoW(&si);
si.wShowWindow = SW_HIDE;
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.hStdInput = hReadPipe2;;
si.hStdError = si.hStdOutput = hWritePipe1;

std::string flag;
char cbBuf[4096] = {}; //读写缓存
PROCESS_INFORMATION pi = {}; //初始化进程信息结构体
DWORD dwRead, dwAvail, dwWrite; //用于储存所读写的字节个数
WCHAR pCommandLine[] = L"C:\\Users\\Administrator\\Desktop\\Cellular_patch.exe"; //执行的命令行
while (flag.length() != 25)
{
int temp{};
pi = {};
flag.push_back('R');
//按命令行,被初始化的启动信息和置空的进程信息创建一个进程
CreateProcess(NULL, pCommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi);

flag.push_back('\r'); flag.push_back('\n'); //用于回车提交flag

WriteFile(hWritePipe2, flag.c_str(), flag.length() + 1, &dwWrite, NULL);
Sleep(10); //这里不睡会影响读取结果
//但是不知道是为什么
flag.pop_back(); flag.pop_back();

dwRead = 0;
if (PeekNamedPipe(hReadPipe1, cbBuf, BUFSIZE, &dwRead, &dwAvail, 0) && dwRead)
{
//如果管子里有东西才读,因为这个题目有一些输入会使得程序崩溃,无法获取输出导致进程卡死,不得不加上
ReadFile(hReadPipe1, cbBuf, BUFSIZE, &dwRead, NULL);
sscanf(cbBuf, "Path:%d", &temp);
}
else
temp = flag.length() - 1; //最后输入的那一位有错误,而且这一位错误使得程序崩溃

CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

//从读取的字节中取出错误
if (temp) //如果有错误处理错误
{
int res{ 0 };
while (1)
{
if (flag[temp - res] != 'L') //初始是'R',如果不是'L',说明这一位已经改过了
{ //那么就看上一位,依次循环上去
flag[temp - res] = 'L';
break;
}
else
res++;
}
}
else if (flag.length() == 25) //如果长度为25而且没有错误!flag出!
{
printf("flag{md5(%s)}", flag.c_str());
break;
}
}
CloseHandle(hReadPipe1);
CloseHandle(hWritePipe1);
CloseHandle(hReadPipe2);
CloseHandle(hWritePipe2);
getchar();
}

还要注意的是要把程序结束后system(“pause”)nop掉 不然看不见输出

运行得到最后的结果是

image-20201009215907505

image-20201008220021533

不过这个题目的话 还有一个师傅的一个爆破思路 我觉得很有创造性,学到许多

所以也在这里写写

首先写了一个main函数,是他那个主函数的main函数

然后把输入改成了循环 从1位到25位RL的全排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>


//构造函数
typedef void* (__cdecl* PFNHONEYCOMB)(void* thiz);

typedef void(__cdecl* PFNINCDEPT)(void* thiz);
typedef bool(__cdecl* PFNCHECKFLAG)(void* thiz, char* szBuff);

int main()
{
PFNHONEYCOMB pfnHoneycomb = (PFNHONEYCOMB)0x402202;
PFNINCDEPT pfnIncDept = (PFNINCDEPT) 0x4012F0;
PFNCHECKFLAG pfnCheckFlag = (PFNCHECKFLAG)0x4020C2;
char szThiz[8] = {0};
char szFlag[32] = { 0 };

//位数
for (int i = 1; i <= 25; i++)
{
//i位数二进制数的范围 0 ~ (1 << i)
for (int j = 0; j < (1 << i); j++)
{
//先全赋值成L
for (int k = 0; k <i; k++)
{
szFlag[k] = 'L';
}
szFlag[i] = 0;

int tmp = j;
int bit = 0;
while (tmp > 0)
{
if (tmp % 2 == 1)
{
szFlag[bit] = 'R';
}
bit++;
tmp /= 2;
}


//try

for (auto& a : szThiz)
{
a = 0;
}

pfnHoneycomb(szThiz);

for (int x = 0; x <= 4; ++x)
{
pfnIncDept(szThiz);
}

//input...

bool bResult = pfnCheckFlag(szThiz, szFlag);
if (bResult)
{
goto LB_END;
}
//else
//{
//}


//printf("%s\r\n", szFlag);

}

//system("pause");
}


LB_END:
printf("%s\r\n", szFlag);
return 0;

}

然后在反汇编软件中 复制这个程序的main函数的字节码

再在进入Cellular的主函数后 分配一段空白内存

在空白内存中粘贴字节码 然后修改EIP到这个地方 于是这个程序就会执行上面的代码了

于是这个程序就自己问自己这些flag是不是对的

是就输出flag

还要注意的是printf的地方要改一下 连带””%s\r\n”也要在后面加一下

但是这个程序他本身是有问题的,用一些特定的输入会跑蹦这个代码

如果再把上面那个printf取消注释 就可以看到代码在这个地方出现了异常

image-20201010144147567

这如果要解决还要去修复他原来的程序,实在麻烦就没有继续往下做了

但是这个思路确实是强啊

文章作者: Usher
文章链接: https://usher2008.github.io/2020/10/10/%E4%B8%80%E9%81%93%E9%A2%84%E6%9C%9F%E8%A7%A3%E6%98%AF%E7%88%86%E7%A0%B4%E7%9A%84%E9%A2%98/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Usher