2011年10月7日星期五

rundll32是如何工作的?

rundll/rundll32是和Windows 95一起出现的一个小工具。因为此工具的文档在Windows任何一版中都不存在,仅仅是微软知识库中的一篇不起眼的文章(后面会提到)而已,因此很多人只知道它可以调用任意DLL中的函数。很多所谓Windows 9x的“秘籍”,也是由运行这个去直接调用一些未公开的API函数来触发的。
不过很少有人知道这个工具所能调用的导出函数是有固定的原型的。这个发现来自我无聊的一个小实验:
rundll32 user32.dll,MessageBoxA yksoft1
结果是
所有用过Win32编程的都知道MessageBoxA的原型是
int WINAPI MessageBoxA(
HWND hWnd,
LPSTR lpText,
LPSTR lpCaption,
UINT uType
);

看看这个图,lpText看上去像一个exe或者PE文件文件头部的几个字节,lpCaption是rundll32的第二个参数,而消息框类型是MB_OKCANCEL也就是1。
最大的问题是lpText为何把某个exe的头部所在的地址传了进来,这肯定涉及到rundll32中调用函数原型的问题了。
在上网搜索之前,我先拿出个法宝来看看。2004年,微软的一家做跨平台解决方案的承包商Mainsoft遭到入侵,导致大量Windows NT4和2000系统的源码文件被泄露。这个法宝其实就是当年0day里的这个源码包了,有洋大人(ReactOS那帮人)因为这个惹上麻烦,我却不怕。
找到/private/windows/shell/rundll32/rundll.c,咱们来看看。
有一个很重要的宏,这应当就是rundll32所调用的函数原型了。
RUNDLLPROC g_lpfnCommand;
它之后这样被调用:
g_lpfnCommand(g_hwndStub, hInstance, pchCmdLine, nCmdShow);
这个宏应该在DDK的某个文件里有,但是找遍了都没找到。倒是继续翻,在这个源码包的/private/windows/inc16/shsemip.h中找到了这个宏定义。
typedef void (WINAPI FAR* RUNDLLPROCA)(HWND hwndStub,
HINSTANCE hAppInstance,
LPSTR lpszCmdLine, int nCmdShow);

typedef void (WINAPI FAR* RUNDLLPROCW)(HWND hwndStub,
HINSTANCE hAppInstance,
LPWSTR lpszCmdLine, int nCmdShow);

这是一个无返回值的stdcall类型函数,有4个参数。它们到底干什么的,还得继续看rundll.c逻辑。
int PASCAL WinMainT (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpszCmdLine, int nCmdShow)
这里是rundll32的WinMain,传进了当前基地址,上个实例的基地址,命令行参数,窗口显示方式(最大化什么的)。
if (!ParseCommand(lpszCmdLine, nCmdShow))
解析输入命令行。跟进去,里面其实包括了关键的对目标DLL进行LoadLibrary、GetProcAddress,把得到的函数地址赋给g_lpfnCommand,还有一些判断比如16位rundll调32位DLL、根据程序的ANSI/Unicode版本自动给函数名加W后缀/A后缀之类的特殊操作。最后,把传入的lpszCmdline的dll名/函数名去掉,只留下第二个参数。
if (!InitStubWindow(hInstance, hPrevInstance))
建立空窗口。其实就是一普通的RegisterClass/CreateWindowEx,不过建立了一个WS_EX_TOOLWINDOW风格的,大小0x0,这样用alt+tab之类就选不到,看上去也不可见,只能用spy++之类的工具看到。这个窗口句柄赋给g_hwndStub。
pchCmdLine = lpszCmdLine;
后面是个把pchCmdLine的Unicode字符串转ANSI的逻辑,和主逻辑关系不大,不说了。不过有个问题,根据ParseCommand里的逻辑,只有自动尝试在函数名后加W,GetProcAddress成功的函数才会被认为是Unicode的函数,如果尝试在名字后加W,加A都GetProcAddress失败,最后直接加载名字的函数就认为是ANSI函数了。这个的影响后面还要说。
try
{
g_lpfnCommand(g_hwndStub, hInstance, pchCmdLine, nCmdShow);
}
_except (EXCEPTION_EXECUTE_HANDLER)
{
RunDllErrMsg(NULL, IDS_LOADERR+17, c_szLocalizeMe, lpszCmdLine, NULL);
}
终于到最后的调用了。根据前面的说明,这些都很清楚了:g_hwndStub是建立那个空窗口的句柄,hInstance是当前rundll32.exe进程在内存中的基地址,pchCmdLine是进行所有操作后最后留下来的第二个命令行参数,nCmdShow是rundll32被启动时系统传入的窗口显示方式。
把我们最初的命令行
rundll32 user32.dll,MessageBoxA yksoft1
放进来看,经过所有操作,最后MessageBoxA会这么被调用:
MessageBoxA(g_hwndStub, hInstance, "yksoft1", nCmdShow);
对比最开始MessageBoxA的原型,第一个参数类型倒对上了,而第二个参数实际上把rundll32进程的基地址强制转换成了LPSTR也就是char*,而Windows虚地址空间中一个进程的基地址后首先是整个PE文件的映像,一开始的几个字节必定是EXE文件头"MZ"之类的,后面过一段通常会出现'\0',因此消息框内容就变成"MZ?"了。第三个参数没什么好说,而为何第四个参数会是MB_OKCANCEL,其实也就是因为系统默认的窗口显示方式是SW_SHOWNORMAL,MB_OKCANCEL==SW_SHOWNORMAL而已。
rundll32这个工具看上去并不是用来公开使用的(要不怎么连个/?参数都没有),但是为了微软长期以来纠结的向下兼容性,直到最新的Windows Developer Preview,仍然保留在系统中。也许正因为这个,这工具的使用其实非常受限制。首先是参数问题,如果rundll32误调用了cdecl传参方式的函数,由于cdecl方式是调用者而不是被调用者对参数栈进行清理,而rundll32按stdcall方式调用,期望是被调用者进行清理,这样退出时必定导致栈被破坏(不知道那个try能不能抓到这个例外)。然后,rundll32总是压进4个参数,如果被调用者希望的参数多于4个或者少于4个,退出后栈都不能平衡。另外就是这个自动加后缀的问题,除了微软自己的API之外,谁会用这样的命名方式把导出函数同时作一套ANSI和一套Unicode的呢?按照现在这个逻辑,没加W的一定被rundll32认为是ANSI的函数,这样就算被调用者理解rundll32调用的函数原型,还得考虑这个问题。
写了这么多,再回头来查文档。不查不知道,其实根本没必要去看Windows的源码,有好几篇微软的文章都提到了rundll32这些东西。
http://support.microsoft.com/kb/q164787/
KB164787.....90年代的老文了,里面我所讲到的基本都讲到了。还有一篇比较有趣的,就是说前面那个栈不平衡的问题老版本倒问题不大,可能是rundll32退出时系统全给清掉了;但是因为Vista下编译器优化的变化,导致非常容易出错。微软的程序员们干脆做了大改,先压N个字节的废物进栈,然后保存栈指针什么的。很蛋疼么。。。。http://blogs.msdn.com/b/oldnewthing/archive/2011/09/09/10208136.aspx
看来那堆当年的Windows“秘籍”什么的,用着都是有风险的!

1 条评论:

  1. 难得翻墙,留名一次。
    说到rundll32.dll我的印象停留在一些组件的注册注销上,没想到现在还有这样的风险。=ω=

    回复删除