杂记

目录

结构体与共用体

PE文件之导入表

导入表作为PE文件中重要的一环,在免杀中有诸多利用。

一个导入表的结构如下

image-20210722112317693

下面只说说重要的项

OriginalFirstThunk 指向导入名称表INT的RVA. Name 存储函数名字的RVA。 FirstThunk 指向导入地址表IAT的RVA。

OriginalFirstThunk 和FirstThunk对应的INT和IAT是个什么样的概念呢?

首先,无论是INT还是IAT,在PE文件被加载前(即在文件形式时)都指向同一种结构:IMAGE_THUNK_DATA,其结构如下

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE  指向一个转向者字符串的RVA
        DWORD Function;             // PDWORD 被输入的函数的内存地址
         DWORD Ordinal;              // 被输入的 API 的序数值
         DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME   指向 IMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

是一个联合体结构(所以只占四个字节), 当最高位为0时,就说明这四字节值为IMAGE_IMPORT_BY_NAME的RVA 当最高位为1时,则去掉最高位后剩下31位则为dll函数在导出表中的导出序号

我们通常取其AddressOfData值来获得另一个结构IMAGE_IMPORT_BY_NAME.

ypedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;       //可能为0,编译器决定,如果不为0,是函数在导出表中的索引
    BYTE    Name[1];    //函数名称,以0结尾,由于不知道到底多长,所以干脆只给出第一个字符,找到0结束
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

以上则是基本的结构,下面分析其工作的时候的实现流程。

首先在加载前,IAT和INT是指向同一个结构IMAGE_THUNK_DATA,的

image-20210722115238741

而加载后,Windows会重写FirstThunk所指向的INT中的每一个IMAGE_THUNK_DATA32,使其直接指向函数地址,这也就是IAT输入地址表的名字由来。

image-20210722115608691

c++中几个内存分配函数的区别

VirtualAlloc

用来分配大块内存。使用VirtualFree来释放

HeapAlloc

在进程初始化的时候,系统会在进程的地址空间中创建1M大小的堆,叫做默认堆。HeapAlloc用于从堆上分配内存块。 堆的使用可以更有效地进行内存管理,避免线程同步的开销以及快速释放内存。 此函数一般用于分配一般大小的内存空间,当分配内存块大于1M最好避免使用此函数。 使用HeapFree释放

malloc

C分配内存的方式,如果您使用C而不是C ++编写代码,并且您希望您的代码也可以在Unix计算机上工作,或者有人明确表示需要使用它,那么请选择此选项。用free解除分配。

值得一提的是,visual c++的malloc调用了HeapAlloc

new

c++分配内存的方式。用delete解除分配。

TEB PEB

TEB,即线程环境块。每一个线程都具有一个TEB结构,记录了相关线程的一些基本信息。 线程运行时,FS段寄存器记录了其TEB的位置,TEB结构的0x30处偏移即为PEB进程环境快。

mov eax,fs:[0x30]

PEB结构很大,有0x210个字节。

通过TEB\&PEB 动态获取函数地址

在PEB结构的0x0c偏移处,是一个指向PEB_LDR_DATA结构的指针。这个结构如下:

image-20210722221413525

他有三个LIST_ENTRY结构,每一个都按照不同顺序将进程加载的所有模块连接起来,通过遍历任意一个LIST_ENTRY我们就可以获得所有模块的基地址,获得每个模块基地址后通过DLL文件导出表就可以获得任一导出函数的地址了。

创建服务

服务

Windows服务,是指运行在windows nt操作系统后台的计算机程序. Windows服务必须符合服务控制管理器的接口规则和协议(SCM)

如何创建一个Windows服务

分为: 1.完成服务程序主函数(进程入口点(Main函数 2.完成服务程序内容主函数(ServiceMain 3.服务的注册器和卸载器

服务程序主函数

这个阶段,主要干两件事: 1.设定好SERVICE_TABLE_ENTRY 结构变量,传入服务名和服务主函数 2.调用StartServiceCtrlDispatcher函数 以下是SERVICE_TABLE_ENTRY结构

typedef struct _SERVICE_TABLE_ENTRYW {
    LPWSTR                      lpServiceName;
    LPSERVICE_MAIN_FUNCTIONW    lpServiceProc;
}SERVICE_TABLE_ENTRYW, *LPSERVICE_TABLE_ENTRYW;

所以我们的服务程序的主函数只需要像这样写即可

int main(){
  SERVICE_TABLE_ENTRY Table[] = { {L"servicename",ServiceMain},{NULL,NULL} };
  StartServiceCtrlDispatcher(Table);
}

在SERVICE_TABLE_ENTRY里我们定义好了一个服务的名字以及其入口函数,然后使用StartServiceCtrlDispatcher去调用这个结构。 第一步就是这么简单,接下来让我们去实现服务的入口函数

完成服务程序内容主函数

这一步主要要干这几个事情: 1.创建服务内容主函数 2在服务内容主函数里实现 SERVICE_STATUS 结构的填充,用于与SCM交流 3.实现服务句柄,并根据有服务句柄改变状态信息,从而实现SCM发来的控制请求 4.逻辑

创建服务内容主函数

根据我们在 SERVICE_TABLE_ENTRY 结构中定义的服务入口函数,创建对应函数

void WINAPI Servicename(DWORD argc, LPTSTR* argv)

实现 SERVICE_STATUS 结构

SERVICE_STATUS 结构定义如下

typedef struct _SERVICE_STATUS {
  DWORD dwServiceType;
  DWORD dwCurrentState;
  DWORD dwControlsAccepted;
  DWORD dwWin32ExitCode;
  DWORD dwServiceSpecificExitCode;
  DWORD dwCheckPoint;
  DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;

每个项都对应了参数…太多了,建议去官方文档对照 https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status

dwservicetype 指定了服务的类型 dwCurrentState 指定服务当前的状态(挂起,暂停,启动,停止…) dwControlsAccepted 指定了服务句柄可以接受的参数 dwWin32ExitCode 服务用于报告错误的错误代码 dwServiceSpecificExitCode 服务特定的错误代码 dwCheckPoint 不太懂 dwWaitHint 挂起的 启动,停止,暂停或继续操作所需的估计时间,以毫秒为单位 。若指定时间已过去而dwCheckPoint未增加或者dwCurrentState尚未更改,则中止服务。

总而言之,就是创建一个结构用于收集服务的各个信息

实现服务句柄&改变状态信息

实现服务句柄主要是依靠RegisterServiceCtrlHandlerA函数实现的。 这个函数的作用是注册一个函数来处理指定服务控制请求。

这个函数的结构是这样

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerA(
  LPCSTR             lpServiceName,
  LPHANDLER_FUNCTION lpHandlerProc
);

lpservicename是指定服务名,lphandlerproc是被注册的函数。所以我们要使用这个函数来注册另一个函数,首先要完成被注册函数的内部逻辑。这里做个范例

void WINAPI ctrl(DWORD Opcode)  //定义式固定
{
  switch (Opcode)
  {
  case SERVICE_CONTROL_PAUSE:
    m_ServiceStatus.dwCurrentState = SERVICE_PAUSED;
    break;
  case SERVICE_CONTROL_CONTINUE:
    m_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
    break;
  case SERVICE_CONTROL_STOP:
    m_ServiceStatus.dwWin32ExitCode = 0;
    m_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    m_ServiceStatus.dwCheckPoint = 0;
    m_ServiceStatus.dwWaitHint = 0;

    SetServiceStatus(m_ServiceStatusHandle, &m_ServiceStatus);
    bRunning = false;
    break;
  case SERVICE_CONTROL_INTERROGATE:
    break;
  }
  return;
}

我们发现这个函数会根据传入的值改变 SERVICE_STATUS 结构。 若把他注册,那么从服务管理控制器发来的控制指令会被当作参数传入该函数,然后该函数会修改 SERVICE_STATUS

那么改变状态信息的函数主要就是SetServiceStatus 了

BOOL SetServiceStatus(
  SERVICE_STATUS_HANDLE hServiceStatus,
  LPSERVICE_STATUS      lpServiceStatus
);

hservicestatus 指向服务句柄 lpservicestatus 指向 SERVICE_STATUS 结构。 完成这个函数后,服务就会向服务控制管理器发送自己最新的状态信息(即 SERVICE_STATUS 里定义的东西)。

DEMO

#include<Windows.h>

SERVICE_STATUS m_ServiceStatus;
SERVICE_STATUS_HANDLE m_ServiceStatusHandle;
BOOL bRunning;
int main() {
  WCHAR Servicename[] = L"ServiceA";
  SERVICE_TABLE_ENTRY Table[] = { {Servicename,ServiceMain},{NULL,NULL} };
  StartServiceCtrlDispatcher(Table);
}

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {


  m_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
  m_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
  m_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
  m_ServiceStatus.dwWin32ExitCode = 0;
  m_ServiceStatus.dwServiceSpecificExitCode = 0;
  m_ServiceStatus.dwCheckPoint = 0;
  m_ServiceStatus.dwWaitHint = 0;

  m_ServiceStatusHandle = RegisterServiceCtrlHandler(L"ServiceA", HandlerFunc);
  m_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
  m_ServiceStatus.dwCheckPoint = 0;
  m_ServiceStatus.dwWaitHint = 0;
  SetServiceStatus(m_ServiceStatusHandle, &m_ServiceStatus);
  bRunning = true;
  while (bRunning) {
    //your code
  }
  return 0;
}

void WINAPI HandlerFunc(DWORD code) {
  switch (code) {
  case SERVICE_CONTROL_PAUSE:
    m_ServiceStatus.dwCurrentState = SERVICE_PAUSED;
    break;
  case SERVICE_CONTROL_CONTINUE:
    m_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
    break;
  case SERVICE_CONTROL_STOP:
    m_ServiceStatus.dwWin32ExitCode = 0;
    m_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    m_ServiceStatus.dwCheckPoint = 0;
    m_ServiceStatus.dwWaitHint = 0;

    SetServiceStatus(m_ServiceStatusHandle, &m_ServiceStatus);
    bRunning = false;
    break;
  case SERVICE_CONTROL_INTERROGATE:
    break;
  }
}

启用SeDebugPrivilege

有很多工具需要启用SeDebugPrivilege才能正常使用,比如mimikatz就有个很经典的 privilege::debug.如果没有这个权限,administrator用openprocess可能都会报错,所以需要提升,管理员组拥有该权限但默认禁用,普通用户不拥有该权限,也就是说仅仅只有管理员账户能启用该权限。这个权限提供了强大的能力以至于可以忽视安全上下文行动。

我们会用到AdjustTokenPrivileges和LookupPrivilegeValueA,以及Token PRIVILEGES结构树

Token PRIVILEGES结构树

typedef struct _TOKEN_PRIVILEGES {
  DWORD               PrivilegeCount;
  LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;

PrirvilegeCount 表示Privileges数组中有多少个元素 Privileges 指向LUID_AND_ATTRIBUTES结构,该结构用于具体表示某个权限的开启或关闭

typedef struct _LUID_AND_ATTRIBUTES {
  LUID  Luid;
  DWORD Attributes;
} LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES;`

Luid 选择一个LUID值,该值对应一个权限 Attributes 选择指定LUID的权限的开放或关闭,其详细参数如下

SE_PRIVILEGE_ENABLED The privilege is enabled.
SE_PRIVILEGE_ENABLED_BY_DEFAULT The privilege is enabled by default.
SE_PRIVILEGE_REMOVED Used to remove a privilege. For details, see AdjustTokenPrivileges.
SE_PRIVILEGE_USED_FOR_ACCESS The privilege was used to gain access to an object or service. This flag is used to identify the relevant privileges in a set passed by a client application that may contain unnecessary privileges.

下面我们来展示如何利用windows 编程获取该权限。

这只是一个函数demo,修改当前进程的权限。

BOOL GetDebugPriv() {
  HANDLE Token;
  TOKEN_PRIVILEGES tp;
  LUID Luid;
  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &Token)) { //TOKEN_ADJUST_PRIVILEGES代表要修改令牌权限
    std::cout << "OpenProcessToken ERROR" << GetLastError() << std::endl;
    return false;
  }

  tp.PrivilegeCount = 1;
  tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
  if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid)) {
    std::cout << "LookupPrivilegeValue ERROR" << GetLastError() << std::endl;
    return false;
  }
  tp.Privileges[0].Luid = Luid;
  if (!AdjustTokenPrivileges(Token, FALSE, &tp, sizeof(tp), NULL, NULL) ){
    std::cout << "AdjustTokenPrivileges ERROR" << GetLastError() << std::endl;
    return false;
  }
  if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {  //ERROR_NOT_ALL_ASSIGNED的出现原因是,用户权限本身就没有sedebugprivilege
    return false;
  }
  else {
    return true;
  }
}

dll编写

DLL文件,即动态链接库文件,在windows中许多pe文件运行时需要将一些dll文件加载进内存,从而调用dll文件里的内容。

DLLMAIN

dllmain,是dll在被加载时执行的函数(dllmain在dll文件中不是必须的)

BOOL WINAPI DLLMain(
//指向自身句柄
_In_HINSTANCE hinstDLL,
//载入状态
_In_DWORD fdwReason,
//加载方式(隐式、显式)
_In_LPVOID IpvReserved
);

其中,载入状态有四种,如下

DLL_PROCESS_ATTACH:进程进入时

DLL_PROCESS_DETACH:进程退出时

DLL_THREAD_ATTACH:线程进入时

DLL_THREAD_DETACH:线程退出时

dllmain根据载入状态的不同,可以做出不同的反应。就像这样

#include <iostream>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
  case DLL_PROCESS_ATTACH:
    std::cout << "Process attach. \n";
    break;
  case DLL_PROCESS_DETACH:
    std::cout << "Process exit. \n";
    break;
  case DLL_THREAD_ATTACH:
    std::cout << "Thread attach. \n";
    break;
  case DLL_THREAD_DETACH:
    std::cout << "Thread exit. \n";
    break;
  }
    return TRUE;
}

DLL 导出函数

dll编写时,要用__declspec关键字标明哪些函数是要导出的,_

__declspec(dllexport) 用于Windows中的动态库中,声明导出函数、类、对象等供外面调用,像这样

extern "C" int _declspec(dllexport) go() {
    std::cout<<"gogo"
}

这样就设置好一个用于导出的函数了。

题外话:怎么使用dll或lib中的导出函数

对于lib(静态链接库)来说,使用静态链接 dll(动态链接库)来说使用动态链接

静态链接

又叫隐式链接。链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件),所以组成的exe文件体积较大

#pragma comment(lib,"testDLL.lib")  //在此处通过静态链接,此之前把生成的lib文件放入loadDLL工程文件夹下面,并设置编译器的附加依赖项中增加此lib
extern "C"_declspec(dllimport) int go(); //声明函数

int main(){
go();
}

动态链接

又叫显示链接,将一些api函数写入到一个dll文件中,在程序在程序运行时再加载到程序的进程空间中使用,这样可以让运行程序本身体积较小,方便模块化编程。

int TestDLL()

{

      HMODULE hModule = NULL;

      typedef int (*Func)(int a, int b);

      // 动态加载 DLL 文件
      hModule = LoadLibrary(_TEXT("..//Debug//FuncDll.dll" ));

      // 获取 fun函数地址
      Func fAdd = (Func)GetProcAddress(hModule, "fun" );

      // 使用函数指针
      printf("%d/n" , fAdd(3, 1));

      // 释放指针
      FreeLibrary(hModule);
      return 0;

}

rundll32

用rundll32.exe 可以执行dll中的某一导出函数

但是不是所有函数都能通过rundll32调用,函数必须符合如下定义

VOID CALLBACK FunctionName (HWND hwnd,HINSTANCE hinst,LPTSTR lpCmdLine,INT nCmdShow);

这里随便写了个

extern "C" __declspec(dllexport) void rundll(HWND hwnd, HINSTANCE hinst, LPTSTR lpCmdLine, INT nCmdShow) {

    MessageBox(NULL, TEXT("-Test-"), TEXT("Rundll32"), MB_OK);

}

image-20210301194831320

消息机制

事件驱动

Windows是基于事件驱动的. 一般来讲, 服务器处理模型有三种: 1每收到一个请求,创建一个新的进程,来处理该请求; 2每收到一个请求,创建一个新的线程,来处理该请求; 3每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

1 开销较大:每处理一个事件就要新建一个进程,开销太大,但是实现起来很简单 2 死锁问题:多线程调节容易遇到死锁问题,程序直接卡死 3 逻辑复杂

那么事件驱动的优势在哪里呢。 打个比方,我们要获取一个鼠标点击的动作。 如果我们通过创建线程或者进程的方法,去循环扫描当前是否有鼠标点击事件那么可能会造成资源浪费(鼠标一直不点击,但仍在进行扫描),响应缓慢(扫描的设备有很多,会造成响应缓慢的问题)等问题。 但是事件驱动就不一样了,它的核心原理是以消息队列为核心,当捕获到一个事件(如鼠标点击)时把他放进消息队列,然后当该事件从队列中被取出时,根据事件类型调用不同的函数来进行处理,其中每个事件一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数 。

消息机制

事件驱动在Windows下的具象化就是消息机制。 事件队列对应过来就是消息队列.

它会为每一个应用程序新开一个对应的消息队列,用于捕获其消息。

我们以一个简单的窗口实现来看看消息机制。 注意下方注释处即可。

#include<Windows.h>


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);


int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInStance, LPSTR lpCmdLine, int nShowCmd) {

  static TCHAR szAppName[] = TEXT("窗口类名称");
  HWND         hwnd;
  MSG          msg;
  WNDCLASSEX   wndclass = { 0 };

  //设计窗口类
  wndclass.cbSize = sizeof(WNDCLASSEX);
  wndclass.style = CS_HREDRAW | CS_VREDRAW;
  wndclass.lpfnWndProc = WndProc;
  wndclass.cbClsExtra = 0;
  wndclass.cbWndExtra = 0;
  wndclass.hInstance = hinstance;
  wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
  wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = szAppName;


  if (!RegisterClassEx(&wndclass))
  {
    MessageBox(NULL, TEXT("RegisterClassEx failed!"),TEXT("title"), MB_ICONERROR);
    return 0;
  }

  hwnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,
    szAppName,
    TEXT("窗口名称"),
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    NULL,
    NULL,
    hinstance,
    NULL);

  ShowWindow(hwnd, nShowCmd);
  UpdateWindow(hwnd);

  while (GetMessage(&msg, hwnd, 0, 0)) {  #从消息队列中获取消息,若存在消息待处理,则进行窗口过程。同时通过句柄得到需要捕捉消息的应用程序。
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
  HDC hdc;
  PAINTSTRUCT ps;
  RECT rect;

  switch (message)
  {
  case WM_PAINT:
    hdc = BeginPaint(hwnd, &ps);
    GetClientRect(hwnd, &rect);
    DrawText(hdc, TEXT("FUCK"), -1, &rect, DT_CENTER);
    EndPaint(hwnd, &ps);
    return 0;

  case WM_LBUTTONUP:
    MessageBox(NULL, TEXT("老子被点了"), TEXT("tick"), 0);
    return 0;
  }

  return DefWindowProc(hwnd, message, wParam, lParam);
}

对于消息机制,有几个点需要特别关注。

1.WM_PAINT,WM_TIMER,WM_QUIT 这几个消息永远被放在消息队列最后。 究其原因很简单,就拿WM_QUIT举例,他是意思是退出,若不放在最后,WM_QUIT后面的消息也就无法处理了。 2.也有部分消息是非队列消息,可以无视队列顺序首先被处理。