← Back to Writeups
CUSTOMMediumReversing

Tri-piki

XESXOR9/2/20263 min read
#reversing#custom#medium#charbychar_flag_reconstruction#code_object_inspection#manual_bytecode_analysis#nop_stripping_disassembly

Tri-piki

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: charbychar_flag_reconstruction, code_object_inspection, manual_bytecode_analysis, nop_stripping_disassembly

Summary

Walkthrough of the CUSTOM Reversing Tri-piki. Task: Python 2.7 .pyc file with NOP-padded bytecode that breaks standard decompilers. Solution: strip 0x09 NOP filler bytes between instructions, manually disassemble to reveal character-by-character

Description

И заключительный этап - добавил кое-что еще

English summary: Final stage of a series ("Tri piki" = "Three spades/peaks", third in a series). A Python 2.7 byte-compiled file pyc_spbctf_3.n.pyc (1469 bytes) is provided. The hint "added something extra" refers to NOP bytes injected into the bytecode stream.

Analysis

Initial Recon

The file is identified as a Python 2.7 byte-compiled module. Standard decompilers (uncompyle6, decompyle3) immediately fail with a parse error at offset 0 — the bytecode stream is corrupted from the decompiler's perspective. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Identifying the Obfuscation

Hex dump of the bytecode reveals a systematic pattern: after every real bytecode instruction, a 0x09 byte (NOP opcode in Python 2.7) is inserted. This padding breaks decompilers because they expect instructions to follow each other contiguously without filler.

Pattern example from the hex dump:

64 00 00 09 64 01 00 09 6c 00 00 09 ...

Each real instruction (e.g., LOAD_CONST 0 = 64 00 00) is followed by a 09 NOP filler byte. The "added something extra" from the description is literally these extra NOP bytes. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Code Object Inspection

Using xdis.load.load_module() to unmarshal the Python 2.7 code object without executing it, the key metadata is extracted:

co_consts (constants pool):

python
(-1, None, 2, 'Bye!', 1, 25, 27, 'Bye-bye!', 26, 0,
 's', 'p', 'b', 3, 'c', 4, 't', 5, 'f', 6, '{',
 'Wow, but not', '}', 24, 'k', 'Bruh', 7, '0', 8, 9,
 'h', 10, 'e', 11, 'n', 12, '_', 13, 'z', 14, 'l', 15,
 16, 'j', 17, 18, '1', 19, 20, 21, '7', 22, '4', 23,
 'Great!', 'Ahhh')

co_names (name references):

python
('sys', 'len', 'argv', 'exit', 'a', 'all', 'any')

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The constants pool already leaks all individual flag characters along with their positional indices — a strong indicator of character-by-character validation.

Solution

Custom Disassembler

A custom Python 2.7 bytecode disassembler was written that:

  1. Walks opcodes using the HAVE_ARGUMENT = 90 threshold (opcodes >= 90 take a 2-byte argument)
  2. Skips the 0x09 NOP filler byte after each instruction
  3. Produces a clean instruction listing

Reconstructed Logic

The disassembled program performs the following checks: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  1. Argument count: len(sys.argv) == 2 — requires exactly one command-line argument
  2. Length check with decoys:
    • len(sys.argv[1]) == 25 → prints "Bye-bye!" (decoy)
    • len(sys.argv[1]) == 27 → prints "Bye-bye!" (decoy)
    • len(sys.argv[1]) == 26 → proceeds to validation (correct path)
  3. Stores input: a = sys.argv[1]
  4. Prefix validation via all(): Checks a[0]='s', a[1]='p', a[2]='b', a[3]='c', a[4]='t', a[5]='f', a[6]='{'
  5. Suffix validation via any(): Checks a[25]='}', a[24]='k'
  6. Character-by-character comparison for positions 7-24 using COMPARE_OP ==: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
PositionCharacterPositionCharacter
0s13z
1p14l
2b150
3c16j
4t17n
5f181
6{19j
7020_
8c217
9h224
10e23s
11n24k
12_25}
  1. If all match → prints "Great!", otherwise "Ahhh"

Flag Assembly

Concatenating all 26 characters in order: spbctf{REDACTED} ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

This is leetspeak for "очень сложный таск" (Russian for "very hard task"). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering charbychar_flag_reconstruction, code_object_inspection, manual_bytecode_analysis, nop_stripping_disassembly was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-05 • CUSTOM • Tri-piki • Difficulty: Medium