关于printf格式化的输出的利用,分为两种读和写。把实践在这里总结一下。
根据例子具体学习一下,这个例子是elf32下的,和在elf64下有一点差异分开来说吧。
这是在ghidra下的反汇编,ghidra很好用,有想尝试的同学可以去试一下。
 
逻辑上很简单,secret是一个全局变量,在.bss里面没有初始化,在看一下give_shell的定义
 give_shell应该相当于一个one_gadget,所以这里的基本第一思路是让
 give_shell应该相当于一个one_gadget,所以这里的基本第一思路是让secert == 0x539成立。跳转到give_shell上。 这里也可以再看一下checksec,
 
就开了一个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参数项里面,而且具体的位置是随着这个字符串的大小变的。如果我们想用这个字符串,必须让他在栈上对齐。
这里有一个小方法,我们依次指定输入为 aaaa,aaaaa,aaaaaa....看它什么时候是对齐的。这里并没有从一个a开始,因为4位以上肯定能覆盖一个单元。这里当输入6个a时aaaaaa刚好对齐,所有输入的规律应该为 'aaaaaa'+n*4即后面的数据以4的倍数增长,所以需要根据后面输入情况padding。
这里我们假设的是参数栈上没有secert的地址,所以这里我们要先构造一个secert的地址,然后在用这个构造的secert的地址写。 第一步构造 1
2'\x28\xa0\x04\x08'+'%1333x'+'%100$n'+'aaaaaa'
 --secert-address---1337-4-------- -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函数。 1
2
3
4
5
6
7
8
9
10
11
12
13gef➤  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.call   0x8048330,这个0x8048330其实是puts在plt表上的位置。再去看一下0x8048330是进行的什么过程 1
2
3
4
5
6gef➤  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知识在这里就介绍都这里。 1
2gef➤  x/1x 0x804a010
0x804a010 <[email protected]>:	0x080483360x804a010这个地址存储的具体地址下,如果我们把0x804a010这里地址里面存储的地址换成give_shell不就行了。所以我们的目标是对0x804a010写入give_shell的地址0x804846b.这个写入的数据比较大,不像前面的1337,这里需要分割一下,分割也是有策略。依次从低位写到高位,不能从高位开始写,并且保证写的数据大小也是从小到大。 1
0x804846b =>  0x804 0x84 0x46
2字节和两个1字节。而且数据大小也是从下到大,pwntool也有专门分割函数,但是需要自己padding一下,保证对齐,并指定首参数的偏移量。这里完整叙述一下手工怎么拼。
上面分为了3 个部分,首先根据要写入的地址0x804a010列出这三个地址,从小到大分别为0x804a010,0x804a011,0x804a012,接着直接拼接: 1
'\x10\xa0\x04\x08'+'\x11\xa0\x04\x08'+'\x12\xa0\x04\x08'+'%95x%173$hhn%25x%174$hhn%1920x%175$nAAAAAA'
173,这是相对的,所以不会改变。这里看见我用了两个%hhn,%hhn换成%n也没事,这是只是为了保证AAAAAA`前面字符串长度是4的倍数。接着我们可以在gdb下试一下,看看调用puts是否跳转到了give_shell上。 
可以看到got表上的位置确实变成了give_shell的位置0x0804846b,可以继续执行一下看看 
上面都是elf32 下的printf,在elf64的printf有点不一样,关于参数的寻址是不一样的。去看看printf的具体的结构
| 1 | gef➤ disas printf | 
在elf64中函数传参用前六个参数用传参rdi,rsi,rax,rcx,r8,r9,如果多余的就用在call printf之前push到栈里,这里也可以看到,参数栈也不是连续的,同时也保存了xmm寄存器的状态。所以在计算参数偏移的时候,偏移量应该是5+(目的地址- ret)/8,这里需要注意一下。