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

查看 81|回复 9
作者:xqyqx   
JSC字节码反编译初探——以Typora 1.10.8为例
在之前的文章中,我们已经尝试了通过hook node api的方式替换公钥,这次我们来尝试一下从那个已经被编译为字节码的jsc入手
在网上搜索jsc反编译相关的内容,找到一篇相关教程,作者通过修改d8,添加Disassemble、LoadJSC函数,从而实现解析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 bytecode,
                        std::unordered_set[u]& 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[u](bytecode.ptr());
  if (visited.count(key)) {
    return;
  }
  visited.insert(key);
  for (int i = 0; i (bytecode.ptr()));
  fflush(stdout);
  v8::internal::OFStream os(stdout);
  bytecode->Disassemble(os);
  auto consts = bytecode->constant_pool();
  for (int i = 0; i length());
  fflush(stdout);
  for (int i = 0; i length(); i++) {
    auto obj = consts->get(i);
    if (v8::internal::IsSharedFunctionInfo(obj)) {
      auto shared = v8::internal::Cast(obj);
      for (int i = 0; i  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 & args) {
  auto isolate = reinterpret_cast(args.GetIsolate());
  for (int i = 0; i ThrowException(v8::Exception::Error(
          v8::String::NewFromUtf8(args.GetIsolate(), "Error loading file").ToLocalChecked()));
      return;
    }
    int length = 0;
    auto filedata = reinterpret_cast[u](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 maybe_fun =
        v8::internal::CodeSerializer::Deserialize(isolate, &cached_data, source, script_details);
    v8::internal::Handle 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[u] 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.h的class Shell中添加LoadJSC声明:
static void LoadJSC(const v8::FunctionCallbackInfo& args);
src/diagnostics/objects-printer.cc:
注释掉:PrintSourceCode(os);

  os
后添加
  os GetActiveBytecodeArray(isolate)->Disassemble(os);
  os

void HeapObject::HeapObjectShortPrint(std::ostream& os) {
  PtrComprCageBase cage_base = GetPtrComprCageBase();
后添加
  Isolate* isolate = nullptr;
  if (!GetIsolateFromHeapObject(*this, &isolate) || isolate == nullptr) {
    os (this->ptr())  map_of_this_object = this->map(cage_base);
  if (map_of_this_object.ptr() == kNullAddress) {
    os (this->ptr()) map(cage_base) != roots.meta_map()) {
    os (this->ptr())

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

    case FIXED_ARRAY_TYPE:
      os (*this)->length() ";
后添加
      os (*this)->FixedArrayPrint(os);
      os

    case OBJECT_BOILERPLATE_DESCRIPTION_TYPE:
      os (*this)->capacity() ";
后添加
      os (*this)
          ->ObjectBoilerplateDescriptionPrint(os);
      os

    case FIXED_DOUBLE_ARRAY_TYPE:
      os (*this)->length()
         ";
后添加
      os (*this)->FixedDoubleArrayPrint(os);
      os

      } else {
        os ";
      }
后添加
      os SharedFunctionInfoPrint(os);
      os
src/snapshot/code-serializer.cc:
替换SanityCheck、SanityCheckWithoutSource函数:
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[I]::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 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 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进制搜索替换掉这些立即数即可

字节, 反编译

Azuria   


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   

感谢楼主的精彩分享,受益匪浅!
我是不会改名的   

应该就是右移 1 位
0xb6>>1     91
0x44>>1     34
PoJieDaWang123   

感谢分享,新的思路
sunflash   

来了来了,看到Typora必进。感谢楼主,也感谢Typora对技术普及作出的贡献
fridaynice   

学习到了,不过还在理解中,受益匪浅
iamok   

学习v8字节码玩法。。
dabaistyle   

谢谢分享💪✨
nickley   

学习了,发现自学还是非常难的
您需要登录后才可以回帖 登录 | 立即注册

返回顶部