CAPI简介
CAPI是由defanive开发的批处理工具,其功能类似于FFI,实现了在批处理脚本内进行内存操作以及调用动态库函数的功能
同类工具还有由Alox开发的CAPIx和由happy886rr开发的ICMD
逆向分析
下面通过逆向工具IDA来对CAPI进行分析,并与由Aloxf开源实现的CAPIx进行对比
主要伪代码如下
if ( !v28(*v27, aApi) && !v28(v27[1], aCall) && pNumArgs >= 4 )// API Call
{
v40 = v27[2];
Value = 0;
v41 = GetModuleHandleW(v40);
if ( !v41 )
v41 = LoadLibraryW(v27[2]); // Module
subcmd = (void *)GetLastError();
if ( v41 )
{
v42 = ToA(v27[3]);
func = GetProcAddress(v41, v42); // Function
subcmd = (void *)GetLastError();
operator delete(v42);
if ( func )
{
v43 = operator new(4 * pNumArgs - 12);
v44 = pNumArgs;
v45 = pNumArgs - 1;
if ( pNumArgs - 1 > 3 )
{
v46 = &v27[v45];
while ( ‘\x01‘ ) // Arg List
{
v43[v44 - v45 - 1] = 0;
v47 = *v46;
switch ( **v46 )
{
case ‘#‘:
prepush = (LPCWSTR)ToA(v47 + 1);
v43[pNumArgs - v45 - 1] = prepush;
stacktop = (void *)prepush;
break;
case ‘$‘:
prepush = v47 + 1;
stacktop = (void *)(v47 + 1);
break;
case ‘*‘:
prepush = GetVar(v47 + 1);
v43[pNumArgs - v45 - 1] = prepush;
stacktop = (void *)prepush;
break;
case ‘.‘:
LOBYTE(v66) = ::wtoi(v47 + 1);
stacktop = v66;
break;
case ‘;‘:
prepush = (LPCWSTR)::wtoi(v47 + 1);
stacktop = (void *)prepush;
break;
default:
break;
}
--v45;
--v46;
if ( v45 <= 3 )
break;
v44 = pNumArgs;
}
}
Value = ((int (__cdecl *)(void *))func)(stacktop);// Call
v48 = (void *)GetLastError();
v27 = cmd;
subcmd = v48;
v49 = pNumArgs - 1;
if ( pNumArgs - 1 > 3 )
{
prepush = (LPCWSTR)&cmd[v49];
do
{
if ( v43[pNumArgs - v49 - 1] )
{
if ( **(_WORD **)prepush == 42 )
dword_1000321C(*(_DWORD *)prepush + 2, v43[pNumArgs - v49 - 1]);
operator delete((void *)v43[pNumArgs - v49 - 1]);
}
--v49;
prepush -= 2;
}
while ( v49 > 3 );
}
operator delete(v43);
}
}
itow((int)aCapiRet, Value);
itow((int)aCapiErr, (int)subcmd);
v28 = wcsicmp;
}
加载模块
CAPI首先通过LoadLibraryW加载动态库,然后调用GetProcAddress获取动态库导出函数的地址
压入参数
接下来对以*
,$
,;
,.
,*
开头的参数进行不同的处理,并将解析的参数值压入到栈中
注意这里IDA没有正确处理push指令,故猜测作者在这里可能内联了汇编语句,需要手动进一步分析
下面以对立即数传值功能$
为例进行分析,切换到汇编模式
可以看到switch分支在jmp离开前,使用了push指令将参数压入栈中
对于其他分支也是用相同的方法压入参数
调用函数
压栈完成后再通过call [ebp+func]
来调用指定的函数,如下图所示
内存释放
在调用完成后,进行相关内存的释放,在此不再赘述
返回值
最后通过itow函数来设置返回值CapiRet和CapiErr
对比CAPIx
与CAPI不同的是,CAPIx使用汇编语句写了一个模块来完成压栈的工作
CAPI_Ret* APIStdCall(void *hProc, int *arr, int len, short type)
{
//int _high;
int _low;
double _double ;
__asm
{
mov ebx, dword ptr [arr] ;//把arr指向的地址(参数列表的尾地址)放入ebx
mov ecx, dword ptr [len] ;//把len的值放入ecx,作为循环控制变量
dec ecx ;//递减ecx
LOOP1:
mov eax, dword ptr [ebx] ;//倒序把数组arr(ebx指向的内容)的内容加载到eax
sub ebx, 4 ;//把ebx的内容递减4(ebx指向的前移一位)
push eax ;//把eax压栈
dec ecx ;//递减ecx
jns LOOP1 ;//如果ecx不为负值,则跳转到LOOP1:
call dword ptr [hProc] ;//调用API
fstp _double;
mov _low, eax ;//返回值存入result
//mov _high, edx ;
mov ebx, dword ptr [len] ;//把len的值放入ebx
SHL ebx, 2 ;//左移两位,这是可变参数的大小
//add esp, ebx ;//恢复堆栈指针 //API use __stdcall needn‘t to add esp
xor eax, eax ;//清空eax
}
CAPI_Ret *ret = (CAPI_Ret *)malloc(sizeof(CAPI_Ret));;
if (type == INT_FUNCTION) {
ret->_int[0] = _low;
} else {
ret->_double = _double;
}
return ret;
}
总结
对于不定长参数问题,CAPI和CAPIx的解决方式有所不同,但都涉及到使用汇编语句进行压栈操作
通过对CAPI和CAPIx的分析,可以加深对32位模式下传参方式的理解