首页
社区
课程
招聘
[原创][入门向]以栈的视角来认识格式化字符串漏洞
发表于: 2023-11-29 15:01 9322

[原创][入门向]以栈的视角来认识格式化字符串漏洞

2023-11-29 15:01
9322

格式化字符串漏洞实际上是printf函数的使用不当产生的。首先来看一个正常的printf函数:

可以看到其printf函数由两部分组成,其一是要输出的字符串,而后面是字符串中要解析的参数,在上述例子中为clothesprice

那么printf函数是如何对其进行解析的?我们使用gdb来跟进看看。将上述代码补全,使用gcc format.c -g -m32 -o format编译为32位程序,将断点下到call printf处,如下所示:

此时,我们使用命令stack 0x10,查看栈如下:

我们知道32位程序中,函数的参数是存储在栈上的。从栈上可以看到,printf函数的第0个参数是要输出的字符串本身的地址;第1个参数是shirt字符串的地址,也就是printf要解析的第一个参数;第2个参数是9,也就是printf需要解析的第二个参数。因此我们可以知道,printf函数实际上是以第一个参数为字符串,并按照顺序将传入printf函数的其他的参数以字符串中%开头的形式进行解析。

上面我们已经明白printf函数的解析过程。若printf函数利用不当,便可以使用其进行栈上数据的泄露,如下所示:

上面这段代码中,printf函数只接受一个参数,而且该参数是从标准输入读取的,是可控的。若我们输入%p%p,可以看到如下结果:

可以看到其输出的结果为0xffffd02c0x20,这明显不符合编写代码者的本意。使用gdb跟进,看看这里面发生了什么。

将断点下在call printf

此时使用命令stack,查看栈如下:

到这里我们便能够还原为什么printf会产生上面的输出了:printf仍然把我们传入的数据当作字符串,并将栈上后面的数据以字符串中%开头的方式进行解析。

这意味着,若我们输入的数据为%p%p时,它将会把栈上后面的数据当作传入函数的参数,并以%p的方式解析。例如,printf将栈上的0xffffd014处内容进行解析,并以十六进制格式输出存放在该栈处的值0xffffd02c,接下来再以同样的方式将0xffffd018处的值以%p的方式进行解析,输出0x20。因此,我们可以以这种方式泄露栈上的数据。

上面我们给printf函数传入了%p%p,并以此泄露了栈上的两个数据。那很显然,若我要泄露栈上第20个数据,自然不可能传入20%p。因此,我们可以使用%$[x]p的方式来泄露栈上指定位置的内容。其中,[x]是要泄露的第几个位置。如下所示,我们使用read传入数据%2$p,查看栈如下:

再单步调试一步,获得输出结果为:

从上面可以看待,当我们传入printf的数据为%2$p时,我们实际上可以输出传入printf函数的第2个参数,也就是栈上的0x20。以此类推,我们可以以%7$p的形式输出0x70243225。只需要更改%的输出形式,就可以将栈上内容以任意方式输出,例如%5$p可以将栈上第5个位置以十六进制形式输出,以%6$s可以将栈上第6个位置以字符串形式输出。

通过上面的内容,我们已经得知如何泄露栈上的任意数据。实际上,printf函数同样可以完成写操作,而且是任意位置写。这是用到了printf函数的%n特性,它可以将已经输出的字符数量写到某个地址上。如下所示:

在上面这段代码中,第一个printf首先会输出1234,然后会遇到第一个%n,而此时输出的字符数量为4,因此count1的值将会被写为4。

然后其会遇到%20c,我们知道这实际上是将occupied输出为长度为20的字符,因此目前相当于总共输出了20+4=24个字符。

然后会遇到最后一个%n,由于目前已经输出了24个字符,因此count2会被赋值为24

因此,上面这段代码的输出如下:

那么,对于一个不规范的printf函数,我们可以利用%n来覆盖任意位置的内存,以这段代码为例:

这段代码中,我们可以任意控制printf函数的参数。我们的目标是覆盖secret指向的堆块的值,若我们成功,即可说明我们完成了任意内存覆盖。

同样是下断点到call printfprintf(content)的那个printf),我们先随便输入一点,比如%p%p,查看栈如下:

我们观察到,实际上我们输入的%p%p就存在于栈上,例如上面是在0xffffd02c。那么输入%7$p即可查看这个值,如下:

0x70243725就是%7$p的十六进制形式,因此确实能够索引到这个值。那么,我们当然也可以用%n来向这个位置写值!

我们上面得知,我们输入的数据会放在栈上的第7个位置。因此,若我们输入以下数据:

将断点下到call printf,查看栈如下:

printf函数会先输出p32(addr_of_secret),输出长度为4个字符。然后再解析%7$n,会将已经输出的字符数量写入栈上的第七个值处。而栈上第七个位置是p32(addr_of_secret),因此会将secret指向的堆块的值写为4

同样的,我们可以控制输出的长度,来使得secret指向的值为任意数值,例如我们发送以下数据:

在解析到addr_of_secret时,其已经输出了p32(addr_of_secret)四个字符加上%20c的二十个字符,因此会使得secret指向的值为24

在某些情况,我们希望将整个secret的值覆盖为一个想要的值,我们可以使用如下方式来进行覆盖:

使用以下方式,我们可以更便利地覆盖内存中的值。例如,一个完整的覆盖secret的值为0x12345678payload如下:

通过这个payload可以得到结果:

让我们来一一解析这个payload

首先我们在栈上第7个和第8个位置分别布置了堆地址的低两位和高两位地址。

printf函数首先会输出这两个地址,长度为8个字节。

接下来printf函数会输出%22128c,加起来总共输出了22136个字符,对应十六进制数为0x5678

接下来printf函数会解析到%7$hn,会将已经输出的字符数量以两个字节的形式写入到栈上第七个位置,也就是将0x5678写到堆地址的低两位上。

接下来printf函数会解析到%48060c,会输出48060个字符,和之前22136加起来总共输出了70196个字符,对应十六进制数为0x11234

接下来printf函数会解析到%8$hn,会将已经输出的字符数量以两个字节的形式写入到栈上第八个位置。而目前已经输出了0x11234个字符,因此取两个字节,会将0x1234写入到堆地址的高两位,从而完成了对堆内存空间的覆盖。

从上面这个过程我们得知,可以利用%$hn%$hhn写指定数量字节的特性来对任意内存空间进行覆盖。

64位下最大的差别是:函数的前6个参数位于寄存器上,多余的参数才位于栈上。

而我们知道64位下的前6个参数分别为:rdirsirdxrcxr8r9上。

rdi会保存字符串本身,因此%$1p将会泄露rsi的值,%$2p会泄露rdx的值。以此类推,栈上的第一个值为%6$p

若你已经掌握32位下的格式化字符串利用,了解上述参数构造的不同后与32位下并无差别。

参考链接

ctf-wiki

int price = 9;
char clothes[] = "shirt";
printf("The price of the %s is %d.\n", clothes, price);
int price = 9;
char clothes[] = "shirt";
printf("The price of the %s is %d.\n", clothes, price);
0x56556240 <main+83>     call   printf@plt                    <printf@plt>
       format: 0x56557008 ◂— 'The price of the %s is %d.\n'
       vararg: 0xffffd046 ◂— 'shirt'
0x56556240 <main+83>     call   printf@plt                    <printf@plt>
       format: 0x56557008 ◂— 'The price of the %s is %d.\n'
       vararg: 0xffffd046 ◂— 'shirt'
pwndbg> stack 0x10
00:0000│ esp 0xffffd030 —▸ 0x56557008 ◂— 'The price of the %s is %d.\n'
01:0004│     0xffffd034 —▸ 0xffffd046 ◂— 'shirt'
02:0008│     0xffffd038 ◂— 9 /* '\t' */
03:000c│     0xffffd03c —▸ 0x56556208 (main+27) ◂— add eax, 0x2dcc
04:0010│     0xffffd040 ◂— 9 /* '\t' */
05:0014│     0xffffd044 ◂— 0x6873d104
06:0018│     0xffffd048 ◂— 0x747269 /* 'irt' */
07:001c│     0xffffd04c ◂— 0xa3ffc00
08:0020│     0xffffd050 —▸ 0xffffd070 ◂— 0x1
09:0024│     0xffffd054 ◂— 0x0
0a:0028│ ebp 0xffffd058 ◂— 0x0
0b:002c│     0xffffd05c —▸ 0xf7de4ee5 (__libc_start_main+245) ◂— add esp, 0x10
0c:0030│     0xffffd060 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c
0d:0034│     0xffffd064 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c
0e:0038│     0xffffd068 ◂— 0x0
0f:003c│     0xffffd06c —▸ 0xf7de4ee5 (__libc_start_main+245) ◂— add esp, 0x10
pwndbg>
pwndbg> stack 0x10
00:0000│ esp 0xffffd030 —▸ 0x56557008 ◂— 'The price of the %s is %d.\n'
01:0004│     0xffffd034 —▸ 0xffffd046 ◂— 'shirt'
02:0008│     0xffffd038 ◂— 9 /* '\t' */
03:000c│     0xffffd03c —▸ 0x56556208 (main+27) ◂— add eax, 0x2dcc
04:0010│     0xffffd040 ◂— 9 /* '\t' */
05:0014│     0xffffd044 ◂— 0x6873d104
06:0018│     0xffffd048 ◂— 0x747269 /* 'irt' */
07:001c│     0xffffd04c ◂— 0xa3ffc00
08:0020│     0xffffd050 —▸ 0xffffd070 ◂— 0x1
09:0024│     0xffffd054 ◂— 0x0
0a:0028│ ebp 0xffffd058 ◂— 0x0
0b:002c│     0xffffd05c —▸ 0xf7de4ee5 (__libc_start_main+245) ◂— add esp, 0x10
0c:0030│     0xffffd060 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c
0d:0034│     0xffffd064 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c
0e:0038│     0xffffd068 ◂— 0x0
0f:003c│     0xffffd06c —▸ 0xf7de4ee5 (__libc_start_main+245) ◂— add esp, 0x10
pwndbg>
char content[0x20];
read(0, content, 0x20);
printf(content);
char content[0x20];
read(0, content, 0x20);
printf(content);
ltfall@ubuntu:/pwn/myhow2heap/formatstring$ ./format
%p%p
0xffffd02c0x20
ltfall@ubuntu:/pwn/myhow2heap/formatstring$ ./format
%p%p
0xffffd02c0x20
0x56556253 <main+70>    call   printf@plt                    <printf@plt>
       format: 0xffffd02c ◂— 0x70257025 ('%p%p')
       vararg: 0xffffd02c ◂— 0x70257025 ('%p%p')
0x56556253 <main+70>    call   printf@plt                    <printf@plt>
       format: 0xffffd02c ◂— 0x70257025 ('%p%p')
       vararg: 0xffffd02c ◂— 0x70257025 ('%p%p')
pwndbg> stack
00:0000│ esp     0xffffd010 —▸ 0xffffd02c ◂— 0x70257025 ('%p%p')
01:0004│         0xffffd014 —▸ 0xffffd02c ◂— 0x70257025 ('%p%p')
02:0008│         0xffffd018 ◂— 0x20 /* ' ' */
03:000c│         0xffffd01c —▸ 0x56556228 (main+27) ◂— add ebx, 0x2da8
04:0010│         0xffffd020 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c
05:0014│         0xffffd024 —▸ 0xf7fe22f0 ◂— endbr32
06:0018│         0xffffd028 ◂— 0x0
07:001c│ eax ecx 0xffffd02c ◂— 0x70257025 ('%p%p')
pwndbg>
pwndbg> stack
00:0000│ esp     0xffffd010 —▸ 0xffffd02c ◂— 0x70257025 ('%p%p')
01:0004│         0xffffd014 —▸ 0xffffd02c ◂— 0x70257025 ('%p%p')
02:0008│         0xffffd018 ◂— 0x20 /* ' ' */
03:000c│         0xffffd01c —▸ 0x56556228 (main+27) ◂— add ebx, 0x2da8
04:0010│         0xffffd020 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c
05:0014│         0xffffd024 —▸ 0xf7fe22f0 ◂— endbr32
06:0018│         0xffffd028 ◂— 0x0
07:001c│ eax ecx 0xffffd02c ◂— 0x70257025 ('%p%p')
pwndbg>
stack
00:0000│ esp     0xffffd010 —▸ 0xffffd02c ◂— 0x70243225 ('%2$p'// 这是第0个参数
01:0004│         0xffffd014 —▸ 0xffffd02c ◂— 0x70243225 ('%2$p'// 这是第1个参数
02:0008│         0xffffd018 ◂— 0x20 /* ' ' */                     // 这是第2个参数
03:000c│         0xffffd01c —▸ 0x56556228 (main+27) ◂— add ebx, 0x2da8        // 这是第3个参数
04:0010│         0xffffd020 —▸ 0xf7fb0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1e9d6c // 这是第4个参数
05:0014│         0xffffd024 —▸ 0xf7fe22f0 ◂— endbr32                          // 这是第5个参数
06:0018│         0xffffd028 ◂— 0x0                                            // 这是第6个参数
07:001c│ eax ecx 0xffffd02c ◂— 0x70243225 ('%2$p')                            // 这是第7个参数

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2023-11-30 22:55 被Ltfall编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (3)
雪    币: 3573
活跃值: (31026)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2023-11-30 10:28
1
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
受益匪浅
2023-11-30 23:00
0
雪    币: 229
活跃值: (309)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享,一步一看好理解多了
2023-12-19 13:54
0
游客
登录 | 注册 方可回帖
返回
//