Typora1.10.8公钥替换

admin 2026-01-09 23:37:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章披露Typora1.10.8改用NodeVM把JS编译为jsc,旧版直接PatchRSA公钥即可离线激活的方法失效;作者提出在原生层劫持node的publicDecrypt接口,通过IDA定位并替换硬编码PEM公钥,实现无视JS层加固的通用注册机方案,给出关键函数调用链与下断技巧。 综合评分: 82 文章分类: 逆向分析,安全工具,漏洞分析,二进制安全


cover_image

Typora 1.10.8公钥替换

利刃信安

2026年1月9日 07:50 北京

以下文章来源于吾爱破解论坛 ,作者吾爱pojie

吾爱破解论坛 .

吾爱破解论坛深耕软件逆向工程与反病毒技术领域,汇聚众多技术爱好者的智慧与经验,共同探索与分享前沿安全技术和防护策略,构建业内最具影响力的技术交流平台。

作者坛账号:xqyqx

Typora 1.10.8公钥替换

在最新版本中的Typora中,解包app.asar会发现软件使用了node vm将js编译成了jsc,在之前的版本中,分析atom.js可以得知Typora的激活实际上就是一个简单RSA公钥解密,只要patch了公钥就可以编写注册机进行离线激活,然而jsc中并没有简单的将公钥作为字符串进行储存(猜测是使用数组进行了解密),而分析jsc机器码又十分困难(需要自行编译定制版v8),因此可以通过native层进行公钥替换,这样不管开发者如何在js层上进行防护,都无法封堵该方法(除非定制node)


我们知道electron应用实际上是对node进行了打包,因此庞大的主程序里面会有node提供的所有函数,进行RSA解密需要用到Crypto.publicDecrypt函数,而Crypto模块是node使用C++编写的。

我们来到node的代码仓库搜索publicDecrypt,可以找到接口定义(node/src/crypto/crypto_cipher.cc):

 复制代码 隐藏代码
  SetMethod(context,
            target,
            "publicDecrypt",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ncrypto::Cipher::recover>);

向下翻,找到PublicKeyCipher::Cipher函数:

&nbsp;复制代码&nbsp;隐藏代码
template <PublicKeyCipher::Operation operation,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PublicKeyCipher::Cipher_t cipher>
voidPublicKeyCipher::Cipher(const&nbsp;FunctionCallbackInfo<Value>& args)&nbsp;{
&nbsp; MarkPopErrorOnReturn mark_pop_error_on_return;
&nbsp; Environment* env = Environment::GetCurrent(args);

unsignedint&nbsp;offset =&nbsp;0;
auto&nbsp;data = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset);
if&nbsp;(!data)&nbsp;return;
constauto& pkey = data.GetAsymmetricKey();
if&nbsp;(!pkey)&nbsp;return;

&nbsp; ArrayBufferOrViewContents<unsignedchar>&nbsp;buf(args[offset]);
if&nbsp;(!buf.CheckSizeInt32()) [[unlikely]] {
&nbsp; &nbsp;&nbsp;return&nbsp;THROW_ERR_OUT_OF_RANGE(env,&nbsp;"buffer is too long");
&nbsp; }
uint32_t&nbsp;padding;
if&nbsp;(!args[offset +&nbsp;1]->Uint32Value(env->context()).To(&padding))&nbsp;return;

if&nbsp;(cipher == ncrypto::Cipher::decrypt &&
&nbsp; &nbsp; &nbsp; operation == PublicKeyCipher::kPrivate && padding == RSA_PKCS1_PADDING) {
&nbsp; &nbsp; EVPKeyCtxPointer ctx = pkey.newCtx();
&nbsp; &nbsp; CHECK(ctx);

&nbsp; &nbsp;&nbsp;if&nbsp;(!ctx.initForDecrypt()) {
&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ThrowCryptoError(env, ERR_get_error());
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// RSA implicit rejection here is not supported by BoringSSL.
&nbsp; &nbsp;&nbsp;if&nbsp;(!ctx.setRsaImplicitRejection()) [[unlikely]] {
&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;THROW_ERR_INVALID_ARG_VALUE(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; env,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"RSA_PKCS1_PADDING is no longer supported for private decryption");
&nbsp; &nbsp; }
&nbsp; }

&nbsp; Digest digest;
if&nbsp;(args[offset +&nbsp;2]->IsString()) {
&nbsp; &nbsp; Utf8Value&nbsp;oaep_str(env->isolate(), args[offset +&nbsp;2]);
&nbsp; &nbsp; digest = Digest::FromName(*oaep_str);
&nbsp; &nbsp;&nbsp;if&nbsp;(!digest)&nbsp;return&nbsp;THROW_ERR_OSSL_EVP_INVALID_DIGEST(env);
&nbsp; }

&nbsp; ArrayBufferOrViewContents<unsignedchar>&nbsp;oaep_label(
&nbsp; &nbsp; &nbsp; !args[offset +&nbsp;3]->IsUndefined() ? args[offset +&nbsp;3] : Local<Value>());
if&nbsp;(!oaep_label.CheckSizeInt32()) [[unlikely]] {
&nbsp; &nbsp;&nbsp;return&nbsp;THROW_ERR_OUT_OF_RANGE(env,&nbsp;"oaepLabel is too big");
&nbsp; }
std::unique_ptr<BackingStore> out;
if&nbsp;(!Cipher<cipher>(env, pkey, padding, digest, oaep_label, buf, &out)) {
&nbsp; &nbsp;&nbsp;return&nbsp;ThrowCryptoError(env, ERR_get_error());
&nbsp; }

&nbsp; Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(),&nbsp;std::move(out));
&nbsp; args.GetReturnValue().Set(
&nbsp; &nbsp; &nbsp; Buffer::New(env, ab,&nbsp;0, ab->ByteLength()).FromMaybe(Local<Value>()));
}

寻找关键函数KeyObjectData::GetPublicOrPrivateKeyFromJs定义(node/src/crypto/crypto_keys.cc):

&nbsp;复制代码&nbsp;隐藏代码
KeyObjectData&nbsp;KeyObjectData::GetPublicOrPrivateKeyFromJs(
&nbsp; &nbsp;&nbsp;const&nbsp;FunctionCallbackInfo<Value>& args,&nbsp;unsignedint* offset)&nbsp;{
if&nbsp;(IsAnyBufferSource(args[*offset])) {
&nbsp; &nbsp; Environment* env = Environment::GetCurrent(args);
&nbsp; &nbsp; ArrayBufferOrViewContents<char>&nbsp;data(args[(*offset)++]);
&nbsp; &nbsp;&nbsp;if&nbsp;(!data.CheckSizeInt32()) [[unlikely]] {
&nbsp; &nbsp; &nbsp; THROW_ERR_OUT_OF_RANGE(env,&nbsp;"keyData is too big");
&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{};
&nbsp; &nbsp; }

&nbsp; &nbsp; EVPKeyPointer::PrivateKeyEncodingConfig config;
&nbsp; &nbsp;&nbsp;if&nbsp;(!KeyObjectData::GetPrivateKeyEncodingFromJs(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;args, offset, kKeyContextInput)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;.To(&config)) {
&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{};
&nbsp; &nbsp; }

&nbsp; &nbsp; ncrypto::Buffer<constunsignedchar> buffer = {
&nbsp; &nbsp; &nbsp; &nbsp; .data = reinterpret_cast<constunsignedchar*>(data.data()),
&nbsp; &nbsp; &nbsp; &nbsp; .len = data.size(),
&nbsp; &nbsp; };

&nbsp; &nbsp;&nbsp;if&nbsp;(config.format == EVPKeyPointer::PKFormatType::PEM) {
&nbsp; &nbsp; &nbsp;&nbsp;// For PEM, we can easily determine whether it is a public or private key
&nbsp; &nbsp; &nbsp;&nbsp;// by looking for the respective PEM tags.
&nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;res = EVPKeyPointer::TryParsePublicKeyPEM(buffer);
&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(res) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;CreateAsymmetric(kKeyTypePublic,&nbsp;std::move(res.value));
&nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(res.error.value() == EVPKeyPointer::PKParseError::NOT_RECOGNIZED) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;TryParsePrivateKey(env, config, buffer);
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; ThrowCryptoError(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; env, res.openssl_error.value_or(0),&nbsp;"Failed to read asymmetric key");
&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{};
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are
&nbsp; &nbsp;&nbsp;// easy, but PKCS#1 can be a public key or a private key.
&nbsp; &nbsp;&nbsp;staticconstauto&nbsp;is_public = [](constauto& config,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;constauto& buffer) ->&nbsp;bool&nbsp;{
&nbsp; &nbsp; &nbsp;&nbsp;switch&nbsp;(config.type) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;EVPKeyPointer::PKEncodingType::PKCS1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;!EVPKeyPointer::IsRSAPrivateKey(buffer);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;EVPKeyPointer::PKEncodingType::SPKI:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returntrue;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;EVPKeyPointer::PKEncodingType::PKCS8:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnfalse;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;EVPKeyPointer::PKEncodingType::SEC1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnfalse;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;default:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; UNREACHABLE("Invalid key encoding type");
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; };

&nbsp; &nbsp;&nbsp;if&nbsp;(is_public(config, buffer)) {
&nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;res = EVPKeyPointer::TryParsePublicKey(config, buffer);
&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(res) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;CreateAsymmetric(KeyType::kKeyTypePublic,&nbsp;std::move(res.value));
&nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; ThrowCryptoError(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; env, res.openssl_error.value_or(0),&nbsp;"Failed to read asymmetric key");
&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{};
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;TryParsePrivateKey(env, config, buffer);
&nbsp; }

&nbsp; CHECK(args[*offset]->IsObject());
&nbsp; KeyObjectHandle* key =
&nbsp; &nbsp; &nbsp; BaseObject::Unwrap<KeyObjectHandle>(args[*offset].As<Object>());
&nbsp; CHECK_NOT_NULL(key);
&nbsp; CHECK_NE(key->Data().GetKeyType(), kKeyTypeSecret);
&nbsp; (*offset) +=&nbsp;4;
return&nbsp;key->Data().addRef();
}

在之前的版本中,我们可以得知js层传入的是PEM格式的公钥,因此寻找EVPKeyPointer::TryParsePublicKeyPEM函数定义(node/deps/ncrypto/ncrypto.cc):

&nbsp;复制代码&nbsp;隐藏代码
EVPKeyPointer::ParseKeyResult&nbsp;EVPKeyPointer::TryParsePublicKeyPEM(
&nbsp; &nbsp;&nbsp;const&nbsp;Buffer<constunsignedchar>& buffer)&nbsp;{
auto&nbsp;bp = BIOPointer::New(buffer.data, buffer.len);
if&nbsp;(!bp)&nbsp;return&nbsp;ParseKeyResult(PKParseError::FAILED);

// Try parsing as SubjectPublicKeyInfo (SPKI) first.
if&nbsp;(auto&nbsp;ret = TryParsePublicKeyInner(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bp,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"PUBLIC KEY",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [](constunsignedchar** p,&nbsp;long&nbsp;l) { &nbsp;// NOLINT(runtime/int)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;d2i_PUBKEY(nullptr, p, l);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })) {
&nbsp; &nbsp;&nbsp;return&nbsp;ret;
&nbsp; }

// Maybe it is PKCS#1.
if&nbsp;(auto&nbsp;ret = TryParsePublicKeyInner(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bp,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"RSA PUBLIC KEY",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [](constunsignedchar** p,&nbsp;long&nbsp;l) { &nbsp;// NOLINT(runtime/int)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })) {
&nbsp; &nbsp;&nbsp;return&nbsp;ret;
&nbsp; }

// X.509 fallback.
if&nbsp;(auto&nbsp;ret = TryParsePublicKeyInner(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bp,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"CERTIFICATE",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [](constunsignedchar** p,&nbsp;long&nbsp;l) { &nbsp;// NOLINT(runtime/int)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; X509Pointer x509(d2i_X509(nullptr, p, l));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;x509 ? X509_get_pubkey(x509.get()) : nullptr;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })) {
&nbsp; &nbsp;&nbsp;return&nbsp;ret;
&nbsp; };

return&nbsp;ParseKeyResult(PKParseError::NOT_RECOGNIZED);
}

在这里可以看到字符串形式的key被传入到了这个函数中(buffer),因此使用ida打开Typora.exe,搜索字符串RSA PUBLIC KEY,可以定位到这个函数:

&nbsp;复制代码&nbsp;隐藏代码
__int64 __fastcall&nbsp;sub_7FF63A554C50(__int64 *a1,&nbsp;char&nbsp;*a2)
{
&nbsp; __int64 v3;&nbsp;// rax
&nbsp; __int64 v4;&nbsp;// rsi
int&nbsp;v5;&nbsp;// ebx
&nbsp; __int64 v6;&nbsp;// rdx
int&nbsp;v7;&nbsp;// ebx
&nbsp; __int64 v8;&nbsp;// rax
&nbsp; __int64 v9;&nbsp;// rcx
unsignedint&nbsp;v10;&nbsp;// ebx
int&nbsp;v12;&nbsp;// ebp
&nbsp; __int64 v13;&nbsp;// rbx
&nbsp; __int64 v14;&nbsp;// rax
&nbsp; __int64 v15;&nbsp;// r14
char&nbsp;**v16;&nbsp;// rcx
unsigned&nbsp;__int64 v17;&nbsp;// [rsp+38h] [rbp-50h] BYREF
unsignedint&nbsp;v18;&nbsp;// [rsp+44h] [rbp-44h] BYREF
unsigned&nbsp;__int64 v19;&nbsp;// [rsp+48h] [rbp-40h] BYREF

&nbsp; v3 = sub_7FF63EA99BF0(a2);
if&nbsp;( !v3 )
&nbsp; &nbsp;&nbsp;return3;
&nbsp; v4 = v3;
&nbsp; v19 =&nbsp;0xAAAAAAAAAAAAAAAAuLL;
&nbsp; v18 =&nbsp;-1431655766;
&nbsp; sub_7FF63DDF6160();
&nbsp; v5 = sub_7FF63DDC9990((unsignedint)&v19, (unsignedint)&v18,&nbsp;0, (unsignedint)"PUBLIC KEY", v4,&nbsp;0LL,&nbsp;0LL);
&nbsp; sub_7FF63DDF61A0();
if&nbsp;( v5 ==&nbsp;1&nbsp;)
&nbsp; {
&nbsp; &nbsp; v17 = v19;
&nbsp; &nbsp; v8 = sub_7FF63DDF5050(0LL, &v17, v18);
&nbsp; &nbsp;&nbsp;goto&nbsp;LABEL_7;
&nbsp; }
if&nbsp;( !(unsignedint)sub_7FF63E302B60(v4) )
&nbsp; {
&nbsp; &nbsp; v16 = off_7FF641CC1B88;
LABEL_21:
&nbsp; &nbsp; sub_7FF639661A70(v16, v6);
&nbsp; }
&nbsp; v19 =&nbsp;0xAAAAAAAAAAAAAAAAuLL;
&nbsp; v18 =&nbsp;-1431655766;
&nbsp; sub_7FF63DDF6160();
&nbsp; v7 = sub_7FF63DDC9990((unsignedint)&v19, (unsignedint)&v18,&nbsp;0, (unsignedint)"RSA PUBLIC KEY", v4,&nbsp;0LL,&nbsp;0LL);
&nbsp; sub_7FF63DDF61A0();
if&nbsp;( v7 ==&nbsp;1&nbsp;)
&nbsp; {
&nbsp; &nbsp; v17 = v19;
&nbsp; &nbsp; v8 = sub_7FF63DDF4F70(6LL,&nbsp;0LL, &v17, v18);
LABEL_7:
&nbsp; &nbsp; v9 = *a1;
&nbsp; &nbsp; *a1 = v8;
LABEL_8:
&nbsp; &nbsp;&nbsp;if&nbsp;( v9 )
&nbsp; &nbsp; &nbsp; sub_7FF6390B1B90();
&nbsp; &nbsp; sub_7FF63DDCB0E0(v19, (int)v18);
&nbsp; &nbsp; v10 =&nbsp;3&nbsp;* (*a1 ==&nbsp;0);
&nbsp; &nbsp;&nbsp;goto&nbsp;LABEL_11;
&nbsp; }
if&nbsp;( !(unsignedint)sub_7FF63E302B60(v4) )
&nbsp; {
&nbsp; &nbsp; v16 = off_7FF641CC1C78;
&nbsp; &nbsp;&nbsp;goto&nbsp;LABEL_21;
&nbsp; }
&nbsp; v19 =&nbsp;0xAAAAAAAAAAAAAAAAuLL;
&nbsp; v18 =&nbsp;-1431655766;
&nbsp; sub_7FF63DDF6160();
&nbsp; v12 = sub_7FF63DDC9990((unsignedint)&v19, (unsignedint)&v18,&nbsp;0, (unsignedint)"CERTIFICATE", v4,&nbsp;0LL,&nbsp;0LL);
&nbsp; sub_7FF63DDF61A0();
&nbsp; v10 =&nbsp;1;
if&nbsp;( v12 ==&nbsp;1&nbsp;)
&nbsp; {
&nbsp; &nbsp; v17 = v19;
&nbsp; &nbsp; v13 =&nbsp;0LL;
&nbsp; &nbsp; v14 = sub_7FF63DDC6890(0LL, &v17, v18);
&nbsp; &nbsp;&nbsp;if&nbsp;( v14 )
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; v15 = v14;
&nbsp; &nbsp; &nbsp; v13 = sub_7FF63DDC7F60(v14);
&nbsp; &nbsp; &nbsp; sub_7FF63DDC67E0(v15);
&nbsp; &nbsp; }
&nbsp; &nbsp; v9 = *a1;
&nbsp; &nbsp; *a1 = v13;
&nbsp; &nbsp;&nbsp;goto&nbsp;LABEL_8;
&nbsp; }
LABEL_11:
&nbsp; sub_7FF63E3029A0(v4);
return&nbsp;v10;
}

在函数头下断点,rdx所指内存区域就是PEM格式公钥

后续编写补丁,注册机就不再赘述了(记得patch掉网验)

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,不会错过新的消息通知

开放注册、精华文章和周边活动等公告


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:利刃信安 《Typora 1.10.8公钥替换》

评论:0   参与:  0