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:
parent
6593aa5689
commit
44f1b5bc99
|
|
@ -0,0 +1,10 @@
|
|||
# ©AngelaMos | 2026
|
||||
# .gitignore
|
||||
|
||||
docs/plans/
|
||||
docs/context/
|
||||
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
*.pyc
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.13
|
||||
|
|
@ -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" }
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -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" }
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
def hello() -> str:
|
||||
return "Hello from rveng!"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
__init__.py
|
||||
"""
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
limits.py
|
||||
"""
|
||||
|
||||
MAX_HEX_BYTES = 4096
|
||||
MAX_DISASM_BYTES = 65536
|
||||
MAX_ANSWER_LEN = 8192
|
||||
DEFAULT_SESSION = "local"
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
server.py
|
||||
"""
|
||||
|
||||
from rveng.api.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
|
@ -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()))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
__init__.py
|
||||
"""
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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" },
|
||||
]
|
||||
Loading…
Reference in New Issue