非PIE二进制ASLR环境下,printf@plt泄露GOT条目输出空串的原因
为什么调用printf@plt后输出空字符串?
这完全是printf和puts的核心行为差异导致的:
puts()的逻辑非常直白:它接收一个指针参数,从该指针指向的位置开始输出字节,直到碰到\x00空字节才停止——不管内容是字符串还是二进制值。所以你直接把GOT条目的地址传给puts,它会把GOT里存的libc地址(二进制数)当作字节流输出,哪怕包含不可打印字符,至少能把地址的原始字节打出来(可以通过管道捕获后分析)。printf()是格式化输出函数,它的第一个参数必须是格式字符串。如果你直接把GOT条目的地址作为第一个参数传给printf,它会把这个地址指向的内容(也就是libc里printf的真实地址,是一串二进制值)当作格式字符串解析。这串二进制值里几乎不可能包含%s这类有效的格式占位符,甚至可能自带\x00字节(比如地址里有0的话)直接让printf停止解析,结果自然就是输出空字符串了。
举个32位系统的例子:假设printf@GOT里存的地址是0x08041234,对应的字节是0x34 0x12 0x04 0x08,printf把这四个字节当作格式字符串,里面没有任何%开头的格式符,所以它什么都不会输出。
还能用printf实现地址泄露吗?当然可以!
关键是给printf传递正确的参数组合,让它把你要泄露的地址当作字符串输出。你需要构造类似这样的调用逻辑:
printf("%s", &printf@GOT);
也就是让printf的第一个参数是包含%s的格式字符串,第二个参数是你要泄露的GOT条目地址。
具体在栈溢出场景下的实现思路:
- 在溢出的payload里嵌入
"%s"字符串(不需要刻意加\x00结尾,printf遇到格式符就会处理后续逻辑); - 控制栈/寄存器来构造printf的参数:
- 32位系统(栈传参):按顺序压栈:先压printf的返回地址(比如后续ROP链或exit@plt),再压GOT条目地址(第二个参数),最后压栈上
"%s"的地址(第一个参数),然后跳转到printf@plt; - 64位系统(寄存器传参):用ROP gadget把
"%s"的地址放到rdi寄存器,把GOT条目地址放到rsi寄存器,再跳转到printf@plt。
- 32位系统(栈传参):按顺序压栈:先压printf的返回地址(比如后续ROP链或exit@plt),再压GOT条目地址(第二个参数),最后压栈上
这样printf会按照%s的要求,输出第二个参数指向的内存内容(也就是GOT里的libc地址),直到碰到空字节,你就能拿到libc的基地址了。
另外还有一种变通技巧:如果栈上或内存里已经存在可利用的格式符(比如环境变量里的字符串),你也可以用%x这类格式符来逐字节泄露栈上的内容,间接拿到GOT地址,但这种方式不如直接用%s直观高效。
内容的提问来源于stack exchange,提问作者awerv
相关产品推荐
相关产品推荐

