# Stack The Flags 2022 Writeups ## Misc / HeatKeeb ### Challenge **Description** ``` Jaga had gained interest in custom keyboards and has created a platform to create your own keebs! We know we created his custom keeb on the 22nd of September 2022, at 12:00:00 SGT. Oddly specific but we know it's true. ``` We are provided a server to connect to, as well as an archive ``misc_heatkeeb.zip`` that contains a folder ``app`` storing the servers' files. In particular, we are provided with ``app.py`` and ``keebcreator.py``. **app.py** ```python import uvicorn from fastapi import FastAPI, Request, Form, BackgroundTasks from fastapi.responses import FileResponse, RedirectResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.middleware import Middleware from starlette.middleware.sessions import SessionMiddleware from keebcreator import draw_keeb, draw_heatmap import os from pathlib import Path from PIL import Image import shelve import datetime, pytz import random middleware = [ Middleware(SessionMiddleware, secret_key="h34tk33bl0l") ] app = FastAPI(middleware=middleware) app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") FLAG = "STF22{REDACTED}" KEY = "REDACTED" ADMIN_TOKEN = "REDACTED" def hex_to_rgb(hex): h = hex.lstrip('#') return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) admin_token = "" adminName = 'j4g4h1ms3lf' adminFrame = hex_to_rgb('#000000') adminKeys = hex_to_rgb('#dcdedb') adminText = hex_to_rgb('#000000') adminKeys = hex_to_rgb('#98b5bb') with shelve.open('keebdb') as db: db[ADMIN_TOKEN] = { 'name': adminName, 'frameColor': adminFrame, 'keyColor': adminKeys, 'textColor': adminText, 'specialColor': adminKeys, 'text': KEY } img = draw_keeb(adminName, adminFrame, adminKeys, adminText, adminKeys) img.save(f'keebs/keeb-{ADMIN_TOKEN}.png') img = draw_heatmap(adminName, adminFrame, adminKeys, adminText, adminKeys, KEY) imgSmall = img.resize((int(img.width / 100), int(img.height / 100)), Image.BILINEAR) result = imgSmall.resize(img.size, Image.NEAREST) result.save(f'keebs/heatmap-{ADMIN_TOKEN}.png') @app.get("/") def root(request: Request): return templates.TemplateResponse("index.html", {"request": request}) @app.get("/menu") def root(request: Request): if 'token' in request.session: return templates.TemplateResponse("menu.html", {"request": request}) else: return RedirectResponse('/') @app.post("/menu") def keeb(request: Request, token: str = Form(...)): with shelve.open('keebdb') as db: if token in db: # set token cookie request.session['token'] = token return templates.TemplateResponse("menu.html", {"request": request, "token": token}) else: return templates.TemplateResponse("index.html", {"request": request}) @app.get("/keeb") def keeb(request: Request): # use pathlib to open keeb if 'token' in request.session: token = request.session['token'] with shelve.open('keebdb') as db: if token in db: return FileResponse(f"keebs/keeb-{token}.png") else: return templates.TemplateResponse("index.html", {"request": request}) @app.get("/build") def build(request: Request): return templates.TemplateResponse("build.html", {"request": request}) @app.post("/build") def build(request: Request, name: str = Form(...), frameColor: str = Form(...), keyColor: str = Form(...), textColor: str = Form(...), specialColor: str = Form(...)): t = datetime.datetime.now(pytz.timezone('Asia/Singapore')) seed = int(t.timestamp()) random.seed(seed) token = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16)) with shelve.open('keebdb') as db: db[token] = { 'name': name, 'frameColor': hex_to_rgb(frameColor), 'keyColor': hex_to_rgb(keyColor), 'textColor': hex_to_rgb(textColor), 'specialColor': hex_to_rgb(specialColor), 'text': 'default' } img = draw_keeb(name, hex_to_rgb(frameColor), hex_to_rgb(keyColor), hex_to_rgb(textColor), hex_to_rgb(specialColor)) img.save(f'keebs/keeb-{token}.png') request.session['token'] = token return templates.TemplateResponse("build.html", {"request": request, "resp": "Success!", "token": token}) @app.get('/rebuild') def rebuild(request: Request): if 'token' in request.session: return templates.TemplateResponse("rebuild.html", {"request": request}) @app.post('/rebuild') def rebuild(request: Request, frameColor: str = Form(...), keyColor: str = Form(...), textColor: str = Form(...), specialColor: str = Form(...)): if 'token' in request.session: token = request.session['token'] with shelve.open('keebdb') as db: if token in db: img = draw_keeb(db[token]['name'], hex_to_rgb(frameColor), hex_to_rgb(keyColor), hex_to_rgb(textColor), hex_to_rgb(specialColor)) img.save(f'keebs/keeb-{token}.png') db[token] = { 'name': db[token]['name'], 'frameColor': hex_to_rgb(frameColor), 'keyColor': hex_to_rgb(keyColor), 'textColor': hex_to_rgb(textColor), 'specialColor': hex_to_rgb(specialColor), 'text': db[token]['text'] } return templates.TemplateResponse("rebuild.html", {"request": request, "resp": "Success!"}) else: return templates.TemplateResponse("index.html", {"request": request}) @app.get('/heatmap/generate') def generate_heatmap(request: Request): if 'token' in request.session: return templates.TemplateResponse("heatmap-gen.html", {"request": request}) @app.post('/heatmap/generate') def generate_heatmap(request: Request, text: str = Form(...)): if 'token' in request.session: token = request.session['token'] with shelve.open('keebdb') as db: if token in db: name = db[token]['name'] frameColor = db[token]['frameColor'] keyColor = db[token]['keyColor'] textColor = db[token]['textColor'] specialColor = db[token]['specialColor'] img = draw_heatmap(name, frameColor, keyColor, textColor, specialColor, text.upper()) imgSmall = img.resize((int(img.width / 100), int(img.height / 100)), Image.BILINEAR) result = imgSmall.resize(img.size, Image.NEAREST) result.save(f'keebs/heatmap-{token}.png') db[token] = { 'name': name, 'frameColor': frameColor, 'keyColor': keyColor, 'textColor': textColor, 'specialColor': specialColor, 'text': text.upper() } return FileResponse(f"keebs/heatmap-{token}.png") @app.get('/heatmap/view') def heatmap(request: Request, background_tasks: BackgroundTasks): if 'token' in request.session: token = request.session['token'] with shelve.open('keebdb') as db: if token in db: try: img = Image.open(f'keebs/heatmap-{token}.png') img.close() except FileNotFoundError: return RedirectResponse('/heatmap/generate') return FileResponse(f"keebs/heatmap-{token}.png") else: return templates.TemplateResponse("index.html", {"request": request}) @app.get('/text') def flag(request: Request): if 'token' in request.session: return templates.TemplateResponse("flag.html", {"request": request}) @app.post('/text') def flag(request: Request, text: str = Form(...)): if 'token' in request.session: with shelve.open('keebdb') as db: token = request.session['token'] if token in db: if token == ADMIN_TOKEN and text.upper() == KEY: return templates.TemplateResponse("flag.html", {"request": request, "word": text, "flag": FLAG}) elif text.upper() == db[token]['text']: return templates.TemplateResponse("flag.html", {"request": request, "word": text}) return templates.TemplateResponse("flag.html", {"request": request}) if __name__ == "__main__": uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) ``` **keebcreator.py** ```python import numpy as np from PIL import ImageFont, ImageDraw, Image def key(text, w=1): return {'t': text, 'w': w} ''' keeb = [ [key('Esc'), key('1'), key('2'), key('3'), key('4'), key('5'), key('6'), key('7'), key('8'), key('9'), key('0'), key('-'), key('='), key('Backspace', 2), key('Home')], [key('Tab', 1.5), key('Q'), key('W'), key('E'), key('R'), key('T'), key('Y'), key('U'), key('I'), key('O'), key('P'), key('['), key(']'), key('\\', 1.5), key('PgUp')], [key('Caps Lock', 1.75), key('A'), key('S'), key('D'), key('F'), key('G'), key('H'), key('J'), key('K'), key('L'), key(';'), key("'"), key('Enter', 2.25), key('PgDn')], [key('Shift', 2.25), key('Z'), key('X'), key('C'), key('V'), key('B'), key('N'), key('M'), key(','), key('.'), key('/'), key('Shift', 1.75), key('↑'), key('End')], [key('Ctrl', 1.25), key('Win', 1.25), key('Alt', 1.25), key('Space', 6.25), key('Alt'), key('Fn'), key('Ctrl'), key('←'), key('↓'), key('→')] ] ''' keeb = [ [key(''), key('1'), key('2'), key('3'), key('4'), key('5'), key('6'), key('7'), key('8'), key('9'), key('0'), key('-'), key('='), key('', 2), key('')], [key('', 1.5), key('Q'), key('W'), key('E'), key('R'), key('T'), key('Y'), key('U'), key('I'), key('O'), key('P'), key('['), key(']'), key('\\', 1.5), key('')], [key('', 1.75), key('A'), key('S'), key('D'), key('F'), key('G'), key('H'), key('J'), key('K'), key('L'), key(';'), key("'"), key('', 2.25), key('')], [key('', 2.25), key('Z'), key('X'), key('C'), key('V'), key('B'), key('N'), key('M'), key(','), key('.'), key('/'), key('', 1.75), key(''), key('')], [key('', 1.25), key('', 1.25), key('', 1.25), key('', 6.25), key(''), key(''), key(''), key(''), key(''), key('')] ] def draw_keeb(name, frameColor, keyColor, textColor, specialColor, keeb=keeb, height=580, width=1720, unit=100): def draw_key(image, key, x, y, w, h, color): x = int(x) y = int(y) text = key['t'] color = specialColor if text == '' else color image[y:int(y+h), x:int(x+w)] = color img_pil = Image.fromarray(image) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype('Poppins-Regular.ttf', 20) textsize = font.getsize(text) textX = (w - textsize[0]) / 2 textY = (h + textsize[1]) / 2 draw.text((x+textX, y+textY), text, textColor, font=font, align='center') return np.array(img_pil) image = np.zeros((height, width, 3), np.uint8) image[:] = frameColor img_pil = Image.fromarray(image) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype('Poppins-Regular.ttf', 20) textsize = font.getsize(name) textX = (width - textsize[0]) / 2 textY = height - 30 draw.text((textX, textY), name, (255,255,255,255), font=font, align='center') image = np.array(img_pil) x = 30 y = 30 for row in keeb: gap = (width - x * 2 - unit * sum([key['w'] for key in row])) / (len(row) - 1) for key in row: w = unit * key['w'] h = unit image = draw_key(image, key, x, y, w, h, keyColor) x += w + gap # draw gap x = 30 y += unit + 5 image = Image.fromarray(image) return image def draw_heatmap(name, frameColor, keyColor, textColor, specialColor, htext, keeb=keeb, height=580, width=1720, unit=100): def draw_key(image, key, x, y, w, h, color): x = int(x) y = int(y) text = key['t'] if text == '': color = specialColor elif text in htext: color = (255, 84, 84) image[y:int(y+h), x:int(x+w)] = color img_pil = Image.fromarray(image) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype('Poppins-Regular.ttf', 20) textsize = font.getsize(text) textX = (w - textsize[0]) / 2 textY = (h + textsize[1]) / 2 draw.text((x+textX, y+textY), text, textColor, font=font, align='center') return np.array(img_pil) image = np.zeros((height, width, 3), np.uint8) image[:] = frameColor img_pil = Image.fromarray(image) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype('Poppins-Regular.ttf', 20) textsize = font.getsize(name) textX = (width - textsize[0]) / 2 textY = height - 30 draw.text((textX, textY), name, (255,255,255,255), font=font, align='center') image = np.array(img_pil) x = 30 y = 30 for row in keeb: gap = (width - x * 2 - unit * sum([key['w'] for key in row])) / (len(row) - 1) for key in row: w = unit * key['w'] h = unit image = draw_key(image, key, x, y, w, h, keyColor) x += w + gap # draw gap x = 30 y += unit + 5 image = Image.fromarray(image) return image if __name__ == '__main__': img = draw_keeb('John Doe', (0, 0, 0), (214, 218, 217), (0, 0, 0), (160, 189, 195)) img.save('keeb.png') heatmap = draw_heatmap('John Doe', (0, 0, 0), (214, 218, 217), (0, 0, 0), (160, 189, 195), "H34TKEYB0ARD") heatmap.save('heatmap.png') ``` ### Solution In order to obtain the flag, we need to call the ``/text`` endpoint as the admin (using the ``ADMIN_TOKEN``) with the text as the ``KEY``. Both the ``ADMIN_TOKEN`` and the ``KEY`` are static, but redacted in the source code. ### Leaking ADMIN_TOKEN When we first connect to the server, a random token will be generated for us via the ``/build`` endpoint. Based on the code, it can be seen that the token is generated based on the current time. Fortunately, from the challenge description, we are informed that Jaga (presumably the admin) "created his custom keeb on the 22nd of September 2022, at 12:00:00 SGT". Using this timestamp, we can generate Jaga's token from the following script: **Script** ```python import random #From https://www.epochconverter.com/ #Epoch timestamp: 1663810877 #Date and time (GMT): Thursday, September 22, 2022 1:41:17 AM #Date and time (your time zone): Thursday, September 22, 2022 9:41:17 AM GMT+08:00 seed = 1663810877 random.seed(seed) admin_token = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16)) print(admin_token) ``` **Output** ``` rMwwbpMkzAwyRoWs ``` Logging in with this token, we manage to successfully log in as the admin. ### Leaking KEY When the server is initialized, the ``KEY`` variable is used to generate a heatmap image using the ``draw_heatmap()`` function. By calling the ``/heatmap/view`` endpoint, we are able to get Jaga's last generated heatmap (which was generated using ``KEY``) when logged in as Jaga, which is the following image: ![image](https://user-images.githubusercontent.com/19924575/206631649-a6db0452-ed50-4d64-96c3-4caa612de8ec.png) By analyzing the ``draw_heatmap()`` function code, it can be observed that the squares in the heatmap image correspond to the characters in a keyboard. When a square is colored red ``(255, 84, 84)``, it means that the keyboard character at that position is present in the text provided to the function (in this case, the text provided is ``KEY``). Note that due to the "bluring" of colors with adjacent squares, the square's color may not be exactly ``(255, 84, 84)``. Nonetheless, this means that we can leak the characters in ``KEY`` based on the heatmap image above. By manually mapping the squares to the keyboard characters, one can obtain the following annotated heatmap image: ![image](https://user-images.githubusercontent.com/19924575/206633164-101d9f94-a7c6-4309-82ad-f45fabd7f9f1.png) The characters found can be confirmed by generating another heatmap image via the ``/heatmap/generate`` endpoint with the text ``ASERTGHNIL``, and comparing the newly generated image with Jaga's last generated heatmap. Although we managed to figure out the characters present in ``KEY``, we do not know the exact order that they are in. Fortunately, we can use an online [Anagram Solver](https://www.thewordfinder.com/anagram-solver/) to find all possible valid English anagrams for ``ASERTGHNIL``. Based on the Anagram Solver, there are only 2 possible anagrams: ``EARTHLINGS`` and ``SLATHERING``. By calling the ``/text`` endpoint with the text ``EARTHLINGS`` while being logged in as Jaga, we successfully manage to obtain the flag ``STF22{h34t_k3yb04rD}``. ## Misc / 2 Pain ### Challenge We are given an APK file called ``2pain4you.apk``. ### Initial Analysis 1. [VirusTotal](https://www.virustotal.com/gui/file/fa3b425bb3e592f99588e74f4c7cb4e0b9151b774e7198d1596c326788457ebc) was used to analyze the APK; 2. An [Online APK Decompiler](http://www.javadecompilers.com/apk) was used to decompile the APK From the VirusTotal analysis, we are able to find out a few things: - Contrary to the name of the challenge, the APK appears to be safe to run; - ``com.example.deva.MainActivity`` is the main activity; - The APK appears to be bundled with some Linux executables ### Solution For starters, an attempt was made to run the APK. Unfortunately, the APK only seems to produce a few meme images/gifs: ![image](https://user-images.githubusercontent.com/19924575/205867310-31f5e5de-fdf3-466b-a497-c7245c2bdb89.png) ![image](https://user-images.githubusercontent.com/19924575/205867362-f0cd7024-5f3c-4e28-9754-f3c27439ddae.png) ![image](https://user-images.githubusercontent.com/19924575/205867451-0fd01d3c-1be6-4b49-a0b2-ff5cc3158c40.png) Since there was nothing of significance found, we took a look at the decompiled ``com.example.deva.MainActivity`` class: ```java package com.example.deva; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.customview.widget.Openable; import androidx.navigation.ActivityKt; import androidx.navigation.NavController; import androidx.navigation.NavGraph; import androidx.navigation.p004ui.AppBarConfiguration; import androidx.navigation.p004ui.NavControllerKt; import com.example.deva.databinding.ActivityMainBinding; import com.scottyab.rootbeer.RootBeer; import kotlin.Metadata; import kotlin.jvm.functions.Function2; import kotlin.jvm.internal.Intrinsics; @Metadata(mo13378d1 = {"\u0000<\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0007\u001a\u00020\bH\u0002J\u0012\u0010\t\u001a\u00020\n2\b\u0010\u000b\u001a\u0004\u0018\u00010\fH\u0014J\u0010\u0010\r\u001a\u00020\b2\u0006\u0010\u000e\u001a\u00020\u000fH\u0016J\u0010\u0010\u0010\u001a\u00020\b2\u0006\u0010\u0011\u001a\u00020\u0012H\u0016J\b\u0010\u0013\u001a\u00020\bH\u0016R\u000e\u0010\u0003\u001a\u00020\u0004X‚.¢\u0006\u0002\n\u0000R\u000e\u0010\u0005\u001a\u00020\u0006X‚.¢\u0006\u0002\n\u0000¨\u0006\u0014"}, mo13379d2 = {"Lcom/example/deva/MainActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "appBarConfiguration", "Landroidx/navigation/ui/AppBarConfiguration;", "binding", "Lcom/example/deva/databinding/ActivityMainBinding;", "isRootDevice", "", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "onCreateOptionsMenu", "menu", "Landroid/view/Menu;", "onOptionsItemSelected", "item", "Landroid/view/MenuItem;", "onSupportNavigateUp", "app_release"}, mo13380k = 1, mo13381mv = {1, 5, 1}, mo13383xi = 48) /* compiled from: MainActivity.kt */ public final class MainActivity extends AppCompatActivity { private AppBarConfiguration appBarConfiguration; private ActivityMainBinding binding; /* access modifiers changed from: protected */ public void onCreate(Bundle bundle) { super.onCreate(bundle); ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater()); Intrinsics.checkNotNullExpressionValue(inflate, "inflate(layoutInflater)"); this.binding = inflate; if (inflate != null) { setContentView((View) inflate.getRoot()); ActivityMainBinding activityMainBinding = this.binding; if (activityMainBinding != null) { setSupportActionBar(activityMainBinding.toolbar); if (isRootDevice()) { finishAffinity(); } NavController findNavController = ActivityKt.findNavController(this, C0631R.C0634id.nav_host_fragment_content_main); NavGraph graph = findNavController.getGraph(); Intrinsics.checkNotNullExpressionValue(graph, "navController.graph"); Openable openable = null; AppBarConfiguration build = new AppBarConfiguration.Builder(graph).setOpenableLayout((Openable) null).setFallbackOnNavigateUpListener(new C0630xbc9c60e7(MainActivity$onCreate$$inlined$AppBarConfiguration$default$1.INSTANCE)).build(); Intrinsics.checkExpressionValueIsNotNull(build, "AppBarConfiguration.Buil…eUpListener)\n .build()"); this.appBarConfiguration = build; AppCompatActivity appCompatActivity = this; if (build != null) { androidx.navigation.p004ui.ActivityKt.setupActionBarWithNavController(appCompatActivity, findNavController, build); ActivityMainBinding activityMainBinding2 = this.binding; if (activityMainBinding2 != null) { activityMainBinding2.fab.setOnClickListener(new MainActivity$$ExternalSyntheticLambda2(this)); } else { Intrinsics.throwUninitializedPropertyAccessException("binding"); throw null; } } else { Intrinsics.throwUninitializedPropertyAccessException("appBarConfiguration"); throw null; } } else { Intrinsics.throwUninitializedPropertyAccessException("binding"); throw null; } } else { Intrinsics.throwUninitializedPropertyAccessException("binding"); throw null; } } /* access modifiers changed from: private */ /* renamed from: onCreate$lambda-2 reason: not valid java name */ public static final void m69onCreate$lambda2(MainActivity mainActivity, View view) { Intrinsics.checkNotNullParameter(mainActivity, "this$0"); Context context = mainActivity; AlertDialog.Builder builder = new AlertDialog.Builder(context); View inflate = LayoutInflater.from(context).inflate(C0631R.layout.popup, (ViewGroup) null); builder.setNegativeButton(17039369, (DialogInterface.OnClickListener) new MainActivity$$ExternalSyntheticLambda0(new MainActivity$onCreate$1$negativeButtonClick$1(mainActivity))); builder.setView(inflate); builder.show(); } /* access modifiers changed from: private */ /* renamed from: onCreate$lambda-2$lambda-1$lambda-0 reason: not valid java name */ public static final void m70onCreate$lambda2$lambda1$lambda0(Function2 function2, DialogInterface dialogInterface, int i) { Intrinsics.checkNotNullParameter(function2, "$tmp0"); function2.invoke(dialogInterface, Integer.valueOf(i)); } public boolean onCreateOptionsMenu(Menu menu) { Intrinsics.checkNotNullParameter(menu, "menu"); getMenuInflater().inflate(C0631R.C0635menu.menu_main, menu); return true; } public boolean onOptionsItemSelected(MenuItem menuItem) { Intrinsics.checkNotNullParameter(menuItem, "item"); if (menuItem.getItemId() == C0631R.C0634id.action_settings) { Context context = this; AlertDialog.Builder builder = new AlertDialog.Builder(context); View inflate = LayoutInflater.from(context).inflate(C0631R.layout.popup2, (ViewGroup) null); builder.setNegativeButton(17039369, (DialogInterface.OnClickListener) new MainActivity$$ExternalSyntheticLambda1(new MainActivity$onOptionsItemSelected$negativeButtonClick$1(this))); builder.setView(inflate); builder.show(); } else { Toast.makeText(getApplicationContext(), "Click Previous man", 1).show(); } return true; } /* access modifiers changed from: private */ /* renamed from: onOptionsItemSelected$lambda-4$lambda-3 reason: not valid java name */ public static final void m71onOptionsItemSelected$lambda4$lambda3(Function2 function2, DialogInterface dialogInterface, int i) { Intrinsics.checkNotNullParameter(function2, "$tmp0"); function2.invoke(dialogInterface, Integer.valueOf(i)); } public boolean onSupportNavigateUp() { NavController findNavController = ActivityKt.findNavController(this, C0631R.C0634id.nav_host_fragment_content_main); AppBarConfiguration appBarConfiguration2 = this.appBarConfiguration; if (appBarConfiguration2 != null) { return NavControllerKt.navigateUp(findNavController, appBarConfiguration2) || super.onSupportNavigateUp(); } Intrinsics.throwUninitializedPropertyAccessException("appBarConfiguration"); throw null; } private final boolean isRootDevice() { try { RootBeer rootBeer = new RootBeer(this); if (!rootBeer.detectRootCloakingApps() && !rootBeer.checkForSuBinary() && !rootBeer.checkForDangerousProps() && !rootBeer.checkForRWPaths() && !rootBeer.checkSuExists() && !rootBeer.checkForRootNative()) { return false; } Log.e("ERR", "ROOT DETECTED"); return true; } catch (Exception e) { Log.e("ERR", "Root check passed"); e.printStackTrace(); return false; } } } ``` Unfortunately, there also appears to be nothing of significance found. Taking a look at the other decompiled classes in the ``com.example.deva`` package, we find the ``com.example.deva.pain`` class: ```java package com.example.deva; public class pain { byte[] painz = {-55, -98, 98, 14, -121, -110, 84, -3, 74, 10, 106, -27, -13, -112, -42, 111, -1, -89, -64, 46, -15, -108, -26, 59, -111, 113, 2, -69, -83, 45, -31, -103, 46, -84, -113, 116, -110, -36, 22, -23, 86, 38, -17, 0, 100, -65, 94, 48, 76, 17, 35, -117, -51, -81, -95, 49, 62, -28, 96, 86, 65, 76, 57, 40}; String something_else = "stinger is all lower-case and absolutely no 1337 speak here"; String something_wrong_with_stinger = "Seems like someone ate the first char of stinger, oh no!"; String stinger = "Ghpcy1pc3NhLXdlaXJkLWtleQ=="; } ``` This class defines a few suspicious values, notably the base64-encoded ``stinger``. Following the hint stored in ``something_wrong_with_stinger``, a single character was added to the front of ``stinger``, e.g. ``aGhpcy1pc3NhLXdlaXJkLWtleQ==``, which when decoded yields ``hhis-issa-weird-key``. From this, we can guess that the ``stinger`` should be ``this-issa-weird-key`` (base64: ``dGhpcy1pc3NhLXdlaXJkLWtleQ==``). Upon further inspection, we also find the ``com.example.deva.rightOfPassage`` class: ```java package com.example.deva; import java.security.MessageDigest; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class rightOfPassage { public static byte[] feelpain(String str, String str2) throws Exception { byte[] bytes = str.getBytes(); byte[] bArr = new byte[16]; new SecureRandom().nextBytes(bArr); IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr); MessageDigest instance = MessageDigest.getInstance("SHA-256"); instance.update(str2.getBytes("UTF-8")); byte[] bArr2 = new byte[16]; System.arraycopy(instance.digest(), 0, bArr2, 0, 16); SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "AES"); Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding"); instance2.init(1, secretKeySpec, ivParameterSpec); byte[] doFinal = instance2.doFinal(bytes); byte[] bArr3 = new byte[(doFinal.length + 16)]; System.arraycopy(bArr, 0, bArr3, 0, 16); System.arraycopy(doFinal, 0, bArr3, 16, doFinal.length); return bArr3; } } ``` In essence, the ``feelpain(str, str2)`` function appears to perform AES [Encryption](https://docs.oracle.com/javase/7/docs/api/constant-values.html#javax.crypto) (CBC mode, with PKCS5 Padding) on ``str``, with the key being ``SHA-256(str2)``, and with the IV being randomly generated. From this, we can assume that ``painz`` in the ``com.example.deva.pain`` class is the ciphertext, while ``SHA-256(this-issa-weird-key)`` is the key. Unfortunately, we do not have the IV used. Nevertheless, we still attempted to perform decryption with a null IV (should be fine, as not knowing the IV will only affect decryption for the first AES block): ![image](https://user-images.githubusercontent.com/19924575/205871565-4f59465f-9bfd-426b-aa09-79a947f12b63.png) Short python utility script to convert ``painz`` into a hex string: ```python painz = [-55, -98, 98, 14, -121, -110, 84, -3, 74, 10, 106, -27, -13, -112, -42, 111, -1, -89, -64, 46, -15, -108, -26, 59, -111, 113, 2, -69, -83, 45, -31, -103, 46, -84, -113, 116, -110, -36, 22, -23, 86, 38, -17, 0, 100, -65, 94, 48, 76, 17, 35, -117, -51, -81, -95, 49, 62, -28, 96, 86, 65, 76, 57, 40] s = "" for x in painz: if x < 0: x += 256 s += hex(x)[2:].zfill(2) print(s) ``` Output: ``` c99e620e879254fd4a0a6ae5f390d66fffa7c02ef194e63b917102bbad2de1992eac8f7492dc16e95626ef0064bf5e304c11238bcdafa1313ee46056414c3928 ``` In addition, [Cyberchef](https://gchq.github.io/CyberChef/#recipe=SHA2('256',64,160)&input=dGhpcy1pc3NhLXdlaXJkLWtleQ) was used to get ``SHA-256("this-issa-weird-key") = 317515b6d4eb725e11ef6bd047eca21ffd880b39610902c6ce77a33909641a90``. From this, we should be able to attempt AES decryption. Unfortunately, the AES decryption was unsuccessful. Fortunately, after closer inspection we realized that only the first ``16`` bytes of the SHA-256 digest were actually being used, due to the the following line: ```java System.arraycopy(instance.digest(), 0, bArr2, 0, 16); ``` Amending the key to be ``317515b6d4eb725e11ef6bd047eca21f`` instead, the [AES decryption](https://gchq.github.io/CyberChef/#recipe=AES_Decrypt(%7B'option':'Hex','string':'317515b6d4eb725e11ef6bd047eca21f'%7D,%7B'option':'Hex','string':'00000000000000000000000000000000'%7D,'CBC','Hex','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D)&input=Yzk5ZTYyMGU4NzkyNTRmZDRhMGE2YWU1ZjM5MGQ2NmZmZmE3YzAyZWYxOTRlNjNiOTE3MTAyYmJhZDJkZTE5OTJlYWM4Zjc0OTJkYzE2ZTk1NjI2ZWYwMDY0YmY1ZTMwNGMxMTIzOGJjZGFmYTEzMTNlZTQ2MDU2NDE0YzM5Mjg) was successful, and we obtained the flag ``STF22{f33l_p41n_4nd_n0w_y0u_w1ll_kn0w_0urs}``. ## Misc / Jaga Almighty's Grand Adventure ### Challenge We are given an executable that is essentially a clone of the game Flappy Bird. We are told that we need to pass ``69`` pipes in order to win and get the flag. ### Idea There was an attempt to run ``strings`` on the executable to look for the flag, as well as decompile the executable to find some flag generation function, however both attempts were unsuccessful. Instead, Cheat Engine was used to try and manipulate the score counter in order to trick the game into thinking we had actually passed ``69`` pipes. ### Attempt 1 - Modify Score Counter Directly (Unsuccessful) To do this, we first need to get the address of the score counter. This can be done by starting a "New Scan", and running multiple rounds of the game and running "Next Scan" with the score value at the end of each round. After a few rounds, one should notice that the address ``super_jaga_adventure.exe+55ECB8`` is the score counter. From there, one can use Cheat Engine's "Change value of selected addresses" function to modify the value to ``69``. However, this is unsuccessful, as it is observed that this address is being constantly updated, causing our modified value to be discarded. ### Attempt 2 - Disable Opcodes that Modify Score Counter (Unsuccessful) To do this, we can use Cheat Engine's "Find out what writes to this address" function. From there, one should notice that the ``mov`` operation at address ``super_jaga_adventure.exe+21CE8F`` is responsible for updating the score counter. An attempt was made to replace the ``mov`` operation there with a ``nop`` operation, followed by modifing the score counter value similar to Attempt 1. However, this too is unsuccessful, as it appears that our modified value is not being read by the flag generation function. ### Attempt 3 - Change Win Condition (Successful) To do this, we can use Cheat Engine's "Find out what accesses this address" function. From there, one should notice that the ``cmp`` operation at address ``super_jaga_adventure.exe+21C8E0`` is responsible for reading the score counter. This presumably, is done to check for the win condition of passing through ``>= 69`` pipes. ![image](https://user-images.githubusercontent.com/19924575/205828836-8c92600a-a446-47ec-b722-e6a4d4a78b9a.png) A few lines below, one can see the ``cmp rsi, 45`` operation. By double-clicking on the operation, one can modify it to ``cmp rsi, 00``. Upon playing another round of the game, eventually one will notice a text at the Top-Left of the screen: ![image](https://user-images.githubusercontent.com/19924575/205830194-a443d49b-65d5-4c84-8606-e331dc3268a4.png) After navigating to the folder that contains the executable, one should notice a file called ``nice_flag.bin`` has been created, which contains the flag ``STF22{iF_n0_s@vep0int_s@d_lIfE}``. ## Crypto / jagacha ### Challenge We are provided a server to connect to, as well as ``jagacha.py`` and ``config.py``: **config.py** ```python #!/usr/bin/env python3 import time def get_seed(): SEED = 0xdeadc0de return SEED + time.time_ns() # Rest of the file has been omitted since it is just ASCII Art ``` **jagacha.py** ```python #!/usr/bin/env python3 import asyncio import logging import random from config import ( get_seed, FLAG, ASCII_JAGA, ASCII_JAGAHACKER, ASCII_JAGASCHOLAR, ASCII_JAGASUPER, ) logger = logging.basicConfig(level=logging.INFO) HOST = "127.0.0.1" PORT = 8080 GACHAS = { "Jaga-chan": ASCII_JAGA, "Jaga Hacker": ASCII_JAGAHACKER, "Jaga Scholar": ASCII_JAGASCHOLAR, "Super Jaga": ASCII_JAGASUPER, } GACHA_KEYS = [*GACHAS.keys()] async def print_menu(writer: asyncio.StreamWriter): writer.writelines( ( b"Welcome to the Jaga Gacha!\n", b"We have an event going on featuring the limited SSS-rarity flag-chan!\n", b"Here are the Gacha pull rates:\n", *[f"- {v}: 25 %\n".encode() for v in GACHAS], b"- flag-chan: 0 %\n", b"All the best!\n", b"\n", b"Options:\n", b"1. Roll a Gacha\n", b"2. I'm Feeling Lucky!\n", b"3. Exit\n", ) ) await writer.drain() async def read_number( reader: asyncio.StreamReader, writer: asyncio.StreamWriter, prompt=b"> " ): writer.write(prompt) await writer.drain() while not (line := await reader.readline()).rstrip().isdigit(): writer.writelines(( b"Input is not a valid number\n", prompt, )) await writer.drain() return int(line) async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): client_ip, client_port = reader._transport.get_extra_info('peername') logging.info(f"New connection from: {client_ip}:{client_port}") rand = random.Random(get_seed()) try: option = None while option != 3: await print_menu(writer) option = await read_number(reader, writer) if option == 1: num = rand.getrandbits(64) gacha = GACHA_KEYS[num % len(GACHA_KEYS)] writer.writelines( ( f"Congrats! You have pulled a {gacha}!\n".encode(), GACHAS[gacha], b"Here are the stats of your character:\n", f"STR: {num>>48 & 0xffff}\n".encode(), f"DEX: {num>>32 & 0xffff}\n".encode(), f"INT: {num>>16 & 0xffff}\n".encode(), f"LUK: {num & 0xffff}\n".encode(), b'\n', ) ) elif option == 2: num = rand.getrandbits(64) lucky_number = await read_number( reader, writer, b"Enter your lucky number: " ) if lucky_number == num: writer.writelines(( b"Congrats! You have pulled the limited SSS-rated rarity flag-chan!\n", FLAG, )) option = 3 # Quit else: writer.write( b"Oops! Looks like you are not as lucky as you thought! Try again!\n\n" ) elif option == 3: writer.write(b"See you again!\n") await writer.drain() finally: writer.write_eof() writer.close() async def main(host, port): srv = await asyncio.start_server(handler, host, port) await srv.serve_forever() if __name__ == "__main__": asyncio.run(main(HOST, PORT)) ``` ### Observations & Ideas - In order to get the flag, we need to select option ``2``, and be able to produce a "lucky number" such that it is equal to ``rand.getrandbits(64)`` (i.e. we need to be able to predict the output of ``rand.getrandbits(64)`` without knowing the random seed). It is not possible to brute force this, as the probability of randomly guessing the correct "lucky number" is a ``1/(2^64)`` probability. - Although we know that the random seed is generated by ``get_seed()``, which makes use of ``time.time_ns()``, and we roughly know the time when the connection is made to the server, because ``get_seed()`` uses time in nanoseconds it would likely be infeasible to brute force the correct seed. - By selecting option ``1``, we are able to get the output of a ``rand.getrandbits(64)`` call since the resulting ``num``'s bits are split up into 4x 16-bit integers, ``STR``, ``DEX``, ``INT``, and ``LUK`` (i.e. we can reconstruct the original ``num`` returned by the ``rand.getrandbits(64)`` call). - The ``getrandbits()`` function found in Python's ``random`` module is [**pseudo-random** and **deterministic**](https://docs.python.org/3/library/random.html) as it uses the Mersenne Twister as the random number generator, meaning it is possible for one to predict the result of a ``getrandbits()`` call without knowing the seed. - There are multiple open-source libraries, such as [RandCrack](https://github.com/tna0y/Python-random-module-cracker) that can automatically crack the seed and perform predictions (e.g. ``getrandbits()``) given sufficient input data. ### Solution Implementation Note: RandCrack's ``submit()`` function only takes in the output of ``getrandbits(32)``. However, for this challenge, the server returns ``getrandbits(64)``. This issue can be resolved by treating the server's ``getrandbits(64)`` result as 2x ``getrandbits(32)``, and submitting it to RandCrack accordingly. ```python from randcrack import RandCrack rc = RandCrack() def submit64Bits(x): rc.submit(x & 0xffffffff) #32 LSB rc.submit(x >> 32 & 0xffffffff) #32 MSB from pwn import * r = remote("178.128.20.125", 31594) #Step 1: Get sufficient samples of getrandbits() for RandCrack for rnd in range(312): #RandCrack needs 624x getrandbits(32) samples (i.e. 312x getrandbits(64) samples) for _ in range(14): r.readline() r.sendline("1") r.recvuntil("Here are the stats of your character:") #Skip all the ASCII Art r.readline() dat = [] for _ in range(4): #Read the STR, DEX, INT, LUK values dat.append(int(r.readline()[5:-1])) num = (dat[0]<<48) + (dat[1]<<32) + (dat[2]<<16) + dat[3] #Reconstruct num print(rnd, num) submit64Bits(num) r.readline() #Step 2: Predict getrandbits() to get flag for _ in range(14): r.readline() r.sendline("2") r.sendline(str(rc.predict_getrandbits(64))) r.interactive() ``` From this, we get the flag ``STF22{W@IFU5_L@1FU5}``. ## Crypto / Encryptdle ### Challenge We are provided a server to connect to, as well as ``server.py``: ```python import os from typing import List from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from fastapi import FastAPI, Query from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import uvicorn try: with open('flag.txt', 'rb') as f: FLAG = f.read() except: FLAG = b'FLAGFLAGFLAGFLAGFLAGFLAGFLAGFLAG' KEY = os.urandom(32) IV = os.urandom(16) MAX_LEN = 32 def pad(bstring): return (bstring + FLAG + b'\x00' * MAX_LEN)[:MAX_LEN] def encrypt(bstring): padded_data = pad(bstring) cipher = Cipher(algorithms.Camellia(KEY), modes.CBC(IV)) encryptor = cipher.encryptor() return encryptor.update(padded_data) + encryptor.finalize() ANSWER = encrypt(FLAG).hex().upper() # correct, present, absent def judge(guess, answer): ALPHABET = '0123456789ABCDEF' result = [{'letter': c, 'presence': 'absent'} for c in guess] count = [0 for i in range(len(ALPHABET))] for c in answer: count[int(c, 16)] += 1 for i in range(len(guess)): if(guess[i] == answer[i]): result[i]['presence'] = 'correct' count[int(guess[i], 16)] -= 1 for i in range(len(guess)): if(result[i]['presence'] == 'correct'): continue if(count[int(guess[i], 16)] > 0): result[i]['presence'] = 'present' count[int(guess[i], 16)] -= 1 return result app = FastAPI() class LetterResult(BaseModel): letter: str presence: str @app.post("/api/compare", response_model=List[LetterResult]) async def compare(guess: str = Query(..., min_length=1, max_length=MAX_LEN)): enc_guess = encrypt(guess.encode()).hex().upper() return judge(enc_guess, ANSWER) @app.get("/api/ping") async def ping(): return "pong" app.mount("/", StaticFiles(directory="frontend/dist/", html=True), name="static") if __name__ == '__main__': uvicorn.run(app, host='127.0.0.1', port=80) ``` ### Observations & Ideas - We can connect to the server via a web browser to enter any input string of our choice (length must be in range [1, 32]). After which, the server will encrypt the first 32 bytes in ``<input string> + flag + 32x \x00 bytes`` and return the ciphertext to us. - The bytes being encrypted are encrypted using the [Camellia Cipher](https://en.wikipedia.org/wiki/Camellia_(cipher)), which is a symmetric block cipher (similar to AES). We can treat it as though it is basically AES, since Camellia has "security levels and processing abilities comparable to the Advanced Encryption Standard" and also shares the same block size of 128-bits as AES. - Since we treat Camellia as being just as secure as AES, this means that vulnerabilities in AES' block cipher mode of operation will also apply to Camellia here. For this challenge, the cipher's ``KEY`` and ``IV`` values are random, and we will not be able to extract their values. - However, since we are able to **prepend** data **before the flag** and get the resulting ciphertext, the flag can be leaked via a [Chosen Plaintext/Oracle Attack](https://crypto.stackexchange.com/questions/42891/chosen-plaintext-attack-on-aes-in-ecb-mode), meaning we can ignore the "correct"/"present"/"not present" Wordle data. The Oracle Attack is a classic exploit, and can be found in [Cryptopals](https://cryptopals.com/sets/2/challenges/12) as well as [CryptoHack's ECB Oracle Challenge](https://cryptohack.org/challenges/aes/). - Although we are provided a UI to enter our predictions, if we use the UI, we are limited to a maximum of 10 attempts before we would need to reset the Docker instance. However, if we use the ``/api/compare`` endpoint, we have unlimited attempts, and can automate the Oracle Attack. - Since we are using a web endpoint to transmit our input string via a URL query string, we have to be careful when sending reserved characters (e.g. '&', '=', etc.). This can be avoided by letting the Python ``requests`` module automatically do the URL escaping for us. ### Solution ```python import requests import json from binascii import unhexlify def encrypt(payload): #payload is in hex payload = unhexlify(payload) print(payload) res = requests.post("http://206.189.89.253:31120/api/compare", params=[('guess', payload)]).text #Ensure payload is properly URL-escaped ans = "" dat = json.loads(res) for x in dat: ans += x["letter"] return ans def encryptC(payload, c): #Returns c-th chunk return encrypt(payload)[(c-1)*32:c*32] flag = "" for x in range(15, 0, -1): #Leak first chunk prepayload = x * "00" print(prepayload) target = encryptC(prepayload, 1) print(target) for i in range(32, 127): res = encryptC(prepayload + flag.encode().hex() + hex(i)[2:], 1) if res == target: flag += chr(i) print("Leaked " + flag) break for x in range(16, 0, -1): #Leak second chunk, start from 16 because we are not allowed to encrypt nothing (i.e. 0-length string) prepayload = x * "00" print(prepayload) target = encryptC(prepayload, 2) print(target) for i in range(32, 127): res = encryptC(prepayload + flag.encode().hex() + hex(i)[2:], 2) if res == target: flag += chr(i) print("Leaked " + flag) break ``` From this, we get the flag ``STF22{iNS3CuR3!_S+4+iC_IVs!!}``. ## Crypto / Pad the Flag ### Challenge We are provided a server to connect to, as well as ``source.py``: ```python import rsa from Crypto.Util.number import bytes_to_long, long_to_bytes from secret import FLAG ENCRYPTED_FLAG = "AAC249B3678C794D115E35E895966E69EE110F9483733602FE27075DB677EA616A5FF69FA22B7E303A4EC7D4FC39E9E62DFF1BDC7A386480F7AD1248D66061D14179B0C5455C48B329D338A9637818794532813940614F367466B49CBECB0A19E74E056170E7049F5F627ABEFB0915CDBDB9E1A5AC5AFFD2F0BE5826A0A9D0C2336CCADDC119CC64B97AF203DCF0E27D3F544A5944485BB9EE935B432BEB39B18410A78B2BC0F5585D6058B2DD3516E6B2F6375B885E6339FED7E7DCD89476D221C7C5A0EB14351373882768894FC154FAB68C45B16047AB85801198CC8050EAD6ABFC30E125A34951DDE958B1054CAE9240A52AF77A59445C20182C1FD5B601" PADDING = b"\x00\x04" with open("private_key.pem", "rb") as f: PRIVATE_KEY = rsa.PrivateKey.load_pkcs1(f.read()) with open("public_key.pem", "rb") as f: RSA_PUBLIC_KEY = rsa.PublicKey.load_pkcs1(f.read()) class RSA(): def __init__(self): self.private_key = PRIVATE_KEY self.padding = PADDING def rsa_decrypt(self, ciphertext, priv_key) -> bytes: encrypted = bytes_to_long(ciphertext) decrypted = priv_key.blinded_decrypt(encrypted) cleartext = long_to_bytes(decrypted,256) return cleartext def unpad(self, padded): padded = padded[2:] idx = padded.find(b'\x00') if (idx == -1): return b"" else: message = padded[idx + 1:] return message def valid(self, plaintext): if len(plaintext) < 11: return False return plaintext[:2] == self.padding def decrypt(self, ciphertext): try: return self.rsa_decrypt(bytes.fromhex(ciphertext), self.private_key) except Exception as e: print(e) return None def main(): rsa = RSA() while True: option = int(input("option: ")) if option == 1: print(f'n: {RSA_PUBLIC_KEY.n}') print(f'e: {RSA_PUBLIC_KEY.e}') elif option == 2: print(f'Encrypted Flag: {ENCRYPTED_FLAG}') elif option == 3: encrypted = input("Enter Encrypted Flag: ") plaintext = rsa.decrypt(encrypted) if plaintext is None: print("Decryption Failed!") continue if not rsa.valid(plaintext): print("Invalid Padding!") continue plaintext = rsa.unpad(plaintext) if plaintext == FLAG: print("The flag is correct!") else: print("The flag is incorrect!") else: print("Goodbye") exit(1) if __name__ == "__main__": main() ``` ### Observations & Ideas - We are given an RSA public key (``n`` and ``e``), as well as the encrypted flag (``ENCRYPTED_FLAG``). Before being encrypted, the flag is padded in the form ``\x00 \x04 <some non-\x00 bytes> \x00 <flag>`` such that the length of the padded string is exactly 256 bytes. - We are able to submit ciphertexts to the server (via option ``3``) for decryption. Assuming that we have formatted the ciphertext properly, the server will return one of 3 outputs (invalid padding, valid padding with incorrect flag, and valid padding with correct flag). - Since the public key appears secure ([RsaCtfTool](https://github.com/RsaCtfTool/RsaCtfTool) finds no vulnerabilities), and no extra data is provided, we can presume that we have to extract the flag through some form of Oracle Attack using option ``3``. - After Googling [Chosen Ciphertext Attacks](https://en.wikipedia.org/wiki/Chosen-ciphertext_attack), one will be able to find [Bleichenbacher's Attack](https://medium.com/@c0D3M/bleichenbacher-attack-explained-bc630f88ff25) which is a Padding Oracle Attack (i.e. Oracle Attack that makes use of the server telling us if the ciphertext is incorrectly padded). - Bleichenbacher's Attack works on the [PKCS #1 Padding Scheme](https://crypto.stackexchange.com/questions/12688/can-you-explain-bleichenbachers-cca-attack-on-pkcs1-v1-5), in which plaintext is padded in the form ``\x00 \x02 <some non-\x00 bytes> \x00 <plaintext>``, which is very similar to the padding used in this challenge (the only difference is ``\x02`` being replaced with ``\x04``). As such, the only thing that needs to be changed is that the value of ``B`` should be doubled (refer to [this article](https://medium.com/@c0D3M/bleichenbacher-attack-explained-bc630f88ff25)). ### Solution Solution is adapted from [emilystamm's rsa-bleichenbacher](https://github.com/emilystamm/rsa-bleichenbacher), with all functions/indivdual lines changed being marked with ``# MODIFIED``. **main.py** ```python from rsa import RSA_Cipher from attack import Bleichenbacher from Crypto.Util.number import bytes_to_long, long_to_bytes # MODIFIED if __name__ == "__main__": # Initial Message message = "AES Session Key = 1234567890ABCDEF" encoded = "AES Session Key = 1234567890ABCDEF".encode("utf-8") # Create RSA Cipher bits = 2048 # Choose bits (1024, 2048, ...) # MODIFIED RSA_Cipher = RSA_Cipher(bits) # Encryption ciphertext = 0xAAC249B3678C794D115E35E895966E69EE110F9483733602FE27075DB677EA616A5FF69FA22B7E303A4EC7D4FC39E9E62DFF1BDC7A386480F7AD1248D66061D14179B0C5455C48B329D338A9637818794532813940614F367466B49CBECB0A19E74E056170E7049F5F627ABEFB0915CDBDB9E1A5AC5AFFD2F0BE5826A0A9D0C2336CCADDC119CC64B97AF203DCF0E27D3F544A5944485BB9EE935B432BEB39B18410A78B2BC0F5585D6058B2DD3516E6B2F6375B885E6339FED7E7DCD89476D221C7C5A0EB14351373882768894FC154FAB68C45B16047AB85801198CC8050EAD6ABFC30E125A34951DDE958B1054CAE9240A52AF77A59445C20182C1FD5B601 # MODIFIED ciphertext = long_to_bytes(ciphertext) # MODIFIED # Attack stolen_message = Bleichenbacher(ciphertext, RSA_Cipher) print("\nBleichenbacher Stolen Message:", stolen_message) print("\nOriginal Message:", message) print("\nStolen Message == Original Message:", message == stolen_message) ``` **rsa.py** ```python from Crypto.PublicKey import RSA from Crypto.Util.number import bytes_to_long, long_to_bytes # MODIFIED from pwn import * # MODIFIED r = remote("167.99.77.149", 30984) # MODIFIED n = 27745498838268342270390541832410459717876029506186104405197835711890151776448053156671628267359667675031113730557765107424510849444151656396790371676445777855008849863539302384743329990999275939998343733038454223585107152570883409665827946862924689758759230343926979376299418234007499792518418292789552633399836649032240094044536084198009044814206431438837607278069180294900543129080438000781188486862675568725667427075189589807189475163889785156857630938353589579525509381815160099955761294061615683975133026307438754024582460800766260510600437895339795565140736784017970754976295616609792522708200768185786779871199 # MODIFIED e = 65537 # MODIFIED class RSA_Cipher: def __init__(self, bits): # MODIFIED self.bits = bits self.public_key = RSA.construct((n, e)) def PublicKey(self): return self.public_key def Bits(self): return self.bits def Encrypt(self, message): # MODIFIED return long_to_bytes(pow(bytes_to_long(message), self.public_key.e, self.public_key.n)) def Oracle(self, ciphertext): # MODIFIED ciphertext = bytes_to_long(ciphertext) r.sendline("3") send = hex(ciphertext)[2:].zfill(512) r.sendline(send) res = r.readline() return b"Invalid Padding!" not in res ``` **attack.py** ```python from Crypto.Util.number import bytes_to_long, long_to_bytes import intervals as I import time def ceil(a, b): return a // b + (a % b > 0) def floor(a,b): return a // b # Decode message def PKCS1_decode(encoded): encoded = encoded[2:] idx = encoded.index(b'\x00') message = encoded[idx + 1:] return message.decode("utf-8") # Call oracle with integer c to determine if corresponding plaintext conforms def CallOracle(c, RSA_Cipher): ciphertext = long_to_bytes(c) return RSA_Cipher.Oracle(ciphertext) # Calculate c_i def CalculateC_i(c,e,n, lower, upper, calls_to_oracle, RSA_Cipher): s_i = lower c_i = (c * pow(s_i,e,n)) % n while not CallOracle(c_i, RSA_Cipher) and s_i <= upper: # To keep track of iterations if calls_to_oracle % 10000 == 0: print("Calls to Oracle : ", calls_to_oracle) calls_to_oracle += 1 # Increment s_i s_i += 1 # Calculate the new c_i c_i = (c * pow(s_i,e,n)) % n if s_i > upper: return 0, calls_to_oracle else: return s_i, calls_to_oracle # Bleichenbacher attack def Bleichenbacher(ciphertext, RSA_Cipher): # Initialize variables bits = RSA_Cipher.Bits() public_key = RSA_Cipher.PublicKey() e = public_key.e n = public_key.n i = 1 calls_to_oracle = 0 start_time = time.time() # Compute constant variable B and initial interval M_0 = [a,b] B = pow(2, 8 * (bits // 8 - 2)) B = B*2 # MODIFIED - To fit new 0x00 0x04 padding a = 2 * B b = 3 * B - 1 M_i_1 = I.closed(a,b) # Step 1: Calculate c, s_0 c = bytes_to_long(ciphertext) s_i_1 = 1 # Printing print("\n\n======================================================\n", "Bleichenbacher Attack on RSA PKCS v1.5", "\n======================================================\n", "\nPublic Key\ne =", e, "\nn =", n, " (", bits, "bits )\n\nCiphertext") print(ciphertext) print("\n\nIteration 0\n---------------------------------------------") print("\nStep 1: find ciphertext c in integer form\nc =", c) print("\nConfirm Call Oracle on Given Ciphertext:", CallOracle(c, RSA_Cipher)) # Check that CallOracle works print("\nM_0 =", M_i_1) # Repeat until you M_i a single interval with one element while M_i_1.lower != M_i_1.upper: print("\n\nIteration ", i, "\n---------------------------------------------") # Step 2 : Find s_i # Step 2a: Calculate s_i, smallest int >= n/3B that conforms if i == 1: print("\nStep 2a: find smallest s_1 >= n/3B such that plaintext corresponding to c(s_1^e) mod n conforms") s_i, calls_to_oracle = CalculateC_i(c, e, n, ceil(n, 3 * B), n-1, calls_to_oracle, RSA_Cipher) print("s_" + str(i), " = ", s_i, "\n") # Step 2b: If M_i-1 has multiple disjoint intervals, # Calculate smallest s_i > s_i_1 that conforms elif len(M_i_1) > 1: print("\nStep 2b: find smallest s_i > s_(i-1) such that plaintext corresponding to c(s_i^e) mod n conforms\ns_i_1 = ", s_i_1) s_i, calls_to_oracle = CalculateC_i(c, e, n, s_i_1 + 1, n-1, calls_to_oracle, RSA_Cipher) #change to s_i + 1 print("s_" + str(i), " = ", s_i, "\n") # Step 2c: Number of intervals = 1 -> find s_i else: print("\nStep 2c: vary integer r_i and s_i until find s_i such that plaintext corresponding to c(s_i^e) mod n conforms") a , b = M_i_1.lower , M_i_1.upper r_i = ceil(2 * b * s_i_1 - 2 * B, n) s_i = 0 # While s_i corresponding plaintext to c(s_i)^e mod n does not conform while s_i == 0: lower = ceil(2 * B + r_i * n, b) upper = ceil(3 * B + r_i * n, a) - 1 s_i, calls_to_oracle = CalculateC_i(c, e, n, lower, upper, calls_to_oracle, RSA_Cipher) r_i += 1 print("s_" + str(i), " = ", s_i, "\n") # Step 3: Reduce M_i_1 to M_i and store # Initialize M_i print("\nStep 3: after s_i found, reduce set M_(i-1) to M_i") M_i = I.empty() # For each disjoint interval [a,b] in M_(i-1) for interval in M_i_1: a , b = interval.lower , interval.upper low_r = ceil(a * s_i - 3 * B + 1, n) high_r = ceil(b * s_i - 2 * B, n) for r in range(low_r, high_r): i_low = max(a , ceil(2 * B + r * n, s_i)) i_high = min(b , floor(3 * B - 1 + r * n, s_i)) M_new = I.closed(i_low, i_high) M_i = M_i | M_new print("M_" + str(i), " = ", M_i) # Reset variables for next round M_i_1 = M_i s_i_1 = s_i i += 1 # Step 4: single remaining element (M_i.lower = M_i.upper) * (s_0 ^-1 mod n) print("Calls to Oracle: ", calls_to_oracle) print("\nStep 4: M_i.lower = M_i.higher = m, the message in integer form") m = M_i.lower print("\nRecovered Message Integer = ", m) sbytes = long_to_bytes(m) print("\nMessage Converted to Bytes", sbytes) message = PKCS1_decode(sbytes) elapsed_time = time.time() - start_time print("\n---------------------------------------------\nTime Elapsed:", elapsed_time / 60, " minutes\n") return message ``` From this, the flag is ``STF22{p@dd1ng_pr0b13m_3v3rywh3r3}``.