PE学习——导出表,加载dll并GetProcAddress获取函数地址的内在原理
导出表
一个可执行程序是由多个PE文件组成,这些PE文件依靠倒入表、导出表进行联系,导出表存储着PE文件提供给其他人使用的函数列表,导入表则存储着PE文件所需要用到的PE文件列表。从PE文件的角度去看,任何PE文件都可以有导入、导出表,从一般情况下来看,EXE文件不会提供导出表,也就是不会提供给他人使用的函数,但这并不代表不可以提供。
定位导出表
在PE格式图中,扩展PE头最后一个成员是结构体数组,在这个结构体数组里面有16个结构体,第一个结构体就是导出表相关的信息,它有2个成员,一个表示导出表的地址,一个表示导出表的大小。如下图所示中的_IMAGE_EXPORT_DIRECTORY,就是PE导出表的结构:
virtualaddress和size示例(注意:这里的RVA其实是加了image base的!我看到官方定义也没有严格说要加image base,见:https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_director, RVA定义:相对虚拟内存地址 从所在模块(dll)基地址开始的地址,即:模块基地址 + RVA = VA,所i有微软在这里并不严谨):
比如:CFF exp打开一个dll文件,看到的导出目录
我们可以自己编写、发布一个DLL,导出表这样写:
EXPORTS
Add
@10
Sub
@12
Div
@13
NONAME
Mul
@15
接着我们可以手动去定位一下导出表的位置,先找到扩展PE头的最后一个成员结构体数组:
然后找到该结构体数组的第一个结构体,里面就包含了导出表的地址和大小:
VirtualAddress:
0x0002AD80
Size:
0x0000017F
这个地址是RVA,它实际上表示的是相对虚拟地址,我们需要将其转为FOA,也就在文件中的偏移地址,由于在当前PE文件中文件对齐和内存对齐是一样的,即RVA等于FOA,所以我们也不需要进行转换:
接着跟进这个地址就可以找到导出表了:
接着我们来看下导出表的结构,你会发现我们实际上找到的导出表,其整体大小是大于如下结构(40字节),这是因为在这张表中还包含了3个子表,也就是如下结构体的最后三个成员:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
// 未使用
DWORD TimeDateStamp;
// 时间戳,表示当前PE文件(DLL)编译时的时间
WORD MajorVersion;
// 未使用
WORD MinorVersion;
// 未使用
DWORD Name;
// 当前导出表文件名字符串的地址
DWORD Base;
// 导出函数起始序号
DWORD NumberOfFunctions;
// 所有导出函数的个数
DWORD NumberOfNames;
// 以函数名字导出的函数个数
DWORD AddressOfFunctions;
// RVA,导出函数地址表
DWORD AddressOfNames;
// 导出函数名称表RVA
DWORD AddressOfNameOrdinals;
// 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
所以排除子表,我们的结构体就只有40字节,接着我们从第4个成员,逐步解析。
解析导出表成员
Name
用于表示当前导出表文件名,这是一个字符串,所以我们只需要找到00即停止寻找:
0x0002ADD2
-> DLLDemo.dll
Base
用于表示当前导出函数的起始序号,也就是你定义的.DEF中最小的那个序号:
0x0000000A
->
10
NumberOfFunctions
用于表示所有导出函数的个数,这个数是按照你定义的.DEF中的序号算的,如果在导出时不按序号顺序导出,则空余位也计入总和,例如如下所示,我们实际上只有4个导出函数,但是因为导出函数的序号没有按照顺序来进行导出,导致计算的时候将空缺的也算入进去了:
0x00000006
->
6
NumberOfNames
用于表示以函数名字导出的函数个数,如果你在定义的.DEF中设置了导出函数的属性为NONAME则就是以序号的方式导出:
0x00000003
->
3
AddressOfFunctions
用于表示导出函数地址表的地址,这里的地址是RVA,如果当前PE文件的文件对齐与内存对齐不一致,需要转换为FOA,我们这边是一样的,所以不需要进行转换;通过NumberOfFunctions知道有6个导出函数,所以从该地址开始依次寻找6个4字节的地址即可,我们可以很清楚的看见这里的地址有些就是00填充的:
0x0002ADA8
->
0x0000100A
0x0000100F
0x00000000
0x00001019
0x00000000
0x00001014
如果你想看见具体的这些函数对应的代码,可以根据偏移地址找到对应的字节码,而后将其放入DTDebug之类的调试工具即可看见完整的代码,但是在这里编译器做了一下优化,加了一层JMP跳转,具体的就要跟下去了:
函数名 |
地址 |
字节码 |
Add |
0x0000100A |
E9 61 00 00 00 |
Sub |
0x0000100F |
E9 8C 00 00 00 |
Div |
0x00001019 |
E9 E7 00 00 00 |
Mul |
0x00001014 |
E9 B2 00 00 00 |
AddressOfNames
用于表示导出函数名称表的地址,这张表是按照首字母A-Za-z排序的,同样这里的地址是RVA,需要转换为FOA;通过NumberOfNames知道有3个是以函数名字导出的函数,所以从该地址开始依次寻找3个4字节的地址即可:
0x0002ADC0
->
0x0002ADDE
0x0002ADE2
0x0002ADE6
根据地址就可以找到对应的函数名(字符串见0x00即止):
AddressOfNameOrdinals
用于表示导出函数序号表的地址,同样这里的地址是RVA,需要转换为FOA;这里表中的成员数与AddressOfNames是一致的,但需要注意,序号表中的每个成员为2字节,那也就是从该地址开始依次寻找3个2字节数据即可:
0x0002ADCC
->
0x0000
0x0005
0x0002
至此,我们就解析完所有的导出表成员了。
子表之间的关系
当我们去调用一个DLL文件,使用其中的方法,需要使用到GetProcAddress函数去获取函数的地址然后调用:
FARPROC GetProcAddress(
HMODULE hModule,
// DLL模块句柄
LPCSTR lpProcName
// 函数名/序号
);
它的第二个参数lpProcName可以是函数名也可以是序号,而它的原理也正可以表示导出表中的三张子表之间的关系。
GetProcAddress(hModule, "Mul") ==》本质!!!
当直接使用函数名去寻找时,它的步骤是这样的:
-
先用函数名字去查询函数名称表的索引(每张表都有一个从0开始的索引);
-
根据函数名称表对应的索引去查函数序号表对应索引的序号;
-
根据函数序号表对应索引的序号查询函数地址表对应索引的地址。
GetProcAddress(hModule, 13) ==》本质!!!
当直接使用序号去寻找时,它的步骤是这样的:
-
使用序号减去Base(起始序号)得到的值;
-
将该值代入函数地址表对应索引,获得函数地址。
另外一个博客里对于导出表做了很好的说明:https://www.cnblogs.com/autopwn/p/15304905.html
相关测试代码
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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | 代码复用的实现: 1.静态链接库 一,创建静态链接库: (1)在VC6中创建项目:Win32 Static Library (2)在项目中创建两个文件:cntftools.h 和 cntftools.cpp; 这里创建两个文件就是上面操作完成之后,直接新建一个 class 即可生成这两文件; cntftools.h文件: #if !defined(AFX_TEST_H__DB32E837_3E66_4BE7_B873_C079BC621AF0__INCLUDED_) #define AFX_TEST_H__DB32E837_3E66_4BE7_B873_C079BC621AF0__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 int Plus( int x, int y); int Sub( int x, int y); int Mul( int x, int y); int Div( int x, int y); #endif cntftools.cpp文件: int Plus( int x, int y) { return x+y; } int Sub( int x, int y) { return x-y; } int Mul( int x, int y) { return x*y; } int Div( int x, int y) { return x/y; } 3.编译 --- 点击编译即可,不需要执行,调试等其他操作; 二,使用静态链接库: 方式一: (1)将上面编译完成之后,生成的cntftools.h 和 cntflibs.lib复制到要使用的项目中,这里放的位置是生成项目的文件夹,不是debug 文件夹里面; (2)在需要使用的文件中包含:#include "cntftools.h" (3)在需要使用的文件中包含:#pragma comment(lib, "cntflibs.lib" ) (4)下面是重新生成的一个项目文件,然后放入上面的头文件和lib文件,编译成功可正常执行,下面是对应的代码 // sjlx.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <stdlib.h> #include "cntftools.h" #pragma comment(lib,"cntflibs.lib") int main() { int x = Plus(2,3); printf ( "%d\r\n" ,x); system ( "pause" ); return 0; } 方式二: (1)将上面编译完成之后,生成的cntftools.h 和 cntflibs.lib复制到要使用的项目中,这里放的位置是生成项目的文件夹,不是debug 文件夹里面; (2)在需要使用的文件中包含:#include "cntftools.h" ; (3)在项目名称中右键-->设置-Link-->找到Object/library Module 在最后面空格一下,添加cntflibs.lib;下面是最终的结果; kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid .lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib、 advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid .lib odbc32.lib odbccp32.lib cntflibs.lib (4)再次编译执行,可成功运行; 三,静态链接库的缺点: (1)使用静态链接生成的可执行文件体积较大,造成浪费; (2)我们常用的 printf 、 memcpy 、 strcpy 等就来自这种静态库 ; (3)最重要的原因就是因为生成的函数对应代码是在变成成功之后的exe里面,所以如果要更改lib的话,很麻烦,需要; 需要再次重新编译生成exe文件;验证这一点,可以通过执行此函数的过程中查看反汇编代码;发现生成的反汇编代码; 其对应的内存地址是带入了ImageBase:0x00400000; 2.动态链接库 一,创建DLL (1)在VC6中创建项目:Win32 Dynamic-Link Library ,名称写dtljkcntf (2)再在项目中创建两个文件:mydll.h 和 mydll.cpp; 这里创建两个文件就是,直接新建一个 class 即可生成这两文件 1.源文件中 int __stdcall Plus( int x, int y) { return x+y; } int __stdcall Sub( int x, int y) { return x-y; } int __stdcall Mul( int x, int y) { return x*y; } int __stdcall Div( int x, int y) { return x/y; } 2.头文件中 extern "C" _declspec( dllexport ) __stdcall int Plus ( int x, int y); extern "C" _declspec( dllexport ) __stdcall int Sub ( int x, int y); extern "C" _declspec( dllexport ) __stdcall int Mul ( int x, int y); extern "C" _declspec( dllexport ) __stdcall int Div ( int x, int y); 说明: 1. extern 表示这是个全局函数,可以供各个其他的函数调用; 2. "C" 按照C语言的方式进行编译、链接 __declspec ( dllexport )告诉编译器此函数为导出函数; 二,使用DLL 方式一:隐式连接 步骤1:将 *.dll *.lib 放到工程目录下面 步骤2:将 #pragma comment(lib, "DLL名.lib" ) 添加到调用文件中 步骤3:加入函数的声明 --这里就是加在项目对应的主程序代码里面,我这里测试就在main函数入口的代码上面; extern "C" __declspec ( dllimport ) __stdcall int Plus ( int x, int y); extern "C" __declspec ( dllimport ) __stdcall int Sub ( int x, int y); extern "C" __declspec ( dllimport ) __stdcall int Mul ( int x, int y); extern "C" __declspec ( dllimport ) __stdcall int Div ( int x, int y); 说明: __declspec ( dllimport )告诉编译器此函数为导入函数; 下面是成功执行的主测试代码; #include "stdafx.h" #include <stdlib.h> __declspec ( dllimport ) int Plus ( int x, int y); __declspec ( dllimport ) int Sub ( int x, int y); __declspec ( dllimport ) int Mul ( int x, int y); __declspec ( dllimport ) int Div ( int x, int y); #pragma comment(lib,"dtljkcntf.lib") int main() { int x = Plus(2,3); printf ( "%d\r\n" ,x); system ( "pause" ); return 0; } 上面需要注意的地方: 因为我测试编译生成dll和lib文件之前写的代码是没有带入 extern "C" 和__stdcall这两个关键字; 当然上面是为了测试,实际情况需要带入是最好的; 测试结果是告诉我们,如果不带入上面两个关键字,那么编译生成的函数名称编译器会给我们重新命名; 简单理解操作就会给添加一些奇怪的字符,目的就是为了防止函数重名,因为在C++里面有重载的说法; 如果函数重名会导致其他异常情况; 所以总结一下:在生成dll和lib之前我们写的什么关键字代码,那么这里就要写什么; 查看dll文件显示函数信息: 使用工具可以是微软VC6.0 ++ 自带的Dependency进行查看; 也可以用OD查看,点击按钮 "E" ; 使用OD查看的时候,记得要要把上面生成好的dtljkcntf.dll文件放在; 可执行程序exe的相同目录下测试查看验证; 下面是带入 extern "C" 和__stdcall关键字的操作; #include "stdafx.h" #include <stdlib.h> extern "C" __declspec ( dllimport ) __stdcall int Plus ( int x, int y); extern "C" __declspec ( dllimport ) __stdcall int Sub ( int x, int y); extern "C" __declspec ( dllimport ) __stdcall int Mul ( int x, int y); extern "C" __declspec ( dllimport ) __stdcall int Div ( int x, int y); #pragma comment(lib,"dtljkcntf.lib") int main() { int x = Plus(998,663); printf ( "%d\r\n" ,x); system ( "pause" ); return 0; } 方式二:显示链接 步骤1: //定义函数指针 typedef int (__stdcall *lpPlus)( int , int ); typedef int (__stdcall *lpSub)( int , int ); typedef int (__stdcall *lpMul)( int , int ); typedef int (__stdcall *lpDiv)( int , int ); 步骤2: //声明函数指针变量 lpPlus myPlus; lpSub mySub; lpMul myMul; lpDiv myDiv; 步骤3: // //动态加载dll到内存中 HINSTANCE hModule = LoadLibrary( "DllDemo.dll" ); 步骤4: //获取函数地址 myPlus = (lpPlus)GetProcAddress(hModule, "_Plus@8" ); mySub = (lpSub)GetProcAddress(hModule, "_Sub@8" ); myMul = (lpMul)GetProcAddress(hModule, "_Mul@8" ); myDiv = (lpDiv)GetProcAddress(hModule, "_Div@8" ); 步骤5: //调用函数 int a = myPlus(10,2); int b = mySub(10,2); int c = myMul(10,2); int d = myDiv(10,2); 上述操作完成之后,对应的可执行代码如下: #include "stdafx.h" #include <stdlib.h> #include <windows.h> //#include <string.h> //定义函数指针; typedef int (__stdcall *lpPlus)( int , int ); typedef int (__stdcall *lpSub)( int , int ); typedef int (__stdcall *lpMul)( int , int ); typedef int (__stdcall *lpDiv)( int , int ); //上面的*lpPlus *lpSub *lpMul *lpDiv都是指针类型; int main() { //在main函数里面声明函数指针变量; lpPlus myPlus; lpSub mySub; lpMul myMul; lpDiv myDiv; //动态加载dll到内存中; HINSTANCE hModule = LoadLibrary( "dtljkcntf.dll" ); //获取函数地址; myPlus = (lpPlus)GetProcAddress(hModule, "_Plus@8" ); mySub = (lpSub)GetProcAddress(hModule, "_Sub@8" ); myMul = (lpMul)GetProcAddress(hModule, "_Mul@8" ); myDiv = (lpDiv)GetProcAddress(hModule, "_Div@8" ); int x = myPlus(998,3); printf ( "%d\r\n" ,x); system ( "pause" ); return 0; } 特别说明: Handle 是代表系统的内核对象,如文件句柄,线程句柄,进程句柄。 HMODULE 是代表应用程序载入的模块 HINSTANCE 在win32下与 HMODULE 是相同的东西 Win16 遗留 HWND 是窗口句柄 其实就是一个无符号整型,Windows之所以这样设计有2个目的: (1)可读性更好 (2)避免在无意中进行运算 3.使用.def导出 下面两步操作之前,先要新建一个动态链接库的文件模板,这里操作跟上面一样,为这里新建的名称是dtljkdef; 然后生成下面.h和.cpp这两个后缀的名字就是新建 class 操作即可; (1)dtdef.h文件 int Plus ( int x, int y); int Sub ( int x, int y); int Mul ( int x, int y); int Div ( int x, int y); (2)dtdef.cpp文件 int Plus( int x, int y) { return x+y; } int Sub( int x, int y) { return x-y; } int Mul( int x, int y) { return x*y; } int Div( int x, int y) { return x/y; } (3)cntfdefs.def文件 ---> 这一步操作就是新建一个文本文件,我这的名称是cntfdefs.def(New - Text File)写入下面文件; 需要注意:这里新建这个文本的时候,要带上后再名称def,然后要勾选上面的添加到项目的复选框,否则编译没问题使用,有问题; EXPORTS Plus @12 Sub @15 NONAME Mul @13 NONAME Div @16 上述的NONAME就是隐藏名字的意思,为这里隐藏了减法和乘法 (4)编译;编译完成之后复制dll文件到需要使用此dll文件的项目文件夹里面即可; (4)使用序号导出的好处: 名字是一段程序就精华的注释,通过名字可以直接猜测到函数的功能 通过使用序号,可以达到隐藏的目的. |
将生成的dll和lib文件拷贝到新建的一个项目里面
使用显示链接的方式调用dll
dll内容
好了,我们自己动手实践下:
vs 2017里,创建一个dll项目,然后:
我贴下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break ; } return TRUE; } |
mydll.h 因为使用了def导出文件,所以就没有必要再使用_declspec(dllexport) 了!
1 2 3 4 5 6 7 8 9 10 11 12 13 | #pragma once /* extern "C" _declspec(dllexport) int __stdcall Plus(int x, int y); extern "C" _declspec(dllexport) int __stdcall Sub(int x, int y); extern "C" _declspec(dllexport) int __stdcall Mul(int x, int y); extern "C" _declspec(dllexport) int __stdcall Div(int x, int y); */ extern "C" int __stdcall Plus( int x, int y); extern "C" int __stdcall Sub( int x, int y); extern "C" int __stdcall Mul( int x, int y); extern "C" int __stdcall Div( int x, int y); |
mydll.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include "pch.h" #include "mydll.h" int __stdcall Plus( int x, int y) { return x + y; } int __stdcall Sub( int x, int y) { return x - y; } int __stdcall Mul( int x, int y) { return x * y; } int __stdcall Div( int x, int y) { return x / y; } |
Source.def文件:
1 2 3 4 5 6 | EXPORTS Plus @12 Sub @15 NONAME Mul @13 NONAME Div @16 |
为了让文件大小对齐和内存对齐一样(主要是偷懒,不想去计算RVA和FVA的转换),都是0x1000(默认是200)
我修改下项目属性:
/FILEALIGN:4096 就是0x1000的十进制表示。
该选项含义:https://learn.microsoft.com/en-us/cpp/build/reference/filealign?view=msvc-150
/FILEALIGN选项使链接器将输出文件中的每个部分对齐到大小值的倍数的边界上。/FILEALIGN选项可用于提高磁盘利用率,或加快从磁盘加载页面的速度。较小的部分大小对于在较小设备上运行的应用程序或保持较小的下载量可能很有用。磁盘上的节对齐不会影响内存中的对齐。
好了,接下来,我们看下生成的DLL导出表:
我们去看下plus函数的函数名位置和代码位置(ImageBase为0x1000000):
我们接下来看看函数的位置1005地方:
验证下看看:
果然一模一样!贴下验证代码:
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 | #include <stdio.h> #include <stdlib.h> #include <windows.h> //#include <string.h> //定义函数指针; typedef int (__stdcall *lpPlus)( int , int ); typedef int (__stdcall *lpSub)( int , int ); typedef int (__stdcall *lpMul)( int , int ); typedef int (__stdcall *lpDiv)( int , int ); //上面的*lpPlus *lpSub *lpMul *lpDiv都是指针类型; int main() { //在main函数里面声明函数指针变量; lpPlus myPlus; lpSub mySub; lpMul myMul; lpDiv myDiv; //动态加载dll到内存中; HINSTANCE hModule = LoadLibrary(L "D:\\source\\repos\\dtljkcntf\\Debug\\dtljkcntf.dll" ); //获取函数地址; myPlus = (lpPlus)GetProcAddress(hModule, "Plus" ); int x = myPlus(998, 3); printf ( "%d\r\n" , x); system ( "pause" ); return 0; } |
使用winhex打开存储在硬盘位置的ipmsg.exe
【更新】:补充一个项目实战例子,当rve和foa不一样的时候,应该如何查看函数代码进行分析?
背景:
例如:注入的文件名 LHShield.dll,要看看注入DLL的功能:
base:1000 0000
section:00001000
filealignment:00000200
QAXStart 0014 1D21(名字) ==》0007 E8C0(地址)
文件偏移FOA:
0007E8C0-00001000+00000400=7DCC0(函数地址)
00141D21-0011A000+00119000=140D21(函数名字)
IDA里对应的地址:
函数地址:1007E8C0
我们看下反汇编代码:
为了显示opcode,修改下number of opcode bytes为6:
我们对比下文件内容和IDA,可以看到两边的opcode结果完全一样:
当然,要弄明白这一大段代码的作用,就需要很多耐心了。
你也知道程序员看没有文档的代码是多么蛋疼!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2020-06-04 AIDE(高级入侵检测环境)——就是讲文件的hash值存到db中,然后比较是否被篡改过
2020-06-04 完整性度量架构(IMA)介绍与分析——当应用程序运行、动态链接库加载、内核模块加载时,将用到的代码和关键数据(如配置文件和结构化数据)做一次hash比较的感觉
2019-06-04 pyspark AttributeError: 'NoneType' object has no attribute 'setCallSite'
2018-06-04 dga-5.29~6.4
2018-06-04 HTTP通道