CISCN 2019 reverse Writeup

ciscn 2019 reverse writeup [1]

正常的逆向(大雾)好像就只有两个。。另外还有一个apk、一个wasm、还有一个MBR。。。这三个后续慢慢来补,需要的知识可能有点多,先把两个相对简单的题写一下。

easygo

从题目就可以看出来这是个go语言的题目,打开一看是去符号表的逆向,IDA里面通过脚本恢复符号表开始看代码

https://github.com/sibears/IDAGolangHelper

跟到main.main看到关键的比较函数:

gdb跟一下,在0x495318地址处下断点即可获得明文的flag:

bbvvmm

一道让人头疼的题目,根据题目名称大概猜到是一道vm的题目,也就是在前一天刚刚正在研究reversing.kr上的另一道vm题目,有点头大。先说说这个题目把。
拖进ida里面,先分析一下:

整个程序我们分为两部分来看,一部分校验用户名,结果存储在v5中,一部分校验密码,结果存储在*(ptr+25)v8里面。最后两者检验均通过之后链接上服务器就能打印flag

先看对用户名的验证,简单跟一下我们的输入经过了这么些函数sub_4066C0(encode_hex) -> sub_4018C4(sm4_en) -> sub_4067BD(encode_hex) -> sub_400AA6(base64) -> strcmp

首先sub_4066C0将我们的输入进行了encode('hex')操作,然后使用sm4加密算法进行加密,而密钥就是中间一长串赋值操作,从v17-v32,共16字节的秘钥,然后加密之后进入了一个魔改的base64函数,方式没变,替换了原本base64的编码表。然后和RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y=进行比较,那我们就可以逆着来就能得到用户名,脚本如下:

import binascii
base64_charset = 'IJLMNOPKABDEFGHCQRTUVWXSYZbcdefa45789+/6ghjklmnioprstuvqwxz0123y'
def b64_decode(base64_str):

    base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if
                    s != '=']
    resp = bytearray()
    nums = len(base64_bytes) // 4
    remain = len(base64_bytes) % 4
    integral_part = base64_bytes[0:4 * nums]

    while integral_part:
        tmp_unit = ''.join(integral_part[0:4])
        tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
        for i in tmp_unit:
            resp.append(i)
        integral_part = integral_part[4:]

    if remain:
        remain_part = ''.join(base64_bytes[nums * 4:])
        tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
        for i in tmp_unit:
            resp.append(i)

    return resp

from sm4 import encrypt, decrypt
from Crypto.Util.number import bytes_to_long, long_to_bytes
mk=0xda98f1da312ab753a5703a0bfd290dd6

data=int("0x"+b64_decode('RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y=').decode('utf-8'),16)
print long_to_bytes(decrypt(data,mk)).decode('hex')

其中的sm4就是从 https://github.com/yang3yen/pysm4这里随便拿的一个sm4.py脚本。
得到用户名为badrer12
用户名的逆向相对简单,但是密码的逆向就麻烦了。main函数调用了sub_405B25,初始化了一个handler,这里一个byte对应一个函数

然后sub_406607开始扫描执行,执行的就是从0x6090e0开始的这部分代码:

而在这些函数中势必有读取我们输入的密码,

所以需要开始分析虚拟机指令对应的函数。不过学弟他们是用angr一会儿就能跑出来。另外想了下似乎可以patch掉system('exit'),然后让它返回到输入来,这样就是本地70亿次的爆破,似乎是可行的。

那既然是复现还是分析下虚拟机。

一下午加一晚上过去了,我分析虚拟机回来了。。。。

分析的脑壳都大了,就是得耐着性子挨个指令看,vm的题做的比较少,所以这道题多花了心血认真分析了。几个比较核心的点,首先0x609240处存储目前的栈顶指针,0x60a4f0则是直接的栈底指针,核心的handler的结构体0x60a010,结果存储在*(0x60a010+0x24)+0x64的地方,也就是最后验证的结果。
整个逻辑分析下来很简单,就是六位输入满足x1^0x78+x2^0x79+x3^0x7a+x4^0x7b+x5^0x7c+x6^0x7d即可,所以x1-x6分别为0x78-0x7d即可,所以输入为xyz{|}

贴一下笔记吧:

//part 0 
B0 19 00 00 00 
B5 0A
B2 0B
B4 09
B0 1A 00 00 00 
B5 0A 
04 0B 09 
B0 1A 00 00 00 
B5 0A 
B2 0B  
B4 09 
90 C2 00 00 00 91   //jmp part 2

//part 1
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
02 09 00                //*(a1)=*(*(a1+0x24)+0x68)
10 09 30 00 00 00 01    // judge(*a1+0x24)!=0 ? *(a1+4)=0x0060b5c0 : *(a1+4)=0x0060b530 
B2 01                   //*a1+4 入栈 
B2 00                   //*a1 入栈
C0                      //judge(*a1)!=0,*a1=4**a1+*a1+4 : *a1+=*a1+4
B5 00                   //*a1 <- 栈顶
B0 F4 FF FF FF          // -c 入栈
B5 0A                   //a1+0x28= -c 
B1 00                   // password逐个取到栈顶
B5 01                   // a1+0x4=password
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
B1 09                   // *[*(a1+0x24)+0x68] 入栈
B5 00                   //*a1 <- 栈顶
10 00 78 00 00 00 00    //judge(*a1)!=0? *a1+=4*0x78:*a1+=0x78;
70 00 FF 00 00 00 00    //*a1=*a1 & 0xff
50 00 18 00 00 00 00    //*a1=*a1 << 0x18
B2 00                   // *a1 入栈
B0 18 00 00 00          // 0x18 入栈
C8                      // *a1 >> 0x18 
B5 00                   // *a1 <- 栈顶
B2 01                   // *a1+0x4 入栈(password)
B2 00                   // *a1 入栈
C3                      // *a1 ^ *a1+0x4
B5 00                   // *a1 <- 栈顶 
50 00 18 00 00 00 00    // *a1=*a1 << 0x18
B2 00                   // *a1 入栈
B0 18 00 00 00          // 0x18 入栈
C8                      // *a1 >> 0x18
B5 00                   // *a1 <- 栈顶 
70 00 FF 00 00 00 01    //*a1+4=*a1 & 0xff 
01 19 00 00 00 0A       //*a1+0x28=0x19
02 09 00                //*(a1)=*(*(a1+0x24)+0x64)
11 01 00 00             // judge(*(a1+4))!=0? *a1=*(a1+4) + 4**a1: *a1=*(a1+4) +*a1
B0 19 00 00 00          // 0x19 入栈
B5 0A                   // *a1+0x28 = 0x19
B2 00                   // *a1 入栈
B4 09                   // *[*(a1+0x24)+0x64]=栈顶
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
B1 09                   //*[*(a1+0x24)+0x68] 入栈
B5 00                   // *a1 <- 栈顶
10 00 01 00 00 00 00    // judge(*a1)!=0? *a1+=4*0x01:*a1+=0x01;
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
04 00 09                //*[*(a1+0x24)+0x68]=*a1

//part 2
B0 1A 00 00 00          // 0x1A 入栈
B5 0A                   // *a1+0x28 = 0x1A
02 09 00                // *(a1)=*(*(a1+0x24)+0x64)
86 00 06 00 00 00 00    // *a1 = len < 6 ;限定长度为6,即循环6次就ok
88 00 26 00 00 00 91    //jmp part 1
FF
B0:   *tmp=*(pc+1);tmp+=4;pc+=5;    push 立即数指令
B5:   *(a1+4*op1)=*tmp;tmp-=4;pc+=2;    pop 指令
B2:   *tmp=*(a1+4*op1);tmp+=4;pc+=2;    push 指令
B4:   *(a1+0x28)<=1000 ? tmp-=4; *[*(a1+4*op1)  + 4 * [*(a1+0x28)] ] = *tmp   : *[*(a1+4*op1) - [*(a1+0x28)] ] =*tmp   ;   pop指令
04:  *(a1+0x28)<=1000 ? *[a1+4*op2 + 4*[*(a1+0x28)]]=*(a1+4*op1):*[*(a1+4*op2) - 4*[*(a1+0x28)]]=*(a1+4*op1)
90:   pc+=*(a1+0x30)+op1
02:   *(a1+0x28)<=1000 ? *(a1+4*op2)=*[*(a1+4*op1) + 4*[*(a1+0x28)]] :*(a1+4*op2)=*[*(a1+4*op1) - 4*[*(a1+0x28)]]
86:   *(a1+4*op6)=*(a1+4*op1) < op2
88:   *(a1+4*op1) != 0 ? pc = *(a1+0x30+4*op1) : pc=op6
01:   *(a1+4*op5)=op1
10:   judge(*(a1+4*op1))!=0 ? *(a1+4*op6)=*(a1+4*op1)+4*op2 : *(a1+4*op6)=*(a1+4*op1)+op2 
C0:   judge(*(tmp-1))!=0 ? *(tmp-1)=4* *(tmp-1) + *(tmp-2) : *(tmp-1)=*(tmp-1) + *(tmp-2) 
B1:   *(a1+0x28)<=1000 ? tmp=*[a1+4*op1  + 4 * [*(a1+0x28)] ]   : tmp=*[a1+4*op1 - 4*[*(a1+0x28)] ]   ;tmp+=4; 
70:   *(a1+4*op6)=*(a1+4*op1) & op2
50:   *(a1+4*op6)=*(a1+4*op1) << op2
C8:   *(tmp-1) = *(tmp-2) >> *(tmp-1);
C3:   *(tmp-1) ^= *(tmp-2); 
11:   judge(*(a1+4*op1))!=0 ? *(a1+4*op3)=*(a1+4*op1)+4**(a1+4*op2) : *(a1+4*op3)=*(a1+4*op1)+*(a1+4*op2)