进程与线程的通信
进程与线程的通信
¶进程和线程的区别和联系
¶进程
进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位,通俗来说:进程就是程序的一个执行过程
进程主要有三个部分组成
-
进程控制块PCB,包含了进程描述信息、进程控制和管理信息、资源分配清单、CPU相关信息
-
数据段,即程序运行过程中的各种数据(比如程序中定义的变量)
-
程序段:就是程序的代码
¶线程
单个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 | //创建内存地址,这些函数指定的权限标志和CreateFileMapping中的权限标志不一致,则会执行失败。 |
¶多线程
¶创建线程
首先来看创建线程的函数
1 | //CreateThread |
线程函数的格式是统一的
1 | DWORD WINAPI 函数名(LPVOID 线程参数) |
线程函数放类中需要加static修饰,或者直接放类外
下面是创建线程的例子
1 |
|
我们在运行时会发现一个问题,多次运行的结果并不相同
这是因为线程原则上是并行执行的,而不是顺序执行的,操作系统来决定线程的执行顺序——CPU调度,所以可能是主线程先结束,也可能是其他线程先结束
同时需要注意Closehandle这个函数,因为他的作用不是用于关闭线程,而是释放线程句柄,表示不再对这个线程进行操作,线程句柄是一个内核对象,我们可以通过线程句柄来操作线程,但是线程的生命周期和线程句柄的生命周期不一样的。线程的生命周期就是线程函数从开始执行到return,线程句柄的生命周期是从CreateThread返回到你CloseHandle()
当我们不再使用句柄时一定要记得释放,因为句柄属于系统资源,使用完就得还回去
¶多线程执行
通过上面的例子我们可以知道多个线程的执行顺序是由操作系统决定的,但是有时候我们需要明确线程的执行先后顺序,这时候我们就需要对线程进行控制
¶WaitForSingleObject()
使用WaitForSingleObject()来控制
通过传入线程的句柄和等待的时间(参数中以毫秒为单位)来控制,INFINITE表示无限(0xFFFFFFFF),即当线程执行完之后再执行其他线程
1 | WaitForSingleObject(hObject, INFINITE); |
而没有WaitForSingleObject是这样的
¶互斥对象
互斥对象是系统内核维护的一种数据结构,它保证了对象对单个线程的访问权
互斥对象的结构
- 一个使用数量:指有多少个线程在调用该对象
- 一个线程ID:指互斥对象维护的线程的ID
- 一个计数器:表示当前线程调用该对象多少次
1 |
|
可以看到此时已经是两个线程交替执行了,但是谁先执行还是不确定的
下面对几个关键的函数进行说明
1 | HANDLE CreateMutexx(//用于创建或打开一个已经命名或者匿名的互斥对象 |
1 | BOOL WINAPI ReleaseMutex( |
1 | DWORD WaitForSingleObject( |
¶双进程保护
双进程保护:实际上是程序本身作为以恶搞进程或一个调试器,并且在调试模式下运行自身程序
这种程序的技术特点是
- 无法被调试,因为程序本身也是一个调试器,我们直到一般情况下一个程序只能被一个调试器所调试,如果他的程序先抢占作为了调试器,那么我们就没办法进行调试,所以解决办法只能是在他的调试器附加之前你先开始调试
- 一般来说,为了防止你抢占调试器,程序中会添加一个异常处理函数,并且在程序中加入使程序异常的代码,然后程序本身作为调试器对异常进行处理,而我们作为调试者,在调试过程中无法处理异常的代码
¶创建子进程
在Windows和Linux下的函数不一样,父进程创建的子进程,如果父进程关闭,子进程不一定关闭
¶Windows下
直接通过当前文件和CreateProcess即可
1 | //CreateProcess说明 |
dwCreationFlags对应的参数,而我们要实现双进程保护需要使用到DEBUG_PROCESS,这样调用进程将被作为调试器来调试新进程,并且把调试程序的所有调试时间通知给调试器,可以使用WaitForDebugEvent函数来接收
dwCreation还可以用来控制新进程的优先类
1 |
|
¶Linux下
使用fork函数
1 |
|
子进程创建之后我们可以使用ptrace进行跟踪,**Ptrace 可以让父进程控制子进程运行,并可以检查和改变子进程的核心image的功能 **
¶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函数创建进程时继承了原本进程的命令行参数,而原本进程是父进程的子进程,继承了父进程的命令行参数,所以待会调试的时候需要加上命令行参数
然后在另一函数处发现了一处SMC,int 3断点会使得子进程通知,并且通知父进程调试,由父进程处理
恢复数据之后的就是数织游戏,并且在init处对数据进行了修改,并且使用了setjmp和longjmp代替了循环
¶父进程
¶第一次循环
waitpid函数用于停止父进程,等待子进程执行
由于原先创建的子进程被替换了,所以信号量会被发送到父进程中,接受到之后使用PTRACE_CONT使子进程继续执行
接着通过prop/{pid}/maps获取子进程程序的开始地址,再从内存中read数据,这里的数据很特殊需要注意一下,第一个存储的是长度
然后继续waitpid,之后子进程运行到第一处int 3中断,返回父进程处理,首先使用PTRACE_GETREGS获取子进程中寄存器的值存储到REG结构体中(这里我没创建结构体(绝对不是因为我们找到REG的结构体成员),所以看起来很怪,因为看不出来哪里给Rip_addr进行了赋值,但实际上这里就是RIP)
紧接着PTRACE_PEEKTEXT从目标地址中返回数据,从接受到的数据我们可以知道返回的数据类型为QWORD,而这个数据正好是子进程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 | 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} |
¶让父进程先结束,而子进程处于运行状态,attach到ida上
需要先对程序patch,首先为了让子进程不结束,我们需要使子进程进入死循环,即原地TP,接着就是不让父进程对子进程解密后的结果加密,也就是修改为XOR 0,最后要让父进程提前结束而不是父进程等待子进程结束之后再结束,需要修改for循环次数为2次,此时所有的patch都已经执行完毕
¶动调dump法
¶exe文件添加和加载资源文件
¶添加资源文件
然后导入文件即可
然后为资源类型命名
效果如下
¶加载资源文件
1 | HRSRC test = FindResourceA(0,(LPCSTR)101, "EXEC");//第二个参数是名称,第三个参数是类型 |