浅谈electron框架逆向

前期准备

分析版本:010editor 版本:1.0.3

环境:

Node.js 版本:v16.13.1

python 版本:3.7

HOOK框架:frida 版本: 15.1.17

asar文件解包工具: asar

前言

浅谈electron框架逆向,electron作为一个跨平台的的开发框架,被广泛应用于众多桌面程序,就拿windows平台来说就有 vscode, 阿里云盘, utools, Typore等等,这里就拿Typore来探究electron框架下的js代码处理。

typora作为一个出色的markdown编写工具,界面十分简洁。更新1.0的后typora开始收费了,虽然基本功能不收费,但是对于逆向人员来说,这个收费界面本身就充满了趣味。

Electron

项目基本结构

Untitled

Chromium : 为Electron提供了强大的UI能力,可以不考虑兼容性的情况下,利用强大的Web生态来开发界面。本质上就是chrome开源版本的浏览器

native apis:N-API为开发者提供了一套C/C++ API用于开发Node.js的Native扩展模块。从Node.js 8.0.0开始,N-API以实验性特性作为Node.js本身的一部分被引入,并且从Node.js 10.0.0开始正式全面支持N-API。

win平台下Electron程序的特征

有以.asar的文件后缀,其中app.asar里面一般就是程序的源码,被封装起来了

Untitled

Untitled

分析步骤

1.对app.asar文件进行解包

  • 安装node.js
  • 用node.js的包管理器npm下载解包工具asar
npm install -g asar
  • 使用asar解包
asar extract app.asar {输出的文件夹}

这里对app.asar这个文件解包

Untitled

Untitled

2.代码解密

Js代码要运行就必须得变成明文或者字节码,这里代码看到是乱码应该是被加密了。一定在某个地方进行了解密的,只需要找到这个地方吗,编写一个一模一样的解密代码就可以了。

这里就注意到main.node这个文件,js代码能运行在nodejs的环境下就是靠的这个文件,在其中一定存在解密函数。

hook法

由于文件一定会被解密后才可以运行,可以直接找到哪个地方是解密完成的点,直接hook这个点获取到解密后的明文即可。

hook的方法这里就很多了,由于最近在看frida hook框架,刚好又在看雪上看到一篇用frida hook到的文章,这里直接用frida来hook了。

看雪师傅的frida的hook代码

let napi_create_string_utf8 = Module.getExportByName(null, 'napi_create_string_utf8');
var index = 0;
if (napi_create_string_utf8) {
    console.log('yes');
    Interceptor.attach(napi_create_string_utf8, {
        onEnter: function (args) {
            console.log('napi_create_string_utf8', '调用', args[0], args[1].readCString().substring(0, 100), args[2], args[3]);
 
            if (args[2].toInt32() > 100) {
                index += 1;
                var f = new File('export_' + String(index) + '.js', 'wb');
                f.write(args[1].readByteArray(args[2].toInt32()));
                f.flush();
                f.close();
 
            }
        }
    });
} else {
    console.log('no');
}

这里选择hook的是napi_create_string_utf8这个api,就是一个创建字符串的api。

Untitled

frida有两种hook模式,一种是结合python把js代码嵌入python中去运行,还有一种是直接调用js代码通过命令行hook,命令行的比较简单,这里直接用命令行的就好。

frida -f Typora.exe -l typorahook.js --no-pause

详细的命令含义直接百度一下,这里不过多解释,给出hook结果

Untitled

Untitled


直接解密法

上面的hook法,虽然比较快,但是没办法得到函数名,如果要修改代码逻辑的话还得是用直接解密的方法才比较好用。

利用ida的插件发现,main.node、调用了AES的s盒的数据

Untitled

交叉引用逆s盒,找到解密代码,去除main,node的地址随机加载ALSR功能,用x64调试typro,在AES解密处下断点,找到AES密钥及其扩散后的结果

Untitled

Untitled

解密代码,写的比较粗糙,再解md5之前得先把文件base64解码,base64我用的是python弄的,这里AES我又是用c语言写的,拼装在一起用哈哈哈哈

#include <stdio.h>
#include <cstdio>
#include <stdlib.h>
#include <stdint.h>
#include <dirent.h>
#include <string.h>
#include <windows.h>
// 逆s盒
int Nk = 8, Nr = 14;
int Nb = 4; 

uint8_t inv_s_box[267] = {
    0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, 
    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, 
    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, 
    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, 
    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, 
    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, 
    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, 
    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, 
    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, 
    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, 
    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, 
    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, 
    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, 
    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, 
    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, 
    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D, 
    0x8D, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36
};
uint8_t key[] = {
	0x4e,0xe1,0xb3,0x82,0x94,0x9a,0x02,0x4b,0x80,0x2f,0x52,0xb4,0xb4,0xfe,0x57,0xf1, 
    0xbe,0xf4,0x08,0x53,0x10,0x92,0x56,0xe2,0xc2,0x0d,0xec,0xa3,0xdd,0x8d,0xd5,0x6d,
    0x12,0xe2,0x8f,0x43,0x86,0x78,0x8d,0x08,0x06,0x57,0xdf,0xbc,0xb2,0xa9,0x88,0x4d,
    0x89,0x27,0xcc,0xb0,0x99,0xb5,0x9a,0x52,0x5b,0xb8,0x76,0xf1,0x86,0x35,0xa3,0x9c,
    0x86,0xe8,0x51,0x07,0x00,0x90,0xdc,0x0f,0x06,0xc7,0x03,0xb3,0xb4,0x6e,0x8b,0xfe,
    0x04,0xb8,0xf1,0x0b,0x9d,0x0d,0x6b,0x59,0xc6,0xb5,0x1d,0xa8,0x40,0x80,0xbe,0x34,
    0x4f,0x46,0x49,0x0e,0x4f,0xd6,0x95,0x01,0x49,0x11,0x96,0xb2,0xfd,0x7f,0x1d,0x4c,
    0x50,0x6a,0x55,0x22,0xcd,0x67,0x3e,0x7b,0x0b,0xd2,0x23,0xd3,0x4b,0x52,0x9d,0xe7,
    0x47,0x18,0xdd,0xbd,0x08,0xce,0x48,0xbc,0x41,0xdf,0xde,0x0e,0xbc,0xa0,0xc3,0x42,
    0x35,0x8a,0x7b,0x0e,0xf8,0xed,0x45,0x75,0xf3,0x3f,0x66,0xa6,0xb8,0x6d,0xfb,0x41,
    0x6b,0x17,0x5e,0xd1,0x63,0xd9,0x16,0x6d,0x22,0x06,0xc8,0x63,0x9e,0xa6,0x0b,0x21,
    0x3e,0xae,0x50,0xf3,0xc6,0x43,0x15,0x86,0x35,0x7c,0x73,0x20,0x8d,0x11,0x88,0x61,
    0xc9,0xd3,0xb1,0x8c,0xaa,0x0a,0xa7,0xe1,0x88,0x0c,0x6f,0x82,0x16,0xaa,0x64,0xa3,
    0x79,0x02,0x13,0xf9,0xbf,0x41,0x06,0x7f,0x8a,0x3d,0x75,0x5f,0x07,0x2c,0xfd,0x3e,
    0xf8,0x87,0x03,0x49,0x52,0x8d,0xa4,0xa8,0xda,0x81,0xcb,0x2a,0xcc,0x2b,0xaf,0x89
};

uint8_t gmult(uint8_t a, uint8_t b) {

	uint8_t p = 0, i = 0, hbs = 0;

	for (i = 0; i < 8; i++) {
		if (b & 1) {
			p ^= a;
		}

		hbs = a & 0x80;
		a <<= 1;
		if (hbs) a ^= 0x1b; // 0000 0001 0001 1011	
		b >>= 1;
	}

	return (uint8_t)p;
}

void coef_mult(uint8_t *a, uint8_t *b, uint8_t *d) {

	d[0] = gmult(a[0],b[0])^gmult(a[3],b[1])^gmult(a[2],b[2])^gmult(a[1],b[3]);
	d[1] = gmult(a[1],b[0])^gmult(a[0],b[1])^gmult(a[3],b[2])^gmult(a[2],b[3]);
	d[2] = gmult(a[2],b[0])^gmult(a[1],b[1])^gmult(a[0],b[2])^gmult(a[3],b[3]);
	d[3] = gmult(a[3],b[0])^gmult(a[2],b[1])^gmult(a[1],b[2])^gmult(a[0],b[3]);
}

void add_round_key(uint8_t *state, uint8_t *w, uint8_t r) {
	
	uint8_t c;
	
	for (c = 0; c < Nb; c++) {
		// 按列循环,计算第c列的值
		// state[row,col] = state[row,col]^w[col,row](r=0)
		state[Nb*0+c] = state[Nb*0+c]^w[4*Nb*r+4*c+0];   //debug, so it works for Nb !=4 
		state[Nb*1+c] = state[Nb*1+c]^w[4*Nb*r+4*c+1];
		state[Nb*2+c] = state[Nb*2+c]^w[4*Nb*r+4*c+2];
		state[Nb*3+c] = state[Nb*3+c]^w[4*Nb*r+4*c+3];	
	}
}
void inv_shift_rows(uint8_t *state) {

	uint8_t i, k, s, tmp;

	for (i = 1; i < 4; i++) {
		s = 0;
		while (s < i) {
			tmp = state[Nb*i+Nb-1];
			
			for (k = Nb-1; k > 0; k--) {
				state[Nb*i+k] = state[Nb*i+k-1];
			}

			state[Nb*i+0] = tmp;
			s++;
		}
	}
}

void inv_sub_bytes(uint8_t *state) {

	uint8_t i, j;
	uint8_t row, col;

	for (i = 0; i < 4; i++) {
		for (j = 0; j < Nb; j++) {
			row = (state[Nb*i+j] & 0xf0) >> 4;
			col = state[Nb*i+j] & 0x0f;
			state[Nb*i+j] = inv_s_box[16*row+col];
		}
	}
}
void inv_mix_columns(uint8_t *state) {

	uint8_t a[] = {0x0e, 0x09, 0x0d, 0x0b}; // a(x) = {0e} + {09}x + {0d}x2 + {0b}x3
	uint8_t i, j, col[4], res[4];

	for (j = 0; j < Nb; j++) {
		for (i = 0; i < 4; i++) {
			col[i] = state[Nb*i+j];
		}

		coef_mult(a, col, res);

		for (i = 0; i < 4; i++) {
			state[Nb*i+j] = res[i];
		}
	}
}

void inv_cipher(uint8_t *in, uint8_t *out, uint8_t *w) {

	uint8_t state[4*Nb];
	uint8_t r, i, j;

	for (i = 0; i < 4; i++) {
		for (j = 0; j < Nb; j++) {
			state[Nb*i+j] = in[i+4*j];
		}
	}

	
	add_round_key(state, w, Nr);

	for (r = Nr-1; r >= 1; r--) {
		inv_shift_rows(state);
		inv_sub_bytes(state);
		add_round_key(state, w, r);
		inv_mix_columns(state);
	}

	inv_shift_rows(state);
	inv_sub_bytes(state);
	add_round_key(state, w, 0);

	for (i = 0; i < 4; i++) {
		for (j = 0; j < Nb; j++) {
			out[i+4*j] = state[Nb*i+j];
		}
	}
}

int main(){
	char path[] = {"E:\\Typora\\resources\\workstation\\app.asar\\License.jsDecodeBase64"};
	char path_decrypt[] = {"E:\\Typora\\resources\\workstation\\app.asar\\LicenceDecodeMD5.js"};
	FILE  *file_decrpt = fopen(path_decrypt,"wb+");
	FILE  *file = fopen(path,"rb");
	fseek(file,0,SEEK_END);//定位到文件的最后面
	long length  = ftell(file);//ftell获得该文件指示符此时的偏移量,此时已经是在文件末尾,故能获得文件的大小
	fseek(file,0,SEEK_SET);//定位到文件的最后面
	BYTE* file_start;
	file_start = (BYTE*)malloc(length);
	char* file_decrpt_code;
	file_decrpt_code = (char*)malloc(length);
	memset(file_decrpt_code,0,length);
	fread(file_start, 1, length, file);
	fclose(file);
	printf("\n%d\n",length);
	uint8_t mod[16];
	uint8_t encode[16];
	for(int i = 0; i<length/16-1;i++){
		for(int j = 0;j<16;j++){
			mod[j] = file_start[j+i*16];
			encode[j] = file_start[j+(i+1)*16];
			//printf("%x ",encode[j]);
		}
		// if(i == (length/16)-1){
		// 	for(int j = 0;j<16;j++){
		// 		printf("%x ",encode[j]);
		// 	}
		// 	break;
		// }
		uint8_t out[16] = { 0,};
		inv_cipher(encode,out,key);
		for(int j = 0;j<16;j++){
			out[j] ^= mod[j]; 
			file_decrpt_code[i*16+j] = out[j];
			//printf("%c",file_decrpt_code[i*16+j]);
		}
	}
	int num = 0;
	for(int j = 15;j>0;j--){
		if(file_decrpt_code[(length/16-1-1)*16+j]==0xb){
			num++;
		}
	}
	fwrite(file_decrpt_code,1,length-16-num,file_decrpt);
	fclose(file_decrpt);
    return 0;
}

运行结果

Untitled

小结

就electron框架而言,官方是没有提供相关代码层面的保护的,electron的这种代码的保护方法是github上一个大佬的提出的源代码保护方案,但是这种保护方案还是比较的好破,同时在当源码被获取到的时候typora并没有后续的检测操作,希望增加检测方式来得知,程序的源码是否被篡改。

新版的electron在asar的解包上做了一定的操作,当我再对新版的app.asar解包的时候并未得到先关的加密后的js代码,只能获得一个main.node文件,详细的信息我还不得而知,等之后探索了再得出结论。

参考博客

https://bbs.pediy.com/thread-272604.htm

https://www.52pojie.cn/forum.php?mod=viewthread&tid=1553967&highlight=Typora

Q.E.D.