屏幕监控

暂未完成

管道实现本地CMD

创建进程的3中方式:

  1. WinExec()
  2. ShellExecute()
  3. CreateProcess()

image-20200629210350955

项目如下:

image-20200629210443728

进程间通信有很多种

  1. 管道(1.匿名管道2.命名管道)
  2. 邮件槽
  3. 剪切板
  4. 消息
  5. 共享内存
  6. 本地socket
  7. 同步事件
  8. 文件
  9. 等等

父子间通信,通过命令行相当于单次通信(由父到子)

今天用管道。

匿名管道(Anonymous Pipes)是在父进程和子进程间单向传输数据的一种未命名的管道,只能在本地计算机中使用,而不可用于网络间的通信。

linux a–>| –>b a程序通过管道给b程序数据流

windows netstat -an | find “80”

上面的解释:netstat -an输出所有端口的结果通过管道”|把结果作为输入,给find “80”命令来处理

windows管道(英语pipe):将a程序的数据发给b程序

CreatePipe 创建管道的api

该函数创建一个匿名管道,返回一个读和写管道的句柄

匿名管道是单向的,命名管道是双向的。

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
HANDLE hReadPipe;//读句柄
HANDLE hWritePipe;//写句柄
bool bRet=CreatePipe(&hReadPipe,//读句柄
&hWritePipe,//写句柄
NULL,
0);//一次能写多大
if(!bRet)
{
printf("失败\n");
return 0;
}
chat szBuf[256];
memset(szBuf,0,256);
int nLength=strlen("Hello World!")+1;
int nWritedLength=0;//一旦成功,写了多大
memset(szBuf,"Hello World!",nLength);
bRet=WriteFile(hWritePipe,//管道和文件都是用同一个api来操作的
szBuf,//写什么
nLength,//写多大
(LPDWORD)&nWritedLength,//写成功多少
NULL);//异步相关(NULL表示同步,意思是他没写完,则程序一直在这等)
if(!bRet)
{
printf("失败\n");
return 0;
}

编程的套路就是调用api。

读管道:

1
2
3
4
5
6
7
8
9
10
11
char szOutBuf[256]={0};
ReadFile(hReadPipe,//读句柄
szOutBuf,//读在哪
256,//读取的最大字节数。
&nReadBytes,
NULL);
if(!bRet)
{
printf("失败\n");
return 0;
}

读的时候是堵塞的,解决卡死的两种方案:

  1. 针对堵塞

    (1)关掉阻塞,异步的写法 必须使用命名管道CreateNamedPipe

    (2)管道里面的数据读不够255个字节,但是你又要求他读255个字节,一直读不够,就会在这里等待,即卡死。使用PeekNamedPipe来判断管道中是否还有剩余字节数。PeekNamedPipe也可用于匿名管道。

因为cmd程序不是我们自己写的,因此并不能在cmd中去读管道,因此我们在创建cmd进程的时候将匿名管道导入标准输入

标准输入输出抽象成设备

image-20200629220911038

命名管道较为复杂,写法有点像网络通信。

因此此处用两个匿名管道来实现。

image-20200629221800688

为了远程向cmd传命令行参数,替换cmd的标准输入输出。

1
2
3
4
5
6
7
8
STARTUPINFO si = {0};
si.cb=sizeof(STARTUPINFO);
si.dwFlags=STARTF_USESTDHANDLES;//标志位,开启句柄替换的标志。不开启底下输入输出换管道就没有意义
si.hStdInput=hCmdReadPipe;//输入改成管道
si.hStdOutput=hCmdWritePipe;//输出改成管道
si.hStdError=hCmdWritePipe;//标准错误输出改成管道输出


image-20200629222151897

TRUE,代表父进程创建子进程的时候允许他继承,即子进程才能继承父进程管道的句柄。不允许继承的话,cmd没法访问管道句柄。

管道也要打开继承属性。

1
2
3
SECURITY_ATTRIBUTES sa={0};
sa.nLength=sizeof(SECURITY_ATTRIBUTES );
sa.bInheritHandle=TRUE;

搞定了以后,把我们的数据通过WriteFile写入hMyWritePipe,通过ReadFile读hMyReadPipe

只用操作hMyWritePipe和hMyReadPipe。

\r是换行,\n是回车

写命令不要带零结尾!!!!

作业

image-20200629225519380

学有余力的话加个UI([[MFC]] [[QT]] libcef/JS)

代码在管道仿cmd

坑点:

  1. CreatePipe的读写句柄写反了
  2. PreTranslateMessage中对回车添加响应
  3. GetDlgItemTextA(this->m_hWnd,IDC_EDIT1, szInBuf,256);通过这个方式读到了char*字符串,转换为CString进行处理和显示,之所以要转换为char*而不直接转换为CString是因为ReadFile等api要求char*变量。
  4. OnInitDialog中添加初始化代码。$err,hr用于监视api返回结果

疑惑点待解决:

ReadFile的堵塞问题。https://zhuanlan.zhihu.com/p/36064645

现存bug是有时候读取不完整,每次cmd还没来得及给管道输入东西,我们已经先把东西读出来了。因此会出现读取不完整的情况

网络编程开发初步

需要一台具备公网ip的服务器

家用的宽带相当于运营商把你放在一台交换机之下的内网中,因此没有公网ip

没有公网ip怎么办呢?

在局域网中通内网的另外一条普通的电脑

通信的api

套接字编程(socket一种编程标准)(linux和windows提供的接口是一样的)

但是windows比较恶心一点,让你用windows api进行一些初始化。

tcp 可靠通信 udp 不可靠通信(包在传送中的丢失我们不处理就叫不可靠)

TCP协议:由操作系统帮我们做好了包的发送和接收的可靠处理,我们写代码不用考虑包究竟到没到目标电脑,中间存不存在丢失。tcp由于全世界所有电脑都在使用,所以他会考虑普通的应用需求,可能速度会有影响。

UDP协议:直接利用底层数据包传输,不考虑丢包问题,因此用户使用时,可能会丢包。用这个要自己考虑丢包问题。

总之tcp降低了速度换取了可靠,解决办法:使用第三方的可靠的库,就是使用了UDP,但是自己在用户层处理了可靠的传输,KCP。

网络里比较好的书:wireshark抓包分析实战,wireshark网络分析就这么简单

使用tcp通信:

需要两端:服务端 客户端

C/S架构

image-20200701220817504

B/S(浏览器)是一种特殊的C/S架构

还有一种架构,p2p架构

网络编程相关的api:

使用tcp通信:

Server端主体api
  1. socket 创建套接字
  2. bind/listen 绑定端口/监听
  3. accept 接收请求
  4. recv/send 收包/发包
  5. closesocket 关闭套接字
Client端主体api
  1. socket 创建套接字
  2. connect 连接服务器
  3. recv/send 收包/发包
  4. closesocket 关闭套接字

可靠体现在哪里?

客户端向服务端发起连接,服务端接收请求,就建立了可靠连接,就可以互相收发包了。

包含通信相关的头文件:

#include<winsock2.h>

包含通信相关的库文件:(告诉连接器连接的时候要找ws2_32.lib,这样你就不用在linker的lib设置里指定这个lib了。)

#pragma comment(lib,”ws2_32.lib”)

服务端:

**p.s.**windows很特殊,需要单独调用api来进行网络的初始化和反初始化。

如下:

image-20200701222427183

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//windows很特殊,需要单独调用api来进行网络的初始化和反初始化
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
//初始化操作
err = WSAStartup(wVersionRequested, &wsaData);
if (err!=0)
{
return 0;
}
//此处添加自己的代码

//...

//反初始化操作
WSACleanup();

socket 创建套接字

1
2
3
4
5
6
7
8
9
SOCKET s=socket(
AF_INET,//INET协议簇
SOCK_STREAM,//表示TCP协议 SOCK_DGRAM表示udp
0
);
if(s==INVALID_SOCKET)
{
...
}

bind/listen 绑定端口/监听

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
//netsata -an打印被占用的端口
//端口 3389:RDP 80:HTTP端口,端口用于区分哪个应用的数据,只是个编号,范围是0-65535(因为在封包中占两个字节)
//一个应用程序可使用多少个端口?
//看见有多少个tcp连接
//源ip:源端口 ---------------- 目标ip:目标端口
//sockaddr addr;//用于描述ip和端口的结构体,但不好用
sockaddr_in addr;//用与替换sockaddr结构体,两者一致,可以混着用,但这个更好用。
addr.sin_family=AF_INET;//写死
addr.sin_addr.S_un.S_addr=inet_addr("0.0.0.0");//固定套路,写0.0.0.0表示当前所有网卡的地址都可以接受外界的连接,如果写死一个ip就只可以接受该ip的地址。想要连外网,局域网最好写0.0.0.0
addr.sin_port=htons(10087);//有个坑。网络字节序(大端)和本地字节序(小端)。比如说端口由两个字节组成,网络的一般是大端排序,所以要htons()函数:host to network short(short表示2个字节,long表示4个字节)
int nLength=sizeof(sockaddr_in);
int nRet=bind(//挑选一个本机其他软件没有使用的端口来绑定,固定的端口不能使用
s,//套接字
(sockaddr*)&addr,//用于描述ip和端口的结构体
nLength
);
if(SOCKET_ERROR==nRet)
{
return 0;
}
//一个端口等别人来连接,要是很多人来就让他排队
//监听
nRet=listen(s,
5 );//同一瞬间来5个,排队容量
if(SOCKET_ERROR==nRet)
{
return 0;
}
//绑定只是在软件里面绑定起来了,listen才是真正在操作系统开了个端口,所以会弹防火墙允许访问的请求对话框。

accept 接收请求(阻塞等到有人连接)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//我们的服务端可以同时连接很多客户端
sockaddr_in remoteAddr;//对方的ip和端口的信息
remoteAddr.sin_family=AF_INET;
int addrlen=sizeof(sockaddr_in);
//返回的socket sClient是专门用于和客户端通信的socket,一般是开线程循环接收
SOCKET sClient=accept(s,//表示连接socket专门用于连接
(sockaddr_in*)&remoteAddr,
&addrlen
);
if(sClient==INVALID_SOCKET)
{
return 0;
}

recv/send 收包/发包

1
2
//一旦成功建立连接,那么接下来就是收包和发包的过程(不用考虑丢包的问题)
int nSendedBytes=send(sClient,"Hello World",strlen("Hello World")+1,0);

closesocket 关闭套接字

1
closesocket(s);

客户端:

connect 连接服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sockaddr_in addr;//用与替换sockaddr结构体,两者一致,可以混着用,但这个更好用。
addr.sin_family=AF_INET;//写死
addr.sin_addr.S_un.S_addr=inet_addr("0.0.0.0");//连谁填谁,目标ip
addr.sin_port=htons(10087);//目标端口 有个坑。网络字节序(大端)和本地字节序(小端)。比如说端口由两个字节组成,网络的一般是大端排序,所以要htons()函数:host to network short(short表示2个字节,long表示4个字节)
int nLength=sizeof(sockaddr_in);
int nRet=connect(//挑选一个本机其他软件没有使用的端口来绑定,固定的端口不能使用
s,//套接字
(sockaddr*)&addr,//用于描述ip和端口的结构体
nLength
);
if(SOCKET_ERROR==nRet)
{
return 0;
}

recv/send 收包/发包

1
2
3
4
5
6
7
8
9
10
11
12
13
//一旦成功建立连接,那么接下来就是收包和发包的过程(不用考虑丢包的问题)
char szBuf[256]={0};
int nRet=recv(s,szBuf,256,0);
if(nRet==0)//这里的判断比较复杂
{
//表示tcp断开了
return 0;
}
else if(nRet>0)
{
//返回值>0 表示成功的字节
printf(szBuf);
}

closesocket 关闭套接字

1
closesocket(s);

作业:

在socket编程双方聊天文件夹中

难点总结:

  1. 第一,服务端发送的套接字是accept中的sClient
  2. 接收字符串的时候用getchar才能控制用户输入的长度,getchar多余的键也会进入缓冲区,需要吃掉多余输入。send发送传入的指针,因此最后必须要\0结尾。

(1)用fflush(stdin);//清空输入缓冲区:兼容性很差,某些环境无效

(2)使用getchar不断获取缓冲区内的内容,直到缓冲区内空为止,这种方法很有效,建议使用,C primer 上也介绍了这种方法。

1
while ((c = getchar()) != EOF && c != '\n');//不停地使用getchar()获取缓冲中字符,直到获取的c是“\n”或文件结尾符EOF为止  

(3) setbuf(stdin, NULL);//使stdin输入流由默认缓冲区转为无缓冲区

tcp原理:

Wireshark是一个抓包工具包(流经网卡的数据包,本质上是一个[[qt]]编写的界面)

这个软件用的是winpcap,别人写好的库来实现的抓包,流经网卡的数据被这个库拦截下来进行分析,wireShark给了他一个界面

数据包根据他的包的结构可以划分多种类型(其实就是根据协议划分)

协议:数据格式的规范

网络协议很复杂,因此,每一层解决一个问题

网络协议分层:OSI七层模型(实际上现在是5层)

计算机里的两种思维:1.分层思维,2.缓存思维

客户端5层:

应用层(软件自己定义的协议/http,smtp,pop3,ftp…)(解决业务逻辑问题)

传输层 (TCP/UDP)(解决通信可靠性的问题)UDP的包只有8个字节 TCP有20个字节

网络层 (IP)(ipv4有2的32次方个ip,ipv6解决两台终端寻找通信的问题,数据包寻路,用于找到目标终端)

数据链路层(Mac地址,可以修改,设备的真正的物理地址,解决两台设备通信的问题(网线,wifi等),数据包的下一跳去哪里)

物理层(物理的光电通信)(把数字信号变成电信号,或者光纤的话变成光信号)

image-20200703214918912

5层,从应用层给传输层最后到物理层,每层都会有相应的处理

TCP:三次握手

tcp在进行通信前需要建立连接,一旦连接建立完成,除非连接中断,否则数据可以完全达到另外一端。

三次握手

image-20200703220301477

黑色代表包丢失了重传的部分

下面就是三次握手(建立连接的过程):

image-20200703221337098

客户端—->服务端发请求包(SYN)

服务端—->客户端发收到包(SYN+ACK)

客户端—->服务端发确定包(ACK)

狂发SYN包攻击。。。

根据时间来判断包是否丢了。

断开连接的四次发包,即四次挥手:

image-20200703222637540

断开连接发起可以是客户端也可以是服务端

客户端—->服务端发请求包(FIN)

服务端—->客户端发收到包(ACK)==不在同一时间所以不能合并

服务端—->客户端发确定包(FIN)=====不在同一时间所以不能合并

-客户端—>服务端发收到包(ACK)

TCP的特性:数据流SOCK_STREAM代表的就是TCP

一次发包发多少都可以总之形成一个流,但这过程会有一个粘包的过程。语音,图片,文本。。所有东西都粘在一起发。对方并不能处理,因此需要分割。一般来说是标志位+长度+数据来分割。这就是完全由自己定义的协议

一般越复杂的软件,协议越复杂。

cmd与网络通信结合

界面版 远程cmd功能 服务端控制客户端?

客户端:

image-20200703225012357

//开辟一个线程来处理读取管道内容并发送回服务端。

1
2
3
4
5
6
7
//头文件:#include<thread>
//创建了一个线程对象lamada表达式(C语言的东西,标准库的写法),括号里是线程的回调函数
std::thread thd([&](){
//表示程序拥有一个新的执行起点
})
//避免主要线程结束时,子线程对象被销毁
thd.detach();

作业:

image-20200704141305778

注意点:子线程改变ui可以通过线程传参或者自定义消息

键盘记录器(键盘钩子)

Hook 钩子

很多平台都能使用钩子的技术

什么是钩子?

针对第三方程序(执行流程通常不能改变,但是我们经常希望修改别人的代码)

已经能成熟的帮我们完成注入的操作的框架:
frida
xposed(更偏重于底层)

windows有一个api叫SetWindowsHookEx
api的好处就是很稳定,不会随系统变化而变化,缺点是太普通了,达不到高精尖的操作。

SetWindowsHookEx

开始处理HOOK才做,钩子链,谁最后下钩子,谁最先被调用

键盘钩子回调函数:

1
2
3
4
5
6
7
8
9
HHOOK g_hhk;
LRESULT CALLBACK KeyboardProc(int code,WPARAM wParam,LPARAM lParam)
{

OutputDebugStringA("keyboard pressed!");
//调用下一个钩子,这是个规范
return CallNextHookEx(g_hhk,code,wParam,lParam);
}

SetWindowsHookEx本地钩子调用过程:

1
2
3
4
5
6
7
8
9
10
g_hhk=SetWindowsHookEx(WH_KEYBOARD,//钩子类型:键盘钩子
(HOOKPROC) KeyboardProc,//回调函数
NULL,//表示第三方的注入DLL,全局钩子使用。比如说第三方本地程序钩子或者全局钩子,系统会自动帮我们把dll注入进目标程序
GetCurrentThreadId())//线程id,填0表示勾所有

if(g_hhk==NULL)
{
AfxMessageBox(_T"下钩子失败");
return;
}

调试输出工具,输出调试字符串,方便调试:DebugView

DebugView 中[中显示数字]代表进程id

写日志,发行后的信息记录

SetWindowsHookEx全局钩子调用过程:

调试的时候:

image-20200705220044433

只会显示到输出界面,不会显示到DebugView中。

虚拟键 我们的键盘在我们的windows中用一些宏来表示

VK虚拟键码,0-9的键值等同于’0’~’9’,可以跳到定义查看。

image-20200705220725333

钩子类型为键盘钩子的时候,wParam存的就是虚拟键码。

lParam里存有能判断是否按下各种状态键没比如alt键等等的信息。

全局钩子需要一个DLL(动态链接库)

image-20200705221146483

查看pe的小工具,可用于查看导出函数

msvcrt.DLL中存着各种c库函数

我们需要编写一个键盘钩子回调函数,编写一个dll,将hook函数放到dll中,然后让操作系统去使用该dll,让其勾住其他进程的键盘消息。

要编写两个程序:dll(钩子回调函数)+exe(调用SetWindowsHookEx)

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
HHOOK g_hhk;
LRESULT CALLBACK KeyboardProc(int code,WPARAM wParam,LPARAM lParam)
{

OutputDebugStringA("keyboard pressed!");
//调用下一个钩子,这是个规范
return CallNextHookEx(g_hhk,code,wParam,lParam);
}




//dll导出函数提供给其他程序使用
BOOL MySetHook()
{
g_hhk=SetWindowsHookEx(WH_KEYBOARD,//钩子类型:键盘钩子
(HOOKPROC) KeyboardProc,//回调函数
g_hModule,//在dll初始化处获得该句柄
0);//线程id,填0表示勾所有

if(g_hhk==NULL)
{
AfxMessageBox(_T("下钩子失败"));
return false;
}
return true;
}

写个导出文件:

1
2
3
LIBRARY
EXPORTS
MySetHook

导出成功:

image-20200705222632266

exe调用:

1
2
3
4
5
6
7
BOOL MySetHook();//函数声明
BOOL bRet=MySetHook();
if(!bRet){
AfxMessageBox(_T("下钩子失败"));
return false;
}

image-20200705222819636

链接时候出错,因此要告诉程序dll所在位置:

1
#pragma comment(lib,"../../bin/KeyHook.lib")

最终结果,有的时候能勾住,有的时候勾不住,一般而言,大部分都能勾住,但是由于微软api的原因,有部分勾不到,比如说qq的账号密码就勾不到。

作业:

客户端按下某键盘,传送到服务端记录

image-20200705224557331

最简单的跨进程通信方式:发送消息WM_COPYDATA

(此处缺失一张图片)

winMain 有界面的程序的入口函数

创建一个结构体,用于将数据传输:

image-20200705230842481

dll中发送消息的函数:

1
2
3
4
5
SendMessage(g_hwnd,//向目标窗口发送消息
WM_COPYDATA,
(WPARAM)g_hwnd,//当前自己的窗口
(LPARAM)&cds //创建一个结构体,用于将数据传输
)

客户端的处理:

1
2
PCOPYDATASTRUCT pcds=(PCOPYDATASTRUCT)lParam;
//lParam.lpData表示我们需要的数据

先做:

image-20200705231803595

作业注意点:

  1. 把dll拷到exe目录或者环境设置目录。用于解决找不到dll,无法执行程序弹框

MFC的自定义消息不会!

消息及粘包处理

1)动态库发送数据给客户端:

windows消息机制

WM_COPYDATA消息

创建一个窗口是需要有一定的步骤

  1. 注册窗口类
  2. 创建窗口
  3. 消息循环(消息机制)(windows解决做什么和什么时候做的事情)解耦合,高内聚,低耦合
  4. 窗口回调函数
1
WM_COPYDATA OnCopyData()

消息比较多,所以消息会排队—-消息队列

钩子和消息的关系:

1
2
3
4
5
6
7
8
9
10
//消息循环
whiletrue){
//从消息队列中取消息
GetMessage WM_KEYDOWN
//派发消息
DispatchMessage
//派发之前先去查找窗口
//判别有没有消息钩子,如果有则先调用
//分发消息给消息回调函数
}

2)客户端将数据发送给服务端

需要处理粘包问题(结构体要注意字节对齐问题)

image-20200706224314705

image-20200706225404931

上方的data[1]实际上就是个指针

image-20200706225517899

发包:

image-20200706225931768

收包处理的时候:要自己完善,并没有处理粘包和断包的问题

需要封装一个函数,收取指定字节大小的数据,先收8字节长度的头部,再根据头部中的长度,收取后面的字节数。recv函数你写要收多少,但实际上他并不能收到那么多个字节。

image-20200706230522339

3)C++中map 哈希表:(空间换时间)

键 键值

key——–value

VK_F1 “VK_F1”

1
2
3
4
5
6
//头文件:#include <map>
std::map<int,std::string> m;
m.insert(std::pair<int,std::string>(VK_F1,"F1"));
m.insert(std::pair<int,std::string>(VK_F2,"F2"));
m.insert(std::pair<int,std::string>(VK_F3,"F3"));
std::cout<<m[VK_F1].c_str()<<std::endl;

注意:

一般有的窗口对全局钩子没有反应是句柄的问题:dll被加载的时候写这个代码:

image-20200706222750126

作业:

image-20200706231402383

出bug点:试图用strcat来拼接结构体。。

GIT代码管理

image-20200708212230973

设置系统环境变量(也可以不需要)

image-20200708212503703

cmd中到代码文件夹路径打git clone 代码网址(从github等git仓中下载源文件到本地)

进到根目录项目名称文件夹下用git status查看当前被管理的工程文件状态(变红表示变红文件被修改了)

git add .表示将修改的文件添加进来,方便下次提交(.表示所有文件)

记录这个代码谁提交的

image-20200708213526235

image-20200708213554905

提交了以后就会进到一个vi编辑器,按冒号键,打q!,回车强制退出

git commit -m “注释内容”将文件保存起来,将我们的文件交给git保存,但仅仅是保存在本机,还没传到github(类似于做了一个备份)

git push提交给github服务器(git push -f强制推送)

有一个.git文件夹表示.git所在文件夹已被git接管

每次push代码前点击.def文件清一下数据,尤其是类似.db这种比较大的文件

结果:

image-20200708213858036

右键项目,点git gui here能看到你的操作记录

FORK代表完全拷贝别人代码的

国内的coding腾讯的也可以。

程序规范

程序的引入库

1
2
3
4
5
#ifdef _DEBUG
#pragma comment(lib,"..\debug\..")
#else
#pragma comment(lib,"..\release\..")
#endif

lib只需要编译时候指定的,而dll是跑程序的时候要用的

mfc:

image-20200708222249636

image-20200709145317675

image-20200709152845052

注意:msdn中methon后缀的就是mfc的方法

要装两个东西,msdn和git

线程过程函数直接放到类里面静态函数,为了封装性

服务器线程中不断的accept

会话结构体

image-20200708225159793

每创建了一个会话后的处理(还)

image-20200708225630542

因为这个map会被很多线程访问,所以要互斥体上锁

image-20200708225859588

完善:(还要完善)

image-20200708230605872

还需要创建线程来接收数据

这种线程创建方式可以使用外界的变量

image-20200708230851498

image-20200708231112490

这个写法出作用域的话会析构对象,就相当于析构了锁对象

常规的锁是这样写的:

image-20200708231153005

插入行

image-20200708231818426

上面的inet_ntoa是char版,所以会乱码

宽窄字符转换方式:

image-20200708232251247

image-20200708232355053

监听ip和端口号可以做成配置文件

粘包+屏幕查看功能

屏幕功能

客户端:不断地截图

服务端:不断地接收截图并显示

第一步先做win项目本地截图显示到程序中进行测试。

GDI api

涉及到的api及基本概念

窗口 — 窗口句柄

windows操作系统全都是窗口,桌面也是窗口

窗口句柄:32位的数据(64位系统也一样),操作系统用来表示窗口的数据

1
2
//获取桌面窗口句柄的api:
HWND hDesktopWnd=GetDesktopWindow();

DC — 设备上下文:

就是windows对显示设备的一种封装,想要把东西显示出来就需要创建一个dc

DC句柄:表示我们创建的dc

1
2
//获取桌面的窗口句柄:
HDC hDeskDC=GetDC(hDesktopWnd);
1
2
//创建一个兼容所有设备的桌面的dc(也叫内存dc)的api:
HDC hMemDC=CreateCompatibleDC(hDeskDC);

dc的理解:用现实中的例子来理解可能更容易些。如果你喜欢画画,你得先准备了画布,画笔,颜料……画画的环境搭建好了,你就可以画画了。这个画画的环境,就是DC。

p.s.兼容dc(内存dc):兼容所有设备的dc,是一个虚拟的内存设备上下文,我们对它进行绘图等操作,不会显示在屏幕或打印机上,而我们可以在它完成之后,拷贝到屏幕上或打印机上来输出,这样我们可以避免因为操作而给屏幕带来的闪烁(其实就相当于开了一个自己的画布,并且画布的数据还能通过内存dc取出来)

用现实中的例子来理解可能更容易些。如果你喜欢画画,你得先准备了画布,画笔,颜料……画画的环境搭建好了,你就可以画画了。这个画画的环境,就是DC。

也就是GDI是画在DC上的,DC再显示在屏幕上,DC为GDI基础。通过GDI绘制出来时看不到的也就是不显示的,只在内存上的一副图片,这幅图可以通过DC绘制在设备上

// 所谓的双缓冲就是把所有的绘制工作都做在一个内存DC上。
// 最后一次拷到屏幕DC上,只能有一次

p.s.这里所强调的“一次”;是不要连续将几个内存DC的内容都拷到屏幕DC上,这样没有起到双缓冲的效果。如果你搞了很多个内存DC,想把这些东西都显示出来,那你应该先把这多个内存DC的内容同时拷到另外一个内存DC上,再把这个内存DC的内容拷到屏幕DC上。

准备好画布以后,还要准备如下:

画笔,画刷,位图等等6种GDI对象

位图:描述了整张图片的每个像素点对应rgb颜色,

R: 255种颜色=8位2进制=2位16进制=1个字节 每个像素点的颜色信息要占4个字节,因为还有透明度

因此一张全屏幕位图多大:1920*1080*4字节=8100KB=大概7.91MB(实际上位图并不是每个点都存,有优化方式可以节省空间)

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
//获取当前桌面分辨率(通过该函数可以拿到系统的某些属性,函数名翻译为:获取系统指标)
int nWidth=GetSystemMetrics(SM_CXFULLSCREEN);
int nHeight=GetSystemMetrics(SM_CYFULLSCREEN);
//我们需要将我们的桌面有关的图片存放在hMenDC的位图中
//创建一个和桌面有关的内存位图:
HBITMAP hBitMap=CreateCompatibleBitmap(hDeskDC,nWidth,nHeight);
//将我们的内存dc与位图相关联
SelectObject(hMemDC,hBitMap);
//将当前桌面DC的具体数据拷贝给内存DC(桌面DC瞬间万变的,拷贝某一瞬间的所有信息,按位拷贝)
BOOL bRet = BitBlt(
hMemDC,//拷贝到内存dc中
0,//拷贝到内存dc中哪里,左上角x坐标
0,//拷贝到内存dc中哪里,左上角y坐标
nWidth,//拷贝多宽
nHeight,//拷贝多高
hDeskDC,//从哪拷贝,源头dc
0,//拷贝源头dc的哪里,左上角x坐标
0,//拷贝源头dc的哪里,左上角y坐标
SRCCOPY//按字节拷贝模式
);
int nBufSize = nWidth*nHeight * 4;//位图的所需字节空间
char* pBitMapBuf = new char[nBufSize];//开辟那么大的内存空间用于存位图数据
//从内存DC中获取位图数据
LONG nBitSize=GetBitmapBits(hBitMap,//位图
nBufSize,//取多少
pBitMapBuf);//取出来的数据

p.s. 上诉过程的理解:物理HDC 设备底层会拥有显存等资源,但是兼容DC并没有给图像像素提供内存空间,因此兼容DC总是和BITMAP配合使用,这样一来,兼容DC就利用BITMAP的图像像素数据空间给自己提供类似于显存的内存空间.这样有很多好处,以来我们可以在加载图片后,在图片上利用DC的各种绘图功能.兼容DC在建立之初,只有1*1像素的尺寸,SelectObject选择bitmap以后才可以进行绘图。兼容DC,也就是用HANDLE memdc = CreateCompatibleDC所创建的,它没有资源或者说是存储空间,所以就把BITMAP扯了进来,你在memdc区域绘图的时候,需要一个位图结构来存储这些数据。就是用SelectObject将hBitmap选入到memdc连联起来,将hBitmap作来memdc的存储空间。HBITMAP不能从直接从屏幕设备描述表那里获得数据,它需要经过内存DC来将屏幕设备描述表中的数据读出来,并拷贝到HBITMAP中。

加上异常处理后:

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
//显示桌面的数据
BOOL GetMyCapture()
{
HWND hDesktopWnd=NULL;
HDC hDeskDC = NULL;
HDC hMemDC = NULL;
HBITMAP hBitMap = NULL;
char* pBitMapBuf = NULL;
BOOL bResult = TRUE;
try
{
//获取桌面的窗口句柄
hDesktopWnd = GetDesktopWindow();
//创建一个桌面dc
hDeskDC = GetDC(hDesktopWnd);
if (hDeskDC == NULL)
{
throw "error";//抛异常
}
//创建一个桌面的内存dc
hMemDC = CreateCompatibleDC(hDeskDC);
if (hMemDC == NULL)
{
throw "error";//抛异常
}
//获取当前桌面分辨率(通过该函数可以拿到系统的某些属性,函数名翻译为:获取系统指标)
int nWidth = GetSystemMetrics(SM_CXFULLSCREEN);
int nHeight = GetSystemMetrics(SM_CYFULLSCREEN);
//我们需要将我们的桌面有关的图片存放在hMenDC的位图中
//创建一个和桌面有关的内存位图:
hBitMap = CreateCompatibleBitmap(hDeskDC, nWidth, nHeight);
if (hBitMap==NULL)
{
throw "error";//抛异常
}
//将我们的内存dc与位图相关联
SelectObject(hMemDC, hBitMap);
BOOL bRet = BitBlt(
hMemDC,//拷贝到内存dc中
0,//拷贝到内存dc中哪里,左上角x坐标
0,//拷贝到内存dc中哪里,左上角y坐标
nWidth,//拷贝多宽
nHeight,//拷贝多高
hDeskDC,//从哪拷贝,源头dc
0,//拷贝源头dc的哪里,左上角x坐标
0,//拷贝源头dc的哪里,左上角y坐标
SRCCOPY//按字节拷贝模式
);
if (!bRet)
{
throw "error";
}

int nBufSize = nWidth*nHeight * 4;//位图的所需字节空间
pBitMapBuf = new char[nBufSize];//开辟那么大的内存空间用于存位图数据
if (pBitMapBuf == NULL)
{
throw "error";
}
//从内存DC中获取位图数据
LONG nBitSize=GetBitmapBits(hBitMap,//位图
nBufSize,//取多少
pBitMapBuf);//取出来的数据
if (nBitSize==0)
{
throw "error";
}

//到这里表示拿到了数据
}
catch (...)
{
bResult = FALSE;
}
//很重要,一定要写,用于释放资源,防止内存泄露,不处理相当于不断申请内存不释放,电脑越来越卡
if (hDeskDC != NULL)
{
ReleaseDC(hDesktopWnd, hDeskDC);
}
if (hMemDC != NULL)
{
DeleteDC(hMemDC);
}
if (hBitMap != NULL)
{
DeleteObject(hBitMap);
}
if (pBitMapBuf != NULL)
{
delete[] pBitMapBuf;
}
//成功就返回true,失败就返回false
return bResult;
}

MFC添加右键菜单的代码:(要添加菜单资源)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CteamviewerServerDlg::OnNMRClickList1(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
//用户右键菜单显示效果
CMenu mn;
//加载菜单资源
mn.LoadMenu(IDR_MENU1);//这里虽然划红线,但实际没有错误就很迷
//获取子菜单
CMenu* pSubMenu = mn.GetSubMenu(0);
//获取鼠标当前的位置
CPoint pt;
GetCursorPos(&pt);
//判断是否选中列表控件的一行,否则不能弹出右键菜单栏
if (m_lst.GetSelectedCount() > 0)
{
//设置菜单显示的风格,和显示的位置当前鼠标位置,和窗口并进行显示
pSubMenu->TrackPopupMenu(TPM_LEFTALIGN, pt.x, pt.y, this);
}
*pResult = 0;
}

单机测试内存DC+位图gdi对象实现的截图功能:

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
HWND hDesktopWnd = NULL;
HDC hDeskDC = NULL;
HDC hMemDC = NULL;
HBITMAP hBitMap = NULL;
char* pBitMapBuf = NULL;
BOOL bResult = TRUE;
try
{
//获取桌面的窗口句柄
hDesktopWnd = ::GetDesktopWindow();
//创建一个桌面dc
hDeskDC = ::GetDC(hDesktopWnd);
if (hDeskDC == NULL)
{
throw "error";//抛异常
}
//创建一个桌面的内存dc
hMemDC = ::CreateCompatibleDC(hDeskDC);
if (hMemDC == NULL)
{
throw "error";//抛异常
}
//获取当前桌面分辨率(通过该函数可以拿到系统的某些属性,函数名翻译为:获取系统指标)
int nWidth = GetSystemMetrics(SM_CXFULLSCREEN);
int nHeight = GetSystemMetrics(SM_CYFULLSCREEN);
//我们需要将我们的桌面有关的图片存放在hMenDC的位图中
//创建一个和桌面有关的内存位图:
hBitMap = CreateCompatibleBitmap(hDeskDC, nWidth, nHeight);
if (hBitMap == NULL)
{
throw "error";//抛异常
}
//将我们的内存dc与位图相关联
SelectObject(hMemDC, hBitMap);
BOOL bRet = BitBlt(
hMemDC,//拷贝到内存dc中
0,//拷贝到内存dc中哪里,左上角x坐标
0,//拷贝到内存dc中哪里,左上角y坐标
nWidth,//拷贝多宽
nHeight,//拷贝多高
hDeskDC,//从哪拷贝,源头dc
0,//拷贝源头dc的哪里,左上角x坐标
0,//拷贝源头dc的哪里,左上角y坐标
SRCCOPY//按字节拷贝模式
);
if (!bRet)
{
throw "error";
}
//这里额外前8位用于存取屏幕的宽和高,以便服务端接收
int nBufSize = nWidth*nHeight * 4 + 8;//位图的所需字节空间
pBitMapBuf = new char[nBufSize];//开辟那么大的内存空间用于存位图数据
if (pBitMapBuf == NULL)
{
throw "error";
}
//将图像的前8个字节的位置用于存放我们的长宽
tagScreenData* pScreenData = (tagScreenData*)pBitMapBuf;
pScreenData->nWidth = nWidth;
pScreenData->nHeight = nHeight;


//从内存DC中获取位图数据
LONG nBitSize = GetBitmapBits(hBitMap,//位图
nBufSize - 8,//取多少
pBitMapBuf + 8);//取出来的数据
if (nBitSize == 0)
{
throw "error";
}

//到这里表示拿到了数据,把位图数据发送到我们的服务端

CDC memDC;//创建一个内存dc
CBitmap bitMap;//创建一个兼容位图
memDC.CreateCompatibleDC(GetDC());
bitMap.CreateCompatibleBitmap(GetDC(), pScreenData->nWidth, pScreenData->nHeight);
memDC.SelectObject(bitMap);
//将获取到的数据直接写入到内存dc的bitmap中
bitMap.SetBitmapBits(nBufSize - 8, pBitMapBuf + 8);
//数据已经到了内存dc中,接下来将内存dc中的数据拷贝到屏幕dc
GetDC()->BitBlt(0, 0, pScreenData->nWidth, pScreenData->nHeight, &memDC, 0, 0, SRCCOPY);

}
catch (...)
{
bResult = FALSE;
}
//很重要,一定要写,用于释放资源,防止内存泄露,不处理相当于不断申请内存不释放,电脑越来越卡
if (hDeskDC != NULL)
{
::ReleaseDC(hDesktopWnd, hDeskDC);
}
if (hMemDC != NULL)
{
::DeleteDC(hMemDC);
}
if (hBitMap != NULL)
{
::DeleteObject(hBitMap);
}
if (pBitMapBuf != NULL)
{
delete[] pBitMapBuf;
}
return;

断线重连(心跳包)

p.s.利用网络封包分析工具Wireshark来分析

tcp可靠的前提是tcp不断,在十分恶劣的情况下tcp也可能断开连接

1.找到一种方法能够判断何时掉线

从TCP协议角度来看,一个已建立的TCP连接有两种关闭方式,一种是正常关闭,即四次挥手关闭连接;还有一种则是异常关闭,我们通常称之为连接重置(RESET)。

recv的返回值,成功收到的时候会返回收到的字节数,如果tcp优雅地四次挥手断开的话,recv会返回0,如果意外断开,recv返回SOCKET_ERROR( 即-1),并且可以通过调用WSAGetLastError获得一个错误码

系统发现软件被强制关闭了但tcp还在连接中的时候,只要TCP栈的读缓冲里还有未读取(read)数据,则调用close时会直接向对端默认发一个RST包,告知对方表示意外断开。

p.s.优雅地四次挥手断开是通过调用closesocket(s);这个api来实现,此时双端的recv都会收到0

除了以上的两种情况,还有一种情况是网线断了,这种情况下recv不会返回任何结果。

针对网线断了或者网络堵车等情况的处理方式就是心跳包超时机制,超时就干掉,节省资源,即周期性检测对方是否在线?(询问对方是否在线)

p.s.tcp本身其实是有超时检测的,但是它本身的超时时间非常长,如果开启tcp自带的心跳检测是半个小时一次,时间太长了

心跳包,只要一边发一边收就可以了,但双方都可以做超时处理,由客户端来发心跳包比较好,因为服务端要发的话还要遍历发,压力很大,服务端每收到一个心跳包回一个心跳包,并且检测客户端最后心跳包时间看是否超时,若超时,调用closesocket(s)强行关闭socket,这时候,堵塞的recv会不阻塞了,返回-1


理论解读:

首先说一下正常关闭时四次挥手的状态变迁,关闭连接的主动方状态变迁是FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT,而关闭连接的被对方的状态变迁是CLOSE_WAIT->LAST_ACK->TIME_WAIT。在四次挥手过程中ACK包都是协议栈自动完成的,而FIN包则必须由应用层通过closesocket或shutdown主动发送,通常连接正常关闭后,recv会得到返回值0,send会得到错误码10058。

​ 除此之外,在我们的日常应用中,连接异常关闭的情况也很多。比如应用程序被强行关闭、本地网络突然中断(禁用网卡、网线拔出)、程序处理不当等都会导致连接重置,连接重置时将会产生RST包,同时网络络缓冲区中未接收(发送)的数据都将丢失。连接重置后,本方send或recv会得到错误码10053(closesocket时是10038),对方recv会得到错误码10054,send则得到错误码10053(closesocket时是10054)。

​ 操作系统为我们提供了两个函数来关闭一个TCP连接,分别是closesocket和shutdown。通常情况下,closesocket会向对方发送一个FIN包,但是也有例外。比如有一个工作线程正在调用recv接收数据,此时外部调用closesocket,会导致连接重置,同时向对方发送一个RST包,这个RST包是由本方主动产生的。

​ shutdown可以用来关闭指定方向的连接,该函数接收两个参数,一个是套接字,另一个是关闭的方向,可用值为SD_SEND,SD_RECEIVE和SD_BOTH。方向取值为SD_SEND时,无论socket处于什么状态(recv阻塞,或空闲状态),都会向对方发送一个FIN包,注意这点与closesocket的区别。此时本方进入FIN_WAIT_2状态,对方进入CLOSE_WAIT状态,本方依然可以调用recv接收数据;方向取值为SD_RECEIVE时,双发连接状态没有改变,依然处于ESTABLISHED状态,本方依然可以send数据,但是,如果对方再调用send方法,连接会被立即重置,同时向对方发送一个RST包,这个RST包是被动产生的,这点注意与closesocket的区别。


p.s.检测网络环境也可以

通过这个选项可以设定是动态链接,还是静态链接

这里写图片描述

MT选项:链接LIB版的C和C++运行库。在链接时就会在将C和C++运行时库集成到程序中成为程序中的代码,程序体积会变大。
MTd选项:LIB的调试版。
MD选项:使用DLL版的C和C++运行库,这样在程序运行时会动态的加载对应的DLL,程序体积会减小,缺点是在系统没有对应DLL时程序无法运行。
MDd选项:表示使用DLL的调试版。

心跳包实现:

发包需要将发送的数据放到队列中,然后由一个单独的线程读取队列发送

p.s.事件相关api:

1
2
3
4
5
6
7
//用事件来处理当前的断线状态
HANDLE hEvent = INVALID_HANDLE_VALUE;
CreateEvent(NULL,//安全属性不用给
FALSE,//表示自动处理事件的状态,有信号也只会让通过一次就变回无信号,如果为TRUE的话需要手动ReSetEvent()函数来恢复无信号
FALSE,//表示初始时没有信号的状态
NULL//名字不用给
);
1
2
//给我们的事件一个状态,表示有信号了
SetEvent(hEvent);
1
2
//表示等待事件信号的来临,INFINITE表示等待无限时间,没有SetEvent就是没有信号就会类似于死循环阻塞在此
WaitForSingleObject(hEvent,INFINITE);

P.S.一关就崩溃,肯定是线程循环没退出,线程资源没释放

BUG修复以及待改进点:

1.崩溃问题要解决,尾部有数据才新建,尾部没数据的话,不新建,却尝试释放,因此崩溃,解决方案是每次清理数据的时候,将开辟堆空间的指针置空,然后判断指针不为空才施放(重点,开辟空间和释放的时候都应该养成习惯套这个指针置空以及判断指针不为空才释放防止出问题)

2.图片压缩一下,省空间。压缩算法有很多,zlib文件压缩,lz4字符串压缩

3.屏幕划分为小模块,哪个模块动了就传输哪个模块,不动的保持不变,节省带宽

4.防止发包冲突,应该专门有一个线程进行发包,一个队列作为发包缓冲

5.频繁的开辟和释放空间效率不好而且不稳定,应该开辟一个足够大的空间作为缓冲空间来使用,不频繁开辟释放空间

p.s.一个好的程序一定是让线程自己退出的,比如说可以设置个标志位

进程/DLL查看器TestProcess

相关api:

image-20200903150113297

P.S.vs没有函数提示,说明头文件没有include进来

查看进程模块信息代码如下:

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
//创建快照的函数,第一个参数选择可用于看模块,线程,进程,堆等,第二个参数当第一个参数选择的不是查看进程的时候,第二个参数表示进程id。( 不支持A版本函数)
#include <Tlhelp32.h>
HANDLE hSnap=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
PROCESSENTRY32 pe32={0};
pe32.dwSize=sizeof(PROCESSENTRY32);
//首先查看第一个进程
BOOL bRet=Process32Fitst(hSnap,&pe32);
/*wprintf(pe32.szExeFile);
wprintf(L"\r\n");
if(wcscmp(pe32.szExeFile,L"procexp.exe")==0)
{
dwPID=pe32.th32ProcessID;
break;
}*/
while(bRet){
//开始查看后面的进程
bRet=Process32Next(hSnap,&pe32);
//wprintf(pe32.szExeFile);
//wprintf(L"\r\n")
if(wcscmp(pe32.szExeFile,L"procexp.exe")==0){
dwPID=pe32.th32ProcessID;
break;
}
}
//遍历模块信息
hSnap=CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwPID);
MODULEENTRY32 me32={0};
ME32.dwSize=sizeof(MODULEENTRY32);
//首先查看第一个模块
BOOL bRet=Module32Fitst(hSnap,&me32);
wprintf(me32.szExeFile);
wprintf(L"\r\n");
while(bRet){
//开始查看后面的模块
//me32中hModule:拥有过程中模块的句柄 szModule:模块名称 szExePath:模块路径。
bRet=Module32Next(hSnap,&me32);
std::wstring wstr=me32.szExePath;
if(wstr.find(L"KERNEL32")!=std::wstring::npos)
{
HMODULE hDestModule=me32.modBaseAddr;//找到模块地址
}
wprintf(me32.szExeFile);
wprintf(L"\r\n");
}

p.s.以下是一个监测读取文件操作的监视器

image-20200903150831035

//能看到进程和进程的父子关系

image-20200903151002842

结束进程的api:

1
2
3
4
//打开某一个进程,获取对应的进程句柄
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPID);
//强制结束进程(关闭正常程序可以,但关闭进0环的恶意程序用什么都关不掉)
TerminateProcess(hProcess,0);

注入DLL

动态库的显式调用我们的dll中的函数:

WinHex软件可以直接复制c语言格式,右键选择edit后继续

image-20200903184510234

粘贴出来的效果,这就是代码的二进制形式

image-20200903184738008

这些代码现在以shellcode形式存在程序的全局区

两种办法解决:

用这个软件修改节

image-20200903185802722

image-20200903185922805

手动调成可执行,意思就是要求代码所在的内存属性是可读可执行的。

除了手动调节之外,还有办法是在当前程序申请一块可读可写可执行的内存区域存代码,api如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//申请内存,返回值为申请到的地址
LPVOID lpAddr=VirtualAlloc(NULL,//表示申请内存在任意地址即可,随机分配内存
1,//内存通常是以分页为单位来给空间的 1页=4k 4096
MEM_COMMIT,//告诉操作系统给分配一块内存 MEM_RESERVE告诉操作系统预定一块空间
PAGE_EXECUTE_READWRITE);//申请的内存的权限属性
if(lpAddr==NULL){
printf("Alloc error!");
}
//到这里表示能够成功的分配内存(系统帮你申请会同时自动帮你把内存置零),将shellcode拷贝到目标内存(data是shellcode的char数组名)
memcpy(lpAddr,data,sizeof(data));
//自己定义的函数指针类型PFN_FOO
typedef void (*PFN_FOO)();//定义函数指针类型
PFN_FOO f=(PFN_FOO)(void*)lpAddr;
f();
//释放内存空间
VirtualFree(lpAddr,
1,//释放多大的页
MEM_DECOMMIT);

理解:

c语言–>编译成obj文件(二进制),多个obj链接成–>exe(可执行文件)

为什么c语言要编译,因为cpu只认识指令集( 机器码)(e.g.intel阵营的8086指令集,ARM阵营的RISC指令集)

至于汇编,只是一种帮助人直观认识的助记符

程序的执行通常会对当前程序有依赖,想要二进制代码有效,必须处理相关的依赖,比如相关的函数地址,字符串地址,自己处理了即进行了重定位。
$$
shellcode:一段与地址无关的代码
$$
把shellcode注入到目标进程里,首先要给目标进程开辟空间

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
//VirtualAlloc只能给自己申请,而VirtualAllocEx可以给别人开辟空间
//通过进程id获取进程句柄
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPID);
//申请内存,返回值为申请到的地址
LPVOID lpAddr=VirtualAllocEx(
hProcess,//目标进程句柄
NULL,//表示申请内存在任意地址即可,随机分配内存
1,//内存通常是以分页为单位来给空间的 1页=4k 4096
MEM_COMMIT,//告诉操作系统给分配一块内存 MEM_RESERVE告诉操作系统预定一块空间
PAGE_EXECUTE_READWRITE);//申请的内存的权限属性


//给任何一个进程写内存
DWORD dwWritedBytes=0;//用于接收下面api成功写入的字节数
bool bRet=WriteProcessMemory(hProcess,//目标进程
lpAddr,//目标地址 在目标进程中
data,//源数据 在当前基础中
sizeof(data),//写多大内容
&dwWritedBytes);//成功写入的字节数

if(!bRet)
{
//释放内存空间
VirtualFreeEx(hProcess,//目标进程句柄
lpAddr,
1,//释放多大的页
MEM_DECOMMIT);
return 0;
}
//向目标程序调用一个线程,创建远程线程api
HANDLE hRemoteThread=CreateRemoteThread(hProcess,//目标进程
NULL,
0,
(LPTHREAD_START_ROUTINE)lpAddr,//目标进程的回调函数
0,
NULL,
0);






//释放内存空间(这里不能马上释放,因为马上释放的话,线程可能还没执行完就释放掉了)
//VirtualFreeEx(hProcess,lpAddr,1,MEM_DECOMMIT);




远程注入代码并执行 的伪代码:

1.向目标进程中申请内存

2.向目标进程内存中写入shellcode(没有特征,但编码比较麻烦)

3.创建远程线程执行shellcode

P.S.让被注入的代码更加容易编写,最好的方式就是使用DLL来编写

能不能直接放入dll,能,但我们就需要自己加载DLL

DLL的加载的两种方式:
  1. 静态调用:通过在我们的程序中添加头文件,以及lib文件来完成调用(键盘钩子)
  2. 动态调用:仅仅只需要一个dll即可完成

注意:

1
2
3
4
//除了前面介绍过的def文件导出函数的方式,以下展示另一种函数导出方式
__declspec(dllexport) void Test(){
MessageBox(NULL,NULL,NULL,NULL);
}

这样也能导出函数,但这种方式会对Test进行名称粉碎,名称粉碎:如下图的名称多余部分是由C++编译器添加的

image-20200904135213693

因此为了让名称合理,我们需要告诉编译器,使用c语言的方式来命名函数,正确方式如下:

1
2
3
4
5
6
7
//不写调用约定,那么就导出的名称为Test,写调用约定的话,导出名称规则下面有写
extern "C"{
__declspec(dllexport) void Test()
{
MessageBox(NULL,NULL,NULL,NULL);
}
}

效果如下:

image-20200904140334505

一般最好把调用约定也指定上

常见的调用约定
  1. __stdcall 标准调用约定 栈传参,函数内部(被调用者)平栈,大部分的Windows api都是这种,也有少部分不是,比如wsprintf不是,因为该函数的参数数量不确定,函数内部并不知道传进的参数是几个,所以也就不能函数内部平栈

  2. __cdecl c调用约定 栈传参,函数外部(调用者)平栈 C/C++默认的函数调用协议。

  3. __fastcall 快速调用约定 前两个参数由寄存器ecx和edx来传递,其余参数还是通过堆栈传递(从右到左)。函数内部(被调用者)平栈。适用于对性能要求较高的场合。

  4. __thiscall 类的调用约定 栈传递 使用ecx寄存器来传递this指针

    p.s.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    1. 1. C语言编译器函数名称修饰规则
    2. 1. __stdcall:编译后,函数名被修饰为“_functionname@number”。
    2. __cdecl:编译后,函数名被修饰为“_functionname”。
    3. __fastcall:编译后,函数名给修饰为“@functionname@nmuber”。
    4. 注:“functionname”为函数名,“number”为参数字节数。
    5. 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
    3. C++语言编译器函数名称修饰规则
    4. 1. __stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
    2. __cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。
    3. __fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。
    4. 注:“******”为函数返回值类型和参数类型表。
    5. 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。
    6. C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。
动态库的显式调用中的函数的步骤

动态加载dll的意思是不需要lib和头文件,仅仅需要一个dll

  1. 将目标dll加载到我们的进程中 dll在linux和安卓里叫so文件

    1
    2
    3
    4
    HMODULE hDll=LoadLibraryA("C:/xxx/xxx/xxx.dll");
    //返回值是模块句柄,也就是当前的dll在当前进程中的首地址
    //加载的过程由我们的操作系统来完成的(包括各节的扩展分配内存,重定位等等)
    //这个函数也可以自己写替代LoadLibraryA,自己写的我们叫内存加载,这种写法是病毒常用的写法
  2. 计算函数的位置(如下图函数在dll中的偏移)

    image-20200904145357478

    1
    2
    3
    typedef void (*PFN_FOO)();//定义函数指针类型
    //计算函数的位置,计算出dll的对应导出函数的地址在哪里,即位置,转成函数指针即可直接调用
    PFN_FOO lpFoo=(PFN_FOO)GetProcAddress(hDll,"Test");
  3. 调用

    1
    lpFoo();

p.s. c++的原始字符串写法:R”(\t\r\n)”

忽略掉\本身的转义效果,括号内的所有东西视为原始字符串

理解:(隐式链接和显示链接的区别)

隐式链接指将DLL的函数符号输出库LIB链接,在执行文件中IMPORT段加入一系列函数的入口点!程序在加载启动时自动加载这些DLL,并查找函数入口点!像普通的SDK程序要加入KERNEL32。LIB链接就是!这样的方法是当使用DLL多时,程序启动很慢、! 动态链接指显式加载DLL,利用LoadLibrary,GetProcAddress取得函数入口点,执行再释放,这种方法是程序简洁,快速!但是不利于输出太多函数的DLL使用!代码量太大!

进一步理解:

调用方式说完了,但最重要的问题依然是要想办法把dll注入进去

远线程注入

我们的目标程序只要有kernel32.dll,就有LoadLibrary函数

ntdll.dll和kernel32.dll是程序里最基本的dll

原理:创建远线程的函数的线程函数正好和LoadLibrary是同样的函数原型

步骤:

  1. 先去找目标进程中的kernel32.dll的位置(遍历找到该模块句柄)
  2. 在该dll中找到LoadLibrary的地址(GetProcAddress)
  3. 创建远线程给目标进程创建一个远线程,跑上一步找到的LoadLibrary的地址
  4. 创建远线程传的参数,即dll路径必须在目标进程内,因此,远程开辟空间,将路径远程内存拷贝过去目标进程的内存中,使目标进程中我们创建的远线程中可以正常的参数调用LoadLibrary将我们的dll导入进去

疑问:通过GetProcAddress获取到的LoadLibrary地址在每个进程都一样吗?

kernel32.dll是第一个加载进进程的,因此地址固定


目录中,进程/DLL查看器那里有代码可获得目标进程的kernel32的基地址

但是无法调用GetProcAddress获取LoadLibrary的地址,因为GetProcAddress我们只能获取到本地进程的LoadLibrary的地址。

由于大家都是公用同一个kernel32.dll,所以LoadLibrary的函数偏移是不变的。
$$
目标进程的LoadLibrary地址-目标进程kernel32的基地址==当前进程的LoadLibrary地址-当前进程kernel32的基地址
$$
所以我们首先计算出当前程序中的kernel32的LoadLibrary的偏移,然后加上目标。

步骤如下:

  1. 首先获取本进程的kernel32地址

    1
    HMODULE hKernel32= GetModuleHandleA("kernel32.dll");
  2. 计算函数的位置,计算出dll的对应函数的地址在哪里,即位置(这里LoadLibrary用A或则W版,则写内存的时候要写对应版本的路径)

    1
    PFN_FOO lpLoadLibrary=(PFN_FOO)GetProcAddress(hKernel32,"LoadLibraryA");
  3. 计算LoadLibrary在kernel32.dll中的偏移地址(hDestModule是在进程查看器中得到的目标进程中的kernel32.dll的地址),以偏移地址加上hDestModule就得到了我们要的目标进程中的LoadLibrary地址

1
LPVOID lpDestAddr=(char*)lpLoadLibrary-(char*)hKernel32+(char*)hDestModule;

红圈表示得到的本进程的两个地址(本进程中的kernel32的模块地址和LoadLibrary地址 ),会发现,kernel32的dll在本进程和目标进程中地址是一样的

image-20200904200111380

p.s.kernel32.dll和ntdll.dll加载时间比较早,因此他们的位置相对比较固定

接下来就是CreateRemoteThread,线程函数是目标进程中LoadLibrary中的地址;线程函数的参数,即dll的路径要写目标进程中的字符串,因此要开辟目标进程中的内存地址,然后用WriteProcessMemory给目标进程该地址写入dll的路径,然后该申请的地址为线程函数的参数。

注意点:DLL自带的switch部分中,不要写同步相关的代码,因为它本身自带同步,否则加载dll会卡死

注入成功后,目标进程中的我们注入的模块地址可以通过线程的退出码来查看,因为我们把LoadLibrary当做线程函数来处理,因此线程返回的时候的退出码就是LoadLibrary的返回值。

1
2
3
DWORD dwRetCode;
//获取远程线程的退出值存进dwRetCode,即LoadLibrary的返回值,即目标进程中注入dll的地址
GetExitCodeThread(hRemoteThread,&dwRetCode);

接下来,我们只需要用偏移值就可以把dll作为一个导出函数的库,用CreateRemoteThread调用dll中的任何和LoadLibrary相同类型的函数

远线程注入,虽然很老了,但是英雄联盟可以成功注入,但是有失败概率,因此写个循环反复注入,总能成功


p.s.

vs错误讲解:(如下图)

image-20200906163427470

异常:0xC0000005 读写内存发生错误,一般是某个堆或者栈不存在,你强行去读取或者写入他,因此触发异常,要留意定位到的行的指针。

C++要求我们自己去管理资源(内存,文件,临界区,互斥体等等需要自己申请和释放的东西)

解决方案

  1. 装死,装自己看不见这个异常,即通过异常处理来解决
1
2
3
4
5
6
7
try{
//可能出错的代码段
}
catch(...)//三个点表示所有异常
{
//接收到异常后的处理
}

1

照上图设置后才能真正靠…接住所有异常,包含我们的C++异常,也包含windows自带的SEH异常

  1. 修改代码,把bug修复好

  2. 我们可以使用智能指针来帮助我们释放资源,将指针放入到智能指针的对象中,然后离开作用域时会自动释放

    1
    2
    3
    4
    //C++11标准
    std::shared_ptr<int> pInt=new int(3);
    //std::shared_ptr<int> pInt(new int);
    //使用智能指针就像普通的指针一样,只是只管申请,不管释放

    需要学习:C++的新标准语法。