在?看看ollvm

相关文章

前言

为什么会接触到ollvm呢,最早是在suctf 2019的re题上碰到的,一开始看到题目数十个while循环的嵌套还有一堆大数的比较看得我一脸懵逼,尤其是看着别的队都解出来了,就那种感觉似乎不是特别难但自己看又完全无法下手的感觉,太惨了。当时我看了十分钟就摸鱼,摸鱼回来发现自己还是不会然后又去摸鱼,往复拖到下午都还完全没有思路。但那天刚好舅舅来我家玩,他进房间看我在玩电脑就过来看了一眼,就看了眼就跟我说这是ollvm,随手地救了我于水火之中,然后说完他告诉我这个很简单的,不用找什么脚本,自己调一下就好了,很简单的。。。然后他就走了orz

ollvm的相关知识我也没怎么掌握,相关的介绍也没有他人讲得详细准确,所以这篇文章就不赘述这些了,有兴趣得可以看前面相关文章得链接

在这篇文章,我将试图总结一下ollvm混淆后的程序的动调方法,以及大体的分析思路

还有,看不到图片的话刷新一下

关于ollvm还是先提一笔

  • ollvm的缺点是不能加密过于复杂的程序流,复杂的for嵌套就不行
  • 以及它原本的代码块都还在,因此不用管分发器啊之类的,只要管核心代码就行(或者叫有效代码

来看两道题

SUCTF 2019 – hardCpp

ida打开程序以后f5是这样的

但是其实已经可以看到一些有效指令了

当然这些函数名是我后来分析完以后改的,一开始这里也挺乱的,看不懂在干什么,而且,这个程序还有一个虚假程序流,也长差不多样子

那怎么办呢,可以在这两个块下断,但是还是会漏掉其它有实际作用的小块(事实的确是这样的,但是F5的代码把每个代码块下断又特别麻烦

这时候应该看一下graph

这就非常清晰了,实际代码就是下面一排,上面的都是分发器。所以我们可以尝试在下面所有的代码块都下断

就像这样全断上好了,暴力hhh

然后我们就只要专注与实际的程序流就行了,开始动调。到断点后就开始单步,过了代码块就直接F9前往下一个代码块

在动调的时候注意几个寄存器的值

第一次在这里,如果直接f9就退出了,并没有前往下一个代码块。

但是我发现它有将var_8的内容和0进行比较,然而我每次调这个值都不是0,说明这是一个反调试之类的,我们看下这个var_8在哪里被赋值

可以看到它call了两次time然后将它们的差存入了var_8,这就是一个简单的反调试,考虑到esi同时赋值给了多个变量,因此我们可以在sub esi,…这里下一个断,然后手动把esi的值设为0

果然前往了下一个代码块

这里看不出什么,过

接着我们来到了一个有strlen的地方

看眼参数,发现是我们刚刚输入的字串,看来它求了我们输入的字串,并于0x15进行比较。我们重来一次,这次输入0x15长度的字串

过了,然后我们又到了几个跟前面差不多的看不出啥用的代码块,这些代码块都是作用于分发器的应该,最后我们到了一个看起来很有用的代码块

这里的函数名已经被我改了,自己调的话可以跟进去,然后会发现又是一些被ollvm混淆的函数,同样的方法调,最后得到了如下表达式

(s[i] + (s[i-1] % 7)) ^ ((s[i-1] ^ 0x12)*3+2) == enc[i-1]

把enc数组dump出来,然后写一下脚本就好了

from z3 import *
enc = [0xF3,0x2E,0x18,0x36,0xE1,0x4C,0x22,0xD1,0xF9,0x8C,0x40,0x76,0xF4,0x0E,0x00,0x05
,0xA3,0x90,0x0E,0xA5]


'''
(s[i] + (s[i-1] % 7)) ^ ((s[i-1] ^ 0x12)*3+2) == enc[i-1]

'''

s = [0x23,0x66]
i = 2
while i < 0x15:
    print i
    print s
    for tmp in range(128):
        if ((tmp + (s[i-1] % 7)) ^ ((s[i-1] ^ 0x12)*3+2)% 0x100) == enc[i-1]:
            s.append(tmp)
    if len(s) != i+1:
        break
    i += 1

m = ''
for i in s:
    m += chr(i)
print m

至此,我人生中第一个被混淆过的程序就被我逆完了2333,先鸽一会,明天更一个更难的——X-NUCA’ 2019-ooollvm

在那道题里我还写了人生中第一个idapython脚本hhh

X-NUCA’ 2019-ooollvm

讲道理做了一个ollvm之后我觉得我会了,所以看到X-NUCA有ollvm我就去做了,然后,然后它跟说好的不一样啊

总的graph
其中的一小段

讲道理打开这个程序的时候根本没法生成graph,node数量超出限制了,我改到100000才能生成,生成的时候还一直未响应,搞了好久才出来,然后还没法F5,得改文件,改完也要好久才能出来。

为什么我一直想先看graph呢,因为根据上一题的经验,我觉得出来会像上一题那样,然后我只要在最下面的有效指令那块下断就行。可是看看这道题的,完全不一样好吧。

(事后我猜测,可能因为上一题是有循环的,而这题没有循环,再加上这题有效指令就非常的长,所以才会变成这个样子

那么没有办法像上一题那样下断,怎么办呢

我利用了硬件断点,因为本质是字串的处理,所以我就在scanf后在我的输入那里下断,一个一个字符下断看看参与了什么运算。这道题在对字符运算之后还会把值存入栈中,所以我在跟单个字符的时候把它们也断上了

。。。懒得写了,我把wp直接搬上来吧

然后我发现都是类似于这样的表达式

v0*0x97e5-(v0**2*0x156-v0**3) == 0x166ca4

每个字符那边都只是变换一下3个参数

但是这样搞完了以后发现有多解,我发现我一开始没调仔细,于是发现每个字符要过3个表达式,例如第6个字符

v0*0x97e5-(v0**2*0x156-v0**3) == 0x166ca4
v0*0x98d4-(v0**2*0x157-v0**3) == 0x16a460
v0*0x895c-(v0**2*0x145-v0**3) == 0x135420

这样重复而且多的式子手调永远调不完XD,但是发现给参数赋值时的代码都是类似的例如

imul    eax, ecx, 156h
···
imul    edx, 97E5h
···
sub     esi, 166CA4h

所以写了一个脚本来dump所有的表达式,然后用z3解

from ida_bytes import get_bytes, patch_bytes

addr = 0x40d500
buf = get_bytes(addr, 0x04E837D-addr)

pattern1 = '\x69\xc1'
pattern2 = '\x69\xd2'
pattern3 = '\x29\xd1\x89\xc6\x81\xee'
pattern3_1 = '\x8b\x08\x81\xe9'
p1_off = buf.find(pattern1)
count = 0
T = []

while p1_off != -1:
    if count == 13*3 or count == 42*3:
        T.append([])
        index = len(T)-1
        T[index].append(0x140)
        if count == 13*3:
            bind = 0x447134-addr
        else:
            bind = 0x4C5A8B-addr

        p2_off = buf.find(pattern2,bind)
        tmp = buf[p2_off+2:p2_off+4]
        p2 = ord(tmp[0]) + ord(tmp[1])*0x100
        T[index].append(p2)
        if count == 13*3:
            T[index].append(0x1256A6)
        else:
            T[index].append(0x12644A)
        count += 3
        p1_off = bind
        p1_off = buf.find(pattern1, p1_off+2)
        continue


    if count % 3 == 0:
        T.append([])
        index = len(T)-1
        # s[i]**2*p1
        tmp = buf[p1_off+2:p1_off+4]
        p1 = ord(tmp[0]) + ord(tmp[1])*0x100
        T[index].append(p1)

    if count % 3 == 0:  # p2
        bind = p1_off
        p2_off = buf.find(pattern2,bind)
        tmp = buf[p2_off+2:p2_off+4]
        p2 = ord(tmp[0]) + ord(tmp[1])*0x100
        T[index].append(p2)
    if count % 3 == 1:  # p3
        bind = p1_off-0x100  # 0x1047
        p3_off = buf.find(pattern3,bind)
        tmp = buf[p3_off+6:p3_off+9]
        p3 = ord(tmp[0]) + ord(tmp[1])*0x100 + ord(tmp[2])*0x10000
        if count > 39*3 and count < 44*3:
            p3_off = buf.find(pattern3,p1_off+0xd00)
            tmp = buf[p3_off+6:p3_off+9]
            p3 = ord(tmp[0]) + ord(tmp[1])*0x100 + ord(tmp[2])*0x10000
        if count > 44*3:
            p3_off = buf.find(pattern3_1,p1_off)
            tmp = buf[p3_off+4:p3_off+7]
            p3 = ord(tmp[0]) + ord(tmp[1])*0x100 + ord(tmp[2])*0x10000
        T[index].append(p3)
    count += 1
    p1_off = buf.find(pattern1, p1_off+2)
T[44][2] = 0xFA479

# creat z3 script
print 'from z3 import *'
print 'from string import *'
print 's = Solver()'
for i in range(len(T)-2):
    s = 'v' + str(i) + ' = Real(\'v' + str(i) + '\')\n'
    s += 's.add(v' + str(i) +'*'+hex(T[i][1]) + '-(v'+str(i)+'**2*'+hex(T[i][0])+'-v'+str(i)+'**3) == '+hex(T[i][2]) +')\n'
    s += 's.add(v' + str(i) +'*'+hex(T[i+1][1]) + '-(v'+str(i)+'**2*'+hex(T[i+1][0])+'-v'+str(i)+'**3) == '+hex(T[i+1][2]) +')\n'
    s += 's.add(v' + str(i) +'*'+hex(T[i+2][1]) + '-(v'+str(i)+'**2*'+hex(T[i+2][0])+'-v'+str(i)+'**3) == '+hex(T[i+2][2]) +')\n'
    print s
print 's.check()'
print 'm = s.model()'
print 'flag = \'flag{\''
for i in range(len(T)-2):
    s = 'tmp = "%s" % m[v'+str(i)+']\n'
    s += 'flag += chr(atoi(tmp))'
    print s
print 'flag += 's}''
print 'print flag'

将输出复制到另一个python文件中,运行就可以得到flag(因为我的z3只在linux上,所以得拷过去到linux上运行)

就如上面所说的,这道题会有很多地方存在多解,就像这里,2-3个同样的约束会导致多解

当然可以把这几个地方单独用z3算一下换一换然后尝试组成单词,手动换一下就行,最后得到正确的flag

但是我觉得我的方法不太对,因为我搞了好久,就算我一开始就会idapython,不去手动dump,也没法像前几队那样那么快得到结果,一定是有更加方便地方法地,利用工具什么的应该能直接出来吧。

说点什么

avatar

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  Subscribe  
提醒