amocafe @ Dreamhack
CTF Challenge Writeup: Amocafe Menu Decoder
Source Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python3
from flask import Flask, request, render_template
app = Flask(__name__)
try:
FLAG = open("./flag.txt", "r").read() # flag is here!
except:
FLAG = "[**FLAG**]"
@app.route('/', methods=['GET', 'POST'])
def index():
menu_str = ''
org = FLAG[10:29]
org = int(org)
st = ['' for i in range(16)]
for i in range (0, 16):
res = (org >> (4 * i)) & 0xf
if 0 < res < 12:
if ~res & 0xf == 0x4:
st[16-i-1] = '_'
else:
st[16-i-1] = str(res)
else:
st[16-i-1] = format(res, 'x')
menu_str = menu_str.join(st)
# POST
if request.method == "POST":
input_str = request.form.get("menu_input", "")
if input_str == str(org):
return render_template('index.html', menu=menu_str, flag=FLAG)
return render_template('index.html', menu=menu_str, flag='try again...')
# GET
return render_template('index.html', menu=menu_str)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
Challenge Overview
This Flask web application presents a CTF challenge where you need to reverse-engineer an obfuscated number from a displayed “menu” string to capture the flag.
Challenge Analysis
The Code Flow
Flag Extraction: The app reads a flag from flag.txt and extracts characters 10-29 (20 characters total) Conversion: These 20 characters are converted to an integer org Obfuscation: The integer is split into 16 nibbles (4-bit chunks) and transformed into a display string Goal: Submit the correct original number org to get the flag
The Encoding Logic
The code processes each nibble (4-bit value, 0-15) of the number:
1
2
3
4
5
6
7
8
9
for i in range(0, 16):
res = (org >> (4 * i)) & 0xf # Extract nibble i
if 0 < res < 12:
if ~res & 0xf == 0x4:
st[16-i-1] = '_'
else:
st[16-i-1] = str(res)
else:
st[16-i-1] = format(res, 'x')
Key observations: i=0 extracts the least significant nibble (bits 0-3) and places it at st[15] (rightmost) i=15 extracts the most significant nibble (bits 60-63) and places it at st[0] (leftmost) The display string reads left-to-right as most-to-least significant
Encoding Rules | Nibble Value | Condition | Output | |————–|———–|——–| | 0 | res == 0 | format(0, 'x') → '0' | | 1-9 | 0 < res < 12 | str(res) → '1' to '9' | | 10 | 0 < res < 12 | str(10) → '10' (2 chars!) | | 11 | ~11 & 0xf == 0x4 ✓ | '_' | | 12-15 | res >= 12 | format(res, 'x') → 'c', 'd', 'e', 'f' |
Critical insight: The underscore _ represents nibble value 11 because: Binary 11 = 1011 Bitwise NOT of 1011 = 0100 0100 & 1111 = 0100 = 4 ✓
Solve Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/usr/bin/env python3
def reverse_menu_to_org(menu_str):
"""
Reverse the menu string back to the original number.
The encoding logic:
- res = 0: format(0, 'x') -> '0'
- res = 1-9: str(res) -> '1'-'9'
- res = 10: str(10) -> '10' (2 characters!)
- res = 11: if ~11 & 0xf == 0x4 -> '_'
- res = 12-15: format(res, 'x') -> 'c', 'd', 'e', 'f'
"""
# Parse the menu string into nibbles
nibbles = []
i = 0
while i < len(menu_str):
char = menu_str[i]
# Check if this might be '10' (two characters)
if char == '1' and i + 1 < len(menu_str) and menu_str[i + 1] == '0':
nibbles.append(10)
i += 2
elif char == '_':
nibbles.append(11)
i += 1
elif char in '0123456789':
nibbles.append(int(char))
i += 1
elif char in 'abcdef':
nibbles.append(int(char, 16))
i += 1
else:
raise ValueError(f"Unexpected character: {char}")
print(f"Parsed nibbles: {nibbles}")
print(f"Number of nibbles: {len(nibbles)}")
# The code does st[16-i-1] for i in range(0, 16)
# When i=0: st[15] gets (org >> 0) & 0xf (least significant)
# When i=15: st[0] gets (org >> 60) & 0xf (most significant)
# So st[0] is MSB, st[15] is LSB
# Reconstruct org from nibbles
org = 0
for idx, nibble in enumerate(nibbles):
# idx=0 is leftmost (most significant)
# We need to place it at the correct bit position
shift = 4 * (len(nibbles) - 1 - idx)
org |= (nibble << shift)
return org
def verify_solution(org, expected_menu):
"""
Verify our solution by running the encoding logic forward.
"""
st = ['' for i in range(16)]
for i in range(0, 16):
res = (org >> (4 * i)) & 0xf
if 0 < res < 12:
if ~res & 0xf == 0x4:
st[16-i-1] = '_'
else:
st[16-i-1] = str(res)
else:
st[16-i-1] = format(res, 'x')
menu_str = ''.join(st)
print(f"Verification - Generated menu: {menu_str}")
print(f"Verification - Expected menu: {expected_menu}")
print(f"Match: {menu_str == expected_menu}")
return menu_str == expected_menu
# The menu from the challenge
menu = "1_c_3_c_0__ff_3e"
print("="*50)
print("CTF Challenge Solver")
print("="*50)
print(f"Input menu: {menu}")
print()
# Solve it
org = reverse_menu_to_org(menu)
print(f"\nOriginal number (org): {org}")
print()
# Verify
print("Verifying solution...")
if verify_solution(org, menu):
print("\n✓ Solution verified!")
print(f"\nSubmit this value: {org}")
else:
print("\n✗ Solution doesn't match - need to debug")
print("\nLet me try alternative interpretation...")
# Maybe the issue is with how we count positions
# Let's try treating it as exactly 16 nibbles
if len(menu) == 16:
print("\nTrying direct 16-character interpretation:")
nibbles = []
for char in menu:
if char == '_':
nibbles.append(11)
elif char in '0123456789':
nibbles.append(int(char))
elif char in 'abcdef':
nibbles.append(int(char, 16))
org2 = 0
for idx, nibble in enumerate(nibbles):
shift = 4 * (15 - idx)
org2 |= (nibble << shift)
print(f"Alternative org: {org2}")
verify_solution(org2, menu)
