Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Linux

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

jobs:
build-and-test:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake \
g++ \
qt6-base-dev \
libgl1-mesa-dev \
libcapstone-dev

- name: Configure
run: cmake -S src -B build -DCMAKE_BUILD_TYPE=Release

- name: Build
run: cmake --build build -j"$(nproc)"

- name: Regression suite
run: tests/regression/run_all.sh
13 changes: 12 additions & 1 deletion DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ This document is for contributors and maintainers.

## Prerequisites
- CMake >= 3.16
- Qt 6 (recommended)
- Qt 6 (recommended), components: Core, Gui, Widgets, Network, Concurrent
- C++14 compiler
- Optional: Capstone (`libcapstone-dev`) for `__TEXT,__text` disassembly
- macOS release tooling: `macdeployqt`, `hdiutil`, `gh`

## Build
Expand All @@ -17,6 +18,16 @@ cmake --build build -j8
./build/MachOExplorer
```

### Linux
```bash
# Debian/Ubuntu deps: sudo apt-get install qt6-base-dev libgl1-mesa-dev
# (optional disassembly: libcapstone-dev)
./build_linux.sh # or: cmake -S src -B build && cmake --build build -j$(nproc)
./build/MachOExplorer
```
On Linux the binary is `build/MachOExplorer` (no `.app` bundle). The CLI mode
(`--cli`) runs without a display, so it works in headless/CI environments.

### Windows
```powershell
cmake -S src -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvcXXXX_64"
Expand Down
27 changes: 27 additions & 0 deletions build_linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail

# Build MachOExplorer on Linux.
#
# Requirements:
# - CMake >= 3.16
# - Qt 6 base + concurrent (e.g. Debian/Ubuntu: qt6-base-dev)
# - A C++14 compiler (g++ or clang++)
# - Optional: libcapstone-dev for __TEXT,__text disassembly
#
# Usage:
# ./build_linux.sh [build_type]
# where build_type defaults to Release.

ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="${ROOT_DIR}/build"
BUILD_TYPE="${1:-Release}"

cmake -S "${ROOT_DIR}/src" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}"
cmake --build "${BUILD_DIR}" -j"$(nproc)"

echo "---------"
echo "built: ${BUILD_DIR}/MachOExplorer"
echo "run: ${BUILD_DIR}/MachOExplorer"
echo "cli: ${BUILD_DIR}/MachOExplorer --cli <file>"
echo "---------"
5 changes: 3 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ if(WIN32)
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
endif()

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network Concurrent)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network Concurrent)

find_path(CAPSTONE_INCLUDE_DIR capstone/capstone.h)
find_library(CAPSTONE_LIBRARY capstone)
Expand Down Expand Up @@ -57,6 +57,7 @@ target_link_libraries(MachOExplorer PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Concurrent
)

if(APPLE)
Expand Down
7 changes: 7 additions & 0 deletions src/libmoex/node/FatHeader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ void FatArch::Init(void *offset, NodeContextPtr &ctx) {
if (object_size < sizeof(uint32_t)) {
throw NodeException("Malformed Fat Mach-O: arch payload too small");
}
// Slices are required to be at least pointer-aligned; real fat binaries
// page-align them. Reject under-aligned offsets so the embedded Mach-O
// header and its load commands are not dereferenced at misaligned
// addresses (undefined behaviour).
if (object_offset % 8 != 0) {
throw NodeException("Malformed Fat Mach-O: arch offset is misaligned");
}

void *mach_offset = reinterpret_cast<char *>(ctx_->file_start) + object_offset;
mh_ = std::make_shared<MachHeader>();
Expand Down
16 changes: 12 additions & 4 deletions src/libmoex/node/MachHeader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,25 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) {
throw NodeException("Malformed Mach-O: load commands exceed file size");
}

const uint32_t cmd_align = is64_ ? 8 : 4;
qv_load_command *first_cmd = reinterpret_cast<qv_load_command*>((char*)offset + cur_datasize);
qv_load_command *cur_cmd = first_cmd;
uint64_t parsed_size = 0;
for(uint32_t index = 0; index < cmd_count; ++index){
if (parsed_size + sizeof(qv_load_command) > sizeofcmds) {
throw NodeException("Malformed Mach-O: truncated load command");
}
if (cur_cmd->cmdsize < sizeof(qv_load_command)) {
// Load commands may sit at a misaligned address in a crafted file, so
// read the header through an aligned copy instead of dereferencing.
qv_load_command lc_head{};
memcpy(&lc_head, cur_cmd, sizeof(qv_load_command));
if (lc_head.cmdsize < sizeof(qv_load_command)) {
throw NodeException("Malformed Mach-O: invalid load command size");
}
if (parsed_size + cur_cmd->cmdsize > sizeofcmds) {
if (lc_head.cmdsize % cmd_align != 0) {
throw NodeException("Malformed Mach-O: misaligned load command size");
}
if (parsed_size + lc_head.cmdsize > sizeofcmds) {
throw NodeException("Malformed Mach-O: load command size overflow");
}

Expand All @@ -83,8 +91,8 @@ void MachHeader::Parse(void *offset,NodeContextPtr& ctx) {
loadcmds_.push_back(cmd);

// next
parsed_size += cur_cmd->cmdsize;
cur_cmd = reinterpret_cast<qv_load_command*>((char*)cur_cmd + cur_cmd->cmdsize);
parsed_size += lc_head.cmdsize;
cur_cmd = reinterpret_cast<qv_load_command*>((char*)cur_cmd + lc_head.cmdsize);
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/libmoex/node/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class NodeException : public std::exception{
#ifdef __APPLE__
const char* what() const _NOEXCEPT override { return error_.c_str();}
#else
const char* what() const override { return error_.c_str();}
const char* what() const noexcept override { return error_.c_str();}
#endif
};

Expand All @@ -34,6 +34,16 @@ struct NodeContext{
};
using NodeContextPtr = std::shared_ptr<NodeContext>;

// True when [addr, addr+size) lies entirely within the mapped file.
static inline bool NodeInFile(const NodeContextPtr &ctx, const void *addr, std::size_t size){
if(!ctx || ctx->file_start == nullptr) return false;
const char *p = static_cast<const char*>(addr);
const char *start = static_cast<const char*>(ctx->file_start);
const char *end = start + ctx->file_size;
if(p < start || p > end) return false;
return size <= static_cast<std::size_t>(end - p);
}

// Base class for each MachO element
class Node{
public:
Expand Down Expand Up @@ -88,6 +98,9 @@ class NodeData : public NodeOffset<T>{
// Init function which should be called in every child class's Init function
void Init(void *offset,NodeContextPtr & ctx){
NodeOffset<T>::Init(offset,ctx);
if(!NodeInFile(ctx, offset, NodeOffset<T>::DATA_SIZE())){
throw NodeException("Malformed file: struct read out of bounds");
}
memcpy(&data_,offset,NodeOffset<T>::DATA_SIZE());
}
};
Expand Down
28 changes: 21 additions & 7 deletions src/libmoex/node/Util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -421,20 +421,27 @@ namespace util {
}

// return next offset
const char * readUnsignedLeb128(const char *cur_offset,uint64_t & data,uint32_t & occupy_size) {
const char * readUnsignedLeb128(const char *cur_offset,const char *end,uint64_t & data,uint32_t & occupy_size) {
const uint8_t* p = (const uint8_t*)cur_offset;
const uint8_t* pend = (const uint8_t*)end;

uint64_t result = 0;
int bit = 0;

do {
if (p >= pend){
// truncated / out of bounds
data = 0;
occupy_size = 0;
return nullptr;
}
uint64_t slice = *p & 0x7f;

if (bit >= 64 || slice << bit >> bit != slice){
// error
data = 0;
occupy_size = 0;
return 0;
return nullptr;
} else {
result |= (slice << bit);
bit += 7;
Expand All @@ -443,30 +450,37 @@ namespace util {
while (*p++ & 0x80);

data = result;
occupy_size = p - (const uint8_t*)cur_offset;
occupy_size = (uint32_t)(p - (const uint8_t*)cur_offset);
return (const char *)p;
}
const char * readSignedLeb128(const char *cur_offset,int64_t & data,uint32_t & occupy_size){
const char * readSignedLeb128(const char *cur_offset,const char *end,int64_t & data,uint32_t & occupy_size){
const uint8_t* p = (const uint8_t*)cur_offset;
const uint8_t* pend = (const uint8_t*)end;

int64_t result = 0;
int bit = 0;
uint8_t byte=0;

do {
if (p >= pend || bit >= 64){
// truncated / out of bounds
data = 0;
occupy_size = 0;
return nullptr;
}
byte = *p++;
result |= ((byte & 0x7f) << bit);
result |= ((int64_t)(byte & 0x7f) << bit);
bit += 7;
} while (byte & 0x80);

// sign extend negative numbers
if ( (byte & 0x40) != 0 )
if ( (byte & 0x40) != 0 && bit < 64 )
{
result |= (-1LL) << bit;
}

data = result;
occupy_size = p - (const uint8_t*)cur_offset;
occupy_size = (uint32_t)(p - (const uint8_t*)cur_offset);
return (const char *)p;
}

Expand Down
7 changes: 5 additions & 2 deletions src/libmoex/node/Util.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ std::vector<std::tuple<qv_vm_prot_t,std::string>> ParseProts(qv_vm_prot_t prot);
std::string FormatTimeStamp(uint32_t timestamp);
std::string FormatVersion(uint32_t ver);

const char * readUnsignedLeb128(const char *cur_offset,uint64_t & data,uint32_t & occupy_size);
const char * readSignedLeb128(const char *cur_offset,int64_t & data,uint32_t & occupy_size);
// Decode (un)signed LEB128. `end` is one-past-the-last readable byte; if the
// encoding would read at/past it (truncated/malformed input) the functions
// return nullptr with data=0 and occupy_size=0 instead of reading out of bounds.
const char * readUnsignedLeb128(const char *cur_offset,const char *end,uint64_t & data,uint32_t & occupy_size);
const char * readSignedLeb128(const char *cur_offset,const char *end,int64_t & data,uint32_t & occupy_size);

std::vector<char*> ParseStringLiteral(char * offset,uint32_t size);

Expand Down
Loading
Loading