格式化字符串利用¶
格式化字符串的基本格式为%[parameter][flags][field width][.precision][length]type。
由于按照x86-64调用约定,前6个整型参数会通过寄存器传递,而后续的参数会通过栈传递,因此在没有传递足够参数的情况下,格式化字符串函数会继续读取寄存器和栈上的内容。通过这种方式,可以读到栈上的数据,从而实现信息泄露等攻击。
利用%?$?语法打印栈上内容¶
利用parameter字段,可以通过%n$?格式说明符来指定读取第n个参数,这在printf没有其他参数时非常有用,能够直接打印出栈上的内容。
参考x86寄存器的内容,函数调用的前6个整型参数会依次存放在寄存器rdi、rsi、rdx、rcx、r8、r9中,之后的参数会依次存放在栈上。rdi是格式字符串,计数从rsi开始,通过指定n>=6,可以读取到栈上的内容。结合p类型,可以以地址的格式(如0x114514191981)打印出栈上的值,方便利用。
例如:
这时候,printf会打印出被视为第7个参数的值,即栈上的第一个值。
利用%n写入任意地址¶
Format String / 利用 § 覆盖内存 — CTF Wiki
%n格式说明符不输出内容,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
例如:
- 这时候count的值会变成13,因为
Hello, World!有13个字符。
除%n(写入4字节)外,还有%hn(写入2字节)、%hhn(写入1字节)等变种,可以根据需要选择使用。
利用%s打印任意地址内容¶
%s格式说明符会把对应的参数视为字符串指针,然后打印出该地址开始的字符串内容,直到遇到\x00结束,可以在往栈上写入想要的地址之后针对性地打印出该地址的内容。
例如对于printf(s);,我们可以构造s为%?$s的形式,并对齐到相应的字节数,使得在格式化字符串的末尾附加的要打印的目标内存地址存储在相应的位置上,就可以打印出该地址指向的字符串内容。
在x86-64架构下,泄露GOT表上的函数地址时,一般把地址放在格式化字符串的最后,因为地址以\x00结尾,会导致printf截断输出。
例如,在x86-64架构下:
用AAAA作为填充,<address>会作为printf的后续参数被压入栈中。因为<address>存储在了栈上接下来的第2个参数位置(即相对printf的第7个参数),所以%7$s会打印出<address>处开始的字符串内容。如果<address>处存储的是某个GOT表项的地址,就可以借此打印出该函数的实际地址。