2023D^3CTF

D^3CTF

有一段时间没摸RE了,最近几天打了铁三决赛和d3ctf作为复建赛。铁三决赛不好评价,CTF赛题质量堪忧,并且数据赛赛中会出标准答案(家人们xxx!),最后几分钟从2掉到4,第一次体验到奖金不够路费XD,能抢返程的车票只能说智行泰库辣。d3ctf体验还是蛮不错的,告别了"llvm加一堆"或是大量堆积某一技术的赛题,从做题角度感觉题目比较精致优雅,🙁Hell的后半段大数运算不好玩。
五月已至,退役ddl,希望还能有机会打qwb和ciscn。

RE

d3sky

TLS_CALLBACK函数中存在反调试和异常处理,正常执行会触发除0异常来修改key为YunZh1JunAlkaid。
image-20230502132136053
main函数主要逻辑是3字节解密opcode,之后通过与非运算,通过与非实现取反、取自身、异或等其他逻辑运算。
image-20230502132146463
输入逐字节读入,可在记录长度处下断之后调试观察对输入的处理,程序主要通过异或操作实现逻辑方程,所以直接对字节码进行处理获取方程,之后用z3求解,并且程序约束最后一个字符为~。
提取方程

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
def Rc4_Encrypt(m):
out = [] # putput
i, j = 0, 0
for p in range(len(m)):
i = (i + 1) % 256
j = (j + s[i]) % 256

s[i], s[j] = s[j], s[i]

index = (s[i] + s[j]) % 256
out.append(s[index] ^ m[p])

return out


def hexdump(m):
for p in m:
print(hex(p)[2:].zfill(2), end=' ')
print()


if __name__ == '__main__':
key = 'YunZh1JunAlkaid'
s = []
t = []
for i in range(256):
s.append(i)
t.append(ord(key[i % len(key)]))
j = 0
for i in range(256):
j = (j + s[i] + t[i]) % 256
s[i], s[j] = s[j], s[i]

enc = [0x009E, 0x0028, 0x00F5, 0x0075, 0x0073, 0x0073, 0x0030, 0x007E, 0x0048, 0x0048, 0x00F2, 0x002F, 0x003D,
0x00EC, 0x0001, 0x0026, 0x003E, 0x00CD, 0x0082, 0x00AD, 0x00B1, 0x00D1, 0x0036, 0x00D2, 0x00B4, 0x00E5,
0x00E8, 0x004C, 0x003D, 0x000C, 0x0073, 0x00FD, 0x0059, 0x00A7, 0x0048, 0x0093, 0x00FD, 0x0006, 0x00E0,
0x0044, 0x0048, 0x0071, 0x0094, 0x004A, 0x008E, 0x00A4, 0x0036, 0x0091, 0x0023, 0x00EE, 0x0068, 0x00C1,
0x005D, 0x000B, 0x004D, 0x001A, 0x0074, 0x0083, 0x0051, 0x0052, 0x00EE, 0x00FE, 0x0011, 0x00A2, 0x00A1,
0x0064, 0x00BD, 0x0098, 0x004D, 0x00B9, 0x0097, 0x0045, 0x00E6, 0x00F7]
ans = Rc4_Encrypt(enc)
hexdump(ans)
print(ans[:37])
print(ans[37:])
op = [...]

for i in range(0, len(op), 3):
tmp = op[i:i + 3]
res = Rc4_Encrypt(tmp)
if (res[0] == res[1] and res[0] >= 0xad4) or res[2] == 0x13:
hexdump(res)
Rc4_Encrypt(tmp)

"""
offset: var
0xad4-0xaf8 mystr
0xaf9 enc
[0xB] = ~(~m[0]&m[1])
[0xC] = ~(~m[1]&m[0])
x:[0x11] = ~([0xB]&[0xC]) #异或

[0xB] = ~(~m[2]&m[3])
[0xC] = ~(~m[2]&m[3])
y:[0x12] = ~([0xB]&[0xC]) #异或

[0xB] = ~(~x&y)
[0xC] = ~(~y&x)
[0x12] = ~([0xB]&[0xC])

[0x13] = [0x12]^enc[0]
"""

线性求解

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
from z3 import *
m=[BitVec("m%d"%i,8) for i in range(37)]
c=[36, 11, 109, 15, 3, 50, 66, 29, 43, 67, 120, 67, 115, 48, 43, 78, 99, 72, 119, 46, 50, 57, 26, 18, 113, 122, 66, 23, 69, 114, 86, 12, 92, 74, 98, 83, 51]
s=Solver()
s.add(m[0]^m[1]^m[2]^m[3]==c[0] )
s.add(m[1]^m[2]^m[3]^m[4]==c[1] )
s.add(m[2]^m[3]^m[4]^m[5]==c[2] )
s.add(m[3]^m[4]^m[5]^m[6]==c[3] )
s.add(m[4]^m[5]^m[6]^m[7]==c[4] )
s.add(m[5]^m[6]^m[7]^m[8]==c[5] )
s.add(m[6]^m[7]^m[8]^m[9]==c[6] )
s.add(m[7]^m[8]^m[9]^m[10]==c[7] )
s.add(m[8]^m[9]^m[10]^m[11]==c[8] )
s.add(m[9]^m[10]^m[11]^m[12]==c[9] )
s.add(m[10]^m[11]^m[12]^m[13]==c[10] )
s.add(m[11]^m[12]^m[13]^m[14]==c[11] )
s.add(m[12]^m[13]^m[14]^m[15]==c[12] )
s.add(m[13]^m[14]^m[15]^m[16]==c[13] )
s.add(m[14]^m[15]^m[16]^m[17]==c[14] )
s.add(m[15]^m[16]^m[17]^m[18]==c[15] )
s.add(m[16]^m[17]^m[18]^m[19]==c[16] )
s.add(m[17]^m[18]^m[19]^m[20]==c[17] )
s.add(m[18]^m[19]^m[20]^m[21]==c[18] )
s.add(m[19]^m[20]^m[21]^m[22]==c[19] )
s.add(m[20]^m[21]^m[22]^m[23]==c[20] )
s.add(m[21]^m[22]^m[23]^m[24]==c[21] )
s.add(m[22]^m[23]^m[24]^m[25]==c[22] )
s.add(m[23]^m[24]^m[25]^m[26]==c[23] )
s.add(m[24]^m[25]^m[26]^m[27]==c[24] )
s.add(m[25]^m[26]^m[27]^m[28]==c[25] )
s.add(m[26]^m[27]^m[28]^m[29]==c[26] )
s.add(m[27]^m[28]^m[29]^m[30]==c[27] )
s.add(m[28]^m[29]^m[30]^m[31]==c[28] )
s.add(m[29]^m[30]^m[31]^m[32]==c[29] )
s.add(m[30]^m[31]^m[32]^m[33]==c[30] )
s.add(m[31]^m[32]^m[33]^m[34]==c[31] )
s.add(m[32]^m[33]^m[34]^m[35]==c[32] )
s.add(m[33]^m[34]^m[35]^m[36]==c[33] )
s.add(m[34]^m[35]^m[36]^m[0]==c[34] )
s.add(m[35]^m[36]^m[0]^m[1]==c[35] )
s.add(m[36]^m[0]^m[1]^m[2]==c[36] )
s.add(m[36]==0x7e)
if s.check() ==z3.sat:
res=s.model()
for i in range(37):
print(chr(res[m[i]].as_long()),end='')
print()

d3recover

ver2版本的pyd没有去符号,并且base64密文可直接解密,推测没有换表,之后用bindiff恢复ver1的符号。
image-20230502132201200
找到可以的check函数,整理主要逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for ( i = 0LL; i <= 31; ++i ){
Item = (_QWORD *)_Pyx_PyInt_From_long(i);
Item = (_QWORD *)_Pyx_PyObject_GetItem(a2, Item);
_Pyx_PyByteArray_Append(v9, v24 ^ 0x23u);
}
for ( j = 0LL; j <= 29; ++j ){
Item = (_QWORD *)_Pyx_PyInt_From_long(j);
v16 = (_QWORD *)_Pyx_PyInt_AddObjC(v10, qword_ABC1E8, 2LL, 0LL, 0LL);
v17 = (_QWORD *)_Pyx_PyObject_GetItem(v9, v16);
v16 = (_QWORD *)PyNumber_Add(Item, v17);
v17 = (_QWORD *)_Pyx_PyInt_AndObjC(v16, qword_ABC220, 255LL, 0LL, 0LL);
v16 = (_QWORD *)_Pyx_PyInt_XorObjC((__int64)v17, qword_ABC218, 0x54LL, 0);
PyObject_SetItem(v9, v10, v16)
}

对可疑base64串解密,并对上述操作求逆即可getflag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import base64
s='08fOyj+E27O2uYDq0M1y/Ngwldvi2JIIwcbF9AfsAl4='
s=base64.b64decode(s)
"""
for i in range(32):
s[i]^=0x23
for j in range(30):
s[i]+=s[i+2]
s[i]&=0x255
s[i]^=0x54
"""
s=list(s)
for i in range(29,-1,-1):
s[i]^=0x54
s[i]=(s[i]-s[i+2]+0x100)&0xff
for i in range(32):
s[i]^=0x23
print(bytes(s))

d3rc4

init_array和fini_array中都添加了处理函数,前者负责解密秘钥和相关数据,后者为真正的flagcheck逻辑。main函数中为假逻辑,创建了全局管道,并且对输入进行了rc4加密,sbox为全局变量。
image-20230502132213127
在fini_array注册的函数中,父程序fork出自程序并通过管道进行通信来对输入进一步加密,考虑到理清进程通信非常的耗时和复杂,所以通过patch程序来进行求解。可知对输入的加密离不开秘钥流keystream,并且秘钥流由单独的函数生成,而在对输入的加密中不会被干扰,所以对输入加密的部分可以随便patch。
image-20230502132225645
程序中存在write函数,所以直接将加密的部分patch为write(1,keystream,len);即可将秘钥流输出到标准输出,之后只需将每次的秘钥流倒叙解密密文,之后再rc4解密即可getflag。

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
write = 0x1040
"""_write(stdout,keystream,len)
.text:0000000000001956 mov edx, slen
.text:000000000000195D lea rsi, keystream ; buf
.text:0000000000001964 mov rdi, 1 ; fd
.text:000000000000196B mov eax, 0
.text:0000000000001970 call _write
"""
f=open(r'dumpkeystream','rb') # 执行 ./d3rc4 > dumpkeystream
buf=f.read()
f.close()
all_xor=[]
print("len mod 36:",len(buf)%36)

for i in range(0,len(buf),36):
t=buf[i:i+36]
all_xor.append(t)

all_xor=all_xor[::-1]

c=[0xF7, 0x5F, 0xE7, 0xB0, 0x9A, 0xB4, 0xE0, 0xE7, 0x9E, 0x05, 0xFE, 0xD8, 0x35, 0x5C, 0x72, 0xE0, 0x86, 0xDE, 0x73, 0x9F, 0x9A, 0xF6, 0x0D, 0xDC, 0xC8, 0x4F, 0xC2, 0xA4, 0x7A, 0xB5, 0xE3, 0xCD, 0x60, 0x9D, 0x04, 0x1F]
c=list(c)
t=[0xDA, 0xE4, 0x26, 0x01, 0xD7, 0xBA, 0x7E, 0xF7, 0xB3, 0x1C, 0x12, 0x3C, 0xDE, 0x34, 0x01, 0x56, 0x4A, 0xF2, 0x77, 0x9A, 0x2F, 0x30, 0xFF, 0x7C, 0x97, 0x7D, 0xAA, 0x06, 0x30, 0x79, 0x06, 0x6D, 0x46, 0xF1, 0x22, 0x3F]
for ks in all_xor[:1]: #调试得知使用一组秘钥流即可
for j in range(0,len(c),2):
c[j+1]=ks[j+1]^c[j+1]
c[j+1]=(c[j]-c[j+1]+0x100)&0xff
c[j]^=ks[j]
c[j]=(c[j]-c[j+1]+0x100)&0xff
if c==t:
print(c)
print(all_xor.index(ks))

print(bytes(c).hex())

image-20230502132255560

d3syscall

注册了init函数,解密出elf文件并释放到/tmp/my_module,之后通过读/proc/kallsyms获取sys_call_table,并通过syscall 313注册该模块出,传入sys_call_table的地址。
image-20230502132311684

main函数通过系统调用分组对输入进行处理,一组处理16字节,而在注册的module中对系统调用进行了hook实现了一个小型的vm。
image-20230502132356684
上述命名并不准确,可见其hook了程序中使用的系统调用,并且模拟了栈,使用了寄存器,并且0x150中实现了左移、异或、加减乘等运算,后续逆向只需解析系统调用,分析其逻辑即可。

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

"""
reg[0]=m[0]
reg[1]=m[1]
data[1]=reg[1]
reg[2]=reg[0]
reg[1]=3
reg[2]<<=reg[1]
reg[1]=0x51E7647E
reg[2]+=reg[1]
reg[3]=reg[0]
reg[1]=3
reg[3]*=reg[1]
reg[1]=0x0E0B4140A
reg[3]+=reg[1]
reg[2]^=reg[3]
reg[3]=reg[0]
reg[1]=0x0E6978F27
reg[3]+=reg[1]
reg[2]^=reg[3]

reg[1]=data[1]
reg[1]+=reg[2]
data[1]=reg[1]
data[2]=reg[0]

reg[2]=reg[1]
reg[0]=6
reg[2]<<=reg[0]
reg[0]=0x53A35337
reg[2]+=reg[0]
reg[3]=reg[1]
reg[0]=5
reg[3]*=reg[0]
reg[0]=0x9840294D
reg[3]+=reg[0]
reg[2]^=reg[3]
reg[3]=reg[1]
reg[0]=0x5EAE4751
reg[3]-=reg[0]
reg[2]^=reg[3]
reg[0]=data[2]
reg[0]+=reg[2]
data[2]=reg[0]

d1=m1+((m0<<3)+0x51E7647E)^(3*m0+0x0E0B4140A)^(m0+0x0E6978F27)
d2=v0+((d1<<6)+0x53A35337)^(5*d1+0x9840294D)^(d1-0x5EAE4751)
"""

code="""
v0=v1+(((v0<<3)+0x51E7647E)^(3*v0+0x0E0B4140A)^(v0+0x0E6978F27))
data[2]=v0+(((v1<<6)+0x53A35337)^(5*v1+0x9840294D)^(v1-0x5EAE4751))
"""
v4=[0]*6
v4[0] = 0xB0800699CB89CC89
v4[1] = 0x4764FD523FA00B19
v4[2] = 0x396A7E6DF099D700
v4[3] = 0xB115D56BCDEAF50A
v4[4] = 0x2521513C985791F4
v4[5] = 0xB03C06AF93AD0BE

from z3 import *
from Crypto.Util.number import *
for i in range(0,6,2):
m0=BitVec('m0',64)
m1=BitVec('m1',64)
d1=m1+(((m0<<3)+0x51E7647E)^(3*m0+0x0E0B4140A)^(m0+0x0E6978F27))
d2=m0+(((d1<<6)+0x53A35337)^(5*d1+0x9840294D)^(d1-0x5EAE4751))
s=Solver()
s.add(d1&0xffffffffffffffff==v4[i])
s.add(d2&0xffffffffffffffff==v4[i+1])
if s.check()==z3.sat:
m=s.model()
print(int.to_bytes(m[m0].as_long(),8,'little').decode(),end='')
print(int.to_bytes(m[m1].as_long(),8,'little').decode(),end='')
else:
print('nnd')

d3Hell

64call32实现smc解密,后续则存在反调试和check patch来控制参数的生成。

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
"""  Hell + SMC
.text:666C164F nop
.text:666C1650 mov dword ptr [ebp-0Ch], offset unk_666C169A
.text:666C1657 mov dword ptr [ebp-8], 0
.text:666C165E mov dword ptr [ebp-8], 1
.text:666C1665 jmp short loc_666C1690
.text:666C1667 ; ---------------------------------------------------------------------------
.text:666C1667
.text:666C1667 loc_666C1667: ; CODE XREF: .text:666C1697↓j
.text:666C1667 mov edx, [ebp-8]
.text:666C166A mov eax, [ebp-0Ch]
.text:666C166D add edx, eax
.text:666C166F mov ecx, [ebp-8]
.text:666C1672 mov eax, [ebp-0Ch]
.text:666C1675 add eax, ecx
.text:666C1677 movzx ecx, byte ptr [eax] //code[i]
.text:666C167A mov eax, [ebp-8]
.text:666C167D lea ebx, [eax-1]
.text:666C1680 mov eax, [ebp-0Ch]
.text:666C1683 add eax, ebx
.text:666C1685 movzx eax, byte ptr [eax] //code[i-1]
.text:666C1688 xor eax, ecx
.text:666C168A mov [edx], al
.text:666C168C add dword ptr [ebp-8], 1
.text:666C1690
.text:666C1690 loc_666C1690: ; CODE XREF: .text:666C1665↑j
.text:666C1690 cmp dword ptr [ebp-8], 249h
.text:666C1697 jle short loc_666C1667
"""

import ida_bytes
c=[...] #op

for i in range(1,len(c)):
c[i]^=c[i-1]
adr=0x666C169A
for i in range(len(c)):
patch_byte(adr+i,c[i])
print('ok')

对dll打patch即可,过掉反调和check patch,结果如下。
image-20230502132413308
调试即可获取解密的字符串,其会更新d3.exe程序中的变量,正确值为0x86928e90099b26cedf3f1f2af783e1。在d3.exe中通过sleep和一些返回恒值的递归函数来耗时,但影响并不算大,最耗时的在sub_401E64及其上层函数,调试可知其通过 (x*x +120) mod m生成数据d,并通过gcd(d,m)来分解m为素因数。

sub_401799是gcd , sub_4040C0是mod等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v0=1
v1=2
x=0x72AE
x=x+1
m=0x86928e90099b26cedf3f1f2af783e1
t=x
while True:
v0+=1
dd=(m+x-(x*x+120))%m
if gp.gcd(dd,m)!=1:
print("get")
break
#print(hex(dd))
x=(x*x+120)%m
# print(hex(x), hex(t))
if t==x :
break
if v0==v1:
t=x
v1*=2
print(x)

所以直接分解698740305822331500978964939673142241,得到两个素因子,后续为将两个素因子的值拼起来即为flag。

d3Tetris

Android游戏题,主要逻辑在GameViewModel类,通过重载的toString函数可知相关变量对应的具体内容。根据题目提供的附件中流量包,可知向192.168.43.57:1234发送了流量包,搜索ip定位关键代码。
image-20230502132427629
其要求分数达到一定值才会发包,所以修改smail代码让其一直发包,并且包的内容与oO0OooOo0oO()和ooooOOOOO00000()函数相关,为libnative导出的函数,并将两者的内容拼接。

1
2
3
System.loadLibrary("native")
private final native byte[] oO0OooOo0oO();
public final native byte[] ooooOOOOO00000();

两个函数均不需要参数所以字符串为内部生成,尝试调试,个人调试过程中在ooooOOOOO00000很容易断下,在oO0OooOo0oO容易崩溃,不过能在首部断下即可修改pc到其他地址(不过这个过程仍不太顺利,pc会跳变回去,可能是一直发包存在线程处理不当的问题)。
调试可知(图未存XD)ooooOOOOO00000函数返回一个16直接的字符串,充当后续aes加密的iv,而oO0OooOo0oO负责加密,为魔改AES_256_CBC和RC4,AES和rc4的key或iv可以调试获取。

加密内容36字节,zer0填充后AES加密了48字节,RC4加密32字节后返回,所以实际上是丢了4字节,也是后来群通知flag补上 “65bd”。

image-20230502132443716
AES256魔改了S盒和加密流程,但具体的处理函数没有变动,伪代码如下。

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
def encrypt(m:bytes,exkey:list):  #魔改
ms=list(m)
round_key(ms,exkey[:16])
for i in range(1,Nr):
byte_sub(ms)
mix_col(ms)
row_shift(ms) #mix和shift的顺序
round_key(ms,exkey[16*i:16*i+16])
byte_sub(ms)
row_shift(ms)
round_key(ms, exkey[16*Nr:])
return bytes(ms)

def decrypt(ec:bytes,exkey:list):
ec=list(ec)
round_key(ec, exkey[16 * Nr:])
inv_row_shift(ec)
inv_sub(ec)
for i in range(Nr-1,0,-1):
round_key(ec, exkey[16 * i:16 * i + 16])
inv_row_shift(ec)
inv_mix_col(ec)
inv_sub(ec)
round_key(ec, exkey[:16])
return bytes(ec)

enc='a6622e62f77ac35c6bf574446d8af6b2a48444f0f78ea1d0dd09c662270874e9'
rc4k='SKJ<HANIOSHDLJKa'
c=bytes.fromhex('43a7e60d237e472964b3b98e08f76cce938078a61bc23b32653865e3af2dfff9')
exk=key_ext(b'A SIMPLE KEY!!!!!!!!!!!!!!!!!!!!')
iv=b'3d354e98963a69b2'
for i in range(0,len(c),16):
res=decrypt(c[i:i+16],exk)
for p in range(16):
print(chr(res[p]^iv[p]),end='')
iv=c[i:i+16]
#dd4ee7c9-031c-4073-bba5-a3efa8fe65bd

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!