pwn.college notes: yansanity
🟡 Yellow Belt -> Program Security -> Reverse Engineering -> Yan85 -> Yansanity
hubertf, 2026-02-28
Contents:
Analysis:
-> flag_seed() runs before main()
y85 code:
IMM REG_A, 17
SYS EXIT REG_A
Create script pc-yansanity-configfinder.py to cache working solutions in pc-yansanity-cache.json to avoid long runs (including hostname and filesize, to later differentiate between -easy and -hard, and also use the values found in pc-yansanity-asm.py; beware to not use timestamp of /flag as this will change if challenge is restarted/reset):
neuland% ssh pc hostname
reverse-engineering~yansanity-hard
…
[ 88%] imm=0x80 sys=0x01 reg=0x04 sysmask=0x20 rc=17
[+] Found SYS_EXIT combination: {'layout': {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}, 'imm_opcode': 128, 'sys_opcode': 1, 'register_tag': 4, 'sys_mask': 32, 'immediate_value': 17}
neuland% python3 pc-yansanity-configfinder.py -h && scp pc-yansanity-configfinder.py pc:/tmp ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py sys-exit ; echo EXIT CODE: $?' ; echo DIFF for pc-yansanity-cache.json: ; ssh pc "cat /tmp/pc-yansanity-cache.json" | diff -u pc-yansanity-cache.json -
…
[+] Winning payload: 11 04 80 : 04 20 01 | imm=0x80 sys=0x01 reg=0x04 sysmask=0x20
EXIT CODE: 0
DIFF for pc-yansanity-cache.json:
neuland%
Needs proper permutation of flags - we do have ONE working register, but do not know which one it is - yet!
yan85 code:
# 1. read 1 byte to 0x330
IMM REG_B = 0x30 / '0' // read buffer: 0x330
IMM REG_C = 0x1 // len=1
IMM REG_A = 0x00 // stdin
SYS READ_MEMORY, ret=REG_D (syscall# 0x01)
# 2. load memory at 0x330 into REG_A
LDM REG_A = memory[0x300 + REG_B] # assuming REG_B is unchanged 0x30 from above
# 3. call exit(REG_A)
SYS EXIT(REG_A)
Goal of step 2 would be to figure out all registers in the right permutation and the READ_MEMORY SYScall-value
neuland% python3 pc-yansanity-configfinder.py -h && scp pc-yansanity-configfinder.py pc:/tmp ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py sys-readmem ; echo EXIT CODE: $?' ; echo DIFF for pc-yansanity-cache.json: ; ssh pc "cat /tmp/pc-yansanity-cache.json" | diff -u pc-yansanity-cache.json -
…
[ 54%] imm=0x80 sys=0x01 ldm=0x10 readmask=0x04 regs(exit=04,buf=20,len=02) stdin=0x0f rc=15
[+] Found SYS_READ_MEMORY configuration: {'layout': {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}, 'imm_opcode': 128, 'sys_opcode': 1, 'ldm_opcode': 16, 'register_exit': 4, 'register_buf': 32, 'register_len': 2, 'sys_mask_read': 4, 'sys_mask_exit': 32, 'stdin_test_values': [65, 97, 0, 255, 240, 15], 'immediate_fd': 0, 'immediate_buf': 48, 'immediate_len': 1, 'flag_state': {'path': '/flag', 'size': 57, 'mtime_ns': 1771782512447539810, 'mtime': '2026-02-22T17:48:32.447540', 'hostname': 'reverse-engineering~yansanity-hard', 'host_label': 'reverse-engineering~yansanity-hard'}}
IMM REG_A (= dein exit-register-tag), 23
IMM REG_B (= dein buffer-register-tag), 0
STM REG_B, REG_A # 0x300 auf den Wert von REG_A setzen
# 2. reset REG_A
IMM REG_A # loeschen!
# 3. restore REG_A
LDM REG_A = memory[0x300+REG_B] # den eben mit STM geschriebenen Wert wieder holen
# 4. pass it to exit()
SYS_EXIT
Upon getting proper exit() code, make sure it also works for further return values, similar to what we did before to ensure the right value is passed from stdin via SYS_READ_MEMORY and LDM to exit().
Only search previously unused opcodes, i.e. exclude IMM, SYS, LDM.
Result:
neuland% python3 pc-yansanity-configfinder.py -h && scp pc-yansanity-configfinder.py pc:/tmp ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-stm ; echo EXIT CODE: $?' ; echo DIFF for pc-yansanity-cache.json: ; ssh pc "cat /tmp/pc-yansanity-cache.json" | diff -u pc-yansanity-cache.json -
…
[ 12%] stm=0x01 test=0x80 rc=128
[+] Found STM configuration: {'layout': {'arg_high': 1, 'arg_low': 0, 'op_byte': 2}, 'imm_opcode': 128, 'sys_opcode': 1, 'ldm_opcode': 16, 'stm_opcode': 1, 'register_exit': 4, 'register_buf': 32, 'sys_mask_exit': 32, 'offset_immediate': 0, 'test_values': [1, 2, 4, 8, 16, 32, 64, 128], 'flag_state': {'path': '/flag', 'size': 57, 'mtime_ns': 1771782512447539810, 'mtime': '2026-02-22T17:48:32.447540', 'hostname': 'reverse-engineering~yansanity-hard', 'host_label': 'reverse-engineering~yansanity-hard'}}
y85 code:
; write "f\0" into memory (offsets 0,1)
IMM REG_B, 'f'
IMM REG_A, 0
STM REG_A, REG_B
IMM REG_B, 0
IMM REG_A, 1
STM REG_A, REG_B
; stash test byte at offset 0x20
IMM REG_B, 0x5A ; ASCII 'Z' (later patched per test)
IMM REG_A, 0x20
STM REG_A, REG_B
; prepare registers for open("f", 0, 0)
IMM REG_A, 0 ; pointer = 0x300 + 0
IMM REG_B, 0x00 ; flags
IMM REG_C, 0x00 ; unused
SYS OPEN?, REG_A ; mask fuzzed in stage
; save fd at scratch offset 0x2F
IMM REG_B, 0x2F
STM REG_B, REG_A
; write one byte from buffer to stdout
IMM REG_A, 1 ; stdout
IMM REG_B, 0x20 ; buffer offset
IMM REG_C, 1 ; length
SYS WRITE?, REG_A ; mask fuzzed in stage
; reload fd and exit with it
IMM REG_B, 0x2F
LDM REG_A, REG_B
SYS EXIT, REG_A
Result:
neuland% python3 pc-yansanity-configfinder.py -h && scp pc-yansanity-configfinder.py pc:/tmp ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-openwrite ; echo EXIT CODE: $?' ; echo DIFF for pc-yansanity-cache.json: ; ssh pc "cat /tmp/pc-yansanity-cache.json" | diff -u pc-yansanity-cache.json -
#fail
y85 code:
IMM reg_exit, <test_byte> ; Wert ins Schreibregister
IMM reg_buf, <offset> ; z.B. 0x00, 0x08, 0x10 ...
STM reg_exit -> reg_buf ; Byte ins VM-Fenster schreiben
IMM reg_exit, 0x00 ; Leser vorab leeren
IMM reg_buf, <offset> ; gleicher Offset
LDM reg_buf -> reg_exit ; Byte zurückholen
SYS reg_exit, exit_mask ; rc = gelesener Wert
Result:
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py verify-stmldm ; echo EXIT CODE: $?' ; echo DIFF for pc-yansanity-cache.json: ; ssh pc "cat /tmp/pc-yansanity-cache.json" | diff -u pc-yansanity-cache.json -
…
[*] STM/LDM verification (write -> read -> exit)...
[*] Flag signature: path=/flag size=22 mtime_ns=1772051319269919762 host=practice~reverse-engineering~yansanity-hard
[01] offset=0x00 test=0x41 rc=0x41
[02] offset=0x00 test=0x61 rc=0x61
[03] offset=0x00 test=0x00 rc=0x00
…
[34] offset=0x28 test=0xff rc=0xff
[35] offset=0x28 test=0xf0 rc=0xf0
[36] offset=0x28 test=0x0f rc=0x0f
[+] STM/LDM verified across offsets: {'test_values': [65, 97, 0, 255, 240, 15], 'offsets': [0, 8, 16, 24, 32, 40], 'register_exit': 4, 'register_buf': 2, 'imm_opcode': 128, 'stm_opcode': 2, 'ldm_opcode': 8, 'sys_opcode': 1, 'sys_mask_exit': 16, 'flag_state': {'path': '/flag', 'size': 22, 'mtime_ns': 1772051319269919762, 'mtime': '2026-02-25T20:28:39.269920', 'hostname': 'practice~reverse-engineering~yansanity-hard', 'host_label': 'practice~reverse-engineering~yansanity-hard'}}
EXIT CODE: 0
…
STM & LDM confirmed working as expected.
y85 code:
; write "f\0" in den VM-Puffer (Offset 0/1 relativ zu 0x300)
IMM REG_B, 'f'
IMM REG_A, 0x00
STM REG_A, REG_B
IMM REG_B, 0x00
IMM REG_A, 0x01
STM REG_A, REG_B
; SYS_OPEN mit pointer=0, flags=0, mode=0
IMM REG_A, 0x00 ; pointer in Fenster
IMM REG_B, 0x00 ; flags
IMM REG_C, 0x00 ; mode
SYS OPEN, REG_A ; <- Masken-Kandidat einsetzen
; EXIT mit fd als returncode
SYS EXIT, REG_A
=> found
y85 code:
For every byte of the marker (::YAN85-WRITE::\n by default):
IMM <byte> -> reg_exit
IMM (write_offset + idx) -> reg_buf
STM reg_exit -> [reg_buf] This copies the marker into VM memory.
IMM write_fd -> reg_exit (stdout is 0x01)
IMM write_offset -> reg_buf
IMM write_len -> reg_len
SYS reg_exit, mask=<candidate> (uses those regs as fd/buffer/len)
IMM 0x00 -> reg_exit
SYS reg_exit, mask=exit_mask to terminate cleanly.
Result:
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-write -v ; echo EXIT CODE: $?'
…
--- PAYLOAD ---
3a 04 80 : 30 02 80 : 04 02 02 : 3a 04 80 : 31 02 80 : 04 02 02 : 59 04 80 : 32 02 80 : 04 02 02 : 41 04 80 : 33 02 80 : 04 02 02 : 4e 04 80 : 34 02 80 : 04 02 02 : 38 04 80 : 35 02 80 : 04 02 02 : 35 04 80 : 36 02 80 : 04 02 02 : 2d 04 80 : 37 02 80 : 04 02 02 : 57 04 80 : 38 02 80 : 04 02 02 : 52 04 80 : 39 02 80 : 04 02 02 : 49 04 80 : 3a 02 80 : 04 02 02 : 54 04 80 : 3b 02 80 : 04 02 02 : 45 04 80 : 3c 02 80 : 04 02 02 : 3a 04 80 : 3d 02 80 : 04 02 02 : 3a 04 80 : 3e 02 80 : 04 02 02 : 0a 04 80 : 3f 02 80 : 04 02 02 : 01 04 80 : 30 02 80 : 10 40 80 : 04 40 01 : 00 04 80 : 04 10 01
[100%] write=0x40 fd=0x01 offset=0x30 len=0x10 rc=0 stdout_len=1087
--- STDOUT ---
[+] Welcome to /challenge/yansanity-hard!
[+] This challenge is an custom emulator. It emulates a completely custom
[+] architecture that we call "Yan85"! You'll have to understand the
[+] emulator to understand the architecture, and you'll have to understand
[+] the architecture to understand the code being emulated, and you will
[+] have to understand that code to get the flag. Good luck!
[+]
[+] This level is a full Yan85 emulator. You'll have to reason about yancode,
[+] and the implications of how the emulator interprets it!
[?] This challenge is special! It randomizes the Yan85 VM based on
[?] the value of the flag. This means that there is no way for you
[?] to know the opcode and argument encodings...
[?]
[?] Keep in mind that the encoding that you observe in practice mode
[?] is going to be different than the actual encoding, because the
[?] practice mode flag is different. How will you adapt?
[?]
[?] Is there maybe a clever side channel you can utilize?
[?] ... Done! VM is randomized!
[!] This time, YOU'RE in control! Please input your yancode: ::YAN85-WRITE::
--------------
[+] Found SYS_WRITE configuration: {'layout': {'arg_high': 1, 'arg_low': 0, 'op_byte': 2}, 'imm_opcode': 128, 'sys_opcode': 1, 'stm_opcode': 2, 'ldm_opcode': 8, 'register_exit': 4, 'register_buf': 2, 'register_len': 64, 'sys_mask_exit': 16, 'sys_mask_read': 128, 'sys_mask_open': 4, 'sys_mask_write': 64, 'write_bytes': [58, 58, 89, 65, 78, 56, 53, 45, 87, 82, 73, 84, 69, 58, 58, 10], 'write_offset': 48, 'write_fd': 1, 'write_len': 16, 'flag_state': {'path': '/flag', 'size': 22, 'mtime_ns': 1772139926687680922, 'mtime': '2026-02-26T21:05:26.687681', 'hostname': 'practice~reverse-engineering~yansanity-hard', 'host_label': 'practice~reverse-engineering~yansanity-hard'}}
EXIT CODE: 0
Confirm in practice-mode with strace:
pwn.college$ echo "3a 04 80 : 30 02 80 : 04 02 02 : 3a 04 80 : 31 02 80 : 04 02 02 : 59 04 80 : 32 02 80 : 04 02 02 : 41 04 80 : 33 02 80 : 04 02 02 : 4e 04 80 : 34 02 80 : 04 02 02 : 38 04 80 : 35 02 80 : 04 02 02 : 35 04 80 : 36 02 80 : 04 02 02 : 2d 04 80 : 37 02 80 : 04 02 02 : 57 04 80 : 38 02 80 : 04 02 02 : 52 04 80 : 39 02 80 : 04 02 02 : 49 04 80 : 3a 02 80 : 04 02 02 : 54 04 80 : 3b 02 80 : 04 02 02 : 45 04 80 : 3c 02 80 : 04 02 02 : 3a 04 80 : 3d 02 80 : 04 02 02 : 3a 04 80 : 3e 02 80 : 04 02 02 : 0a 04 80 : 3f 02 80 : 04 02 02 : 01 04 80 : 30 02 80 : 10 40 80 : 04 40 01 : 00 04 80 : 04 10 01" | xxd -r -p | /challenge/yansanity-hard
[+] Welcome to /challenge/yansanity-hard!
…
[!] This time, YOU'RE in control! Please input your yancode: ::YAN85-WRITE::
Expected output is there, so we now know the SYS-number for write, SYS_WRITE.
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py build-config ; echo EXIT CODE: $?' ; scp pc:/tmp/\*.json .
…
SCP:
pc-yansanity-configfinder.py 100% 87KB 160.2KB/s 00:00
SSH:
[+] wrote config to pc-yansanity-asm-config.json
[*] Assignment status:
- registers: confirmed -> REG_A, REG_B, REG_C
- registers: inferred -> REG_D, REG_S, REG_I, REG_F
- instructions: confirmed -> IMM, STM, LDM, SYS
- instructions: inferred -> STK, ADD, JMP, CMP
- syscalls: confirmed -> WRITE, OPEN, READ_MEMORY, EXIT
- syscalls: inferred -> SLEEP, READ_CODE
- flags: inferred -> LESS, GREATER, EQUAL, NOT_EQUAL, BOTH_ZERO
EXIT CODE: 0
pc-yansanity-asm-config.json 100% 2180 6.1KB/s 00:00
pc-yansanity-cache.json 100% 5384 14.9KB/s 00:00
neuland% cat pc-yansanity-asm-config.json
{
"assignment_sources": {
"flags": {
"BOTH_ZERO": "inferred",
"EQUAL": "inferred",
"GREATER": "inferred",
"LESS": "inferred",
"NOT_EQUAL": "inferred"
},
"instructions": {
"ADD": "inferred",
"CMP": "inferred",
"IMM": "confirmed",
"JMP": "inferred",
"LDM": "confirmed",
"STK": "inferred",
"STM": "confirmed",
"SYS": "confirmed"
},
"registers": {
"REG_A": "confirmed",
"REG_B": "confirmed",
"REG_C": "confirmed",
"REG_D": "inferred",
"REG_F": "inferred",
"REG_I": "inferred",
"REG_S": "inferred"
},
"syscalls": {
"EXIT": "confirmed",
"OPEN": "confirmed",
"READ_CODE": "inferred",
"READ_MEMORY": "confirmed",
"SLEEP": "inferred",
"WRITE": "confirmed"
}
},
"flag_map": {
"0": "ALWAYS",
"1": "LESS",
"2": "GREATER",
"4": "EQUAL",
"8": "NOT_EQUAL",
"16": "BOTH_ZERO"
},
"instruction_layout": [
"arg_low",
"arg_high",
"op_byte"
],
"memory_register_offsets": {
"1": 1027,
"2": 1025,
"4": 1024,
"8": 1028,
"16": 1029,
"32": 1030,
"64": 1026
},
"operation_masks": {
"ADD": {
"byte": "op_byte",
"mask": 16
},
"CMP": {
"byte": "op_byte",
"mask": 64
},
"IMM": {
"byte": "op_byte",
"mask": 128
},
"JMP": {
"byte": "op_byte",
"mask": 32
},
"LDM": {
"byte": "op_byte",
"mask": 8
},
"STK": {
"byte": "op_byte",
"mask": 4
},
"STM": {
"byte": "op_byte",
"mask": 2
},
"SYS": {
"byte": "op_byte",
"mask": 1
}
},
"register_map": {
"0": "NONE",
"1": "REG_D",
"2": "REG_B",
"4": "REG_A",
"8": "REG_S",
"16": "REG_I",
"32": "REG_F",
"64": "REG_C"
},
"syscall_bits": {
"1": "SLEEP",
"2": "READ_CODE",
"4": "OPEN",
"16": "EXIT",
"64": "WRITE",
"128": "READ_MEMORY"
},
"syscall_map": {
"1": "SLEEP",
"2": "READ_CODE",
"4": "OPEN",
"16": "EXIT",
"64": "WRITE",
"128": "READ_MEMORY"
}
}
neuland% python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis
Yan85 Bytecode Disassembly
==================================================
Total bytes: 102
Instructions: 34
0x0000 (i=0x000): 2f 04 80 | /.. | IMM REG_A = 0x2f / '/'
0x0003 (i=0x001): 00 02 80 | ... | IMM REG_B = 0x00
0x0006 (i=0x002): 04 02 02 | ... | STM memory[0x300 + REG_B] = REG_A
0x0009 (i=0x003): 66 04 80 | f.. | IMM REG_A = 0x66 / 'f'
0x000c (i=0x004): 01 02 80 | ... | IMM REG_B = 0x01
0x000f (i=0x005): 04 02 02 | ... | STM memory[0x300 + REG_B] = REG_A
0x0012 (i=0x006): 6c 04 80 | l.. | IMM REG_A = 0x6c / 'l'
0x0015 (i=0x007): 02 02 80 | ... | IMM REG_B = 0x02
0x0018 (i=0x008): 04 02 02 | ... | STM memory[0x300 + REG_B] = REG_A
0x001b (i=0x009): 61 04 80 | a.. | IMM REG_A = 0x61 / 'a'
0x001e (i=0x00a): 03 02 80 | ... | IMM REG_B = 0x03
0x0021 (i=0x00b): 04 02 02 | ... | STM memory[0x300 + REG_B] = REG_A
0x0024 (i=0x00c): 67 04 80 | g.. | IMM REG_A = 0x67 / 'g'
0x0027 (i=0x00d): 04 02 80 | ... | IMM REG_B = 0x04
0x002a (i=0x00e): 04 02 02 | ... | STM memory[0x300 + REG_B] = REG_A
0x002d (i=0x00f): 00 04 80 | ... | IMM REG_A = 0x00
0x0030 (i=0x010): 05 02 80 | ... | IMM REG_B = 0x05
0x0033 (i=0x011): 04 02 02 | ... | STM memory[0x300 + REG_B] = REG_A
0x0036 (i=0x012): 00 40 80 | .@. | IMM REG_C = 0x00
0x0039 (i=0x013): 00 04 80 | ... | IMM REG_A = 0x00
0x003c (i=0x014): 00 02 80 | ... | IMM REG_B = 0x00
0x003f (i=0x015): 40 04 01 | @.. | SYS OPEN, ret=REG_C (syscall# 0x04)
0x0042 (i=0x016): 30 02 80 | 0.. | IMM REG_B = 0x30 / '0'
0x0045 (i=0x017): 40 02 02 | @.. | STM memory[0x300 + REG_B] = REG_C
0x0048 (i=0x018): 02 04 08 | ... | LDM REG_A = memory[0x300 + REG_B]
0x004b (i=0x019): 20 02 80 | .. | IMM REG_B = 0x20 / ' '
0x004e (i=0x01a): 39 40 80 | 9@. | IMM REG_C = 0x39 / '9'
0x0051 (i=0x01b): 04 80 01 | ... | SYS READ_MEMORY, ret=REG_A (syscall# 0x80)
0x0054 (i=0x01c): 01 04 80 | ... | IMM REG_A = 0x01
0x0057 (i=0x01d): 20 02 80 | .. | IMM REG_B = 0x20 / ' '
0x005a (i=0x01e): 39 40 80 | 9@. | IMM REG_C = 0x39 / '9'
0x005d (i=0x01f): 04 40 01 | .@. | SYS WRITE, ret=REG_A (syscall# 0x40)
0x0060 (i=0x020): 2a 04 80 | *.. | IMM REG_A = 0x2a / '*'
0x0063 (i=0x021): 04 10 01 | ... | SYS EXIT, ret=REG_A (syscall# 0x10)
Disassembly complete.
Assembling and disassembling works fine, no complaints about unknown values.
neuland% python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | ssh pc "sudo strace -o /tmp/x /challenge/yansanity-hard ; echo STRACE: ; tail /tmp/x"
[+] Welcome to /challenge/yansanity-hard!
[+] This challenge is an custom emulator. It emulates a completely custom
[+] architecture that we call "Yan85"! You'll have to understand the
[+] emulator to understand the architecture, and you'll have to understand
[+] the architecture to understand the code being emulated, and you will
[+] have to understand that code to get the flag. Good luck!
[+]
[+] This level is a full Yan85 emulator. You'll have to reason about yancode,
[+] and the implications of how the emulator interprets it!
[?] This challenge is special! It randomizes the Yan85 VM based on
[?] the value of the flag. This means that there is no way for you
[?] to know the opcode and argument encodings...
[?]
[?] Keep in mind that the encoding that you observe in practice mode
[?] is going to be different than the actual encoding, because the
[?] practice mode flag is different. How will you adapt?
[?]
[?] Is there maybe a clever side channel you can utilize?
[?] ... Done! VM is randomized!
[!] This time, YOU'RE in control! Please input your yancode: pwn.college{practice}
STRACE:
write(1, "\n", 1) = 1
write(1, "[?] ... Done! VM is randomized!", 31) = 31
write(1, "\n", 1) = 1
write(1, "[!] This time, YOU'RE in control"..., 61) = 61
read(0, "/\4\200\0\2\200\4\2\2f\4\200\1\2\200\4\2\2l\4\200\2\2\200\4\2\2a\4\200\3\2"..., 768) = 103
openat(AT_FDCWD, "/flag", O_RDONLY) = 4
read(4, "pwn.college{practice}\n", 57) = 22
write(1, "pwn.college{practice}\n\0\0\0\0\0\0\0\0\0\0"..., 57) = 57
exit_group(42) = ?
+++ exited with 42 +++
Solution - overview:
date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-all ; echo EXIT CODE: $?'
date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py build-config ; echo EXIT CODE: $?'
scp pc:/tmp/\*.json .
python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis
( python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 ) | ssh pc "sudo strace -o /tmp/x /challenge/yansanity-easy ; echo STRACE: ; tail /tmp/x"
python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | ssh pc /challenge/yansanity-easy
Files:
neuland% wc -l pc-yansan*py pc-yansan*y85 *.json
790 pc-yansanity-asm.py
2038 pc-yansanity-configfinder.py
55 pc-yansanity-catflag.y85
120 pc-yansanity-asm-config.json
195 pc-yansanity-cache.json
3198 total
pc-yansanity-configfinder.py:
neuland% cat pc-yansanity-configfinder.py
#!/usr/bin/env python3
"""Yan85 helper: local probing + config JSON builder."""
import argparse
import json
import os
import socket
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from datetime import datetime
from typing import Iterable, List, Optional, Sequence, TextIO
from collections import Counter
from itertools import combinations
VALUE_POOL = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
PATH_POINTER_OFFSET = 0x00
MAX_FD = 0x0A # host only hands out a handful of descriptors (stdin=0, stdout=1, stderr=2, next fd ≤ 10)
IMM_EXIT_VALUE = 17
DEFAULT_TIMEOUT = 0.5 # seconds before a hung run is killed
POST_PAYLOAD_DELAY = 0.05
DEFAULT_FLAG_PATH = '/flag'
DEFAULT_CACHE_FILE = 'pc-yansanity-cache.json'
STAGE_SYS_EXIT = 'sys-exit'
STAGE_SYS_READMEM = 'sys-readmem'
STAGE_FIND_STM = 'find-stm'
STAGE_FIND_OPEN = 'find-open'
STAGE_FIND_WRITE = 'find-write'
STAGE_VERIFY_STMLDM = 'verify-stmldm'
DEFAULT_STDIN_BYTES = [0x41, 0x61, 0x00, 0xFF, 0xF0, 0x0F]
DEFAULT_STM_VALUES = VALUE_POOL.copy()
DEFAULT_WRITE_BYTES = [ord(ch) for ch in "::YAN85-WRITE::\n"]
DEFAULT_PROBE_PATH = '/etc/group'
DEFAULT_PROBE_TARGET = '/etc/group'
SCRATCH_OFFSET = 0x2F
REGISTER_NAMES = ['REG_A', 'REG_B', 'REG_C', 'REG_D', 'REG_S', 'REG_I', 'REG_F']
INSTRUCTION_NAMES = ['IMM', 'STK', 'ADD', 'STM', 'LDM', 'JMP', 'CMP', 'SYS']
SYSCALL_NAMES = ['WRITE', 'SLEEP', 'OPEN', 'READ_MEMORY', 'EXIT', 'READ_CODE']
FLAG_NAMES = ['LESS', 'GREATER', 'EQUAL', 'NOT_EQUAL', 'BOTH_ZERO']
# ---------------------------------------------------------------------------
# Stage helpers: SYS_EXIT probing
# ---------------------------------------------------------------------------
@dataclass
class ExecResult:
cmd: Sequence[str]
returncode: int
stdout: bytes
stderr: bytes
def build_instruction_bytes(layout: dict, *, opcode: int, arg_low: int, arg_high: int) -> bytes:
instruction = [0x00, 0x00, 0x00]
instruction[layout['op_byte']] = opcode & 0xFF
instruction[layout['arg_low']] = arg_low & 0xFF
instruction[layout['arg_high']] = arg_high & 0xFF
return bytes(instruction)
def build_two_instruction_payload(layout: dict, *, imm_opcode: int, sys_opcode: int,
register_tag: int, sys_mask: int,
imm_value: int, padding_instr: int) -> bytes:
first = build_instruction_bytes(
layout,
opcode=imm_opcode,
arg_low=imm_value,
arg_high=register_tag,
)
second = build_instruction_bytes(
layout,
opcode=sys_opcode,
arg_low=register_tag,
arg_high=sys_mask,
)
payload = first + second
payload += b"\x00" * (padding_instr * 3)
return payload
def log_line(log_handle: Optional[TextIO], text: str) -> None:
if log_handle:
log_handle.write(text + '\n')
log_handle.flush()
def read_flag_state(flag_path: Optional[str], host_label: Optional[str]):
if not flag_path:
return None, 'flag path disabled'
path = Path(flag_path).expanduser()
try:
stat_result = path.stat()
except OSError as exc:
return None, f'{exc.__class__.__name__}: {exc}'
hostname = socket.gethostname()
return {
'path': str(path),
'size': stat_result.st_size,
'mtime_ns': stat_result.st_mtime_ns,
'mtime': datetime.fromtimestamp(stat_result.st_mtime).isoformat(),
'hostname': hostname,
'host_label': host_label or hostname,
}, None
def ensure_cache_shape(data: Optional[dict]) -> dict:
if not isinstance(data, dict):
data = {}
if 'entries' not in data or not isinstance(data['entries'], dict):
data['entries'] = {}
data.setdefault('version', 1)
return data
def default_challenge_path() -> str:
hostname = socket.gethostname().lower()
if 'yansanity-easy' in hostname:
return '/challenge/yansanity-easy'
if 'yansanity-hard' in hostname:
return '/challenge/yansanity-hard'
return '/challenge/yansanity-hard'
def load_cache(cache_path: Optional[Path]) -> dict:
if cache_path is None or not cache_path.exists():
return ensure_cache_shape(None)
try:
return ensure_cache_shape(json.loads(cache_path.read_text()))
except (ValueError, OSError):
return ensure_cache_shape(None)
def build_cache_key(challenge_path: str, flag_state: Optional[dict], stage: str, *, include_mtime: bool = False) -> str:
if flag_state:
host_part = flag_state.get('host_label') or flag_state.get('hostname') or 'host:unknown'
size_part = flag_state.get('size')
flag_part = f"{host_part}|{flag_state['path']}|size:{size_part}"
if include_mtime and 'mtime_ns' in flag_state:
flag_part += f"|mtime:{flag_state['mtime_ns']}"
else:
flag_part = 'flag:unknown'
return f"{challenge_path}|{flag_part}|stage:{stage}"
def lookup_cached_result(cache_data: dict, challenge_path: str, flag_state: Optional[dict], stage: str):
key = build_cache_key(challenge_path, flag_state, stage)
entry = cache_data['entries'].get(key)
if entry:
return entry
legacy_flag_key = build_cache_key(challenge_path, flag_state, stage, include_mtime=True)
entry = cache_data['entries'].get(legacy_flag_key)
if entry:
return entry
if stage == STAGE_SYS_EXIT:
legacy_stage_key = build_cache_key(challenge_path, flag_state, 'legacy')
entry = cache_data['entries'].get(legacy_stage_key)
if entry:
return entry
legacy_stage_key = build_cache_key(challenge_path, flag_state, 'legacy', include_mtime=True)
return cache_data['entries'].get(legacy_stage_key)
return None
def store_cache_entry(cache_path: Optional[Path], cache_data: dict, challenge_path: str,
flag_state: Optional[dict], finding: dict, stage: str) -> None:
if cache_path is None:
return
key = build_cache_key(challenge_path, flag_state, stage)
payload = {
'challenge': challenge_path,
'flag_state': flag_state,
'result': {k: v for k, v in finding.items() if k != 'flag_state'},
'cached_at': datetime.now().isoformat(),
'stage': stage,
}
cache_data['entries'][key] = payload
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(json.dumps(cache_data, indent=2, sort_keys=True) + '\n')
def describe_flag_state(flag_state: Optional[dict]) -> str:
if not flag_state:
return '(flag stats unavailable)'
host_display = flag_state.get('host_label') or flag_state.get('hostname') or 'unknown-host'
return (
f"path={flag_state['path']} size={flag_state['size']} "
f"mtime_ns={flag_state['mtime_ns']} host={host_display}"
)
def gather_cache_seeds(cache_data: dict, challenge_path: str) -> dict:
seeds = {
'imm_opcode': [],
'sys_opcode': [],
'register_tag': [],
'sys_mask': [],
}
for entry in cache_data.get('entries', {}).values():
if entry.get('challenge') != challenge_path:
continue
result = entry.get('result') or {}
for key in seeds:
value = result.get(key)
if isinstance(value, int):
seeds[key].append(value)
return seeds
def prioritize_pool(preferred: List[int]) -> List[int]:
ordered = []
seen = set()
for value in preferred:
if value in VALUE_POOL and value not in seen:
ordered.append(value)
seen.add(value)
for value in VALUE_POOL:
if value not in seen:
ordered.append(value)
seen.add(value)
return ordered
def format_payload_summary(imm_opcode: int, sys_opcode: int, register_tag: int, sys_mask: int) -> str:
imm_bytes = ' '.join(
f"{byte:02x}" for byte in (
IMM_EXIT_VALUE & 0xFF,
register_tag & 0xFF,
imm_opcode & 0xFF,
)
)
sys_bytes = ' '.join(
f"{byte:02x}" for byte in (
register_tag & 0xFF,
sys_mask & 0xFF,
sys_opcode & 0xFF,
)
)
return f"{imm_bytes} : {sys_bytes} | " \
f"imm=0x{imm_opcode:02x} sys=0x{sys_opcode:02x} " \
f"reg=0x{register_tag:02x} sysmask=0x{sys_mask:02x}"
def gather_stage_values(cache_data: dict, challenge_path: str, stage: str, field_name: str) -> List[int]:
values: List[int] = []
for entry in cache_data.get('entries', {}).values():
if entry.get('challenge') != challenge_path or entry.get('stage') != stage:
continue
result = entry.get('result') or {}
value = result.get(field_name)
if isinstance(value, int):
values.append(value)
return values
def collect_stage_entry(cache_data: dict, challenge_path: str, stage: str) -> Optional[dict]:
candidates = [
entry for entry in cache_data.get('entries', {}).values()
if entry.get('challenge') == challenge_path and entry.get('stage') == stage
]
if not candidates:
return None
candidates.sort(key=lambda entry: entry.get('cached_at') or '')
return candidates[-1]
def infer_layout_tokens(layout_dict: Optional[dict]) -> List[str]:
if not layout_dict:
return ['arg_low', 'arg_high', 'op_byte']
ordered = sorted(layout_dict.items(), key=lambda item: item[1])
return [name for name, _ in ordered]
def build_permutation(names: List[str], assignments: dict):
used: set = set()
order: List[int] = []
status: dict = {}
remaining_reserved = Counter(
value for value in assignments.values() if isinstance(value, int)
)
def reserve_value(value: int) -> None:
remaining_reserved[value] -= 1
if remaining_reserved[value] <= 0:
del remaining_reserved[value]
def allocate_fallback(name: str) -> None:
fallback = next(
(
candidate
for candidate in VALUE_POOL
if candidate not in used and remaining_reserved.get(candidate, 0) == 0
),
None,
)
if fallback is None:
raise ValueError('Exhausted VALUE_POOL while building permutation')
order.append(fallback)
status[name] = 'inferred'
used.add(fallback)
for name in names:
value = assignments.get(name)
if isinstance(value, int) and remaining_reserved.get(value, 0) > 0:
if value in used:
reserve_value(value)
allocate_fallback(name)
continue
order.append(value)
status[name] = 'confirmed'
used.add(value)
reserve_value(value)
continue
allocate_fallback(name)
return order, status
def build_search_order(seed_values: List[int]) -> List[int]:
seed_unique: List[int] = []
seen = set()
for value in seed_values:
if value in VALUE_POOL and value not in seen:
seed_unique.append(value)
seen.add(value)
if seed_unique:
return seed_unique
# Fall back to prioritized full pool (seeds first, then rest)
return prioritize_pool(seed_unique)
def build_register_combos(exit_order: List[int], buf_order: List[int], len_order: List[int]) -> List[tuple]:
combos: List[tuple] = []
for reg_exit in exit_order:
for reg_buf in buf_order:
if reg_buf == reg_exit:
continue
for reg_len in len_order:
if reg_len in (reg_exit, reg_buf):
continue
combos.append((reg_exit, reg_buf, reg_len))
return combos
def build_instruction_stream(layout: dict, instructions: List[dict], padding_instr: int = 0) -> bytes:
payload = b''.join(
build_instruction_bytes(
layout,
opcode=inst['opcode'],
arg_low=inst['arg_low'],
arg_high=inst['arg_high'],
)
for inst in instructions
)
if padding_instr:
payload += b"\x00" * (padding_instr * 3)
return payload
def ensure_probe_path(probe_path: str, target: str, log_handle: Optional[TextIO]) -> bool:
path = Path(probe_path)
if path.exists():
return True
target_path = Path(target)
if not target_path.exists():
warning = f"[!] Probe target {target} does not exist; cannot create symlink {probe_path}."
print(warning)
log_line(log_handle, warning)
return False
try:
os.symlink(str(target_path), str(path))
note = f"[*] Created probe symlink {probe_path} -> {target}"
print(note)
log_line(log_handle, note)
return True
except OSError as exc:
warning = f"[!] Failed to create symlink {probe_path} -> {target}: {exc}"
print(warning)
log_line(log_handle, warning)
return False
def encode_path_bytes(path: str) -> List[int]:
encoded = path.encode('utf-8', errors='strict')
return [byte for byte in encoded] + [0x00]
def parse_byte_list(value: str) -> List[int]:
tokens = [token.strip() for token in value.replace(';', ',').split(',') if token.strip()]
if not tokens:
raise argparse.ArgumentTypeError('Expected at least one byte literal')
values: List[int] = []
for token in tokens:
number = int(token, 0)
if not 0 <= number <= 0xFF:
raise argparse.ArgumentTypeError(f'Byte literal out of range: {token}')
values.append(number & 0xFF)
return values
def run_challenge(payload: bytes, *, challenge_path: str, log_handle: Optional[TextIO],
post_payload_input: bytes = b'') -> ExecResult:
cmd = [challenge_path]
if not post_payload_input:
try:
proc = subprocess.run(
cmd,
input=payload + b'\n',
capture_output=True,
check=False,
timeout=DEFAULT_TIMEOUT,
)
return ExecResult(cmd, proc.returncode, proc.stdout, proc.stderr)
except subprocess.TimeoutExpired as exc:
message = f'[!] Timeout ({DEFAULT_TIMEOUT}s) waiting for process, killing...'
print(message)
log_line(log_handle, message)
return ExecResult(cmd, -1, exc.stdout or b'', exc.stderr or b'')
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
try:
assert proc.stdin is not None
proc.stdin.write(payload + b'\n')
proc.stdin.flush()
time.sleep(POST_PAYLOAD_DELAY)
proc.stdin.write(post_payload_input)
proc.stdin.flush()
proc.stdin.close()
stdout, stderr = proc.communicate(timeout=DEFAULT_TIMEOUT)
return ExecResult(cmd, proc.returncode, stdout, stderr)
except subprocess.TimeoutExpired:
proc.kill()
stdout, stderr = proc.communicate()
message = f'[!] Timeout ({DEFAULT_TIMEOUT}s) waiting for process, killing...'
print(message)
log_line(log_handle, message)
return ExecResult(cmd, -1, stdout, stderr)
except BrokenPipeError:
stdout, stderr = proc.communicate()
message = '[!] Broken pipe while feeding stdin (process exited early).'
print(message)
log_line(log_handle, message)
return ExecResult(cmd, proc.returncode if proc.returncode is not None else -1, stdout, stderr)
def mode_sys_exit(args):
print('[*] Local IMM+SYS sweep (looking for host exit matching immediate value)...')
attempt = 0
log_handle: Optional[TextIO] = None
cache_path: Optional[Path] = None
cache_data: dict = ensure_cache_shape(None)
flag_state: Optional[dict] = None
log_path_str: Optional[str] = None
if args.log_file == '':
log_path_str = None
elif args.log_file:
log_path_str = args.log_file
else:
log_path_str = f"pc-yansanity-configfinder.log-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
if log_path_str:
log_path = Path(log_path_str).expanduser()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_handle = log_path.open('a')
log_line(log_handle, f"--- sys-exit run {datetime.now().isoformat()} ---")
log_line(log_handle, f"challenge={args.challenge} imm=0x{IMM_EXIT_VALUE:02x} timeout={DEFAULT_TIMEOUT}s")
if args.cache_file not in (None, ''):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
flag_state, flag_error = read_flag_state(args.flag_path, args.host_label)
if flag_error and args.flag_path:
warning = f"[!] Could not stat flag path {args.flag_path}: {flag_error}"
print(warning)
log_line(log_handle, warning)
else:
note = f"[*] Flag signature: {describe_flag_state(flag_state)}"
print(note)
log_line(log_handle, note)
if cache_path:
cached_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_EXIT)
if cached_entry:
print('[+] Cache hit for current challenge/flag combo:')
print(json.dumps(cached_entry['result'], indent=2))
result_summary = format_payload_summary(
cached_entry['result']['imm_opcode'],
cached_entry['result']['sys_opcode'],
cached_entry['result']['register_tag'],
cached_entry['result']['sys_mask'],
)
print('[+] Winning payload:', result_summary)
log_line(log_handle, '[+] Cache hit, returning cached result')
log_line(log_handle, '[+] Winning payload: ' + result_summary)
return cached_entry['result']
layout = {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}
cache_seeds = gather_cache_seeds(cache_data, args.challenge)
imm_order = build_search_order(cache_seeds['imm_opcode'])
sys_order = build_search_order(cache_seeds['sys_opcode'])
reg_order = build_search_order(cache_seeds['register_tag'])
mask_order = build_search_order(cache_seeds['sys_mask'])
total_attempts = (
max(len(imm_order), 1)
* max(len(sys_order), 1)
* max(len(reg_order), 1)
* max(len(mask_order), 1)
)
for imm_opcode in imm_order:
for sys_opcode in sys_order:
for register_tag in reg_order:
for sys_mask in mask_order:
payload = build_two_instruction_payload(
layout,
imm_opcode=imm_opcode,
sys_opcode=sys_opcode,
register_tag=register_tag,
sys_mask=sys_mask,
imm_value=IMM_EXIT_VALUE,
padding_instr=args.padding_instr,
)
result = run_challenge(payload, challenge_path=args.challenge, log_handle=log_handle)
attempt += 1
progress = (attempt * 100) // total_attempts
imm_bytes = ' '.join(f"{byte:02x}" for byte in payload[:3])
sys_bytes = ' '.join(f"{byte:02x}" for byte in payload[3:6])
summary = (
f"[{progress:3d}%] {imm_bytes} : {sys_bytes} | "
f"imm=0x{imm_opcode:02x} sys=0x{sys_opcode:02x} "
f"reg=0x{register_tag:02x} sysmask=0x{sys_mask:02x} rc={result.returncode}"
)
print(summary)
log_line(log_handle, summary)
if args.verbose:
stdout_text = result.stdout.decode('utf-8', errors='replace')
stderr_text = result.stderr.decode('utf-8', errors='replace')
if stdout_text.strip():
print('--- STDOUT ---')
print(stdout_text.rstrip())
log_line(log_handle, '--- STDOUT ---')
log_line(log_handle, stdout_text.rstrip())
if stderr_text.strip():
print('--- STDERR ---')
print(stderr_text.rstrip())
log_line(log_handle, '--- STDERR ---')
log_line(log_handle, stderr_text.rstrip())
if stdout_text.strip() or stderr_text.strip():
print('--------------')
log_line(log_handle, '--------------')
if result.returncode == IMM_EXIT_VALUE:
finding = {
'layout': layout,
'imm_opcode': imm_opcode,
'sys_opcode': sys_opcode,
'register_tag': register_tag,
'sys_mask': sys_mask,
'immediate_value': IMM_EXIT_VALUE,
'flag_state': flag_state,
}
print('[+] Found SYS_EXIT combination:', finding)
log_line(log_handle, '[+] Found SYS_EXIT combination: ' + json.dumps(finding))
store_cache_entry(cache_path, cache_data, args.challenge, flag_state, finding, STAGE_SYS_EXIT)
if log_handle:
log_handle.close()
return finding
print('[!] Exhausted search space without matching exit code.')
log_line(log_handle, '[!] Exhausted search space without matching exit code.')
if log_handle:
log_handle.close()
return None
def mode_sys_readmem(args):
print('[*] SYS_READ_MEMORY sweep (stdin bytes -> exit)...')
attempt = 0
log_handle: Optional[TextIO] = None
cache_path: Optional[Path] = None
cache_data: dict = ensure_cache_shape(None)
flag_state: Optional[dict] = None
log_path_str: Optional[str] = None
if args.log_file == '':
log_path_str = None
elif args.log_file:
log_path_str = args.log_file
else:
log_path_str = f"pc-yansanity-configfinder.log-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
if log_path_str:
log_path = Path(log_path_str).expanduser()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_handle = log_path.open('a')
log_line(log_handle, f"--- sys-readmem run {datetime.now().isoformat()} ---")
log_line(
log_handle,
f"challenge={args.challenge} stdin_bytes={str_list(args.stdin_bytes or DEFAULT_STDIN_BYTES)} "
f"timeout={DEFAULT_TIMEOUT}s",
)
if args.cache_file not in (None, ''):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
flag_state, flag_error = read_flag_state(args.flag_path, args.host_label)
if flag_error and args.flag_path:
warning = f"[!] Could not stat flag path {args.flag_path}: {flag_error}"
print(warning)
log_line(log_handle, warning)
else:
note = f"[*] Flag signature: {describe_flag_state(flag_state)}"
print(note)
log_line(log_handle, note)
exit_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_EXIT)
if not exit_entry:
message = '[!] Missing SYS_EXIT cache entry. Run sys-exit stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
exit_result = exit_entry['result']
exit_mask = exit_result['sys_mask']
exit_reg_seed = exit_result['register_tag']
imm_seed = exit_result['imm_opcode']
sys_seed = exit_result['sys_opcode']
layout = {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}
stdin_values = args.stdin_bytes or DEFAULT_STDIN_BYTES
fd_imm = args.fd_imm & 0xFF
buf_imm = args.buf_imm & 0xFF
len_imm = args.len_imm & 0xFF
imm_order = build_search_order([imm_seed] + gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'imm_opcode'))
sys_order = build_search_order([sys_seed] + gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'sys_opcode'))
ldm_order = build_search_order(gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'ldm_opcode'))
read_mask_order = build_search_order(gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'sys_mask_read'))
reg_exit_order = build_search_order([exit_reg_seed] + gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'register_exit'))
reg_buf_order = build_search_order(gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'register_buf'))
reg_len_order = build_search_order(gather_stage_values(cache_data, args.challenge, STAGE_SYS_READMEM, 'register_len'))
if not ldm_order:
ldm_order = prioritize_pool(VALUE_POOL)
if not read_mask_order:
read_mask_order = prioritize_pool(VALUE_POOL)
if not reg_exit_order:
reg_exit_order = prioritize_pool(VALUE_POOL)
if not reg_buf_order:
reg_buf_order = prioritize_pool(VALUE_POOL)
if not reg_len_order:
reg_len_order = prioritize_pool(VALUE_POOL)
reg_combos = build_register_combos(reg_exit_order, reg_buf_order, reg_len_order)
if not reg_combos:
reg_combos = build_register_combos(VALUE_POOL, VALUE_POOL, VALUE_POOL)
total_attempts = (
max(len(imm_order), 1)
* max(len(sys_order), 1)
* max(len(ldm_order), 1)
* max(len(read_mask_order), 1)
* max(len(reg_combos), 1)
)
if total_attempts == 0:
total_attempts = 1
for imm_opcode in imm_order:
for sys_opcode in sys_order:
for ldm_opcode in ldm_order:
for read_mask in read_mask_order:
for reg_exit, reg_buf, reg_len in reg_combos:
attempt += 1
progress = (attempt * 100) // total_attempts
instructions = [
{'opcode': imm_opcode, 'arg_low': fd_imm, 'arg_high': reg_exit},
{'opcode': imm_opcode, 'arg_low': buf_imm, 'arg_high': reg_buf},
{'opcode': imm_opcode, 'arg_low': len_imm, 'arg_high': reg_len},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': read_mask},
{'opcode': ldm_opcode, 'arg_low': reg_buf, 'arg_high': reg_exit},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': exit_mask},
]
payload = build_instruction_stream(layout, instructions, args.padding_instr)
all_match = True
for test_value in stdin_values:
stdin_bytes = bytes([test_value & 0xFF])
result = run_challenge(
payload,
challenge_path=args.challenge,
log_handle=log_handle,
post_payload_input=stdin_bytes,
)
summary = (
f"[{progress:3d}%] imm=0x{imm_opcode:02x} sys=0x{sys_opcode:02x} "
f"ldm=0x{ldm_opcode:02x} readmask=0x{read_mask:02x} "
f"regs(exit={reg_exit:02x},buf={reg_buf:02x},len={reg_len:02x}) "
f"stdin=0x{test_value & 0xFF:02x} rc={result.returncode}"
)
print(summary)
log_line(log_handle, summary)
if args.verbose:
stdout_text = result.stdout.decode('utf-8', errors='replace')
stderr_text = result.stderr.decode('utf-8', errors='replace')
if stdout_text.strip():
print('--- STDOUT ---')
print(stdout_text.rstrip())
log_line(log_handle, '--- STDOUT ---')
log_line(log_handle, stdout_text.rstrip())
if stderr_text.strip():
print('--- STDERR ---')
print(stderr_text.rstrip())
log_line(log_handle, '--- STDERR ---')
log_line(log_handle, stderr_text.rstrip())
if stdout_text.strip() or stderr_text.strip():
print('--------------')
log_line(log_handle, '--------------')
if result.returncode != (test_value & 0xFF):
all_match = False
break
if not all_match:
continue
finding = {
'layout': layout,
'imm_opcode': imm_opcode,
'sys_opcode': sys_opcode,
'ldm_opcode': ldm_opcode,
'register_exit': reg_exit,
'register_buf': reg_buf,
'register_len': reg_len,
'sys_mask_read': read_mask,
'sys_mask_exit': exit_mask,
'stdin_test_values': stdin_values,
'immediate_fd': fd_imm,
'immediate_buf': buf_imm,
'immediate_len': len_imm,
'flag_state': flag_state,
}
print('[+] Found SYS_READ_MEMORY configuration:', finding)
log_line(log_handle, '[+] Found SYS_READ_MEMORY configuration: ' + json.dumps(finding))
store_cache_entry(cache_path, cache_data, args.challenge, flag_state, finding, STAGE_SYS_READMEM)
if log_handle:
log_handle.close()
return finding
message = '[!] Exhausted search space without matching stdin echo.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
def mode_find_stm(args):
print('[*] STM validation (write -> read -> exit)...')
attempt = 0
log_handle: Optional[TextIO] = None
cache_path: Optional[Path] = None
cache_data: dict = ensure_cache_shape(None)
flag_state: Optional[dict] = None
log_path_str: Optional[str] = None
if args.log_file == '':
log_path_str = None
elif args.log_file:
log_path_str = args.log_file
else:
log_path_str = f"pc-yansanity-configfinder.log-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
if log_path_str:
log_path = Path(log_path_str).expanduser()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_handle = log_path.open('a')
log_line(log_handle, f"--- find-stm run {datetime.now().isoformat()} ---")
log_line(
log_handle,
f"challenge={args.challenge} test_values={str_list(args.test_values or DEFAULT_STM_VALUES)} "
f"timeout={DEFAULT_TIMEOUT}s",
)
if args.cache_file not in (None, ''):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
flag_state, flag_error = read_flag_state(args.flag_path, args.host_label)
if flag_error and args.flag_path:
warning = f"[!] Could not stat flag path {args.flag_path}: {flag_error}"
print(warning)
log_line(log_handle, warning)
else:
note = f"[*] Flag signature: {describe_flag_state(flag_state)}"
print(note)
log_line(log_handle, note)
exit_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_EXIT)
if not exit_entry:
message = '[!] Missing SYS_EXIT cache entry. Run sys-exit stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
readmem_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_READMEM)
if not readmem_entry:
message = '[!] Missing SYS_READMEM cache entry. Run sys-readmem stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
exit_result = exit_entry['result']
readmem_result = readmem_entry['result']
layout = exit_result.get('layout') or {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}
imm_opcode = exit_result['imm_opcode']
sys_opcode = exit_result['sys_opcode']
exit_mask = exit_result['sys_mask']
reg_exit = exit_result['register_tag']
reg_buf = readmem_result['register_buf']
ldm_opcode = readmem_result['ldm_opcode']
excluded_opcodes = {imm_opcode, sys_opcode, ldm_opcode}
stm_seeds = [
value for value in gather_stage_values(cache_data, args.challenge, STAGE_FIND_STM, 'stm_opcode')
if value not in excluded_opcodes
]
stm_order = [value for value in build_search_order(stm_seeds) if value not in excluded_opcodes]
if not stm_order:
candidate_pool = [value for value in VALUE_POOL if value not in excluded_opcodes]
stm_order = prioritize_pool(candidate_pool)
test_values = args.test_values or DEFAULT_STM_VALUES
offset_imm = args.offset_imm & 0xFF
total_attempts = max(len(stm_order), 1)
for stm_opcode in stm_order:
if stm_opcode in excluded_opcodes:
continue
attempt += 1
progress = (attempt * 100) // total_attempts
base_instructions = [
{'opcode': imm_opcode, 'arg_low': 0x00, 'arg_high': reg_exit}, # placeholder for test value
{'opcode': imm_opcode, 'arg_low': offset_imm, 'arg_high': reg_buf},
{'opcode': stm_opcode, 'arg_low': reg_exit, 'arg_high': reg_buf},
{'opcode': imm_opcode, 'arg_low': 0x00, 'arg_high': reg_exit},
{'opcode': ldm_opcode, 'arg_low': reg_buf, 'arg_high': reg_exit},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': exit_mask},
]
all_match = True
for test_value in test_values:
instructions = base_instructions.copy()
instructions[0] = {
'opcode': imm_opcode,
'arg_low': test_value & 0xFF,
'arg_high': reg_exit,
}
payload = build_instruction_stream(layout, instructions, args.padding_instr)
result = run_challenge(payload, challenge_path=args.challenge, log_handle=log_handle)
summary = (
f"[{progress:3d}%] stm=0x{stm_opcode:02x} test=0x{test_value & 0xFF:02x} "
f"rc={result.returncode}"
)
print(summary)
log_line(log_handle, summary)
if args.verbose:
stdout_text = result.stdout.decode('utf-8', errors='replace')
stderr_text = result.stderr.decode('utf-8', errors='replace')
if stdout_text.strip():
print('--- STDOUT ---')
print(stdout_text.rstrip())
log_line(log_handle, '--- STDOUT ---')
log_line(log_handle, stdout_text.rstrip())
if stderr_text.strip():
print('--- STDERR ---')
print(stderr_text.rstrip())
log_line(log_handle, '--- STDERR ---')
log_line(log_handle, stderr_text.rstrip())
if stdout_text.strip() or stderr_text.strip():
print('--------------')
log_line(log_handle, '--------------')
if result.returncode != (test_value & 0xFF):
all_match = False
break
if not all_match:
continue
if stm_opcode in excluded_opcodes:
continue
finding = {
'layout': layout,
'imm_opcode': imm_opcode,
'sys_opcode': sys_opcode,
'ldm_opcode': ldm_opcode,
'stm_opcode': stm_opcode,
'register_exit': reg_exit,
'register_buf': reg_buf,
'sys_mask_exit': exit_mask,
'offset_immediate': offset_imm,
'test_values': test_values,
'flag_state': flag_state,
}
print('[+] Found STM configuration:', finding)
log_line(log_handle, '[+] Found STM configuration: ' + json.dumps(finding))
store_cache_entry(cache_path, cache_data, args.challenge, flag_state, finding, STAGE_FIND_STM)
if log_handle:
log_handle.close()
return finding
message = '[!] Exhausted STM opcode search without matching exit codes.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
def mode_verify_stmldm(args):
print('[*] STM/LDM verification (write -> read -> exit)...')
attempt = 0
log_handle: Optional[TextIO] = None
cache_path: Optional[Path] = None
cache_data: dict = ensure_cache_shape(None)
flag_state: Optional[dict] = None
log_path_str: Optional[str] = None
if args.log_file == '':
log_path_str = None
elif args.log_file:
log_path_str = args.log_file
else:
log_path_str = f"pc-yansanity-configfinder.log-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
if log_path_str:
log_path = Path(log_path_str).expanduser()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_handle = log_path.open('a')
log_line(log_handle, f"--- verify-stmldm run {datetime.now().isoformat()} ---")
log_line(
log_handle,
f"challenge={args.challenge} test_values={str_list(args.stdin_bytes or DEFAULT_STDIN_BYTES)} "
f"timeout={DEFAULT_TIMEOUT}s",
)
if args.cache_file not in (None, ''):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
flag_state, flag_error = read_flag_state(args.flag_path, args.host_label)
if flag_error and args.flag_path:
warning = f"[!] Could not stat flag path {args.flag_path}: {flag_error}"
print(warning)
log_line(log_handle, warning)
else:
note = f"[*] Flag signature: {describe_flag_state(flag_state)}"
print(note)
log_line(log_handle, note)
exit_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_EXIT)
if not exit_entry:
message = '[!] Missing SYS_EXIT cache entry. Run sys-exit stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
readmem_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_READMEM)
if not readmem_entry:
message = '[!] Missing SYS_READMEM cache entry. Run sys-readmem stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
stm_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_FIND_STM)
if not stm_entry:
message = '[!] Missing STM cache entry. Run find-stm stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
exit_result = exit_entry['result']
readmem_result = readmem_entry['result']
stm_result = stm_entry['result']
layout = exit_result.get('layout') or {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}
imm_opcode = exit_result['imm_opcode']
sys_opcode = exit_result['sys_opcode']
stm_opcode = stm_result['stm_opcode']
ldm_opcode = readmem_result['ldm_opcode']
exit_mask = exit_result['sys_mask']
reg_exit = exit_result['register_tag']
reg_buf = readmem_result['register_buf']
test_values = args.stdin_bytes or DEFAULT_STDIN_BYTES
offsets = [(PATH_POINTER_OFFSET + idx * 0x08) & 0xFF for idx in range(len(DEFAULT_STDIN_BYTES))]
for offset in offsets:
for test_value in test_values:
attempt += 1
instructions = [
{'opcode': imm_opcode, 'arg_low': test_value & 0xFF, 'arg_high': reg_exit},
{'opcode': imm_opcode, 'arg_low': offset, 'arg_high': reg_buf},
{'opcode': stm_opcode, 'arg_low': reg_exit, 'arg_high': reg_buf},
{'opcode': imm_opcode, 'arg_low': 0x00, 'arg_high': reg_exit},
{'opcode': imm_opcode, 'arg_low': offset, 'arg_high': reg_buf},
{'opcode': ldm_opcode, 'arg_low': reg_buf, 'arg_high': reg_exit},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': exit_mask},
]
payload = build_instruction_stream(layout, instructions, args.padding_instr)
grouped = ' : '.join(
' '.join(f"{byte:02x}" for byte in payload[i:i + 3])
for i in range(0, len(payload), 3)
)
if args.verbose:
print('--- PAYLOAD ---')
print(grouped)
log_line(log_handle, '--- PAYLOAD ---')
log_line(log_handle, grouped)
result = run_challenge(payload, challenge_path=args.challenge, log_handle=log_handle)
summary = (
f"[{attempt:02d}] offset=0x{offset:02x} test=0x{test_value & 0xFF:02x} "
f"rc=0x{result.returncode & 0xFF:02x}"
)
print(summary)
log_line(log_handle, summary)
if args.verbose:
stdout_text = result.stdout.decode('utf-8', errors='replace')
stderr_text = result.stderr.decode('utf-8', errors='replace')
if stdout_text.strip():
print('--- STDOUT ---')
print(stdout_text.rstrip())
log_line(log_handle, '--- STDOUT ---')
log_line(log_handle, stdout_text.rstrip())
if stderr_text.strip():
print('--- STDERR ---')
print(stderr_text.rstrip())
log_line(log_handle, '--- STDERR ---')
log_line(log_handle, stderr_text.rstrip())
if stdout_text.strip() or stderr_text.strip():
print('--------------')
log_line(log_handle, '--------------')
if result.returncode != (test_value & 0xFF):
message = (
f"[!] STM/LDM mismatch at offset 0x{offset:02x}: expected 0x{test_value & 0xFF:02x} "
f"got 0x{result.returncode & 0xFF:02x}"
)
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
finding = {
'test_values': test_values,
'offsets': offsets,
'register_exit': reg_exit,
'register_buf': reg_buf,
'imm_opcode': imm_opcode,
'stm_opcode': stm_opcode,
'ldm_opcode': ldm_opcode,
'sys_opcode': sys_opcode,
'sys_mask_exit': exit_mask,
'flag_state': flag_state,
}
print('[+] STM/LDM verified across offsets:', finding)
log_line(log_handle, '[+] STM/LDM verified: ' + json.dumps(finding))
store_cache_entry(cache_path, cache_data, args.challenge, flag_state, finding, STAGE_VERIFY_STMLDM)
if log_handle:
log_handle.close()
return finding
def mode_find_open(args):
print('[*] SYS_OPEN sweep (fd exit check)...')
attempt = 0
log_handle: Optional[TextIO] = None
cache_path: Optional[Path] = None
cache_data: dict = ensure_cache_shape(None)
flag_state: Optional[dict] = None
log_path_str: Optional[str] = None
if args.log_file == '':
log_path_str = None
elif args.log_file:
log_path_str = args.log_file
else:
log_path_str = f"pc-yansanity-configfinder.log-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
if log_path_str:
log_path = Path(log_path_str).expanduser()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_handle = log_path.open('a')
log_line(log_handle, f"--- find-open run {datetime.now().isoformat()} ---")
log_line(
log_handle,
f"challenge={args.challenge} timeout={DEFAULT_TIMEOUT}s",
)
if args.cache_file not in (None, ''):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
flag_state, flag_error = read_flag_state(args.flag_path, args.host_label)
if flag_error and args.flag_path:
warning = f"[!] Could not stat flag path {args.flag_path}: {flag_error}"
print(warning)
log_line(log_handle, warning)
else:
note = f"[*] Flag signature: {describe_flag_state(flag_state)}"
print(note)
log_line(log_handle, note)
exit_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_EXIT)
if not exit_entry:
message = '[!] Missing SYS_EXIT cache entry. Run sys-exit stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
readmem_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_READMEM)
if not readmem_entry:
message = '[!] Missing SYS_READMEM cache entry. Run sys-readmem stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
stm_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_FIND_STM)
if not stm_entry:
message = '[!] Missing STM cache entry. Run find-stm stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
exit_result = exit_entry['result']
readmem_result = readmem_entry['result']
stm_result = stm_entry['result']
layout = exit_result.get('layout') or {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}
imm_opcode = exit_result['imm_opcode']
sys_opcode = exit_result['sys_opcode']
exit_mask = exit_result['sys_mask']
reg_exit = exit_result['register_tag']
reg_buf = readmem_result['register_buf']
reg_len = readmem_result['register_len']
ldm_opcode = readmem_result['ldm_opcode']
stm_opcode = stm_result['stm_opcode']
read_mask = readmem_result['sys_mask_read']
pointer_offsets = [PATH_POINTER_OFFSET]
flag_candidates = [0x00] + VALUE_POOL
mode_candidates = [0x00] + VALUE_POOL
register_pool = [reg_exit, reg_buf, reg_len]
role_combos: List[tuple] = []
seen_roles: set = set()
for ptr in register_pool:
for flags_reg in register_pool:
if flags_reg == ptr:
continue
for mode_reg in register_pool:
if mode_reg in (ptr, flags_reg):
continue
role = (ptr, flags_reg, mode_reg)
if role in seen_roles:
continue
seen_roles.add(role)
role_combos.append(role)
probe_path = DEFAULT_PROBE_PATH
if not ensure_probe_path(probe_path, DEFAULT_PROBE_TARGET, log_handle):
if log_handle:
log_handle.close()
return None
path_bytes = encode_path_bytes(probe_path)
open_seeds = gather_stage_values(cache_data, args.challenge, STAGE_FIND_OPEN, 'sys_mask_open')
excluded = {exit_mask, read_mask}
base_candidates = [value for value in VALUE_POOL if value not in excluded]
def unique_order(values, allowed: Optional[set] = None) -> List[int]:
seen: set = set()
order: List[int] = []
allowed = allowed or set(range(0x100))
for value in values:
if value not in allowed:
continue
if value in seen:
continue
order.append(value)
seen.add(value)
return order
base_allowed = set(base_candidates)
seed_order = unique_order(open_seeds, base_allowed)
remaining_candidates = [value for value in base_candidates if value not in seed_order]
open_order = seed_order + remaining_candidates
if args.verbose and open_order:
order_display = ', '.join(f"0x{value:02x}" for value in open_order)
print(f"[*] SYS_OPEN mask order: {order_display}")
log_line(log_handle, f"SYS_OPEN order: {order_display}")
total_attempts = (
max(len(open_order), 1)
* max(len(pointer_offsets), 1)
* max(len(flag_candidates), 1)
* max(len(mode_candidates), 1)
* max(len(role_combos), 1)
)
for path_offset in pointer_offsets:
path_setup: List[dict] = []
for idx, value in enumerate(path_bytes):
offset = (path_offset + idx) & 0xFF
path_setup.extend([
{'opcode': imm_opcode, 'arg_low': value, 'arg_high': reg_exit},
{'opcode': imm_opcode, 'arg_low': offset, 'arg_high': reg_buf},
{'opcode': stm_opcode, 'arg_low': reg_exit, 'arg_high': reg_buf},
])
for ptr_reg, flags_reg, mode_reg in role_combos:
for flag_value in flag_candidates:
for mode_value in mode_candidates:
instructions = path_setup + [
{'opcode': imm_opcode, 'arg_low': path_offset, 'arg_high': ptr_reg},
{'opcode': imm_opcode, 'arg_low': flag_value & 0xFF, 'arg_high': flags_reg},
{'opcode': imm_opcode, 'arg_low': mode_value & 0xFF, 'arg_high': mode_reg},
{'opcode': sys_opcode, 'arg_low': ptr_reg, 'arg_high': 0x00},
{'opcode': imm_opcode, 'arg_low': SCRATCH_OFFSET & 0xFF, 'arg_high': reg_buf},
{'opcode': stm_opcode, 'arg_low': ptr_reg, 'arg_high': reg_buf},
{'opcode': ldm_opcode, 'arg_low': reg_buf, 'arg_high': reg_exit},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': exit_mask},
]
open_mask_index = len(instructions) - 5
for open_mask in open_order:
attempt += 1
progress = (attempt * 100) // total_attempts
instructions[open_mask_index]['arg_high'] = open_mask
payload = build_instruction_stream(layout, instructions, args.padding_instr)
grouped = ' : '.join(
' '.join(f"{byte:02x}" for byte in payload[i:i + 3])
for i in range(0, len(payload), 3)
)
if args.verbose:
print('--- PAYLOAD ---')
print(grouped)
log_line(log_handle, '--- PAYLOAD ---')
log_line(log_handle, grouped)
result = run_challenge(payload, challenge_path=args.challenge, log_handle=log_handle)
summary = (
f"[{progress:3d}%] path={probe_path} ptr=0x{path_offset:02x} open=0x{open_mask:02x} "
f"flags=0x{flag_value:02x}@0x{flags_reg:02x} mode=0x{mode_value:02x}@0x{mode_reg:02x} "
f"ptr_reg=0x{ptr_reg:02x} rc={result.returncode} stdout_len={len(result.stdout)}"
)
print(summary)
log_line(log_handle, summary)
if args.verbose:
stdout_text = result.stdout.decode('utf-8', errors='replace')
stderr_text = result.stderr.decode('utf-8', errors='replace')
if stdout_text.strip():
print('--- STDOUT ---')
print(stdout_text.rstrip())
log_line(log_handle, '--- STDOUT ---')
log_line(log_handle, stdout_text.rstrip())
if stderr_text.strip():
print('--- STDERR ---')
print(stderr_text.rstrip())
log_line(log_handle, '--- STDERR ---')
log_line(log_handle, stderr_text.rstrip())
if stdout_text.strip() or stderr_text.strip():
print('--------------')
log_line(log_handle, '--------------')
fd = result.returncode
recent_values = {
flag_value & 0xFF,
mode_value & 0xFF,
path_offset & 0xFF,
ptr_reg & 0xFF,
}
fd_ok = 3 <= fd <= MAX_FD and (fd & 0xFF) not in recent_values
if not fd_ok:
continue
finding = {
'layout': layout,
'imm_opcode': imm_opcode,
'sys_opcode': sys_opcode,
'stm_opcode': stm_opcode,
'ldm_opcode': ldm_opcode,
'register_exit': reg_exit,
'register_buf': reg_buf,
'register_len': reg_len,
'sys_mask_exit': exit_mask,
'sys_mask_read': read_mask,
'sys_mask_open': open_mask,
'probe_path': probe_path,
'flag_value': flag_value & 0xFF,
'mode_value': mode_value & 0xFF,
'ptr_register': ptr_reg,
'flags_register': flags_reg,
'mode_register': mode_reg,
'flag_state': flag_state,
}
print('[+] Found SYS_OPEN configuration:', finding)
log_line(log_handle, '[+] Found SYS_OPEN configuration: ' + json.dumps(finding))
store_cache_entry(cache_path, cache_data, args.challenge, flag_state, finding, STAGE_FIND_OPEN)
if log_handle:
log_handle.close()
return finding
message = '[!] Exhausted SYS_OPEN search without success.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
def mode_find_write(args):
print('[*] SYS_WRITE sweep (stdout marker check)...')
attempt = 0
log_handle: Optional[TextIO] = None
cache_path: Optional[Path] = None
cache_data: dict = ensure_cache_shape(None)
flag_state: Optional[dict] = None
log_path_str: Optional[str] = None
if args.log_file == '':
log_path_str = None
elif args.log_file:
log_path_str = args.log_file
else:
log_path_str = f"pc-yansanity-configfinder.log-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
if log_path_str:
log_path = Path(log_path_str).expanduser()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_handle = log_path.open('a')
log_line(log_handle, f"--- find-write run {datetime.now().isoformat()} ---")
log_line(
log_handle,
f"challenge={args.challenge} timeout={DEFAULT_TIMEOUT}s",
)
if args.cache_file not in (None, ''):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
flag_state, flag_error = read_flag_state(args.flag_path, args.host_label)
if flag_error and args.flag_path:
warning = f"[!] Could not stat flag path {args.flag_path}: {flag_error}"
print(warning)
log_line(log_handle, warning)
else:
note = f"[*] Flag signature: {describe_flag_state(flag_state)}"
print(note)
log_line(log_handle, note)
exit_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_EXIT)
if not exit_entry:
message = '[!] Missing SYS_EXIT cache entry. Run sys-exit stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
readmem_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_SYS_READMEM)
if not readmem_entry:
message = '[!] Missing SYS_READMEM cache entry. Run sys-readmem stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
stm_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_FIND_STM)
if not stm_entry:
message = '[!] Missing STM cache entry. Run find-stm stage first.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
open_entry = lookup_cached_result(cache_data, args.challenge, flag_state, STAGE_FIND_OPEN)
exit_result = exit_entry['result']
readmem_result = readmem_entry['result']
stm_result = stm_entry['result']
layout = exit_result.get('layout') or {'arg_low': 0, 'arg_high': 1, 'op_byte': 2}
imm_opcode = exit_result['imm_opcode']
sys_opcode = exit_result['sys_opcode']
exit_mask = exit_result['sys_mask']
reg_exit = exit_result['register_tag']
reg_buf = readmem_result['register_buf']
reg_len = readmem_result['register_len']
ldm_opcode = readmem_result['ldm_opcode']
stm_opcode = stm_result['stm_opcode']
read_mask = readmem_result['sys_mask_read']
open_mask = None
if open_entry:
open_mask = open_entry['result'].get('sys_mask_open')
write_bytes = args.write_bytes or DEFAULT_WRITE_BYTES
write_offset = args.write_offset & 0xFF
write_len = args.write_len if args.write_len is not None else len(write_bytes)
write_len &= 0xFF
fd_value = args.write_fd & 0xFF
marker = bytes(write_bytes)
data_setup: List[dict] = []
for idx, value in enumerate(write_bytes):
offset = (write_offset + idx) & 0xFF
data_setup.extend([
{'opcode': imm_opcode, 'arg_low': value & 0xFF, 'arg_high': reg_exit},
{'opcode': imm_opcode, 'arg_low': offset, 'arg_high': reg_buf},
{'opcode': stm_opcode, 'arg_low': reg_exit, 'arg_high': reg_buf},
])
instructions = data_setup + [
{'opcode': imm_opcode, 'arg_low': fd_value, 'arg_high': reg_exit},
{'opcode': imm_opcode, 'arg_low': write_offset, 'arg_high': reg_buf},
{'opcode': imm_opcode, 'arg_low': write_len, 'arg_high': reg_len},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': 0x00},
{'opcode': imm_opcode, 'arg_low': 0x00, 'arg_high': reg_exit},
{'opcode': sys_opcode, 'arg_low': reg_exit, 'arg_high': exit_mask},
]
write_mask_index = len(instructions) - 3
write_seeds = gather_stage_values(cache_data, args.challenge, STAGE_FIND_WRITE, 'sys_mask_write')
excluded = {exit_mask, read_mask}
if open_mask is not None:
excluded.add(open_mask)
base_candidates = [value for value in VALUE_POOL if value not in excluded]
def unique_order(values, allowed: Optional[set] = None) -> List[int]:
seen: set = set()
order: List[int] = []
allowed = allowed or set(range(0x100))
for value in values:
if value not in allowed:
continue
if value in seen:
continue
order.append(value)
seen.add(value)
return order
base_allowed = set(base_candidates)
seed_order = unique_order(write_seeds, base_allowed)
remaining = [value for value in base_candidates if value not in seed_order]
write_order = seed_order + remaining
total_attempts = max(len(write_order), 1)
if args.verbose and write_order:
order_display = ', '.join(f"0x{value:02x}" for value in write_order)
print(f"[*] SYS_WRITE mask order: {order_display}")
log_line(log_handle, f"SYS_WRITE order: {order_display}")
for write_mask in write_order or [0]:
attempt += 1
progress = (attempt * 100) // total_attempts
instructions[write_mask_index]['arg_high'] = write_mask
payload = build_instruction_stream(layout, instructions, args.padding_instr)
grouped = ' : '.join(
' '.join(f"{byte:02x}" for byte in payload[i:i + 3])
for i in range(0, len(payload), 3)
)
if args.verbose:
print('--- PAYLOAD ---')
print(grouped)
log_line(log_handle, '--- PAYLOAD ---')
log_line(log_handle, grouped)
result = run_challenge(payload, challenge_path=args.challenge, log_handle=log_handle)
summary = (
f"[{progress:3d}%] write=0x{write_mask:02x} fd=0x{fd_value:02x} "
f"offset=0x{write_offset:02x} len=0x{write_len:02x} rc={result.returncode} "
f"stdout_len={len(result.stdout)}"
)
print(summary)
log_line(log_handle, summary)
if args.verbose:
stdout_text = result.stdout.decode('utf-8', errors='replace')
stderr_text = result.stderr.decode('utf-8', errors='replace')
if stdout_text.strip():
print('--- STDOUT ---')
print(stdout_text.rstrip())
log_line(log_handle, '--- STDOUT ---')
log_line(log_handle, stdout_text.rstrip())
if stderr_text.strip():
print('--- STDERR ---')
print(stderr_text.rstrip())
log_line(log_handle, '--- STDERR ---')
log_line(log_handle, stderr_text.rstrip())
if stdout_text.strip() or stderr_text.strip():
print('--------------')
log_line(log_handle, '--------------')
if marker not in result.stdout:
continue
finding = {
'layout': layout,
'imm_opcode': imm_opcode,
'sys_opcode': sys_opcode,
'stm_opcode': stm_opcode,
'ldm_opcode': ldm_opcode,
'register_exit': reg_exit,
'register_buf': reg_buf,
'register_len': reg_len,
'sys_mask_exit': exit_mask,
'sys_mask_read': read_mask,
'sys_mask_open': open_mask,
'sys_mask_write': write_mask,
'write_bytes': write_bytes,
'write_offset': write_offset,
'write_fd': fd_value,
'write_len': write_len,
'flag_state': flag_state,
}
print('[+] Found SYS_WRITE configuration:', finding)
log_line(log_handle, '[+] Found SYS_WRITE configuration: ' + json.dumps(finding))
store_cache_entry(cache_path, cache_data, args.challenge, flag_state, finding, STAGE_FIND_WRITE)
if log_handle:
log_handle.close()
return finding
message = '[!] Exhausted SYS_WRITE search without success.'
print(message)
log_line(log_handle, message)
if log_handle:
log_handle.close()
return None
def parse_value_list(text: str, expected_len: int) -> List[int]:
if not text:
raise ValueError('Missing required value list')
parts = [part.strip() for part in text.replace('\n', ',').split(',') if part.strip()]
values = [int(part, 0) for part in parts]
if expected_len and len(values) != expected_len:
raise ValueError(f'Expected {expected_len} values, got {len(values)} ({values})')
if any(value not in VALUE_POOL for value in values):
raise ValueError(f'All values must be drawn from {VALUE_POOL}, got {values}')
if len(set(values)) != len(values):
raise ValueError(f'Values must be unique, got duplicates in {values}')
return values
def build_register_map(order: List[int]):
mapping = {value: name for value, name in zip(order, REGISTER_NAMES)}
mapping[0x00] = 'NONE'
return mapping
def build_memory_offsets(order: List[int]):
return {value: 0x400 + idx for idx, value in enumerate(order)}
def build_operation_masks(order: List[int]):
return {name: {'byte': 'op_byte', 'mask': value} for name, value in zip(INSTRUCTION_NAMES, order)}
def build_syscall_bits(order: List[int]):
return {value: name for value, name in zip(order, SYSCALL_NAMES)}
def build_flag_map(order: List[int]):
mapping = {value: name for value, name in zip(order, FLAG_NAMES)}
mapping[0x00] = 'ALWAYS'
return mapping
def prepare_config(spec_order, inst_order, sys_order, flag_order, layout):
register_map = build_register_map(spec_order)
memory_offsets = build_memory_offsets(spec_order)
operation_masks = build_operation_masks(inst_order)
syscall_bits = build_syscall_bits(sys_order)
flag_map = build_flag_map(flag_order)
return {
'register_map': register_map,
'memory_register_offsets': memory_offsets,
'operation_masks': operation_masks,
'syscall_bits': {mask: name for mask, name in syscall_bits.items()},
'syscall_map': {mask: name for mask, name in syscall_bits.items()},
'flag_map': flag_map,
'instruction_layout': layout,
}
def str_list(value: Iterable) -> str:
return ', '.join(f'0x{x:02x}' for x in value)
class HelpfulArgumentParser(argparse.ArgumentParser):
def error(self, message):
self.print_usage()
self.print_help()
self.exit(2, f"error: {message}\n")
HELP_TEXT = (
"Value pool: 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80.\n"
"Example config build:\n"
" pc-yansanity-configfinder.py build-config \\\n+ --spec-order '0x40,0x08,0x80,0x02,0x20,0x10,0x01' \\\n+ --inst-order '0x40,0x02,0x01,0x20,0x10,0x08,0x04,0x80' \\\n+ --sys-order '0x20,0x02,0x01,0x80,0x08,0x40' \\\n+ --flag-order '0x40,0x01,0x80,0x02,0x20'"
)
def build_arg_parser():
parser = HelpfulArgumentParser(
description='Yan85 helper (probing + config builder)',
formatter_class=argparse.RawTextHelpFormatter,
epilog=HELP_TEXT,
)
subparsers = parser.add_subparsers(dest='command', required=True)
sys_parser = subparsers.add_parser(
'sys-exit',
help='Local IMM+SYS sweep (looking for host exit matching immediate value)...',
formatter_class=argparse.RawTextHelpFormatter,
description=(
'Builds two-instruction payloads that set a fixed immediate value (0x11) into a register and '
'immediately invoke SYS EXIT using the same register. When the host exit code matches 0x11, '
'the combination is recorded.'
)
)
sys_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
sys_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the two test opcodes (default: 0)')
sys_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
sys_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
sys_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
sys_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
sys_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
readmem_parser = subparsers.add_parser(
'sys-readmem',
help='Probe SYS_READ_MEMORY (stdin byte -> exit)',
formatter_class=argparse.RawTextHelpFormatter,
description='Reads one byte from stdin into VM memory, loads it, and exits with that value. '
'Fuzzes register tags, SYS opcode/mask, and LDM opcode until the stdin byte mirrors '
'the host exit code.',
)
readmem_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
readmem_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the test opcodes (default: 0)')
readmem_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
readmem_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
readmem_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
readmem_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
readmem_parser.add_argument('--stdin-bytes', type=parse_byte_list, default=DEFAULT_STDIN_BYTES,
help='Comma separated byte values to verify via stdin '
'(default: 0x41,0x61,0x00,0xff,0xf0,0x0f)')
readmem_parser.add_argument('--fd-imm', type=lambda x: int(x, 0), default=0x00,
help='Immediate written to FD register before SYS_READ_MEMORY (default: 0)')
readmem_parser.add_argument('--buf-imm', type=lambda x: int(x, 0), default=0x30,
help='Immediate written to buffer register (default: 0x30)')
readmem_parser.add_argument('--len-imm', type=lambda x: int(x, 0), default=0x01,
help='Immediate written to length register (default: 1)')
readmem_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
findall_parser = subparsers.add_parser(
'find-all',
help='Run sys-exit, sys-readmem, find-stm, verify-stmldm, find-open, find-write sequentially',
formatter_class=argparse.RawTextHelpFormatter,
description='Convenience wrapper that executes sys-exit → sys-readmem → find-stm → verify-stmldm → find-open → find-write.',
)
findall_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
findall_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the test opcodes (default: 0)')
findall_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
findall_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
findall_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
findall_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
findall_parser.add_argument('--stdin-bytes', type=parse_byte_list, default=DEFAULT_STDIN_BYTES,
help='Comma separated byte values to verify via stdin '
'(default: 0x41,0x61,0x00,0xff,0xf0,0x0f)')
findall_parser.add_argument('--fd-imm', type=lambda x: int(x, 0), default=0x00,
help='Immediate written to FD register before SYS_READ_MEMORY (default: 0)')
findall_parser.add_argument('--buf-imm', type=lambda x: int(x, 0), default=0x30,
help='Immediate written to buffer register (default: 0x30)')
findall_parser.add_argument('--len-imm', type=lambda x: int(x, 0), default=0x01,
help='Immediate written to length register (default: 1)')
findall_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
open_parser = subparsers.add_parser(
'find-open',
help='Discover SYS_OPEN mask via fd exit check',
formatter_class=argparse.RawTextHelpFormatter,
description='Writes the probe path "f" into VM memory, attempts SYS_OPEN, and exits with the returned fd. '
'Sweeps SYS masks until the exit code falls in the valid fd range (>=3).',
)
open_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
open_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the payload (default: 0)')
open_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
open_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
open_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
open_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
open_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
write_parser = subparsers.add_parser(
'find-write',
help='Discover SYS_WRITE mask via stdout marker',
formatter_class=argparse.RawTextHelpFormatter,
description='Writes a marker string into VM memory and sweeps SYS masks until the marker appears on stdout.',
)
write_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
write_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the payload (default: 0)')
write_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
write_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
write_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
write_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
write_parser.add_argument('--write-bytes', type=parse_byte_list, default=None,
help='Comma separated byte values to emit via SYS_WRITE '
'(default: ::YAN85-WRITE::\\n)')
write_parser.add_argument('--write-offset', type=lambda x: int(x, 0), default=0x30,
help='Offset within VM window used for buffer writes (default: 0x30)')
write_parser.add_argument('--write-len', type=lambda x: int(x, 0), default=None,
help='Length passed to SYS_WRITE (default: len(write-bytes))')
write_parser.add_argument('--write-fd', type=lambda x: int(x, 0), default=1,
help='FD immediate passed to SYS_WRITE (default: 1 / stdout)')
write_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
stm_parser = subparsers.add_parser(
'find-stm',
help='Verify STM opcode by round-tripping through VM memory',
formatter_class=argparse.RawTextHelpFormatter,
description='Writes a series of immediate values into memory via STM, reloads them with LDM, and exits '
'with the result to confirm the STM opcode.',
)
stm_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
stm_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the test opcodes (default: 0)')
stm_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
stm_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
stm_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
stm_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
stm_parser.add_argument('--test-values', type=parse_byte_list, default=DEFAULT_STM_VALUES,
help='Comma separated byte values to write + read via STM/LDM '
'(default: VALUE_POOL)')
stm_parser.add_argument('--offset-imm', type=lambda x: int(x, 0), default=0x00,
help='Immediate written to buffer register before STM (default: 0)')
stm_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
verify_parser = subparsers.add_parser(
'verify-stmldm',
help='Sanity-check STM/LDM by poking bytes into the VM window and reading them back',
formatter_class=argparse.RawTextHelpFormatter,
description='Writes bytes from DEFAULT_STDIN_BYTES into the VM window via STM, reads them back via LDM, '
'and exits with the observed value to ensure the register/offset layout is sound.',
)
verify_parser.add_argument('--challenge', default=default_challenge_path(),
help='Local challenge path (default: /challenge/yansanity-hard)')
verify_parser.add_argument('--padding-instr', type=int, default=0,
help='Extra 3-byte zero instructions appended after the payload (default: 0)')
verify_parser.add_argument('--log-file', default=None,
help='Log file path (default: pc-yansanity-configfinder.log-YYYYMMDD-HHMMSS). '
'Pass empty string to disable logging.')
verify_parser.add_argument('--flag-path', default=DEFAULT_FLAG_PATH,
help='Path to flag file used to fingerprint cache entries (default: /flag)')
verify_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file for discovered configs (default: ./pc-yansanity-cache.json). '
'Pass empty string to disable caching.')
verify_parser.add_argument('--host-label', default=None,
help='Optional host label to store alongside hostname (default: system hostname)')
verify_parser.add_argument('--stdin-bytes', type=parse_byte_list, default=DEFAULT_STDIN_BYTES,
help='Comma separated byte values to write/read via STM/LDM '
'(default: 0x41,0x61,0x00,0xff,0xf0,0x0f)')
verify_parser.add_argument('-v', '--verbose', action='store_true', help='Print STDOUT/STDERR for each run')
build_parser = subparsers.add_parser(
'build-config',
help='Derive JSON config from cached stage results',
formatter_class=argparse.RawTextHelpFormatter,
description='Uses cached SYS/IMM/STM discoveries to emit pc-yansanity-asm compatible config.',
)
build_parser.add_argument('--challenge', default=default_challenge_path(),
help='Challenge path whose cache entries should be consumed (default: auto-host)')
build_parser.add_argument('--cache-file', default=DEFAULT_CACHE_FILE,
help='Cache file to read stage results from (default: ./pc-yansanity-cache.json)')
build_parser.add_argument('-o', '--output', default='pc-yansanity-asm-config.json',
help='Output JSON path (default: pc-yansanity-asm-config.json)')
return parser
def mode_build_config(args):
cache_path = Path(args.cache_file).expanduser()
cache_data = load_cache(cache_path)
challenge = args.challenge
stage_entries = {
stage: collect_stage_entry(cache_data, challenge, stage)
for stage in (
STAGE_SYS_EXIT,
STAGE_SYS_READMEM,
STAGE_FIND_STM,
STAGE_FIND_OPEN,
STAGE_FIND_WRITE,
)
}
missing = [stage for stage, entry in stage_entries.items() if entry is None]
if missing:
print(f"[!] Cannot build config, missing cache entries: {', '.join(missing)}")
return None
exit_result = stage_entries[STAGE_SYS_EXIT]['result']
readmem_result = stage_entries[STAGE_SYS_READMEM]['result']
open_result = stage_entries[STAGE_FIND_OPEN]['result']
write_result = stage_entries[STAGE_FIND_WRITE]['result']
layout_tokens = infer_layout_tokens(exit_result.get('layout') or readmem_result.get('layout'))
register_assignments = {
'REG_A': exit_result.get('register_tag'),
'REG_B': readmem_result.get('register_buf'),
'REG_C': readmem_result.get('register_len'),
}
instruction_assignments = {
'IMM': exit_result.get('imm_opcode'),
'SYS': exit_result.get('sys_opcode'),
'STM': stage_entries[STAGE_FIND_STM]['result'].get('stm_opcode'),
'LDM': stage_entries[STAGE_FIND_STM]['result'].get('ldm_opcode'),
}
syscall_assignments = {
'EXIT': exit_result.get('sys_mask'),
'READ_MEMORY': readmem_result.get('sys_mask_read'),
'OPEN': open_result.get('sys_mask_open'),
'WRITE': write_result.get('sys_mask_write'),
}
flag_assignments: dict = {}
spec_order, spec_status = build_permutation(REGISTER_NAMES, register_assignments)
inst_order, inst_status = build_permutation(INSTRUCTION_NAMES, instruction_assignments)
sys_order, sys_status = build_permutation(SYSCALL_NAMES, syscall_assignments)
flag_order, flag_status = build_permutation(FLAG_NAMES, flag_assignments)
config = prepare_config(spec_order, inst_order, sys_order, flag_order, layout_tokens)
config['assignment_sources'] = {
'registers': spec_status,
'instructions': inst_status,
'syscalls': sys_status,
'flags': flag_status,
}
serialized = json.dumps(config, indent=2, sort_keys=True)
output_path = args.output
if output_path in (None, '-', ''):
print(serialized)
else:
target = Path(output_path)
target.write_text(serialized + '\n')
print(f'[+] wrote config to {target}')
print('[*] Assignment status:')
for category, status_map in config['assignment_sources'].items():
confirmed = [name for name, state in status_map.items() if state == 'confirmed']
inferred = [name for name, state in status_map.items() if state != 'confirmed']
if confirmed:
print(f" - {category}: confirmed -> {', '.join(confirmed)}")
if inferred:
print(f" - {category}: inferred -> {', '.join(inferred)}")
return config
def mode_find_all(args):
print('[*] Running sys-exit stage...')
exit_result = mode_sys_exit(args)
if exit_result is None:
print('[!] sys-exit stage failed; aborting find-all')
return None
print('[*] Running sys-readmem stage...')
readmem_result = mode_sys_readmem(args)
if readmem_result is None:
print('[!] sys-readmem stage failed.')
return None
if not hasattr(args, 'test_values'):
setattr(args, 'test_values', None)
if not hasattr(args, 'offset_imm'):
setattr(args, 'offset_imm', 0)
print('[*] Running find-stm stage...')
stm_result = mode_find_stm(args)
if stm_result is None:
print('[!] find-stm stage failed.')
return None
print('[*] Running verify-stmldm stage...')
verify_result = mode_verify_stmldm(args)
if verify_result is None:
print('[!] verify-stmldm stage failed.')
return None
if not hasattr(args, 'write_bytes'):
setattr(args, 'write_bytes', None)
if not hasattr(args, 'write_offset'):
setattr(args, 'write_offset', 0x30)
if not hasattr(args, 'write_len'):
setattr(args, 'write_len', None)
if not hasattr(args, 'write_fd'):
setattr(args, 'write_fd', 1)
print('[*] Running find-open stage...')
open_result = mode_find_open(args)
if open_result is None:
print('[!] find-open stage failed.')
return None
print('[*] Running find-write stage...')
write_result = mode_find_write(args)
if write_result is None:
print('[!] find-write stage failed.')
return None
print('[+] Completed all stages.')
return {
'sys_exit': exit_result,
'sys_readmem': readmem_result,
'find_stm': stm_result,
'verify_stmldm': verify_result,
'find_open': open_result,
'find_write': write_result,
}
def main(argv=None):
parser = build_arg_parser()
args = parser.parse_args(argv)
if args.command == 'sys-exit':
mode_sys_exit(args)
elif args.command == 'sys-readmem':
mode_sys_readmem(args)
elif args.command == 'find-all':
mode_find_all(args)
elif args.command == 'find-stm':
mode_find_stm(args)
elif args.command == 'verify-stmldm':
mode_verify_stmldm(args)
elif args.command == 'find-open':
mode_find_open(args)
elif args.command == 'find-write':
mode_find_write(args)
elif args.command == 'build-config':
mode_build_config(args)
else:
raise SystemExit(f"Unknown command {args.command}")
if __name__ == '__main__':
main()
pc-yansanity-cache.json (created by find-all)
neuland% cat pc-yansanity-cache.json
{
"entries": {
"/challenge/yansanity-easy|reverse-engineering~yansanity-easy|/flag|size:57|stage:find-open": {
"cached_at": "2026-02-27T22:00:34.255552",
"challenge": "/challenge/yansanity-easy",
"flag_state": {
"host_label": "reverse-engineering~yansanity-easy",
"hostname": "reverse-engineering~yansanity-easy",
"mtime": "2026-02-27T20:57:34.616020",
"mtime_ns": 1772225854616020268,
"path": "/flag",
"size": 57
},
"result": {
"flag_value": 0,
"flags_register": 64,
"imm_opcode": 16,
"layout": {
"arg_high": 1,
"arg_low": 0,
"op_byte": 2
},
"ldm_opcode": 32,
"mode_register": 32,
"mode_value": 0,
"probe_path": "/etc/group",
"ptr_register": 16,
"register_buf": 64,
"register_exit": 16,
"register_len": 32,
"stm_opcode": 4,
"sys_mask_exit": 64,
"sys_mask_open": 8,
"sys_mask_read": 1,
"sys_opcode": 1
},
"stage": "find-open"
},
"/challenge/yansanity-easy|reverse-engineering~yansanity-easy|/flag|size:57|stage:find-stm": {
"cached_at": "2026-02-27T22:00:33.914220",
"challenge": "/challenge/yansanity-easy",
"flag_state": {
"host_label": "reverse-engineering~yansanity-easy",
"hostname": "reverse-engineering~yansanity-easy",
"mtime": "2026-02-27T20:57:34.616020",
"mtime_ns": 1772225854616020268,
"path": "/flag",
"size": 57
},
"result": {
"imm_opcode": 16,
"layout": {
"arg_high": 1,
"arg_low": 0,
"op_byte": 2
},
"ldm_opcode": 32,
"offset_immediate": 0,
"register_buf": 64,
"register_exit": 16,
"stm_opcode": 4,
"sys_mask_exit": 64,
"sys_opcode": 1,
"test_values": [
1,
2,
4,
8,
16,
32,
64,
128
]
},
"stage": "find-stm"
},
"/challenge/yansanity-easy|reverse-engineering~yansanity-easy|/flag|size:57|stage:find-write": {
"cached_at": "2026-02-27T22:00:34.789746",
"challenge": "/challenge/yansanity-easy",
"flag_state": {
"host_label": "reverse-engineering~yansanity-easy",
"hostname": "reverse-engineering~yansanity-easy",
"mtime": "2026-02-27T20:57:34.616020",
"mtime_ns": 1772225854616020268,
"path": "/flag",
"size": 57
},
"result": {
"imm_opcode": 16,
"layout": {
"arg_high": 1,
"arg_low": 0,
"op_byte": 2
},
"ldm_opcode": 32,
"register_buf": 64,
"register_exit": 16,
"register_len": 32,
"stm_opcode": 4,
"sys_mask_exit": 64,
"sys_mask_open": 8,
"sys_mask_read": 1,
"sys_mask_write": 32,
"sys_opcode": 1,
"write_bytes": [
58,
58,
89,
65,
78,
56,
53,
45,
87,
82,
73,
84,
69,
58,
58,
10
],
"write_fd": 1,
"write_len": 16,
"write_offset": 48
},
"stage": "find-write"
},
"/challenge/yansanity-easy|reverse-engineering~yansanity-easy|/flag|size:57|stage:sys-exit": {
"cached_at": "2026-02-27T21:54:53.566632",
"challenge": "/challenge/yansanity-easy",
"flag_state": {
"host_label": "reverse-engineering~yansanity-easy",
"hostname": "reverse-engineering~yansanity-easy",
"mtime": "2026-02-27T20:57:34.616020",
"mtime_ns": 1772225854616020268,
"path": "/flag",
"size": 57
},
"result": {
"imm_opcode": 16,
"immediate_value": 17,
"layout": {
"arg_high": 1,
"arg_low": 0,
"op_byte": 2
},
"register_tag": 16,
"sys_mask": 64,
"sys_opcode": 1
},
"stage": "sys-exit"
},
"/challenge/yansanity-easy|reverse-engineering~yansanity-easy|/flag|size:57|stage:sys-readmem": {
"cached_at": "2026-02-27T22:00:33.822847",
"challenge": "/challenge/yansanity-easy",
"flag_state": {
"host_label": "reverse-engineering~yansanity-easy",
"hostname": "reverse-engineering~yansanity-easy",
"mtime": "2026-02-27T20:57:34.616020",
"mtime_ns": 1772225854616020268,
"path": "/flag",
"size": 57
},
"result": {
"imm_opcode": 16,
"immediate_buf": 48,
"immediate_fd": 0,
"immediate_len": 1,
"layout": {
"arg_high": 1,
"arg_low": 0,
"op_byte": 2
},
"ldm_opcode": 32,
"register_buf": 64,
"register_exit": 16,
"register_len": 32,
"stdin_test_values": [
65,
97,
0,
255,
240,
15
],
"sys_mask_exit": 64,
"sys_mask_read": 1,
"sys_opcode": 1
},
"stage": "sys-readmem"
},
"/challenge/yansanity-easy|reverse-engineering~yansanity-easy|/flag|size:57|stage:verify-stmldm": {
"cached_at": "2026-02-27T22:00:34.226600",
"challenge": "/challenge/yansanity-easy",
"flag_state": {
"host_label": "reverse-engineering~yansanity-easy",
"hostname": "reverse-engineering~yansanity-easy",
"mtime": "2026-02-27T20:57:34.616020",
"mtime_ns": 1772225854616020268,
"path": "/flag",
"size": 57
},
"result": {
"imm_opcode": 16,
"ldm_opcode": 32,
"offsets": [
0,
8,
16,
24,
32,
40
],
"register_buf": 64,
"register_exit": 16,
"stm_opcode": 4,
"sys_mask_exit": 64,
"sys_opcode": 1,
"test_values": [
65,
97,
0,
255,
240,
15
]
},
"stage": "verify-stmldm"
}
},
"version": 1
}
pc-yansanity-asm-config.json (created by build-config)
neuland% cat pc-yansanity-asm-config.json
{
"assignment_sources": {
"flags": {
"BOTH_ZERO": "inferred",
"EQUAL": "inferred",
"GREATER": "inferred",
"LESS": "inferred",
"NOT_EQUAL": "inferred"
},
"instructions": {
"ADD": "inferred",
"CMP": "inferred",
"IMM": "confirmed",
"JMP": "inferred",
"LDM": "confirmed",
"STK": "inferred",
"STM": "confirmed",
"SYS": "confirmed"
},
"registers": {
"REG_A": "confirmed",
"REG_B": "confirmed",
"REG_C": "confirmed",
"REG_D": "inferred",
"REG_F": "inferred",
"REG_I": "inferred",
"REG_S": "inferred"
},
"syscalls": {
"EXIT": "confirmed",
"OPEN": "confirmed",
"READ_CODE": "inferred",
"READ_MEMORY": "confirmed",
"SLEEP": "inferred",
"WRITE": "confirmed"
}
},
"flag_map": {
"0": "ALWAYS",
"1": "LESS",
"2": "GREATER",
"4": "EQUAL",
"8": "NOT_EQUAL",
"16": "BOTH_ZERO"
},
"instruction_layout": [
"arg_low",
"arg_high",
"op_byte"
],
"memory_register_offsets": {
"1": 1027,
"2": 1028,
"4": 1029,
"8": 1030,
"16": 1024,
"32": 1026,
"64": 1025
},
"operation_masks": {
"ADD": {
"byte": "op_byte",
"mask": 8
},
"CMP": {
"byte": "op_byte",
"mask": 128
},
"IMM": {
"byte": "op_byte",
"mask": 16
},
"JMP": {
"byte": "op_byte",
"mask": 64
},
"LDM": {
"byte": "op_byte",
"mask": 32
},
"STK": {
"byte": "op_byte",
"mask": 2
},
"STM": {
"byte": "op_byte",
"mask": 4
},
"SYS": {
"byte": "op_byte",
"mask": 1
}
},
"register_map": {
"0": "NONE",
"1": "REG_D",
"2": "REG_S",
"4": "REG_I",
"8": "REG_F",
"16": "REG_A",
"32": "REG_C",
"64": "REG_B"
},
"syscall_bits": {
"1": "READ_MEMORY",
"2": "SLEEP",
"4": "READ_CODE",
"8": "OPEN",
"32": "WRITE",
"64": "EXIT"
},
"syscall_map": {
"1": "READ_MEMORY",
"2": "SLEEP",
"4": "READ_CODE",
"8": "OPEN",
"32": "WRITE",
"64": "EXIT"
}
}
pc-yansanity-asm.py
neuland% cat pc-yansanity-asm.py
#!/usr/bin/env python3
import argparse
import ast
import copy
import json
from pathlib import Path
import sys
# Yan85 Variant Configuration (CORRECTED from Ghidra analysis)
DEFAULT_CONFIG = {
'register_map': {
0x01: 'REG_A', # -> 0x400
0x02: 'REG_B', # -> 0x401
0x04: 'REG_C', # -> 0x402
0x08: 'REG_D', # -> 0x403
0x10: 'REG_S', # -> 0x404 (stack pointer)
0x20: 'REG_I', # -> 0x405 (instruction pointer)
0x40: 'REG_F', # -> 0x406
0x00: 'NONE',
},
'memory_register_offsets': {
0x01: 0x400,
0x02: 0x401,
0x04: 0x402,
0x08: 0x403,
0x10: 0x404,
0x20: 0x405,
0x40: 0x406,
0x00: 0x000,
},
'syscall_map': {
0x01: 'WRITE',
0x02: 'SLEEP',
0x04: 'OPEN',
0x08: 'READ_MEMORY',
0x10: 'EXIT',
0x20: 'READ_CODE',
},
'syscall_bits': {
0x01: 'WRITE',
0x02: 'SLEEP',
0x04: 'OPEN',
0x08: 'READ_MEMORY',
0x10: 'EXIT',
0x20: 'READ_CODE',
},
'flag_map': {
0x01: 'LESS',
0x02: 'GREATER',
0x04: 'EQUAL',
0x08: 'NOT_EQUAL',
0x10: 'BOTH_ZERO',
0x00: 'ALWAYS',
},
# Byte roles listed in little-endian order (offset 0 -> offset 2)
'instruction_layout': (
'arg_low',
'arg_high',
'op_byte',
),
'operation_masks': {
'ADD': {'byte': 'op_byte', 'mask': 0x01},
'STK': {'byte': 'op_byte', 'mask': 0x02},
'LDM': {'byte': 'op_byte', 'mask': 0x04},
'IMM': {'byte': 'op_byte', 'mask': 0x08},
'CMP': {'byte': 'op_byte', 'mask': 0x10},
'STM': {'byte': 'op_byte', 'mask': 0x20},
'JMP': {'byte': 'op_byte', 'mask': 0x40},
'SYS': {'byte': 'op_byte', 'mask': 0x80},
},
}
DEFAULT_CONFIG_PATH = Path('pc-yansanity-asm-config.json')
REQUIRED_ROLES = ('arg_low', 'arg_high', 'op_byte')
def _parse_int(value, *, allow_hex=True):
if isinstance(value, int):
return value
if isinstance(value, str):
value = value.strip()
if not value:
raise ValueError('Empty string cannot represent an integer')
base = 0 if allow_hex else 10
return int(value, base)
raise ValueError(f"Value '{value}' must be an int or int-like string")
def _convert_int_keyed_dict(mapping, *, value_converter=None):
converted = {}
for key, raw_value in mapping.items():
int_key = _parse_int(key)
converted[int_key] = value_converter(raw_value) if value_converter else raw_value
return converted
def _convert_value_int_dict(mapping):
return {key: _parse_int(value) for key, value in mapping.items()}
def _merge_config(base, override):
merged = copy.deepcopy(base)
for key, value in override.items():
if key in {
'register_map',
'memory_register_offsets',
'syscall_map',
'syscall_bits',
'flag_map',
}:
converted = _convert_int_keyed_dict(value)
if key == 'memory_register_offsets':
converted = _convert_value_int_dict(converted)
merged[key] = converted
elif key == 'instruction_layout':
merged[key] = tuple(value)
elif key == 'operation_masks':
merged[key] = value
else:
merged[key] = value
return merged
def _prepare_config_for_json(config):
def convert(obj):
if isinstance(obj, dict):
return {str(k): convert(v) for k, v in obj.items()}
if isinstance(obj, tuple):
return [convert(v) for v in obj]
if isinstance(obj, list):
return [convert(v) for v in obj]
return obj
return convert(config)
def _normalize_operation_specs(raw_specs):
specs = {}
for op_name, raw in raw_specs.items():
if isinstance(raw, int):
normalized = {'byte': 'op_byte', 'mask': raw}
else:
if 'mask' not in raw:
raise ValueError(f"operation '{op_name}' must define a mask")
normalized = {
'byte': raw.get('byte', 'op_byte'),
'mask': raw['mask'],
}
byte_role = normalized['byte']
if byte_role not in BYTE_ROLE_TO_INDEX:
raise ValueError(
f"operation '{op_name}' references unknown byte role '{byte_role}'"
)
mask = normalized['mask']
if not (0 <= mask <= 0xFF):
raise ValueError(f"operation '{op_name}' mask must be 0..0xFF")
specs[op_name] = {'byte': byte_role, 'mask': mask}
return specs
YAN85_CONFIG = copy.deepcopy(DEFAULT_CONFIG)
ASSIGNMENT_SOURCES = {
'registers': {},
'instructions': {},
'syscalls': {},
'flags': {},
}
WARNINGS_EMITTED = set()
REGISTER_VALUES = {}
INSTRUCTION_LAYOUT = REQUIRED_ROLES
BYTE_ROLE_TO_INDEX = {}
OPERATION_SPECS = {}
FLAG_NAME_TO_MASK = {}
SYSCALL_NAME_TO_BIT = {}
def set_active_config(config_dict):
global YAN85_CONFIG
global ASSIGNMENT_SOURCES, WARNINGS_EMITTED
YAN85_CONFIG = config_dict
ASSIGNMENT_SOURCES = {
category: dict(values)
for category, values in (config_dict.get('assignment_sources') or {}).items()
}
WARNINGS_EMITTED = set()
rebuild_context()
def rebuild_context():
global REGISTER_VALUES, INSTRUCTION_LAYOUT, BYTE_ROLE_TO_INDEX
global OPERATION_SPECS, FLAG_NAME_TO_MASK, SYSCALL_NAME_TO_BIT
REGISTER_VALUES = {
name: value for value, name in YAN85_CONFIG['register_map'].items()
}
REGISTER_VALUES.setdefault('NONE', 0x00)
layout = tuple(YAN85_CONFIG.get('instruction_layout', REQUIRED_ROLES))
if set(layout) != set(REQUIRED_ROLES) or len(layout) != len(REQUIRED_ROLES):
raise ValueError('instruction_layout must contain arg_low, arg_high, op_byte exactly once')
INSTRUCTION_LAYOUT = layout
BYTE_ROLE_TO_INDEX = {
role: idx for idx, role in enumerate(INSTRUCTION_LAYOUT)
}
OPERATION_SPECS = _normalize_operation_specs(YAN85_CONFIG['operation_masks'])
FLAG_NAME_TO_MASK = {
name: mask for mask, name in YAN85_CONFIG['flag_map'].items()
}
FLAG_NAME_TO_MASK.setdefault('ALWAYS', 0x00)
SYSCALL_NAME_TO_BIT = {
name: bit for bit, name in YAN85_CONFIG['syscall_bits'].items()
}
def warn_if_inferred(category, name, *, context):
status_map = ASSIGNMENT_SOURCES.get(category) or {}
status = status_map.get(name)
if status == 'confirmed':
return
key = (category, name, context)
if key in WARNINGS_EMITTED:
return
WARNINGS_EMITTED.add(key)
origin = 'inferred' if status else 'unknown'
print(
f"[!] {context}: {category.rstrip('s').capitalize()} '{name}' is {origin}; verify before relying on it.",
file=sys.stderr,
)
rebuild_context()
# Toggle for emitting opcode/arg debug metadata (enable with `-v` CLI flag)
SHOW_DEBUG_METADATA = False
def load_config_file(path):
override = json.loads(Path(path).read_text())
merged = _merge_config(DEFAULT_CONFIG, override)
set_active_config(merged)
def dump_active_config(path=None):
serialized = json.dumps(_prepare_config_for_json(YAN85_CONFIG), indent=2, sort_keys=True)
if path in (None, '-'):
print(serialized)
else:
Path(path).write_text(serialized + '\n')
def get_register_name(reg_value):
"""Returns register name based on hex value"""
name = YAN85_CONFIG['register_map'].get(reg_value, f'unknown_{reg_value:02x}')
if name.startswith('REG_'):
warn_if_inferred('registers', name, context='disassembler')
return name
def normalize_register_name(name):
token = name.strip().upper()
if token.startswith('REG_'):
key = token
elif token == 'NONE':
key = 'NONE'
else:
key = f'REG_{token}'
if key not in REGISTER_VALUES:
raise ValueError(f"Unknown register '{name}'")
warn_if_inferred('registers', key, context='assembler')
return key
def parse_number(token):
token = token.strip()
if not token:
raise ValueError('Empty numeric token')
try:
return int(token, 0)
except ValueError:
try:
literal = ast.literal_eval(token)
except Exception as exc: # noqa: BLE001
raise ValueError(f"Cannot parse value '{token}'") from exc
if isinstance(literal, str):
if len(literal) != 1:
raise ValueError(f"String literal '{token}' must be length 1")
return ord(literal)
if isinstance(literal, int):
return literal
raise ValueError(f"Unsupported literal '{token}'")
def parse_flag_mask(token):
parts = token.replace('+', ' ').split()
if not parts:
return 0
mask = 0
for part in parts:
name = part.upper()
if name not in FLAG_NAME_TO_MASK:
raise ValueError(f"Unknown flag '{part}'")
warn_if_inferred('flags', name, context='assembler')
mask |= FLAG_NAME_TO_MASK[name]
return mask
def parse_syscall_mask(token):
parts = token.replace('+', ' ').split()
if not parts:
return 0
mask = 0
for part in parts:
try:
value = parse_number(part)
except ValueError:
name = part.upper()
if name not in SYSCALL_NAME_TO_BIT:
raise ValueError(f"Unknown syscall '{part}'")
warn_if_inferred('syscalls', name, context='assembler')
value = SYSCALL_NAME_TO_BIT[name]
mask |= value
if mask > 0xFF:
print(
f"[!] assembler: SYS mask 0x{mask:02x} exceeds byte range; low byte will be used",
file=sys.stderr,
)
return mask & 0xFF
def split_operands(text):
if not text:
return []
return [operand.strip() for operand in text.split(',') if operand.strip()]
def decode_instruction(instruction_bytes):
"""Decodes a 3-byte Yan85 instruction according to Ghidra interpret_instruction()"""
if len(instruction_bytes) != 3:
return None
role_values = {
role: instruction_bytes[BYTE_ROLE_TO_INDEX[role]]
for role in REQUIRED_ROLES
}
arg_low = role_values['arg_low']
arg_high = role_values['arg_high']
op_byte = role_values['op_byte']
operations = [
op_name
for op_name, spec in OPERATION_SPECS.items()
if instruction_bytes[BYTE_ROLE_TO_INDEX[spec['byte']]] & spec['mask']
]
return {
'op_byte': op_byte,
'arg_low': arg_low,
'arg_high': arg_high,
'operations': operations,
'bytes': instruction_bytes,
}
def format_instruction(decoded, addr):
"""Formats a decoded instruction for output"""
index = addr // 3 # REG_I (Instruction Index)
if not decoded:
return f"0x{addr:04x} (i=0x{index:03x}): ???"
ops = decoded['operations']
op_byte = decoded['op_byte']
arg_low = decoded['arg_low']
arg_high = decoded['arg_high']
bytes_str = ' '.join([f"{b:02x}" for b in decoded['bytes']])
ascii_str = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in decoded['bytes'])
debug_suffix = ""
if SHOW_DEBUG_METADATA:
debug_suffix = f" [op_byte=0x{op_byte:02x}, arg_high=0x{arg_high:02x}, arg_low=0x{arg_low:02x}]"
result = f"0x{addr:04x} (i=0x{index:03x}): {bytes_str} | {ascii_str} |{debug_suffix}"
for op in ops:
warn_if_inferred('instructions', op, context='disassembler')
if 'LDM' in ops:
dest_reg = get_register_name(arg_high)
src_reg = get_register_name(arg_low)
return (
f"0x{addr:04x} (i=0x{index:03x}): {bytes_str} | {ascii_str} |{debug_suffix}"
f" LDM {dest_reg} = memory[0x300 + {src_reg}]"
)
if 'SYS' in ops:
syscall_mask = arg_high & 0xFF
ret_reg = get_register_name(arg_low)
header = f"0x{addr:04x} (i=0x{index:03x}): {bytes_str} | {ascii_str} |{debug_suffix}"
bit_names = [
name for bit, name in sorted(YAN85_CONFIG['syscall_bits'].items())
if syscall_mask & bit
]
sys_name = ' + '.join(bit_names) if bit_names else 'UNKNOWN'
for name in bit_names:
warn_if_inferred('syscalls', name, context='disassembler')
result = (
f"{header} SYS {sys_name}, ret={ret_reg} "
f"(syscall# 0x{syscall_mask:02x})"
)
elif 'IMM' in ops:
reg_name = get_register_name(arg_high)
value = arg_low
ascii_char = ''
if 32 <= value <= 126:
ascii_char = f" / '{chr(value)}'"
result = f"0x{addr:04x} (i=0x{index:03x}): {bytes_str} | {ascii_str} |{debug_suffix} IMM {reg_name} = 0x{value:02x}{ascii_char}"
elif 'STM' in ops:
src_reg = get_register_name(arg_low)
offset_reg = get_register_name(arg_high)
result += f" STM memory[0x300 + {offset_reg}] = {src_reg}"
elif 'CMP' in ops:
reg1 = get_register_name(arg_high)
reg2 = get_register_name(arg_low)
result += f" CMP {reg1}, {reg2}"
elif 'ADD' in ops:
reg1 = get_register_name(arg_high)
reg2 = get_register_name(arg_low)
result += f" ADD {reg1} += {reg2}"
elif 'STK' in ops:
reg1 = get_register_name(arg_high)
reg2 = get_register_name(arg_low)
# Format: push/pop based on register values
if reg2 != 'NONE':
if reg1 != 'NONE':
result += f" STK push {reg2}, pop {reg1}"
else:
result += f" STK push {reg2}"
elif reg1 != 'NONE':
result += f" STK pop {reg1}"
else:
result += f" STK"
elif 'JMP' in ops:
target_reg = get_register_name(arg_low)
condition_flags = arg_high
# Decode flag bits based on describe_flags function
flag_names = []
for mask, name in YAN85_CONFIG['flag_map'].items():
if condition_flags & mask:
warn_if_inferred('flags', name, context='disassembler')
flag_names.append(name)
if condition_flags == 0:
flag_names.append('ALWAYS')
flags_str = '+'.join(flag_names) if flag_names else 'NONE'
if condition_flags == 0:
result += f" JMP if ALWAYS -> {target_reg}"
else:
result += f" JMP if {flags_str} (REG_F & 0x{condition_flags:02x}) -> {target_reg}"
if not ops:
result += f" ??? arg_high=0x{arg_high:02x} op_byte=0x{op_byte:02x} arg_low=0x{arg_low:02x}"
return result
class AssemblerError(Exception):
"""Domain specific error for assembling Yan85 code."""
def resolve_byte_value(token, labels):
if token in labels:
value = labels[token]['offset']
else:
value = parse_number(token)
if not 0 <= value <= 0xFF:
raise AssemblerError(f"Value '{token}' does not fit in byte (got {value})")
return value
def parse_instruction_entry(mnemonic, operands, labels, line):
role_values = {role: 0 for role in REQUIRED_ROLES}
def set_role(role, value):
role_values[role] = value & 0xFF
def reg(token):
return REGISTER_VALUES[normalize_register_name(token)]
def enable_operation(op_name):
if op_name not in OPERATION_SPECS:
raise AssemblerError(f"Unknown operation '{op_name}' (line {line})")
warn_if_inferred('instructions', op_name, context='assembler')
spec = OPERATION_SPECS[op_name]
role_values[spec['byte']] |= spec['mask']
upper = mnemonic.upper()
if upper == 'IMM':
if len(operands) != 2:
raise AssemblerError(f"IMM expects dst, value (line {line})")
enable_operation('IMM')
set_role('arg_high', reg(operands[0]))
set_role('arg_low', resolve_byte_value(operands[1], labels))
elif upper == 'ADD':
if len(operands) != 2:
raise AssemblerError(f"ADD expects dst, src (line {line})")
enable_operation('ADD')
set_role('arg_high', reg(operands[0]))
set_role('arg_low', reg(operands[1]))
elif upper == 'CMP':
if len(operands) != 2:
raise AssemblerError(f"CMP expects reg1, reg2 (line {line})")
enable_operation('CMP')
set_role('arg_high', reg(operands[0]))
set_role('arg_low', reg(operands[1]))
elif upper == 'JMP':
if len(operands) != 2:
raise AssemblerError(f"JMP expects flags, target (line {line})")
enable_operation('JMP')
set_role('arg_high', parse_flag_mask(operands[0]))
set_role('arg_low', reg(operands[1]))
elif upper == 'SYS':
if len(operands) != 2:
raise AssemblerError(f"SYS expects mask, ret (line {line})")
enable_operation('SYS')
mask = parse_syscall_mask(operands[0])
set_role('arg_high', mask & 0xFF)
set_role('arg_low', reg(operands[1]))
elif upper == 'STK':
if len(operands) != 2:
raise AssemblerError(f"STK expects pop, push (line {line})")
enable_operation('STK')
set_role('arg_high', reg(operands[0]))
set_role('arg_low', reg(operands[1]))
elif upper == 'STM':
if len(operands) != 2:
raise AssemblerError(f"STM expects offset_reg, src_reg (line {line})")
enable_operation('STM')
set_role('arg_high', reg(operands[0]))
set_role('arg_low', reg(operands[1]))
elif upper == 'LDM':
if len(operands) != 2:
raise AssemblerError(f"LDM expects dst_reg, offset_reg (line {line})")
enable_operation('LDM')
set_role('arg_high', reg(operands[0]))
set_role('arg_low', reg(operands[1]))
elif upper == 'HALT':
enable_operation('SYS')
mask = parse_syscall_mask('EXIT')
if mask & 0xFF:
raise AssemblerError("EXIT mask occupies low byte")
set_role('arg_high', (mask >> 8) & 0xFF)
set_role('arg_low', REGISTER_VALUES['NONE'])
else:
raise AssemblerError(f"Unknown mnemonic '{mnemonic}' (line {line})")
return bytes(role_values[role] & 0xFF for role in INSTRUCTION_LAYOUT)
def parse_source(lines):
entries = []
labels = {}
sections = ('code', 'data')
offsets = {section: 0 for section in sections}
bases = {section: None for section in sections}
current_section = 'code'
def record_entry(entry_type, section, **kwargs):
entries.append({'type': entry_type, 'section': section, **kwargs})
for idx, raw in enumerate(lines, 1):
stripped = raw.split(';', 1)[0].strip()
if not stripped:
continue
while ':' in stripped:
label_candidate, remainder = stripped.split(':', 1)
label_name = label_candidate.strip()
if not label_name.isidentifier():
break
if label_name in labels:
raise AssemblerError(f"Duplicate label '{label_name}' (line {idx})")
labels[label_name] = {
'section': current_section,
'offset': offsets[current_section],
'absolute': None,
}
stripped = remainder.strip()
if not stripped:
break
if not stripped:
continue
if stripped.startswith('.'):
parts = stripped.split(None, 1)
directive = parts[0].lower()
operand_text = parts[1] if len(parts) > 1 else ''
operands = split_operands(operand_text)
if directive == '.origin':
if len(operands) != 1:
raise AssemblerError(f".origin expects one value (line {idx})")
value = parse_number(operands[0])
bases[current_section] = value
offsets[current_section] = 0
elif directive == '.byte':
record_entry(
'data' if current_section == 'data' else 'raw',
current_section,
values=operands,
offset=offsets[current_section],
line=idx,
)
offsets[current_section] += len(operands)
elif directive == '.space':
if len(operands) != 1:
raise AssemblerError(f".space expects one value (line {idx})")
width = parse_number(operands[0])
record_entry('space', current_section, size=width, offset=offsets[current_section], line=idx)
offsets[current_section] += width
elif directive == '.code':
current_section = 'code'
elif directive == '.data':
current_section = 'data'
elif directive == '.name':
continue
else:
raise AssemblerError(f"Unknown directive '{directive}' (line {idx})")
continue
if current_section != 'code':
raise AssemblerError(f"Instructions are only allowed in .code section (line {idx})")
parts = stripped.split(None, 1)
mnemonic = parts[0]
operand_text = parts[1] if len(parts) > 1 else ''
operands = split_operands(operand_text)
record_entry('instr', 'code', mnemonic=mnemonic, operands=operands, offset=offsets['code'], line=idx)
offsets['code'] += 3
lengths = {section: offsets[section] for section in sections}
return entries, labels, bases, lengths
def assemble_source(text):
lines = text.splitlines()
entries, labels, bases, lengths = parse_source(lines)
code_base = bases['code'] if bases['code'] is not None else 0
code_length = lengths['code']
data_length = lengths['data']
if data_length:
if bases['data'] is not None:
data_base = bases['data']
else:
data_base = code_base + code_length
else:
data_base = bases['data'] if bases['data'] is not None else code_base + code_length
total_length = 0
total_length = max(total_length, code_base + code_length)
total_length = max(total_length, data_base + data_length)
output = bytearray(total_length)
for name, info in labels.items():
section = info['section']
offset = info['offset']
base = code_base if section == 'code' else data_base
info['absolute'] = base + offset
for entry in entries:
base = code_base if entry['section'] == 'code' else data_base
target_address = base + entry['offset']
if entry['type'] == 'instr':
encoded = parse_instruction_entry(entry['mnemonic'], entry['operands'], labels, entry['line'])
output[target_address:target_address+3] = encoded
elif entry['type'] == 'data':
values = [resolve_byte_value(value, labels) for value in entry['values']]
output[target_address:target_address+len(values)] = bytes(values)
elif entry['type'] == 'raw':
raw_values = [resolve_byte_value(value, labels) for value in entry['values']]
output[target_address:target_address+len(raw_values)] = bytes(raw_values)
elif entry['type'] == 'space':
output[target_address:target_address+entry['size']] = b'\x00' * entry['size']
else:
raise AssemblerError(f"Unknown entry type '{entry['type']}'")
return bytes(output)
def disassemble_bytes(data, base_addr=0, max_bytes=None):
length = len(data)
if max_bytes is not None:
length = min(length, max_bytes)
print("Yan85 Bytecode Disassembly")
print("=" * 50)
print(f"Total bytes: {length}")
print(f"Instructions: {length // 3}")
print()
addr = 0
while addr + 2 < length:
instruction_bytes = data[addr:addr+3]
decoded = decode_instruction(instruction_bytes)
absolute_addr = base_addr + addr
print(format_instruction(decoded, absolute_addr))
addr += 3
if addr < length:
remaining = data[addr:length]
print(f"0x{base_addr + addr:04x}: leftover bytes {remaining.hex()}")
print()
print("Disassembly complete.")
def load_binary_input(path):
if path in (None, '-'):
return sys.stdin.buffer.read()
return Path(path).read_bytes()
def write_output_bytes(data, path, emit_array=False):
if path not in (None, '-'):
Path(path).write_bytes(data)
print(f"[+] Wrote {len(data)} bytes to {path}", file=sys.stderr)
else:
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
if emit_array:
values = ', '.join(f"0x{b:02x}" for b in data)
print(values, file=sys.stderr)
def read_text_source(path):
if path in (None, '-'):
return sys.stdin.read()
return Path(path).read_text()
def build_arg_parser():
parser = argparse.ArgumentParser(description="Yan85 disassembler/assembler")
parser.add_argument('-v', '--verbose', action='store_true', help='show opcode metadata while disassembling')
parser.add_argument('-c', '--config', help='load configuration overrides from JSON file')
parser.add_argument('--write-config', metavar='PATH', nargs='?', const='-',
help='dump the current config (after overrides) to PATH (default: stdout)')
subparsers = parser.add_subparsers(dest='subcommand', required=True)
asm_parser = subparsers.add_parser('asm', aliases=['a', 'as'], help='assemble Yan85 text into bytecode')
asm_parser.add_argument('source', nargs='?', default='-', help='assembly input file (default: stdin)')
asm_parser.add_argument('-o', '--output', type=str, default='-', help='output binary path (default: stdout)')
asm_parser.add_argument('--emit-array', action='store_true', help='also dump a Python-style byte array to stderr')
dis_parser = subparsers.add_parser('disas', aliases=['d', 'dis', 'disasm'], help='disassemble Yan85 bytecode into text')
dis_parser.add_argument('input', nargs='?', default='-', help='binary input file (default: stdin)')
dis_parser.add_argument('--base', type=lambda v: int(v, 0), default=0, help='base address for listings')
dis_parser.add_argument('--count', type=lambda v: int(v, 0), help='maximum bytes to disassemble')
write_parser = subparsers.add_parser('writeconfig', aliases=['w'], help='write the current config to stdout or file')
write_parser.add_argument('-o', '--output', metavar='PATH', default='-', help='target path (default: stdout)')
return parser
def main(argv=None):
global SHOW_DEBUG_METADATA
parser = build_arg_parser()
args = parser.parse_args(argv)
SHOW_DEBUG_METADATA = args.verbose
if args.config:
load_config_file(args.config)
elif DEFAULT_CONFIG_PATH.exists():
load_config_file(DEFAULT_CONFIG_PATH)
if args.write_config and args.subcommand not in ('writeconfig', 'w'):
dump_active_config(args.write_config)
subcommand = args.subcommand.lower()
if subcommand in ('writeconfig', 'w'):
dump_active_config(getattr(args, 'output', '-'))
return
if subcommand.startswith('a'):
try:
source_text = read_text_source(args.source)
except FileNotFoundError as exc:
print(f"[!] Cannot read assembly: {exc}", file=sys.stderr)
sys.exit(1)
try:
bytecode = assemble_source(source_text)
except AssemblerError as exc:
print(f"[!] Assembly failed: {exc}", file=sys.stderr)
sys.exit(1)
target = None if args.output in (None, '-') else args.output
write_output_bytes(bytecode, target, args.emit_array)
return
# disassembly path
try:
data = load_binary_input(args.input)
except FileNotFoundError as exc:
print(f"[!] Cannot read input: {exc}", file=sys.stderr)
sys.exit(1)
if not data:
print('[!] No input data provided for disassembly', file=sys.stderr)
sys.exit(1)
disassemble_bytes(data, base_addr=args.base, max_bytes=args.count)
if __name__ == "__main__":
main()
pc-yansanity-catflag.y85
neuland% cat pc-yansanity-catflag.y85
.name "dump_flag"
.origin 0x000
.code
start:
; copy "/flag\0" using REG_A=source byte, REG_B=offset selector
IMM REG_A, '/'
IMM REG_B, 0
STM REG_B, REG_A
IMM REG_A, 'f'
IMM REG_B, 1
STM REG_B, REG_A
IMM REG_A, 'l'
IMM REG_B, 2
STM REG_B, REG_A
IMM REG_A, 'a'
IMM REG_B, 3
STM REG_B, REG_A
IMM REG_A, 'g'
IMM REG_B, 4
STM REG_B, REG_A
IMM REG_A, 0
IMM REG_B, 5
STM REG_B, REG_A
; fd = open("/flag", O_RDONLY)
IMM REG_C, 0 ; pointer = 0x300 + 0
IMM REG_A, 0x00 ; flags register
IMM REG_B, 0x00 ; mode register
SYS OPEN, REG_C ; fd returned in REG_C
; shuffle fd (REG_C) into SYS fd register (REG_A)
IMM REG_B, 0x30 ; scratch offset
STM REG_B, REG_C
LDM REG_A, REG_B
; read(fd, buf, 57)
IMM REG_B, 0x20 ; buffer offset within VM window
IMM REG_C, 57
SYS READ_MEMORY, REG_A
; write(1, buf, 57)
IMM REG_A, 1 ; stdout fd
IMM REG_B, 0x20
IMM REG_C, 57
SYS WRITE, REG_A
; exit(42)
IMM REG_A, 42
SYS EXIT, REG_A
Solution - all parts put together:
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-all ; echo EXIT CODE: $?'
…
[+] Found SYS_WRITE configuration: {'layout': {'arg_high': 1, 'arg_low': 0, 'op_byte': 2}, 'imm_opcode': 16, 'sys_opcode': 1, 'stm_opcode': 4, 'ldm_opcode': 32, 'register_exit': 16, 'register_buf': 64, 'register_len': 32, 'sys_mask_exit': 64, 'sys_mask_read': 1, 'sys_mask_open': 8, 'sys_mask_write': 32, 'write_bytes': [58, 58, 89, 65, 78, 56, 53, 45, 87, 82, 73, 84, 69, 58, 58, 10], 'write_offset': 48, 'write_fd': 1, 'write_len': 16, 'flag_state': {'path': '/flag', 'size': 57, 'mtime_ns': 1772225854616020268, 'mtime': '2026-02-27T20:57:34.616020', 'hostname': 'reverse-engineering~yansanity-easy', 'host_label': 'reverse-engineering~yansanity-easy'}}
[+] Completed all stages.
EXIT CODE: 0
neuland%
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py build-config ; echo EXIT CODE: $?'
…
[+] wrote config to pc-yansanity-asm-config.json
[*] Assignment status:
- registers: confirmed -> REG_A, REG_B, REG_C
- registers: inferred -> REG_D, REG_S, REG_I, REG_F
- instructions: confirmed -> IMM, STM, LDM, SYS
- instructions: inferred -> STK, ADD, JMP, CMP
- syscalls: confirmed -> WRITE, OPEN, READ_MEMORY, EXIT
- syscalls: inferred -> SLEEP, READ_CODE
- flags: inferred -> LESS, GREATER, EQUAL, NOT_EQUAL, BOTH_ZERO
EXIT CODE: 0
neuland%
neuland% scp pc:/tmp/\*.json .
pc-yansanity-asm-config.json 100% 2176 6.5KB/s 00:00
pc-yansanity-cache.json 100% 6220 18.4KB/s 00:00
neuland%
neuland% python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis
Yan85 Bytecode Disassembly
==================================================
Total bytes: 102
Instructions: 34
0x0000 (i=0x000): 2f 10 10 | /.. | IMM REG_A = 0x2f / '/'
0x0003 (i=0x001): 00 40 10 | .@. | IMM REG_B = 0x00
0x0006 (i=0x002): 10 40 04 | .@. | STM memory[0x300 + REG_B] = REG_A
0x0009 (i=0x003): 66 10 10 | f.. | IMM REG_A = 0x66 / 'f'
0x000c (i=0x004): 01 40 10 | .@. | IMM REG_B = 0x01
0x000f (i=0x005): 10 40 04 | .@. | STM memory[0x300 + REG_B] = REG_A
0x0012 (i=0x006): 6c 10 10 | l.. | IMM REG_A = 0x6c / 'l'
0x0015 (i=0x007): 02 40 10 | .@. | IMM REG_B = 0x02
0x0018 (i=0x008): 10 40 04 | .@. | STM memory[0x300 + REG_B] = REG_A
0x001b (i=0x009): 61 10 10 | a.. | IMM REG_A = 0x61 / 'a'
0x001e (i=0x00a): 03 40 10 | .@. | IMM REG_B = 0x03
0x0021 (i=0x00b): 10 40 04 | .@. | STM memory[0x300 + REG_B] = REG_A
0x0024 (i=0x00c): 67 10 10 | g.. | IMM REG_A = 0x67 / 'g'
0x0027 (i=0x00d): 04 40 10 | .@. | IMM REG_B = 0x04
0x002a (i=0x00e): 10 40 04 | .@. | STM memory[0x300 + REG_B] = REG_A
0x002d (i=0x00f): 00 10 10 | ... | IMM REG_A = 0x00
0x0030 (i=0x010): 05 40 10 | .@. | IMM REG_B = 0x05
0x0033 (i=0x011): 10 40 04 | .@. | STM memory[0x300 + REG_B] = REG_A
0x0036 (i=0x012): 00 20 10 | . . | IMM REG_C = 0x00
0x0039 (i=0x013): 00 10 10 | ... | IMM REG_A = 0x00
0x003c (i=0x014): 00 40 10 | .@. | IMM REG_B = 0x00
0x003f (i=0x015): 20 08 01 | .. | SYS OPEN, ret=REG_C (syscall# 0x08)
0x0042 (i=0x016): 30 40 10 | 0@. | IMM REG_B = 0x30 / '0'
0x0045 (i=0x017): 20 40 04 | @. | STM memory[0x300 + REG_B] = REG_C
0x0048 (i=0x018): 40 10 20 | @. | LDM REG_A = memory[0x300 + REG_B]
0x004b (i=0x019): 20 40 10 | @. | IMM REG_B = 0x20 / ' '
0x004e (i=0x01a): 39 20 10 | 9 . | IMM REG_C = 0x39 / '9'
0x0051 (i=0x01b): 10 01 01 | ... | SYS READ_MEMORY, ret=REG_A (syscall# 0x01)
0x0054 (i=0x01c): 01 10 10 | ... | IMM REG_A = 0x01
0x0057 (i=0x01d): 20 40 10 | @. | IMM REG_B = 0x20 / ' '
0x005a (i=0x01e): 39 20 10 | 9 . | IMM REG_C = 0x39 / '9'
0x005d (i=0x01f): 10 20 01 | . . | SYS WRITE, ret=REG_A (syscall# 0x20)
0x0060 (i=0x020): 2a 10 10 | *.. | IMM REG_A = 0x2a / '*'
0x0063 (i=0x021): 10 40 01 | .@. | SYS EXIT, ret=REG_A (syscall# 0x40)
Disassembly complete.
neuland%
neuland% python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | ssh pc /challenge/yansanity-easy
[+] Welcome to /challenge/yansanity-easy!
[+] This challenge is an custom emulator. It emulates a completely custom
[+] architecture that we call "Yan85"! You'll have to understand the
[+] emulator to understand the architecture, and you'll have to understand
[+] the architecture to understand the code being emulated, and you will
[+] have to understand that code to get the flag. Good luck!
[+]
[+] This level is a full Yan85 emulator. You'll have to reason about yancode,
[+] and the implications of how the emulator interprets it!
[?] This challenge is special! It randomizes the Yan85 VM based on
[?] the value of the flag. This means that there is no way for you
[?] to know the opcode and argument encodings...
[?]
[?] Keep in mind that the encoding that you observe in practice mode
[?] is going to be different than the actual encoding, because the
[?] practice mode flag is different. How will you adapt?
[?]
[?] Is there maybe a clever side channel you can utilize?
[?] ... Done! VM is randomized!
[!] This time, YOU'RE in control! Please input your yancode: [+]
[+] This is a *teaching* challenge, which means that it will output
[+] a trace of the Yan85 code as it processes it. The output is here
[+] for you to understand what the challenge is doing, and you should use
[+] it as a guide to help with your reversing of the code.
[+]
[V] a:0 b:0 c:0 d:0 s:0 i:0x1 f:0
[I] op:0x10 arg1:0x10 arg2:0x2f
[s] IMM a = 0x2f
[V] a:0x2f b:0 c:0 d:0 s:0 i:0x2 f:0
[I] op:0x10 arg1:0x40 arg2:0
[s] IMM b = 0
[V] a:0x2f b:0 c:0 d:0 s:0 i:0x3 f:0
[I] op:0x4 arg1:0x40 arg2:0x10
[s] STM *b = a
[V] a:0x2f b:0 c:0 d:0 s:0 i:0x4 f:0
[I] op:0x10 arg1:0x10 arg2:0x66
[s] IMM a = 0x66
[V] a:0x66 b:0 c:0 d:0 s:0 i:0x5 f:0
[I] op:0x10 arg1:0x40 arg2:0x1
[s] IMM b = 0x1
[V] a:0x66 b:0x1 c:0 d:0 s:0 i:0x6 f:0
[I] op:0x4 arg1:0x40 arg2:0x10
[s] STM *b = a
[V] a:0x66 b:0x1 c:0 d:0 s:0 i:0x7 f:0
[I] op:0x10 arg1:0x10 arg2:0x6c
[s] IMM a = 0x6c
[V] a:0x6c b:0x1 c:0 d:0 s:0 i:0x8 f:0
[I] op:0x10 arg1:0x40 arg2:0x2
[s] IMM b = 0x2
[V] a:0x6c b:0x2 c:0 d:0 s:0 i:0x9 f:0
[I] op:0x4 arg1:0x40 arg2:0x10
[s] STM *b = a
[V] a:0x6c b:0x2 c:0 d:0 s:0 i:0xa f:0
[I] op:0x10 arg1:0x10 arg2:0x61
[s] IMM a = 0x61
[V] a:0x61 b:0x2 c:0 d:0 s:0 i:0xb f:0
[I] op:0x10 arg1:0x40 arg2:0x3
[s] IMM b = 0x3
[V] a:0x61 b:0x3 c:0 d:0 s:0 i:0xc f:0
[I] op:0x4 arg1:0x40 arg2:0x10
[s] STM *b = a
[V] a:0x61 b:0x3 c:0 d:0 s:0 i:0xd f:0
[I] op:0x10 arg1:0x10 arg2:0x67
[s] IMM a = 0x67
[V] a:0x67 b:0x3 c:0 d:0 s:0 i:0xe f:0
[I] op:0x10 arg1:0x40 arg2:0x4
[s] IMM b = 0x4
[V] a:0x67 b:0x4 c:0 d:0 s:0 i:0xf f:0
[I] op:0x4 arg1:0x40 arg2:0x10
[s] STM *b = a
[V] a:0x67 b:0x4 c:0 d:0 s:0 i:0x10 f:0
[I] op:0x10 arg1:0x10 arg2:0
[s] IMM a = 0
[V] a:0 b:0x4 c:0 d:0 s:0 i:0x11 f:0
[I] op:0x10 arg1:0x40 arg2:0x5
[s] IMM b = 0x5
[V] a:0 b:0x5 c:0 d:0 s:0 i:0x12 f:0
[I] op:0x4 arg1:0x40 arg2:0x10
[s] STM *b = a
[V] a:0 b:0x5 c:0 d:0 s:0 i:0x13 f:0
[I] op:0x10 arg1:0x20 arg2:0
[s] IMM c = 0
[V] a:0 b:0x5 c:0 d:0 s:0 i:0x14 f:0
[I] op:0x10 arg1:0x10 arg2:0
[s] IMM a = 0
[V] a:0 b:0x5 c:0 d:0 s:0 i:0x15 f:0
[I] op:0x10 arg1:0x40 arg2:0
[s] IMM b = 0
[V] a:0 b:0 c:0 d:0 s:0 i:0x16 f:0
[I] op:0x1 arg1:0x8 arg2:0x20
[s] SYS 0x8 c
[s] ... open
[s] ... return value (in register c): 0x4
[V] a:0 b:0 c:0x4 d:0 s:0 i:0x17 f:0
[I] op:0x10 arg1:0x40 arg2:0x30
[s] IMM b = 0x30
[V] a:0 b:0x30 c:0x4 d:0 s:0 i:0x18 f:0
[I] op:0x4 arg1:0x40 arg2:0x20
[s] STM *b = c
[V] a:0 b:0x30 c:0x4 d:0 s:0 i:0x19 f:0
[I] op:0x20 arg1:0x10 arg2:0x40
[s] LDM a = *b
[V] a:0x4 b:0x30 c:0x4 d:0 s:0 i:0x1a f:0
[I] op:0x10 arg1:0x40 arg2:0x20
[s] IMM b = 0x20
[V] a:0x4 b:0x20 c:0x4 d:0 s:0 i:0x1b f:0
[I] op:0x10 arg1:0x20 arg2:0x39
[s] IMM c = 0x39
[V] a:0x4 b:0x20 c:0x39 d:0 s:0 i:0x1c f:0
[I] op:0x1 arg1:0x1 arg2:0x10
[s] SYS 0x1 a
[s] ... read_memory
[s] ... return value (in register a): 0x39
[V] a:0x39 b:0x20 c:0x39 d:0 s:0 i:0x1d f:0
[I] op:0x10 arg1:0x10 arg2:0x1
[s] IMM a = 0x1
[V] a:0x1 b:0x20 c:0x39 d:0 s:0 i:0x1e f:0
[I] op:0x10 arg1:0x40 arg2:0x20
[s] IMM b = 0x20
[V] a:0x1 b:0x20 c:0x39 d:0 s:0 i:0x1f f:0
[I] op:0x10 arg1:0x20 arg2:0x39
[s] IMM c = 0x39
[V] a:0x1 b:0x20 c:0x39 d:0 s:0 i:0x20 f:0
[I] op:0x1 arg1:0x20 arg2:0x10
[s] SYS 0x20 a
[s] ... write
pwn.college{YAPebivgk9JiAzwijavcNgigGiJ.01N4IDL2ATOykzW}
[s] ... return value (in register a): 0x39
[V] a:0x39 b:0x20 c:0x39 d:0 s:0 i:0x21 f:0
[I] op:0x10 arg1:0x10 arg2:0x2a
[s] IMM a = 0x2a
[V] a:0x2a b:0x20 c:0x39 d:0 s:0 i:0x22 f:0
[I] op:0x1 arg1:0x40 arg2:0x10
[s] SYS 0x40 a
[s] ... exit
neuland%
Summary by Windsurf (with GPT-5.1-Codex) with focus on y85 code:
pwn.college{YAPebivgk9JiAzwijavcNgigGiJ.01N4IDL2ATOykzW}
Analysis:
Solution - overview:
date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-all ; echo EXIT CODE: $?'
date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py build-config ; echo EXIT CODE: $?'
scp pc:/tmp/\*.json .
python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis
( python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 ) | ssh pc "sudo strace -o /tmp/x /challenge/yansanity-hard ; echo STRACE: ; tail /tmp/x"
python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | ssh pc /challenge/yansanity-hard
Solution:
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py find-all ; echo EXIT CODE: $?'
…
[+] Found SYS_OPEN configuration: {'layout': {'arg_high': 1, 'arg_low': 0, 'op_byte': 2}, 'imm_opcode': 128, 'sys_opcode': 1, 'stm_opcode': 2, 'ldm_opcode': 16, 'register_exit': 4, 'register_buf': 32, 'register_len': 2, 'sys_mask_exit': 32, 'sys_mask_read': 4, 'sys_mask_open': 128, 'probe_path': '/etc/group', 'flag_value': 0, 'mode_value': 0, 'ptr_register': 2, 'flags_register': 4, 'mode_register': 32, 'flag_state': {'path': '/flag', 'size': 57, 'mtime_ns': 1772231375308906799, 'mtime': '2026-02-27T22:29:35.308907', 'hostname': 'reverse-engineering~yansanity-hard', 'host_label': 'reverse-engineering~yansanity-hard'}}
[*] Running find-write stage...
[*] SYS_WRITE sweep (stdout marker check)...
[*] Flag signature: path=/flag size=57 mtime_ns=1772231375308906799 host=reverse-engineering~yansanity-hard
[ 20%] write=0x01 fd=0x01 offset=0x30 len=0x10 rc=0 stdout_len=1087
[+] Found SYS_WRITE configuration: {'layout': {'arg_high': 1, 'arg_low': 0, 'op_byte': 2}, 'imm_opcode': 128, 'sys_opcode': 1, 'stm_opcode': 2, 'ldm_opcode': 16, 'register_exit': 4, 'register_buf': 32, 'register_len': 2, 'sys_mask_exit': 32, 'sys_mask_read': 4, 'sys_mask_open': 128, 'sys_mask_write': 1, 'write_bytes': [58, 58, 89, 65, 78, 56, 53, 45, 87, 82, 73, 84, 69, 58, 58, 10], 'write_offset': 48, 'write_fd': 1, 'write_len': 16, 'flag_state': {'path': '/flag', 'size': 57, 'mtime_ns': 1772231375308906799, 'mtime': '2026-02-27T22:29:35.308907', 'hostname': 'reverse-engineering~yansanity-hard', 'host_label': 'reverse-engineering~yansanity-hard'}}
[+] Completed all stages.
EXIT CODE: 0
neuland%
neuland% date ; echo SMOKE TEST: ; python3 pc-yansanity-configfinder.py -h && echo SCP: ; scp pc-yansanity-configfinder.py pc:/tmp ; echo SSH: ; ssh pc 'cd /tmp ; python3 -u pc-yansanity-configfinder.py build-config ; echo EXIT CODE: $?'
…
[+] wrote config to pc-yansanity-asm-config.json
[*] Assignment status:
- registers: confirmed -> REG_A, REG_B, REG_C
- registers: inferred -> REG_D, REG_S, REG_I, REG_F
- instructions: confirmed -> IMM, STM, LDM, SYS
- instructions: inferred -> STK, ADD, JMP, CMP
- syscalls: confirmed -> WRITE, OPEN, READ_MEMORY, EXIT
- syscalls: inferred -> SLEEP, READ_CODE
- flags: inferred -> LESS, GREATER, EQUAL, NOT_EQUAL, BOTH_ZERO
EXIT CODE: 0
neuland% scp pc:/tmp/\*.json .
pc-yansanity-asm-config.json 100% 2178 6.5KB/s 00:00
pc-yansanity-cache.json 100% 6218 18.2KB/s 00:00
neuland%
neuland% python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis
Yan85 Bytecode Disassembly
==================================================
Total bytes: 102
Instructions: 34
0x0000 (i=0x000): 2f 04 80 | /.. | IMM REG_A = 0x2f / '/'
0x0003 (i=0x001): 00 20 80 | . . | IMM REG_B = 0x00
0x0006 (i=0x002): 04 20 02 | . . | STM memory[0x300 + REG_B] = REG_A
0x0009 (i=0x003): 66 04 80 | f.. | IMM REG_A = 0x66 / 'f'
0x000c (i=0x004): 01 20 80 | . . | IMM REG_B = 0x01
0x000f (i=0x005): 04 20 02 | . . | STM memory[0x300 + REG_B] = REG_A
0x0012 (i=0x006): 6c 04 80 | l.. | IMM REG_A = 0x6c / 'l'
0x0015 (i=0x007): 02 20 80 | . . | IMM REG_B = 0x02
0x0018 (i=0x008): 04 20 02 | . . | STM memory[0x300 + REG_B] = REG_A
0x001b (i=0x009): 61 04 80 | a.. | IMM REG_A = 0x61 / 'a'
0x001e (i=0x00a): 03 20 80 | . . | IMM REG_B = 0x03
0x0021 (i=0x00b): 04 20 02 | . . | STM memory[0x300 + REG_B] = REG_A
0x0024 (i=0x00c): 67 04 80 | g.. | IMM REG_A = 0x67 / 'g'
0x0027 (i=0x00d): 04 20 80 | . . | IMM REG_B = 0x04
0x002a (i=0x00e): 04 20 02 | . . | STM memory[0x300 + REG_B] = REG_A
0x002d (i=0x00f): 00 04 80 | ... | IMM REG_A = 0x00
0x0030 (i=0x010): 05 20 80 | . . | IMM REG_B = 0x05
0x0033 (i=0x011): 04 20 02 | . . | STM memory[0x300 + REG_B] = REG_A
0x0036 (i=0x012): 00 02 80 | ... | IMM REG_C = 0x00
0x0039 (i=0x013): 00 04 80 | ... | IMM REG_A = 0x00
0x003c (i=0x014): 00 20 80 | . . | IMM REG_B = 0x00
0x003f (i=0x015): 02 80 01 | ... | SYS OPEN, ret=REG_C (syscall# 0x80)
0x0042 (i=0x016): 30 20 80 | 0 . | IMM REG_B = 0x30 / '0'
0x0045 (i=0x017): 02 20 02 | . . | STM memory[0x300 + REG_B] = REG_C
0x0048 (i=0x018): 20 04 10 | .. | LDM REG_A = memory[0x300 + REG_B]
0x004b (i=0x019): 20 20 80 | . | IMM REG_B = 0x20 / ' '
0x004e (i=0x01a): 39 02 80 | 9.. | IMM REG_C = 0x39 / '9'
0x0051 (i=0x01b): 04 04 01 | ... | SYS READ_MEMORY, ret=REG_A (syscall# 0x04)
0x0054 (i=0x01c): 01 04 80 | ... | IMM REG_A = 0x01
0x0057 (i=0x01d): 20 20 80 | . | IMM REG_B = 0x20 / ' '
0x005a (i=0x01e): 39 02 80 | 9.. | IMM REG_C = 0x39 / '9'
0x005d (i=0x01f): 04 01 01 | ... | SYS WRITE, ret=REG_A (syscall# 0x01)
0x0060 (i=0x020): 2a 04 80 | *.. | IMM REG_A = 0x2a / '*'
0x0063 (i=0x021): 04 20 01 | . . | SYS EXIT, ret=REG_A (syscall# 0x20)
Disassembly complete.
neuland%
neuland% python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | ssh pc /challenge/yansanity-hard
[+] Welcome to /challenge/yansanity-hard!
[+] This challenge is an custom emulator. It emulates a completely custom
[+] architecture that we call "Yan85"! You'll have to understand the
[+] emulator to understand the architecture, and you'll have to understand
[+] the architecture to understand the code being emulated, and you will
[+] have to understand that code to get the flag. Good luck!
[+]
[+] This level is a full Yan85 emulator. You'll have to reason about yancode,
[+] and the implications of how the emulator interprets it!
[?] This challenge is special! It randomizes the Yan85 VM based on
[?] the value of the flag. This means that there is no way for you
[?] to know the opcode and argument encodings...
[?]
[?] Keep in mind that the encoding that you observe in practice mode
[?] is going to be different than the actual encoding, because the
[?] practice mode flag is different. How will you adapt?
[?]
[?] Is there maybe a clever side channel you can utilize?
[?] ... Done! VM is randomized!
[!] This time, YOU'RE in control! Please input your yancode: pwn.college{Q7XcDoS6o3pGuVu4eMqC8bH0PAv.0FO4IDL2ATOykzW}
neuland%
pwn.college{Q7XcDoS6o3pGuVu4eMqC8bH0PAv.0FO4IDL2ATOykzW}
Summary:
https://mastodon.social/@hubertf/116145111334304964