吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3207|回复: 14
收起左侧

[原创] JSC字节码反编译初探——以Typora 1.10.8为例

  [复制链接]
xqyqx 发表于 2025-8-21 02:11
本帖最后由 xqyqx 于 2025-8-21 09:40 编辑

JSC字节码反编译初探——以Typora 1.10.8为例

在之前的文章中,我们已经尝试了通过hook node api的方式替换公钥,这次我们来尝试一下从那个已经被编译为字节码的jsc入手

在网上搜索jsc反编译相关的内容,找到一篇相关教程,作者通过修改d8,添加DisassembleLoadJSC函数,从而实现解析jsc,我们也依照此思路进行分析

制作反编译器

在安装目录下的version文件中可以看到electron 版本为32.1.2,在网上搜索可知对应的v8版本为12.8.374.33,我们先在本地搭建v8编译环境,并git checkout 12.8.374.33

由于自12版本开始,v8引擎做了比较大的api变动,作者原先教程中的修改代码不再适用,下面是我在此基础上做的修正:

src/d8/d8.cpp中添加下面两个方法:

static void Disassemble(v8::internal::Isolate* isolate, 
                        v8::internal::Tagged<v8::internal::BytecodeArray> bytecode, 
                        std::unordered_set<uintptr_t>& visited,
                        int depth) {
  if (depth > 100) { 
    v8::internal::PrintF("Recursion depth limit reached, aborting disassembly for this path.\n");
    fflush(stdout);
    return;
  }

  uintptr_t key = reinterpret_cast<uintptr_t>(bytecode.ptr());
  if (visited.count(key)) {
    return;
  }
  visited.insert(key);
  for (int i = 0; i < depth; ++i) v8::internal::PrintF("  ");
  v8::internal::PrintF("Disassembling BytecodeArray at: %p\n", reinterpret_cast<void*>(bytecode.ptr()));
  fflush(stdout);
  v8::internal::OFStream os(stdout);
  bytecode->Disassemble(os);

  auto consts = bytecode->constant_pool();

  for (int i = 0; i < depth; ++i) v8::internal::PrintF("  ");
  v8::internal::PrintF("Constant pool size: %d\n", consts->length());
  fflush(stdout);

  for (int i = 0; i < consts->length(); i++) {
    auto obj = consts->get(i);
    if (v8::internal::IsSharedFunctionInfo(obj)) {
      auto shared = v8::internal::Cast<v8::internal::SharedFunctionInfo>(obj);

      for (int i = 0; i < depth; ++i) v8::internal::PrintF("  ");
      v8::internal::PrintF("--> Found SFI in constant pool at index %d: ", i);

      auto function_name = shared->Name();
      if (function_name->length() > 0) {
          v8::internal::PrintF("%s\n", function_name->ToCString().get());
      } else {
          v8::internal::PrintF("(anonymous)\n");
      }
      fflush(stdout);

      if (shared->HasBytecodeArray()) {
          Disassemble(isolate, shared->GetBytecodeArray(isolate), visited, depth + 1);
      } else {
          for (int i = 0; i < depth; ++i) v8::internal::PrintF("  ");
          v8::internal::PrintF("    (SFI has no bytecode array, skipping)\n");
          fflush(stdout);
      }
    }
  }
}

void v8::Shell::LoadJSC(const v8::FunctionCallbackInfo<v8::Value>& args) {
  auto isolate = reinterpret_cast<v8::internal::Isolate*>(args.GetIsolate());
  for (int i = 0; i < args.Length(); i++) {
    v8::String::Utf8Value filename(args.GetIsolate(), args[i]);
    if (*filename == NULL) {
      args.GetIsolate()->ThrowException(v8::Exception::Error(
          v8::String::NewFromUtf8(args.GetIsolate(), "Error loading file").ToLocalChecked()));
      return;
    }
    int length = 0;
    auto filedata = reinterpret_cast<uint8_t*>(ReadChars(*filename, &length));
    if (filedata == NULL) {
      args.GetIsolate()->ThrowException(v8::Exception::Error(
          v8::String::NewFromUtf8(args.GetIsolate(), "Error reading file").ToLocalChecked()));
      return;
    }
    v8::internal::AlignedCachedData cached_data(filedata, length);
    auto source = isolate->factory()
                      ->NewStringFromUtf8(base::CStrVector("source"))
                      .ToHandleChecked();
    v8::internal::ScriptDetails script_details;
    v8::internal::MaybeHandle<v8::internal::SharedFunctionInfo> maybe_fun =
        v8::internal::CodeSerializer::Deserialize(isolate, &cached_data, source, script_details);

    v8::internal::Handle<v8::internal::SharedFunctionInfo> fun;
    if (!maybe_fun.ToHandle(&fun)) {
      args.GetIsolate()->ThrowException(v8::Exception::Error(
          v8::String::NewFromUtf8(args.GetIsolate(), "Deserialize failed, possibly version mismatch or invalid .jsc file").ToLocalChecked()));
      delete[] filedata;
      return;
    }

    v8::internal::PrintF("---- Starting disassembly of %s ----\n", *filename);
    fflush(stdout);

    std::unordered_set<uintptr_t> visited;
    Disassemble(isolate, fun->GetBytecodeArray(isolate), visited, 0); 

    v8::internal::PrintF("---- Finished disassembly of %s ----\n", *filename);
    fflush(stdout);

    delete[] filedata;
  }
}

并在Shell::CreateGlobalTemplate中添加代码:

global_template->Set(
    v8::String::NewFromUtf8(isolate, "loadjsc", v8::NewStringType::kNormal)
        .ToLocalChecked(),
    v8::FunctionTemplate::New(isolate, v8::Shell::LoadJSC));

src/d8/d8.hclass Shell中添加LoadJSC声明:

static void LoadJSC(const v8::FunctionCallbackInfo<v8::Value>& args);

src/diagnostics/objects-printer.cc:

注释掉:PrintSourceCode(os);

  os << "\n - age: " << age();
  os << "\n";

后添加

  os << "\nStart BytecodeArray\n";
  this->GetActiveBytecodeArray(isolate)->Disassemble(os);
  os << "\nEnd BytecodeArray\n";

void HeapObject::HeapObjectShortPrint(std::ostream& os) {
  PtrComprCageBase cage_base = GetPtrComprCageBase();

后添加

  Isolate* isolate = nullptr;
  if (!GetIsolateFromHeapObject(*this, &isolate) || isolate == nullptr) {
    os << "[!!! Corrupted HeapObject (cannot get Isolate) at "
       << reinterpret_cast<void*>(this->ptr()) << " !!!]";
    return;
  }
  ReadOnlyRoots roots(isolate);
  Tagged<Map> map_of_this_object = this->map(cage_base);
  if (map_of_this_object.ptr() == kNullAddress) {
    os << "[!!! Corrupted HeapObject (null map pointer) at "
       << reinterpret_cast<void*>(this->ptr()) << " !!!]";
    return;
  }
  if (map_of_this_object->map(cage_base) != roots.meta_map()) {
    os << "[!!! Corrupted HeapObject (invalid map) at "
       << reinterpret_cast<void*>(this->ptr()) << " !!!]";
    return;
  }

    os << accumulator.ToCString().get();
    return;
  }

后添加

  if (map(cage_base)->instance_type() == ASM_WASM_DATA_TYPE) {
    os << "<ArrayBoilerplateDescription> ";
    Cast<ArrayBoilerplateDescription>(*this)
        ->constant_elements()
        ->HeapObjectShortPrint(os);
    return;
  }

    case FIXED_ARRAY_TYPE:
      os << "<FixedArray[" << Cast<FixedArray>(*this)->length() << "]>";

后添加

      os << "\nStart FixedArray\n";
      Cast<FixedArray>(*this)->FixedArrayPrint(os);
      os << "\nEnd FixedArray\n";

    case OBJECT_BOILERPLATE_DESCRIPTION_TYPE:
      os << "<ObjectBoilerplateDescription["
         << Cast<ObjectBoilerplateDescription>(*this)->capacity() << "]>";

后添加

      os << "\nStart ObjectBoilerplateDescription\n";
      Cast<ObjectBoilerplateDescription>(*this)
          ->ObjectBoilerplateDescriptionPrint(os);
      os << "\nEnd ObjectBoilerplateDescription\n";

    case FIXED_DOUBLE_ARRAY_TYPE:
      os << "<FixedDoubleArray[" << Cast<FixedDoubleArray>(*this)->length()
         << "]>";

后添加

      os << "\nStart FixedDoubleArray\n";
      Cast<FixedDoubleArray>(*this)->FixedDoubleArrayPrint(os);
      os << "\nEnd FixedDoubleArray\n";

      } else {
        os << "<SharedFunctionInfo>";
      }

后添加

      os << "\nStart SharedFunctionInfo\n";
      shared->SharedFunctionInfoPrint(os);
      os << "\nEnd SharedFunctionInfo\n";

src/snapshot/code-serializer.cc

替换SanityCheckSanityCheckWithoutSource函数:

SerializedCodeSanityCheckResult SerializedCodeData::SanityCheck(
    uint32_t expected_ro_snapshot_checksum,
    uint32_t expected_source_hash) const {
  return SerializedCodeSanityCheckResult::kSuccess;
}

SerializedCodeSanityCheckResult SerializedCodeData::SanityCheckWithoutSource(
    uint32_t expected_ro_snapshot_checksum) const {
  // Always return kSuccess to bypass all checks.
  return SerializedCodeSanityCheckResult::kSuccess;
}

src/snapshot/deserializer.cc

替换ReadReadOnlyHeapRef函数:

int Deserializer<IsolateT>::ReadReadOnlyHeapRef(uint8_t data,
                                                SlotAccessor slot_accessor) {
  uint32_t chunk_index = source_.GetUint30();
  uint32_t chunk_offset = source_.GetUint30();

  ReadOnlySpace* read_only_space = isolate()->heap()->read_only_space();

  if (chunk_index >= read_only_space->pages().size()) {
    Tagged<Hole> the_hole = *isolate()->factory()->the_hole_value();

    return WriteHeapPointer(slot_accessor, the_hole,
                            GetAndResetNextReferenceDescriptor());
  }

  ReadOnlyPageMetadata* page = read_only_space->pages()[chunk_index];
  Address address = page->OffsetToAddress(chunk_offset);
  Tagged<HeapObject> heap_object = HeapObject::FromAddress(address);

  return WriteHeapPointer(slot_accessor, heap_object,
                          GetAndResetNextReferenceDescriptor());
}

以上为全部修改,之后使用ninja -C out.gn/x64.release d8编译

编译好后运行./out.gn/x64.release/d8 -e "loadjsc('atom.compiled.dist.jsc')" > atom.txt即可得到反编译后的结果:

https://wwri.lanzouo.com/iB9Ea34181jc

分析atom.txt

面对海量的字节码,我们直奔主题,寻找rsa公钥,在之前版本的atom.js中,我们可以得知公钥是base64解析出来的:

T = JSON.parse(Buffer.from("WyItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLSIsIk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN25Wb0dDSHFJTUp5cWdBTEVVcmMiLCI1SkpoYXAwK0h0SnF6UEUwNHB6NHkrbnJPbVk3LzEyZjNIdlp5eW9Sc3hLZFhUWmJPMHdFSEZJaDBjUnFzdWFKIiwiUHlhT09QYkEwQnNhbG9mSUFZM21SaFFRM3ZTZitybjNnK3cwUyt1ZFdtS1Y5RG5tSmxwV3FpekZhalU0VC9FNCIsIjVaZ01OY1h0M0UxaXBzMzJyZGJUUjBObmVuOVBWSVR2cmJKM2w2Q0kyQkZCSW1aUVoyUDhOK0xzcWZKc3F5VlYiLCJ3RGt0M21IQVZ4VjdGWmJmWVdHKzhGRFN1S1FIYUNtdmdBdENoeDlod2wzSjZSZWtrcURWYTZHSVYxM0QyM0xTIiwicWRrMEpiNTIxd0ZKaS9WNlFBSzZTTEJpYnk1Z1lONnpRUTVSUXBqWHRSNTNNd3pUZGlBekdFdUtkT3RyWTJNZSIsIkR3SURBUUFCIiwiLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tIiwiIiwiIl0=","base64").toString("utf8")).join("\n"),
I = 864e5;
var W = "https://store.typora.io";

我们直接在反编译结果中搜索这段base64没有搜索到,怀疑是v8对其进行了优化,我们可以通过后面的字符串https://store.typora.io来尝试对这段代码进行定位,在反编译结果中搜索,向上可以找到一个长度为476的数组:

0x3340001a57f9: [FixedArray] in OldSpace
 - map: [!!! Corrupted HeapObject (cannot get Isolate) at 0x33400000065d !!!]
 - length: 476
           0: 91
           1: 34
         2-6: 45
           7: 66
           8: 69
           9: 71
          10: 73
          11: 78
          12: 32
          13: 80
          14: 85
          15: 66
          16: 76
          17: 73
          18: 67

将数字转换为ascii,发现是PEM公钥的数组形式

那我们如何对其进行修改呢?

注意到这个数组在Constant pool中,也就是储存在堆中的,而不是动态生成,因此我们可以在jsc文件中找到并对其进行修改

在jsc中我们同样以https://store.typora.io作为锚点进行寻找,发现往上有一个很长的数据块:

03 00 00 B6 00 00 00 44 00 00 00 5A 00 00 00 5A
00 00 00 5A 00 00 00 5A 00 00 00 5A 00 00 00 84
00 00 00 8A 00 00 00 8E 00 00 00 92 00 00 00 9C
00 00 00 40 00 00 00 A0 00 00 00 AA 00 00 00 84
00 00 00 98 00 00 00 92 00 00 00 86 00 00 00 40
00 00 00 96 00 00 00 8A 00 00 00 B2 00 00 00 5A
00 00 00 5A 00 00 00 5A 00 00 00 5A 00 00 00 5A
00 00 00 44 00 00 00 58 00 00 00 44 00 00 00 9A
00 00 00 92 00 00 00 92 00 00 00 84 00 00 00 92
00 00 00 D4 00 00 00 82 00 00 00 9C 00 00 00 84
......

这里我们发现了两组5个重复的0x5A,中间夹着16个字节,这恰好与我们先前在Constant pool中找到的数组相对应(两组5个重复的45中间夹着16个数),可以确定这段数据就是公钥

尝试异或发现不对,于是猜测应该是类似cython一样,每个字符都有对应的标识符(例如这里0x5A对应45),由于原始公钥内有足够多的字符,应该可以生成一个对应表,这样就可以替换公钥了

关于网验:
可以找到下面一段字节码:

         0xba50004e020 @   76 : c2                Star8
         0xba50004e021 @   77 : 0d 2f             LdaSmi [47]
         0xba50004e023 @   79 : c0                Star10
         0xba50004e024 @   80 : 0d 61             LdaSmi [97]
         0xba50004e026 @   82 : bf                Star11
         0xba50004e027 @   83 : 0d 70             LdaSmi [112]
         0xba50004e029 @   85 : be                Star12
         0xba50004e02a @   86 : 0d 69             LdaSmi [105]
         0xba50004e02c @   88 : bd                Star13
         0xba50004e02d @   89 : 0d 2f             LdaSmi [47]
         0xba50004e02f @   91 : bc                Star14
         0xba50004e030 @   92 : 0d 63             LdaSmi [99]
         0xba50004e032 @   94 : bb                Star15
         0xba50004e033 @   95 : 0d 6c             LdaSmi [108]
         0xba50004e035 @   97 : 18 e9             Star r16
         0xba50004e037 @   99 : 0d 69             LdaSmi [105]
         0xba50004e039 @  101 : 18 e8             Star r17
         0xba50004e03b @  103 : 0d 65             LdaSmi [101]
         0xba50004e03d @  105 : 18 e7             Star r18
         0xba50004e03f @  107 : 0d 6e             LdaSmi [110]
         0xba50004e041 @  109 : 18 e6             Star r19
         0xba50004e043 @  111 : 0d 74             LdaSmi [116]
         0xba50004e045 @  113 : 18 e5             Star r20
         0xba50004e047 @  115 : 0d 2f             LdaSmi [47]
         0xba50004e049 @  117 : 18 e4             Star r21
         0xba50004e04b @  119 : 0d 72             LdaSmi [114]
         0xba50004e04d @  121 : 18 e3             Star r22
         0xba50004e04f @  123 : 0d 65             LdaSmi [101]
         0xba50004e051 @  125 : 18 e2             Star r23
         0xba50004e053 @  127 : 0d 6e             LdaSmi [110]
         0xba50004e055 @  129 : 18 e1             Star r24
         0xba50004e057 @  131 : 0d 65             LdaSmi [101]
         0xba50004e059 @  133 : 18 e0             Star r25
         0xba50004e05b @  135 : 0d 77             LdaSmi [119]

对应的字符串为/api/client/renew,作者应该是在js源码中使用charCodeAt防止直接搜字符串被搜到用16进制搜索替换掉这些立即数即可

免费评分

参与人数 9吾爱币 +11 热心值 +7 收起 理由
Li1y + 2 + 1 用心讨论,共获提升!
adam717 + 1 用心讨论,共获提升!
fin618 + 1 + 1 热心回复!
pedoc + 1 + 1 谢谢@Thanks!
小朋友呢 + 2 + 1 我很赞同!
PhiFever + 1 + 1 我很赞同!
scz + 1 + 1 谢谢
helian147 + 1 + 1 热心回复!
fireworld + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Azuria 发表于 2025-8-21 10:46
本帖最后由 Azuria 于 2025-8-21 12:29 编辑
03 00 00 B6 00 00 00 44 00 00 00 5A 00 00 00 5A
00 00 00 5A 00 00 00 5A 00 00 00 5A 00 00 00 84
00 00 00 8A 00 00 00 8E 00 00 00 92 00 00 00 9C
00 00 00 40 00 00 00 A0 00 00 00 AA 00 00 00 84
00 00 00 98 00 00 00 92 00 00 00 86 00 00 00 40
00 00 00 96 00 00 00 8A 00 00 00 B2 00 00 00 5A
00 00 00 5A 00 00 00 5A 00 00 00 5A 00 00 00 5A
00 00 00 44 00 00 00 58 00 00 00 44 00 00 00 9A
00 00 00 92 00 00 00 92 00 00 00 84 00 00 00 92
00 00 00 D4 00 00 00 82 00 00 00 9C 00 00 00 84
......

这里我们发现了两组5个重复的0x5A,中间夹着16个字节,这恰好与我们先前在Constant pool中找到的数组相对应(两组5个重复的45中间夹着16个数),可以确定这段数据就是公钥

尝试异或发现不对,于是猜测应该是类似cython一样,每个字符都有对应的标识符(例如这里0x5A对应45),由于原始公钥内有足够多的字符,应该可以生成一个对应表,这样就可以替换公钥了

这里的公钥并没有对齐,数组事实上是从 B6 00 00 00 开始的。

公钥和用于验证公钥的明文密文都用的v8 smi存储,最低位其实是tag,对于smi来说总是0,如果整数较大的话这里可能会存HeapObject的指针,并将tag置为1。

misc做的多的话很容易发现字节码里有三个元素大小为dword的smi大数组,仅更改私钥的情况下typora.log可见block type is not 01错误,即解密后数据不满足pkcs1 padding,也就是密钥不对。

事实上如果仅patch字节码的话需要修改的地方不止是公钥,不过这里不再叙述,毕竟typora开发者换人了。

pangpang12138 发表于 2025-8-21 07:00
我是不会改名的 发表于 2025-8-21 08:49
应该就是右移 1 位
0xb6>>1     91
0x44>>1     34

免费评分

参与人数 1吾爱币 +2 热心值 +1 收起 理由
xqyqx + 2 + 1 我很赞同!

查看全部评分

PoJieDaWang123 发表于 2025-8-21 09:21
感谢分享,新的思路
sunflash 发表于 2025-8-21 09:37
来了来了,看到Typora必进。感谢楼主,也感谢Typora对技术普及作出的贡献
fridaynice 发表于 2025-8-21 10:03
学习到了,不过还在理解中,受益匪浅
iamok 发表于 2025-8-21 10:20
学习v8字节码玩法。。
dabaistyle 发表于 2025-8-21 10:25
谢谢分享&#128170;&#10024;
nickley 发表于 2025-8-21 11:05
学习了,发现自学还是非常难的
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-11-10 02:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表