吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5508|回复: 34
收起左侧

[原创] 【L3HCTF】double-joy引出的解VM类题型新思路

  [复制链接]
Qfrost 发表于 2021-11-16 10:35

刚刚结束的L3HCTF,可以说题目质量很高,题目《很有意思》。这次比赛中印象比较深的是double-joy这题,也是对VM类题目有了一些新的解题思路。之前大大小小比赛,出现过很多VM类题目,楼主一直用的都是最传统的 “跟调、爆破、dump” 等方法,但是这次比赛,double-joy这个VM调用关系太复杂了,用之前的方法根本看不懂,(VM跑了202次,人傻了),所以采用了一种新的方法来解VM。

VM其实做的是一个opcode模拟shellcode的运行过程,opcode进入VM译码执行对应的汇编操作,如果能把shellcode dump出来,通过静态分析工具建数据流和控制流,甚至生成伪代码,那相当于剥离VM了,程序的分析难度也大大降低了。而实现这个过程,需要手动的分析出VM里每条opcode对应的汇编,并dump出opcode将所有opcode译码成汇编输出并编译。如果前面的过程都没有错,就可以在IDA等静态分析工具上清晰的看到VM做了什么事情。其实这种方法之前就看有师傅用过,但因为嫌麻烦一直没尝试,这次实在没办法用了一下,“真香”!

听着还是有点抽象,直接以L3HCTF的double-joy来讲一下

1e179bbe47324b02b8b5534b17b42e97.zip (6.06 KB, 下载次数: 19)

double-joy

IDA打开程序,main函数里首先是要求输入39个字符,然后做了一堆vm的初始化,这里调过去看和输入没关系就暂时没有管,然后会进入一个嵌套的while

double-joy_1.jpg

然后可以分析出vm_instance结构体的结构,为了方便分析,可以在IDA里面创建一个结构体。在Structures窗口里,Add struct type(Insert键)一个名为VM的结构

double-joy_2.jpg

然后按Data(D键),创建VM结构体成员并定义对应的数据类型

double-joy_3.jpg

然后开始分析VM(sub_1D90)

这题可以说用了IDA静态分析的bug,VM函数是一个switch结构但是这个结构没有被识别出来,导致F5什么都看不到。这是因为出题人在switch的jmp前加了一条 db 0x3E,他干扰了IDA对switch的识别。不过这个问题大家都知道,直接把这个位置patch成nop就可以正常F5了。

F5后,如果已经建好了VM实例的结构,那就很简单了,是一个很清晰的VM。18个opcode,分析发现虚拟机的操作和Python虚拟机很像,都是基于栈从栈顶做操作的,没有通用寄存器

double-joy_4.jpg

18个opcode都非常简单,然后就是重点了,要对这18种opcode写出对应的汇编。每条opcode的具体实现就不一一细说了,可以看代码

ip = 0
asm = ""
while ip < len(opcode):
    asm += "label_{}:\n".format(ip)     # ip作为label
    if opcode[ip] == 0:     # add
        ip += 1
        asm += """  \
        pop eax;
        pop ebx;
        add eax, ebx;
        push eax;
        """
    elif opcode[ip] == 1:     # sub
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        sub eax, ebx;
        push eax;
        """
    elif opcode[ip] == 2:       # imul
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        imul eax, ebx;
        push eax;
        """
    elif opcode[ip] == 3:       # idiv
        ip += 1
        asm += """\
        xor edx, edx;
        pop eax;
        pop ebx;
        idiv ebx;
        push eax;    
        """
    elif opcode[ip] == 4:       # mod
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        idiv ebx;
        push edx;    
        """
    elif opcode[ip] == 5:       # and
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        and eax, ebx;
        push eax;
        """
    elif opcode[ip] == 6:       # or
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        or eax, ebx;
        push eax;
        """
    elif opcode[ip] == 7:       # xor
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        xor eax, ebx;
        push eax;
        """
    elif opcode[ip] == 8:       # stack[TOS1]=TOS
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        mov [ebp+4*ebx], eax;
        """
    elif opcode[ip] == 9:       # TOS=stack[TOS]
        ip += 1
        asm += """\
        mov eax, [esp];
        mov ebx, [ebp+4*eax];
        mov [esp], ebx;
        """
    elif opcode[ip] == 10:      # TOS=!TOS
        ip += 1
        asm += """\
        xor ebx,ebx;
        mov eax, [esp];
        test eax, eax;
        sete bl;
        mov [esp], ebx;
        """
    elif opcode[ip] == 11:      # TOS=TOS<0
        ip += 1
        asm += """\
        mov eax, [esp];
        shr eax, 31;
        mov [esp], eax;
        """
    elif opcode[ip] == 12:      # xchg TOS1, TOS
        ip += 1
        asm += """\
        pop eax;
        pop ebx;
        push eax;
        push ebx;
        """
    elif opcode[ip] == 13:      # pop
        ip += 1
        asm += """\
        pop eax;
        """
    elif opcode[ip] == 14:      # push
        val = struct.unpack("<i", bytes(opcode)[ip+1:ip+5])[0]
        ip += 5
        asm += """\
        push {};
        """.format(val)
    elif opcode[ip] == 15:      # jmp
        val = struct.unpack("<i", bytes(opcode)[ip+1:ip+5])[0]
        ip += 5
        asm += """\
        jmp label_{};
        """.format(ip+val)
    elif opcode[ip] == 16:      # jnz  TOS
        val = struct.unpack("<i", bytes(opcode)[ip+1:ip+5])[0]
        # if stack[sp] != 0:
        #     ip += 5 + val
        # else:
        #     ip += 5
        ip += 5
        asm += """\
        pop eax;
        cmp eax, 0;
        jnz label_{};
        """.format(ip+val)
    elif opcode[ip] == 17:      # sp += imm
        val = struct.unpack("<i", bytes(opcode)[ip+1:ip+5])[0]
        ip += 5
        asm += """\
        sub esp, {};
        """.format(val*4)
    elif opcode[ip] == 18:      # ret
        val = struct.unpack("<i", bytes(opcode)[ip+1:ip+5])[0]
        ip += 5
        asm += """\
        mov eax, {};
        retn;
        """.format(val)
    else:
        print("default")
        ip += 1
        pass

    # time.sleep(0.1)

print(asm)

fp = open("shellcode.asm", 'w')
fp.write("section .text\n")
fp.write("bits 32\n")
fp.write(asm)
fp.close()

我对每条opcode都插入了一个label,因为VM里涉及跳转,而且跳转都是相对于opcode进行的跳转,但在汇编里的跳转是相对于汇编的,所以以opcode为单位设置label,在跳转汇编里直接写对应的label就可以了,非常方便。

然后把opcode dump出来,用上面的脚本可以输出asm汇编文件。然后用nasm编译成二进制文件。

  • nasm shellcode.asm

然后可以对照着shellcode和动调的结果,把栈上的各个位置所表示的含义修复出来。

double-joy_5.jpg

再看shellcode,就非常清晰了。第一段shellcode:

double-joy_6.jpg

第二段shellcode:

double-joy_7.jpg

可以很清晰的看出,第一段shellcode在做xTEA,而第二段shellcode在做TEA。其中delta密钥的初始化均在其中,并且在TEA和xTEA算法之前都有一次异或。

然后就是坑的地方了,动调会发现这个VM走了很多很多次,而且TEA和xTEA的opcode是交替执行的,且EIP还不一样(这两段shellcode dump出来会发现分别都有两个入口)。对于这个问题,我在 0x148B 的位置下了一个条件断点,把每次进入VM前的相关数据打印出来

stack_base = get_qword(cpu.rdi+8)
stack = []
for i in range(22):
    stack.append(hex(get_wide_dword(stack_base+i*4)))
print("EIP:%s \t" % hex(get_wide_word(cpu.rdi+0x10)), stack)

double-joy_8.jpg

可以看到,总共进入VM了202次,只有第一次和第二次进入时EIP为0,其他时候走的都是另一个入口,并且两段opcode是交替着跑的。分析可以知道第一次和第二次走入口为0时,做的是TEA和xTEA的初始化操作,而后面只是跑for循环,而且跑一轮就出来,外面控制参数再跑。输入共10个DWORD数,两个一组分为5组,每组跑20轮TEA和20轮xTEA,再加两次初始化操作,正好202次VM。

然后就可以写脚本出了,就是一个xTEA和TEA的交替算法

#include<cstdio>
#include<stdio.h>

int cipher[10]={-1360987416, -63028991, 377269650, 1374317758, 606732544, 22092315, 1364027028, 794804203, 1188258712, 2045699056};

void decrypt(int *v)
{

    int xtea_key[4]={0x494c, 0x6f76, 0x6520, 0x4355};
    int tea_key[4]={0x5354, 0x4f4d, 0x2074, 0x6561};

    int round_max;
    int xtea_sum=0x423a35c6;
    int tea_sum=0x6042aaa4;

    for(int i=0;i<10;i+=2) {
        if(i==0) round_max = 19;
        else     round_max = 20;
        int v0=v[i],v1=v[i+1];
        for(int round=0;round<round_max;round++) {
            xtea_sum += 0x75bcd15;
            tea_sum += 0x154cbf7;
        }
    }    

        for (int i=8;i>=0;i-=2) {
            int round_max=(i==0)?19:20;
            int v0=v[i],v1=v[i+1];
            for(int round=round_max-1;round>=0;round--) {
                v1 -= ((v0 * 16) + tea_key[2]) ^ (v0 + tea_sum) ^ ((v0 / 32) + tea_key[3]);
                v0 -= ((v1 * 16) + tea_key[0]) ^ (v1 + tea_sum) ^ ((v1 / 32) + tea_key[1]);
                tea_sum -= 0x154cbf7;
                v1 -= (((v0 * 16) ^ (v0 / 32)) + v0) ^ (xtea_sum + xtea_key[(xtea_sum / 2048) & 3]);
                xtea_sum -= 0x75bcd15;
                v0 -= (((v1 * 16) ^ (v1 / 32)) + v1) ^ (xtea_sum + xtea_key[xtea_sum & 3]);
            }
            v[i]=v0;v[i+1]=v1;

        }        
        v[1] -= ((16 * v[0] + 8308) ^ (v[0] + 1614981796) ^ (v[0] / 32 + 25953));
        v[0] -= (16 * v[1] + 21332) ^ (v[1] + 1614981796) ^ (v[1] / 32 + 20301);

        for (int i=0;i<10;i++)
            v[i]^=0x01010101*(i+1);
        v[1] -= (((16 * v[0]) ^ (v[0] / 32)) + v[0]) ^ 0x423A9AE6;
        v[0] -= (((16 * v[1]) ^ (v[1] / 32)) + v[1]) ^ 0x3ADED827;

        for (int i=0;i<10;i++)
            v[i]^=0x01010101*(i+1);
}

int main()  {
    decrypt(cipher);
    for(int i=0;i<40;i++)
        printf("%c",*((char*)cipher+i));
}
// L3HCTF{D0uBle_vM_W1th_dOubIe_TEA}

免费评分

参与人数 20吾爱币 +17 热心值 +18 收起 理由
MrZT + 1 + 1 我很赞同!
jindar + 1 热心回复!
zxl1227 + 1 我很赞同!
ZJevon + 2 我很赞同!
shiyucj + 1 + 1 我很赞同!
timeni + 1 + 1 我很赞同!
yixi + 1 + 1 热心回复!
ctfy55 + 1 我很赞同!
zsky + 2 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
这是追求不是梦 + 1 + 1 热心回复!
nightmare77 + 1 + 1 学习学习ing
chenxingda + 1 我很赞同!
staynaive + 1 谢谢@Thanks!
raycerlane + 1 谢谢@Thanks!
搜索曾经的回忆 + 1 + 1 nice!
gaosld + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
SmallRadar + 1 + 1 很厉害!
minibeetuaman + 1 + 1 我很赞同!
hongshen233 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

d1ves 发表于 2021-11-16 13:35
太厉害了,你写的每一个字我都认识。合一起我啥也看不懂
Ichild 发表于 2021-11-17 09:30
db 0x3E并不是出题人故意加的,你可以看到db 0x3E的地址和下一条jmp的地址是相同的,这好像是指示某个段?具体不记得了。建议学一下switch的修复:edit -> other -> specify switch idiom
minibeetuaman 发表于 2021-11-16 12:52
精彩,庖丁解牛把VM的emulator部分弄出来,釜底抽薪的做法!
yudream 发表于 2021-11-16 17:06
太强了,对VM类的分析提供了新的思路
jiemo365 发表于 2021-11-16 17:11
学习了~谢谢分享
93244 发表于 2021-11-16 17:27
感谢楼主,虽然看不懂
搜索曾经的回忆 发表于 2021-11-16 17:36
很好的思路,学习了
raycerlane 发表于 2021-11-16 19:01
学习了,非常感谢!回去试试
dragonjelly 发表于 2021-11-16 19:37
厉害了,又学习到了新思路,感谢楼主分享
Xiaomi0814 发表于 2021-11-16 20:14

我也过来学习一下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 11:35

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表