关于printf格式化的输出的利用,分为两种读和写。把实践在这里总结一下。
根据例子具体学习一下,这个例子是elf32下的,和在elf64下有一点差异分开来说吧。
这是在ghidra下的反汇编,ghidra很好用,有想尝试的同学可以去试一下。
逻辑上很简单,secret
是一个全局变量,在.bss
里面没有初始化,在看一下give_shell
的定义
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
,这里需要注意一下。