A nearly dependency-free disassembler and assembler for the Hermes bytecode, written in Rust.
Note: Still a WIP - A PR is always welcome. The API is subject to change as I iterate over use cases and improve the design.
For the sake transparency, the current dependencies are:
sha1 serde - Optional specta - Optional specta-util - Optional A special thanks to P1sec for digging through the Hermes git repo, pulling all of the BytecodeDef files out, and tagging them. This made writing this tool much easier.
I created and launched Bytecode Studio - a comprehensive tool built for reverse engineering, analyzing, disassembling, and decompiling React Native applications and Hermes binaries.
If you're using hermes_rs or any other tool for static analysis for an engagement or want a more comprehensive analysis and workflow, I'd use Bytecode Studio!
| HBC Version | Disassembler | (Binary) Assembler | (Textual) Assembler | Decompiler |
|---|---|---|---|---|
| 76 | ✅ | ✅ | ❌ | ❌ |
| 89 | ✅ | ✅ | ❌ | ❌ |
| 90 | ✅ | ✅ | ❌ | ❌ |
| 93 | ✅ | ✅ | ❌ | ❌ |
| 94 | ✅ | ✅ | ❌ | ❌ |
| 95 | ✅ | ✅ | ❌ | ❌ |
| 96 | ✅ | ✅ | ❌ | ❌ |
A couple of features are missing currently, as they're low priority for me at the moment.
* Supports u8 buffer for manual population
Chances are you need to compile the program that uses this crate with the --release flag and run the binary directly.
The size of a binary produced with hermes_rs is going to be a bit larger than one compiled directly with hermes. This is primarily due to the fact that hermes does some string merging to keep bundles down.
Example: If car and racecar are both strings within the app, only racecar will be written. Two StringTableEntry objects are created: Entry { offset: 0, length: 7 } and Entry { offset: 4, length: 3 }.
As of right now, hermes_rs does not implement this functionality. It's pretty literal when it comes to strings, and doesn't try to be clever or save space.
cargo run --bin bytecode index.android.bundle cargo run --bin strings index.android.bundle To add hermes_rs to your project, simply run:
cargo add hermes_rs
["v89","v90","v93","v94","v95", "v96"] ["v94","v95", "v96"] serde feature specta feature cargo run --bin gen_ts --features specta will output *.d.ts to ./ts. specta. Dumps the entire applications bytecode - definitely direct it to a file since it's probably going to be huge.
cargo run --bin bytecode ./path/to/file/index.android.bundle > bytecode_ouput.txt
Dumps all string definitions
cargo run --bin strings ./path/to/file/index.android.bundle > strings_output.txt
Most React Native applications use the Metro bundler as a pre-Hermes compilation step. This utility finds the bundler function and where it's used, then outputs JavaScript-like representation of calls to it.
cargo run --bin modules ./path/to/file/index.android.bundle > output.txt
Example output:
// Function "$FUNC_73" being registered as a Metro module with a moduleId of 0
__d(73, 0, [1, 2, 5, 392, 2483, 1927, 1783, 698, 1348, 525, 2234] /* Arr IDX: 0 */);
// Function "$FUNC_79" being registered as a Metro module with a moduleId of 1
__d(79, 1, [...new Array(0)] /* Arr IDX: 0 */);
// Function "$FUNC_81" being registered as a Metro module with a moduleId of 2
__d(81, 2, [3] /* Arr IDX: 45 */);
// Function "$FUNC_82" being registered as a Metro module with a moduleId of 3
__d(82, 3, [4] /* Arr IDX: 50 */);
// Function "$FUNC_133" being registered as a Metro module with a moduleId of 4
__d(133, 4, [...new Array(0)] /* Arr IDX: 0 */);
// Function "$FUNC_137" being registered as a Metro module with a moduleId of 5
__d(137, 5, [6, 7, 8, 52, 191, 291, 292, 247, 277, 297, 298, 300, 301, 303, 310, 315, 316, 188, 319, 253, 320, 256, 287, 322, 323, 293, 325, 192, 328, 200, 331, 207, 212, 330, 83, 249, 288, 332, 143, 213, 334, 337, 351, 354, 347, 357, 359, 361, 362, 60, 242, 86, 308, 364, 223, 261, 262, 366, 369, 144, 126, 125, 370, 372, 59, 374, 376, 377, 379, 58, 44, 380, 29, 24, 382, 385, 386, 387, 389, 9, 390, 153, 30, 48, 67, 77, 164, 307, 64, 197, 391, 281] /* Arr IDX: 55 */);
Parses and dumps the contents of the Array storage buffer within the bundle in a JavaScript-like representation.
cargo run --bin arrays ./path/to/file/index.android.bundle > output.txt
Example Output:
const arr_53614 = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"];
const arr_53621 = ["iPhone", "iPad", "iPod"];
const arr_53630 = ["Win32", "Win64", "Windows", "WinCE"];
const arr_53632 = ["get"];
const arr_53637 = ["head", "options"];
const arr_53640 = ["put"];
const arr_53642 = ["delete"];
const arr_53659 = ["ETIMEDOUT", "ECONNRESET", "EADDRINUSE", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EPIPE", "EHOSTUNREACH", "EAI_AGAIN"];
const arr_53719 = ["ENOTFOUND", "ENETUNREACH", "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID", "CERT_HAS_EXPIRED", "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED", "ERROR_IN_CERT_NOT_BEFORE_FIELD", "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "CERT_CHAIN_TOO_LONG", "CERT_REVOKED", "INVALID_CA", "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE", "CERT_UNTRUSTED", "CERT_REJECTED"];
const arr_53728 = ["log", "debug", "info", "warn"];
const arr_53730 = ["error"];
const arr_53732 = ["type"];
Dump the raw object keys and values from an application bundle.
Note: The object keys and values aren't joined with this utility,
as they're correlated by individual indexes with the NewObjectWithBuffer or NewObjectWithBufferLong instructions.
To see proper Object key/value pairs, use the Dump Bytecode utility. When you disassemble the binary
with this utility, it'll print out the full object next to the two instructions above in the output.
cargo run --bin objects ./path/to/file/index.android.bundle > output.txt
Example Output:
const objkeys_7 = ["trace", "info", "warn"];
const objkeys_9 = ["error"];
const objkeys_12 = ["value", "enumerable"];
const objkeys_16 = ["message", "title", "color"];
const objkeys_19 = ["channelId"];
const objkeys_22 = ["value", "enumerable"];
const objkeys_27 = ["configurable", "writable"];
....
const objvals_17 = [0, 1, 2, 3];
const objvals_18 = [true];
const objvals_19 = [false];
const objvals_24 = ["You have new messages", "Kustomer Support"];
const objvals_25 = [null];
Special thanks to AbhiTheModder for this one!
Generate a r2 script for a hermes binary to take advantage of all of the amazing features r2 has to offer.
cargo run --bin r2_script ./path/to/file/index.android.bundle > my_script.r2
Example Output:
# R2 script for Hermes bytecode file: ./index.android.bundle
# Generated by hermes_rs
# Hermes File Version: 84
# File Size: 5471568 bytes
# Function Count: 27835
f hbc.header=0x0
f hbc.header.size=0x537d50
# Function flags
'f func.hermes.global 32 0x1cb368
'f func.hermes.func_1 16 0x1dd8b4
'f func.hermes.clear 16 0x1dd9b1
'f func.hermes.define 16 0x1dd9ce
'f func.hermes.metroRequire 16 0x1dda2b
... <snip>
# String flags
'f str.API_STATUS_0 10 0x28
'f str.ActionCreator_1 13 0x17a
'f str.Actions_2 7 0x1c9
'f str.Animated_3 8 0x2ac
'f str.d_4 1 0x2b3
... <snip>
let filename = "./input_data/index.android.bundle";
let f = File::open(filename).expect("no file found");
let mut reader = io::BufReader::new(f);
let mut hermes_file = HermesFile::deserialize(&mut reader);
println!("{:?}", hermes_file.header);
Output:
HermesHeader {
magic: 2240826417119764422,
version: 94,
sha1: [20, 178, 139, 133, 105, 198, 134, 29, 58, 101, 194, 248, 210, 173, 84, 79, 162, 174, 43, 205],
file_length: 11059884,
global_code_index: 0,
function_count: 54483,
string_kind_count: 3,
identifier_count: 35878,
string_count: 65091,
overflow_string_count: 425,
string_storage_size: 2238216,
big_int_count: 0,
big_int_storage_size: 0,
reg_exp_count: 448,
reg_exp_storage_size: 49719,
array_buffer_size: 132510,
obj_key_buffer_size: 43517,
obj_value_buffer_size: 137207,
segment_id: 0,
cjs_module_count: 0,
cjs_module_offset: 0,
function_source_count: 1361,
debug_info_offset: 11059836,
options: BytecodeOptions {
static_builtins: false,
cjs_modules_statically_resolved: false,
has_async: false,
flags: false
},
}
println!("Strings: {:?}", hermes_file.get_strings());
Output:
Strings: ["$$typeof", "type", "API", "isArray", "Array", ... ]
```rust for func in hermes_file.function_headers { println!("{:?}", func); }
// Prints the following: Small(SmallFunctionHeader { offset: 252641, param_count: 3, byte_size: 63, func_name: 5168, info_offset: 842244, frame_size: 16, env_size: 0, highest_read_cache_index: 1, highest_write_cache_index: 0, flags: FunctionHeaderFlag { prohibit_invoke: ProhibitNone, strict_mode: false, has_exception_handler: false, has_debug_info: false, overflowed: false }, exception_handlers: [], debug_info: None }) Small(LargeFunctionHeader { offset: 252704, param_count: 2, byte_size: 41, func_name: 3756, info_offset: 842244, frame_size: 14, env_size: 0, highest_read_cache_index: 1, highest_write_cache_index: 0, flags: FunctionHeaderFlag { prohibit_invoke:
$ claude mcp add hermes_rs \
-- python -m otcore.mcp_server <graph>