找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3944|回复: 12

【C】只导入ntdll.dll的Hello World

[复制链接]
发表于 2020-5-24 15:17:44 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
一个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段拿到进程环境块的地址,进而取得进程参数的结构体指针。拿到进程参数后,直接取出那三个句柄即可,代码如下:
  1. HANDLE StdIn=NULL;
  2. HANDLE StdOut=NULL;
  3. HANDLE StdErr=NULL;
  4. PPEB Peb=NULL;
  5. PRTL_USER_PROCESS_PARAMETERS ProcessParameters=NULL;

  6. void Init()
  7. {
  8.         Peb=(PPEB)__readtebptr(TEB_PEB_OFFSET);
  9.         ProcessParameters=Peb->ProcessParameters;
  10.         StdIn=ProcessParameters->StandardInput;
  11.         StdOut=ProcessParameters->StandardOutput;
  12.         StdErr=ProcessParameters->StandardError;
  13. }
复制代码

StdIn, StdOut, StdErr三个变量赋值以后,我们就可以读写控制台了。
接下来我们就需要实现printf了,这个实现其实很简单,先把完整的字符串用sprintf之类的函数打印出来,再用NtWriteFile输出到控制台上。
ntdll.dll中有个导出函数叫_vsnprintf,正适合我们格式化字符串,尤其是它还能防止栈溢出。通常来说,512字节够我们使了,代码如下:
  1. #define PRINT_BUFFER_SIZE        512

  2. int __cdecl ntprintf(const char* format,...)
  3. {
  4.         IO_STATUS_BLOCK iosb={0};
  5.         int len;
  6.         char buff[PRINT_BUFFER_SIZE];
  7.         va_list args;
  8.         va_start(args,format);
  9.         len=_vsnprintf(buff,PRINT_BUFFER_SIZE,format,args);
  10.         if(len>0)NtWriteFile(StdOut,NULL,NULL,NULL,&iosb,buff,len,NULL,NULL);
  11.         va_end(args);
  12.         return len;
  13. }
复制代码

如果嫌不够就加大PRINT_BUFFER_SIZE这个值。
那么程序的入口函数就需要先Init(),再ntprintf(),代码如下:
  1. void Main()
  2. {
  3.         Init();
  4.         ntprintf("Hello Native Console!\n");
  5. }
复制代码

将其编译并运行,效果如下:
ntcmd_test.JPG
拖进IDA,可以发现已经导入表里只有ntdll.dll:
ntcmd.PNG
不过即便是只导入ntdll.dll,系统依然会加载kernel32.dll。由于枚举模块列表可以通过直接遍历LDR双向链表来实现,不需要API,代码如下:
  1. void PrintLdrList()
  2. {
  3.         PLDR_DATA_TABLE_ENTRY pLdr=(PLDR_DATA_TABLE_ENTRY)PebLdrData->InLoadOrderModuleList.Flink;
  4.         PLDR_DATA_TABLE_ENTRY tLdr=pLdr;
  5.         do
  6.         {
  7.                 ntprintf("Base: 0x%p Size: 0x%08X Name: %wZ\t Path: %wZ\n",pLdr->DllBase,pLdr->SizeOfImage,&pLdr->BaseDllName,&pLdr->FullDllName);
  8.                 pLdr=(PLDR_DATA_TABLE_ENTRY)pLdr->InLoadOrderLinks.Flink;
  9.         }while(pLdr!=tLdr);
  10. }
复制代码

然后修改Init和Main函数:
  1. PPEB_LDR_DATA PebLdrData=NULL;

  2. void Init()
  3. {
  4.         Peb=(PPEB)__readtebptr(TEB_PEB_OFFSET);
  5.         ProcessParameters=Peb->ProcessParameters;
  6.         PebLdrData=Peb->Ldr;
  7.         StdIn=ProcessParameters->StandardInput;
  8.         StdOut=ProcessParameters->StandardOutput;
  9.         StdErr=ProcessParameters->StandardError;
  10. }

  11. void Main()
  12. {
  13.         Init();
  14.         PrintLdrList();
  15. }
复制代码

编译并运行:
ntcmdldr.JPG
似乎就可以得出结论:即便程序的整个导入表树上没有kernel32.dll,系统仍然会加载kernel32.dll。
最后再实现个阻塞控制台吧,代码如下:
  1. void Pause()
  2. {
  3.         IO_STATUS_BLOCK iosb={0};
  4.         char k;
  5.         NtWriteFile(StdOut,NULL,NULL,NULL,&iosb,"Press Enter key to continue...",30,NULL,NULL);
  6.         NtReadFile(StdIn,NULL,NULL,NULL,&iosb,&k,1,NULL,NULL);
  7. }
复制代码

在入口函数的结尾处调用刚写的Pause函数,其中NtReadFile的调用会阻塞住控制台,双击运行能看到控制台,结果如下:
ntcmdpause.JPG


结语
由于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即可。

NtdllConsole.zip

7.08 MB, 下载次数: 8

下载源码不回帖是一种很欠扁的行为

回复

使用道具 举报

发表于 2020-5-24 19:39:42 | 显示全部楼层
俺自己捏导入库惹,用到啥就加啥
回复 赞! 靠!

使用道具 举报

发表于 2020-5-24 22:34:22 | 显示全部楼层
可以自己申请控制台 打开 conin$ conout$获取 stdin 和 stdout句柄
回复 赞! 靠!

使用道具 举报

发表于 2020-5-25 06:42:20 | 显示全部楼层
没看懂纠结是否加载KERNEL32有啥意义。。。
不过简单WIN32小程序用WDK7编译是很好的,它会直接导入MSVCRT.DLL,编译出来的EXE/DLL非常小。
回复 赞! 靠!

使用道具 举报

发表于 2020-5-25 10:03:31 | 显示全部楼层
学习一下了。
回复 赞! 靠!

使用道具 举报

发表于 2020-5-26 14:56:09 | 显示全部楼层
美俪女神 发表于 2020-5-25 06:42
没看懂纠结是否加载KERNEL32有啥意义。。。
不过简单WIN32小程序用WDK7编译是很好的,它会直接导入MSVCRT.D ...

很多古代的程序对kernel32.dll的版本有要求,比如有的就只能加载Win95和WinME的kernel32.dll。

虽说我们现在可以不需要担心因为依赖了kernel32.dll而出现对系统版本的过分要求,但如果不依赖,似乎更容易在那些只会玩eXeScope的小朋友圈子里成为爸爸。

以及有些环境(具体记不得了)你不一定有Kernel32.dll可用
回复 赞! 靠!

使用道具 举报

发表于 2020-6-2 18:12:02 | 显示全部楼层
顶一下 原来这些函数r3 也可以用
回复 赞! 靠!

使用道具 举报

发表于 2020-6-18 16:10:48 | 显示全部楼层
本帖最后由 小冰 于 2020-6-18 16:15 编辑

我正确获取到了STDOUT的句柄, 使用下面的方式
  1. #define STD_OUTPUT_HANDLE_INDEX 3UL
  2. HANDLE hStdOut = ((PPEB)RtlGetCurrentPeb())->ProcessParameters->Reserved2[STD_OUTPUT_HANDLE_INDEX];
复制代码


hStdOut的值和调用GetStdHandle(STD_OUTPUT_HANDLE)返回的值是一个值。

但问题就出在输出
  1. PCSTR str = "Hello World!";
  2. 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来输出。
  1. WriteFile(hStdOut, str, strlen(str), NULL, NULL);
复制代码


但是什么原因导致NtWriteFile不能输出呢?
您的这个例子我下载到我的电脑中编译执行也输出不了。

下面是我所有的代码:

  1. #ifndef WIN32_LEAN_AND_MEAN
  2. #define WIN32_LEAN_AND_MEAN
  3. #endif

  4. #include <Windows.h>
  5. #include <winternl.h>

  6. #pragma comment(lib, "ntdll.lib")

  7. NTSYSAPI PVOID NTAPI RtlGetCurrentPeb();

  8. #define STD_OUTPUT_HANDLE_INDEX 3UL

  9. NTSYSAPI NTSTATUS NTAPI NtWriteFile(
  10.   HANDLE           FileHandle,
  11.   HANDLE           Event,
  12.   PIO_APC_ROUTINE  ApcRoutine,
  13.   PVOID            ApcContext,
  14.   PIO_STATUS_BLOCK IoStatusBlock,
  15.   PVOID            Buffer,
  16.   ULONG            Length,
  17.   PLARGE_INTEGER   ByteOffset,
  18.   PULONG           Key
  19. );

  20. int main(void) {
  21.   HANDLE hStdOut = ((PPEB)RtlGetCurrentPeb())->ProcessParameters->Reserved2[STD_OUTPUT_HANDLE_INDEX];
  22.   IO_STATUS_BLOCK iosb = {NULL};
  23.   PCSTR str = "Hello World!";
  24.   NTSTATUS status = NtWriteFile(hStdOut, NULL, NULL, NULL, &iosb, str, strlen(str), NULL, NULL);
  25.   //WriteFile(hStdOut, str, strlen(str), NULL, NULL);
  26.   return 0;
  27. }
复制代码


我的电脑是windows 7 64位。
回复 赞! 靠!

使用道具 举报

发表于 2020-6-18 19:48:25 | 显示全部楼层
小冰 发表于 2020-6-18 16:10
我正确获取到了STDOUT的句柄, 使用下面的方式
[code]#define STD_OUTPUT_HANDLE_INDEX 3UL
H ...


因为控制台没经过ntwritefile 走应该是写指针 然后 ntRequestWaitReplyPort来着
回复 赞! 靠!

使用道具 举报

发表于 2020-6-18 22:03:06 | 显示全部楼层
Ayala 发表于 2020-6-18 19:48
因为控制台没经过ntwritefile 走应该是写指针 然后 ntRequestWaitReplyPort来着

用IDA看了一下WriteFile, 它调用了WriteConsole, 然后又调用了WriteConsoleInternal, 最后调用CsrClientCallServer给CSRSS发消息。不过我好奇,为啥在唐的机器上可以输出,是只有在win10上才有效?我手头上没有win10的机器。
回复 赞! 靠!

使用道具 举报

发表于 2020-6-19 18:48:29 | 显示全部楼层
小冰 发表于 2020-6-18 22:03
用IDA看了一下WriteFile, 它调用了WriteConsole, 然后又调用了WriteConsoleInternal, 最后调用CsrClientC ...

win7上不同版本也不一样
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2020-6-20 10:07:35 | 显示全部楼层
小冰 发表于 2020-6-18 22:03
用IDA看了一下WriteFile, 它调用了WriteConsole, 然后又调用了WriteConsoleInternal, 最后调用CsrClientC ...

我在win7上倒是测试成功的,只不过把所有能打的补丁都给打了。至于是什么原因我其实并没有想过,以后再研究,或者你自己研究吧。
回复 赞! 靠!

使用道具 举报

发表于 2020-6-23 11:12:01 | 显示全部楼层
tangptr@126.com 发表于 2020-6-20 10:07
我在win7上倒是测试成功的,只不过把所有能打的补丁都给打了。至于是什么原因我其实并没有想过,以后再研 ...

好的,谢谢你。
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-12-26 23:51 , Processed in 0.036858 second(s), 27 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表