---title: "seccon23 rev/Perfect Blu"publishDate: "18 Sep 2023"description: "Author: es3n1n / Editor: teidesu"tags: ["rev", "seccon23"]---
## Description
Perfect Blu (135 pt)
No, I'm real!
[perfect-blu.tar.gz](https://github.com/cr3mov/writeups/blob/master/seccon23-rev-perfect-blu/challenge/perfect-blu.tar.gz) 367b6ed67dda0afbbc975ee70ee946b4c7bf9268
#### Overview
Once I opened the downloaded `.tar.gz` file, I noticed there was a `.iso` file inside. When I mounted it, I saw that it had a file structure that looked like fs structure of a DVD. I then dragged this `.iso` file into VLC and got very surprised
![1](./img/1.png)
As you have probably already figured out, this task is a DVD image with an interactive menu asking you to enter the flag.Pressing the `CHECK` button shows either `SUCCESS` or `WRONG...` message, depending on the flag entered
#### Analysing
_I'm gonna be completely honest, I've never had any experience with all this DVD stuff before, so I had to spend a couple of hours googling how to analyse this stuff first._
Apparently, there are multiple types of DVD menus:- Java Menus- IGS Menus
I first checked for any `.jar` files (or anything at all Java-related), but found nothing, which meant that I was dealing with IGS menu.
The first step in IGS menu analysis is to download [BDEdit](https://bdedit.pel.hu/) and open up our mounted `BDMV\index.bdmv`
![2](./img/2.png)
Looks scary. I know.The first thing I did was to check the stream's first clip information, looking for any bytecode or anything else of interest. I did this by clicking on the `CLIPINFO` menu.
![3](./img/3.png)
Here, at the top left corner, there's a combo box with a clip selector.There are 96 clips and all of them have some buttons. 
Within that menu, I reviewed each stream and identified the `und` stream which appeared to store the bytecode. When I double-clicked on it, the menu with buttons opened up and I saw a lot of buttons and the disassembly of the code that they were doing:
![4](./img/4.png)
By messing around and guessing I found that the first default valid button (`1FDE` on the screenshot above) contains some random stuff I wasn't interested in, which meant I should check others instead.
![13](./img/13.png)
In this bytecode there's a `Call Object **` instruction (opcode `21820000`). This instruction simply starts playing another menu provided as its first operand. Knowing this, I started analysing what the other buttons were doing.
By observing all the other buttons I saw three types of buttons.
---
**JumpTo48** - Most buttons will take you from the current menu to menu 48.
![6](./img/6.png)
---
**JumpTo1** - This button takes you from the current menu to menu 1.
![7](./img/7.png)
---
**JumpTo96** - This button takes you from the current menu to menu 96.
![8](./img/8.png)
Jumping from clip 0 to clip 96, huh? I checked the clip 96 by playing the `BDMV\STREAM\00096.m2ts` file in VLC and got this:
![9](./img/9.png)
---
At this point, I knew that there were only three destinations in the first menu* Clip 1* Clip 48* Clip 96 (`WRONG...`) 
When exploring other clips(1, 48) in VLC, all seemed to show the same controls prompting for the flag.
Let's define this behavior as a pattern that we can then match with other menus:* Jump to `CURRENT + 1` (1, in this case)* Jump to `96` (`WRONG...`)* Jump to `CURRENT + 48` (48, in this case)
I didn't want to end up on scene 96, so I checked the menus 1 and 48.
When I opened up the menu 1 in BDEdit I clearly saw the same pattern that I saw in menu 0.
Moving on to menu 48, and the pattern observed was mostly the same, but there were only 2 type of buttons:* Jump to `CURRENT + 1` (49)* Jump to `96` (`WRONG`)
Basically, the 2 types of buttons got merged and it was jumping to only 2 destinations.
Seems odd, huh? Instead of 3 directions of the codeflow we got only 2, and one of it was just showing the `WRONG` message.I investigated it a bit further, and it seemed like we would always end up on the menu 96 if we are on the menu that's index is >= 48.
There's one exception though, which I just guessed. Remember how I opened stream 96 in vlc? Well, I did the same thing for clip 95 and got this:
![10](./img/10.png)
From now on solving this challenge seemed trivial, I just needed to parse all the clips and find what buttons lead to clip 95.
#### Solving
While the idea was easy enough, I struggled for half an hour trying to parse the clips.
I tried a bunch of libraries to parse the clips and extract the bytecode from them.However, none of them worked, so I decided I should just do it myself.
![11](./img/11.png)
I grabbed the `Call Object` instruction opcode (`21820000`)and searched for it in HxD across the entire ISO.
I ended up in the same m2ts files where I got a lot of occurrences. I assumed that this bytecode is indeed stored in the same file as the stream itself, so I should parse it directly from those files
![12](./img/12.png)
When assembled, this instruction looks like this:```js>───────┐ ┌───────┐ ┌───────> │2182 0000 0000 0030 0000 0000 │ !......0....│         │         │2182──────│─────────│───────────────────────── Opcode          30────────│───────────────────────── Operand 1                    00──────────────────────── Operand 2```
Let's write all of these as the constants for the solver
```pyOPCODE_SIZE: int = 4OPERAND_SIZE: int = 4INSN_SIZE = OPCODE_SIZE + (OPERAND_SIZE * 2)
CALL_OBJECT = b'\x21\x82\x00\x00'  # Call Object {DST}```
After that, I iterated over the first 47 streams and extracted their buttons.```py# Returns { button_id: jmp_to }def parse_buttons(mnu_data: bytes) -> dict[int, int]:    result = dict()    i = 0    start_off = 0
    while True:        # Searching for `Call Object` opcode        s = mnu_data.find(CALL_OBJECT, start_off)        if s == -1:            break
        # Move next iter        start_off = s + INSN_SIZE
        # Read the current chunk and extract op1 from it        chunk = mnu_data[s:s + INSN_SIZE]        op1 = int.from_bytes(chunk[4:8], 'big')
        # Save the dst        result[i] = op1        i += 1
    return result
# menu index -> buttons from `parse_buttons`menus: dict[int, dict[int, int]] = dict()
for menu in p2.iterdir():    menu_id = int(menu.name.split('.')[0])    if menu_id > 47:        break
    with open(menu, 'rb') as f:        content = f.read()
    menus[menu_id] = parse_buttons(content)```
At this point, I already had all the playlists and parsed buttons from these playlists. To make the other logic a bit easier to implement, I collected all the successors and predecessors for menus into separate dicts.```py# menu index -> possible exitsmenus_possibilities: dict[int, list[int]] = dict()# menu index -> { jmp_dst: [buttons] }menus_referrers: dict[int, dict[int, list[int]]] = dict()
for key in sorted(menus.keys()):    value = menus[key]    menus_possibilities[key] = list()    menus_referrers[key] = dict()
    for k, possible_value in value.items():        if possible_value not in menus_referrers[key]:            menus_referrers[key][possible_value] = list()
        menus_referrers[key][possible_value].append(k)
        if possible_value in menus_possibilities[key]:            continue        menus_possibilities[key].append(possible_value)```
Now, the solution to this challenge is basically a path from menu 0 to menu 95:```py# menu -> buttonpath: dict[int, int] = dict()
for k, v in menus_possibilities.items():    tgt = None
    # Selecting the first menu that id is <=47 (or 95)    for possible_move in v:        if possible_move > 47 and possible_move != 95:            continue
        tgt = possible_move        break
    if not tgt:        print('[!] Unknown tgt?!')        break
    path[k] = menus_referrers[k][tgt][0]    print('[+] Menu:', k, 'Button:', path[k], 'Next:', tgt)```
Looking at the output below, I tried to guess what alphabet I needed to use to convert these numbers to characters.```js[+] Menu: 0 Button: 21 Next: 1[+] Menu: 1 Button: 12 Next: 2[+] Menu: 2 Button: 32 Next: 3[+] Menu: 3 Button: 32 Next: 4[+] Menu: 4 Button: 18 Next: 5[+] Menu: 5 Button: 35 Next: 6[+] Menu: 6 Button: 29 Next: 7...```
The first button that I should click on is 21. Knowing that the flag starts with `SECCON{`, I know that the first char is `S` with ID 21.I looked at the button layout to decode the alphabet:```js1 2 3 4 5 6 7 8 9 0Q W E R T Y U I O PA S D F G H J K L {Z X C V B N M _ - }```
And oh well, when I concatenated it to a single string `1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}` and searched for `S` there, I got:```py>>> '1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}'.find('S')21>>>```
All I had left to do is to just grab all the button IDs and convert them to characters using this alphabet:```pyALPHABET = '1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}'FLAG: str = ''
for k, v in path.items():    if v >= len(ALPHABET):        break
    FLAG += ALPHABET[v]
print('[+] Flag:', FLAG)```
Which _finally_ produced the flag.
#### Flag`SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV}`
#### Full solver source
```pyfrom pathlib import Path
p2 = Path(__file__).parent / 'menus'# p2 = Path('F:\\BDMV\\STREAM')
"""95 - win96 - lose"""
# DST - first operand# SRC - second operand
# in bytesOPCODE_SIZE: int = 4OPERAND_SIZE: int = 4INSN_SIZE = OPCODE_SIZE + (OPERAND_SIZE * 2)
BIT_CLEAR = b'\x50\x40\x00\x0D'  # Bit Clear GPR{DST}, {SRC}CALL_OBJECT = b'\x21\x82\x00\x00'  # Call Object {DST}
# Returns { button_id: jmp_to }def parse_buttons(mnu_data: bytes) -> dict[int, int]:    result = dict()    i = 0    start_off = 0
    while True:        s = mnu_data.find(CALL_OBJECT, start_off)        if s == -1:            break
        start_off = s + INSN_SIZE
        chunk = mnu_data[s:s + INSN_SIZE]
        # opcode = int.from_bytes(chunk[:4], 'big')        op1 = int.from_bytes(chunk[4:8], 'big')        # op2 = int.from_bytes(chunk[8:], 'big')
        # print('[+] i =', i, 'CALL_OBJECT', op1, op2)        result[i] = op1
        i += 1
    return result
# menu index -> buttons from `parse_buttons`menus: dict[int, dict[int, int]] = dict()
for menu in p2.iterdir():    menu_id = int(menu.name.split('.')[0])    if menu_id > 47:        break
    with open(menu, 'rb') as f:        content = f.read()
    menus[menu_id] = parse_buttons(content)
# menu index -> possible exitsmenus_possibilities: dict[int, list[int]] = dict()# menu index -> { jmp_dst: [buttons] }menus_referrers: dict[int, dict[int, list[int]]] = dict()
for key in sorted(menus.keys()):    value = menus[key]    menus_possibilities[key] = list()    menus_referrers[key] = dict()
    for k, possible_value in value.items():        if possible_value not in menus_referrers[key]:            menus_referrers[key][possible_value] = list()
        menus_referrers[key][possible_value].append(k)
        if possible_value in menus_possibilities[key]:            continue        menus_possibilities[key].append(possible_value)
# menu -> buttonpath: dict[int, int] = dict()
for k, v in menus_possibilities.items():    tgt = None
    # Selecting the first menu that id is <=47 (or 95)    for possible_move in v:        if (possible_move > 47 and possible_move != 95) or possible_move == 0:            continue
        if tgt:            print('[!] What should i choose master', tgt, possible_move)
        tgt = possible_move
    if not tgt:        print('[!] Unknown tgt?!')        break
    path[k] = menus_referrers[k][tgt][0]    print('[+] Menu:', k, 'Button:', path[k], 'Next:', tgt)
ALPHABET = '1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}'FLAG: str = ''
for k, v in path.items():    if v >= len(ALPHABET):        break
    FLAG += ALPHABET[v]
print('[+] Flag:', FLAG)
```