php内核学习(1)

PG:存放全局变量

SG:存放SAPI所需的全局变量

CG:存放compiler需要的全局变量

EG:存放执行器需要的全局变量

其他的前置知识可以看我之前的先知文章https://xz.aliyun.com/t/7330

如果我们在php中执行phpinfo(),php底层到底是怎么处理的呢,毫无疑问,第一步肯定是先输入然后执行,再输出。那底层究竟做了什么呢。

新建一个php文件

1
<?=phpinfo();?>

现在来看下他的处理过程吧

调用函数栈

执行函数顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
php_output_op output.c:1073
php_output_write output.c:252
php_output_stack_pop output.c:1234
php_output_end output.c:325
zif_phpinfo info.c:1257 //
ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER zend_vm_execute.h:1240//如果是自定义,调用自定义函数,如果是系统函数,则调用系统函数
execute_ex zend_vm_execute.h:51845//使用while循环执行文件中的函数
zend_execute zend_vm_execute.h:56139//zend虚拟机执行,将文件中的数据pull到栈上去
zend_execute_scripts zend.c:1661//按照prepend_file,primary_file,append_file来顺序编译,执行文件
php_execute_script main.c:2584//如果设置了auto_append_file,那么就将此文件加载进去目前执行的文件
do_cli php_cli.c:956//加载php文件路径,将文件信息加载进入zend_file_handle中
main php_cli.c:1351//加载php.ini进入PG
start 0x00007fff70c1d7fd
start 0x00007fff70c1d7fd

一、加载汇编

进入main函数入口

img

二、进入main()

img

这里具体初始化了哪些函数可以看下面sapi_module的结构体内容吧。

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
49
50
51
52
53
54
struct _sapi_module_struct {  // SAPI模块结构
char *name; // 应用层名称,比如cli,cgi等
char *pretty_name; // 应用层更易读的名字,比如cli对应的就是Command Line Interface

int (*startup)(struct _sapi_module_struct *sapi_module); // 当一个应用要调用php的时候,这个模块启动的时候会调用的函数
int (*shutdown)(struct _sapi_module_struct *sapi_module); // 当一个应用要调用php的时候,这个模块结束的时候会调用的函数

int (*activate)(void); // 在处理每个request的时候,激活需要调用的函数
int (*deactivate)(void); // 在处理完每个request的时候,收尾时候要调用的函数

size_t (*ub_write)(const char *str, size_t str_length); // 这个函数告诉php如何输出数据
void (*flush)(void *server_context); // 提供给php的刷新缓存的函数指针
zend_stat_t *(*get_stat)(void); // 用来判断要执行文件的权限,来判断是否有执行权限
char *(*getenv)(char *name, size_t name_len); // 获取环境变量的方法

void (*sapi_error)(int type, const char *error_msg, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3); // 错误处理方法

int (*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op, sapi_headers_struct *sapi_headers); // 这个函数会在我们调用header()的时候被调用
int (*send_headers)(sapi_headers_struct *sapi_headers); // 发送所有的header
void (*send_header)(sapi_header_struct *sapi_header, void *server_context); // 单独发送某一个header

size_t (*read_post)(char *buffer, size_t count_bytes); // 如何获取HTTP POST中的数据
char *(*read_cookies)(void); // 如何获取cookie中的数据

void (*register_server_variables)(zval *track_vars_array); // 这个函数可以给$_SERVER中获取变量
void (*log_message)(char *message, int syslog_type_int); // 输出错误信息函数
double (*get_request_time)(void); // 获取请求时间的函数
void (*terminate_process)(void); // TODO: 调用exit的时候调用的方法

char *php_ini_path_override; // PHP的ini文件被复写了所复写的地址

void (*default_post_reader)(void); // 这里和前面的read_post有个差别,read_post负责如何获取POST数据,而这里的函数负责如何解析POST数据
void (*treat_data)(int arg, char *str, zval *destArray); // 对数据进行处理,比如进行安全过滤等。 default_post_reader/tread_data/input_filter是三个能对输入进行过滤和处理的函数
char *executable_location; // 执行的地理位置

int php_ini_ignore; // 是否不使用任何ini配置文件,比如php -n 就将这个位置设置为1
int php_ini_ignore_cwd; // 不在当前路径寻找php.ini

int (*get_fd)(int *fd); // 获取执行文件的fd

int (*force_http_10)(void); // 强制使用http1.0

int (*get_target_uid)(uid_t *); // 获取执行程序的uid
int (*get_target_gid)(gid_t *); // 获取执行程序的gid

unsigned int (*input_filter)(int arg, char *var, char **val, size_t val_len, size_t *new_val_len); // 对输入进行过滤。比如将输入参数填充到自动全局变量$_GET, $_POST, $_COOKIE中

void (*ini_defaults)(HashTable *configuration_hash); // 默认的ini配置
int phpinfo_as_text; // 是否打印phpinfo信息

char *ini_entries; // 有没有附带的ini配置,比如使用php -d date.timezone=America/Adak,可以在命令行中设置时区
const zend_function_entry *additional_functions; // 每个SAPI模块特有的一些函数注册,比如cli的cli_get_process_title
unsigned int (*input_filter_init)(void); // TODO:
};

do_cli()

这是整个默认配置加载完成后调用do_cli(),在此函数中,系统获取之前传入的文件名,然后将文件名加载进入zend_file_handle结构体的filename中。

img

而后会调用php_request_startup()来执行zend引擎初始化

img

php_execute_script

我们继续进入到php_execute_script()函数在此函数中将初始化文件

img

如果在php.ini中设置了auto_prepend_file则加载其设置的文件,也可以在,user.ini中设置。

img

在这些文件中,prepend_file_p是之前auto_prepend_file设置的文件,此处注意他们的顺序,进入到zend_execute_scripts()函数中去

zend_execute_scripts()

img

在此处使用了一个for循环,而循环文件到顺序就是之前加载的顺序,这里就可以解释上传漏洞中.user.ini照成的文件rce了,而在for循环中,先进入zend_compile_file()来编译函数,然后通过zend_execute()来执行前面编译完成的opcode。

info.c

在zend_execute()中则一行,一行opcode来进行执行,当检测到phpinfo()时,判断phpinfo()是一个内置函数,于是调用internal_function.handler()来进行执行,然后就进入了info.c的PHP_FUNCTION(phpinfo)了,下面是phpinfo()中的三个函数

img

output.c

然后进入php_output_start_default()来进行初始化,进入php_print_info()来输出消息

img

以上就是整个php执行phpinfo()的全过程了