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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/skills/authoring-challenges/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ your archetype.
be torn apart.)
- **Respect sequencing.** Don't introduce advanced topics (e.g. ASLR) early, even as a
footnote/`NOTE`. They get their own much-later level.
- **Respect local ordering inside a DESCRIPTION.** Don't use an instruction, term, or
comparison before the paragraph that introduces it; move the sentence later or rewrite it
using only concepts the learner has already seen.
- **The phenomenon you teach must be REAL, not manufactured by your own scaffolding.**
If the lesson only "works" because of a wrapper you added (e.g. faking a tool's
behavior with `env -i`), the design is wrong — redesign, don't ship the contrivance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ path:
```

This puts the address of the "/flag" string into `rdi`, rather than loading the contents of the string into `rdi`.
Think of `path` as the address where the first byte of `"/flag\0"` lives: `mov` copies bytes from there, while `lea` copies the address so the kernel can walk those bytes until the null byte.

Now, a quick note about the math here: though we write `[rip+path]` above, what _actually_ gets added to `rip` is the delta in addresses between `rip` (which, again, is pointing to the instruction after `lea`) and the "/flag" string.
It's a weird syntax, and yet another little quirk of x86.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,54 @@ def split_operands(insn):
def normalize_reg(reg):
return checker.SUBREG_TO_64.get(reg, reg)

def instruction_sets_reg(insn, reg, value):
if not insn.op_str:
return False

if insn.mnemonic == "mov":
dst, src = split_operands(insn)
return normalize_reg(dst) == reg and normalize_reg(src) == value

if value == "0" and insn.mnemonic == "xor":
dst, src = split_operands(insn)
return normalize_reg(dst) == reg and normalize_reg(src) == reg

return False

def instruction_writes_reg(insn, reg):
if insn.mnemonic not in {"lea", "mov"} or not insn.op_str:
if insn.mnemonic not in {"lea", "mov", "xor"} or not insn.op_str:
return False
return normalize_reg(split_operands(insn)[0]) == reg

def rax_value_before(disas, idx):
for insn in reversed(disas[:idx]):
if instruction_sets_reg(insn, "rax", "0"):
return "0"
if insn.mnemonic == "mov" and insn.op_str:
dst, src = split_operands(insn)
if normalize_reg(dst) == "rax":
return normalize_reg(src)
return None

def assert_write_count_from_read(disas):
syscalls = [i for i, insn in enumerate(disas) if insn.mnemonic == "syscall"]
read_i = next((i for i in syscalls if rax_value_before(disas, i) == "0"), None)
write_i = next((i for i in syscalls if rax_value_before(disas, i) == "1"), None)
assert read_i is not None, "You need to invoke the read syscall (set rax to 0)!"
assert write_i is not None, "You need to invoke the write syscall (set rax to 1)!"
assert read_i < write_i, "You need to read the data before you write it back out!"

rdx_writes_after_read = [
insn
for insn in disas[read_i + 1:write_i]
if instruction_writes_reg(insn, "rdx")
]
assert rdx_writes_after_read and instruction_sets_reg(rdx_writes_after_read[-1], "rdx", "rax"), (
"write's length (rdx) must come from read's return value (rax).\n"
"read returns how many bytes it actually read, so after your read syscall do\n"
"`mov rdx, rax` --- write exactly that many bytes --- rather than hardcoding a length."
)

def check_disassembly(disas):
mov_operands = checker.mov_operands(disas)
syscall_indices = [i for i, insn in enumerate(disas) if insn.mnemonic == "syscall"]
Expand Down Expand Up @@ -65,7 +108,7 @@ def check_disassembly(disas):
"You need to set rax to 2, the syscall number for open!"
)

assert ['rax', '0'] in mov_operands, (
assert any(instruction_sets_reg(insn, "rax", "0") for insn in disas), (
"You need to set rax to 0, the syscall number for read!"
)

Expand All @@ -82,7 +125,7 @@ def check_disassembly(disas):

# Write back exactly what you read: write's length (rdx) must come from read's
# return value (rax), the idiom you learned in read-exact --- not a hardcoded count.
checker.assert_write_count_from_read(disas)
assert_write_count_from_read(disas)

exit_syscall = syscall_indices[-1]
rax_writes_before_exit = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ hacker@dojo:~$ env -i /challenge/program
hacker@dojo:~$ env -i FOO=xxxxxxxx /challenge/program
```

Remember that the whole environment string is placed on the stack, so `FOO=` and the trailing null byte count toward the shift too.
Remember that the whole environment string is placed on the stack, so `FOO=`, the value, and the trailing null byte count toward the shift.
You're not modifying the program at all, just changing how it's launched, which influences where its data ends up!
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Above, the following sequence of events took place:
Now it's your turn!
In this challenge, we have `/challenge/hack`, `/challenge/the`, and `/challenge/planet`.
Run the `/challenge/hack` command, and duplicate its output as input to both the `/challenge/the` and the `/challenge/planet` commands!
Remember that `tee` can take multiple output targets, and `>(command)` is one kind of target.
Scroll back through the previous challenges "Duplicating piped data with tee" and "Process substitution for input" if you need a refresher on this method.

----
Expand Down