在之前的文章中,我们已经尝试了通过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进制搜索替换掉这些立即数即可