- UID
- 1043
- 精华
- 积分
- 11692
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
一个Hello World程序的关键代码就是printf的调用,而按照posix标准,printf是对stdout输出字符串。按这个道理,在Windows中,可以通过取得stdout的句柄后用WriteFile函数把文本输出到控制台。不过这必然会依赖GetStdHandle这个kernel32.dll的API,因此我们就有必要跳过这个函数去找我们需要的控制台句柄。不过如果你深入读过并探索我写的关于不使用API获取当前进程路径的文章的话(https://www.0xaa55.com/thread-16873-1-1.html),你就会注意到进程参数中保存了这三个控制台句柄。拿到这三个句柄之后,就可以直接使用ntdll.dll的NtReadFile和NtWriteFile函数读写控制台。
鉴于我们直接使用ntdll.dll的函数,并且还要用PEB等结构体的定义,一般的SDK是没有这些定义的,但也不是没有,WRKv1.2的SDK就包含这些。为了方便起见,本文附带的源码包为傻瓜式源码包,里面有需要用到的头文件以及编译器(注:WRKv1.2的编译器版本相近于VC2005)等。只需要将压缩包完整解压出来,双击批处理就可以开始编译。
先说说怎么拿到控制台句柄。这个句柄位于进程参数中,我们可以通过fs gs段拿到进程环境块的地址,进而取得进程参数的结构体指针。拿到进程参数后,直接取出那三个句柄即可,代码如下:
- HANDLE StdIn=NULL;
- HANDLE StdOut=NULL;
- HANDLE StdErr=NULL;
- PPEB Peb=NULL;
- PRTL_USER_PROCESS_PARAMETERS ProcessParameters=NULL;
- void Init()
- {
- Peb=(PPEB)__readtebptr(TEB_PEB_OFFSET);
- ProcessParameters=Peb->ProcessParameters;
- StdIn=ProcessParameters->StandardInput;
- StdOut=ProcessParameters->StandardOutput;
- StdErr=ProcessParameters->StandardError;
- }
复制代码
StdIn, StdOut, StdErr三个变量赋值以后,我们就可以读写控制台了。
接下来我们就需要实现printf了,这个实现其实很简单,先把完整的字符串用sprintf之类的函数打印出来,再用NtWriteFile输出到控制台上。
ntdll.dll中有个导出函数叫_vsnprintf,正适合我们格式化字符串,尤其是它还能防止栈溢出。通常来说,512字节够我们使了,代码如下:
- #define PRINT_BUFFER_SIZE 512
- int __cdecl ntprintf(const char* format,...)
- {
- IO_STATUS_BLOCK iosb={0};
- int len;
- char buff[PRINT_BUFFER_SIZE];
- va_list args;
- va_start(args,format);
- len=_vsnprintf(buff,PRINT_BUFFER_SIZE,format,args);
- if(len>0)NtWriteFile(StdOut,NULL,NULL,NULL,&iosb,buff,len,NULL,NULL);
- va_end(args);
- return len;
- }
复制代码
如果嫌不够就加大PRINT_BUFFER_SIZE这个值。
那么程序的入口函数就需要先Init(),再ntprintf(),代码如下:
- void Main()
- {
- Init();
- ntprintf("Hello Native Console!\n");
- }
复制代码
将其编译并运行,效果如下:
拖进IDA,可以发现已经导入表里只有ntdll.dll:
不过即便是只导入ntdll.dll,系统依然会加载kernel32.dll。由于枚举模块列表可以通过直接遍历LDR双向链表来实现,不需要API,代码如下:
- void PrintLdrList()
- {
- PLDR_DATA_TABLE_ENTRY pLdr=(PLDR_DATA_TABLE_ENTRY)PebLdrData->InLoadOrderModuleList.Flink;
- PLDR_DATA_TABLE_ENTRY tLdr=pLdr;
- do
- {
- ntprintf("Base: 0x%p Size: 0x%08X Name: %wZ\t Path: %wZ\n",pLdr->DllBase,pLdr->SizeOfImage,&pLdr->BaseDllName,&pLdr->FullDllName);
- pLdr=(PLDR_DATA_TABLE_ENTRY)pLdr->InLoadOrderLinks.Flink;
- }while(pLdr!=tLdr);
- }
复制代码
然后修改Init和Main函数:
- PPEB_LDR_DATA PebLdrData=NULL;
- void Init()
- {
- Peb=(PPEB)__readtebptr(TEB_PEB_OFFSET);
- ProcessParameters=Peb->ProcessParameters;
- PebLdrData=Peb->Ldr;
- StdIn=ProcessParameters->StandardInput;
- StdOut=ProcessParameters->StandardOutput;
- StdErr=ProcessParameters->StandardError;
- }
- void Main()
- {
- Init();
- PrintLdrList();
- }
复制代码
编译并运行:
似乎就可以得出结论:即便程序的整个导入表树上没有kernel32.dll,系统仍然会加载kernel32.dll。
最后再实现个阻塞控制台吧,代码如下:
- void Pause()
- {
- IO_STATUS_BLOCK iosb={0};
- char k;
- NtWriteFile(StdOut,NULL,NULL,NULL,&iosb,"Press Enter key to continue...",30,NULL,NULL);
- NtReadFile(StdIn,NULL,NULL,NULL,&iosb,&k,1,NULL,NULL);
- }
复制代码
在入口函数的结尾处调用刚写的Pause函数,其中NtReadFile的调用会阻塞住控制台,双击运行能看到控制台,结果如下:
结语
由于ntdll.dll里有很多crt函数(比如qsort,sin,_wcsnicmp),很多时候在写C程序时可以绕过msvcrt.dll,甚至是kernel32.dll。比如NtAllocateVirtualMemory替代VirtualAlloc(Ex),RtlAllocateHeap替代HeapAlloc等等。总之,脱离msvcrt甚至kernel32都是可行的,只要愿意挖掘Windows的特色即可。
源码包里包含了WRKv1.2的编译器和SDK头文件,以及WDK7600中2K3版的ntdll.lib。其中还包括了四个批处理文件分别用于32位和64位的Debug和Release编译(注:chk即Checked,也就是Debug编译;fre即Free,也就是Release编译),以及一个用于清理所有已编译文件的批处理文件。双击批处理文件即可实现编译。
很多人调用ntdll.dll的函数时都会用GetProcAddress去取函数地址,但实则大可不必,链接器中带上ntdll.lib即可。 |
|