记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

2021 羊城杯 Babyvm Writeup

2021-10-03 10:36
robots

 

基本分析

程序逻辑

  • 虚拟机类题目
  • 首先初始化 engine 实例。这里建立了一个 engine_t 结构体使得代码的可读性更高,并且可以根据语义推测出每个字段是什么
  • 然后有个 SMC 自修改,但是这里并没有反调,所以直接步过跳过去。然后可以把修改后的代码 dump 下来,再 patch 回去即可。最后再把对 SMC 函数的调用 nop 掉即可
      import idc
    start_addr = 0x80487A8
    end_addr = 0x8048F45

    def dump():
    data = idc.get_bytes(start_addr, end_addr-start_addr)
    print(data)

    def patch(data):
    for i in range(len(data)):
    idc.del_items(start_addr+i)
    for i, b in enumerate(data):
    idc.patch_byte(start_addr+i, b)
  • exec_engine 就是一个 while 循环,里面模拟 CPU 进行执行
  • 虚拟机类题目理解题意并不是很难,主要难点是在不容易梳理出一个平坦的控制流,这里就对写脚本的能力有很大的考验
  • 可以考虑用 Python 脚本把 ip 的值和要执行的代码关联起来,然后复现一下 CPU 运行的过程,就可以得到所有运行的代码了

获取所有执行的代码

  • 使用一个 dict 做映射
      insn = b'\xa1\xc1\x00\xb1w\xc2J\x01\x00\x00\xc1\x01\xb2w\xc2\x19\x01\x00\x00\xc1\x02\xb4w\xc2\xdd\x01\x00\x00\xc1\x03\xb3w\xc2\x0f\x01\x00\x00\xc1\x04\xb2w\xc2\x1b\x01\x00\x00\xc1\x05\xb4w\xc2\x89\x01\x00\x00\xc1\x06\xb1w\xc2\x19\x01\x00\x00\xc1\x07\xb3w\xc2T\x01\x00\x00\xc1\x08\xb1w\xc2O\x01\x00\x00\xc1\t\xb1w\xc2N\x01\x00\x00\xc1\n\xb3w\xc2U\x01\x00\x00\xc1\x0b\xb3w\xc2V\x01\x00\x00\xc1\x0c\xb4w\xc2\x8e\x00\x00\x00\xc1\r\xb2w\xc2I\x00\x00\x00\xc1\x0e\xb3w\xc2\x0e\x01\x00\x00\xc1\x0f\xb1w\xc2K\x01\x00\x00\xc1\x10\xb3w\xc2\x06\x01\x00\x00\xc1\x11\xb3w\xc2T\x01\x00\x00\xc1\x12\xb2w\xc2\x1a\x00\x00\x00\xc1\x13\xb1w\xc2B\x01\x00\x00\xc1\x14\xb3w\xc2S\x01\x00\x00\xc1\x15\xb1w\xc2\x1f\x01\x00\x00\xc1\x16\xb3w\xc2R\x01\x00\x00\xc1\x17\xb4w\xc2\xdb\x00\x00\x00\xc1\x18\xb1w\xc2\x19\x01\x00\x00\xc1\x19\xb4w\xc2\xd9\x00\x00\x00\xc1\x1a\xb1w\xc2\x19\x01\x00\x00\xc1\x1b\xb3w\xc2U\x01\x00\x00\xc1\x1c\xb2w\xc2\x19\x00\x00\x00\xc1\x1d\xb3w\xc2\x00\x01\x00\x00\xc1\x1e\xb1w\xc2K\x01\x00\x00\xc1\x1f\xb2w\xc2\x1e\x00\x00\x00\xc1 \x80\x02\x18\x00\x00\x00#\x10\xc1!\x80\x02\x10\x00\x00\x00#\xf7\xc1"\x80\x02\x08\x00\x00\x00#\xf7\xc1#\xf7\xfe\x80\x02\x05\x00\x00\x00"w\x10\x80\x02\x07\x00\x00\x00#\x80\x02#w\xf1\x981w\x10\x80\x02\x18\x00\x00\x00#\x80\x02 \xb9\xe451w\x10\x80\x02\x12\x00\x00\x00"w\xa0\xc1$\x80\x02\x18\x00\x00\x00#\x10\xc1%\x80\x02\x10\x00\x00\x00#\xf7\xc1&\x80\x02\x08\x00\x00\x00#\xf7\xc1\'\xf7\xfe2 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\x102 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\xc7\xc1(\x80\x02\x18\x00\x00\x00#\x10\xc1)\x80\x02\x10\x00\x00\x00#\xf7\xc1*\x80\x02\x08\x00\x00\x00#\xf7\xc1+\xf7\xfe2 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\x102 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\xc8\x99'
    code = {}
    code[0x71] = ('*--_sp = *(_DWORD *)(_ip + 1);_ip += 5;', 5)
    code[0x41] = ('r[1] += r[2];_ip += 1;', 1)
    code[0x42] = ('r[1] -= r[4];_ip += 1;', 1)
    code[0x43] = ('r[1] *= r[3];_ip += 1;', 1)
    code[0x37] = ('r[1] = r[5];_ip += 1;', 1)
    code[0x38] = ('r[1] ^= r[4];_ip += 1;', 1)
    code[0x39] = ('r[1] ^= r[5];_ip += 1;', 1)
    code[0x35] = ('r[5] = r[1];_ip += 1;', 1)
    code[0xf7] = ('r7 += r[1];_ip += 1;', 1)
    code[0x44] = ('r[1] /= r[5];_ip += 1;', 1)
    code[0x80] = ('r[_ip[1]] = *(_DWORD *)(_ip + 2);_ip += 6;', 6)
    code[0x77] = ('r[1] ^= r7;_ip += 1;', 1)
    code[0x53] = ('putchar(*(char *)r[3]);_ip += 2;', 2)
    code[0x22] = ('r[1] = r[1] >> r[2];_ip += 1;', 1)
    code[0x23] = ('r[1] <<= r[2];_ip += 1;', 1)
    code[0x76] = ('r[3] = *_sp;*_sp++ = 0;_ip += 5;', 5)
    code[0x54] = ('v2 = &r[3];*v2 = getchar();_ip += 2;', 2)
    code[0x30] = ('r[1] |= r[2];_ip += 1;', 1)
    code[0x31] = ('r[1] &= r[2];_ip += 1;', 1)
    code[0x32] = ('r[3] = _ip[1];_ip += 2;', 2)
    code[0x9] = (' r[1] = 0x6FEBF967;_ip += 1;', 1)
    code[0x10] = ('r7 = r[1];_ip += 1;', 1)
    code[0x33] = ('r[4] = r[1];_ip += 1;', 1)
    code[0x34] = ('r[2] = _ip[1];_ip += 2;', 2)
    code[0xfe] = ('r[1] = r7;_ip += 1;', 1)
    code[0x11] = ('printf("%x\n", r[1]);_ip += 1;', 1)
    code[0xa0] = ('assert(r[1] == 0x6FEBF967);_ip += 1;', 1)
    code[0xa1] = ('read;_ip += 1;', 1)
    code[0xb1] = ('r7 = key[0];_ip += 1;', 1)
    code[0xb2] = ('r7 = key[1];_ip += 1;', 1)
    code[0xa4] = ('key[_ip[1]] = r[1];_ip += 4;', 4)
    code[0xb3] = ('r7 = key[2];_ip += 1;', 1)
    code[0xb4] = ('r7 = key[3];_ip += 1;', 1)
    code[0xc1] = ('r[1] = plain[_ip[1]];_ip += 2;', 2)
    code[0xc7] = ('assert(cipher1==r[1]);_ip += 1;', 1)
    code[0xc8] = ('assert(cipher2==r[1]);_ip += 1;', 1)
    code[0xc2] = ('assert(_ip[1] == r[1]);_ip += 5;', 5)
    code[0x99] = ('break;_ip += 10;', 10)

    ip = 0
    parsed = ""
    while ip < len(insn):
    parsed += code[insn[ip]][0]
    ip += code[insn[ip]][1]
    with open("parsed.c", "w")as f:
    f.write("""#include<stdio.h>
    #include<stdint.h>
    int main(void){
    %s
    }""" % parsed)
  • 得到 C 代码后手动对其进行调整,代码量略大,所以这里贴到 Github gist 上了2021.09.12-YangCheng-Babyvm.c

 

关键逻辑

Part 1

  • 前面部分的代码大致都是这样
      ip += 1;
    r[1] = plain[ip[1]];
    ip += 2;
    r7 = key[0];
    ip += 1;
    r[1] ^= r7;
    ip += 1;
    assertEqual(ip[1], r[1]);
  • 可以看到,每次就是取一个 byte 然后进行运算,亦或一个 key,与密文比较
  • 因为我们把输入设置为了 44 个 0,所以这里直接亦或 assertEqual 传入的两个值就可以得到前 32 个 byte
      void assertEqual(uint32_t a, uint32_t b) {
    uint8_t res = a == b;
    if (!res) {
    printf("%c", (char)(a ^ b ^ 0));
    }
    }
  • 得到前 32 byte 是 16584abc45baff901c59dde3b1bb6701

Part 2

  • 后面逻辑就复杂很多了。这时候变成了每次取 4 个 byte 按照大端序转换成 int,再运算并进行比较
  • 这里首先试了试 z3 能不能解出来,发现不行,但是由于输入的字符都是 16 进制字符(0..9...f),所以可以考虑爆破出来
  • 爆破的话选择了多线程比较 nb 的 Golang
  • 这里还有一个不太好处理的问题就是运算的时候从指令里面取了立即数。这里我选择动调然后把取的立即数 copy 出来

 

脚本

  • 前 32 字节直接运行 C 代码就能得到
  • 后 12 字节通过 Golang 爆破
      package main

    import (
    "encoding/binary"
    "fmt"
    "sync"
    )

    type Caculator func(uint32) uint32

    type Task struct {
    calc_f Caculator
    plain uint32
    cipher uint32
    solved bool
    }

    func main() {
    ciphers := []uint32{0x6FEBF967, 0x0CF1304DC, 0x283B8E84}
    caculators := []Caculator{calc0, calc1, calc2}
    tasks := make([]Task, 0)
    for i := 0; i < len(ciphers); i++ {
    tasks = append(tasks, Task{
    calc_f: caculators[i],
    cipher: ciphers[i],
    solved: false,
    })
    }
    brute(tasks)
    for i := 0; i < len(ciphers); i++ {
    fmt.Print(unpack(tasks[i].plain))
    }
    }

    func brute(tasks []Task) {
    var a, b, c, d uint8
    alphabet := []byte("0123456789abcdef")
    wg := sync.WaitGroup{}

    for _, a = range alphabet {
    for _, b = range alphabet {
    for _, c = range alphabet {
    for _, d = range alphabet {
    guess := pack(a, b, c, d)
    for i := 0; i < 3; i++ {
    if !tasks[i].solved {
    wg.Add(1)
    tI := i
    go func() {
    defer wg.Done()
    result := tasks[tI].calc_f(guess)
    if result == tasks[tI].cipher {
    tasks[tI].solved = true
    tasks[tI].plain = guess
    fmt.Println(guess)
    }
    }()
    }
    }
    }
    }
    }
    }
    wg.Wait()
    }

    func pack(a, b, c, d uint8) uint32 {
    raw := []uint8{a, b, c, d}
    return binary.BigEndian.Uint32(raw)
    }

    func unpack(val uint32) string {
    bs := make([]byte, 4)
    binary.BigEndian.PutUint32(bs, val)
    return string(bs)
    }

    func calc0(guess uint32) uint32 {
    r := make([]uint32, 6)
    r7 := guess
    r[1] = r7
    r[1] >>= 5
    r[1] ^= r7
    r7 = r[1]
    r[1] <<= 7
    r[1] &= 2565961507
    r[1] ^= r7
    r7 = r[1]
    r[1] <<= 24
    r[1] &= 904182048
    r[1] ^= r7
    r7 = r[1]
    r[1] >>= 18
    r[1] ^= r7
    return r[1]
    }

    func calc1(guess uint32) uint32 {
    r := make([]uint32, 6)
    r7 := guess
    r[1] = r7
    r[3] = 32
    r[1] *= r[3]
    r[4] = r[1]
    r[1] ^= r7
    r[2] = 17
    r[1] >>= r[2]
    r[5] = r[1]
    r[1] = r[5]
    r[1] ^= r[4]
    r[1] ^= r7
    r[2] = 13
    r[1] <<= r[2]
    r[1] ^= r7
    r[1] ^= r[4]
    r[1] ^= r[5]
    r7 = r[1]
    r[3] = 32
    r[1] *= r[3]
    r[4] = r[1]
    r[1] ^= r7
    r[2] = 17
    r[1] >>= r[2]
    r[5] = r[1]
    r[1] = r[5]
    r[1] ^= r[4]
    r[1] ^= r7
    r[2] = 13
    r[1] <<= r[2]
    r[1] ^= r7
    r[1] ^= r[4]
    r[1] ^= r[5]
    return r[1]
    }

    func calc2(guess uint32) uint32 {
    r := make([]uint32, 6)
    r7 := guess
    r[1] = r7
    r[3] = 32
    r[1] <<= 5
    r[4] = r[1]
    r[1] ^= r7
    r[2] = 17
    r[1] >>= r[2]
    r[5] = r[1]
    r[1] = r[5]
    r[1] ^= r[4]
    r[1] ^= r7
    r[2] = 13
    r[1] <<= r[2]
    r[1] ^= r7
    r[1] ^= r[4]
    r[1] ^= r[5]
    r7 = r[1]
    r[3] = 32
    r[1] <<= 5
    r[4] = r[1]
    r[1] ^= r7
    r[2] = 17
    r[1] >>= r[2]
    r[5] = r[1]
    r[1] = r[5]
    r[1] ^= r[4]
    r[1] ^= r7
    r[2] = 13
    r[1] <<= r[2]
    r[1] ^= r7
    r[1] ^= r[4]
    r[1] ^= r[5]
    return r[1]
    }
  • 得到后 12 byte 是 a254b06cdc23

 

总结

  • 这题比赛的时候做了挺久,而且因为种种原因是熬夜做的…当时做得很难受,主要就是比较忙乱
  • 所以就意识到虚拟机类题目一定不要想着投机取巧,比如直接黑盒啥的。最通用的方法就是还原所有执行的代码,然后再当成普通题目解

知识来源: https://www.anquanke.com/post/id/253155

阅读:579113 | 评论:0 | 标签:无

想收藏或者和大家分享这篇好文章→复制链接地址

“2021 羊城杯 Babyvm Writeup”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

黑帝公告 📢

十年经营持续更新精选优质黑客技术文章Hackdig,帮你成为掌握黑客技术的英雄

🙇🧎¥由自富财,长成起一↓

❤用费0款退球星,年1期效有员会

标签云 ☁

本页关键词 💎