intellij idea安装 北斗系统 idea 常用快捷键 devise vue树形菜单 相亲网站源码 微信小游戏开发视频 android经典项目开发实战 erp系统源码 android小程序源代码 华为路由器ipv6配置 kafka默认端口 一兆等于多少字节 linux获取当前时间 java接收数组 oracle查看所有数据库 python数据格式 python写文件 python的文件操作 python时间戳 python函数内定义函数 java正则表达 java教材 java字符串查找 java中的继承 java课程 randomjava javafloat java时间戳转换 java八大基本数据类型 java获取当前日期 linux密码 苹果手机老是自动重启 深入浅出通信原理 sim卡注册失败 bz2 字符串分割成数组 dnf瞎子传说套选择 处理器虚拟化技术 头条视频解析
当前位置: 首页 > 学习教程  > 编程语言

恶意代码研究 -- windows PE恶意代码(四)远程cmd

2021/1/28 22:36:52 文章标签:

远程CMD流程相关APICreatePipe函数CreateProcess函数WaitForSingleObject函数ReadFile函数示例代码总结windows系统中,cmd相当于在windows窗口中使用dos系统。简单理解就是通过黑乎乎的命令行界面完成键盘鼠标的操作,类似linux下的bash。 远程cmd可以看…

远程CMD

  • 流程
  • 相关API
    • CreatePipe函数
    • CreateProcess函数
    • WaitForSingleObject函数
    • ReadFile函数
  • 示例代码
  • 总结

windows系统中,cmd相当于在windows窗口中使用dos系统。简单理解就是通过黑乎乎的命令行界面完成键盘鼠标的操作,类似linux下的bash。

远程cmd可以看作一种后门,用于将受害者信息发送给攻击者或者传输恶意可执行程序(下载器),最常用的功能是接收攻击端传送过来的命令,执行某些操作。

Windows系统中有很多WIN32 API可以执行CMD命令,例如system Winexe CreateProcess等,但是它们只能执行命令,不能获得命令执行结果,所以对黑客来说价值就大打折扣了,实现远程CMD关键就在于获取执行结果。这里介绍通过匿名管道实现远程CMD。

流程

  • 初始化匿名管道的SECURITY_ATTRIBUTES结构体,调用CreatePipe创建匿名管道,获取管道数据读取句柄和写入句柄。

  • 初始化STARTUPINFO结构体,隐藏进程窗口,并把管道数据写入句柄赋值给新进程控制台窗口的缓存句柄。

  • 调用CreateProcess函数创建进程,执行CMD命令并调用WaitForSingleObject等待命令执行完。

  • 调用ReadFile根据匿名管道的数据读取句柄从匿名管道的缓冲区中读取数据。

  • 关闭句柄,释放资源。

相关API

CreatePipe函数

创造一个匿名管道,并从中得到读写管道的句柄。

BOOL WINAPI CreatePipe(
    _Out_ PHANDLE hReadPipe,
    _Out_ PHANDLE hWritePipe,
    _In_opt_ LPSECURITY_ATTRIBUTES lpPipeAttributes,
    _In_ DWORD nSize
    );

参数

  • hReadPipe :返回一个可读管道数据的文件句柄
  • hWritePipe:返回一个可写管道数据的文件句柄
  • lpPipeAttributes:传入一个SECURITY_ATTRIBUTES结构体指针,决定此函数返回的句柄能否由子进程继承,若是NULL则不能被继承
  • nSize指向管道的缓冲区大小(理想值)

返回值

  • 如果函数成功,则为TRUE;否则为FALSE

CreateProcess函数

用来创建一个新的进程和它的主线程,这个新进程运行指定的可执行文件。

BOOL WINAPI CreateProcess
(
    _In_opt_ LPCWSTR lpApplicationName,
    _Inout_opt_ LPWSTR lpCommandLine,
    _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ BOOL bInheritHandles,
    _In_ DWORD dwCreationFlags,
    _In_opt_ LPVOID lpEnvironment,
    _In_opt_ LPCWSTR lpCurrentDirectory,
    _In_ LPSTARTUPINFOW lpStartupInfo,
    _Out_ LPPROCESS_INFORMATION lpProcessInformation
);

参数(太多了,就写几个注意的到的):

  • lpApplicationName:这个参数用于指定可执行文件的名称也可以是可执行文件的路径,实际使用中常常把lpApplicationName设置为Null,用lpCommandLine参数来指定可执行文件名,这样可以扩大windows寻找可执行文件的范围

  • lpCommandLine:这个参数用于指定传递给新进程的命令行参数,更多时候我们把可执行文件名也包含在内(这意味着lpApplicationName应该设置为Null),比如:LPWSTR cmdLine = (LPWSTR)"XXX.exe -X -Y";

  • lpProcessAttributes:这个是一个指向SECURITY_ATTRIBUTES结构的指针,用来给进程对象设置安全性,即返回的新进程对象句柄能否被子进程继承,一般传递NULL,让windows把进程内核对象设为默认安全性,即新进程对象句柄不能被子进程继承

  • lpThreadAttributes:和上一个参数差不多。

  • bInheritHandles:关于安全性的标识符,是个BOOL型变量,用于控制新进程是否可以从调用进程处继承所有可继承的句柄,而被继承的句柄与原进程拥有完全相同的值和访问权限。设置为TRUE新进程将继承调用CreateProcess的原进程的所有可继承的句柄,而设置为FALSE则不会继承原进程的任何句柄。

  • dwCreationFlags:是个DWORD的标识,用于设置新进程创建的方式,各个标识符以宏的形式定义,可以用位或组合使用,同时指定多个标识符

  • lpStartupInfo:这个参数指向一个STARTUPINFOSTARTUPINFOEX的结构体,大可以全部置零,结构体定义如下:

typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;
  • lpProcessInformation:一个指向LPPROCESS_INFOMATION结构的指针,这个结构由调用CreateProcess的进程创建,由CreateProcess更改,用于返回新建进程的信息,其结构定义如下:
typedef struct _PROCESS_INFORMATION {
  HANDLE hProcess;
  HANDLE hThread;
  DWORD  dwProcessId;
  DWORD  dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION,*LPPROCESS_INFORMATION;

分别是进程句柄、线程句柄、进程ID和线程ID

WaitForSingleObject函数

等待一个内核对象变为已通知状态

DWORD WINAPI WaitForSingleObject(
    _In_ HANDLE hHandle, //指明一个内核对象的句柄
    _In_ DWORD dwMilliseconds //等待时间
    );

参数:

  • hHandle:传递一个内核对象句柄,该句柄标识一个内核对象,如果该内核对象处于未通知状态,则该函数导致线程进入阻塞状态;如果该内核对象处于已通知状态,则该函数立即返回WAIT_OBJECT_0

  • dwMilliseconds :指明了需要等待的时间(毫秒),可以传递INFINITE指明要无限期等待下去,如果第二个参数为0,那么函数就测试同步对象的状态并立即返回。如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED

ReadFile函数

从指定的文件或者I/O设备中,读取数据。

BOOL WINAPI ReadFile(
    _In_ HANDLE hFile,
    _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToRead,
    _Out_opt_ LPDWORD lpNumberOfBytesRead,
    _Inout_opt_ LPOVERLAPPED lpOverlapped
    );

参数:

  • hFile:文件指针(可以是管道)

  • lpBuffer:指针指向从文件读出的数据存放的缓冲区

  • nNumberOfBytesToRead:要读取的最多的字节数

  • lpNumberOfBytesRead:

    • 当使用同步读的方式的时候,一个指向变量的指针,表示实际读取的字节数。
    • ReadFile函数在实际工作或者检查错误之前,设置这个值为0.
    • 如果是异步操作,可以将这个值设置为NULL,以避免错误的结果。
    • 只有lpOverlapped 参数不为NULL的时候,这个参数才可以设置为NULL。
  • lpOverlapped:指向OVERLAPPED结构体的指针。

示例代码

以命令ping 127.0.0.1为例

#include<Windows.h>
#include<stdio.h>
#include<tchar.h>

void ShowError(char *pszText)
{
	char szErr[MAX_PATH] = { 0 };
	::wsprintf(szErr, "%s Error[%d]\n", pszText, ::GetLastError());
	::MessageBox(NULL, szErr, "ERROR", MB_OK);
}

/*
pszCmd:执行的系统命令
pszResultBuffer:存放命令执行结果的缓冲区
dwResultBufferSize:缓冲区读取最大字节数
*/
BOOL PipeCmd(char *pszCmd, char *pszResultBuffer, DWORD dwResultBufferSize)
{
	HANDLE hReadPipe = NULL;
	HANDLE hWritePipe = NULL;
	SECURITY_ATTRIBUTES securityAttributes = { 0 };
	BOOL bRet = FALSE;
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };

	// 设定管道的安全属性
	securityAttributes.bInheritHandle = TRUE;
	securityAttributes.nLength = sizeof(securityAttributes);
	securityAttributes.lpSecurityDescriptor = NULL;
	// 创建匿名管道
	bRet = ::CreatePipe(&hReadPipe, &hWritePipe, &securityAttributes, 0);

	if (FALSE == bRet)
	{
		ShowError("CreatePipe");
		return FALSE;
	}

	// 设置新进程参数
	si.cb = sizeof(si);
	si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
	si.wShowWindow = SW_HIDE;
	si.hStdError = hWritePipe;
	si.hStdOutput = hWritePipe;
	// 创建新进程执行命令, 将执行结果写入匿名管道中
	bRet = ::CreateProcess(NULL, pszCmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
	if (FALSE == bRet)
	{
		ShowError("CreateProcess");
	}

	// 等待命令执行结束
	::WaitForSingleObject(pi.hThread, INFINITE);
	::WaitForSingleObject(pi.hProcess, INFINITE);

	// 从匿名管道中读取结果到输出缓冲区
	::RtlZeroMemory(pszResultBuffer, dwResultBufferSize);
	::ReadFile(hReadPipe, pszResultBuffer, dwResultBufferSize, NULL, NULL);
	// 关闭句柄, 释放内存
	::CloseHandle(pi.hThread);
	::CloseHandle(pi.hProcess);
	::CloseHandle(hWritePipe);
	::CloseHandle(hReadPipe);

	return TRUE;
}

int _tmain(int argc, _TCHAR* argv[])
{
	char szCmd[] = "ping 127.0.0.1";
	char szResultBuffer[512] = { 0 };
	DWORD dwResultBufferSize = 512;

	// 执行 cmd 命令, 并获取执行结果数据
	if (FALSE == PipeCmd(szCmd, szResultBuffer, dwResultBufferSize))
	{
		printf("pipe cmd error.\n");
	}
	else
	{
		printf("CMD执行结果为:\n%s\n", szResultBuffer);
	}

	system("pause");
	return 0;
}

执行结果:
在这里插入图片描述
IDA反汇编后的代码如下:
在这里插入图片描述在这里插入图片描述可以看到编译时RtlZeroMemorymemset取代了。

总结

主进程,子进程,管道的关系可用下图表示:

写入命令执行结果
主进程读取命令执行结果
子进程
管道
主进程

整个流程如下图所示:

Created with Raphaël 2.2.0 主进程准备好读取命令执行结果用的缓冲区 主进程创建管道 主进程创建子进程执行命令 子进程执行命令,并将命令执行结果传入管道 主进程从管道读取命令执行结果,打印

这里只是创建子进程执行ping命令并且打印,实际恶意代码中,打印的操作可以用后门传输的操作替代。


本文链接: http://www.dtmao.cc/news_show_650036.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?