D3piano

按正确的音色弹琴拿到flag。

逻辑在so里面,加载了D3piano。查看后看字符串可以定位到check函数。

里面是一个gmp库实现的ras校验,获取公钥即可解密。这里用fridahook获取。

因为是大数,寄存器里应该是一个结构体,这里我们用frida调用gmpz_get_str把hook的数据转为十六进制。

有frida检测,在player.so的init_array里面,hook去掉检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//frida version 17.1.0

function reada(){
var f = File.readAllText("/proc/self/task");
console.log(f)
}

function findSo(name){
var lib=null;
console.log(`finding ${name}`);
try {
lib = Process.findModuleByName(name)
if (lib === null) {
setTimeout(findSo,200,name);
}
else {
console.log(`found ${name} at ${lib.base}`);
return lib;
}
} catch { }
}

function hookI(name) {
var lib = findSo(name);
try {
var destFuncAddr = lib.base.add(0x08922A0);
Interceptor.replace(destFuncAddr, new NativeCallback(function () {
console.log(`replace: ${name} func: ${destFuncAddr}`);
return 0;
}, 'int', []))
} catch { }
}

function hookInitArray(name) {
var linkermodule = Process.getModuleByName("linker64");
var call_function_addr = null;
var symbols = linkermodule.enumerateSymbols();

for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
// console.log(symbol.name);

if (symbol.name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
call_function_addr = symbol.address;
console.log("call_function_addr:" + call_function_addr);
Interceptor.attach(call_function_addr, {
onEnter: function (args) {
console.log("call_constructors");
hookI(name);
}
})
return;

}
}
}

function hook(){
var addr = null;
try {
var module = Process.getModuleByName("libc.so");
addr = module.getExportByName("dlopen");
} catch (e) {
console.log(e);
}
console.log("dlopen:",addr);
Interceptor.attach(addr,{
onEnter: function(args){
var loadName = args[0].readCString();
console.log("dlopen: ",loadName);
},
onLeave: function (retval) {
console.log("handle:", retval);
}
});

var android_dlopen_ext = module.getExportByName("android_dlopen_ext");
console.log("android_dlopen_ext:",android_dlopen_ext);
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
this.call_hook = false;
var so_name = ptr(args[0]).readCString();
console.log("android_dlopen_ext:", so_name);
},
onLeave: function (retval) {}
});
}
function getpq(){
var libapp = findSo("libD3piano.so").base;
const fn_addr = 0x29AA0;
console.log("hook"+libapp);
Interceptor.attach(libapp.add(fn_addr), {
onEnter: function () {
try{
var x1 = this.context.x1;
var x2 = this.context.x2;
var gmpz_get_str = new NativeFunction(libapp.add(0x5CFD0),"pointer",["pointer","int","pointer"]);
var mallocAddrp = Memory.alloc(0x1000);
var mallocAddrq = Memory.alloc(0x1000);
var p = gmpz_get_str(mallocAddrp, 16, x1);
var q = gmpz_get_str(mallocAddrq, 16, x2);
console.log(mallocAddrp.readCString());
console.log(mallocAddrq.readCString());

}catch(e){
console.log(e);
}
}
});
}

hookInitArray('libMediaPlayer.so'); //hook initarray去掉反调试

getpq();

拿到pq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import long_to_bytes
import gmpy2

p = 0xcd88775691357147eea5dc584718edab9ca314cdd52a8c1cf847dbbb8371798f15e9bdca2bfaa4595d47eecae21bea38691a26e1c707867b5ea2f6f2f03bf4d
q = 0x565c0138487b57e4b76d0924163f67facb17a77f83e354cc3c8432879dab4611c2442cdd73f71c9e6cb4e56a7c45a403148e6d558f986ec6505882ae095c34d3

n = p * q

e = gmpy2.mpz(65537)

enc = 0xc901acacbb426c9c447acda82513965ccc3faf6c9dc58d24ed34b62c7fb1548f9ad06b9355c7d20704cfdfdfc89a3f893801e31719564683fdc7de26d807ed27f898edb3efd51b6e8e2a192d6a0929554342adfed541cd8399da0fbacfeaa5b608b887fd74f4f0e31f9bb5816c54163b8e46d27553798233bef6eaf848c64e

m = pow(enc, e, n)
plaintext = long_to_bytes(m)

print(plaintext)

#b'This_is_a_fake_flag'

解密到fake flag。

仔细看init_array中的内容可知它内置了一个frida,把加密函数和获取声音的函数全部hook了。我们要去寻找hook的listener来看hook后的操作。

根据多线程的信号量可以知道执行顺序。

listener1 hook获取sound顺序的函数的返回值,让sound的顺序固定。

listener2 hook check调用两个虚表函数进行检测,onLeave修改原函数的返回值。

listener3 hook 十二进制转str的函数,把结果转为16进制字符串。

check:

LZW压缩算法。

密钥加密和魔改chacha20。

只要先把chacha20解密出来就能看出是压缩算法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

#include "include/CTFCPP.h"
#include "include/ChaCha.h"
#include "include/LZW.h"
#include <unordered_map>

using namespace std;

int main() {

string key = "welCoME_70_D3c7F_2025-r3VERSE!!!";
string nonce = "CDEFGABdegab";
vector<uint8_t> nonce1(13);
copy(nonce.begin(), nonce.end(), nonce1.begin());
vector<uint8_t> key1(33);
copy(key.begin(), key.end(), key1.begin());
vector<uint8_t> enc1 = {0x2E, 0xD2, 0xDF, 0x53, 0x41, 0xE6, 0x51, 0xA2, 0xD0, 0x8E, 0x43, 0x59, 0x6F, 0xC4, 0x15, 0xAD,
0x97, 0xC2, 0x98, 0xBD, 0x11, 0x05, 0xFE, 0xFF, 0x96, 0x4C, 0xE8, 0x06, 0x50, 0x0E, 0x1D, 0xCA,
0x0E, 0xB2, 0x18, 0xCA, 0x06, 0x54, 0x2E, 0xFA, 0xCD, 0x19, 0xD2, 0x9E, 0xDB, 0x9E, 0x33, 0xCC,
0x5D, 0xAF, 0xED, 0x69, 0x4A, 0xEF, 0x17, 0xB8, 0xD8, 0x40, 0x14, 0x48, 0xCD, 0x37, 0xFC, 0xD0,
0x14, 0x5C, 0x3C, 0x31, 0xC9, 0x15, 0xE6, 0xCF, 0x77, 0x28};

Chacha::ChaChaEncrypt32(key1,nonce1,enc1,0x221221);

vector<uint8_t> res = LZW::LZW_decode(enc1);
string s(res.begin(), res.end());
cout << s << endl;
}

chacha20,注释的地方就是bia原来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#include "ChaCha.h"

namespace Chacha
{

static inline void u32t8le(uint32_t v, uint8_t p[4])
{
p[0] = v & 0xff;
p[1] = (v >> 8) & 0xff;
p[2] = (v >> 16) & 0xff;
p[3] = (v >> 24) & 0xff;
}

static inline uint32_t u8t32le(uint8_t p[4])
{
uint32_t value = p[3];

value = (value << 8) | p[2];
value = (value << 8) | p[1];
value = (value << 8) | p[0];

return value;
}

static inline uint32_t rotl32(uint32_t x, int n)
{
// http://blog.regehr.org/archives/1063
return x << n | (x >> (-n & 31));
}

// https://tools.ietf.org/html/rfc7539#section-2.1
static void chacha20_quarterround(uint32_t *x, int a, int b, int c, int d)
{
x[a] += x[b];
x[d] = rotl32(x[d] ^ x[a], 16);
x[c] += x[d];
x[b] = rotl32(x[b] ^ x[c], 12);
x[a] += x[b];
x[d] = rotl32(x[d] ^ x[a], 8);
x[c] += x[d];
x[b] = rotl32(x[b] ^ x[c], 7);
}

static void chacha20_serialize(uint32_t in[16], uint8_t output[64])
{
int i;
for (i = 0; i < 16; i++)
{
u32t8le(in[i], output + (i << 2));
}
}

static void chacha20_block(uint32_t in[16], uint8_t out[64], int num_rounds)
{ // num_rounds 一般为20
int i;
uint32_t x[16];

memcpy(x, in, sizeof(uint32_t) * 16);

for (i = num_rounds; i > 0; i -= 2)
{
// // odd round
// chacha20_quarterround(x, 0, 4, 8, 12);
// chacha20_quarterround(x, 1, 5, 9, 13);
// chacha20_quarterround(x, 2, 6, 10, 14);
// chacha20_quarterround(x, 3, 7, 11, 15);
// // even round
// chacha20_quarterround(x, 0, 5, 10, 15);
// chacha20_quarterround(x, 1, 6, 11, 12);
// chacha20_quarterround(x, 2, 7, 8, 13);
// chacha20_quarterround(x, 3, 4, 9, 14);

// odd round
chacha20_quarterround(x, 0, 4, 8, 12);
chacha20_quarterround(x, 5, 9, 13, 1);
chacha20_quarterround(x, 10, 14, 2, 6);
chacha20_quarterround(x, 15, 3, 7, 11);
// even round
chacha20_quarterround(x, 0, 1, 2, 3);
chacha20_quarterround(x, 5, 6, 7, 4);
chacha20_quarterround(x, 10, 11, 8, 9);
chacha20_quarterround(x, 15, 12, 13, 14);
}

for (i = 0; i < 16; i++)
{
x[i] += in[i];
}

chacha20_serialize(x, out);
}

// https://tools.ietf.org/html/rfc7539#section-2.3
static void chacha20_init_state(uint32_t s[16], uint8_t key[32], uint32_t counter, uint8_t nonce[12])
{
int i;

// refer: https://dxr.mozilla.org/mozilla-beta/source/security/nss/lib/freebl/chacha20.c
// convert magic number to string: "expand 32-byte k"
// s[0] = 0x61707865;
// s[1] = 0x3320646e;
// s[2] = 0x79622d32;
// s[3] = 0x6b206574;

// for (i = 0; i < 8; i++)
// {
// s[4 + i] = u8t32le(key + i * 4);
// }

// s[12] = counter;

// for (i = 0; i < 3; i++)
// {
// s[13 + i] = u8t32le(nonce + i * 4);
// }
// s[0] = 0x61707865;
// s[1] = 0x3320646e;
// s[2] = 0x79622d32;
// s[3] = 0x6b206574;
// s[4] = *(uint32_t *)&key[0];
// s[5] = *(uint32_t *)&key[4];
// s[6] = *(uint32_t *)&key[8];
// s[7] = *(uint32_t *)&key[12];
// s[8] = *(uint32_t *)&key[16];
// s[9] = *(uint32_t *)&key[20];
// s[10] = *(uint32_t *)&key[24];
// s[11] = *(uint32_t *)&key[28];
// s[12] = (uint32_t)(counter & 0xFFFFFFFF);
// s[13] = (uint32_t)(counter >> 32);
// s[14] = *(uint32_t *)&nonce[0];
// s[15] = *(uint32_t *)&nonce[4];

s[0] = 0x61707865;
s[1] = *(uint32_t *)&key[0];
s[2] = *(uint32_t *)&key[4];
s[3] = *(uint32_t *)&key[8];
s[4] = *(uint32_t *)&key[12];
s[5] = 0x3320646e;
s[6] = *(uint32_t *)&nonce[0];
s[7] = *(uint32_t *)&nonce[4];
s[8] = (uint32_t)(counter & 0xFFFFFFFF);
s[9] = (uint32_t)(0);
s[10] = 0x79622d32;
s[11] = *(uint32_t *)&key[16];
s[12] = *(uint32_t *)&key[20];
s[13] = *(uint32_t *)&key[24];
s[14] = *(uint32_t *)&key[28];
s[15] = 0x6b206574;
}

void ChaCha20XOR(uint8_t key[32], uint32_t counter, uint8_t nonce[12], uint8_t *in, int inlen, int rounds)
{
int i, j;

uint32_t s[16];
uint8_t block[64];

chacha20_init_state(s, key, counter, nonce);

for (i = 0; i < inlen; i += 64)
{
chacha20_block(s, block, 20);
// s[12]++;
s[8]++;

for (j = i; j < i + 64; j++)
{
if (j >= inlen)
{
break;
}
in[j] ^= block[j - i];
}
}
}

void ChaChaEncrypt16(std::vector<uint8_t> key,std::vector<uint8_t> nonce, std::vector<uint8_t> &in,uint32_t counter, int rounds)
{
std::vector<uint8_t> key32(32);
std::copy(key.begin(), key.end(), key32.begin());
ChaCha20XOR(key32.data(), counter, nonce.data(), in.data(), in.size(), rounds);
}

void ChaChaEncrypt32(std::vector<uint8_t> key, std::vector<uint8_t> nonce, std::vector<uint8_t> &in,uint32_t counter, int rounds)
{
ChaCha20XOR(key.data(), counter, nonce.data(), in.data(), in.size(), rounds);
}

}

LZW就用官方wp里的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "LZW.h"
namespace LZW{
std::vector<uint8_t> LZW_decode(std::vector<uint8_t> &compressed)
{
std::unordered_map<int, std::vector<uint8_t>> dictionary;
for (int i = 0; i < 256; ++i)
{
dictionary[i] = {static_cast<uint8_t>(i)};
}
std::vector<uint8_t> codes;
for (uint8_t byte : compressed)
{
codes.push_back(static_cast<int>(byte));
}
std::vector<uint8_t> result;
uint8_t next_code = 0;
if (codes.empty())
return{};
uint8_t prev_code = codes[0];
result.insert(result.end(), dictionary[prev_code].begin(),
dictionary[prev_code].end());
for (size_t i = 1; i < codes.size(); ++i)
{
uint8_t curr_code = codes[i];
std::vector<uint8_t> entry;
if (curr_code == next_code)
{
entry = dictionary[prev_code];
entry.push_back(dictionary[prev_code][0]);
}
else if (dictionary.count(curr_code))
{
entry = dictionary[curr_code];
}
else
{
std::cerr << "Error: Invalid code " << curr_code << std::endl;
return{};
}
result.insert(result.end(), entry.begin(), entry.end());
std::vector<uint8_t> new_entry = dictionary[prev_code];
new_entry.push_back(entry[0]);
dictionary[next_code++] = new_entry;
prev_code = curr_code;
}
return result;
}
}

//bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAFgdgDBDBgeggbAeFagaEedbA

把十二进制转为字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import long_to_bytes

sound = "CDEFGABdegab"
m = "0123456789AB"

melody = "bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAFgdgDBDBgeggbAeFagaEedbA"

table = {}
res =""

for i,j in zip(sound, m):
table[i] = j

for i in melody:
res += table[i]

res = int(res, 12)

print(long_to_bytes(res))

#b'Fly1ng_Pi@n0_Key$_play_4_6e@utiful~melody'

D3Kernel

用层通过通过动态解析函数地址来隐藏了函数的调用,暂时没发现反调试,动调看一下函数调用

从上往下依次调用

kernelbase_GetModuleHandleA(Wkernel32.dll) 加载dll

kernel32_GetProcAddress() 获取api

ntdll_RtlAddVectoredExceptionHandler() 设置异常处理函数,这里我重名名为except_func1

kernel32_VirtualAlloc() 动态分配一段内存

shellcode 触发除0异常

后执行的shellcode触发异常使得ntdll_RtlAddVectoredExceptionHandler()中的函数被执行。异常处理函数里面有很多东西没有被反编译出来。

有反调试,在_scrt_common_main_seh下断点,用scyllahide注入自带的dll可以去掉反调试。(后面找到了反调试的位置)

去除反调试加载驱动之后继续研究程序逻辑,发现跳转到了另一个异常处理函数,在第一个异常处理的函数(except_func1)的右边,重命名为except_func2

image(1)

同样的except_func2里面执行的函数也是通过kernel32_GetProcAddress() 获取的,动调进去看调用的api

从上往下是

kernel32_IsDebuggerPresent()

kernel32_CheckRemoteDebuggerPresent()

发现反调试,已经用scyllahide绕过了,不必理会

继续往下执行,发现正式开始加载驱动了,逻辑来到了except_func2的左侧,

api调用按顺序依次是

kernel32_CreateFileW(”\\.\d3ctf”)

输入name和passwd

kernel32_DeviceIoControl(IOCT:0X222000) 传入输入的数据

kernel32_DeviceIoControl(IOCT:0X222004) 校验

得到IOCT码,开始分析内核

驱动里面第二个函数就是IOCT处理函数,第一个函数是线程启动的一个反调试函数直接patch掉就行。

image.png

IOCT==0X222000时复制了用户名和密码的数据到内存中。

我们的输入被保存到140005200处并且前256字节是name,后256位是passwd。

image(3)

IOCT==0X222004 是校验逻辑

后面是一个虚拟机,分析后得到opcode。

1
2
3
4
5
code = {
"ret1": 0, "add": 1, "sub": 2, "mul": 3, "xor": 4, "low": 5, "equl": 6, "ip": 7, "top==1": 8,
"top==0": 9, "in": 10, "lea": 11, "lea1": 12, "str": 13, "str1": 14, "pop": 15, "call": 16,
"ret2": 17,"ret3": 18,"jmp": 19, "jnz": 20, "jz": 21,"lea2": 22, "str2": 23,"smc": 24
}

调用逻辑如下

image.png

直接提取最后的opcode查看加密逻辑

image(5)

1
unsigned int opc[] = {0x0000000a,0x00000000,0x00000018,0x005801c0,0xffa7fe4e,0x00000000,0x0000000a,0x00000000,0x00000018,0x20160070,0xdfe9ff9e,0x00000001,0x0000000a,0x00000000,0x00000018,0x4805801c,0xb7fa7ff2,0x00000002,0x0000000a,0x00000000,0x00000018,0x12016007,0xedfea007,0x00000003,0x0000000a,0x00000064,0x00000018,0x04805801,0xfb7fa80d,0x00000004,0x0000000c,0x0000000a,0x0000000c,0x00000000,0x00000005,0x00000008,0x0000004c,0x0000000a,0x0000000a,0x0000000c,0x00000000,0x00000001,0x00000016,0x0000000e,0x00000002,0x0000000a,0x0000000b,0x0000000c,0x00000000,0x00000001,0x00000016,0x0000000e,0x00000003,0x0000000c,0x00000002,0x0000000c,0x00000003,0x00000004,0x00000017,0x00000004,0x0000000c,0x00000000,0x0000000a,0x00000001,0x00000001,0x0000000e,0x00000000,0x0000000c,0x00000004,0x0000000a,0x00000001,0x00000001,0x0000000e,0x00000004,0x00000007,0x0000001e,0x00000012,0xffffc588,0x00000000,0x55555555,0x00000150,0x00000150,0x00000000,0x00000000,0x00000006,0x00000000,0x00000000,0x00000000};

分析一下内存结构

image(6)

v13和stack2可从cal的分析得来,因为数据的加载和校验不在同一个虚拟机的函数,所以虚拟机是很可能有全局变量的。根据加载数据时的opcode(0E000000000000000Ah)发现他调用的存储是str1,这存储的就是全局变量,那么从全局变量中取出就是lea1。str2和lea2也都是在全局变量操作只不过索引不同。str和lea的存储位置和栈帧有关(在cal处可以看到)推测是函数的私有内存,而且私有内存的地址初始值是-101,只有在call的时候才会加上101,所以只有后面call的函数才有这个私有内存。

image(7)

image(8)

emm,算一下新cal的函数的内存分布。假设是第一个cal的函数:

1
2
3
4
stacklen: size
stackbase: vm1 + 3628 + 404 ; 4032
stackend: vm1 + 8*453 + 101 + (ip+3) ; 大于3717
varindex: vm1 + *ip + 4*1008 ; 4032

我们发现函数的私有内存和栈帧的起始地址是相同的,所以新call的函数就只有一块内存,一起作为栈帧和内存使用。

所以内存布置如下,因为直接用结构体调用,所以数组的大小只要不过小就行了,顺序没有要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct newStack{
int memory[1000];
int retaddr;
}

struct vm{
int* opcode;
int size;
int array[1000];
int length;
int stack[1000];
struct newStack nS[100];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
import string

opcodes = {
"ADD": 1,
"SUB": 2,
"MUL": 3,
"XOR": 4,
"LT": 5,
"EQ": 6,

"JMP": 7,
"JNZ": 8,
"JZ": 9,
"JMP_REL": 19,
"JNZ_REL": 20,
"JZ_REL": 21,

"PUSH": 10,
"POP": 15,
"LOAD_LOCAL": 11,
"STORE_LOCAL": 13,
"LOAD_GLOBAL": 12,
"STORE_GLOBAL": 14,
"LOAD_INDIRECT": 22,
"STORE_INDIRECT": 23,

"CALL": 16,
"RET": 17,
"HALT": 18,
"SMC": 24,
}


class NewStack:
def __init__(self, ret_addr=0):
self.return_ip = ret_addr
self.locals = [0] * 1000

class VM:
def __init__(self, nglobals_val=200):
self.code = []
self.code_size = 0
self.globals = [0] * nglobals_val
self.stack = []
self.call_stack = []

def set_code(self, codes, code_size=None):
self.code = codes
if code_size is not None:
self.code_size = code_size
else:
self.code_size = len(self.code)

def vm_exec(self, start_ip, smc):
ip = start_ip

# 打印global中的参数
for i in range(min(37, len(self.globals))):
print(self.globals[i], end=" ")
print()

opcode = self.code[ip]

while opcode != opcodes["HALT"] and ip < self.code_size:
ip += 1 # 指向操作数或下一条指令

if opcode == opcodes["ADD"]:
b = self.stack.pop()
a = self.stack.pop()
self.stack.append(a + b)
print(f"{a} + {b} = {a + b}")

elif opcode == opcodes["SUB"]:
b = self.stack.pop()
a = self.stack.pop()
self.stack.append(a - b)
print(f" {a} - {b} = {a - b}")

elif opcode == opcodes["MUL"]:
b = self.stack.pop()
a = self.stack.pop()
self.stack.append(a * b)
print(f" {a} * {b} = {a * b}")

elif opcode == opcodes["XOR"]:
b = self.stack.pop()
a = self.stack.pop()
self.stack.append(a ^ b)
print(f" {a} ^ {b} = {a ^ b}")

elif opcode == opcodes["LT"]:
b = self.stack.pop()
a = self.stack.pop()
self.stack.append(1 if a < b else 0)
print(f" {a} < {b} == {a<b}")

elif opcode == opcodes["EQ"]:
b = self.stack.pop()
a = self.stack.pop()
self.stack.append(1 if a == b else 0)
print(f" {a} == {b} == {a == b}")

elif opcode == opcodes["JMP"]:
target_ip = self.code[ip]
ip = target_ip

elif opcode == opcodes["JNZ"]:
addr = self.code[ip]
ip += 1
val = self.stack.pop()
if val == 1:
ip = addr

elif opcode == opcodes["JZ"]:
addr = self.code[ip]
ip += 1
val = self.stack.pop()
if val == 0:
ip = addr

elif opcode == opcodes["JMP_REL"]:
offset_val = self.code[ip]
ip = (ip - 1) + offset_val

elif opcode == opcodes["JNZ_REL"]:
offset_val = self.code[ip]
target_addr = (ip - 1) + offset_val
ip += 1
val = self.stack.pop()
if val == 1:
ip = target_addr

elif opcode == opcodes["JZ_REL"]:
offset_val = self.code[ip]
target_addr = (ip - 1) + offset_val
ip += 1
val = self.stack.pop()
if val == 0:
ip = target_addr

elif opcode == opcodes["PUSH"]:
value = self.code[ip]
ip += 1
self.stack.append(value)

elif opcode == opcodes["LOAD_LOCAL"]:
offset = self.code[ip]
ip += 1
current_context = self.call_stack[-1] # 加载当前上下文,即最里层函数
self.stack.append(current_context.locals[offset])

elif opcode == opcodes["LOAD_GLOBAL"]:
addr = self.code[ip]
ip += 1
self.stack.append(self.globals[addr])
print(f"LoadGlobal [{addr}] = {self.globals[addr]}")

elif opcode == opcodes["LOAD_INDIRECT"]:
addr_from_stack = self.stack[-1]
value_from_globals = self.globals[addr_from_stack]
self.stack[-1] = value_from_globals
print(f"Load Global [{addr_from_stack}] = {value_from_globals}")

elif opcode == opcodes["STORE_LOCAL"]:
offset = self.code[ip]
ip += 1
value_to_store = self.stack.pop()
current_context = self.call_stack[-1]
current_context.locals[offset] = value_to_store

elif opcode == opcodes["STORE_GLOBAL"]:
addr = self.code[ip]
ip += 1
value_to_store = self.stack.pop()
self.globals[addr] = value_to_store
print(f"Store Global: [{addr}] = {value_to_store}")

elif opcode == opcodes["STORE_INDIRECT"]:
global_addr_operand = self.code[ip]
ip += 1
effective_address = self.globals[global_addr_operand]
value_to_store = self.stack.pop()
self.globals[effective_address] = value_to_store
print(f"str2 global[globals[{global_addr_operand}] ({effective_address})] = {value_to_store}")

elif opcode == opcodes["POP"]:
self.stack.pop()

elif opcode == opcodes["CALL"]:
func_addr = self.code[ip]
ip += 1
nargs = self.code[ip]
ip += 1
nlocals_op = self.code[ip]
ip += 1

new_context = NewStack(ip)
if nlocals_op > 100: return False

for i in range(nargs):
new_context.locals[i] = self.stack[len(self.stack) - 1 - i] # 转移参数到新栈帧

self.stack = self.stack[:-nargs] # 弹出参数

self.call_stack.append(new_context)
ip = func_addr

elif opcode == opcodes["RET"]:
last_context = self.call_stack.pop()
ip = last_context.return_ip

elif opcode == opcodes["SMC"]:
change_code_val = self.code[ip]
ip += 1
smc(self, ip, change_code_val)

else:
print(f"VM Error: Unknown opcode {opcode} at IP {ip-1}.")
return False # 未知操作码,执行失败

if ip < self.code_size:
opcode = self.code[ip]
else:
break
return True


def py_int_handler(vm_instance, next_ip, value):
if 0 <= next_ip < vm_instance.code_size:
vm_instance.code[next_ip] += value
else:
print("error")
return 0


def to_signed32(val):
val &= 0xFFFFFFFF
if val & 0x80000000: # 最高位为1,表示负数
return val - 0x100000000
return val

def main():
opc_hex_values = [
0x0000000a,0x00000000,0x00000018,0x005801c0,0xffa7fe4e,0x00000000,0x0000000a,0x00000000,
0x00000018,0x20160070,0xdfe9ff9e,0x00000001,0x0000000a,0x00000000,0x00000018,0x4805801c,
0xb7fa7ff2,0x00000002,0x0000000a,0x00000000,0x00000018,0x12016007,0xedfea007,0x00000003,
0x0000000a,0x00000064,0x00000018,0x04805801,0xfb7fa80d,0x00000004,0x0000000c,0x0000000a,
0x0000000c,0x00000000,0x00000005,0x00000008,0x0000004c,0x0000000a,0x0000000a,0x0000000c,
0x00000000,0x00000001,0x00000016,0x0000000e,0x00000002,0x0000000a,0x0000000b,0x0000000c,
0x00000000,0x00000001,0x00000016,0x0000000e,0x00000003,0x0000000c,0x00000002,0x0000000c,
0x00000003,0x00000004,0x00000017,0x00000004,0x0000000c,0x00000000,0x0000000a,0x00000001,
0x00000001,0x0000000e,0x00000000,0x0000000c,0x00000004,0x0000000a,0x00000001,0x00000001,
0x0000000e,0x00000004,0x00000007,0x0000001e,0x00000012,0xffffc588,0x00000000,0x55555555,
0x00000150,0x00000150,0x00000000,0x00000000,0x00000006,0x00000000,0x00000000,0x00000000
]
bytecode = [to_signed32(x) for x in opc_hex_values] # 将字节码转换为有符号32位整数,在smc中使用到了,原字节码也是int类型的
input_string = (string.ascii_uppercase+ string.ascii_lowercase)[:36:]

vm_instance = VM()
vm_instance.globals = [0] * 1000
vm_instance.globals[10] = 36 # 数据长度

for i in range(min(len(input_string), 36)):
if (i + 1) < len(vm_instance.globals):
vm_instance.globals[i + 11] = ord(input_string[i]) # 读取输入到global中

vm_instance.set_code(bytecode, len(bytecode)) # 设置字节码

success = vm_instance.vm_exec(start_ip=0, smc=py_int_handler)

if success:
print("\nsuccess")
else:
print("\nfailed")

if __name__ == "__main__":
main()

模拟执行并进行打印,大概可以看出是前一位异或后一位。最后的判断逻辑是先判断用户名再判断passwd,两者的加密方法是相同的。

但是这样来说最后的密文应该只有35位,可现实是36位,不难发现,其实字符长度36也参与了异或运算。

如果没有想到第一位是36,可以先忽略第一个字节把后面的字节进行前后异或解密,再用结果去爆破一下即可,因为缺的只是第一位,而异或是有连续性的,只需要全部再异或某一个字节就行。这里用cyber chef的xor brute force解密。结果证明这个字节是24。

密文如下,解密就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data = [0x45,0x57,0xE,0x5C,0x2,0x4,0x52,0x6,0x1B,0x1A,0xE,0x1,0x5E,0x4B,0x19,0x56,0x6,0x55,0x1C,0x14,0x5C,0x5D,0x9,0x1C,0x1D,0x1,0x0,0x50,0x0,0x4,0x6,0x52,0x0,0x2,0x55,0x56,0x6A,0x4,0x1D,0x7,0x6,0x1D,0x9]

passwd = data[:36:]

res = [0]*37
res[0] =36 #补回第一位

for i in range(1,37):
res[i] = data[i-1] ^ res[i-1]

flag = "".join(chr(i) for i in res)

print(flag)
#d3ctf{a68dfb06-798f-4bd1-9e81-011aaec113f0}