Typora 1.10.8公钥替换

admin 2025年6月25日16:42:19评论6 views字数 7729阅读25分45秒阅读模式
作者坛账号: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",            PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,                                    ncrypto::Cipher::recover>);

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

 复制代码 隐藏代码template <PublicKeyCipher::Operation operation,          PublicKeyCipher::Cipher_t cipher>voidPublicKeyCipher::Cipher(const FunctionCallbackInfo<Value>& args) {  MarkPopErrorOnReturn mark_pop_error_on_return;  Environment* env = Environment::GetCurrent(args);unsignedint offset = 0;auto data = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset);if (!data) return;constauto& pkey = data.GetAsymmetricKey();if (!pkey) return;  ArrayBufferOrViewContents<unsignedcharbuf(args[offset]);if (!buf.CheckSizeInt32()) [[unlikely]] {return THROW_ERR_OUT_OF_RANGE(env, "buffer is too long");  }uint32_t padding;if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return;if (cipher == ncrypto::Cipher::decrypt &&      operation == PublicKeyCipher::kPrivate && padding == RSA_PKCS1_PADDING) {    EVPKeyCtxPointer ctx = pkey.newCtx();    CHECK(ctx);if (!ctx.initForDecrypt()) {return ThrowCryptoError(env, ERR_get_error());    }// RSA implicit rejection here is not supported by BoringSSL.if (!ctx.setRsaImplicitRejection()) [[unlikely]] {return THROW_ERR_INVALID_ARG_VALUE(          env,"RSA_PKCS1_PADDING is no longer supported for private decryption");    }  }  Digest digest;if (args[offset + 2]->IsString()) {    Utf8Value oaep_str(env->isolate(), args[offset + 2]);    digest = Digest::FromName(*oaep_str);if (!digest) return THROW_ERR_OSSL_EVP_INVALID_DIGEST(env);  }  ArrayBufferOrViewContents<unsignedcharoaep_label(      !args[offset + 3]->IsUndefined() ? args[offset + 3] : Local<Value>());if (!oaep_label.CheckSizeInt32()) [[unlikely]] {return THROW_ERR_OUT_OF_RANGE(env, "oaepLabel is too big");  }std::unique_ptr<BackingStore> out;if (!Cipher<cipher>(env, pkey, padding, digest, oaep_label, buf, &out)) {return ThrowCryptoError(env, ERR_get_error());  }  Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(out));  args.GetReturnValue().Set(      Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local<Value>()));}

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

 复制代码 隐藏代码KeyObjectData KeyObjectData::GetPublicOrPrivateKeyFromJs(const FunctionCallbackInfo<Value>& args, unsignedint* offset) {if (IsAnyBufferSource(args[*offset])) {    Environment* env = Environment::GetCurrent(args);    ArrayBufferOrViewContents<chardata(args[(*offset)++]);if (!data.CheckSizeInt32()) [[unlikely]] {      THROW_ERR_OUT_OF_RANGE(env, "keyData is too big");return {};    }    EVPKeyPointer::PrivateKeyEncodingConfig config;if (!KeyObjectData::GetPrivateKeyEncodingFromJs(             args, offset, kKeyContextInput)             .To(&config)) {return {};    }    ncrypto::Buffer<constunsignedchar> buffer = {        .data = reinterpret_cast<constunsignedchar*>(data.data()),        .len = data.size(),    };if (config.format == EVPKeyPointer::PKFormatType::PEM) {// For PEM, we can easily determine whether it is a public or private key// by looking for the respective PEM tags.auto res = EVPKeyPointer::TryParsePublicKeyPEM(buffer);if (res) {return CreateAsymmetric(kKeyTypePublic, std::move(res.value));      }if (res.error.value() == EVPKeyPointer::PKParseError::NOT_RECOGNIZED) {return TryParsePrivateKey(env, config, buffer);      }      ThrowCryptoError(          env, res.openssl_error.value_or(0), "Failed to read asymmetric key");return {};    }// For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are// easy, but PKCS#1 can be a public key or a private key.staticconstauto is_public = [](constauto& config,constauto& buffer) -> bool {switch (config.type) {case EVPKeyPointer::PKEncodingType::PKCS1:return !EVPKeyPointer::IsRSAPrivateKey(buffer);case EVPKeyPointer::PKEncodingType::SPKI:returntrue;case EVPKeyPointer::PKEncodingType::PKCS8:returnfalse;case EVPKeyPointer::PKEncodingType::SEC1:returnfalse;default:          UNREACHABLE("Invalid key encoding type");      }    };if (is_public(config, buffer)) {auto res = EVPKeyPointer::TryParsePublicKey(config, buffer);if (res) {return CreateAsymmetric(KeyType::kKeyTypePublic, std::move(res.value));      }      ThrowCryptoError(          env, res.openssl_error.value_or(0), "Failed to read asymmetric key");return {};    }return TryParsePrivateKey(env, config, buffer);  }  CHECK(args[*offset]->IsObject());  KeyObjectHandle* key =      BaseObject::Unwrap<KeyObjectHandle>(args[*offset].As<Object>());  CHECK_NOT_NULL(key);  CHECK_NE(key->Data().GetKeyType(), kKeyTypeSecret);  (*offset) += 4;return key->Data().addRef();}

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

 复制代码 隐藏代码EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM(const Buffer<constunsignedchar>& buffer) {auto bp = BIOPointer::New(buffer.data, buffer.len);if (!bp) return ParseKeyResult(PKParseError::FAILED);// Try parsing as SubjectPublicKeyInfo (SPKI) first.if (auto ret = TryParsePublicKeyInner(          bp,"PUBLIC KEY",          [](constunsignedchar** p, long l) {  // NOLINT(runtime/int)return d2i_PUBKEY(nullptr, p, l);          })) {return ret;  }// Maybe it is PKCS#1.if (auto ret = TryParsePublicKeyInner(          bp,"RSA PUBLIC KEY",          [](constunsignedchar** p, long l) {  // NOLINT(runtime/int)return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l);          })) {return ret;  }// X.509 fallback.if (auto ret = TryParsePublicKeyInner(          bp,"CERTIFICATE",          [](constunsignedchar** p, long l) {  // NOLINT(runtime/int)            X509Pointer x509(d2i_X509(nullptr, p, l));return x509 ? X509_get_pubkey(x509.get()) : nullptr;          })) {return ret;  };return ParseKeyResult(PKParseError::NOT_RECOGNIZED);}

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

 复制代码 隐藏代码__int64 __fastcall sub_7FF63A554C50(__int64 *a1, char *a2){  __int64 v3; // rax  __int64 v4; // rsiint v5; // ebx  __int64 v6; // rdxint v7; // ebx  __int64 v8; // rax  __int64 v9; // rcxunsignedint v10; // ebxint v12; // ebp  __int64 v13; // rbx  __int64 v14; // rax  __int64 v15; // r14char **v16; // rcxunsigned __int64 v17; // [rsp+38h] [rbp-50h] BYREFunsignedint v18; // [rsp+44h] [rbp-44h] BYREFunsigned __int64 v19; // [rsp+48h] [rbp-40h] BYREF  v3 = sub_7FF63EA99BF0(a2);if ( !v3 )return3;  v4 = v3;  v19 = 0xAAAAAAAAAAAAAAAAuLL;  v18 = -1431655766;  sub_7FF63DDF6160();  v5 = sub_7FF63DDC9990((unsignedint)&v19, (unsignedint)&v18, 0, (unsignedint)"PUBLIC KEY", v4, 0LL0LL);  sub_7FF63DDF61A0();if ( v5 == 1 )  {    v17 = v19;    v8 = sub_7FF63DDF5050(0LL, &v17, v18);goto LABEL_7;  }if ( !(unsignedint)sub_7FF63E302B60(v4) )  {    v16 = off_7FF641CC1B88;LABEL_21:    sub_7FF639661A70(v16, v6);  }  v19 = 0xAAAAAAAAAAAAAAAAuLL;  v18 = -1431655766;  sub_7FF63DDF6160();  v7 = sub_7FF63DDC9990((unsignedint)&v19, (unsignedint)&v18, 0, (unsignedint)"RSA PUBLIC KEY", v4, 0LL0LL);  sub_7FF63DDF61A0();if ( v7 == 1 )  {    v17 = v19;    v8 = sub_7FF63DDF4F70(6LL0LL, &v17, v18);LABEL_7:    v9 = *a1;    *a1 = v8;LABEL_8:if ( v9 )      sub_7FF6390B1B90();    sub_7FF63DDCB0E0(v19, (int)v18);    v10 = 3 * (*a1 == 0);goto LABEL_11;  }if ( !(unsignedint)sub_7FF63E302B60(v4) )  {    v16 = off_7FF641CC1C78;goto LABEL_21;  }  v19 = 0xAAAAAAAAAAAAAAAAuLL;  v18 = -1431655766;  sub_7FF63DDF6160();  v12 = sub_7FF63DDC9990((unsignedint)&v19, (unsignedint)&v18, 0, (unsignedint)"CERTIFICATE", v4, 0LL0LL);  sub_7FF63DDF61A0();  v10 = 1;if ( v12 == 1 )  {    v17 = v19;    v13 = 0LL;    v14 = sub_7FF63DDC6890(0LL, &v17, v18);if ( v14 )    {      v15 = v14;      v13 = sub_7FF63DDC7F60(v14);      sub_7FF63DDC67E0(v15);    }    v9 = *a1;    *a1 = v13;goto LABEL_8;  }LABEL_11:  sub_7FF63E3029A0(v4);return v10;}

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

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

原文始发于微信公众号(吾爱破解论坛):Typora 1.10.8公钥替换

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月25日16:42:19
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Typora 1.10.8公钥替换https://cn-sec.com/archives/4199339.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息