Flareon5 #6 Challenge : Magic

In this post I’ll try to explain how I bruteforced challenge 6 with frida.

First of all lets try to understand what binary does.

< 6 > ./magic 
Welcome to the ever changing magic mushroom!
666 trials lie ahead of you!
Challenge 1/666. Enter key: 123
No soup for you!

Binary will take 666 input. Follow the flow of the program, it goes to sub_402dcf after taking input from user.
ida1

In sub_402dcf, program iterates 0x21 times.
ida2
It checks size of input with 0x40. If its less than 0x40 program goes to sub_402cc7 and exits. I should avoid sub_402cc7.

Each iteration program goes to 0x605100 + 0x120*i and take somethings. Lets look at structure for first iteration (0x605100)
ida3

0x400c55,0x147,0x02,0x03,0x25,0x61385a …

Firstly it takes 0x400c55 (first region) and 0x61385a (second region) and 0x147(size) . It xors first region with second region. After that it calls first region at 0x402f06 (call rcx).

gdb

If you look at size of “A”s in rax ,it says 61 times but I entered 63 times and remember 0x2 from first structure. So 0x2 is index of string. Also see 0x3 at rsi register. After analyzing 0x400c55 it turns out 0x3 is our length.

What I learned so far.

  • Key size is 69, (max number at structure 0x42+3)
  • Before call rcx, program xors region by getting parameters from structure.
  • Call rcx is taking length and index of strings which comes from structure.
  • If its correct program goes to next iteration, so probably different function will be executed for checking input.

I tried to understand each function. I reversed 3 functions but stuck at 4th. (turns out it was base64). Then I looked at structure, and saw each function takes only 1-3 length.
So why not bruteforce it ? Since I love frida and never tried instrumentation on linux binaries lets give it a shot.

First lets learn how functions called with frida. If functions exported you can define NativeFunction with

NativeFunction(Module.findExportByName(null,'printf'), 'pointer', []);

But I will use functions that aren’t exported. So I will give pointer to NativeFunction such as:

var xor = new NativeFunction( ptr(0x402CDF), 'void', ['pointer', 'pointer', 'pointer']);

First parameter is pointer to beginning of routine. Second parameter is type of what function will return. Third parameter is types of parameter which function will accept.

Lets try it

var xor = new NativeFunction( ptr(0x402CDF), 'void', ['pointer','pointer','pointer']);
var fnc1 = new NativeFunction( ptr(0x400c55), 'int', ['pointer','pointer','pointer','pointer','pointer','pointer','pointer']);

Interceptor.replace(ptr(0x400c55), new NativeCallback(function (p,p1,p2,p3,p4,p5,p6) {

console.log(JSON.stringify(this.context))
return 1

}, 'int', ['pointer','pointer','pointer','pointer','pointer','pointer','pointer']));



xor(ptr(0x400c55),ptr(0x147),ptr(0x61385a))

Memory.writeS32(ptr(0x00605000),0x20676e)
t = Memory.readUtf8String(ptr(0x00605000),3)
console.log("Written : " + t)

q = ptr(0x605001)
q1= ptr(0x605002)
q2= ptr(0x605003)
q3= ptr(0x605004)
q4= ptr(0x605005)
q5= ptr(0x605006)
q6= ptr(0x605007)
s = fnc1(q,q1,q2,q3,q4,q5,q6)
if (s){
console.log("Yey")
}

I defined 2 functions, first one xor routine other one is where our input checked. Second routine returns int, After that I set a Interceptor to see registers before getting into function.
You can write into memory with Memory.writeS* . First parameter is pointer , second is what you want to write. You read locations as a string with readUtf8String function.
I give 7 parameter to fnc1 to see state of registers. Attach to binary with frida magic -l brute.js

gdb2

So order of parameters is : rdi,rsi,rdx,rcx,r8,r9..
I used this to understand which register my parameter goes.

Now I can define functions with pointers, write strings to memory and read memory. So I have everything I need.

Before bruteforcing lets parse structure, open all functions with xor.

s=[]
for(l =0;l<33;l++){
base = 0x605100+(0x120*l)
s[l*5] = Memory.readS32(ptr(base+0x8))
s[l*5+1] = Memory.readS32(ptr(base+0xc))
s[l*5+2] = Memory.readS32(ptr(base+0x10))
s[l*5+3] = Memory.readS32(ptr(base))
s[l*5+4] = Memory.readS32(ptr(base+0x18))
}

for(var i =0;i<s.length/5;i++)
{
xor(ptr(s[i*5+3]),ptr(s[i*5]),ptr(s[i*5+4]))
}

Define our pools with len 1, 2 and 3 for bruteforcing

pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ "
pool_1 = []
pool_2 = []
pool_3 = []

for (var i =0;i<pool.length;i++)
{

pool_1[i] = pool[i]
for (var j =0;j<pool.length;j++)
{
pool_2[i*pool.length+j] = pool[i]+pool[j]
for (var k =0;k<pool.length;k++)
{
pool_3[i*pool.length*pool.length+j*pool.length+k] = pool[i]+pool[j]+pool[k]
}
}
}

Get parameters from s and bruteforce functions.

function toHex(str,hex){

str = str.split("").reverse().join("");
hex = unescape(encodeURIComponent(str))
.split('').map(function(v){
return v.charCodeAt(0).toString(16)
}).join('')
stri = '0x'+hex
return parseInt(stri)
}

sol = " "
for(var i=0;i<s.length/5;i++)
{
pool_l = []
len = s[i*5+2]
if(len == 1) pool_l = pool_1
else if(len == 2) pool_l = pool_2
else if(len == 3) pool_l = pool_3
else pool_l = pool
found = false
var fnc1 = new NativeFunction(ptr(s[i*5+3]), 'int', ['pointer','pointer','pointer']);
for(var j = 0;j<pool_l.length;j++)
{

Memory.writeS32(ptr(0x400000),toHex(pool_l[j]))
t = Memory.readS32(ptr(0x400000),len)
b = 0x605120+(0x120*i)
fnc_r = fnc1(ptr(0x400000),ptr(len),ptr(b))
if (fnc_r == 1){
sol = [sol.slice(0,s[i*5+1]),pool_l[j],sol.slice(s[i*5+1]+len)].join('');
sol[i*5+1] = pool_l[j]
console.log(sol)
found = true
break
}
}
}
console.log(sol)

I defined fnc1 NativeFunction which checks user input.
Lastly wrap all script into

setTimeout(function () {
/* your code here */
}, 0);

because if I dont it will timeout.

So running this script will give this.

gdb3

Looks promising. BUT script couldn’t find 2 string. Even now I dont know why it doesnt find it. Maybe some register values doesnt fit idk. Since I was so close to finding first key, I tried to bypass checks and looked program at gdb.

Lets put breakpoint at after call rcx (0x402F0a) and 0x403B6C which is after all input is checked. Give string to program and if doesnt correct bypass next check by setting eax 1.

gdb4

Annd whoa. After hitting 0x403b6c, you can see “Ah, there is nothi like the hot winds of He blowing in your face.” . Turns out program shuffled our input to create meaningfull string.

Googleing sentence and blanks filled with “ng “ and “ll”. This is good. If my script cant find string with len 2, I can say its “ll” , if len is 3 its “ng “.

After giving correct input to binary, without knowing what program does I attached my frida script again. And whoila again I found string. And pool of letters is same.

So I know now, each key is shuffled, and shuffled parts are same. And I can lower my pool to this :

pool1 = 'abcdefghilnorstuwy.,AH '
pool2 = ['e ',' H','is','no','of',' g','bl','ow','ur',' w','in','yo']
pool3 = ["ds ","hot","the",' yo',"ace","thi",'Ah,',' in',' bl','lik']

My plan is set my frida script such that it will find correct key, send to binary, and again again.. for each key. I will use python helper for that.

from pwn import *

s = ['inds isng llg w. e HthitheoftheAh,urnolik inefe yo blrhot in owace']
r = process("./magic")

r.sendline(s[0])
fr = process(['python',"./fri.py"])
for i in range(665):
key =fr.recvline()
print i #
r.sendline(key[:-1])
print(i,key)
r.recvuntil("Congrats!")
r.interactive()

I will start magic and my frida script same time. Get key from frida and send to binary.

I will wrap my frida script to timeout so it will execute after given time (70ms).

import frida
import sys
#inds isng llg w. e HthitheoftheAh,urnolik inefe yo blrhot in owace
#abcdefghijklmnopqrstuvwxyz., ABCDEFGHIJKLMNOPQRSTUVWXYZ
#abcdefghilnorstuwy.,AH
session = frida.attach("magic")
script = session.create_script("""
pool2 = ['e ',' H','is','no','of',' g','bl','ow','ur',' w','in','yo']
pool3 = ["ds ","hot","the",' yo',"ace","thi",'Ah,',' in',' bl','lik']
pool = 'abcdefghilnorstuwy.,AH '
s=[]

var xor = new NativeFunction(ptr(0x402CDF), 'void', ['pointer','pointer','pointer']);

function toHex(str,hex){

str = str.split("").reverse().join("");
hex = unescape(encodeURIComponent(str))
.split('').map(function(v){
return v.charCodeAt(0).toString(16)
}).join('')
stri = '0x'+hex

return parseInt(stri)
}

var remaining = 667;
function crackNext() {

c = 0
sol = " "
for(l =0;l<33;l++){
base = 0x605100+(0x120*l)
s[l*5] = Memory.readS32(ptr(base+0x8))
s[l*5+1] = Memory.readS32(ptr(base+0xc))
s[l*5+2] = Memory.readS32(ptr(base+0x10))
s[l*5+3] = Memory.readS32(ptr(base))
s[l*5+4] = Memory.readS32(ptr(base+0x18))
}
for(var i =0;i<s.length/5;i++)
{
xor(ptr(s[i*5+3]),ptr(s[i*5]),ptr(s[i*5+4]))
}
possible = []
for(var i=0;i<s.length/5;i++)
{
pool_l = []
len = s[i*5+2]
if(len == 1) pool_l = pool
else if(len == 2) pool_l = pool2
else if(len == 3) pool_l = pool3
else pool_l = pool
found = false
var fnc1 = new NativeFunction(ptr(s[i*5+3]), 'int', ['pointer','pointer','pointer']);
for(var j = 0;j<pool_l.length;j++)
{
Memory.writeS32(ptr(0x400000),toHex(pool_l[j]))
t = Memory.readS32(ptr(0x400000),len)
b = 0x605120+(0x120*i)
fnc_r = fnc1(ptr(0x400000),ptr(len),ptr(b))
if (fnc_r == 1){
sol = [sol.slice(0,s[i*5+1]),pool_l[j],sol.slice(s[i*5+1]+len)].join('');
sol[i*5+1] = pool_l[j]
found = true
break
}
}
if(!found){
if(len==3){
sol = [sol.slice(0,s[i*5+1]),"ng ",sol.slice(s[i*5+1]+len)].join('');
}
else{
sol = [sol.slice(0,s[i*5+1]),"ll",sol.slice(s[i*5+1]+len)].join('');
}
}
}
console.log(sol)

for(var i =0;i<s.length/5;i++)
{
//console.log(i*5+3)
xor(ptr(s[i*5+3]),ptr(s[i*5]),ptr(s[i*5+4]))
}
if (--remaining > 0) {
setTimeout(crackNext,70);
}
}
setTimeout(crackNext, 0);




""")
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()



Running sol.py

gdb5

It was fun to use frida. I learned a lot from this challenge. :)