进程与线程的通信

进程与线程的通信

进程和线程的区别和联系

进程的联系和区别

举例理解进程和线程

进程

进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位,通俗来说:进程就是程序的一个执行过程

进程主要有三个部分组成

  1. 进程控制块PCB,包含了进程描述信息、进程控制和管理信息、资源分配清单、CPU相关信息

  2. 数据段,即程序运行过程中的各种数据(比如程序中定义的变量)

  3. 程序段:就是程序的代码

线程

单个CPU一次只能运行一个任务,而进程就代表CPU所能处理的单个任务,而一个进程可以包括多个线程。线程是独立调度的基本单位。 CPU有单核和多核区别,单核CPU其实就是多个线程会轮流得到那一个CPU核心的支持;在多核CPU中,一个核心可以服务于一个线程,例如我的电脑是4核的话,有四个线程A、B、C、D需要处理,那CPU会将他们分配到核心1、2、3、4,如果还有其他更多的线程,也必须要等待CPU的切换执行。

线程的通信和同步方式

共享进程空间

一个进程的内存空间是共享的,每个线程都可以使用这些共享内存

共享内存

当某个线程使用一些共享空间时,其他线程必须等待它试用结束才能继续使用这一块内存

信号量

用于保证多个线程不会互相冲突,互斥体就是其中的一种体现,通过CreateMutex和ReleaseMutex告知下一个线程此时内存可以访问,BUU-Youngter-drive

互斥锁

防止多个线程同时读写一块内存区域,而互斥锁就是信号量的一种特殊情况

进程间通信

由于资源分配时将资源分配给了不同的进程,这样进程的资源是独立的,进程之间就无法共享数据和进行互相访问,如果我们需要在不同进程之间实现信息交互和状态传递,此时就需要进程间的通信

创建共享空间

细节

CreateFileMapping

MapViewOfFile

OpenFileMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//创建内存地址,这些函数指定的权限标志和CreateFileMapping中的权限标志不一致,则会执行失败。
char init[] = "Helloworld!";
char final[30] = { 0 };
//第一个是需要创建映射的文件句柄,0xFFFF表示创建空间
HANDLE hobject = CreateFileMapping(0, 0, PAGE_READWRITE, 0, 0x1000u, (LPCWSTR)"FLAG");
LPVOID address = MapViewOfFile(hobject, 0xF001F, 0, 0, 0x1000u);
memcpy((void*)address, init, strlen(init));
//第一个参数是文件映射对对象的句柄,第二个为属性为访问权限
/*HANDLE hobject1 = CreateFileMappingA(0, 0, PAGE_READWRITE, 0, 0x1000u, "FLAG");
LPVOID address1 = MapViewOfFile(hobject, 0xF001F, 0, 0, 0x1000u);*/
HANDLE Myshare = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, (LPCWSTR)"FLAG");
LPVOID address1 = MapViewOfFile(hobject, 0xF001F, 0, 0, 0x1000u);
memcpy(final, address1,strlen(init));
printf("%s", final);
UnmapViewOfFile(Myshare);
CloseHandle(address1);
UnmapViewOfFile((LPVOID)address);
CloseHandle(hobject);

多线程

创建线程

首先来看创建线程的函数

1
2
3
4
5
6
7
8
9
//CreateThread
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//安全属性,通常设置为NULL
SIZE_T dwStackSize,//参数用于设定参数可以将多少地址空间用于他自己的堆栈,每个线程拥有自己的堆栈
LPTHREAD_START_ROUTINE lpStartAddress,//参数用于指明想要新线程执行的线程函数的地址
LPVOID lpParameter,//线程函数的参数,在线程启动执行时将该参数传递给线程函数,既可以是数字,也可以是指向包含其他信息的一个数据结构的指针
DWORD dwCreationFlags,//0表示创建完毕后立即调度,CREATE_SUSPENDED表示创建之后挂起
LPDWORD lpThreadId//线程ID
);//返回值,线程句柄

线程函数的格式是统一的

1
DWORD WINAPI 函数名(LPVOID 线程参数)

线程函数放类中需要加static修饰,或者直接放类外

下面是创建线程的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<Windows.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>


DWORD WINAPI Thread(LPVOID lparam) {
printf("This is A Thread Test\n");
return 0;
}

int main()
{
printf("this is main\n");
HANDLE hObject = CreateThread(0, 0, Thread, 0, 0, 0);
CloseHandle(hObject);

return 0;
}

我们在运行时会发现一个问题,多次运行的结果并不相同

这是因为线程原则上是并行执行的,而不是顺序执行的,操作系统来决定线程的执行顺序——CPU调度,所以可能是主线程先结束,也可能是其他线程先结束

CloseHandle

同时需要注意Closehandle这个函数,因为他的作用不是用于关闭线程,而是释放线程句柄,表示不再对这个线程进行操作,线程句柄是一个内核对象,我们可以通过线程句柄来操作线程,但是线程的生命周期和线程句柄的生命周期不一样的。线程的生命周期就是线程函数从开始执行到return,线程句柄的生命周期是从CreateThread返回到你CloseHandle()

当我们不再使用句柄时一定要记得释放,因为句柄属于系统资源,使用完就得还回去

多线程执行

通过上面的例子我们可以知道多个线程的执行顺序是由操作系统决定的,但是有时候我们需要明确线程的执行先后顺序,这时候我们就需要对线程进行控制

WaitForSingleObject()

使用WaitForSingleObject()来控制

通过传入线程的句柄和等待的时间(参数中以毫秒为单位)来控制,INFINITE表示无限(0xFFFFFFFF),即当线程执行完之后再执行其他线程

1
WaitForSingleObject(hObject, INFINITE);

而没有WaitForSingleObject是这样的

互斥对象

互斥对象创建

多线程环境

互斥对象是系统内核维护的一种数据结构,它保证了对象对单个线程的访问权

互斥对象的结构

  1. 一个使用数量:指有多少个线程在调用该对象
  2. 一个线程ID:指互斥对象维护的线程的ID
  3. 一个计数器:表示当前线程调用该对象多少次
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
#include<stdio.h>
#include<Windows.h>
HANDLE hObject;
int k = 10;
DWORD WINAPI Fun1(LPVOID data)
{
while (1)
{
WaitForSingleObject(hObject, INFINITE);
if (k-- > 0)
{
printf("第%d次调用Thread1\n",k);
}
else {
break;
}
ReleaseMutex(hObject);
}
return 0;
}

DWORD WINAPI Fun2(LPVOID data)
{
while (1)
{
WaitForSingleObject(hObject, INFINITE);
if (k-- > 0)
{
printf("第%d次调用Thread2\n",k);
}
else {
break;
}
ReleaseMutex(hObject);
}
return 0;
}

int main()
{
::hObject = CreateMutex(0, 0, 0);//表示对全局变量进行赋值
HANDLE hObject1 = CreateThread(0, 0, Fun1, 0, 0, 0);
HANDLE hObject2 = CreateThread(0, 0, Fun2, 0, 0, 0);
CloseHandle(hObject1);
CloseHandle(hObject2);
Sleep(0x1000);
return 0;
}

可以看到此时已经是两个线程交替执行了,但是谁先执行还是不确定的

下面对几个关键的函数进行说明

1
2
3
4
5
HANDLE CreateMutexx(//用于创建或打开一个已经命名或者匿名的互斥对象
LPSECURITY_ATTRIBUTES lpMutexAttributes,//一般设置为NULL,表示对互斥对象使用默认的安全性
BOOL bInitialOwner,//True表示创建者对象的线程获得该对象的使用权,内部计数器+1,否则该线程不获得互斥对象的所有权
LPCSTR lpName//如果此参数为NULL,表示创建一个匿名的互斥对象
);//返回互斥对象的句柄
1
2
3
4
5
BOOL WINAPI ReleaseMutex(
//当我们对共享资源访问结束后,我们需要释放该对象的所有权,让这个互斥对象处于已通知状态,此时调用ReleaseMutex相当于互斥对象的计数器-1
//当线程调用该函数,表示当前线程使用完毕该资源,其他线程有机会获得对象的所有权,进而访问共享资源
HANDLE hMutex;//需要释放的互斥对象的句柄
)
1
2
3
4
DWORD WaitForSingleObject(
HANDLE hHandle,//所请求互斥对象的句柄,一旦互斥对象处于有信号状态,该函数就返回(表示未被使用中),如果互斥对象一直处于无状态对象(表示在被其他线程使用),该线程暂停执行
DWORD dwMilliseconds//指定等待的时间间隔,以毫秒为单位,INFINITE表示一直等待直到共享资源处于未被使用状态
);

双进程保护

双进程保护:实际上是程序本身作为以恶搞进程或一个调试器,并且在调试模式下运行自身程序

这种程序的技术特点是

  1. 无法被调试,因为程序本身也是一个调试器,我们直到一般情况下一个程序只能被一个调试器所调试,如果他的程序先抢占作为了调试器,那么我们就没办法进行调试,所以解决办法只能是在他的调试器附加之前你先开始调试
  2. 一般来说,为了防止你抢占调试器,程序中会添加一个异常处理函数,并且在程序中加入使程序异常的代码,然后程序本身作为调试器对异常进行处理,而我们作为调试者,在调试过程中无法处理异常的代码

创建子进程

在Windows和Linux下的函数不一样,父进程创建的子进程,如果父进程关闭,子进程不一定关闭

Windows下

直接通过当前文件和CreateProcess即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//CreateProcess说明
BOOL CreateProcess
(
LPCTSTR lpApplicationName,//要创建进程的文件路径,当其为NULL时,可执行模块的名字必须处于lpCommandLine最前面并且由空格符与后面的字符分开
LPTSTR lpCommandLine, //传入给新进程的命令行字符串
//前两个参数可以搭配使用也可以单独使用
LPSECURITY_ATTRIBUTES lpProcessAttributes,//决定句柄是否被子进程继承
LPSECURITY_ATTRIBUTES lpThreadAttributes, //决定线程是否被继承
BOOL bInheritHandles, //指示新进程是否从调用进程继承了句柄,如果为真,调用进程中的每一个可继承的打开句柄都将被子进程继承
DWORD dwCreationFlags, //指定附加的、用来控制优先类的进程的创建的表示,他有多个值并且对应不同的含义
LPVOID lpEnvironment,//指向一个新进程的环境块,如果参数为空,新进程使用调用进程的环境
LPCTSTR lpCurrentDirectory,//指定子进程的工作路径
LPSTARTUPINFO lpStartupInfo,//指向一个用于决定新进程主窗体如何实现的STARTINFO结构体,我们只需要给这个结构体的第一个成员(cb)赋值为其结构体大小
LPPROCESS_INFORMATION lpProcessInformation//指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体,该结构返回有关新进程及其主线程的信息
/*
HANDLE hPRocess;//进程句柄
HANDLE hThread;//主线程句柄
DWORD dwProcessId;//进程ID
DWORD dwThreadId;//线程ID
只要创建成功系统就会给这个结构体赋值
*/
);

dwCreationFlags对应的参数,而我们要实现双进程保护需要使用到DEBUG_PROCESS,这样调用进程将被作为调试器来调试新进程,并且把调试程序的所有调试时间通知给调试器,可以使用WaitForDebugEvent函数来接收

dwCreationFlags常量值

dwCreation还可以用来控制新进程的优先类

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
#define  _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
#include<string.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{

if (argc == 2)//根据命令行参数的个数来区分主进程和子进程
{
__asm int 3;//断点
MessageBoxA(0, (LPCSTR)"这是一个简单的例子", (LPCSTR)"TraceMe", 0);
}
else
{
struct _STARTUPINFOW StartupInfo;
struct _PROCESS_INFORMATION ProcessInformation;
GetStartupInfoW(&StartupInfo);
//printf("%s\n", argv[0]);
TCHAR CommandLine[] = TEXT("D:\\new\\wmctf2022\\Test_RAAA\\Release\\Test_RAAA.exe DIO");
printf("Father\n");
BOOL test = CreateProcess(NULL, CommandLine, 0, 0, 0, DEBUG_ONLY_THIS_PROCESS|DEBUG_PROCESS, 0, 0, &StartupInfo, &ProcessInformation);
printf("%d\n", test);
struct _DEBUG_EVENT DebugEvent;
while (1)
{
WaitForDebugEvent(&DebugEvent, INFINITE);//等待调试事件
if (DebugEvent.dwDebugEventCode == 5)
{
break;//表示退出调试事件
}
if (DebugEvent.dwDebugEventCode == 1)
{
int test[23];
CONTEXT Context;
//报告异常处理事件
memcpy(test, &DebugEvent.u, 0x54);
if (test[0] == EXCEPTION_BREAKPOINT)//如果触发的是断点异常
{
HANDLE hThread=NULL;
//获取触发异常的上下文数据,然后进行修改后赋值回去
memset(&Context, 0, sizeof(Context));
Context.ContextFlags = 0x10007;
GetThreadContext(hThread, &Context);
Context.Eip += 1;//获取上下文信息之后处理异常
SetThreadContext(hThread, &Context);
}
}
ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, 0x10002);//继续执行子进程
}
}
return 0;
}

DebugEvent.Code

Linux下

使用fork函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
pid_t pid;
pid=fork();
if(pid<0)
{
printf("创建子进程失败\n");
exit(0);
}
else if(pid==0)
{
printf("进入子进程,进程ID为%d\n",getpid());
}
else{
printf("这是父进程,进程ID为%d\n",getpid());
}
return 0;
}

子进程创建之后我们可以使用ptrace进行跟踪,**Ptrace 可以让父进程控制子进程运行,并可以检查和改变子进程的核心image的功能 **

Linux ptrace

Linux Ptrace

QWB-easyre

考点

PTRACE双进程保护、SMC

题目分析

首先使用Finger恢复符号

这里ptrace的值原本是0,而ptrace的第一个参数是个共用体,我们可以将鼠标置于第一个参数上,然后按M导入enum,搜索对应的共用体即可

程序首先创建re3文件,将内嵌的ELF文件数据写入文件中,然后fork一个子进程,此时若是父进程fork,会返回子进程的PID,进入if语句,子进程fork返回值为0,进入else语句,接着让生成的子进程执行PTRACE_TRACEME,然后启动新的程序re3文件,此时re3作为新的进程替换子进程,(这一过程就相当于Windows中的双击程序之后先创建explorer进程,然后通过explorer进程运行我们想打开的程序,然后explorer进程就相当于结束了,而我们的程序变成了新的进程)那么re3仍然接受父进程的调试并且继承原来进程的命令行参数

execve函数

子进程

首先进行命令行参数判断,由于使用execve函数创建进程时继承了原本进程的命令行参数,而原本进程是父进程的子进程,继承了父进程的命令行参数,所以待会调试的时候需要加上命令行参数

然后在另一函数处发现了一处SMC,int 3断点会使得子进程通知,并且通知父进程调试,由父进程处理

恢复数据之后的就是数织游戏,并且在init处对数据进行了修改,并且使用了setjmp和longjmp代替了循环

setjmp和longjmp

父进程
第一次循环

waitpid函数用于停止父进程,等待子进程执行

由于原先创建的子进程被替换了,所以信号量会被发送到父进程中,接受到之后使用PTRACE_CONT使子进程继续执行

接着通过prop/{pid}/maps获取子进程程序的开始地址,再从内存中read数据,这里的数据很特殊需要注意一下,第一个存储的是长度

从内存中read的数据

然后继续waitpid,之后子进程运行到第一处int 3中断,返回父进程处理,首先使用PTRACE_GETREGS获取子进程中寄存器的值存储到REG结构体中(这里我没创建结构体(绝对不是因为我们找到REG的结构体成员),所以看起来很怪,因为看不出来哪里给Rip_addr进行了赋值,但实际上这里就是RIP)

紧接着PTRACE_PEEKTEXT从目标地址中返回数据,从接受到的数据我们可以知道返回的数据类型为QWORD,而这个数据正好是子进程int 3断点位置处的数据

返回的数据

子进程int 3处的数据

接下里进入SMC函数,首先获取长度0x27,然后获取异常处地址然后+10处的数据,read_from_addr函数通过异常处+10位置-程序开始的地址然后从前面读出来的一大串数据进行比对取值,比如此次相减得到0x2213,也就是8723,那么对应得到820555419

而0x2213正好是int 3断点处+10

接着对得到的820555419字符串进行MD5加密,再从后往前开始传递数据,只传递一半,并且将数据改为字符串形式,最后将字符串改为QWORD,比如820555419MD5之后为357C98DED772654AE188CFC9EA1C2723,从后往前每两个一组转为字符串,得到23271CEAC9CF88E1,最后转为QWORD:0x23271CEAC9CF88E1h,然后和原来地址处的值异或

最后PTRACE_POKETEXT将数据写回去

for循环直到所有地址处的数据都进行SMC解密

结束SMC之后修改RIP,然后将新的REG结构体写入子进程中,并且调用PTRACE_CONT从新的地址处开始执行

第二次循环

程序继续wait,等到程序运行到第二个int 3处,此时该地址的QWORD的值正好为0xCAFE1055BFCCLL

进入else中

此时就是对原来SMC的数据进行重新加密并放入子进程中

第三次循环

最后waitpid等待子进程结束,返回信号,最后remove掉re3文件,父进程退出

恢复SMC的数据

编写脚本恢复

首先我们先使用条件断点获取到最终异或的值,然后编写idapython脚本恢复数据并且nop掉一些没用的数据

Edit BreakPoint->“…”->编写脚本打印RAX寄存器的值

得到

1
0x23271ceac9cf88e1,0x4d9403a34275494c,0xf1ac2fea63c94ea9,0xf32554baaf233dcc,0xad5ea15de7bcf568,0xabdfc454b2ec9fd0,0xa5ee2b4680957b2b,0xaf42f81128b7fb38,0xca34bde4268cae3,0x4ee274bc39f2d547,0x53458e3ea10ab93b,0x2e5fb32efac34cff,0x99f8f6faa7a64aec,0xef38004300eda44d,0xee67c44e2bcd18fc,0x9b1c209768ecb41e,0xfae74344fcba3cdb,0x62654e739151118d,0xbfa53d12825ac60,0x5fda7e9212d8d034,0xe8e15b2ffd058214,0x6258db99ec82ff1f,0xc1f8d40001b68bf6,0x6211d421f8ab1d50,0xd25bc129ebbbd366,0xaea9e2a30d3fcd24,0x12e2013bc48da1de,0x1db06bde7ca30286,0x226499b91812859b,0xb2b0d80d0f244ce4,0xfba26ec5f66ad4a5,0xef4975489b39baa5,0x75da0adeb0d03511,0xcbb9c9ef1c68088d,0xb707f2ec82b077b8,0x4989b97aadc513bb,0x74c613b6d47fcde,0x1d6396837a7ad9d8,0x7f1a74782535fe54

然后写脚本将对应位置处的值异或上即可

1
2
3
4
5
6
7
8
9
10
xor_data = {8723: 2533025110152939745, 8739: 5590097037203163468, 8755: 17414346542877855401, 8771: 17520503086133755340, 8787: 12492599841064285544, 8803: 12384833368350302160, 8819: 11956541642520230699, 8835: 12628929057681570616, 8851: 910654967627959011, 8867: 5684234031469876551, 8883: 6000358478182005051, 8899: 3341586462889168127, 8915: 11094889238442167020, 8931: 17237527861538956365, 8947: 17178915143649401084, 8963: 11176844209899222046, 8979: 18079493192679046363, 8995: 7090159446630928781, 9011: 863094436381699168, 9027: 6906972144372600884, 9043: 16780793948225765908, 9059: 7086655467811962655, 9075: 13977154540038163446, 9091: 7066662532691991888, 9107: 15157921356638311270, 9123: 12585839823593393444, 9139: 1360651393631625694, 9155: 2139328426318955142, 9171: 2478274715212481947, 9187: 12876028885252459748, 9203: 18132176846268847269, 9219: 17242441603067001509, 9235: 8492111998925944081, 9251: 14679986489201789069, 9267: 13188777131396593592, 9283: 5298970373130621883, 9299: 525902164359904478, 9315: 2117701741234018776, 9331: 9158760851580517972}

addr = 0x2213

while True:
data = get_qword(addr)
key = xor_data[addr]
dec = data ^ key
idc.patch_qword(addr, dec)
addr += 16
让父进程先结束,而子进程处于运行状态,attach到ida上

需要先对程序patch,首先为了让子进程不结束,我们需要使子进程进入死循环,即原地TP,接着就是不让父进程对子进程解密后的结果加密,也就是修改为XOR 0,最后要让父进程提前结束而不是父进程等待子进程结束之后再结束,需要修改for循环次数为2次,此时所有的patch都已经执行完毕

FallW1nd的神之一手

动调dump法

Itlly师傅的动调dump大法

exe文件添加和加载资源文件

添加资源文件

然后导入文件即可

然后为资源类型命名

1661435800476

效果如下

加载资源文件

1
2
3
4
5
6
7
8
9
HRSRC test = FindResourceA(0,(LPCSTR)101, "EXEC");//第二个参数是名称,第三个参数是类型
DWORD Size = SizeofResource(0, test);
HGLOBAL hResData = LoadResource(0, test);//返回资源的句柄
LPCVOID lpBuffer = LockResource(hResData);//返回资源的第一个字节的地址
HANDLE hFile = CreateFileA("tmp.exe", 0xC0000000, 0, 0, 2, 0x80, 0);//创建文件句柄
DWORD NumberofByteWritten = 0;
BOOL final = WriteFile(hFile, lpBuffer, Size, &NumberofByteWritten, 0);//写入文件
FlushFileBuffers(hFile);
CloseHandle(hFile);