【C】只导入ntdll.dll的Hello World
一个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;
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即可。 俺自己捏导入库惹,用到啥就加啥 可以自己申请控制台 打开 conin$ conout$获取 stdin 和 stdout句柄 没看懂纠结是否加载KERNEL32有啥意义。。。
不过简单WIN32小程序用WDK7编译是很好的,它会直接导入MSVCRT.DLL,编译出来的EXE/DLL非常小。 学习一下了。 美俪女神 发表于 2020-5-25 06:42
没看懂纠结是否加载KERNEL32有啥意义。。。
不过简单WIN32小程序用WDK7编译是很好的,它会直接导入MSVCRT.D ...
很多古代的程序对kernel32.dll的版本有要求,比如有的就只能加载Win95和WinME的kernel32.dll。
虽说我们现在可以不需要担心因为依赖了kernel32.dll而出现对系统版本的过分要求,但如果不依赖,似乎更容易在那些只会玩eXeScope的小朋友圈子里成为爸爸。
以及有些环境(具体记不得了)你不一定有Kernel32.dll可用 顶一下 原来这些函数r3 也可以用 本帖最后由 小冰 于 2020-6-18 16:15 编辑
我正确获取到了STDOUT的句柄, 使用下面的方式
#define STD_OUTPUT_HANDLE_INDEX 3UL
HANDLE hStdOut = ((PPEB)RtlGetCurrentPeb())->ProcessParameters->Reserved2;
hStdOut的值和调用GetStdHandle(STD_OUTPUT_HANDLE)返回的值是一个值。
但问题就出在输出
PCSTR str = "Hello World!";
NTSTATUS status = NtWriteFile(hStdOut, NULL, NULL, NULL, &iosb, str, strlen(str), NULL, NULL);
status的值是0xc0000024, 经查MSDN列出的NTSTATUS VALUE, 得知是STATUS_OBJECT_TYPE_MISMATCH
在32/64位程序中的执行结果都如此。
但能使用WriteFile来输出。
WriteFile(hStdOut, str, strlen(str), NULL, NULL);
但是什么原因导致NtWriteFile不能输出呢?
您的这个例子我下载到我的电脑中编译执行也输出不了。
下面是我所有的代码:
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
NTSYSAPI PVOID NTAPI RtlGetCurrentPeb();
#define STD_OUTPUT_HANDLE_INDEX 3UL
NTSYSAPI NTSTATUS NTAPI NtWriteFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINEApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
PLARGE_INTEGER ByteOffset,
PULONG Key
);
int main(void) {
HANDLE hStdOut = ((PPEB)RtlGetCurrentPeb())->ProcessParameters->Reserved2;
IO_STATUS_BLOCK iosb = {NULL};
PCSTR str = "Hello World!";
NTSTATUS status = NtWriteFile(hStdOut, NULL, NULL, NULL, &iosb, str, strlen(str), NULL, NULL);
//WriteFile(hStdOut, str, strlen(str), NULL, NULL);
return 0;
}
我的电脑是windows 7 64位。 小冰 发表于 2020-6-18 16:10
我正确获取到了STDOUT的句柄, 使用下面的方式
#define STD_OUTPUT_HANDLE_INDEX 3UL
H ...
因为控制台没经过ntwritefile 走应该是写指针 然后 ntRequestWaitReplyPort来着 Ayala 发表于 2020-6-18 19:48
因为控制台没经过ntwritefile 走应该是写指针 然后 ntRequestWaitReplyPort来着
用IDA看了一下WriteFile, 它调用了WriteConsole, 然后又调用了WriteConsoleInternal, 最后调用CsrClientCallServer给CSRSS发消息。不过我好奇,为啥在唐的机器上可以输出,是只有在win10上才有效?我手头上没有win10的机器。 小冰 发表于 2020-6-18 22:03
用IDA看了一下WriteFile, 它调用了WriteConsole, 然后又调用了WriteConsoleInternal, 最后调用CsrClientC ...
win7上不同版本也不一样 小冰 发表于 2020-6-18 22:03
用IDA看了一下WriteFile, 它调用了WriteConsole, 然后又调用了WriteConsoleInternal, 最后调用CsrClientC ...
我在win7上倒是测试成功的,只不过把所有能打的补丁都给打了。至于是什么原因我其实并没有想过,以后再研究,或者你自己研究吧。 tangptr@126.com 发表于 2020-6-20 10:07
我在win7上倒是测试成功的,只不过把所有能打的补丁都给打了。至于是什么原因我其实并没有想过,以后再研 ...
好的,谢谢你。
页:
[1]