• printf(format) 被调用时,printf 会把栈上的数据当作它的参数
  • 需要修改特定变量,那么就需要知道当前变量存在栈上的哪里,也就是offset是多少
  • 如何探测offset:

    • 例:输入AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
    • 如果在输出中看到了 0x41414141(即 AAAA 的十六进制),数一下它是第几个 %p,如果它是第 7 个出现的,那么你的偏移量就是 7。
  • 原理:

    • format变量内容并不是在当前栈顶的,当前栈顶所存储的是format的变量地址
    • 栈结构:
==== 低地址 (Low Address) | 栈顶方向 ==== 
                      |
                      |  (printf 函数刚被调用,ESP 指向参数列表)
                      V
+---------------------+  <-- ESP (栈顶)
|  format 字符串的地址 |  (这是 printf 的第 0 个参数,它只当剧本读,不当数据写)
+---------------------+  <-- ESP + 4
|   [ printf 参数 1 ]  |  (如果你写 %1$p,看到的就是这里的数据)
+---------------------+  <-- ESP + 8
|   [ printf 参数 2 ]  |  (如果你写 %2$p,看到的就是这里的数据)
+---------------------+
|         ...         |  (这里是栈上的“空隙”,存着一些函数残余数据或局部变量)
+---------------------+
|   [ printf 参数 k-1 ]|
+---------------------+  <-- 🚨 偏移量 k 指向这里!(ebp - 0x6C)
|  "AAAA" (0x41414141)|  <-- 你的输入 format[100] 的开头就在这!
|         或          |  
|  num 的地址 (p32)    |  (改写 num 时,这里放 0x0804A030)
+---------------------+
|  "BBBB" (填充字符)   |  (format 数组的后续部分)
+---------------------+
|   "%k$n" (指令)      |  (format 数组的后续部分)
+---------------------+
|         ...         |  (format[100] 剩下的空间)
+---------------------+  <-- 原本的 ebp - 8
|       p_argc        |  (main 函数定义的局部变量)
+---------------------+  <-- 原本的 ebp + 0
|   旧 EBP (Old EBP)   |  
+---------------------+  <-- 原本的 ebp + 4
|      返回地址        |  
+---------------------+
                      |
==== 高地址 (High Address) | 栈底方向 ====
  • format 数组的起始位置,和 printf 函数调用时压栈的位置,通常不是同一个地方。
  • 在 32 位 Linux 中,由于 main 函数里还有 p_argcsetvbuf 的参数等局部变量压在栈上,你的 format 数组通常被分配在离栈顶有一段距离的地方,并不是直接挨着栈顶放。所以,printf 必须得往后看很多位(例如跳过 108/4 = 27 步左右),才能看到你的 AAAA,所跳过的位也就是偏移量offset

  • 如何修改栈上的目标变量?

    • 在 C 语言的设计中,%n 是一个非常特殊的格式化字符。其他的字符(比如 %d, %s, %x)都是用来输出(读)数据的,唯独 %n 是用来输入(写)数据的。它不打印任何东西,而是把 到目前为止已经打印出来的字符总数,写入到对应的参数(一个指针)所指向的内存地址中
    • 例:
int count = 0;
// printf 遇到 %n 时,会把已经打印的字符数量,写进后面的变量指针里
printf("Hello World!%n", &count);
  • 在这个例子中,printf 打印了 Hello World!(12 个字符)。当它遇到 %n 时,它会寻找下一个参数(也就是 &count 的地址),然后像这样在底层执行:*(&count) = 12;->于是,变量 count 的值就被改写成了 12。
  • !! %n 的工作原理就是“找一个地址,把当前打印的总字数塞进去”。 !!
  • 利用 $k:

    • $ 符号可以直接指定读取第几个参数
    • printf("%3$d", a, b, c);:这里 %3$d 的意思是:“别管前面的 ab,直接给我把第 3 个参数(c)当作整数打印出来。”
  • 于是把 %n(写内存)和 $k(指定位置)结合起来,变成 %k$n。它的意思是:“去栈上的第 k 个参数位置,拿到那里存放的地址,然后把计数值写进那个地址里。