在 C/C++ 语言函数库中提供了一组用于在线程内 “长条转” 的函数,它是大多数 C/C++ 协程 “Coroutine” 库实现的核心函数,当然C语言的异常处理大多也皆是此方式实现的。
在 x64 环境中 longjmp 还是只允许 int 传参,当然这无法在 x64 平台表示一个有效的 QWORD PTR 指针,当然本文的实现并不是在 x64 环境下实现的,而是在 x86 环境下实现的,当然本文的目的仅仅只是探讨原理,而不是实现目的,x64 汇编与 x86 汇编并不会有太多的区别,想要 x64 的实现拿起你的MASM拽写把。
setjmp 函数的原理就是把调用 “setjmp” 函数处的 “寄存器” 的值全部保存,我们无法直接访问到 EIP 寄存器获取 IL 执行位置,但可以通过获取 RIP(返回地址)的办法来确定当前调用 “setjmp” 的函数的 EIP 位置,这是大多数框架都会采取的一种足够可靠有效的办法,而你仅仅只需要对一个函数是如何调用与返回的具有相应的知识,即可。
值得注意的一点是,“setjmp” 在不同的实现中可能会有不同,例:Win32k 提供的 setjmp / longjmp 函数的实现,需要保存 16 个关键值,我们过滤两个段用于存储,EIP、FAR OUT,那么还剩下14个值,在过滤掉 8 个通用寄存器,还有六个寄存器的值,我们稍微猜测猜测就可以得知,无外乎 “Dr 0~3、Dr 6~7” or “CS、DS、ES、FS、GS、SS” 六个寄存器,但显然 Dr 调试寄存器组的可能行性是很低的,那么就只能是 “CS、DS、ES” 这一类的扩展辅助寄存器的值。
我们想一想保存且恢复 “EAX” 的值意义是否存在?显然我们会从调用了 “setjmp” 函数的位置返回,此时会改变EAX寄存器,若利用保存的EAX寄存器恢复是没有意义的,但按照 C 语言定义的格式,我们在调用 longjmp 函数时无法传递一个 0 的值,本人为了突出本文实现与 C函数库实现不同,所以调用 longjmp 函数回到 “setjmp” 函数的位置,返回 longjmp 提供的参数是 FAR OUT 形式传出提供的值。
前提提到保存且恢复 “EAX” 的值并不存在意义,那么保存其它六个寄存器值的是否有必要?假设我们想要超级通用显然是需要的,但是从另一方面来说保不保存恢不恢复也没有太大的必要,一般来说开发人员都很难会访问这些寄存器,大多数寄存器并不具有任何有效的值,皆NULL除FS寄存器,同时在没有代码显示的改变这几个寄存器值的情况下,它是不变的,当然要改变它们的值只可以通过汇编的形式完成,正常的C/C++语言代码编写与API调用是不会改写这些寄存器的值的。
那么我们先整理整理上述提到的东西,那么我们所需保存与恢复的通用寄存器的值大概只有几个就可以了,EAX、其它扩展的寄存器是不需要的。
typedef struct
{
void* rsv; // 0(eax)
void* ebx; // 4
void* ecx; // 8
void* edx; // 12
void* esi; // 16
void* edi; // 20
void* ebp; // 24
void* esp; // 28
void* eip; // 32
void* out; // 36
} __setjmp_buf;
为了显著的提供 “setjmp”、“longjmp” 函数的工作效率,显然我们需要动用一些优化手段,同时为了更快速的利用在 C/C++ 函数中内联的 _asm 代码片段,本人比较惯用在 C/C++ 函数设定 gadget 的办法。
static void* _scanrgadgetaddr(const void* function_)
{
if (function_ == NULL)
{
return NULL;
}
BYTE* p = (BYTE*)function_;
for (int i = 0; i < 50; i++)
{
BYTE* p1 = p + i;
INT64 n = *(INT64*)(p1);
if (n == 0xE5FFE4FFE5FFE4FF)
{
return p1 + sizeof(INT64);
}
}
return p;
}
以上的函数是用于在一个 C/C++ 函数中扫描一个被定义有效的 “gadget” 代码片位置地址的函数,gadget 代码片段之前你必须嵌入指定特征的 _asm 代码,另外串特征代码几乎不可能在任何C/C++编译器生成的或DLL库代码中出现。
_asm
{
jmp esp
jmp ebp
jmp esp
jmp ebp
}
jmp esp // FF E5
jmp ebp // FF E4
但为了降低 _asm 碰撞率,所以本人建议设置多条这类型的指令用于充当,gadget 扫描的一种判定依据,当然在上述提供的条件是足够利用了,0xE5FFE4FFE5FFE4FF = jmp esp; jmp ebp; jmp esp; jmp ebp;
那么我们就可以对自定义的 setjmp 、longjmp 函数安装 inline-hook 了,让函数直接 jmp 到函数体内嵌入的 _asm 代码处开始执行,这样将能保证 get 到绝对原汁原味的各种寄存器的值,而不必很麻烦的对各种寄存器、stack 之间进行回溯,还有一个需要关注的是,这个方法是无法 100% 一定可以回溯到调用 call 指令后一条指令未执行的情况的。
static bool __insthookprocjmp(const void* exportproc, const void* nextproc)
{
if (exportproc == NULL || nextproc == NULL)
{
return false;
}
DWORD flOldProtect;
if (!VirtualProtect((void*)exportproc, 5, PAGE_EXECUTE_READWRITE, &flOldProtect))
{
return false;
}
INT32 RVA = (BYTE*)nextproc - ((BYTE*)exportproc + 5); // JMP RVA
*(BYTE*)exportproc = 0xE9;
*(INT32*)(((char*)exportproc) + 1) = RVA;
return VirtualProtect((void*)exportproc, 5, flOldProtect, &flOldProtect);
}
那么此时我们就可以在 “setjmp”、“longjmp” 函数内部内嵌我们需要的 _ASM 代码了,我们先来看看 “setjmp” 函数的实现,它仅仅是把所需要的值保存到 “jmp_buf_” 之中,成功时返回 0 通知调用方函数 setjmp 点成功了。
static int __setjmp(const __setjmp_buf* jmp_buf_, void** out_)
{
_asm
{
jmp esp
jmp ebp
jmp esp
jmp ebp
}
_asm
{
mov dword ptr[esp + 12], ecx
mov ecx, dword ptr[esp + 4]
mov dword ptr[ecx], 0
mov dword ptr[ecx + 4], ebx
mov eax, dword ptr[esp + 12]
mov dword ptr[ecx + 12], eax
mov dword ptr[ecx + 12], edx
mov dword ptr[ecx + 16], esi
mov dword ptr[ecx + 20], edi
mov dword ptr[ecx + 24], ebp
mov dword ptr[ecx + 28], esp
mov eax, dword ptr[esp]
mov dword ptr[ecx + 32], eax // EIP
mov eax, dword ptr[esp + 8]
mov dword ptr[eax], 0
mov dword ptr[ecx + 36], eax
mov eax, 0
retn 0
}
}
现在来看看 longjmp 函数的实现,其实可以看到与 setjmp 的功能是类似的,只不过它是从 “jmp_buf_” 中提取需要的值,然后将其还原出来,同时 jmp 回到调用 “setjmp” 函数的位置。
static void __longjmp(const __setjmp_buf* jmp_buf_, const void* out_)
{
_asm
{
jmp esp
jmp ebp
jmp esp
jmp ebp
}
_asm
{
mov ecx, dword ptr[esp + 4]
mov eax, dword ptr[ecx]
mov ebx, dword ptr[ecx + 4]
mov edx, dword ptr[ecx + 12]
mov esi, dword ptr[ecx + 16]
mov edi, dword ptr[ecx + 20]
mov ebp, dword ptr[ecx + 24]
mov ecx, dword ptr[ecx + 36]
mov eax, dword ptr[esp + 4]
mov dword ptr[eax], ecx
mov ecx, dword ptr[esp + 4]
mov eax, dword ptr[ecx + 36]
mov ecx, dword ptr[esp + 8]
mov dword ptr[eax], ecx
mov ecx, dword ptr[esp + 4]
mov eax, dword ptr[ecx + 32]
mov esp, dword ptr[ecx + 28]
mov dword ptr[esp], eax
mov eax, 1
mov ecx, dword ptr[ecx + 8]
retn 0
}
}
此处提供一段用于测试的示例代码:
int main(int argc, char* argv[])
{
SetConsoleTitleA("setjmp longjmp");
__insthookprocjmp(&__setjmp, _scanrgadgetaddr(&__setjmp));
__insthookprocjmp(&__longjmp, _scanrgadgetaddr(&__longjmp));
__setjmp_buf jmp_buf_;
void* out_;
if (!__setjmp(&jmp_buf_, &out_))
{
int count = 10;
__longjmp(&jmp_buf_, &count);
}
else
{
for (int i = 0; i < *(int*)out_; i++)
{
printf("%d\n", i + 1);
}
}
return getchar();
}