本来是一名pwn选手。可能因为疫情的关系,最近不务正业喜欢上了搞逆向,真的是消磨时间的神器,一道题玩一天不是梦,而且感觉看反汇编代码更有感觉了XD。题目都放在附件里了,对于大佬来说应该都不难,不过对于我这种基础差的还是比较有挑战性的。
比赛持续三天,一共11道逆向,赛后我又连着肝了两天,感觉踏上了修仙的路,一共搞出来九道半的题目,还有一个太难了。下面分享一下我的解题思路。
VM题目,而且是极其规范的那种,所以直接用angr梭哈了:
import angr proj = angr.Project("./vm",auto_load_libs=False) st = proj.factory.entry_state() sm = proj.factory.simulation_manager(st) sm.explore(find=0x400b73, avoid=0x400b81) print(sm.found[0].posix.dumps(0))
flag{vm_is_not_easy}
可以对文件进行加密,不过测试发现只能加密16个字节,后面都是明文。程序被ollvm混淆过,本来想用现成的脚本解混淆,结果angr版本总是有问题。后来直接IDA里硬看,其实也很容易找到逻辑,AES的ECB加密而已,flag最后是个png。
from Crypto.Cipher import AES with open("flag.lck") as f: tmp = f.read() end = tmp[16:] tmp = tmp[:16] print len(end), len(tmp) key = 'thisisthekey!!!!' cipher = AES.new(key, AES.MODE_ECB) res = cipher.decrypt(tmp) with open("res", "a+") as r: r.write(res+end) #print cipher.decrypt()
flag{3ff32148-e229-41fd-b7b9-d09e76d35daf}
日常消费卢老爷,程序是用QT写的,一开始OD里找不到关键点,所以我就跑到IDA里慢慢找,发现0x402150
是关键函数。然后OD调到对应位置,输入”lubenwei“,“abcd”,在经过一些处理后有加密结果,怎么加密的并不用关注,只要看到最后的比较过程就可以了。看到此时EDI是我们的输入转化而成,EAX就是对应的结果,直接输入这段结果就行。
flag{41d26f00}
程序逻辑大致为把输入进行处理作为key,之后对key进行某些验证,然后对一段密文进行xxtea解密。其他的部分并不用关心,因为这些本来就不是我们控制的。sub_7FF6E5231890
找到对key进行判断的算法,只要让其绕过you are wrong!
这里就行了,后面的不用关注。
把算法模拟了出来,爆破就可以找到key,这道题比赛的时候本来可以做出来,不过因为比赛的时候只爆了八个字节,结果绕过了检测导致后面解密失败,之后才发现原来是九个.
# from pwn import * # import struct a = "AB AA AA 3E 39 8E E3 3D 39 8E 63 3E 39 8E E3 3D 39 8E E3 3D 39 8E E3 3D" a2 = "00 00 00 00 AB AA AA 3E 39 8E E3 3E AB AA 2A 3F 72 1C 47 3F 39 8E 63 3F" xmm1 = ['0x3eaaaaab', '0x3de38e39', '0x3e638e39', '0x3de38e39', '0x3de38e39', '0x3de38e39'] xmm2 = ['0x0', '0x3eaaaaab', '0x3ee38e39', '0x3f2aaaab', '0x3f471c72', '0x3f638e39'] f1, f2 = [], [] f1 = [0.33333334326744, 0.11111111193895, 0.22222222387791, 0.11111111193895, 0.11111111193895, 0.11111111193895] f2 = [0.0, 0.33333334326744, 0.44444444775581, 0.66666668653488,0.77777779102325, 0.88888889551163] print(f1) print(f2) t = "8ab697" def calc(ctx): res = 0.0 v1 = 1.0 for i in ctx: tmp = v1 #v1 = v1 * f1[t.index(i)-1] #res += tmp * f2[t.index(i)-1] v1 = v1 * f1[t.index(i)] res += tmp * f2[t.index(i)] return res def find_key(): res = 0.0 ctx = "" for i1 in t: for i2 in t: for i3 in t: for i4 in t: for i5 in t: for i6 in t: for i7 in t: for i8 in t: for i9 in t: ctx = i1+i2+i3+i4+i5+i6+i7+i8+i9 ctx = ctx.replace(" ", "") res = calc(ctx) if abs(res-0.129556) <= 0.000001: print ctx find_key()
会出现好几个结果,最后在输出下断点依次尝试,最后发现8ab86897
是正确的:
flag{8ab86897-25c9-811a-ce9a-18547ae6801e}
真 病毒分析,模拟DNA的序列配对,其实是在做简单的单表替换。最后三个字符一组会产生固定的字符串,但是固定的字符串对应的输入可能有多个,这里需要爆破。
这个写得有点粗糙,可能有的情况被不小心遗漏了,而且速度很慢了.
from hashlib import md5 b = "Met Cys Leu Ala Arg Leu Phe Ser Ile Leu Asn Val Cys Gly Lys Leu" def re2(c): l1 = "AUGC" l2 = "UACG" res = "" for i in c: res += l1[l2.index(i)] return res def ap(a): res = [] res.append(a+"A") res.append(a+"U") res.append(a+"C") res.append(a+"G") return res r = [] r.append(["UAC"]) r.append(["ACA","ACG"]) r.append(ap("GA")+["AAC", "AAU"]) r.append(ap("CG")) r.append(ap("GC")) r.append(ap("GA")+["AAC", "AAU"]) r.append(["AAA","AAG"]) r.append(ap("AG")) r.append(["UAA", "UAG", "UAU"]) r.append(ap("GA")+["AAC", "AAU"]) r.append(["UUA","UUG"]) r.append(ap("CA")) r.append(["ACA", "ACG"]) r.append(ap("CC")) r.append(["UUU", "UUC"]) r.append(ap("GA")+["AAC", "AAU"]) r.append(["UAA", "UAG", "UGA"]) # brute # for i0 in r[0]: # ... # if md5(xxx).hexdigest()[0:8] == "e03657e0":
flag{AUGUGCCUUGCAAGACUUUUCUCGAUACUUAACGUCUGUGGAAAACUUUAA}
安装过程中会要求输入密码,不过即使错误也可以解压,解压后的文件是NES模拟器,就是PC机上模拟FC游戏的,还带了个赤色要塞文件hhh,应该是在安装包里搞了一些操作,有点难调。
后来发现这个东西叫NSIS,file命令可查,用1505的7zip就可以提出来脚本。
key是明文,不过也没啥用:
NSIISSOEASY
nsis的脚本跟汇编类似,所以也能明白个大概:
Function func_429 Pop $9 StrCpy $3 "" StrCpy $0 $9 StrCpy $1 0 label_433: StrCpy $2 $0 1 $1 StrCmp $2 "" label_443 Push $2 Call func_445 Pop $2 IntOp $2 $2 ^ 1 IntFmt $2 %c $2 IntOp $1 $1 + 1 StrCpy $3 $3$2 Goto label_433 label_443: Push $3 FunctionEnd Function func_445 Exch $0 ; Push $0 ; Exch ; Pop $0 Push $1 Push $2 StrCpy $2 1 label_451: IntFmt $1 %c $2 StrCmpS $1 $0 0 label_455 StrCpy $0 $2 Goto label_458 label_455: IntOp $2 $2 + 1 StrCmp $2 255 0 label_451 StrCpy $0 0 label_458: Pop $2 Pop $1 Exch $0 ; Push $0 ; Exch ; Pop $0 FunctionEnd
第二个函数在我眼里基本啥也没干,应该是想实现字符转ascii码把,所以其实就是简单的异或1而已:
a = '''gm`fzd787`7bb,g72d,592b,8`g1,cg96813e8d``|''' flag = "" for i in a: flag += chr((ord(i))^1) print flag
flag{e696a6cc-f63e-483c-9af0-bf87902d9eaa}
因为SEH的机制,IDA的F5会显示得很奇怪,大段地逻辑是无法看到伪C的,所以就OD+IDA看汇编硬做,差不多玩了一天XD。
不过看反汇编的代码可以看到代码的逻辑,loc_4154A1
应该是函数的主要逻辑,OD里可以调试,注意这里会先弹出hook再跳转到这里。
说实话,逆了很久才逆明白,程序要求输入格式是:flag{xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx},里面是uuid格式,第一部分和第二部分各少一个字节。
我把算法还原了,这里处理的是uuid格式识别之后的数据:
magic = "3nder5tandf10@t" mem = [] # s is input # s = "bd60d0456d311ea93f497358c3c6bc" i, j = 0, 0 s_len = len(s) print s_len for i in range(0, s_len, 2): mem.append(int(s[i:i+2], 16)^ord(magic[j])) # i += 2 j += 1 print map(hex, mem), len(mem) num = [] def g1(i, ax, cx): tmp = mem[i:i+5] tmp.reverse() a = tmp[0]+(tmp[1]<<8)+(tmp[2]<<16)+(tmp[3]<<24) b = tmp[4] num.append(a ^ ax) num.append(b ^ cx) g1(0, 0x76BDF08F, 0x0FEE1DEAF) g1(5, 0x846F5F42, 0x11DC37D) g1(10, 0x14031D68, 0x2cc5d9) print map(hex, num) r = ((num[0]+(num[1]<<32))**3)&0xffffffffffffffff r += ((num[2]+(num[3]<<32))**3)&0xffffffffffffffff r += ((num[4]+(num[5]<<32))**3)&0xffffffffffffffff r = r&0xffffffffffffffff print r if r == 42: print "right" else: print "error"
我这个是按照它这个32位逻辑一点点逆的,还原的时候直接按照64位处理就行。
关于42立方和问题,早就被玩烂了:42=(-80538738812075974)^3 + 80435758145817515^3 + 12602123297335631^3
最后的脚本:
a = -80538738812075974&0xffffffffffffffff b = 80435758145817515&0xffffffffffffffff c = 12602123297335631&0xffffffffffffffff a = a^0x0FEE1DEAF76BDF08F b = b^0x11DC37D846F5F42 c = c^0x2cc5d914031D68 a = hex(a)[2:-1].decode("hex") b = hex(b)[2:-1].decode("hex") c = hex(c)[2:-1].decode("hex") magic = "3nder5tandf10@t" flag = "" j = 0 for i in a: flag += chr(ord(i)^ord(magic[j])) j += 1 for i in b: flag += chr(ord(i)^ord(magic[j])) j += 1 for i in c: flag += chr(ord(i)^ord(magic[j])) j += 1 flag = flag.encode("hex") res = "flag{"+flag[:7]+"-"+flag[7:11]+"-"+flag[11:15] res += "-"+flag[15:19]+"-"+flag[19:]+"}" print res
flag{ed82ab5-5c7a-da78-b7a8-d2f5fbef453}
虽然没有mac,不过IDA里逻辑看起来跟源代码基本没啥区别了,写爆破脚本硬算:
a = [0x0000000000000001, 0x00000000000001FE, 0x0000000000001A79, 0x0000000000004940, 0x000000000000712F, 0x000000000000E1C5, 0x000000000001E866, 0x000000000003B85C, 0x00000000000760B0, 0x00000000000ED95D, 0x00000000001DB360, 0x00000000003B4D46, 0x000000000076A007, 0x0000000000ED528C, 0x0000000001DA9434, 0x0000000003B51CEA, 0x00000000076A592D, 0x000000000ED4AA88, 0x000000001DA951A4, 0x000000003B529EF7, 0x0000000076A55442, 0x00000000ED4AB07B, 0x00000001DA9560A0, 0x00000003B52AACC4, 0x000000076A5553D9, 0x0000000ED4AA997D, 0x0000001DA9553387, 0x0000003B52AA7EED, 0x00000076A554F324, 0x000000ED4AA9E5D7, 0x000001DA9553C9B2, 0x000003B52AA79A0C] res = [0x00030970372813D2, 0x0002D3A89BCA52AC, 0x00031551E79154A2, 0x0002C522E9A5298A, 0x0002A61367C5C698, 0x000264491C01CAFD, 0x00026CA3A06C98B3, 0x0002DACBD12FB903, 0x0002E470707574E1, 0x000309E5DC39A9A7] for i in range(32): a[i]*=0x10A9FC70042 a[i]%=0x682669BC19DB print map(hex, a) def enc(b): res = 0 for i in range(32): res += a[31-i]*(b&1) b = b >> 1 return res flag = "flag" import string d1 = string.letters + string.digits + "_-{}" d1 = string.printable #j = 0 def brute(idx): for i1 in d1: for i2 in d1: for i3 in d1: for i4 in d1: n = int((i1+i2+i3+i4).encode("hex"), 16) if enc(n) == res[idx]: #print i1+i2+i3+i4 return i4+i3+i2+i1 else: continue for i in range(1, len(res)): flag += brute(i) print flag
flag{m3Rkl3_h3LLMaN_KNaPsacK_Al90R17Hm!}
这个题差不多也玩了一天,是一个通过父子进程信号传递opcode和变量的虚拟机。我觉得很复杂了,分析了非常非常久,最后在IDA里设置虚拟机的结构体:
我把逻辑用python复现出来了,可以帮助理解,运行起来会有一些奇怪的问题hhhh。
ops = [17, 52, 0, 42, 5, 16, 20, 9, 23, 0, \ 32, 5, 3, 17, 29, 6, 0, 0, 5, 3,\ 17, 64, 6, 0, 64, 5, 17, 29, 23, 14,\ 1, 21, 4, 15, 1, 22, 2, 0, 0, 4, \ 3, 5, 16, 20, 50, 5, 9, 2, 19, 29,\ 5, 18, 21, 4, 16, 20, 61, 10, 1, 19,\ 52, 3, 4, 18, 14, 1, 21, 4, 7, 1,\ 22, 2, 0, 0, 4, 3, 5, 16, 20, 85, 5, 9, 1, 19, 64, 5, 18] # 0x7fffffffddec: sign [rbp-0x14] # 0x7ffff7ff7100 stack # 0x7ffff7ff7000 input # 0x7ffff7ff7790: arg1 # 0x7ffff7ff7791: arg2 # 0x7ffff7ff7792: arg3 # 0x7ffff7ff7793: sp [rax+0x13] # 0x7ffff7ff7794: rip [rax+0x14] # 0x7ffff7ff7795: res [rax+0x15] class VM: def __init__(self): self.stack = [0]*0x300 self.flag = [72]*42 + [0] self.arg = [0, 0, 0] self.sp = 0 self.ip = 0 self.res = 0 def push(self, numb): self.stack[self.sp] = numb self.sp += 1 #print "push %d" %numb def pop(self, numb): self.sp -= 1 self.arg[numb] = self.stack[self.sp] #print self.stack print "pop %d to arg%d" %(self.arg[numb], numb+1) def Call(self): self.stack[self.sp] = self.ip self.sp += 1 self.ip = tmp print "call %d" %self.ip #print self.ip def Ret(self): self.sp -= 1 self.ip = self.stack[self.sp] print "ret to %d" %self.ip def flag2stack(self): self.stack[self.sp] = self.flag[self.arg[2]] self.sp += 1 print "push flag[%d]" %self.arg[2] #print self.flag[0] def stack2flag(self): self.sp -= 1 self.flag[self.arg[2]] = self.stack[self.sp] print "pop to flag[%d]"%self.arg[2] two_op = [0, 8, 9, 10, 12, 13, 14, 17, 19, 20] global tmp vm = VM() from time import sleep while True: #sleep(0.2) op = ops[vm.ip] if op == 23: print "end" break vm.ip += 1 if op in two_op: op2 = ops[vm.ip] vm.ip += 1 tmp = op2 if op == 0: vm.push(tmp) print "push %d"%tmp elif op==1: vm.push(vm.arg[0]) print "push arg1 ==> %d"%vm.arg[0] elif op==2: vm.push(vm.arg[1]) print "push arg2 ==> %d"%vm.arg[1] elif op==3: vm.push(vm.arg[2]) print "push idx ==> %d"%vm.arg[2] elif op==4: vm.pop(0) elif op==5: vm.pop(1) elif op==6: vm.pop(2) elif op==7: vm.arg[0] += vm.arg[1] print "arg1 = arg1+arg2" elif op==8: vm.arg[0] += tmp print "arg1 += %d" %tmp elif op==9: vm.arg[1] += tmp print "arg2 += %d" %tmp elif op==10: vm.arg[2] += tmp print "idx += %d" %tmp elif op==11: vm.arg[0] -= vm.arg[1] print "arg1 = arg1-arg2" elif op==12: vm.arg[0] -= tmp print "arg1 -= %d" %tmp elif op==13: vm.arg[1] -= tmp print "arg2 -= %d" %tmp elif op==14: vm.arg[2] -= tmp print "idx -= %d" %tmp elif op==15: vm.arg[0] ^= vm.arg[1] print "arg1 = arg1^arg2" elif op==16: vm.res = (vm.arg[0]==vm.arg[1]) print "if arg1==arg2:res=1" #print vm.res elif op==17: vm.Call() elif op==18: vm.Ret() elif op==19: # jmp vm.ip = tmp print "jmp %d" %tmp elif op==20: # if jmp if vm.res == 1 : vm.ip = tmp print "if res: jmp %d" %tmp else: print "else" elif op==21: vm.flag2stack() elif op==22: vm.stack2flag() else: print "illegal" break
之后写了个稀碎的大致流程:
a = [17, 52, 0, 42, 5, 16, 20, 9, 23, 0, \ 32, 5, 3, 17, 29, 6, 0, 0, 5, 3,\ 17, 64, 6, 0, 64, 5, 17, 29, 23, 14,\ 1, 21, 4, 15, 1, 22, 2, 0, 0, 4, \ 3, 5, 16, 20, 50, 5, 9, 2, 19, 29,\ 5, 18, 21, 4, 16, 20, 61, 10, 1, 19,\ 52, 3, 4, 18, 14, 1, 21, 4, 7, 1,\ 22, 2, 0, 0, 4, 3, 5, 16, 20, 85, 5, 9, 1, 19, 64, 5, 18] #0: # 17, 52, call 52, # 长度等于42,跳到9,否则结束 #2: # 0, 42, 5, 16, 20, 9, 23 arg2 = 42 if arg1==arg2: jmp 9, else:end # Main 函数,调用了三段加密函数 #9: # 0,32, 5, 3, 17, 29, # 6, 0, 0, 5, 3, 17, 64, # 6, 0, 64, 5, 17, 29, 23, arg2 = 32 push idx call 29 pop idx arg2 = 0 push idx call 64 pop idx arg2 = 64 call 29 end # 异或处理,传入的arg2需要有初值,每次+2 #29: # 14,1, 21, 4, 15, 1, 22, # 2, 0, 0, 4, 3, 5, 16, 20, 50, # 5, 9, 2, 19, 29 idx -= 1 arg1 = flag[idx] flag[idx] = arg1 ^ arg2 push arg2 if idx=0:jmp 50 pop arg2 arg2 += 2 jmp 29 #50: # 5, 18 pop arg2 ret # 可以计算出输入的长度 #52: # 21, 4, 16, 20, 61, 10, 1, 19, 52, arg1 = flag[idx] if arg1==arg2: jmp 61 idx += 1 jmp 52 # arg1获得idx,返回2 #61: # 3, 4, 18, arg1 = idx ret # 相减处理,arg2有初值,每次+1 #64: # 14, 1, 21, 4, 7, 1, 22, 2, 0, 0, 4, 3, 5, # 16, 20, 85, 5, 9, 1, 19, 64 idx -= 1 arg1 = flag[idx] arg1 += arg2 flag[idx] = arg1 push arg2 arg1 = 0 arg2 = idx if arg1 == arg2: jmp 85 pop to arg2 arg2 += 1 jmp 64 #85: # 5, 18 pop arg2 ret
然后就可以写出最后的加密逻辑和反向的解密逻辑了:
res = [0xAF, 0xD4, 0xB8, 0xBD, 0xBC, 0xB9, 0xFC, 0xF1, 0xF6, 0xA1, 0xF5, 0xFE, 0xF1, 0xE9, 0x0B, 0xF3, 0x22, 0x0F, 0x14, 0xE2, 0xED, 0xE5, 0xE2, 0x1F, 0x56, 0x54, 0x4B, 0x3A, 0x7E, 0x3E, 0x5A, 0x5A, 0x5D, 0x0B, 0x6B, 0x68, 0x54, 0x54, 0x64, 0x07, 0x51, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] def enc(flag): j = 0 for i in range(0, 42, 1)[::-1]: ord(flag[i])^(32+j) j += 2 k = 0 for i in range(0, 42, 1)[::-1]: ord(flag[i])+k k += 1 m = 0 for i in range(0, 42, 1)[::-1]: ord(flag[i])^(64+m) m += 2 def dec(res): flag = res m = 0 for i in range(0, 42, 1)[::-1]: flag[i] = flag[i]^(64+m) m += 2 k = 0 for i in range(0, 42, 1)[::-1]: flag[i] = flag[i]-k k += 1 j = 0 for i in range(0, 42, 1)[::-1]: flag[i] = flag[i]^(32+j) j += 2 return flag l = dec(res) print "".join([chr(i) for i in l])
flag{e171a284-49e7-4817-ad8d-b704c02309e0}
没做上,不过说一下我分析出来的东西吧。
首先会根据规则将输入数据插入:
mem = dword_427000[0x50] aa = "" # 输入 j = 0 for i in range(0x50): if mem[i] == 0: mem[i] = ord(aa[j]) - 0x30 j += 1
之后每九个字符进行一次检测,分析完可以确定每9个字符只能是0-9数字,且每九个字符不重复,所以推测这个东西是数独。网上随便找了个脚本跑,得到第一段输入序列:
28163746738936845178453226739434617961829429853594162
后面根据提示是ZUC算法,之后又从第一段输入序列里取偶数下标的生成了一段字符串,按理说就是第二段输入的内容应该被ZUC加密然后跟什么东西比较,不过这里我还没找到,如果明天我还肝的话,想办法冲了它hhh。
参考别人的wp,得知是魔改的AES,本来也做了一部分的,后来有一些事情给我打断了,然后思路全没了从0开始,心态崩了,就不做了。
准备接下来好好学学魔改AES这种东西的特征吧。