feat: rveng M0-M2 reverse-engineering learning platform engine and API

Framework-free analysis engine (hex dump, hand-rolled ELF64 parser,
capstone x86-64 disassembly, string extraction, byte patching, challenge
grading) behind a thin read-only FastAPI layer over curated pre-compiled
challenge binaries.

No-execution security posture: the backend only reads and decodes bytes,
never runs a target; patch challenges are graded by static byte diff.
Reveal-after-solve pedagogy: challenge source is returned only on a
correct submission. Input size-caps enforced at the API boundary.

M0 research (ELF format, x86-64/capstone, pedagogy, no-execution) is
cross-checked against a real compiled binary. 63 tests green, including
an ELF parser cross-check against pyelftools and disassembly that matches
objdump instruction-for-instruction.
This commit is contained in:
CarterPerez-dev 2026-07-05 13:20:45 -04:00
parent 6593aa5689
commit 44f1b5bc99
44 changed files with 2740 additions and 0 deletions

10
PROJECTS/advanced/rveng/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# ©AngelaMos | 2026
# .gitignore
docs/plans/
docs/context/
__pycache__/
.pytest_cache/
.venv/
*.pyc

View File

@ -0,0 +1 @@
3.13

View File

View File

@ -0,0 +1,7 @@
{
"id": "03-flip-the-gate",
"module": "patching",
"title": "Flip the gate",
"mission": "The check at file offset 0x1154 is a conditional jump (jne, bytes 75 07) that skips the unlock path. Patch those two bytes so the branch is never taken and the unlock path always runs. Submit the two replacement bytes as hex.",
"answer": { "category": "patched_bytes", "offset": 4436, "patch": "9090" }
}

View File

@ -0,0 +1,23 @@
#include <stdio.h>
#include <stdlib.h>
int check(int n) {
if (n == 1337) {
return 1;
}
return 0;
}
int main(int argc, char **argv) {
char *secret = "the_flag_is_here";
int n = 0;
if (argc > 1) {
n = atoi(argv[1]);
}
if (check(n)) {
printf("unlocked: %s\n", secret);
} else {
printf("wrong number\n");
}
return 0;
}

Binary file not shown.

View File

@ -0,0 +1,7 @@
{
"id": "04-name-the-function",
"module": "strings-symbols",
"title": "Name the function",
"mission": "One function in this binary decides whether the gate opens. Read the symbol table and name it.",
"answer": { "category": "identified_symbol", "name": "check" }
}

View File

@ -0,0 +1,23 @@
#include <stdio.h>
#include <stdlib.h>
int check(int n) {
if (n == 1337) {
return 1;
}
return 0;
}
int main(int argc, char **argv) {
char *secret = "the_flag_is_here";
int n = 0;
if (argc > 1) {
n = atoi(argv[1]);
}
if (check(n)) {
printf("unlocked: %s\n", secret);
} else {
printf("wrong number\n");
}
return 0;
}

View File

@ -0,0 +1,7 @@
{
"id": "05-find-the-gate",
"module": "disassembly",
"title": "Find the gate",
"mission": "This binary checks a number against a magic value. Disassemble the check function and find the constant it compares against. Answer in decimal or hex.",
"answer": { "category": "found_value", "expected": 1337 }
}

View File

@ -0,0 +1,23 @@
#include <stdio.h>
#include <stdlib.h>
int check(int n) {
if (n == 1337) {
return 1;
}
return 0;
}
int main(int argc, char **argv) {
char *secret = "the_flag_is_here";
int n = 0;
if (argc > 1) {
n = atoi(argv[1]);
}
if (check(n)) {
printf("unlocked: %s\n", secret);
} else {
printf("wrong number\n");
}
return 0;
}

Binary file not shown.

View File

@ -0,0 +1,140 @@
<!-- ©AngelaMos | 2026 -->
<!-- 01-elf-format.md -->
# ELF Format Research
Primary source: the System V Application Binary Interface and the ELF-64
Object File Format specification. Every field size and offset below is
cross-checked against a real binary compiled locally.
## Ground-truth sample
```
gate.c compiled with: gcc -no-pie -fno-stack-protector -O0 -o gate gate.c
file: ELF 64-bit LSB executable, x86-64, EXEC, dynamically linked, not stripped
```
Chosen `-no-pie` so the load addresses are fixed and readable (EXEC type, not a
PIE/DYN shared object with load-time relocation). This is the shape the curated
challenge binaries use so a learner sees stable addresses.
## ELF header (Elf64_Ehdr)
`readelf -h gate` reported, and the first 64 raw bytes confirm, this layout.
Offsets are into the file from byte 0.
| Off | Size | Field | Value in sample | Raw bytes |
|-----|------|-------|-----------------|-----------|
| 0x00 | 16 | e_ident | magic + class + data + version | `7f 45 4c 46 02 01 01 00` then padding |
| 0x10 | 2 | e_type | EXEC (2) | `02 00` |
| 0x12 | 2 | e_machine | x86-64 (62) | `3e 00` |
| 0x14 | 4 | e_version | 1 | `01 00 00 00` |
| 0x18 | 8 | e_entry | 0x401060 | `60 10 40 00 00 00 00 00` |
| 0x20 | 8 | e_phoff | 64 | `40 00 00 00 00 00 00 00` |
| 0x28 | 8 | e_shoff | 13984 (0x36a0) | `a0 36 00 00 00 00 00 00` |
| 0x30 | 4 | e_flags | 0 | `00 00 00 00` |
| 0x34 | 2 | e_ehsize | 64 | `40 00` |
| 0x36 | 2 | e_phentsize | 56 | `38 00` |
| 0x38 | 2 | e_phnum | 14 | `0e 00` |
| 0x3a | 2 | e_shentsize | 64 | `40 00` |
| 0x3c | 2 | e_shnum | 30 | `1e 00` |
| 0x3e | 2 | e_shstrndx | 29 | `1d 00` |
e_ident breakdown (the first 16 bytes):
- bytes 0-3: magic `7f 45 4c 46` = `\x7f E L F`. Every ELF starts with these.
- byte 4 (EI_CLASS): `02` = ELFCLASS64 (64-bit). `01` would be 32-bit.
- byte 5 (EI_DATA): `01` = little-endian (ELFDATA2LSB).
- byte 6 (EI_VERSION): `01`.
- bytes 7-15: OS/ABI + padding, zero here.
The raw hex cross-check is exact: the entry point bytes at file offset 0x18 are
`60 10 40 00` little-endian = 0x401060, matching both `readelf -h` and the entry
point of `.text`. The section header table offset at 0x28 is `a0 36 00 00` =
0x36a0 = 13984, matching e_shoff.
## Section header table (Elf64_Shdr)
Located at e_shoff (0x36a0), e_shnum (30) entries of e_shentsize (64) bytes
each. Section names are indices into the section named by e_shstrndx (29,
`.shstrtab`). Sections relevant to the learning modules, from `readelf -S`:
| Nr | Name | Type | Address | Offset | Size | Flags |
|----|------|------|---------|--------|------|-------|
| 11 | .init | PROGBITS | 0x401000 | 0x1000 | 0x17 | AX |
| 12 | .plt | PROGBITS | 0x401020 | 0x1020 | 0x40 | AX |
| 13 | .text | PROGBITS | 0x401060 | 0x1060 | 0x182 | AX |
| 15 | .rodata | PROGBITS | 0x402000 | 0x2000 | 0x30 | A |
| 24 | .data | PROGBITS | 0x404018 | 0x3018 | 0x10 | WA |
| 25 | .bss | NOBITS | 0x404028 | 0x3028 | 0x8 | WA |
| 27 | .symtab | SYMTAB | 0 | 0x3048 | 0x378 | |
| 28 | .strtab | STRTAB | 0 | 0x33c0 | 0x1ca | |
Key facts the engine must model:
- Flags: A = allocated into memory, X = executable, W = writable. `.text` is
AX (code), `.rodata` is A (read-only data), `.data` is WA, `.bss` is WA.
- `.bss` type is NOBITS: it occupies memory at runtime but has zero size in the
file (Size 0x8 but no file bytes). The engine must not read file bytes for a
NOBITS section.
- A section with Address 0 is not loaded into the process image (`.symtab`,
`.strtab`, `.shstrtab`). These exist only in the file.
- The entry point 0x401060 equals `.text` Address, and the symbol table shows
`_start` at 0x401060. Entry is the first instruction the loader jumps to.
## Program header table (Elf64_Phdr)
Located at e_phoff (64), e_phnum (14) entries of e_phentsize (56) bytes. These
describe segments: how the file maps into memory at load time. From
`readelf -l`, the LOAD segments and their flags:
| Offset | VirtAddr | FileSiz | Flags | Holds |
|--------|----------|---------|-------|-------|
| 0x0000 | 0x400000 | 0x568 | R | ELF header + read-only metadata |
| 0x1000 | 0x401000 | 0x1ed | R E | .init .plt .text .fini (code) |
| 0x2000 | 0x402000 | 0x14c | R | .rodata + eh_frame |
| 0x2df8 | 0x403df8 | 0x230 | RW | .data .bss and relro |
Sections are the linker/analysis view. Segments are the loader view. The engine
parses sections for the learning modules (naming, layout) and can show segments
for the elf-anatomy module to explain how file bytes become a running process.
## Symbol table (Elf64_Sym)
`.symtab` at file offset 0x3048, entry size 24 (0x18). Names index into
`.strtab`. Sample function symbols from `readelf -s`:
| Value | Size | Type | Bind | Name |
|-------|------|------|------|------|
| 0x401060 | 34 | FUNC | GLOBAL | _start |
| 0x401146 | 30 | FUNC | GLOBAL | check |
| 0x401164 | 126 | FUNC | GLOBAL | main |
Each symbol carries value (address), size, a type (FUNC/OBJECT/etc.), a binding
(LOCAL/GLOBAL/WEAK), and a section index. `check` at 0x401146 with size 30 is
the target of the strings-symbols and disassembly modules. A stripped binary
has no `.symtab`, which is why later challenges can strip symbols to force
learners into raw disassembly.
## What we hand-roll vs delegate
Hand-roll (this is the ELF-format learning surface):
- e_ident + Elf64_Ehdr field parse from raw bytes (the table above).
- Section header table walk: read e_shoff/e_shnum/e_shentsize, iterate entries,
resolve names via e_shstrndx.
Delegate to pyelftools where hand-rolling adds no pedagogical value:
- Program header details, relocation tables, dynamic section, DWARF.
pyelftools 0.33 is the installed reference and is used to cross-check the
hand-rolled parser in the M1 known-answer tests: the hand-rolled header and
section fields must equal what pyelftools reports for the same binary.
## Facts an adversarial check confirmed or corrected
- Confirmed: entry point, e_shoff, e_shnum, and all e_ident bytes match the raw
file bytes exactly (not taken from memory or a generic ELF diagram).
- Confirmed: `.bss` is NOBITS with a nonzero memory size and no file content;
the parser must special-case it.
- Corrected assumption: an EXEC (non-PIE) binary has fixed addresses; a default
`gcc` build is PIE (ET_DYN) with load-relative addresses. The challenge
binaries are compiled `-no-pie` on purpose so addresses in `readelf`,
`objdump`, and the engine all agree with what a learner sees.

View File

@ -0,0 +1,127 @@
<!-- ©AngelaMos | 2026 -->
<!-- 02-x86-64-capstone.md -->
# x86-64 Disassembly and Capstone Research
Primary sources: the Intel 64 and IA-32 Architectures Software Developer
Manuals for instruction semantics, and the Capstone engine documentation for
API usage. Cross-checked against `objdump -d -M intel` on the local sample.
## Minimum x86-64 mental model for these challenges
A learner solving the five modules needs to recognize a small instruction set,
not the whole ISA. The instructions that actually appear in the challenge
binaries:
- `mov dst, src` : copy a value. `mov dword ptr [rbp-4], edi` stores the first
argument (edi) into a local stack slot.
- `cmp a, b` : compute `a - b` and set flags, discarding the result. This is
how a program tests a value. The compared constant is the thing a learner
hunts for.
- `test a, b` : bitwise AND setting flags, commonly `test eax, eax` to check
for zero.
- the jcc family (`je`, `jne`, `jg`, `jle`, ...) : conditional jump based on the
flags a preceding `cmp`/`test` set. `jne` after `cmp x, 0x539` means "branch
if x is not 1337".
- `jmp` : unconditional jump.
- `call` / `ret` : function call and return.
- `lea` : load effective address, often used to point at a string in `.rodata`.
- `push` / `pop` : stack, seen in function prologue/epilogue with `rbp`.
Registers that show up: `rax`/`eax` (return value and scratch), `rbp`/`rsp`
(frame and stack pointers), `edi`/`esi`/`edx` (first three integer arguments in
the System V AMD64 calling convention).
Intel syntax is used throughout (`mov dst, src`, destination first) rather than
AT&T (`mov src, dst`, `%` and `$` sigils) because it is easier for a beginner
and matches `objdump -d -M intel`.
## The gate pattern
The whole disassembly module rests on one recognizable shape: a `cmp` against a
constant followed by a conditional jump. In the sample, `check(int n)` compiles
to exactly this. From `objdump -d -M intel gate`:
```
401146 <check>:
40114d: 81 7d fc 39 05 00 00 cmp DWORD PTR [rbp-0x4],0x539
401154: 75 07 jne 40115d <check+0x17>
401156: b8 01 00 00 00 mov eax,0x1
40115b: eb 05 jmp 401162 <check+0x1c>
40115d: b8 00 00 00 00 mov eax,0x0
```
`0x539` = 1337. The C source compared `n == 1337`. A learner reads the `cmp`,
converts the hex, and has the magic number. The engine's job is to surface this
`cmp` clearly and annotate that the following `jne` is the gate.
## Capstone API
Installed reference: capstone 5.0.9 (Python binding).
Construction and iteration:
```python
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_OPT_SYNTAX_INTEL
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.syntax = CS_OPT_SYNTAX_INTEL # = 1; Intel is also the x86 default
for ins in md.disasm(code_bytes, start_vaddr):
ins.address # int, virtual address of the instruction
ins.mnemonic # str, e.g. "cmp"
ins.op_str # str, e.g. "dword ptr [rbp - 4], 0x539"
ins.bytes # bytes, the raw machine bytes of this instruction
ins.size # int, length in bytes
```
`md.disasm(code, addr)` yields instructions until it hits bytes it cannot
decode. For the engine, `code` is the `.text` bytes and `addr` is `.text`'s
virtual address, so `ins.address` lines up with `readelf`/`objdump` addresses.
## objdump vs capstone agreement (the M1 KAT)
Disassembling the exact bytes of `check()` at vaddr 0x401146 with capstone
(Intel syntax) produced, instruction for instruction:
```
0x401146: push rbp [55]
0x401147: mov rbp, rsp [4889e5]
0x40114a: mov dword ptr [rbp - 4], edi [897dfc]
0x40114d: cmp dword ptr [rbp - 4], 0x539 [817dfc39050000]
0x401154: jne 0x40115d [7507]
0x401156: mov eax, 1 [b801000000]
0x40115b: jmp 0x401162 [eb05]
0x40115d: mov eax, 0 [b800000000]
0x401162: pop rbp [5d]
0x401163: ret [c3]
```
Every address, mnemonic, operand, and byte string matches `objdump -d -M intel`.
The only differences are cosmetic formatting (capstone renders `0x539` where
older objdump may print the same value, and omits the `<check+0x17>` symbol
annotation that objdump adds from the symbol table). This byte-and-mnemonic
agreement is the known-answer test the M1 `disasm` module is verified against:
disassembling this fixed byte range must reproduce this instruction sequence.
## Annotation the engine adds
Capstone gives raw instructions. The engine adds a thin annotation layer for
teaching:
- mark `cmp`/`test` as comparisons and surface the immediate operand in both
hex and decimal (so `0x539` is shown as 1337 without the learner converting).
- mark jcc instructions as the conditional branch that acts on the preceding
comparison.
- resolve intra-function jump/call targets to a relative label
(`check+0x17`) using the function's start address, matching objdump's
readability, sourced from the symbol table when present.
## Facts an adversarial check confirmed or corrected
- Confirmed: capstone 5.0.9 with `md.syntax = 4` reproduces objdump Intel-syntax
disassembly byte-for-byte on the sample; the agreement is real, not assumed.
- Confirmed: `0x539` decodes to 1337 and originates from the source `n == 1337`.
- Corrected: `CS_OPT_SYNTAX_INTEL` is 1, not 4. The value 4 is
`CS_OPT_SYNTAX_MASM`, which happens to render these simple instructions
identically to Intel, so an early proof script using the literal 4 produced
correct-looking output by coincidence. Intel is already the x86 default in
capstone 5.0.9 (verified: default and explicit-Intel yield identical output,
AT&T differs). The engine sets `CS_OPT_SYNTAX_INTEL` by name, never a literal.

View File

@ -0,0 +1,85 @@
<!-- ©AngelaMos | 2026 -->
<!-- 03-pedagogy.md -->
# Reverse-Engineering Pedagogy Research
Source: the design of the predecessor rveng plus how the five modules actually
teach. The goal is a machine-checkable version of the same solve-then-reveal
loop, so the platform can grade a learner instead of just handing them tools.
## The solve-then-reveal loop
The predecessor's model, kept intact:
1. A module teaches one skill through a short concept note.
2. A challenge gives a compiled binary and a mission ("find the magic number").
3. The learner uses the tools (now in-browser) to solve it.
4. After solving, the original C source is revealed so the learner connects the
machine-level evidence back to the code that produced it.
Revealing source only after a correct answer is what makes the loop teach. The
learner must reach the answer from the binary, then gets to see they were right
and why. This requires the answer to be checkable before the reveal, which the
predecessor could not do (it just trusted the learner). The platform grades.
## Answer-spec categories (what the verifier must grade)
Every challenge needs a machine-checkable answer. Three categories cover all
five modules, each grounded in the sample `gate` binary:
- **found-value** : the learner locates a value in the binary and submits it.
Example: the magic number in `check()` is `0x539` = 1337, read from
`cmp DWORD PTR [rbp-0x4],0x539`. The verifier accepts the value in decimal or
hex, normalized before comparison. Also covers finding a string
(`the_flag_is_here` at .rodata 0x402004) or an offset.
- **identified-symbol** : the learner names a function, section, or symbol.
Example: "which function decides the outcome?" answer `check` (at 0x401146,
size 30 in `.symtab`). The verifier compares against the known symbol name,
case-insensitive.
- **patched-bytes** : the learner changes bytes to alter behavior and submits
the patched byte range. Example: flip the gate by changing the `jne` (opcode
`75`) at 0x401154 so the unlock path is always taken. The verifier diffs the
submission against a known-good patched target statically. It never runs the
result.
## Module to category mapping
| # | Module | Skill | Answer category |
|---|--------|-------|-----------------|
| 01 | hex-reading | read hex dumps, locate values | found-value |
| 02 | elf-anatomy | header, sections, entry point | found-value / identified-symbol |
| 03 | patching | change bytes, change behavior | patched-bytes |
| 04 | strings-symbols | find names and strings | found-value / identified-symbol |
| 05 | disassembly | read assembly, find the gate | found-value |
## What makes a good challenge per module
- hex-reading: a value or ASCII string is visible in the hex dump at a
findable offset. The learner practices reading offsets and the ASCII gutter.
- elf-anatomy: the answer is a header field or a section fact (entry point,
which section holds a string, how many sections). Teaches the file's skeleton.
- patching: a single conditional jump or comparison gates behavior; a
one-to-few-byte edit changes the outcome. Teaches that behavior is bytes.
- strings-symbols: a password or a function name is recoverable from `.rodata`
or `.symtab` without disassembly. Teaches the cheap wins before the hard work.
- disassembly: symbols may be stripped so the learner must read the `cmp`/jcc
gate directly. Teaches assembly as ground truth when names are gone.
## Grading rules
- found-value: normalize both sides to an integer when numeric (accept `0x539`,
`539h`, `1337`), or exact-match after trim/lowercase when a string.
- identified-symbol: exact match after trim/lowercase against the known name.
- patched-bytes: the submitted bytes, applied at the specified offset to the
original, must equal the known-good patched target under a static byte diff.
No execution, ever.
## Facts an adversarial check confirmed
- Confirmed against the real binary: the magic number is `0x539` (1337), the
deciding function is `check` at 0x401146, and the gate is the `jne` at
0x401154. These are the concrete answers the sample's challenges grade.
- The reveal-after-solve ordering is the load-bearing pedagogy: source is
withheld until a correct submission, which is only possible because every
answer is machine-checkable.

View File

@ -0,0 +1,87 @@
<!-- ©AngelaMos | 2026 -->
<!-- 04-no-execution-posture.md -->
# No-Execution Security Posture Research
The single load-bearing security decision for the platform: the backend never
executes any binary. This document enumerates every engine operation and
confirms each is read-only static analysis, and records why patch grading is a
static diff rather than a behavioral test.
## The threat that this design removes
A naive "web app that analyzes binaries" invites users to upload arbitrary
executables that the server then runs or analyzes with tools that themselves
execute code. That path requires sandboxing untrusted native code (containers,
seccomp, resource limits, escape mitigation) and is a large, permanent attack
surface. The platform sidesteps it entirely:
- Challenge binaries are curated and pre-compiled by the author and shipped as
static assets. Users do not upload binaries to run.
- Every operation the engine performs is reading and parsing bytes, not
executing them.
If nothing is ever executed, there is no arbitrary-code-execution surface, no
sandbox to escape, and no resource-exhaustion path through a hostile binary.
## Every engine operation is read-only
| Operation | What it does | Executes target? |
|-----------|--------------|------------------|
| hex dump | read bytes, format offset/hex/ascii | no |
| ELF header parse | read fixed-offset fields from bytes | no |
| section/segment walk | read the section/program header tables | no |
| symbol table read | read `.symtab`/`.strtab` bytes | no |
| string extraction | scan bytes for printable runs | no |
| disassembly | capstone decodes bytes to mnemonics | no |
| patch verify | static byte diff of two buffers | no |
Disassembly is decoding, not running: capstone reads instruction bytes and
returns their textual form. It never transfers control to the decoded code.
## Patch grading without execution
The patching module asks the learner to change bytes to alter behavior (for the
sample, flip the `jne` gate so the unlock path always runs). The tempting way to
grade this is to run the patched binary and check its output. That would mean
executing learner-influenced bytes on the server. Rejected.
Instead, each patch challenge ships a known-good patched target alongside the
original. Grading is: apply the learner's submitted bytes at the specified
offset to the original, then compare the result against the known-good patched
target with a static byte diff. Equal means correct. The patched binary is
never run. This grades the exact skill (produce the right byte edit) with zero
execution.
## Input constraints at the API boundary
These are responsibilities of the M2 route layer, not the M1 engine. The engine
grades totally (a malformed submission returns "not correct", never a crash),
but the size and range limits below are enforced where untrusted input enters,
at the API boundary, and are not yet present because the route layer does not
yet exist.
- Analysis routes accept a challenge id, never an arbitrary uploaded binary to
analyze or run. The binary analyzed is always a curated challenge asset.
- The submit route accepts an answer: a value, a name, or a patched byte range.
Submitted bytes are only ever diffed, never assembled into something executed.
- Byte inputs will be length-bounded at the route so a submission cannot be an
arbitrarily large blob. Until M2 lands this bound, the engine still never
executes the bytes; the open item is DoS size-capping, not code execution.
## The standing rule
Any future feature that would require executing a binary (running a challenge to
observe behavior, an interactive debugger, dynamic analysis) is out of scope by
default. It may be reconsidered only with a separate design that preserves this
posture, most likely by moving execution to an isolated, disposable sandbox that
is explicitly not the analysis backend. Until then: the backend reads bytes, it
does not run them.
## Facts an adversarial check confirmed
- Enumerated every engine operation; all seven are read-only byte processing.
- Confirmed capstone disassembly is pure decoding with no control transfer to
the target.
- Confirmed patch grading is achievable as a static diff against a known-good
target, so the one operation that "sounds like" it needs execution does not.

View File

@ -0,0 +1,38 @@
<!-- ©AngelaMos | 2026 -->
<!-- INDEX.md -->
# rveng Domain Research Archive
Reverse-engineering-platform research: the ELF file format, x86-64 disassembly
via capstone, the solve-then-reveal teaching model, and the no-execution
security posture. Every field size, offset, opcode, and disassembly line is
cross-checked against a real binary compiled locally (`gate`, built with
`gcc -no-pie -fno-stack-protector -O0`), not taken from memory or a generic
diagram. Each doc carries the VERIFIED fact where an adversarial check overruled
the first pass.
Honest grounding to keep across all of these: the engine reads bytes, it never
executes them. Challenge binaries are curated and pre-compiled; all analysis is
read-only static parsing and disassembly; patch challenges are graded by static
byte diff against a known-good target. This is what makes a web app that eats
binaries safe, and it is a hard constraint, not a preference.
| File | Read when |
|---|---|
| `01-elf-format.md` | implementing `engine/elf.py`: the Elf64 header field table with raw-byte cross-check, section/segment/symbol tables, NOBITS handling, what we hand-roll vs delegate to pyelftools, why the challenges are `-no-pie` |
| `02-x86-64-capstone.md` | implementing `engine/disasm.py`: the minimal x86-64 instruction set these challenges use, the cmp/jcc gate pattern, capstone 5.0.9 API, the objdump-vs-capstone byte-for-byte agreement KAT, the Intel-syntax constant correction |
| `03-pedagogy.md` | implementing `engine/challenge.py` and the runner: the solve-then-reveal loop, the three answer-spec categories (found-value, identified-symbol, patched-bytes), module-to-category mapping, grading rules |
| `04-no-execution-posture.md` | any API or engine work touching binaries: the enumeration proving every operation is read-only, why patch grading is a static diff, the API input constraints, the standing no-execution rule |
Key verified facts pulled forward:
- ELF header cross-checks exactly to raw bytes: entry `0x401060`, e_shoff
`0x36a0` (13984), e_shnum 30, e_ident `7f 45 4c 46 02 01 01`.
- The sample gate: `check` at `0x401146` (size 30), magic `cmp ...,0x539` = 1337
at `0x40114d`, gate `jne` at `0x401154`, secret `the_flag_is_here` at .rodata
`0x402004`.
- capstone 5.0.9 reproduces objdump Intel disassembly byte-for-byte;
`CS_OPT_SYNTAX_INTEL` is 1 (not 4, which is MASM), and Intel is already the
x86 default.
Build contract and full architecture: `../plans/rveng-design.md`.
Implementation plan: `../plans/rveng-implementation-plan.md`.

View File

@ -0,0 +1,25 @@
[project]
name = "rveng"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "CarterPerez-dev", email = "cartertechsolution@gmail.com" }
]
requires-python = ">=3.13"
dependencies = [
"capstone>=5.0.9",
"fastapi>=0.139.0",
"pyelftools>=0.33",
"uvicorn>=0.50.0",
]
[build-system]
requires = ["uv_build>=0.10.2,<0.11.0"]
build-backend = "uv_build"
[dependency-groups]
dev = [
"httpx>=0.28.1",
"pytest>=9.1.1",
]

View File

@ -0,0 +1,2 @@
def hello() -> str:
return "Hello from rveng!"

View File

@ -0,0 +1,4 @@
"""
©AngelaMos | 2026
__init__.py
"""

View File

@ -0,0 +1,164 @@
"""
©AngelaMos | 2026
app.py
"""
from pathlib import Path
from fastapi import FastAPI, HTTPException, Query
from rveng.api import schemas
from rveng.api.limits import (
DEFAULT_SESSION,
MAX_DISASM_BYTES,
MAX_HEX_BYTES,
)
from rveng.api.store import (
ChallengeStore,
InMemoryProgress,
ProgressStore,
load_store,
)
from rveng.engine import disasm, hex as hexmod, strings
from rveng.engine.challenge import Challenge, grade
from rveng.engine.elf import ElfImage, NotAnElf
CHALLENGES_ROOT = Path(__file__).resolve().parents[3] / "challenges"
def create_app(
store: ChallengeStore | None = None,
progress: ProgressStore | None = None) -> FastAPI:
"""
Build the rveng API over a challenge store and progress store
"""
store = store or load_store(CHALLENGES_ROOT)
progress = progress or InMemoryProgress()
app = FastAPI(title="rveng")
def require(cid: str) -> Challenge:
challenge = store.get(cid)
if challenge is None:
raise HTTPException(404, "challenge not found")
return challenge
def elf_of(challenge: Challenge) -> ElfImage:
try:
return ElfImage(challenge.binary)
except NotAnElf as exc:
raise HTTPException(422, f"not an ELF: {exc}")
@app.get("/api/challenges")
def list_challenges() -> list[schemas.ChallengeSummary]:
return [
schemas.ChallengeSummary(
id=c.id, module=c.module, title=c.title)
for c in store.list()
]
@app.get("/api/challenges/{cid}")
def get_challenge(cid: str) -> schemas.ChallengeDetail:
c = require(cid)
return schemas.ChallengeDetail(
id=c.id, module=c.module, title=c.title, mission=c.mission,
category=c.category, size=len(c.binary))
@app.get("/api/challenges/{cid}/hex")
def hex_view(
cid: str,
offset: int = Query(0, ge=0),
length: int = Query(256, ge=1)) -> schemas.HexView:
c = require(cid)
start = min(offset, len(c.binary))
take = min(length, MAX_HEX_BYTES)
chunk = c.binary[start:start + take]
return schemas.HexView(
base=start,
length=len(chunk),
lines=hexmod.hexdump(chunk, base=start).splitlines())
@app.get("/api/challenges/{cid}/elf")
def elf_view(cid: str) -> schemas.ElfView:
c = require(cid)
image = elf_of(c)
return schemas.ElfView(
type=image.header.e_type,
machine=image.header.e_machine,
entry=image.header.e_entry,
sections=[
schemas.SectionView(
index=s.index, name=s.name, type=s.type_name,
addr=s.addr, offset=s.offset, size=s.size,
flags=s.flag_str)
for s in image.sections
],
functions=[
schemas.FunctionView(
name=f.name, value=f.value, size=f.size)
for f in image.functions() if f.name
])
@app.get("/api/challenges/{cid}/disasm")
def disasm_view(
cid: str, symbol: str = Query(...)) -> schemas.DisasmView:
c = require(cid)
image = elf_of(c)
sym = image.symbol(symbol)
if sym is None:
raise HTTPException(404, "symbol not found")
try:
instructions = disasm.disassemble_symbol(image, sym)
except ValueError as exc:
raise HTTPException(422, str(exc))
if sum(i.size for i in instructions) > MAX_DISASM_BYTES:
raise HTTPException(413, "symbol too large to disassemble")
gate = disasm.find_gate(instructions)
gate_addr = gate.address if gate else None
return schemas.DisasmView(
symbol=symbol,
gate_address=gate_addr,
instructions=[
schemas.InstructionView(
address=i.address, mnemonic=i.mnemonic,
op_str=i.op_str, bytes=i.raw.hex(),
immediate=i.immediate,
branch_target=i.branch_target,
is_gate=(i.address == gate_addr))
for i in instructions
])
@app.get("/api/challenges/{cid}/strings")
def strings_view(
cid: str,
min_length: int = Query(4, ge=1, le=64)) -> schemas.StringsView:
c = require(cid)
found = strings.extract(c.binary, min_length=min_length)
return schemas.StringsView(
strings=[
schemas.StringView(offset=s.offset, text=s.text)
for s in found
])
@app.post("/api/challenges/{cid}/submit")
def submit(cid: str, body: schemas.SubmitRequest) -> schemas.SubmitResult:
c = require(cid)
if not body.within_limits():
raise HTTPException(413, "answer too long")
result = grade(c, body.answer)
if result.correct:
progress.mark_solved(body.session, c.id)
return schemas.SubmitResult(
correct=result.correct,
message=result.message,
revealed_source=result.revealed_source)
@app.get("/api/progress")
def get_progress(
session: str = Query(DEFAULT_SESSION)) -> schemas.ProgressView:
solved = progress.solved(session)
return schemas.ProgressView(
session=session,
solved=sorted(solved),
total=len(store.list()))
return app

View File

@ -0,0 +1,9 @@
"""
©AngelaMos | 2026
limits.py
"""
MAX_HEX_BYTES = 4096
MAX_DISASM_BYTES = 65536
MAX_ANSWER_LEN = 8192
DEFAULT_SESSION = "local"

View File

@ -0,0 +1,98 @@
"""
©AngelaMos | 2026
schemas.py
"""
from pydantic import BaseModel
from rveng.api.limits import DEFAULT_SESSION, MAX_ANSWER_LEN
class ChallengeSummary(BaseModel):
id: str
module: str
title: str
class ChallengeDetail(BaseModel):
id: str
module: str
title: str
mission: str
category: str
size: int
class HexView(BaseModel):
base: int
length: int
lines: list[str]
class FunctionView(BaseModel):
name: str
value: int
size: int
class SectionView(BaseModel):
index: int
name: str
type: str
addr: int
offset: int
size: int
flags: str
class ElfView(BaseModel):
type: int
machine: int
entry: int
sections: list[SectionView]
functions: list[FunctionView]
class InstructionView(BaseModel):
address: int
mnemonic: str
op_str: str
bytes: str
immediate: int | None
branch_target: int | None
is_gate: bool
class DisasmView(BaseModel):
symbol: str
instructions: list[InstructionView]
gate_address: int | None
class StringView(BaseModel):
offset: int
text: str
class StringsView(BaseModel):
strings: list[StringView]
class SubmitRequest(BaseModel):
answer: str
session: str = DEFAULT_SESSION
def within_limits(self) -> bool:
return len(self.answer) <= MAX_ANSWER_LEN
class SubmitResult(BaseModel):
correct: bool
message: str
revealed_source: str | None
class ProgressView(BaseModel):
session: str
solved: list[str]
total: int

View File

@ -0,0 +1,8 @@
"""
©AngelaMos | 2026
server.py
"""
from rveng.api.app import create_app
app = create_app()

View File

@ -0,0 +1,118 @@
"""
©AngelaMos | 2026
store.py
"""
import json
from pathlib import Path
from typing import Protocol
from rveng.engine import patch
from rveng.engine.challenge import (
Challenge,
FoundValue,
IdentifiedSymbol,
PatchedBytes,
)
MANIFEST = "challenge.json"
TARGET = "target"
SOURCE = "source.c"
CAT_FOUND_VALUE = "found_value"
CAT_IDENTIFIED_SYMBOL = "identified_symbol"
CAT_PATCHED_BYTES = "patched_bytes"
class ChallengeError(ValueError):
"""
Raised when a challenge asset directory is malformed
"""
def _build_answer(spec: dict, binary: bytes):
category = spec.get("category")
if category == CAT_FOUND_VALUE:
return FoundValue(spec["expected"])
if category == CAT_IDENTIFIED_SYMBOL:
return IdentifiedSymbol(spec["name"])
if category == CAT_PATCHED_BYTES:
offset = spec["offset"]
replacement = bytes.fromhex(spec["patch"])
known_good = patch.apply(binary, offset, replacement)
return PatchedBytes(offset=offset, known_good=known_good)
raise ChallengeError(f"unknown answer category: {category}")
def load_challenge(directory: Path) -> Challenge:
"""
Load one challenge from its asset directory
"""
manifest_path = directory / MANIFEST
if not manifest_path.is_file():
raise ChallengeError(f"no manifest in {directory}")
manifest = json.loads(manifest_path.read_text())
binary = (directory / TARGET).read_bytes()
source = (directory / SOURCE).read_text()
return Challenge(
id=manifest["id"],
module=manifest["module"],
title=manifest["title"],
mission=manifest["mission"],
binary=binary,
source=source,
answer=_build_answer(manifest["answer"], binary),
)
class ChallengeStore:
"""
An in-memory registry of loaded challenges keyed by id
"""
def __init__(self, challenges: list[Challenge]):
self._by_id = {c.id: c for c in challenges}
def list(self) -> list[Challenge]:
return sorted(self._by_id.values(), key=lambda c: c.id)
def get(self, challenge_id: str) -> Challenge | None:
return self._by_id.get(challenge_id)
def load_store(root: Path) -> ChallengeStore:
"""
Load every challenge directory under root into a store
"""
challenges = []
for directory in sorted(root.iterdir()):
if (directory / MANIFEST).is_file():
challenges.append(load_challenge(directory))
return ChallengeStore(challenges)
class ProgressStore(Protocol):
"""
Persistence boundary for solved-challenge tracking
"""
def mark_solved(self, session: str, challenge_id: str) -> None:
...
def solved(self, session: str) -> set[str]:
...
class InMemoryProgress:
"""
A process-local progress store, swapped for SQLite in M4
"""
def __init__(self):
self._solved: dict[str, set[str]] = {}
def mark_solved(self, session: str, challenge_id: str) -> None:
self._solved.setdefault(session, set()).add(challenge_id)
def solved(self, session: str) -> set[str]:
return set(self._solved.get(session, set()))

View File

@ -0,0 +1,4 @@
"""
©AngelaMos | 2026
__init__.py
"""

View File

@ -0,0 +1,163 @@
"""
©AngelaMos | 2026
challenge.py
"""
from dataclasses import dataclass
from rveng.engine import patch
Category = str
FOUND_VALUE: Category = "found_value"
IDENTIFIED_SYMBOL: Category = "identified_symbol"
PATCHED_BYTES: Category = "patched_bytes"
class AnswerError(ValueError):
"""
Raised when a submission cannot be interpreted for its category
"""
@dataclass(frozen=True)
class FoundValue:
"""
The learner must locate a numeric value or a string
"""
expected: int | str
@dataclass(frozen=True)
class IdentifiedSymbol:
"""
The learner must name a function, section, or symbol
"""
name: str
@dataclass(frozen=True)
class PatchedBytes:
"""
The learner must edit bytes to match a known-good patched target
"""
offset: int
known_good: bytes
AnswerSpec = FoundValue | IdentifiedSymbol | PatchedBytes
@dataclass(frozen=True)
class Challenge:
"""
A curated challenge: a binary, a mission, an answer, and hidden source
"""
id: str
module: str
title: str
mission: str
binary: bytes
source: str
answer: AnswerSpec
@property
def category(self) -> Category:
match self.answer:
case FoundValue():
return FOUND_VALUE
case IdentifiedSymbol():
return IDENTIFIED_SYMBOL
case PatchedBytes():
return PATCHED_BYTES
case _:
raise AnswerError("unknown answer spec")
@dataclass(frozen=True)
class GradeResult:
"""
The outcome of grading, revealing source only when correct
"""
correct: bool
message: str
revealed_source: str | None
def normalize_int(text: str) -> int:
"""
Parse a submitted integer in hex, trailing-h, or decimal form
"""
token = text.strip().lower()
if not token:
raise AnswerError("empty value")
if token.startswith("0x"):
return int(token, 16)
if token.endswith("h"):
return int(token[:-1], 16)
return int(token, 10)
def _grade_found_value(spec: FoundValue, submitted: str) -> bool:
if isinstance(spec.expected, int):
try:
return normalize_int(submitted) == spec.expected
except (AnswerError, ValueError):
return False
return submitted.strip().lower() == spec.expected.strip().lower()
def _grade_identified_symbol(
spec: IdentifiedSymbol, submitted: str) -> bool:
return submitted.strip().lower() == spec.name.strip().lower()
def _grade_patched_bytes(
spec: PatchedBytes,
original: bytes,
submitted: bytes) -> bool:
return patch.verify_patch(
original, spec.offset, submitted, spec.known_good)
def grade(challenge: Challenge, submission: str | bytes) -> GradeResult:
"""
Grade a submission against the challenge answer spec
"""
spec = challenge.answer
match spec:
case FoundValue():
correct = _grade_found_value(spec, _as_text(submission))
case IdentifiedSymbol():
correct = _grade_identified_symbol(spec, _as_text(submission))
case PatchedBytes():
try:
submitted = _as_bytes(submission)
except AnswerError:
return GradeResult(False, "invalid patch bytes", None)
correct = _grade_patched_bytes(
spec, challenge.binary, submitted)
case _:
raise AnswerError("unknown answer spec")
if correct:
return GradeResult(True, "correct", challenge.source)
return GradeResult(False, "not correct yet", None)
def _as_text(submission: str | bytes) -> str:
if isinstance(submission, bytes):
return submission.decode("utf-8", "replace")
return submission
def _as_bytes(submission: str | bytes) -> bytes:
if isinstance(submission, str):
try:
return bytes.fromhex(submission.strip().replace(" ", ""))
except ValueError as exc:
raise AnswerError("submission is not valid hex") from exc
return submission

View File

@ -0,0 +1,115 @@
"""
©AngelaMos | 2026
disasm.py
"""
from dataclasses import dataclass
from capstone import CS_ARCH_X86, CS_MODE_64, CS_OPT_SYNTAX_INTEL, Cs
from capstone.x86 import X86_OP_IMM
from rveng.engine.elf import ElfImage, Symbol
COMPARE_MNEMONICS = frozenset({"cmp", "test"})
CONDITIONAL_JUMPS = frozenset({
"je", "jne", "jz", "jnz", "jg", "jge", "jl", "jle",
"ja", "jae", "jb", "jbe", "js", "jns", "jo", "jno",
"jp", "jnp", "jc", "jnc",
})
CALL_JUMP = frozenset({"call", "jmp"})
@dataclass(frozen=True)
class Instruction:
"""
One decoded x86-64 instruction with teaching annotation
"""
address: int
mnemonic: str
op_str: str
raw: bytes
immediate: int | None
branch_target: int | None = None
@property
def size(self) -> int:
return len(self.raw)
@property
def is_compare(self) -> bool:
return self.mnemonic in COMPARE_MNEMONICS
@property
def is_conditional_branch(self) -> bool:
return self.mnemonic in CONDITIONAL_JUMPS
@property
def is_flow(self) -> bool:
return (self.mnemonic in CONDITIONAL_JUMPS
or self.mnemonic in CALL_JUMP)
def render(self) -> str:
return f"{self.address:x}: {self.mnemonic} {self.op_str}".rstrip()
def _new_engine() -> Cs:
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.syntax = CS_OPT_SYNTAX_INTEL
md.detail = True
return md
def _immediate(ins) -> int | None:
value = None
for operand in ins.operands:
if operand.type == X86_OP_IMM:
value = operand.imm
return value
def disassemble(code: bytes, base: int = 0) -> list[Instruction]:
"""
Decode a byte range into annotated instructions at virtual base
"""
md = _new_engine()
out = []
for ins in md.disasm(code, base):
imm = _immediate(ins)
flow = (ins.mnemonic in CONDITIONAL_JUMPS
or ins.mnemonic in CALL_JUMP)
out.append(Instruction(
address=ins.address,
mnemonic=ins.mnemonic,
op_str=ins.op_str,
raw=bytes(ins.bytes),
immediate=None if flow else imm,
branch_target=imm if flow else None,
))
return out
def disassemble_symbol(
image: ElfImage, symbol: Symbol) -> list[Instruction]:
"""
Disassemble one function symbol from its owning in-file section
"""
shndx = symbol.shndx
if shndx <= 0 or shndx >= len(image.sections):
raise ValueError("symbol is not defined in an in-file section")
section = image.sections[shndx]
start = symbol.value - section.addr + section.offset
if start < 0 or start + symbol.size > len(image.data):
raise ValueError("symbol maps outside the file")
code = image.data[start:start + symbol.size]
return disassemble(code, symbol.value)
def find_gate(instructions: list[Instruction]) -> Instruction | None:
"""
Return the first comparison instruction that carries an immediate
"""
for ins in instructions:
if ins.is_compare and ins.immediate is not None:
return ins
return None

View File

@ -0,0 +1,301 @@
"""
©AngelaMos | 2026
elf.py
"""
import struct
from dataclasses import dataclass
ELF_MAGIC = b"\x7fELF"
EI_CLASS = 4
EI_DATA = 5
ELFCLASS64 = 2
ELFDATA_LSB = 1
ELFDATA_MSB = 2
EHDR_SIZE = 64
E_TYPE = 0x10
E_MACHINE = 0x12
E_ENTRY = 0x18
E_PHOFF = 0x20
E_SHOFF = 0x28
E_SHENTSIZE = 0x3A
E_SHNUM = 0x3C
E_SHSTRNDX = 0x3E
SHDR_SIZE = 64
SH_NAME = 0x00
SH_TYPE = 0x04
SH_FLAGS = 0x08
SH_ADDR = 0x10
SH_OFFSET = 0x18
SH_SIZE = 0x20
SH_LINK = 0x28
SH_ENTSIZE = 0x38
SYM_SIZE = 24
ST_NAME = 0x00
ST_INFO = 0x04
ST_SHNDX = 0x06
ST_VALUE = 0x08
ST_SIZE = 0x10
SHT_SYMTAB = 2
SHT_NOBITS = 8
SHF_WRITE = 0x1
SHF_ALLOC = 0x2
SHF_EXECINSTR = 0x4
STT_FUNC = 2
STB_GLOBAL = 1
SECTION_TYPES = {
0: "NULL",
1: "PROGBITS",
2: "SYMTAB",
3: "STRTAB",
4: "RELA",
6: "DYNAMIC",
7: "NOTE",
8: "NOBITS",
11: "DYNSYM",
}
SYMBOL_TYPES = {0: "NOTYPE", 1: "OBJECT", 2: "FUNC", 3: "SECTION"}
SYMBOL_BINDS = {0: "LOCAL", 1: "GLOBAL", 2: "WEAK"}
class NotAnElf(ValueError):
"""
Raised when the bytes are not a supported ELF64 image
"""
def _cstring(blob: bytes, offset: int) -> str:
end = blob.find(b"\x00", offset)
if end == -1:
end = len(blob)
return blob[offset:end].decode("utf-8", "replace")
@dataclass(frozen=True)
class ElfHeader:
"""
Parsed Elf64 header fields
"""
endian: str
e_type: int
e_machine: int
e_entry: int
e_phoff: int
e_shoff: int
e_shentsize: int
e_shnum: int
e_shstrndx: int
@dataclass(frozen=True)
class Section:
"""
One entry of the section header table
"""
index: int
name: str
type: int
flags: int
addr: int
offset: int
size: int
link: int
entsize: int
@property
def type_name(self) -> str:
return SECTION_TYPES.get(self.type, f"0x{self.type:x}")
@property
def is_nobits(self) -> bool:
return self.type == SHT_NOBITS
@property
def flag_str(self) -> str:
out = ""
if self.flags & SHF_WRITE:
out += "W"
if self.flags & SHF_ALLOC:
out += "A"
if self.flags & SHF_EXECINSTR:
out += "X"
return out
def file_bytes(self, image: bytes) -> bytes:
if self.is_nobits:
return b""
return image[self.offset:self.offset + self.size]
@dataclass(frozen=True)
class Symbol:
"""
One entry of a symbol table
"""
name: str
value: int
size: int
type: int
bind: int
shndx: int
@property
def type_name(self) -> str:
return SYMBOL_TYPES.get(self.type, f"0x{self.type:x}")
@property
def bind_name(self) -> str:
return SYMBOL_BINDS.get(self.bind, f"0x{self.bind:x}")
class ElfImage:
"""
A parsed ELF64 image: header, sections, and symbols
"""
def __init__(self, data: bytes):
self.data = data
self.header = parse_header(data)
self.sections = parse_sections(data, self.header)
self.symbols = parse_symbols(
data, self.sections, self.header.endian)
def section(self, name: str) -> Section | None:
for sec in self.sections:
if sec.name == name:
return sec
return None
def symbol(self, name: str) -> Symbol | None:
for sym in self.symbols:
if sym.name == name:
return sym
return None
def functions(self) -> list[Symbol]:
return [s for s in self.symbols if s.type == STT_FUNC]
def parse_header(data: bytes) -> ElfHeader:
"""
Parse and validate the Elf64 header
"""
if len(data) < EHDR_SIZE or not data.startswith(ELF_MAGIC):
raise NotAnElf("missing ELF magic")
if data[EI_CLASS] != ELFCLASS64:
raise NotAnElf("not ELF64")
endian = "<" if data[EI_DATA] == ELFDATA_LSB else ">"
read_h = lambda off: struct.unpack_from(endian + "H", data, off)[0]
read_q = lambda off: struct.unpack_from(endian + "Q", data, off)[0]
header = ElfHeader(
endian=endian,
e_type=read_h(E_TYPE),
e_machine=read_h(E_MACHINE),
e_entry=read_q(E_ENTRY),
e_phoff=read_q(E_PHOFF),
e_shoff=read_q(E_SHOFF),
e_shentsize=read_h(E_SHENTSIZE),
e_shnum=read_h(E_SHNUM),
e_shstrndx=read_h(E_SHSTRNDX),
)
_validate_section_table(data, header)
return header
def _validate_section_table(data: bytes, hdr: ElfHeader) -> None:
if hdr.e_shnum == 0:
return
if hdr.e_shentsize < SHDR_SIZE:
raise NotAnElf("section entry size too small")
table_end = hdr.e_shoff + hdr.e_shnum * hdr.e_shentsize
if hdr.e_shoff < EHDR_SIZE or table_end > len(data):
raise NotAnElf("section table out of bounds")
if hdr.e_shstrndx >= hdr.e_shnum:
raise NotAnElf("section name table index out of range")
def _raw_sections(data: bytes, hdr: ElfHeader) -> list[dict]:
en = hdr.endian
rows = []
for i in range(hdr.e_shnum):
base = hdr.e_shoff + i * hdr.e_shentsize
rows.append({
"index": i,
"name_off": struct.unpack_from(en + "I", data, base + SH_NAME)[0],
"type": struct.unpack_from(en + "I", data, base + SH_TYPE)[0],
"flags": struct.unpack_from(en + "Q", data, base + SH_FLAGS)[0],
"addr": struct.unpack_from(en + "Q", data, base + SH_ADDR)[0],
"offset": struct.unpack_from(en + "Q", data, base + SH_OFFSET)[0],
"size": struct.unpack_from(en + "Q", data, base + SH_SIZE)[0],
"link": struct.unpack_from(en + "I", data, base + SH_LINK)[0],
"entsize": struct.unpack_from(en + "Q", data, base + SH_ENTSIZE)[0],
})
return rows
def parse_sections(data: bytes, hdr: ElfHeader) -> list[Section]:
"""
Parse the section header table and resolve section names
"""
rows = _raw_sections(data, hdr)
if not rows:
return []
shstr = rows[hdr.e_shstrndx]
names = data[shstr["offset"]:shstr["offset"] + shstr["size"]]
sections = []
for row in rows:
sections.append(Section(
index=row["index"],
name=_cstring(names, row["name_off"]),
type=row["type"],
flags=row["flags"],
addr=row["addr"],
offset=row["offset"],
size=row["size"],
link=row["link"],
entsize=row["entsize"],
))
return sections
def parse_symbols(
data: bytes,
sections: list[Section],
endian: str = "<") -> list[Symbol]:
"""
Parse the .symtab entries and resolve their names via the linked strtab
"""
symtab = next(
(s for s in sections if s.type == SHT_SYMTAB), None)
if symtab is None or symtab.entsize == 0:
return []
if symtab.link >= len(sections):
return []
strtab = sections[symtab.link]
names = data[strtab.offset:strtab.offset + strtab.size]
body = data[symtab.offset:symtab.offset + symtab.size]
en = endian
symbols = []
for base in range(0, len(body) - SYM_SIZE + 1, SYM_SIZE):
name_off = struct.unpack_from(en + "I", body, base + ST_NAME)[0]
info = body[base + ST_INFO]
symbols.append(Symbol(
name=_cstring(names, name_off),
value=struct.unpack_from(en + "Q", body, base + ST_VALUE)[0],
size=struct.unpack_from(en + "Q", body, base + ST_SIZE)[0],
type=info & 0xF,
bind=info >> 4,
shndx=struct.unpack_from(en + "H", body, base + ST_SHNDX)[0],
))
return symbols

View File

@ -0,0 +1,63 @@
"""
©AngelaMos | 2026
hex.py
"""
from dataclasses import dataclass
BYTES_PER_LINE = 16
GROUP_SIZE = 8
NON_PRINTABLE = "."
PRINTABLE_LOW = 0x20
PRINTABLE_HIGH = 0x7E
OFFSET_WIDTH = 8
@dataclass(frozen=True)
class HexLine:
"""
One rendered line of a hex dump
"""
offset: int
data: bytes
def hex_cells(self) -> str:
cells = []
for index in range(BYTES_PER_LINE):
if index < len(self.data):
cells.append(f"{self.data[index]:02x}")
else:
cells.append(" ")
if index == GROUP_SIZE - 1:
cells.append("")
return " ".join(cells)
def ascii_gutter(self) -> str:
chars = []
for byte in self.data:
if PRINTABLE_LOW <= byte <= PRINTABLE_HIGH:
chars.append(chr(byte))
else:
chars.append(NON_PRINTABLE)
return "".join(chars)
def render(self) -> str:
offset = f"{self.offset:0{OFFSET_WIDTH}x}"
return f"{offset} {self.hex_cells()} |{self.ascii_gutter()}|"
def iter_lines(data: bytes, base: int = 0):
"""
Yield HexLine rows for data starting at virtual base
"""
for start in range(0, len(data), BYTES_PER_LINE):
chunk = data[start:start + BYTES_PER_LINE]
yield HexLine(offset=base + start, data=chunk)
def hexdump(data: bytes, base: int = 0) -> str:
"""
Render data as a canonical offset hex dump with ascii gutter
"""
return "\n".join(line.render() for line in iter_lines(data, base))

View File

@ -0,0 +1,61 @@
"""
©AngelaMos | 2026
patch.py
"""
from dataclasses import dataclass
class PatchError(ValueError):
"""
Raised when a patch cannot be applied within bounds
"""
@dataclass(frozen=True)
class ByteDiff:
"""
A single differing byte between two equal-length buffers
"""
offset: int
before: int
after: int
def diff(original: bytes, modified: bytes) -> list[ByteDiff]:
"""
Return the per-byte differences between two equal-length buffers
"""
if len(original) != len(modified):
raise PatchError("buffers differ in length")
out = []
for offset, (a, b) in enumerate(zip(original, modified)):
if a != b:
out.append(ByteDiff(offset=offset, before=a, after=b))
return out
def apply(original: bytes, offset: int, new_bytes: bytes) -> bytes:
"""
Return original with new_bytes written at offset, length preserved
"""
if offset < 0 or offset + len(new_bytes) > len(original):
raise PatchError("patch out of bounds")
return (original[:offset]
+ new_bytes
+ original[offset + len(new_bytes):])
def verify_patch(
original: bytes,
offset: int,
submitted: bytes,
known_good: bytes) -> bool:
"""
True when applying submitted at offset reproduces the known-good target
"""
try:
return apply(original, offset, submitted) == known_good
except PatchError:
return False

View File

@ -0,0 +1,75 @@
"""
©AngelaMos | 2026
strings.py
"""
from dataclasses import dataclass
from rveng.engine.elf import ElfImage, Section
DEFAULT_MIN_LENGTH = 4
PRINTABLE_LOW = 0x20
PRINTABLE_HIGH = 0x7E
@dataclass(frozen=True)
class FoundString:
"""
A printable run located at a file offset
"""
offset: int
text: str
@property
def length(self) -> int:
return len(self.text)
def _is_printable(byte: int) -> bool:
return PRINTABLE_LOW <= byte <= PRINTABLE_HIGH
def extract(
data: bytes,
min_length: int = DEFAULT_MIN_LENGTH,
base: int = 0) -> list[FoundString]:
"""
Find printable byte runs of at least min_length in data
"""
found = []
run = bytearray()
start = 0
for index, byte in enumerate(data):
if _is_printable(byte):
if not run:
start = index
run.append(byte)
continue
if len(run) >= min_length:
found.append(FoundString(base + start, run.decode("ascii")))
run.clear()
if len(run) >= min_length:
found.append(FoundString(base + start, run.decode("ascii")))
return found
def extract_in_section(
image: ElfImage,
section: Section,
min_length: int = DEFAULT_MIN_LENGTH) -> list[FoundString]:
"""
Extract strings from one section, offsets relative to the file
"""
body = section.file_bytes(image.data)
return extract(body, min_length, base=section.offset)
def find(data: bytes, needle: str) -> FoundString | None:
"""
Return the first extracted string equal to needle
"""
for item in extract(data, min_length=len(needle)):
if item.text == needle:
return item
return None

View File

@ -0,0 +1,20 @@
"""
©AngelaMos | 2026
conftest.py
"""
from pathlib import Path
import pytest
FIXTURES = Path(__file__).parent / "fixtures"
@pytest.fixture(scope="session")
def gate_path() -> Path:
return FIXTURES / "gate"
@pytest.fixture(scope="session")
def gate_bytes(gate_path: Path) -> bytes:
return gate_path.read_bytes()

BIN
PROJECTS/advanced/rveng/tests/fixtures/gate vendored Executable file

Binary file not shown.

View File

@ -0,0 +1,23 @@
#include <stdio.h>
#include <stdlib.h>
int check(int n) {
if (n == 1337) {
return 1;
}
return 0;
}
int main(int argc, char **argv) {
char *secret = "the_flag_is_here";
int n = 0;
if (argc > 1) {
n = atoi(argv[1]);
}
if (check(n)) {
printf("unlocked: %s\n", secret);
} else {
printf("wrong number\n");
}
return 0;
}

View File

@ -0,0 +1,132 @@
"""
©AngelaMos | 2026
test_api.py
"""
import pytest
from fastapi.testclient import TestClient
from rveng.api.app import create_app
from rveng.api.limits import MAX_HEX_BYTES
ELF_MAGIC_LINE = (
"00000000 7f 45 4c 46 02 01 01 00 "
"00 00 00 00 00 00 00 00 |.ELF............|"
)
@pytest.fixture()
def client() -> TestClient:
return TestClient(create_app())
def test_list_has_three_seed_challenges(client: TestClient):
body = client.get("/api/challenges").json()
ids = {c["id"] for c in body}
assert ids == {
"03-flip-the-gate", "04-name-the-function", "05-find-the-gate"}
def test_detail_does_not_leak_source_or_answer(client: TestClient):
body = client.get("/api/challenges/05-find-the-gate").json()
assert body["category"] == "found_value"
assert "source" not in body
assert "answer" not in body
assert "expected" not in body
def test_missing_challenge_is_404(client: TestClient):
assert client.get("/api/challenges/nope").status_code == 404
def test_hex_first_line_is_elf_magic(client: TestClient):
body = client.get(
"/api/challenges/05-find-the-gate/hex?offset=0&length=16").json()
assert body["lines"][0] == ELF_MAGIC_LINE
def test_hex_length_is_capped(client: TestClient):
body = client.get(
"/api/challenges/05-find-the-gate/hex?length=999999").json()
assert body["length"] == MAX_HEX_BYTES
def test_elf_view_reports_entry_and_functions(client: TestClient):
body = client.get("/api/challenges/05-find-the-gate/elf").json()
assert body["entry"] == 0x401060
names = {f["name"] for f in body["functions"]}
assert "check" in names and "main" in names
def test_disasm_marks_the_gate(client: TestClient):
body = client.get(
"/api/challenges/05-find-the-gate/disasm?symbol=check").json()
assert body["gate_address"] == 0x40114D
gate = next(i for i in body["instructions"] if i["is_gate"])
assert gate["mnemonic"] == "cmp"
assert gate["immediate"] == 1337
def test_disasm_unknown_symbol_is_404(client: TestClient):
r = client.get("/api/challenges/05-find-the-gate/disasm?symbol=nope")
assert r.status_code == 404
def test_strings_finds_the_secret(client: TestClient):
body = client.get("/api/challenges/05-find-the-gate/strings").json()
texts = {s["text"] for s in body["strings"]}
assert "the_flag_is_here" in texts
def test_submit_found_value_correct_reveals_source(client: TestClient):
r = client.post(
"/api/challenges/05-find-the-gate/submit",
json={"answer": "0x539"}).json()
assert r["correct"] is True
assert "1337" in r["revealed_source"] or "the_flag" in r["revealed_source"]
def test_submit_found_value_wrong_hides_source(client: TestClient):
r = client.post(
"/api/challenges/05-find-the-gate/submit",
json={"answer": "42"}).json()
assert r["correct"] is False
assert r["revealed_source"] is None
def test_submit_identified_symbol(client: TestClient):
r = client.post(
"/api/challenges/04-name-the-function/submit",
json={"answer": "check"}).json()
assert r["correct"] is True
def test_submit_patched_bytes_correct(client: TestClient):
r = client.post(
"/api/challenges/03-flip-the-gate/submit",
json={"answer": "9090"}).json()
assert r["correct"] is True
def test_submit_bad_hex_does_not_500(client: TestClient):
r = client.post(
"/api/challenges/03-flip-the-gate/submit",
json={"answer": "zz"})
assert r.status_code == 200
assert r.json()["correct"] is False
def test_submit_oversized_answer_rejected(client: TestClient):
r = client.post(
"/api/challenges/05-find-the-gate/submit",
json={"answer": "9" * 100000})
assert r.status_code == 413
def test_progress_tracks_solves(client: TestClient):
client.post(
"/api/challenges/05-find-the-gate/submit",
json={"answer": "1337", "session": "s1"})
body = client.get("/api/progress?session=s1").json()
assert body["solved"] == ["05-find-the-gate"]
assert body["total"] == 3

View File

@ -0,0 +1,69 @@
"""
©AngelaMos | 2026
test_challenge.py
"""
import pytest
from rveng.engine import challenge as ch
from rveng.engine import patch
JNE_FILE_OFFSET = 0x1154
NOP_NOP = b"\x90\x90"
SOURCE = "int check(int n){ return n == 1337; }"
def _challenge(answer, binary=b"") -> ch.Challenge:
return ch.Challenge(
id="t", module="m", title="t", mission="find it",
binary=binary, source=SOURCE, answer=answer)
def test_normalize_int_accepts_hex_dec_and_h():
assert ch.normalize_int("0x539") == 1337
assert ch.normalize_int("539h") == 1337
assert ch.normalize_int(" 1337 ") == 1337
def test_found_value_numeric_correct_reveals_source():
c = _challenge(ch.FoundValue(1337))
result = ch.grade(c, "0x539")
assert result.correct
assert result.revealed_source == SOURCE
def test_found_value_wrong_hides_source():
c = _challenge(ch.FoundValue(1337))
result = ch.grade(c, "1234")
assert not result.correct
assert result.revealed_source is None
def test_found_value_string():
c = _challenge(ch.FoundValue("the_flag_is_here"))
assert ch.grade(c, "the_flag_is_here").correct
assert not ch.grade(c, "nope").correct
def test_identified_symbol_case_insensitive():
c = _challenge(ch.IdentifiedSymbol("check"))
assert ch.grade(c, "Check").correct
assert not ch.grade(c, "main").correct
def test_category_property():
assert _challenge(ch.FoundValue(1)).category == ch.FOUND_VALUE
assert _challenge(
ch.IdentifiedSymbol("x")).category == ch.IDENTIFIED_SYMBOL
assert _challenge(
ch.PatchedBytes(0, b"")).category == ch.PATCHED_BYTES
def test_patched_bytes_graded_by_static_diff(gate_bytes: bytes):
known_good = patch.apply(gate_bytes, JNE_FILE_OFFSET, NOP_NOP)
c = _challenge(
ch.PatchedBytes(JNE_FILE_OFFSET, known_good), binary=gate_bytes)
assert ch.grade(c, NOP_NOP).correct
assert ch.grade(c, "9090").correct
assert not ch.grade(c, b"\x75\x07").correct

View File

@ -0,0 +1,54 @@
"""
©AngelaMos | 2026
test_disasm.py
"""
import pytest
from rveng.engine import disasm, elf
CHECK_BYTES = bytes.fromhex(
"554889e5897dfc817dfc390500007507"
"b80100000 0eb05b80000000 05dc3".replace(" ", ""))
EXPECTED = [
(0x401146, "push", "rbp"),
(0x401147, "mov", "rbp, rsp"),
(0x40114A, "mov", "dword ptr [rbp - 4], edi"),
(0x40114D, "cmp", "dword ptr [rbp - 4], 0x539"),
(0x401154, "jne", "0x40115d"),
(0x401156, "mov", "eax, 1"),
(0x40115B, "jmp", "0x401162"),
(0x40115D, "mov", "eax, 0"),
(0x401162, "pop", "rbp"),
(0x401163, "ret", ""),
]
def test_matches_objdump_instruction_for_instruction():
got = disasm.disassemble(CHECK_BYTES, 0x401146)
assert [(i.address, i.mnemonic, i.op_str) for i in got] == EXPECTED
def test_gate_immediate_is_1337():
got = disasm.disassemble(CHECK_BYTES, 0x401146)
gate = disasm.find_gate(got)
assert gate.mnemonic == "cmp"
assert gate.immediate == 1337
assert gate.address == 0x40114D
def test_conditional_branch_flagged():
got = disasm.disassemble(CHECK_BYTES, 0x401146)
jne = next(i for i in got if i.mnemonic == "jne")
assert jne.is_conditional_branch
assert jne.is_flow
def test_disassemble_symbol_from_image(gate_bytes: bytes):
image = elf.ElfImage(gate_bytes)
check = image.symbol("check")
got = disasm.disassemble_symbol(image, check)
assert got[0].address == 0x401146
assert disasm.find_gate(got).immediate == 1337
assert got[-1].mnemonic == "ret"

View File

@ -0,0 +1,79 @@
"""
©AngelaMos | 2026
test_elf.py
"""
import pytest
from rveng.engine import elf
@pytest.fixture(scope="module")
def image(gate_bytes: bytes) -> elf.ElfImage:
return elf.ElfImage(gate_bytes)
def test_header_matches_readelf(image: elf.ElfImage):
hdr = image.header
assert hdr.e_type == 2
assert hdr.e_machine == 0x3E
assert hdr.e_entry == 0x401060
assert hdr.e_shoff == 13984
assert hdr.e_shentsize == 64
assert hdr.e_shnum == 30
assert hdr.e_shstrndx == 29
def test_rejects_non_elf():
with pytest.raises(elf.NotAnElf):
elf.ElfImage(b"not an elf at all, definitely not")
def test_text_section(image: elf.ElfImage):
text = image.section(".text")
assert text is not None
assert text.addr == 0x401060
assert text.offset == 0x1060
assert text.size == 0x182
assert text.flag_str == "AX"
def test_rodata_holds_the_secret(image: elf.ElfImage):
rodata = image.section(".rodata")
assert rodata.addr == 0x402000
assert b"the_flag_is_here" in rodata.file_bytes(image.data)
def test_bss_is_nobits_no_file_bytes(image: elf.ElfImage):
bss = image.section(".bss")
assert bss.is_nobits
assert bss.file_bytes(image.data) == b""
def test_check_symbol(image: elf.ElfImage):
check = image.symbol("check")
assert check.value == 0x401146
assert check.size == 30
assert check.type_name == "FUNC"
assert check.bind_name == "GLOBAL"
def test_main_symbol(image: elf.ElfImage):
main = image.symbol("main")
assert main.value == 0x401164
assert main.size == 126
def test_matches_pyelftools(gate_bytes: bytes, image: elf.ElfImage):
from io import BytesIO
from elftools.elf.elffile import ELFFile
ref = ELFFile(BytesIO(gate_bytes))
assert image.header.e_entry == ref.header.e_entry
assert image.header.e_shnum == ref.num_sections()
assert image.section(".text").addr == ref.get_section_by_name(
".text").header.sh_addr
ref_check = ref.get_section_by_name(".symtab").get_symbol_by_name(
"check")[0]
assert image.symbol("check").value == ref_check.entry.st_value

View File

@ -0,0 +1,40 @@
"""
©AngelaMos | 2026
test_hex.py
"""
from rveng.engine import hex as hexmod
ELF_MAGIC_LINE = (
"00000000 7f 45 4c 46 02 01 01 00 "
"00 00 00 00 00 00 00 00 |.ELF............|"
)
def test_first_line_matches_known_elf_header(gate_bytes: bytes):
first = hexmod.hexdump(gate_bytes[:16])
assert first == ELF_MAGIC_LINE
def test_ascii_gutter_marks_non_printable():
line = hexmod.HexLine(offset=0, data=b"\x7fELF\x00")
assert line.ascii_gutter() == ".ELF."
def test_base_offset_is_applied():
line = next(hexmod.iter_lines(b"\x90", base=0x401060))
assert line.render().startswith("00401060 90")
def test_short_final_line_pads_hex_but_not_ascii():
dump = hexmod.hexdump(b"\x41\x42")
assert dump.startswith("00000000 41 42 ")
assert dump.endswith("|AB|")
def test_full_and_partial_line_count():
data = bytes(range(20))
lines = list(hexmod.iter_lines(data))
assert len(lines) == 2
assert lines[0].offset == 0
assert lines[1].offset == 16

View File

@ -0,0 +1,55 @@
"""
©AngelaMos | 2026
test_patch.py
"""
import pytest
from rveng.engine import disasm, elf, patch
JNE_FILE_OFFSET = 0x1154
JNE_BYTES = b"\x75\x07"
NOP_NOP = b"\x90\x90"
def test_diff_finds_changed_bytes():
diffs = patch.diff(b"\x75\x07\x90", b"\x90\x07\x90")
assert len(diffs) == 1
assert diffs[0].offset == 0
assert diffs[0].before == 0x75
assert diffs[0].after == 0x90
def test_diff_rejects_length_mismatch():
with pytest.raises(patch.PatchError):
patch.diff(b"\x00", b"\x00\x00")
def test_apply_preserves_length():
out = patch.apply(b"AAAA", 1, b"BB")
assert out == b"ABBA"
def test_apply_out_of_bounds_raises():
with pytest.raises(patch.PatchError):
patch.apply(b"AAAA", 3, b"BB")
def test_gate_byte_is_jne(gate_bytes: bytes):
assert gate_bytes[JNE_FILE_OFFSET:JNE_FILE_OFFSET + 2] == JNE_BYTES
def test_verify_flip_the_gate(gate_bytes: bytes):
known_good = patch.apply(gate_bytes, JNE_FILE_OFFSET, NOP_NOP)
assert patch.verify_patch(
gate_bytes, JNE_FILE_OFFSET, NOP_NOP, known_good)
assert not patch.verify_patch(
gate_bytes, JNE_FILE_OFFSET, b"\x75\x07", known_good)
def test_patched_gate_removes_conditional_branch(gate_bytes: bytes):
known_good = patch.apply(gate_bytes, JNE_FILE_OFFSET, NOP_NOP)
image = elf.ElfImage(known_good)
check = image.symbol("check")
ins = disasm.disassemble_symbol(image, check)
assert not any(i.is_conditional_branch for i in ins)

View File

@ -0,0 +1,94 @@
"""
©AngelaMos | 2026
test_robustness.py
"""
import struct
import pytest
from rveng.engine import challenge as ch
from rveng.engine import disasm, elf, patch
JNE_FILE_OFFSET = 0x1154
NOP_NOP = b"\x90\x90"
E_SHOFF = 0x28
E_SHNUM = 0x3C
E_SHSTRNDX = 0x3E
def _corrupt(data: bytes, offset: int, fmt: str, value: int) -> bytes:
buf = bytearray(data)
struct.pack_into("<" + fmt, buf, offset, value)
return bytes(buf)
def test_bad_shstrndx_raises_notanelf(gate_bytes: bytes):
bad = _corrupt(gate_bytes, E_SHSTRNDX, "H", 0xFFFF)
with pytest.raises(elf.NotAnElf):
elf.ElfImage(bad)
def test_section_table_past_eof_raises_notanelf(gate_bytes: bytes):
bad = _corrupt(gate_bytes, E_SHOFF, "Q", len(gate_bytes) + 0x1000)
with pytest.raises(elf.NotAnElf):
elf.ElfImage(bad)
def test_huge_shnum_raises_notanelf(gate_bytes: bytes):
bad = _corrupt(gate_bytes, E_SHNUM, "H", 0xFFFF)
with pytest.raises(elf.NotAnElf):
elf.ElfImage(bad)
def test_truncated_file_raises_notanelf(gate_bytes: bytes):
with pytest.raises(elf.NotAnElf):
elf.ElfImage(gate_bytes[:0x1000])
@pytest.mark.parametrize("bad_hex", ["zz", "909", "0xff", "not hex"])
def test_bad_hex_patch_submission_does_not_crash(
gate_bytes: bytes, bad_hex: str):
known_good = patch.apply(gate_bytes, JNE_FILE_OFFSET, NOP_NOP)
c = ch.Challenge(
id="t", module="m", title="t", mission="m", binary=gate_bytes,
source="src", answer=ch.PatchedBytes(JNE_FILE_OFFSET, known_good))
result = ch.grade(c, bad_hex)
assert result.correct is False
assert result.revealed_source is None
def test_disassemble_symbol_rejects_undefined_symbol(gate_bytes: bytes):
image = elf.ElfImage(gate_bytes)
undefined = elf.Symbol(
name="imported", value=0x401146, size=30,
type=elf.STT_FUNC, bind=elf.STB_GLOBAL, shndx=0)
with pytest.raises(ValueError):
disasm.disassemble_symbol(image, undefined)
def test_disassemble_symbol_rejects_out_of_range_shndx(gate_bytes: bytes):
image = elf.ElfImage(gate_bytes)
weird = elf.Symbol(
name="weird", value=0x401146, size=30,
type=elf.STT_FUNC, bind=elf.STB_GLOBAL, shndx=0xFFF1)
with pytest.raises(ValueError):
disasm.disassemble_symbol(image, weird)
def test_interior_space_is_not_concatenated_into_a_match():
c = ch.Challenge(
id="t", module="m", title="t", mission="m", binary=b"",
source="src", answer=ch.FoundValue(1337))
assert ch.grade(c, "13 37").correct is False
assert ch.grade(c, "1337").correct is True
def test_flow_instruction_target_not_labeled_immediate():
code = bytes.fromhex("817dfc390500007507")
got = disasm.disassemble(code, 0x40114D)
cmp_ins = got[0]
jne_ins = got[1]
assert cmp_ins.immediate == 0x539
assert jne_ins.immediate is None
assert jne_ins.branch_target == 0x40115D

View File

@ -0,0 +1,34 @@
"""
©AngelaMos | 2026
test_strings.py
"""
from rveng.engine import elf, strings
def test_extract_basic_runs():
data = b"\x00\x01hello\x00\xffworld!!\x00"
got = strings.extract(data, min_length=4)
assert [s.text for s in got] == ["hello", "world!!"]
def test_min_length_filters_short_runs():
data = b"ab\x00abcd\x00"
got = strings.extract(data, min_length=4)
assert [s.text for s in got] == ["abcd"]
def test_secret_found_in_rodata_at_known_offset(gate_bytes: bytes):
image = elf.ElfImage(gate_bytes)
rodata = image.section(".rodata")
got = strings.extract_in_section(image, rodata)
texts = [s.text for s in got]
assert "the_flag_is_here" in texts
secret = next(s for s in got if s.text == "the_flag_is_here")
assert secret.offset == 0x2004
def test_find_returns_offset(gate_bytes: bytes):
hit = strings.find(gate_bytes, "wrong number")
assert hit is not None
assert hit.text == "wrong number"

View File

@ -0,0 +1,352 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
]
[[package]]
name = "capstone"
version = "5.0.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/39/01/6516910f546fbb996068207b9dd0229b14bc8dae223114d5e0e27d3cad11/capstone-5.0.9.tar.gz", hash = "sha256:0429af292ddc604d3c9344696807e281d5e728db029f00e6a4ea9e3bff1aac9e", size = 2947300, upload-time = "2026-05-28T16:05:57.968Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/f3/43f4dbfc36c7740d512621a9d42b276ac21bb6ca7cb974d4c8a19060d387/capstone-5.0.9-py3-none-macosx_10_9_universal2.whl", hash = "sha256:58a874d9a6cb15122135b1385a782e5b54c8a4d9396161b6498465046d2e0442", size = 2195391, upload-time = "2026-05-28T16:05:45.515Z" },
{ url = "https://files.pythonhosted.org/packages/70/74/d00de02c62ce864c6b43796c524516ea064e3e6fc1327d452f78270e8323/capstone-5.0.9-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:8c4a1d7134d7d5290e5d77066500385f2c7a4bf66835ccbfde5180042d508b3c", size = 1190392, upload-time = "2026-05-28T16:05:46.953Z" },
{ url = "https://files.pythonhosted.org/packages/42/9a/4eecfbc94961cda70301fd13de69042be1943bee456e37bc1369840601b1/capstone-5.0.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc269c5082e17d7f9f265cd6980e2fdc6ab572df913951d76db0357e06b8001d", size = 1202361, upload-time = "2026-05-28T16:05:48.1Z" },
{ url = "https://files.pythonhosted.org/packages/45/d9/dbeadb9fcd461e608ce79b1cb647fed6baa812fa6e1c68b0ba5ef81291af/capstone-5.0.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41772ad0c71aa5d3e97d541e802593c2e71fb94ac7e20d4202f97ce2a3eb8ed5", size = 1461016, upload-time = "2026-05-28T16:05:49.554Z" },
{ url = "https://files.pythonhosted.org/packages/d1/39/17747862222bb062e86b501f1f148d5ff589b77908b080d30f7f085cbfb7/capstone-5.0.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273fd8d747d2e35c88f91450be51a603ecfaafb00d96d9f315dcb8689c86193e", size = 1485316, upload-time = "2026-05-28T16:05:50.721Z" },
{ url = "https://files.pythonhosted.org/packages/12/56/9a694f60f5b26612039d975c1c84d2ad8fd0ad839b0b1058dbe68dd32b0b/capstone-5.0.9-py3-none-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f5871b57e987d5bf23b531fbe868211b74ad750fa9b830a3d3cdc4956bc0ae3", size = 1483221, upload-time = "2026-05-28T16:05:51.925Z" },
{ url = "https://files.pythonhosted.org/packages/b9/74/c178ac241892bb3668b633065bdbe6dc719c2aa54668d68a8e23a6753525/capstone-5.0.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:69064219c814ad64af35469a7c4ddd19b730cacb96a1a796435ccde0e1567d05", size = 1459140, upload-time = "2026-05-28T16:05:53.182Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f3/3865a0371603994f8bf521595b3072080bfe1b715f8bd080988d026dd2a1/capstone-5.0.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0cc3b1ec319ab0530efc6b181491b77dd3373c81b6dbcf2f05a80e3d8dd61d5e", size = 1488205, upload-time = "2026-05-28T16:05:54.31Z" },
{ url = "https://files.pythonhosted.org/packages/09/a1/39a575aa27a35b43b0ad93065a1803dba39038060eccfb29031d82e3fb2c/capstone-5.0.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cfec34e6e01472fe60850c87c5bea9918c274fa2a605b510e6489bf87c0c9960", size = 1488865, upload-time = "2026-05-28T16:05:55.603Z" },
{ url = "https://files.pythonhosted.org/packages/50/e6/6f06fdb6a9ed32b2f7cd9c036b92d5324112c3ef7080f2c71efc367d40dd/capstone-5.0.9-py3-none-win_amd64.whl", hash = "sha256:732cedbbb56d42e723f14d7af6387f1454194a820b4b96b56d1e53f865ef85d0", size = 1273459, upload-time = "2026-05-28T16:05:56.73Z" },
]
[[package]]
name = "certifi"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
name = "click"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "fastapi"
version = "0.139.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pydantic"
version = "2.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
]
[[package]]
name = "pyelftools"
version = "0.33"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/11/767522582afab1b884d277de0e6e011640cb9d7292a38694b4b1a1df1ae8/pyelftools-0.33.tar.gz", hash = "sha256:660d82dcbeb8e83d1702bd97f223f761625da06111c0cc988eac6b8ab0c1b61f", size = 15068655, upload-time = "2026-05-29T12:56:22.553Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/2a/f9697576603dae937727827505a6126a066affb227034e77e6f9068910da/pyelftools-0.33-py3-none-any.whl", hash = "sha256:f215ad5f47d3f1373a21496a6c9e0707c622840d0622f23ff7ce08678b020036", size = 201178, upload-time = "2026-05-29T12:56:20.587Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "rveng"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "capstone" },
{ name = "fastapi" },
{ name = "pyelftools" },
{ name = "uvicorn" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "capstone", specifier = ">=5.0.9" },
{ name = "fastapi", specifier = ">=0.139.0" },
{ name = "pyelftools", specifier = ">=0.33" },
{ name = "uvicorn", specifier = ">=0.50.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "pytest", specifier = ">=9.1.1" },
]
[[package]]
name = "starlette"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "uvicorn"
version = "0.50.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/41/06cce5dbb9f77591512957710ac709e60b12e6216a2f2d0d607fd49706e8/uvicorn-0.50.0.tar.gz", hash = "sha256:0c92e1bc2259cb7faa4fcef774a5966588f2e88542744550b66799fba10b76f1", size = 93257, upload-time = "2026-07-04T05:03:26.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/3a/eb70620ca2bf8213603d5c731460687c49fee38b0072f0b4a637781f0a53/uvicorn-0.50.0-py3-none-any.whl", hash = "sha256:05f0eb19edf38208f79f43df8a63081b48df31b0cd1e5997be957a4dc97d1b19", size = 72716, upload-time = "2026-07-04T05:03:24.848Z" },
]