基于栈的 VM 题,单纯从指令来讲并不难,只需要简单的解析一下即可分析出进行的运算,但是运算上的一些小细节加大了本题的难度。

Analysis

main 函数没有太多的内容,主要是获取输入之后传入 vm 进行运算

运算出来的结果和正确的进行逐字节的对比

进入 vm ,程序根据指令调用不同的函数,共 19 种不同的函数

根据汇编和调试很容易看出来每一个函数的作用,可以写一个简单的 python 脚本用来分析

while i < len(opcodes):
    if opcodes[i] == 0:
        print(hex(i)[2:].rjust(3, '0'), "inplace_add")
        i += 1
    elif opcodes[i] == 1:
        print(hex(i)[2:].rjust(3, '0'), "inplace_sub")
        i += 1
    elif opcodes[i] == 2:
        print(hex(i)[2:].rjust(3, '0'), "inplace_mul")
        i += 1
    elif opcodes[i] == 3:
        print(hex(i)[2:].rjust(3, '0'), "inplace_div")
        i += 1
    elif opcodes[i] == 4:
        print(hex(i)[2:].rjust(3, '0'), "inplace_mod")
        i += 1
    elif opcodes[i] == 5:
        print(hex(i)[2:].rjust(3, '0'), "inplace_and")
        i += 1
    elif opcodes[i] == 6:
        print(hex(i)[2:].rjust(3, '0'), "inplace_or")
        i += 1
    elif opcodes[i] == 7:
        print(hex(i)[2:].rjust(3, '0'), "inplace_xor")
        i += 1
    elif opcodes[i] == 8:
        print(hex(i)[2:].rjust(3, '0'), "stack[TOS1]=TOS")
        i += 1
    elif opcodes[i] == 9:
        print(hex(i)[2:].rjust(3, '0'), "TOS=stack[TOS]")
        i += 1
    elif opcodes[i] == 0xA:
        print(hex(i)[2:].rjust(3, '0'), "if TOS==0: TOS=1 else: TOS=0")
        i += 1
    elif opcodes[i] == 0xB:
        print(hex(i)[2:].rjust(3, '0'), "if TOS<0: TOS=1 else: TOS=0")
        i += 1
    elif opcodes[i] == 0xC:
        print(hex(i)[2:].rjust(3, '0'), "Inplace_swap")
        i += 1
    elif opcodes[i] == 0xD:
        print(hex(i)[2:].rjust(3, '0'), "remove_top")
        i += 1
    elif opcodes[i] == 0xE:
        tmp = (opcodes[i+1] | (opcodes[i+2] << 8) | opcodes[i+3]
               << 16 | opcodes[i+4] << 24) & 0xffffffff
        print(hex(i)[2:].rjust(3, '0'), f"push {hex(tmp)}")
        i += 5
    elif opcodes[i] == 0xF:
        tmp = (opcodes[i+1] | (opcodes[i+2] << 8) | opcodes[i+3]
               << 16 | opcodes[i+4] << 24) & 0xffffffff
        print(hex(i)[2:].rjust(3, '0'), f"jmp {hex((tmp+i+5)&0xffffffff)}")
        i += 5
    elif opcodes[i] == 0x10:
        tmp = (opcodes[i+1] | (opcodes[i+2] << 8) | opcodes[i+3]
               << 16 | opcodes[i+4] << 24) & 0xffffffff
        print(hex(i)[2:].rjust(3, '0'), f"if TOS==1: jmp {hex(i+tmp+5)}")
        i += 5
    elif opcodes[i] == 0x11:
        tmp = (opcodes[i+1] | (opcodes[i+2] << 8) | opcodes[i+3]
               << 16 | opcodes[i+4] << 24) & 0xffffffff

        print(hex(i)[2:].rjust(3, '0'), f"sub rsp,{hex(tmp)}")
        i += 5
    elif opcodes[i] == 0x12:
        tmp = (opcodes[i+1] | (opcodes[i+2] << 8) | opcodes[i+3]
               << 16 | opcodes[i+4] << 24) & 0xffffffff
        print(hex(i)[2:].rjust(3, '0'), f"return {tmp}")
        i += 5

调试的时候可以发现传入该 vm 函数的并不仅仅是指令,而是一个结构体,结构体的定义如下

该 vm 是一个基于栈的 vm,所有的数据保存在栈上。调试中发现,该程序共传入了两套 vm 指令,做题时一开始以为只有一套指令,解不出结果之后才发现后续又运行了另一套指令。

为了更快做题,只是用上面写的脚本简单解析了一下这两套指令,对于虚拟机的题目,有一种处理方式是将每一个指令写成汇编指令集的形式,编译之后借助IDA等工具进行分析,好在这题的指令并不复杂,只要抓住特征很容易把算法还原出来。两套指令有相同的地方,但在运算和入栈的数据上有很大的区别。

可以很容易发现第一套指令在进行了异或操作之后,进行的是魔改版的 xtea 加密,有一个明显的特征在于进行了两次如下操作,左移 4 位,右移 5 位,异或并相加操作。

148 push 0x10
14d inplace_mul
14e push 0xd
153 TOS=stack[TOS]
154 push 0x20
159 Inplace_swap
15a inplace_div
15b inplace_xor
15c push 0xd
161 TOS=stack[TOS]
162 inplace_add

修改的地方在于 sum 的初值和 delta 不同,加密轮次也不同,秘钥、sum 和 delta 都在程序开始时入栈,很容易找。

第二套指令同样进行异或操作,但是后续进行的是魔改版的 tea 加密,特征同样也很明显,出现了两次移位,两次异或,多次相加

15a TOS=stack[TOS]
15b push 0x10
160 inplace_mul
161 push 0x0
166 push 0xe
16b inplace_add
16c TOS=stack[TOS]
16d inplace_add
16e push 0xd
173 TOS=stack[TOS]
174 push 0xb
179 TOS=stack[TOS]
17a inplace_add
17b push 0xd
180 TOS=stack[TOS]
181 push 0x20
186 Inplace_swap
187 inplace_div
188 push 0x1
18d push 0xe
192 inplace_add
193 TOS=stack[TOS]
194 inplace_add
195 inplace_xor
196 inplace_xor
197 inplace_add

sum、delta 和 key 都和第一套不同。

但是到这里题目还有很多坑没有填。

第一点就是,在一轮加密结束之后,程序会进行一次 return,与全部加密结束之后的返回值不同,返回之后程序会在 main 函数内进行下一次循环,运行另一套指令,因此两套加密是在一轮加密结束之后进行交换,轮流进行。

第二点就是,两套指令都在开头对每一个字进行了异或,都进行了一次,并且值相同(0x1010101-0xa0a0a0a),所以除了前两个字在异或之后立刻进行了一轮加密之外,后续的所有字都连续进行了两次相同的异或操作,并没有任何影响。

第三点就是与 xtea/tea 算法不同,该算法每一组字的 sum 并不重置,而是使用前一组字计算的结果继续进行,因此在解密的时候由最后一组向前进行并且将 sum 设置为全局变量是比较简单的方式。

第四点是原版 xtea/tea 基本上都是无符号数运算,但是这里的除法操作之前使用了 cdq 指令,用 EAX 的符号位填充了 EDX,很明显是一个有符号数操作。

第五点是原版 xtea/tea 使用的是右移运算,而这里用的是除法运算,这也就意味着在某些语言的处理中(比如 C),根据舍入规则的不同,有符号数的右移和除法运算的结果在最后一位上存在误差,多轮加密导致误差扩大并不断引入新的误差,因此应该使用除法的方式。

Solution

根据上述分析可以很容易复现加密的脚本和对应的解密脚本

#include <stdio.h>
#include <stdint.h>

void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[8])
{
    unsigned int i;

    int32_t v0 = v[0], v1 = v[1];
    int32_t delta1 = 123456789, base1 = 987654321, sum1 = base1;
    int32_t delta2 = 0x154cbf7, base2 = 0x5eeddead, sum2 = base2;

    v0 ^= 0x1010101;
    v1 ^= 0x2020202;
    printf("%x %x\n", v0, v1);

    v0 += (((v1 << 4) ^ (v1 / 32)) + v1) ^ (sum1 + key[sum1 & 3]);
    sum1 += delta1;
    v1 += (((v0 << 4) ^ (v0 / 32)) + v0) ^ (sum1 + key[(sum1 / 0x800) & 3]);
    printf("%x %x\n", v0, v1);

    v0 ^= 0x1010101;
    v1 ^= 0x2020202;

    printf("%x %x\n", v0, v1);

    sum2 += delta2;
    v0 += ((v1 << 4) + key[4]) ^ (v1 + sum2) ^ ((v1 / 32) + key[5]);
    v1 += ((v0 << 4) + key[6]) ^ (v0 + sum2) ^ ((v0 / 32) + key[7]);
    printf("%x %x\n", v0, v1);

    for (i = 1; i < num_rounds; i++)
    {

        v0 += (((v1 << 4) ^ (v1 / 32)) + v1) ^ (sum1 + key[sum1 & 3]);
        sum1 += delta1;
        v1 += (((v0 << 4) ^ (v0 / 32)) + v0) ^ (sum1 + key[(sum1 / 0x800) & 3]);
        printf("%x %x\n", v0, v1);

        sum2 += delta2;
        v0 += ((v1 << 4) + key[4]) ^ (v1 + sum2) ^ ((v1 / 32) + key[5]);
        v1 += ((v0 << 4) + key[6]) ^ (v0 + sum2) ^ ((v0 / 32) + key[7]);
        printf("%x %x\n", v0, v1);
    }
    v[0] = v0;
    v[1] = v1;
}

int32_t delta1 = 123456789, base1 = 987654321, sum1 = delta1 * 0x14 * 5 + base1;
int32_t delta2 = 0x154cbf7, base2 = 0x5eeddead, sum2 = delta2 * 0x14 * 5 + base2;

void decipher(unsigned int num_rounds, uint32_t v[2], int32_t const key[8])
{
    unsigned int i;
    int32_t v0 = v[0], v1 = v[1];

    for (i = 1; i < num_rounds; i++)
    {
        v1 -= ((v0 << 4) + key[6]) ^ (v0 + sum2) ^ ((v0 / 32) + key[7]);
        v0 -= ((v1 << 4) + key[4]) ^ (v1 + sum2) ^ ((v1 / 32) + key[5]);
        sum2 -= delta2;

        v1 -= (((v0 << 4) ^ (v0 / 32)) + v0) ^ (sum1 + key[(sum1 / 0x800) & 3]);
        sum1 -= delta1;
        v0 -= (((v1 << 4) ^ (v1 / 32)) + v1) ^ (sum1 + key[sum1 & 3]);
    }

    v1 -= ((v0 << 4) + key[6]) ^ (v0 + sum2) ^ ((v0 / 32) + key[7]);
    v0 -= ((v1 << 4) + key[4]) ^ (v1 + sum2) ^ ((v1 / 32) + key[5]);
    sum2 -= delta2;

    v0 ^= 0x1010101;
    v1 ^= 0x2020202;

    v1 -= (((v0 << 4) ^ (v0 / 32)) + v0) ^ (sum1 + key[(sum1 / 0x800) & 3]);
    sum1 -= delta1;
    v0 -= (((v1 << 4) ^ (v1 / 32)) + v1) ^ (sum1 + key[sum1 & 3]);

    v0 ^= 0x1010101;
    v1 ^= 0x2020202;

    v[0] = v0;
    v[1] = v1;
}

void decipher2(unsigned int num_rounds, uint32_t v[2], int32_t const key[8])
{
    unsigned int i;
    int32_t v0 = v[0], v1 = v[1];
    for (i = 0; i < num_rounds; i++)
    {
        v1 -= ((v0 << 4) + key[6]) ^ (v0 + sum2) ^ ((v0 / 32) + key[7]);
        v0 -= ((v1 << 4) + key[4]) ^ (v1 + sum2) ^ ((v1 / 32) + key[5]);
        sum2 -= delta2;

        v1 -= (((v0 << 4) ^ (v0 / 32)) + v0) ^ (sum1 + key[(sum1 / 0x800) & 3]);
        sum1 -= delta1;
        v0 -= (((v1 << 4) ^ (v1 / 32)) + v1) ^ (sum1 + key[sum1 & 3]);
    }

    v[0] = v0;
    v[1] = v1;
}

int main()
{
    uint32_t v[10] = {
        0xAEE0FAE8, 0xFC3E4101, 0x167CAD92, 0x51EA6CBE, 0x242A0100, 0x01511A1B, 0x514D6694, 0x2F5FBFEB,
        0x46D36398, 0x79EEE3F0};
    int32_t const k[8] = {0x0000494C, 0x00006F76, 0x00006520, 0x00004355, 0x00005354, 0x00004F4D, 0x00002074, 0x00006561};
    unsigned int r = 0x14;
    for (int i = 8; i >= 2; i -= 2)
    {
        uint32_t tmp_v[] = {v[i], v[i + 1]};
        decipher2(r, tmp_v, k);
        v[i] = tmp_v[0];
        v[i + 1] = tmp_v[1];
    }

    decipher(r, v, k);
    for (auto i : v)
    {
        while (i != 0)
        {
            printf("%c", i & 0xff);
            i >>= 8;
        }
    }
    return 0;
}
点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖