跨越标准库

source-pic

群里一个朋友问上面的代码是什么原理,为什么这样就可以输出Hello world!了呢?

这段代码奇怪在哪里呢?它居然没有 #include <stdio.h> 等任何标准库的头文件!好吧,不包含头文件,一样可以使用printf()等函数。头文件的存在不过是告诉编译器去链接哪个符号的,即使没有,编译器会自动链接运行时库来查找这些符号的,只是可能会给出警告而已。然而,细读下去,整段代码居然也没有使用任何标准库的函数。这就比较尴尬了……

标准库和系统调用

为了弄清出其中的原理,首先需要了解C标准库和系统调用之间的关系:不准确地说,C标准库是对系统调用(syscall)的封装,其目的是实现一套跨平台的统一的API。而系统调用(syscall)是操作系统提供的使用户可以访问操作系统内核的一套API。 既然标准库是由系统调用实现的,那么必然存在跨过C标准库直接使用系统调用的方法。但是还有一个阻碍,既然没有标准库(在Linux gcc下为glibc,Windows VC下则为msvcrtXX.dll),动态链接器是怎么查找到系统调用的符号地址的呢?如此,就需要了解可执行文件的载入过程。

编译->汇编->链接->执行,这是每个学习C的人都耳熟能详的过程。以上代码的编译和汇编阶段毫无疑问是没问题的(虽然可能会有Unused variable的警告)。问题是,在没有使用任何外部符号(symbol)的情况下,链接过程中,会链接到别的运行库么?我们可以在编译时打开gcc的--verbose选项显示每个过程的详细信息,或者使用ldd查看生成的二进制程序都链接了哪些运行库。

而在Windows下,Win32程序的在运行时都是要链接到动态运行库ntdll.dllkernel32.dll的。ntdll.dllkernel32.dll是什么呢?

未完待续,先贴个简单的分析

大致过程是:
1. threadEnvironmentBlock(): 利用线程的FS寄存器寻址找到当前线程的TEB结构
2. processEnvironmentBlock(): 从TEB结构体中找到线程所在进程的PEB结构
3. ktv() 5-12: 使用PEB结构体中的flink指针(侵入式链表指针)遍历寻找ntdll.dllkernel32.dll在内存中的地址
4. 13-19: 从kernel32.dll中寻找符号导出表(EXPORT)的地址
5. 20-27: 从符号导出表中利用指针偏移寻找getStdHandle()调用从而找到stdout这个Handle(在linux下叫做file descriptor);寻找Console()调用的函数地址
6. 28-32: 使用已经寻找到的Console()调用向stdout输出”hello world!\n”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
typedef unsigned char * pointer;
typedef unsigned short int uint16;
typedef unsigned int uint32;
typedef uint32 number;
pointer threadEnvironmentBlock(void) {
pointer ptr = 0;
__asm__("mov %%fs[0x18],%0" : "=r"(ptr));
return ptr;
}
pointer processEnvironmentBlock(void) {
return *(pointer *)(threadEnvironmentBlock() + 0x30);
}
pointer ktv(void) {
pointer peb = processEnvironmentBlock();
pointer ldr = *(pointer *)(peb + 0xc);
pointer flink = *(pointer *)(ldr + 0xc);
flink = *(pointer *)flink;
flink = *(pointer *)flink;
pointer dllbase = *(pointer *)(flink + 0x18);
pointer nt = dllbase + *(number *)(dllbase + 0x3c);
if (*(uint16 *)nt == 0x4550) {
pointer EXPORT = dllbase + *(number *)(nt + 0x78);
number length = *(number *)(EXPORT + 0x18);
pointer * str = (pointer *)(dllbase + *(number *)(EXPORT + 0x20));
int ii = 441;
number base = *(number *)(EXPORT + 0x10);
number * address = (number *)(dllbase + *(number *)(EXPORT + 0x1c));
uint16 * index = (uint16 *)(dllbase + *(number *)(EXPORT + 0x24));
pointer(*getStdHandle)(uint32) = (pointer(*)(uint32))(dllbase+(number)address[base + index[ii] - 1]);
pointer strout = getStdHandle((uint32)-11);
//printf("%s %x\n",(dllbase + (number)str[ii]),getStdHandle);
ii = 922;
int (*Console)(void*,char const*,uint32,uint32*,pointer*) =
(int (*)(void*,char const*,uint32,uint32*,pointer*))(dllbase+(number)address[base + index[ii] - 11]);
//printf("%s %x\n",(dllbase + (number)str[ii]),Console);
char * all = "hello world!";
uint32 ddw;
Console(strout, all, 12, &ddw, 0);
Console(strout, "\n", 1, &ddw, 0);
}
}
int main(void) {
ktv();
return 0;
}

参考:
TEB/PEB/PEB_LDR_DATA
https://msdn.microsoft.com/zh-cn/aa813708(v=vs.85))
http://www.weixianmanbu.com/article/76.html
http://www.cnblogs.com/dsky/archive/2012/02/20/2358864.html
http://www.nirsoft.net/kernel_struct/vista/peb.html
http://www.nirsoft.net/kernel_struct/vista/TEB.html
http://www.nirsoft.net/kernel_struct/vista/PEB_LDR_DATA.html

GCC内联汇编
https://linux.cn/article-7688-1.html

LIST_ENTRY (侵入式链表)
https://linux.cn/article-7321-1.html