librz/bin: add CaRT container format support (#5964)

* Add CaRT extractor plugin
* Update cmd_list expected output for CaRT extractor plugin

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ayush 2026-03-07 18:21:50 +05:30 committed by GitHub
parent 7bac9f75fa
commit 5fab5584e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 442 additions and 1 deletions

View file

@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2025 Ayush Dwivedi <ayushd785@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include "cart.h"
#include <rz_crypto/rz_rc4.h>
#include <rz_util.h>
/**
* Default RC4 key used by CaRT: first 8 digits of PI repeated twice.
* Reference: https://github.com/CybercentreCanada/cart/blob/master/cart/cart.py#L34
*/
static const ut8 CART_DEFAULT_RC4_KEY[CART_ARC4_KEY_LENGTH] = {
0x03, 0x01, 0x04, 0x01, 0x05, 0x09, 0x02, 0x06,
0x03, 0x01, 0x04, 0x01, 0x05, 0x09, 0x02, 0x06
};
static bool cart_key_is_null(const ut8 *key) {
for (int i = 0; i < CART_ARC4_KEY_LENGTH; i++) {
if (key[i] != 0) {
return false;
}
}
return true;
}
RZ_API bool rz_bin_cart_check_buffer(RZ_NONNULL RzBuffer *buf) {
rz_return_val_if_fail(buf, false);
ut64 sz = rz_buf_size(buf);
if (sz < CART_MINIMUM_LENGTH) {
return false;
}
ut8 header_magic[CART_HEADER_MAGIC_SIZE];
if (rz_buf_read_at(buf, 0, header_magic, CART_HEADER_MAGIC_SIZE) != CART_HEADER_MAGIC_SIZE) {
return false;
}
if (memcmp(header_magic, CART_HEADER_MAGIC, CART_HEADER_MAGIC_SIZE) != 0) {
return false;
}
ut8 footer_magic[CART_FOOTER_MAGIC_SIZE];
ut64 footer_start = sz - CART_FOOTER_LENGTH;
if (rz_buf_read_at(buf, footer_start, footer_magic, CART_FOOTER_MAGIC_SIZE) != CART_FOOTER_MAGIC_SIZE) {
return false;
}
if (memcmp(footer_magic, CART_FOOTER_MAGIC, CART_FOOTER_MAGIC_SIZE) != 0) {
return false;
}
return true;
}
RZ_API RZ_OWN CartObj *rz_bin_cart_new_from_buffer(RZ_NONNULL RzBuffer *buf) {
rz_return_val_if_fail(buf, NULL);
if (!rz_bin_cart_check_buffer(buf)) {
return NULL;
}
ut64 sz = rz_buf_size(buf);
CartObj *obj = RZ_NEW0(CartObj);
if (!obj) {
return NULL;
}
obj->file_size = sz;
ut64 off = 0;
if (!rz_buf_read_offset(buf, &off, (ut8 *)obj->header.magic, CART_HEADER_MAGIC_SIZE) ||
!rz_buf_read_le16_offset(buf, &off, &obj->header.version) ||
!rz_buf_read_le64_offset(buf, &off, &obj->header.reserved) ||
!rz_buf_read_offset(buf, &off, obj->header.arc4_key, CART_ARC4_KEY_LENGTH) ||
!rz_buf_read_le64_offset(buf, &off, &obj->header.opt_header_len)) {
goto fail;
}
if (obj->header.version != CART_VERSION) {
RZ_LOG_WARN("CaRT: Unsupported version %d (expected %d)\n", obj->header.version, CART_VERSION);
goto fail;
}
if (obj->header.reserved != 0) {
RZ_LOG_WARN("CaRT: Invalid header reserved value (expected 0)\n");
goto fail;
}
ut64 footer_start = sz - CART_FOOTER_LENGTH;
off = footer_start;
if (!rz_buf_read_offset(buf, &off, (ut8 *)obj->footer.magic, CART_FOOTER_MAGIC_SIZE) ||
!rz_buf_read_le64_offset(buf, &off, &obj->footer.reserved) ||
!rz_buf_read_le64_offset(buf, &off, &obj->footer.opt_footer_pos) ||
!rz_buf_read_le64_offset(buf, &off, &obj->footer.opt_footer_len)) {
goto fail;
}
if (obj->footer.reserved != 0) {
RZ_LOG_WARN("CaRT: Invalid footer reserved value (expected 0)\n");
goto fail;
}
if (obj->footer.opt_footer_pos + obj->footer.opt_footer_len != footer_start) {
RZ_LOG_WARN("CaRT: Invalid footer position/length\n");
goto fail;
}
obj->data_start = CART_HEADER_LENGTH + obj->header.opt_header_len;
obj->data_len = obj->footer.opt_footer_pos - obj->data_start;
if (obj->data_start > sz || obj->data_start + obj->data_len > sz) {
RZ_LOG_WARN("CaRT: Invalid data block boundaries\n");
goto fail;
}
return obj;
fail:
free(obj);
return NULL;
}
RZ_API void rz_bin_cart_free(RZ_NULLABLE CartObj *obj) {
free(obj);
}
RZ_API RZ_OWN ut8 *rz_bin_cart_extract(RZ_NONNULL RzBuffer *buf, RZ_NONNULL CartObj *obj, RZ_NONNULL int *out_size) {
rz_return_val_if_fail(buf && obj && out_size, NULL);
*out_size = 0;
if (obj->data_len == 0) {
RZ_LOG_WARN("CaRT: Empty data block\n");
return NULL;
}
if (obj->data_len > 0x10000000) {
RZ_LOG_WARN("CaRT: Data block too large\n");
return NULL;
}
ut8 *encrypted = malloc(obj->data_len);
if (!encrypted) {
return NULL;
}
if (rz_buf_read_at(buf, obj->data_start, encrypted, obj->data_len) != obj->data_len) {
free(encrypted);
return NULL;
}
const ut8 *key = obj->header.arc4_key;
if (cart_key_is_null(key)) {
key = CART_DEFAULT_RC4_KEY;
}
ut8 *decrypted = malloc(obj->data_len);
if (!decrypted) {
free(encrypted);
return NULL;
}
rz_rc4_crypt(key, CART_ARC4_KEY_LENGTH, encrypted, decrypted, obj->data_len);
free(encrypted);
int decompressed_len = 0;
ut8 *decompressed = rz_inflate(decrypted, obj->data_len, NULL, &decompressed_len);
free(decrypted);
if (!decompressed) {
RZ_LOG_WARN("CaRT: Failed to decompress payload\n");
return NULL;
}
*out_size = decompressed_len;
return decompressed;
}
RZ_API RZ_OWN RzBuffer *rz_bin_cart_extract_buf(RZ_NONNULL RzBuffer *buf, RZ_NONNULL CartObj *obj) {
rz_return_val_if_fail(buf && obj, NULL);
int payload_size = 0;
ut8 *payload = rz_bin_cart_extract(buf, obj, &payload_size);
if (!payload) {
return NULL;
}
RzBuffer *result = rz_buf_new_with_pointers(payload, payload_size, true);
if (!result) {
free(payload);
return NULL;
}
return result;
}

View file

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2025 Ayush Dwivedi <ayushd785@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#ifndef RZ_BIN_CART_H
#define RZ_BIN_CART_H
#include <rz_types.h>
#include <rz_util.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CART_HEADER_MAGIC "CART"
#define CART_FOOTER_MAGIC "TRAC"
#define CART_HEADER_MAGIC_SIZE 4
#define CART_FOOTER_MAGIC_SIZE 4
#define CART_VERSION 1
#define CART_HEADER_LENGTH 38
#define CART_FOOTER_LENGTH 28
#define CART_MINIMUM_LENGTH (CART_HEADER_LENGTH + CART_FOOTER_LENGTH)
#define CART_ARC4_KEY_LENGTH 16
#define CART_BLOCK_SIZE (64 * 1024)
typedef struct cart_header_t {
char magic[CART_HEADER_MAGIC_SIZE];
ut16 version;
ut64 reserved;
ut8 arc4_key[CART_ARC4_KEY_LENGTH];
ut64 opt_header_len;
} CartHeader;
typedef struct cart_footer_t {
char magic[CART_FOOTER_MAGIC_SIZE];
ut64 reserved;
ut64 opt_footer_pos;
ut64 opt_footer_len;
} CartFooter;
typedef struct cart_obj_t {
CartHeader header;
CartFooter footer;
ut64 data_start;
ut64 data_len;
ut64 file_size;
} CartObj;
/** Check if buffer contains valid CaRT magic (CART/TRAC) */
RZ_API bool rz_bin_cart_check_buffer(RZ_NONNULL RzBuffer *buf);
/** Parse CaRT header and footer from buffer */
RZ_API RZ_OWN CartObj *rz_bin_cart_new_from_buffer(RZ_NONNULL RzBuffer *buf);
/** Free CartObj */
RZ_API void rz_bin_cart_free(RZ_NULLABLE CartObj *obj);
/** Extract decrypted and decompressed payload */
RZ_API RZ_OWN ut8 *rz_bin_cart_extract(RZ_NONNULL RzBuffer *buf, RZ_NONNULL CartObj *obj, RZ_NONNULL int *out_size);
/** Extract payload as RzBuffer */
RZ_API RZ_OWN RzBuffer *rz_bin_cart_extract_buf(RZ_NONNULL RzBuffer *buf, RZ_NONNULL CartObj *obj);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -60,6 +60,7 @@ bin_plugins_list = [
]
bin_xtr_plugins_list = [
'cart',
'fatmach0',
'sep64',
# this was never built.
@ -157,6 +158,7 @@ rz_bin_sources = [
'p/bin_wasm.c',
'p/bin_xbe.c',
'p/bin_xnu_kernelcache.c',
'p/bin_xtr_cart.c',
'p/bin_xtr_fatmach0.c',
# this was never built.
# 'p/bin_xtr_pemixed.c',
@ -276,6 +278,7 @@ rz_bin_sources = [
'format/pyc/marshal.c',
'format/pyc/pyc.c',
'format/pyc/pyc_magic.c',
'format/cart/cart.c',
'format/te/te.c',
'format/wasm/wasm.c',
@ -328,6 +331,7 @@ rz_bin = library('rz_bin', rz_bin_sources,
rz_search_dep,
rz_cons_dep,
rz_io_dep,
rz_crypto_dep,
rz_type_dep,
libmspack_dep,
],

110
librz/bin/p/bin_xtr_cart.c Normal file
View file

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 Ayush Dwivedi <ayushd785@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_types.h>
#include <rz_util.h>
#include <rz_lib.h>
#include <rz_bin.h>
#include "../format/cart/cart.h"
static bool check_buffer(RzBuffer *buf) {
rz_return_val_if_fail(buf, false);
return rz_bin_cart_check_buffer(buf);
}
static void free_xtr(void *xtr_obj) {
rz_bin_cart_free((CartObj *)xtr_obj);
}
static void destroy(RzBin *bin) {
if (bin && bin->cur) {
free_xtr(bin->cur->xtr_obj);
bin->cur->xtr_obj = NULL;
}
}
static bool load(RzBin *bin) {
rz_return_val_if_fail(bin && bin->cur, false);
bin->cur->xtr_obj = rz_bin_cart_new_from_buffer(bin->cur->buf);
return bin->cur->xtr_obj != NULL;
}
static int size(RzBin *bin) {
rz_return_val_if_fail(bin && bin->cur && bin->cur->xtr_obj, 0);
CartObj *obj = (CartObj *)bin->cur->xtr_obj;
return (int)obj->file_size;
}
static RzBinXtrData *oneshot_buffer(RzBin *bin, RzBuffer *b, int idx) {
rz_return_val_if_fail(bin && bin->cur && b, NULL);
if (idx != 0) {
return NULL;
}
if (!bin->cur->xtr_obj) {
bin->cur->xtr_obj = rz_bin_cart_new_from_buffer(b);
}
if (!bin->cur->xtr_obj) {
return NULL;
}
CartObj *obj = (CartObj *)bin->cur->xtr_obj;
RzBuffer *payload_buf = rz_bin_cart_extract_buf(b, obj);
if (!payload_buf) {
return NULL;
}
RzBinXtrMetadata *metadata = RZ_NEW0(RzBinXtrMetadata);
if (!metadata) {
rz_buf_free(payload_buf);
return NULL;
}
metadata->xtr_type = "cart";
metadata->libname = rz_str_dup("cart_payload");
ut64 payload_size = rz_buf_size(payload_buf);
RzBinXtrData *res = rz_bin_xtrdata_new(payload_buf, 0, payload_size, 1, metadata);
rz_buf_free(payload_buf);
return res;
}
static RzList /*<RzBinXtrData *>*/ *oneshotall_buffer(RzBin *bin, RzBuffer *b) {
RzBinXtrData *data = oneshot_buffer(bin, b, 0);
if (!data) {
return NULL;
}
RzList *res = rz_list_newf(rz_bin_xtrdata_free);
if (!res) {
rz_bin_xtrdata_free(data);
return NULL;
}
rz_list_append(res, data);
return res;
}
RzBinXtrPlugin rz_bin_xtr_plugin_cart = {
.name = "xtr.cart",
.desc = "CaRT (Compressed and RC4 Transport) extractor",
.license = "LGPL3",
.check_buffer = check_buffer,
.load = &load,
.destroy = &destroy,
.size = &size,
.extract_from_buffer = &oneshot_buffer,
.extractall_from_buffer = &oneshotall_buffer,
.free_xtr = &free_xtr,
};
#ifndef RZ_PLUGIN_INCORE
RZ_API RzLibStruct rizin_plugin = {
.type = RZ_LIB_TYPE_BIN_XTR,
.data = &rz_bin_xtr_plugin_cart,
.version = RZ_VERSION
};
#endif

View file

@ -3,6 +3,7 @@
#include <rz_lib.h>
#include <rz_crypto.h>
#include <rz_crypto/rz_rc4.h>
#include <rz_util.h>
struct rc4_state {
@ -71,6 +72,21 @@ static void rc4_crypt(struct rc4_state *const state, const ut8 *inbuf, ut8 *outb
}
}
/**
* \brief One-shot RC4 encrypt/decrypt.
*
* Initializes the RC4 state with the given key and processes the input buffer.
* Since RC4 is symmetric, the same function is used for both encryption and decryption.
*/
RZ_API void rz_rc4_crypt(RZ_NONNULL const ut8 *key, int keylen, RZ_NONNULL const ut8 *in, RZ_NONNULL ut8 *out, int len) {
rz_return_if_fail(key && in && out);
struct rc4_state state = { 0 };
if (!rc4_init_state(&state, key, keylen)) {
return;
}
rc4_crypt(&state, in, out, len);
}
///////////////////////////////////////////////////////////
static bool rc4_set_key(RzCrypto *cry, const ut8 *key, int keylen, int mode, int direction) {

View file

@ -167,6 +167,7 @@ install_headers(rz_il_files, install_dir: join_paths(rizin_incdir, 'rz_il'))
rz_crypto_files = [
'rz_crypto/rz_des.h',
'rz_crypto/rz_aes.h',
'rz_crypto/rz_rc4.h',
'rz_crypto/rz_sm4.h',
]
install_headers(rz_crypto_files, install_dir: join_paths(rizin_incdir, 'rz_crypto'))

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2025 Ayush Dwivedi <ayushd785@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#ifndef RZ_RC4_H
#define RZ_RC4_H
#include <rz_types.h>
#ifdef __cplusplus
extern "C" {
#endif
RZ_API void rz_rc4_crypt(RZ_NONNULL const ut8 *key, int keylen, RZ_NONNULL const ut8 *in, RZ_NONNULL ut8 *out, int len);
#ifdef __cplusplus
}
#endif
#endif // RZ_RC4_H

File diff suppressed because one or more lines are too long

25
test/db/formats/cart Normal file
View file

@ -0,0 +1,25 @@
NAME=CaRT: iI and extractor info
FILE=bins/cart/sample.cart
CMDS=<<EOF
iI~binsz
i~packet
EOF
EXPECT=<<EOF
binsz 0x00000039
packet xtr.cart
EOF
RUN
NAME=CaRT: Extract payload with rz-bin -x
FILE==
CMDS=<<EOF
!rm -rf sample.cart.fat
!rz-bin -x bins/cart/sample.cart
!cat sample.cart.fat/sample.cart.unknown.cart_payload_0.0
!rm -rf sample.cart.fat
EOF
EXPECT=<<EOF
sample.cart.fat/sample.cart.unknown.cart_payload_0.0 created (57)
This is a test payload for CaRT container format testing
EOF
RUN