0x06 Windows编程提高(内核、进程线程)

1. 内核对象


  • 什么是内核对象
    • 操作系统中的三种对象:GDI对象(GDI32) USER对象(User32) 内核对象(kernel32)
      • 其中,GDI对象和USER对象在所有应用程序中的值都是固定的。(句柄时全局的)
    • 内核对象本质上是一个结构体,但是这个结构体处于内核层,在用户层,只能通过操作系统提供的接口进行间接的访问。
    • 常见的内核对象有:进程,线程,文件,邮槽,IOCP,事件 等
  • 内核对象的特点:
    • 全局性:内核对象是跨进程的,可以在不同进程中访问同一个对象,通常使用字符串或ID标识一个对象。
    • 引用计数:每一个内核对象都有一个引用计数,当创建或者打开一个内核对象的时候,引用计数会+1,当关闭一个内核对象的时候,引用计数会-1,当引用计数为0时,内核对象会被销毁。
    • 安全性:除IOCP对象,所有的内核对象都需要传入安全描述符用于控制访问权限。
    • 句柄表:每一个进程都有一个句柄表,相同的内核对象在不同的进程中,可能句柄值是不一样的。

2. 文件操作


  • 文件操作相关函数
    • 不需要传入句柄
      • 删除文件:DeleteFile
      • 拷贝文件:CopyFileA
      • 移动文件:MoveFileA
      • 获取文件属性:GetFileAttributesExA
    • 需要传入句柄
      • 打开\创建文件: CreateFile,失败返回 INVALID_HANDLE_VALID(-1)
      • 获取文件指针:SetFilePointer
      • 读取\写入数据: Read\WriteFile
      • 获取文件大小:低位 = GetFileSize(文件句柄,大小的高位)
      • 设置文件结尾: SetEndOfFile
    • 遍历文件API
      • FindFirstFile 、 FindNextFile、 注意 “ * “,遍历文件需要排除 . ..

3. 进程和模块


  • 什么是进程
    • 进程可以看作是一个运行中的程序,应该由一个可执行程序(.exe)产生。一个进程最少包含了
    • 一个进程内核对象、一个线程内核对象、一块虚拟地址空间(4GB)、需要用到的模块。不执行代码
  • 什么是模块
    • windows下的可执行文件通常被称作模块,用于提供必须的数据以及代码。
  • 进程的类型(windows子系统)
    • GUI程序:窗口程序,入口为 (w)WinMain
    • CUI程序:控制台程序,入口为 (w)main
  • 进程的相关函数
    • 创建进程:CreateProcess、WinExec、system、ShellExecute 等
    • 结束进程:TerminateProcess、 ExitProcess
    • 打开进程:OpenProcess,不同的操作需要传入不同的权限
    • 获取当前的句柄:GetCurrentProcess() 返回的是伪句柄,使用 DumplicateHandle进行转换
    • 获取当前PID:GetCurrentProcessId(),pid和tid使用的是同一个序列
    • 遍历进程:CreateToolhelp32Snapshot \ Process32First \ Process32Next
  • 进程通信的方式:
    • 邮槽、套接字、WM_COPYDATA、管道、文件映射

4. 线程和线程同步


  • 线程的基本概念
    • 线程其实可以理解为一段正在执行中的代码,它最少由一个线程内核对象和一个栈(4M)组成。
    • 线程之间是没有从属关系的,同一进程下的所有线程都可以访问进程内的所有内容。
    • 主线程其实是创建进程时创建的线程,主线程一旦退出,所有子线程也会退出。
  • 线程的相关函数
    • 创建线程:CreateThread、_beginthreadex()
    • 结束线程:TerminateThread、_endthreadex()、ExitThread
    • 等待线程:WaitForSingleObject,线程结束是有信号的
    • 挂起线程:SuspendThread
      • 线程有挂起次数,挂起多少次就要恢复多少次
    • 恢复线程:ResumeThread
    • 切换线程:SwitchToThread Sleep
    • 遍历线程:CreateToolhelp32Snapshot、Thread32First、Thread32Next
      • 无论PID传入的是什么,遍历到的都是所有线程
  • 线程的退出方式
      1. 主线程函数(main\WinMain)返回,最为友好,会调用析构函数、会清理栈
      1. 使用ExitThread:不会调用析构函数
      1. 使用TerminateThread:不会调用析构函数,不会清理栈
      1. 结束进程:可能来不及保存工作结果
  • 线程的优先级
    • 线程只有相对于进程的优先级,随着进程优先级的改变,线程的相对优先级并不会改变
    • 通常情况下手动修改优先级并不会对程序的执行产生变化
  • 线程环境
    • 线程环境指的是一个线程当前的执行状态,包括了当前执行到了那里,局部变量和参数有哪些等。
    • windows是一个分时系统,当一个线程时间片用完时,会保存当前线程的执行状态,并且切
    • 换到新的线程,同时将新线程上次的执行状态恢复。这就是线程切换的基本原理
    • 使用 GetThreadContext 和 SetThreadContext 可以获取和设置线程环境
      • 记得给结构体的第一个字段传参。
      • 调试器
  • 线程同步
    • 同步和互斥
      • 同步:就是按照一定的规则执行不同的线程
      • 互斥:当多个线程要访问相同的数据时
    • 用户层的线程同步
      • 原子操作(Interlocked…)
        • INLINE HOOK的时候用于填充OPCODE
        • 特点:将一条语句转换成了具有同等功能的单条汇编指令 lock(指令前缀)
        • 缺点:只能给单个整数类型(4/8)进行保护,不能给使一段代码变成原子操作
        • 函数:
          • InterlockedExchange 交换两个数【关键】inline hook 中会用到
          • InterlockedIncrement 自增
      • 临界区(CriticalSection)
        • 特点:拥有线程拥有者的概念,同一个线程可以不断的重新进入临界区,但是进入了多
        • 少次,就要退出多少。
        • 缺点:一旦拥有临界区的线程崩溃,那么所有等待临界区的线程就会产生死锁。
        • 函数:
          • 初始化: InitializeCriticalSection
          • 保护:EnterCriticalSection 这两个函数之间的内容就是被保护的代码
          • 结束保护 :LeaveCriticalSection
          • 删除:DeleteCriticalSection
    • 内核层的线程同步
      • 等待函数__(WaitForObject)
        • 等待函数的形式
          • 单个:WaitForSingleObject
          • 多个:WaitForMultipleObjects
      • 一个可以被等待的对象通常由两种状态,分别是:
        • 可等待(激发态)(有信号):等待函数【不会阻塞】
        • 不可等待(非激发态)(无信号):等待函数需要等待一定时长并【阻塞】
      • 等待函数的副作用:
        • 改变被等待对象的信号状态(激发态变为 非激发态)
      • 互斥体(Mutex)
        • 特点:拥有临界区的线程拥有者概念,但是线程崩溃不会死锁,执行较慢
        • 函数
          • 创建:CreateMutex,如果第二个参数是FALSE,那么就是可等待
          • 打开:OpenMutex
          • 保护:WaitForSingleObject: 把互斥体置为不可等待
          • 离开保护:ReleaseMutex把互斥体置为可等待
          • 关闭:CloseHandle
      • 事件(Event)
        • 特点:可以手动选择是否产生副作用,如果设置为手动状态,那么就不产生副作用
        • 函数:
          • 创建:CreateEvent()
          • 打开:OpenEvent()
          • 保护:WaitForSingleObject()
          • 设置为非激发态:ResetEvent()
          • 退出(设置为激发态):SetEvent()
          • 关闭:CloseHandle
      • 信号量(Semaphore)
        • 特点:有多把锁,可以控制活动线程的数量
        • 函数:
          • 创建:CreateSemaphore
          • 打开:OpenSemaphore
          • 上锁:WaitForSingleObject
          • 解锁:ReleaseSemaphore
          • 关闭:CloseHandle
        • 用途:一般用做消费者-工厂模式

5. 同步IO和异步IO


  • 同步和异步
    • 同步:以普通的方式读写一个文件就是同步IO,当读写的数量较大的时候,所在线程会阻塞
      • ReadFile() \ WriteFile
    • 异步:以重叠IO的方式读写一个文件的操作,它会在调用IO函数的时候提交一个IO请求给操作系统,然后函数不等IO 操作执行完毕继续往下执行,当IO操作结束时,操作系统会以某种方式返回一个信号。
      • 在打开文件的时候必须指定 FILE_FLAG_OVERLAPPED 标记
      • 必须在读写函数内传入 OVERLAPPEND 结构体
      • 当使用重叠IO的时候,Read|WriteFile 的第四个字段可以传入 NULL
      • 在重叠IO中文件指针是没有用的,需要使用 OVERLAPPEND 结构体中的Offset(High)字段进行设置
  • 异步操作 - 等待句柄 ```cpp // 缺点:当IO请求过多的时候,不知道等待完成的是哪一个IO操作

    include

    include

    using namespace std; int main() { // 1. 打开一个文件 HANDLE FileHandle = CreateFileA( “F:\test.txt”, GENERIC_ALL, NULL, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); // 2. 进行读写操作 CHAR Buffer[0x100] = { 0 }; OVERLAPPED OverLappend = { 0 }; OverLappend.Offset = 4; // 使用重叠IO必须的结构体

ReadFile(FileHandle, Buffer, 0x100, NULL, &OverLappend); // 3. 通过等待文件句柄可以知道一个IO操作是否完成 WaitForSingleObject(FileHandle, INFINITE); return 0; }

  1. - **异步操作 - 等待对象**
  2. ```cpp
  3. // 相对于等待句柄来说,可以识别是哪一个IO操作完成了
  4. #include <iostream>
  5. #include <windows.h>
  6. using namespace std;
  7. int main()
  8. {
  9. // 1. 打开一个文件
  10. HANDLE FileHandle = CreateFileA(
  11. "F:\\test.txt", GENERIC_ALL, NULL,
  12. NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
  13. // 2. 进行读写操作(读取0~5)
  14. CHAR BufferA[0x100] = { 0 };
  15. OVERLAPPED OverLappendA = { 0 };
  16. OverLappendA.Offset = 0;
  17. // 使用重叠IO必须的结构体
  18. OverLappendA.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  19. ReadFile(FileHandle, BufferA, 0x5, NULL, &OverLappendA);
  20. // 3. 进行读写操作(读取6~10)
  21. CHAR BufferB[0x100] = { 0 };
  22. OVERLAPPED OverLappendB = { 0 };
  23. OverLappendB.Offset = 6;
  24. // 使用重叠IO必须的结构体
  25. OverLappendB.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  26. ReadFile(FileHandle, BufferB, 0x5, NULL, &OverLappendB);
  27. // 3. 通过等待文件句柄可以知道一个IO操作是否完成
  28. WaitForSingleObject(OverLappendA.hEvent, INFINITE);
  29. WaitForSingleObject(OverLappendB.hEvent, INFINITE);
  30. return 0;
  31. }
  • 异步操作 - APC函数

    • APC队列: 异步过程调用队列,保存的是一组函数的地址以及需要的参数
      • APC队列内的函数在什么时候调用?当线程属于闲暇状态(可警醒)的时候会查看当前的APC队列是否存在函数,如果存在函数就会一一调用,如果不存在就会休眠或者继续执行。
      • 投递的请求并不一定按照投递顺序完成
    • APC队列中的函数是根据完成的顺序放入的
    • APC队列时线程相关的,每一个线程都有一个APC队列 ```cpp

      include

      include

      using namespace std; CHAR BufferA[0x100] = { 0 }; CHAR BufferB[0x100] = { 0 }; // IO操作完成后操作系统会向APC队列添加一个这样的函数 VOID WINAPI OverLappedRoutine( DWORD dwErrorCode, // 错误码 DWORD dwNumberOfBytesTransfered,// 实际操作的字节数 LPOVERLAPPED lpOverlapped // 重叠IO结构体 ) { // 判断当前时哪一个消息 if (lpOverlapped->hEvent == (HANDLE)0x01) { printf(“%s\n”, BufferA); } else { printf(“%s\n”, BufferB); } }

    int main() { // 1. 打开一个文件 HANDLE FileHandle = CreateFileA( “F:\test.txt”, GENERIC_ALL, NULL, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); // 2. 进行读写操作(读取0~5) OVERLAPPED OverLappendA = { 0 }; OverLappendA.Offset = 0; // 使用重叠IO必须的结构体

    OverLappendA.hEvent = (HANDLE)0x01; ReadFileEx(FileHandle, BufferA, 0x5, &OverLappendA, OverLappedRoutine); // 3. 进行读写操作(读取6~10) OVERLAPPED OverLappendB = { 0 }; OverLappendB.Offset = 6; // 使用重叠IO必须的结构体

OverLappendB.hEvent = (HANDLE)0x02; ReadFileEx(FileHandle, BufferB, 0x5, &OverLappendB, OverLappedRoutine); // 将线程设置为可警醒状态 SleepEx(0, TRUE); return 0; } // 最常使用的是完成那个端口模型 ```