pwn.college notes: yansanity

🟡 Yellow Belt -> Program Security -> Reverse Engineering -> Yan85 -> Yansanity

hubertf, 2026-02-28

Contents:

✅ Yansanity (Easy)

✅ Yansanity (Hard)


✅ Yansanity (Easy)

Analysis:

  1. randomization of VM based on flag - different between production/practice mode -> rerandomize() function -> shuffle_values() - rand()
  2. srand() is in flag_seed(), which uses srand-value based on contents of first 32 bytes of /flag, i.e. is constant across runs - but needs to be determined at runtime. Also: /flag is different in practice and real mode, so cannot use practice mode. As we don't know /flag, we don't know the srand() val - but it's constant for us. Also means the various aspects of the VM are constant - but also unknown.
  3. Where is flag_seed() called?
  1. In _start:
    __libc_start_main(main,param_2,&stack0x00000008,__libc_csu_init,__libc_csu_fini,param_1,auStack_8)
  2. __libc_csu_init() reads .init_array / __frame_dummy_init_array_entry which has flag_seed (and frame_dummy).

        -> flag_seed() runs before main()

  1. How to find out what settings the VM has? e.g. instruction structure (opcode, args), instruction opcodes, register values etc.
  2. First idea: look for IMM - but we cannot trace that!
  3. Second idea: find 2 proper bytes for SYS and SYS_EXIT, look with strace/ltrace if there's our exit() call
  4. Assumption: -easy and -hard have the same instruction format (opcode in MSB, args in the LSBs), which saves lots of time. Also, besides more debug output, -hard seems to be mostly the same as hard.
  5. [[[ solving this for -hard now, to use the same solution for -easy later]]]
  6. Step 1: find working opcodes for running exit() with a given value.

        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%

  1. Step 2: figure out registers by fuzzing for the following idea:
    read 1 byte via SYS_READ_MEMORY from stdin, then pass that to SYS_EXIT

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'}}

  1. Step 3: figure out opcodes for STM (and LDM) by putting some value at 0x300 via STM, then reading it back with LDM and using it as SYS_EXIT code.

    yan85 code:
    # 1. prepare STM

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'}}

  1. We can now rewrite the previous catflag.y85 program to not use REG_D -> done
  2. Step 4: What's left is the syscall numbers for SYS_OPEN (reading /flag) and SYS_WRITE (writing to stdout). For SYS_OPEN, we open /flag and use the filedescriptor as exit code. For SYS_WRITE, we write a single byte to stdout and see if that works - repeat with a few bytes to make sure everything works as expected.


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        

  1. Backpedal - cannot get this working, so split find-openwrite into find-open and find-write. Also, make triple-sure that STM and LDM work as expected. So:
  2. Step 4 (new): add "verify-stmldm" to test STM and LDM with different values and offsets.

        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.

  1. Step 5 (new):  find-open to open "f" and return the file descriptor via exit(). Early tests didn't work, which resulted in verify-stmldm. Idea is to open "f" and return the file descriptor via exit(fd). Make extra sure that register for return-value is properly encoded in instruction.

    Also, make EXTRA sure that testing is actually not done with "f" as a symlink (e.g. to /etc/group) but as real path "/etc/group". We may have included O_NOFOLLOW in our open() flags by accident…

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

  1. Step 5: find-write

        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.

  1. Then it prepares the write syscall:

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.

  1. Step 6: consolidate what we know up to now, and assemble it into a config file for our assembler/disassembler in pc-yansanity-asm-config.json. Add what we known (confirmed) and what "filler" we use (inferred).

        

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"

  }

}

  1. Last, assemble our catflat.y85 file and run it! Target practice in privileged mode! First assemble & disassemble, then run:

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.

  1. Now just assemble and send the bytecode over to the target:

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 +++

  1. So much for the testrun. Now repeat this one in the actual easy-host to finally get the flag! (... after like 2 weeks of work!)

Solution - overview:

  1. Restart host to start from scratch
  2. Figure out settings with find-all:

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: $?'

  1. Consolidate findings from cache, fill rest and create asm-config.json:

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: $?'

  1. Grab files for local assembling:

scp pc:/tmp/\*.json .

  1. Assemble & disassemble for testing:

python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis

  1. If on practice host, use strace to see syscalls:

( 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"

  1. Assemble and run on the target system:

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:

  1. sys-exit – We send minimal 3-byte programs that try candidate opcode layouts plus syscall masks: load the presumed exit register with IMM, invoke SYS using the mask under test, and observe which combination actually triggers exit_group(rc). That simultaneously nails down the actual IMM opcode, SYS opcode, exit mask, and exit register.
  2. sys-readmem – With IMM/SYS fixed, we brute-force buffer/length/pointer roles by emitting programs that write a marker into scratch memory via IMM+STM, call SYS READ_MEMORY with mask guesses, then mirror whichever register got the data back into the exit register before exiting. Successful payloads reveal the READ_MEMORY mask plus the buffer/len/pointer registers.
  3. find-stm & verify-stmldm – find-stm produces sequences that slot candidate opcodes between IMM writes to see which one actually stores bytes at our scratch address. Once a storing opcode is discovered, verify-stmldm crafts a round-trip (IMM → STM, then LDM) to confirm STM/LDM pairing and ensure they don’t collide with already-confirmed opcodes.
  4. find-open – Using the known instruction layout, we build longer payloads that write group (or another probe path) into memory byte-by-byte, set up flag/mode immediates, and issue SYS OPEN while sweeping mask bits. The return file descriptor gets copied back into the exit register before SYS EXIT, so the kernel return code identifies the correct OPEN mask and the registers used for pointer/flags/mode.
  5. find-write – Similar to find-open: the payload writes a marker buffer, loads fd/pointer/length registers, tests candidate WRITE masks, and arranges for the exit status (or stdout bytes) to confirm success, copying the count written into the exit register just before the final SYS EXIT.
  6. build-config – Once each stage has cached its confirmed values, this command collates them into pc-yansanity-asm-config.json, tagging every opcode/register/syscall mask as confirmed versus inferred. That config feeds the assembler/disassembler so catflag.y85 assembles cleanly and matches the remote challenge.

pwn.college{YAPebivgk9JiAzwijavcNgigGiJ.01N4IDL2ATOykzW}

✅ Yansanity (Hard)

Analysis:

  1. see 🚴 Yansanity (Easy) - we did develop this with hard & practice-mode

Solution - overview:

  1. Restart host to start from scratch
  2. Figure out settings with find-all:

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: $?'

  1. Consolidate findings from cache, fill rest and create asm-config.json:

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: $?'

  1. Grab files for local assembling:

scp pc:/tmp/\*.json .

  1. Assemble & disassemble for testing:

python3 pc-yansanity-asm.py asm pc-yansanity-catflag.y85 | python3 pc-yansanity-asm.py dis

  1. If on practice host, use strace to see syscalls:

( 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"

  1. Assemnle and run on the target system:

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