0%

[C++][Windows]通过Hook伪造截屏/录屏画面

前言

这个其实是我很长一段时间之前弄的了,不过当时没有发出来(现在发出来水一下博客)

刚开学那会我在学校的某群吹水时,听说了微机室电脑有控制软件可以让老师看屏幕,于是在第一节微机课的前一天晚上连夜弄了一下这个

正文

0x01 Find

想要伪造画面,自然要用到Hook技术,可是要Hook哪里呢?

这里先贴一段截屏代码供参考
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
BOOL WriteBmp(const TSTRING &strFile, const std::vector<BYTE> &vtData, const SIZE &sizeImg);
BOOL WriteBmp(const TSTRING &strFile, HDC hdc);
BOOL WriteBmp(const TSTRING &strFile, HDC hdc, const RECT &rcDC);

BOOL WriteBmp(const TSTRING &strFile, const std::vector<BYTE> &vtData, const SIZE &sizeImg)
{

BITMAPINFOHEADER bmInfoHeader = { 0 };
bmInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
bmInfoHeader.biWidth = sizeImg.cx;
bmInfoHeader.biHeight = sizeImg.cy;
bmInfoHeader.biPlanes = 1;
bmInfoHeader.biBitCount = 24;

//Bimap file header in order to write bmp file
BITMAPFILEHEADER bmFileHeader = { 0 };
bmFileHeader.bfType = 0x4d42; //bmp
bmFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
bmFileHeader.bfSize = bmFileHeader.bfOffBits + ((bmInfoHeader.biWidth * bmInfoHeader.biHeight) * 3); ///3=(24 / 8)

HANDLE hFile = CreateFile(strFile.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return FALSE;
}

DWORD dwWrite = 0;
WriteFile(hFile, &bmFileHeader, sizeof(BITMAPFILEHEADER), &dwWrite, NULL);
WriteFile(hFile, &bmInfoHeader, sizeof(BITMAPINFOHEADER), &dwWrite, NULL);
WriteFile(hFile, &vtData[0], vtData.size(), &dwWrite, NULL);


CloseHandle(hFile);

return TRUE;
}


BOOL WriteBmp(const TSTRING &strFile, HDC hdc)
{
int iWidth = GetDeviceCaps(hdc, HORZRES);
int iHeight = GetDeviceCaps(hdc, VERTRES);
RECT rcDC = { 0,0,iWidth,iHeight };

return WriteBmp(strFile, hdc, rcDC);
}

BOOL WriteBmp(const TSTRING &strFile, HDC hdc, const RECT &rcDC)
{
BOOL bRes = FALSE;
BITMAPINFO bmpInfo = { 0 };
BYTE *pData = NULL;
SIZE sizeImg = { 0 };
HBITMAP hBmp = NULL;
std::vector<BYTE> vtData;
HGDIOBJ hOldObj = NULL;
HDC hdcMem = NULL;

//Initilaize the bitmap information
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = rcDC.right - rcDC.left;
bmpInfo.bmiHeader.biHeight = rcDC.bottom - rcDC.top;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 24;

//Create the compatible DC to get the data
hdcMem = CreateCompatibleDC(hdc);
if (hdcMem == NULL)
{
goto EXIT;
}

//Get the data from the memory DC
hBmp = CreateDIBSection(hdcMem, &bmpInfo, DIB_RGB_COLORS, reinterpret_cast<VOID **>(&pData), NULL, 0);
if (hBmp == NULL)
{
goto EXIT;
}
hOldObj = SelectObject(hdcMem, hBmp);

//Draw to the memory DC
sizeImg.cx = bmpInfo.bmiHeader.biWidth;
sizeImg.cy = bmpInfo.bmiHeader.biHeight;
StretchBlt(hdcMem,
0,
0,
sizeImg.cx,
sizeImg.cy,
hdc,
rcDC.left,
rcDC.top,
rcDC.right - rcDC.left + 1,
rcDC.bottom - rcDC.top + 1,
SRCCOPY);


vtData.resize(sizeImg.cx * sizeImg.cy * 3);
memcpy(&vtData[0], pData, vtData.size());
bRes = WriteBmp(strFile, vtData, sizeImg);

SelectObject(hdcMem, hOldObj);


EXIT:
if (hBmp != NULL)
{
DeleteObject(hBmp);
}

if (hdcMem != NULL)
{
DeleteDC(hdcMem);
}

return bRes;
}

int main()
{
HDC hdc = GetDC(0);
HDC hMemDC = CreateCompatibleDC(hdc);
SelectObject(hMemDC, CreateCompatibleBitmap(hdc, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)));
BitBlt(hMemDC, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), hdc, 0, 0, SRCCOPY);
WriteBmp(L"directWrite.bmp", hdc);
WriteBmp(L"copyWrite.bmp", hMemDC);
ReleaseDC(GetActiveWindow(), hdc);
DeleteDC(hMemDC);
return 0;
}

CreateDCA和CreateDCW?

研究截屏的实现代码,首先可以想到CreateDCA和CreateDCW两个点,把程序拿到的屏幕DC替换掉

但能够发现,GetDC(0)和GetDC(GetDesktopWindow())也能拿到截屏用的DC,这样我们又多了一个需要Hook的点,并且可能还有更多方法来获得这个DC

还有,屏幕HDC并不一定只是用来读的,如果替换掉屏幕DC,很可能影响程序的正常显示

因此,这个Hook点是不能用的

BitBlt和StretchBlt

再看一眼,我们可以发现,为了把屏幕DC中内容读出,也就是copy出来,在上面的示例代码中用到了BitBlt和StretchBlt,实际上也就只有这两个API可用

这两个API的操作方向在参数中又是明确的,我们可以对参数进行确认避免误伤,所以这两个点不失为好的选择

0x02 Hook

决定了Hook的点,我们就可以开始Hook了

在这里,我们将用到MinHook这个简洁而好用的库来进行Hook操作

为了判断一个DC属不属于屏幕设备,我们可以判断他的三个属性:TECHNOLOGY、HORZRES和VERTRES是否和屏幕设备相同

如果确定了一个DC属于屏幕设备,那我们就可以把准备好的伪造画面代替原DC复制到目标DC中,伪造完成

为了方便进入别的进程进行Hook操作,我们将用一个DLL进行全部操作

同样贴出代码供参考
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
83
std::map<PVOID, PVOID> hookMap;

void DoHook(HMODULE dll, const char* name, void* newfunc) {
PVOID procAddress = GetProcAddress(dll, name);
PVOID originJmp;
MH_CreateHook(procAddress, newfunc, &originJmp);
MH_EnableHook(procAddress);
hookMap[procAddress] = originJmp;
}

HDC realDisplayDC = NULL;
HDC fakeDisplayDC = NULL;
int width, height;

BOOL WINAPI MyStretchBlt(
HDC hdcDest,
int xDest,
int yDest,
int wDest,
int hDest,
HDC hdcSrc,
int xSrc,
int ySrc,
int wSrc,
int hSrc,
DWORD rop
) {
static auto origin = decltype(&StretchBlt) (hookMap[&StretchBlt]);
if (GetDeviceCaps(hdcSrc, TECHNOLOGY) == DT_RASDISPLAY && GetDeviceCaps(hdcSrc, HORZRES) == width && GetDeviceCaps(hdcSrc, VERTRES) == height)
return origin(hdcDest, xDest, yDest, wDest, hDest, fakeDisplayDC, xSrc, ySrc, wSrc, hSrc, rop);
else
return origin(hdcDest, xDest, yDest, wDest, hDest, hdcSrc, xSrc, ySrc, wSrc, hSrc, rop);
}

BOOL WINAPI MyBitBlt(
HDC hdc,
int x,
int y,
int cx,
int cy,
HDC hdcSrc,
int x1,
int y1,
DWORD rop
) {
static auto origin = decltype(&BitBlt) (hookMap[&BitBlt]);
if (GetDeviceCaps(hdcSrc, TECHNOLOGY) == DT_RASDISPLAY && GetDeviceCaps(hdcSrc, HORZRES) == width && GetDeviceCaps(hdcSrc, VERTRES) == height)
return origin(hdc, x, y, cx, cy, fakeDisplayDC, x1, y1, rop);
else
return origin(hdc, x, y, cx, cy, hdcSrc, x1, y1, rop);
}

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MH_Initialize();
width = GetSystemMetrics(SM_CXSCREEN);
height = GetSystemMetrics(SM_CYSCREEN);
realDisplayDC = CreateDCA("DISPLAY", NULL, NULL, NULL);
fakeDisplayDC = CreateCompatibleDC(realDisplayDC);
SelectObject(fakeDisplayDC, CreateCompatibleBitmap(realDisplayDC, width, height));
BitBlt(fakeDisplayDC, 0, 0, width, height, realDisplayDC, 0, 0, SRCCOPY);
DoHook(GetModuleHandle(L"gdi32"), "StretchBlt", MyStretchBlt);
DoHook(GetModuleHandle(L"gdi32"), "BitBlt", MyBitBlt);
break;
case DLL_PROCESS_DETACH:
DeleteDC(realDisplayDC);
DeleteDC(fakeDisplayDC);
for (std::pair<PVOID, PVOID> eHook : hookMap) MH_DisableHook(eHook.first);
MH_Uninitialize();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}

这份代码将DLL加载时的屏幕保存,并将以后的截屏内容都替换为保存的内容

0x03 Inject

DLL已经完成,我们剩下的唯一任务就是把DLL注入到目标进程

在这里我们将使用QueueUserAPC这个比较稳定的方法进行注入

当然,注入一个DLL还有很多不同的方法,也许以后我会写一篇博客专门介绍一下

参考代码
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
void ByQueueUserAPC(PCSTR pszLibFile, DWORD dwProcessId)
{
size_t cb = (strlen(pszLibFile) + 1) * sizeof(char);

HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessId);
if (hProcess == NULL)
{
printf("Error: Couldn't open process.\n");
return;
}

LPVOID pszLibFileRemote = VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
if (pszLibFileRemote == NULL)
{
printf("Error: Couldn't allocate memory.\n");
CloseHandle(hProcess);
return;
}

LPVOID pfnThreadRtn = GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
if (pfnThreadRtn == NULL)
{
printf("Error: Couldn't find LoadLibraryA.\n");
CloseHandle(hProcess);
return;
}

DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, pszLibFile, cb, NULL);
if (n == 0)
{
printf("Error: Couldn't write.\n");
CloseHandle(hProcess);
return;
}

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
printf("Error: Couldn't get thread information.\n");
return;
}

DWORD threadId = 0;
THREADENTRY32 threadEntry;
threadEntry.dwSize = sizeof(THREADENTRY32);

BOOL least = FALSE;
std::list<HANDLE> handles;
BOOL bResult = Thread32First(hSnapshot, &threadEntry);
while (bResult)
{
bResult = Thread32Next(hSnapshot, &threadEntry);
if (bResult)
{
if (threadEntry.th32OwnerProcessID == dwProcessId)
{
threadId = threadEntry.th32ThreadID;
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, threadId);
if (hThread == NULL)
continue;
else
{
SuspendThread(hThread);
handles.push_back(hThread);
Sleep(100);
DWORD dwResult = QueueUserAPC((PAPCFUNC) pfnThreadRtn, hThread, (ULONG_PTR) pszLibFileRemote);
if (dwResult && !least)
least = TRUE;
}
}
}
}

for (HANDLE handle : handles) {
ResumeThread(handle);
CloseHandle(handle);
}

if (!threadId)
printf("Error: No threads found.\n");

if(!least)
printf("Error: No threads injected.\n");

CloseHandle(hSnapshot);
CloseHandle(hProcess);
}

int main(int argc, char* argv[])
{
if (argc < 3)
return 1;
DWORD pid = atol(argv[1]);
std::string path;
for (int i = 2; i < argc; ++i) {
path += argv[i];
path += ' ';
}

ByQueueUserAPC(path.c_str(), pid);
return 0;
}

0x04 Test

至此,我们需要的东西已经准备齐全

让我们测试一下成果

打开我们的OBS
OBS

用我们的工具注入一下
Inject

再看我们的OBS,钩子已经挂上,预览画面的时间定格在注入的一刻
Success

除了OBS的界面显示出了一些问题之外没有什么大问题,还挺不错

结语

然而,学校的古老XP系统并不支持我运行这个程序,再加上后来我发现我直接关掉软件老师也不管,于是这东西就没用了(当然,可以用来水博客)