网络安全公益赛RE
2020-02-26 18:58:03 Author: bbs.pediy.com(查看原文) 阅读量:437 收藏

本来是一名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这种东西的特征吧。

[公告] 防守篇征题进行中!看雪2020 KCTF春季赛防守方征题中 !


文章来源: https://bbs.pediy.com/thread-257831.htm
如有侵权请联系:admin#unsafe.sh