Emeditor 反盗版校验分析
版本 build 11.24 v25.4.3 由于这个玩意十年来开裂方案众多,包括但不限于算本地校验,一码多用,公钥替换等等,故如何开裂或者本地注册我们就不看了,只看看最抽象的反盗版。
1. 通过导入表追踪 WinVerifyTrust
// Emeditor 主程序
// 在emeddlgs.dll中有高度相似的函数。
__int64 __fastcall sub_141803100(__int64 a1)
{
char v2; // al
__int64 result; // rax
unsigned int va; // edi
UINT n130; // edx
HWND ActiveWindow; // rax
int v7; // ecx
GUID pgActionID; // [rsp+30h] [rbp-D0h] BYREF
_QWORD v9[2]; // [rsp+40h] [rbp-C0h] BYREF
__int128 v10; // [rsp+50h] [rbp-B0h]
__int128 pWVTData; // [rsp+60h] [rbp-A0h] BYREF
__int128 v12; // [rsp+70h] [rbp-90h]
__int64 v13; // [rsp+80h] [rbp-80h]
_QWORD *v14; // [rsp+88h] [rbp-78h]
__int128 v15; // [rsp+90h] [rbp-70h]
__int128 v16; // [rsp+A0h] [rbp-60h]
__int64 v17; // [rsp+B0h] [rbp-50h]
wchar_t String[80]; // [rsp+C0h] [rbp-40h] BYREF
WCHAR Buffer[800]; // [rsp+160h] [rbp+60h] BYREF
__int64 v20; // [rsp+7B8h] [rbp+6B8h] BYREF
if ( byte_141C8644F ) // 是否是UWP版本
return 1;
v2 = byte_141C3A79E;
if ( byte_141C3A79E == -1 )
{
v2 = sub_140F57E00(L"SDS", 0) == 0x78C19A68; // 跳过签名检查
byte_141C3A79E = v2;
}
if ( v2 == 1 )
return 1;
result = sub_140F58980(a1, 0xFFFFFFFFLL);
if ( !(_DWORD)result )
return result;
v9[0] = 32;
v9[1] = a1;
v17 = 0;
v13 = 1;
pWVTData = 0;
v12 = 0;
v14 = v9;
v16 = 0;
LODWORD(pWVTData) = 88;
v10 = 0;
DWORD2(v12) = 2;
v15 = 0;
DWORD2(v16) = 16;
pgActionID.Data1 = 0xAAC56B;
*(_DWORD *)&pgActionID.Data2 = 0x11D0CD44;
*(_DWORD *)pgActionID.Data4 = 0xC000C28C;
*(_DWORD *)&pgActionID.Data4[4] = 0xEE95C24F;
// {00AAC56B-CD44-11d0-8CC2-00C04FC295EE}
va = WinVerifyTrust(0, &pgActionID, &pWVTData); // 检查签名
/*
.text:00000001418031F4 call cs:WinVerifyTrust
.text:00000001418031FA mov edi, eax
.text:00000001418031FC test eax, eax
.text:00000001418031FE jz loc_1418032C8
*/
if ( !va )
return 1;
memset(Buffer, 0, sizeof(Buffer));
n130 = 130;
if ( va == (unsigned int)CERT_E_CHAINING )
n130 = 222;
LoadStringW(hInstance, n130, Buffer, 800);
if ( va != (unsigned int)CERT_E_CHAINING )
{
swprintf(String, 0x50u, L" (0x%x)", va);
sub_1418FE480(Buffer, 800, String);
}
LODWORD(v20) = 0;
ActiveWindow = GetActiveWindow();
v7 = sub_140F94A60(ActiveWindow, (__int64)Buffer, a1, (__int64)&v20, 2, 65534);
result = v7 == 1;
if ( (_DWORD)v20 )
{
if ( v7 == 1 )
byte_141C3A79E = 1;
}
return result;
}
如果需要绕过检查并最少修改,最少影响,可以直接把 test eax, eax 修改为 xor eax, eax。没什么话讲,根据微软文档: If the trust provider verifies that the subject is trusted for the specified action, the return value is zero.,该函数会在签名正常时返回 0,因为不确定 eax 是否还有别的影响。另外,dll 和主程序都要修补。
如果主程序没过验证,根据代码逻辑,会根据语言载入对应资源 dll 中的字符串,弹报错窗口。

dll 没过验证会在打开注册信息或关于时弹报错窗,与上面的弹窗类似。
2. 公钥差异
公钥:
.rdata:0000000141987530 aBeginPublicKey db '-----BEGIN PUBLIC KEY-----',0Ah
.rdata:0000000141987530 ; DATA XREF: sub_140024CC0+2B↑o
.rdata:000000014198754B db 'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEdFXkM+nFOhoI8bjAsThl/59LXkYK7f2F',0Ah
.rdata:000000014198758C db '1baTcak6GyFecRF0CXvvF2UzIozW1LZ/8bxd1XnBUvry5X6zchitsQ80ZppPrCBv',0Ah
.rdata:00000001419875CD db '7SqnMf/A2QscH1uA5kf1iPPezfOONKla',0Ah
.rdata:00000001419875EE db '-----END PUBLIC KEY-----',0Ah
如果是已注册,且 dll 和主程序公钥不同,在打开注册信息或关于时会被弹盗版窗口,并根据语言打开 url: https://zh-cn.emeditor.com/crack-keygen-serial/ 。此处一时半会没找到对应函数,估计也是哪个虚函数的,通过消息发送的。。。
3. 反盗版弹窗
总购买弹窗,弹出不同的购买网页
__int64 __fastcall purchase_dlg_140F1EA80(__int64 a1, unsigned int a2, __int16 id)
{
__int64 v4; // rsi
int Locale; // edx
int n1041; // eax
int v8; // edx
int n10; // edx
const wchar_t *ko; // r8
const wchar_t *_purchase; // r8
__int64 n883; // rax
v4 = a2;
sub_1418FE3F0(a1, a2, L"https://");
Locale = ::Locale;
if ( (id == 4058 || id == 1111 || id == 919 || id == 333 || id == 1) && (::Locale == 1031 || ::Locale == 1049) )
Locale = 0;
if ( id == 2 )
{
n1041 = 0;
if ( Locale == 1041 )
n1041 = 1041;
Locale = n1041;
}
if ( Locale <= 1042 )
{
if ( Locale == 1042 )
{
ko = L"ko";
goto LABEL_27;
}
v8 = Locale - 1028;
if ( !v8 )
{
ko = L"zh-tw";
goto LABEL_27;
}
n10 = v8 - 3;
if ( !n10 )
{
ko = L"de";
goto LABEL_27;
}
if ( n10 == 10 )
{
ko = L"jp";
goto LABEL_27;
}
goto LABEL_24;
}
if ( Locale == 1049 )
{
ko = L"ru";
}
else
{
if ( Locale != 2052 )
{
LABEL_24:
ko = L"www";
goto LABEL_27;
}
ko = L"zh-cn";
}
LABEL_27:
sub_1418FE480(a1, v4, ko);
sub_1418FE480(a1, v4, L".emeditor.com/");
switch ( id )
{
case 4248:
_purchase = L"#purchase";
break;
case 1111:
_purchase = L"crack-keygen-serial/";
break;
case 919:
_purchase = L"increase-virtual-memory/";
break;
case 333:
_purchase = L"text-editor-features/history/emeditor-free/";
break;
case 1:
_purchase = L"category/emeditor-core/";
break;
case 2:
_purchase = L"forums/forum/beta/";
break;
default:
n883 = 883;
if ( id != 883 )
return n883;
_purchase = L"#download";
break;
}
return sub_1418FE480(a1, v4, _purchase);
}
基本上是本地化处理+字符串拼接。我们主要关心 传入的 id,最重要的是其等于 1111 的情况。
xref,找到总购买函数第三个参数为 1111 的情况
__int64 __fastcall sub_140F5CA60(HWND *lpParameter, int n2)
{
[...折叠一下,这样你才知道看的是代码...]
if ( g_check_flag == 3 )
{
sub_140F76B00((__int64)lpParameter, 0x457u, 48, 0);
purchase_dlg_140F1EA80((__int64)File, 0x100u, 1111);
sub_140F4F570(lpParameter[1], File, 0);
LODWORD(v9) = 0;
}
else if ( n7_1 > 7 )
{
LODWORD(v9) = -1;
}
if ( n2 == 2 || n7 <= 1 )
{
sub_140F99220(lpParameter);
sub_1408D8FE0(&SystemTimeAsFileTime, 1);
v17 = sub_1408D90F0((HMODULE *)&SystemTimeAsFileTime, (const CHAR *)5);
if ( v17 )
{
hMutex = (void *)sub_140422E10(0, L"DlgMutex", 1);
if ( hMutex )
{
n1812 = ((__int64 (__fastcall *)(HWND, _QWORD))v17)(lpParameter[1], (unsigned int)n7);
n2032 = n1812;
if ( n1812 != 2 )
{
if ( n1812 == 1009 )
{
sub_140F5CD60(lpParameter, 1);
LODWORD(v9) = n2032;
}
else if ( n1812 == 1812 && sub_140F04C90(lpParameter) )
{
LODWORD(v9) = 1;
}
if ( byte_141C8644F )
{
if ( n2032 == 2032 )
{
byte_141C865E5 = 1;
byte_141C865EA = 1;
sub_141783840(lpParameter);
LODWORD(v9) = 1009;
}
}
}
ReleaseMutex(hMutex);
}
}
else
{
sub_140F5CA20(v16, v15);
}
sub_1408D90E0(&SystemTimeAsFileTime);
}
else
{
sub_140F97B40(v10, 10, 0, n7);
}
return (unsigned int)v9;
}
如果 g_check_flag 为 3 ,emeditor 会调用 sub_140F76B00 创建一个强制前台(SetForegroundWindow),所有按钮事件均为向主程序发送关闭消息的窗口,弹盗版窗。此处的 g_check_flag 值得关注,我们 xref 下。
4. g_check_flag 分析
mov cs:g_check_flag, 3
第一处
__int64 __fastcall sub_140F0C210(_BYTE *lpParameter)
{
if ( !byte_141CA7881 )
{
byte_141CA7881 = 1;
g_check_flag = 3;
sub_140F5CA60(lpParameter, 1);
byte_141CA7881 = 0;
}
return 0;
}
第二处
__int64 __fastcall sub_140F0C250(__int64 n768)
{
g_check_flag = 3;
sub_140F97B40(n768, 37, 0, 0);
return 0;
}
非常有趣,此处 sub_140F0C210 将 g_check_flag 设置为 3 后调用前面的 sub_140F5CA60,那还说啥了,再 xref 呗。

sub_140E8E000是一个数千行的超级无敌大状态机。此处注意到在 id 为 32929 时会调用前面的 sub_140F0C250,这里截图没截全。x 一下 big_sw1,看看 sub_140F0C210 调用前哪里对 big_sw1 有写操作。
_BOOL8 __fastcall sub_140E8E000(
char *lpParameter,
int a2,
__int64 n32797,
HMENU n50332098,
LPFILETIME lpCreationTime,
__int64 *a6,
unsigned int item)
根据代码,可以知道 32929 这个值是作为 sub_140E8E000 第三个参数直接传递进来的。
sub_140E8E000 是一个虚函数,要找会非常红温。但是我们知道 32928,也就是 80A0h。其要么是硬编码的 mov r8d, 80A0h,要么是通过 windows 消息机制传递 mov edx, 80A0h 再 call PostMessageW 或者 call SendMessageW。此处搜 80A1h 也是一样的,会找到 sub_140F0C250 的相关引用。但 ida 分析都是一坨。
在 EmEditor 主程序里搜索,无结果:
在 emeddlgs.dll 中搜索,看到了 edx:

__int64 __fastcall sub_180363EF0(_QWORD *p_hWnd)
{
[...折叠一下,这样你才知道看的是代码...]
if ( (unsigned __int8)sub_1803F9BF0(v3) )
{
v9 = p_hWnd + 9;
if ( p_hWnd[12] <= 7u )
v12 = p_hWnd + 9;
else
v12 = (_QWORD *)*v9;
v13 = p_hWnd[11];
v10 = p_hWnd + 5;
*(_OWORD *)wParam = 0;
v31 = 0;
n7_1 = 0;
if ( p_hWnd[8] <= 0xFu )
v14 = p_hWnd + 5;
else
v14 = (_QWORD *)*v10;
sub_1803623B0(wParam, v14, p_hWnd[7]);
v15 = sub_1803321BC(&v33);
v16 = sub_1803F9F30(v15);
v17 = sub_1803321BC(v16);
*(_QWORD *)&v29 = v12;
*((_QWORD *)&v29 + 1) = v13;
v11 = p_hWnd + 1;
sub_180355890(&v28, v17, (struct_a1 *)(p_hWnd + 1), (__int64)wParam, &v29);
if ( v28.byte20 )
{
if ( v28.ill_check == 19 )
{
n36 = 0;
n32929 = 32929;
}
else if ( v28.ill_check == 20 )
{
n32929 = 32911;
n36 = 36;
}
else
{
n36 = 0;
if ( v28.ill_check == 21 )
n32929 = 32928; // 看这里
else
n32929 = 32930;
}
PostMessageW((HWND)*p_hWnd, n32929, n36, 0);
v21 = v28.byte20 == 0;
}
else
{
*(_OWORD *)wParam_2 = 0;
v26 = 0;
n7 = 0;
v18 = &v28;
if ( *((_QWORD *)&v28.oword10 + 1) > 7u )
v18 = *(struct_a1 **)&v28.ill_check;
sub_18032B7C4(wParam_2, v18, *(_QWORD *)&v28.oword10);
wParam_1 = wParam_2;
if ( n7 > 7 )
wParam_1 = (WPARAM *)wParam_2[0];
SendMessageW((HWND)*p_hWnd, 0x80A5u, (WPARAM)wParam_1, 0);
if ( n7 > 7 )
{
if ( 2 * n7 + 2 < 0x1000 )
{
v20 = (void *)wParam_2[0];
}
else
{
v20 = *(void **)(wParam_2[0] - 8);
if ( wParam_2[0] - (unsigned __int64)v20 - 8 > 0x1F )
__fastfail(5u);
}
sub_1803F8518(v20);
}
v26 = 0;
n7 = 7;
LOWORD(wParam_2[0]) = 0;
v21 = v28.byte20 == 0;
}
if ( v21 )
sub_18032A2F4(&v28);
sub_1803321D4(&v33);
}
else
{
v4 = sub_1803321BC(&v33);
v5 = sub_1803F9920(v4, wParam_2);
v6 = sub_1803F9760(v5);
v7 = sub_18042C880(v6);
*(_QWORD *)&v29 = v6;
*((_QWORD *)&v29 + 1) = v7;
sub_1803541B0(wParam, &v29);
sub_1803F9580(wParam_2);
wParam_3 = wParam;
if ( n7_1 > 7 )
wParam_3 = (WPARAM *)wParam[0];
SendMessageW((HWND)*p_hWnd, 0x80A5u, (WPARAM)wParam_3, 0);
sub_18032A2F4(wParam);
sub_1803321D4(&v33);
v9 = p_hWnd + 9;
v10 = p_hWnd + 5;
v11 = p_hWnd + 1;
}
sub_1803F80B8();
sub_18032A2F4(v9);
sub_180333260(v10);
sub_180333260(v11);
sub_1803F8518(p_hWnd);
return 0;
}
在这里 ill_check 的值实际上由 v31 决定。再追就不礼貌了,指针太几把多了,ida 还很弱智的全识别成数值了。知道消息由 dll 传入就行。我们可以选择将其 patch 为一个无效的消息。
或者将之后的任意一处 patch 掉,比如跳过 g_check_flag 检查,手动设置为合法值或者把购买函数和紫砂函数 patch 等等等。。。
回到主程序,g_check_flag 总处理函数
这是一套完全不按顺序的抽象检测:
__int64 sub_140F02E80(_BYTE *lpParameter, int n2, ...)
{
double _XMM2; // xmm2_8
bool v4; // zf
int g_check_flag; // eax
unsigned __int16 n1108; // dx
__int64 v7; // rdx
unsigned __int64 n2_2; // rsi
int v9; // esi
int v10; // eax
__int64 v11; // rcx
int n5; // eax
UINT n354; // edx
int n1009; // eax
HWND LastActivePopup; // rax
HWND LastActivePopup_1; // rsi
__int64 v17; // rcx
WCHAR *pszPath; // rbx
struct _SYSTEMTIME SystemTime; // [rsp+30h] [rbp-D0h] BYREF
__m128i i; // [rsp+40h] [rbp-C0h] BYREF
_BYTE v22[80]; // [rsp+80h] [rbp-80h] BYREF
WCHAR Buffer[264]; // [rsp+D0h] [rbp-30h] BYREF
_BYTE v24[2016]; // [rsp+2E0h] [rbp+1E0h] BYREF
int n2_1; // [rsp+AD8h] [rbp+9D8h] BYREF
struct _FILETIME SystemTimeAsFileTime; // [rsp+AE0h] [rbp+9E0h] BYREF
va_list SystemTimeAsFileTimea; // [rsp+AE0h] [rbp+9E0h]
FILETIME FileTime2; // [rsp+AE8h] [rbp+9E8h] BYREF
va_list FileTime2a; // [rsp+AE8h] [rbp+9E8h]
FILETIME FileTime1; // [rsp+AF0h] [rbp+9F0h] BYREF
va_list FileTime1a; // [rsp+AF0h] [rbp+9F0h]
va_list va3; // [rsp+AF8h] [rbp+9F8h] BYREF
va_start(va3, n2);
va_start(FileTime1a, n2);
va_start(FileTime2a, n2);
va_start(SystemTimeAsFileTimea, n2);
SystemTimeAsFileTime = va_arg(FileTime2a, struct _FILETIME);
va_copy(FileTime1a, FileTime2a);
FileTime2 = va_arg(FileTime1a, FILETIME);
va_copy(va3, FileTime1a);
FileTime1 = va_arg(va3, FILETIME);
n2_1 = n2;
if ( dword_141C3A784 )
{
if ( lpParameter[31022] )
goto LABEL_6;
v4 = *((_QWORD *)lpParameter + 2287) == 0;
}
else
{
v4 = lpParameter[31022] == 0;
}
if ( v4 )
return 0;
LABEL_6:
if ( byte_141C8644F && !byte_141C865E4 ) // 这里检查 UWP 版本和另一个诡异的全局变量?
goto LABEL_45;
if ( *((_QWORD *)lpParameter + 2287) )
*((_QWORD *)lpParameter + 2287) = 0;
g_check_flag = g_check_flag;
if ( lpParameter[31017] )
{
if ( (g_check_flag & 0xFFFFFFFC) != 0 || g_check_flag == 2 )
{
n1108 = 1108;
if ( ((g_check_flag - 2) & 0xFFFFFFFD) != 0 )
n1108 = 1137;
sub_140F76B00((__int64)lpParameter, n1108, 48, 0);
PostMessageW(*((HWND *)lpParameter + 1), 0x16u, 1u, 4097);
}
goto LABEL_45;
}
if ( g_check_flag == 1 )
{
if ( (unsigned __int8)sub_1418BC860(&i, v22, 14, v24, 1000) )
{
n2_2 = sub_1419110F0(&i, v7, _XMM2);
if ( (n2_2 < 2 || (unsigned int)sub_1403BF240(&i, L"r-", 2)) && ((n2_2 - 20) & 0xFFFFFFFFFFFFFFFBuLL) != 0 )
{
GetLocalTime(&SystemTime);
v9 = sub_141803610(SystemTime.wYear, SystemTime.wMonth, SystemTime.wDay);
v10 = sub_1418036E0(&i);
if ( v10 )
{
v11 = (unsigned int)(v9 + 30);
if ( v10 <= (int)v11 )
{
n5 = v10 - v9;
n354 = 354;
if ( n5 <= 5 )
{
LABEL_29:
if ( lpParameter[31021] || (unsigned int)sub_140F02800(lpParameter, n354) == 7 )
return 0;
goto LABEL_31;
}
sub_140F97B40(v11, 11, 0, n5);
}
}
}
}
}
else
{
if ( g_check_flag != 6 )
goto LABEL_32;
if ( sub_140F57E00(L"PromptRegExpired2", 1) )
{
n354 = 355;
goto LABEL_29;
}
}
LABEL_31:
g_check_flag = g_check_flag;
LABEL_32:
if ( g_check_flag != 1 )
{
if ( g_check_flag == 4
&& LoadStringW(hInstance, 0x474u, Buffer, 260)
&& (unsigned int)sub_140F76AD0(lpParameter, Buffer, 52) == 6 )
{
sub_140F4F570(*((HWND *)lpParameter + 1), L"https://support.emeditor.com/", 0);
}
n1009 = sub_140F5CA60((HWND *)lpParameter, 0);
if ( n1009 == -1 )
{
if ( g_check_flag != 3 )
{
sub_140F76B00((__int64)lpParameter, 0x14Fu, 16, 0);
sub_140F580A0(L"Edition", 4);
LABEL_52:
PostMessageW(*((HWND *)lpParameter + 1), 0x16u, 1u, 4097);
return 0;
}
}
else
{
if ( n1009 == 1009 )
{
if ( lpParameter[31022] )
sub_140F580A0(L"Edition", 2);
return 0;
}
if ( g_check_flag == 3 && !n1009 )
goto LABEL_52;
}
}
LABEL_45:
if ( lpParameter[31020] )
{
LastActivePopup = GetLastActivePopup(*((HWND *)lpParameter + 1));
LastActivePopup_1 = LastActivePopup;
if ( LastActivePopup )
{
if ( LastActivePopup != *((HWND *)lpParameter + 1) && IsWindow(LastActivePopup) )
SendMessageW(LastActivePopup_1, 0x805Eu, 0, g_check_flag);
}
}
if ( lpParameter[31022] )
{
sub_140F580A0(L"Edition", 2);
v17 = *((_QWORD *)lpParameter + 1);
lpParameter[31022] = 0;
if ( (unsigned int)sub_140F955E0(v17) == 1 )
goto LABEL_52;
}
else if ( (unsigned __int8)sub_140F5AA80() )
{
if ( !qword_141C865C0 )
{
pszPath = (WCHAR *)j__malloc_base(0x10000u);
if ( pszPath )
*pszPath = 0;
if ( GetTempPathW(0x8000u, pszPath) )
{
PathCchAppendEx(pszPath, 0x8000u, L"eeinst.exe", 1u);
DeleteFileW(pszPath);
}
j__free_base(pszPath);
}
FileTime2 = 0;
n2_1 = 2;
if ( (unsigned int)sub_140F5AC30((FILETIME *)FileTime2a, &n2_1) )
{
if ( sub_140F57E00(L"NewVerAvailable", 0) )
{
sub_140F034C0(lpParameter);
}
else
{
SystemTimeAsFileTime = 0;
GetSystemTimeAsFileTime((LPFILETIME)SystemTimeAsFileTimea);
FileTime1 = SystemTimeAsFileTime;
if ( !FileTime2.dwHighDateTime && !FileTime2.dwLowDateTime
|| (FileTime2 = (FILETIME)((FileTime2.dwLowDateTime | ((unsigned __int64)FileTime2.dwHighDateTime << 32))
+ 864000000000LL * n2_1),
CompareFileTime((const FILETIME *)FileTime1a, (const FILETIME *)FileTime2a) >= 0) )
{
sub_140F033C0(lpParameter, 0);
}
}
}
}
return 0;
}
值 1 根据看雪 EmEditor 24.5.3-25.1.x Stripe注册码 以及官方的 EmEditor 帮助使用 Stripe 登录 EmEditor,我们知道"r-“开头的注册码是特殊的 Stripe 注册码。
值 2 和大于 3 的值放到一起检查,说明 2 和大于 3 的值都是无效值。
值 3 之前说过,查到盗版了。
这里屎山代码发力了,虽然排除了大于 3 的值,但又在先前为值 4 和值 6 加入了额外检查。
值 4 不知道是啥,但会进行日期的一个检的查。并且还试图弹窗和打开 support。
值 6 弹一个 PromptRegExpired2 的窗口,可能是试用期过期。
0 则是是一切安好。
这里逻辑极其怪异,包括但不限于:(g_check_flag & 0xFFFFFFFC) != 0 || g_check_flag == 2 混写位运算和排除代码,乱序值检查,0 到 6 七个值只有 0, 1, 3, 4, 6 是有效的等等。