Reverse Engineering

Challenge
Link

F is for Flag

Jump

Reaction

F is for Flag

Description

-

Solution

Given ELF file, open it using IDA. Through dynamic analysis we able to recover some of the function names.

Above code basically doing validation for the result from function std::function<std::variant<unsigned int,std::string,std::shared_ptr<Cons>> ()(void)>::function<main::{lambda(void)#1},void>(. To take a look on the actual code from lambda function we can follow below pattern

  • Click lambda function

    • Click function next to (a1 + 3) or _M_invoke

      • Click __invoke__ function

        • Click __invoke__ function

          • Click operator()

We will see below code as the main process

lambda_1_op1

This function do conversion from input string to dword.

res = ((arr[idx])|(arr[idx+1] << 8)|(arr[idx+2] << 16)|(arr[idx+3] << 24))

lambda_1_op2

  • Click function with the most argument lambda_op2_wrapper(a1, a2, v4, v8, v9);

    • Choose function that processed function_if_false

    • image

      • Click function next to (a1 + 3) or _M_invoke

      • Then do the same flow like the first lambda function

lambda_1_op1 actual code will be look like below

To take a look on the actual code for each wrapper we can do the same approach. During the competition we analyze it dynamically and make conclusion for each wrapper

  • lambda_op_2_false_wrapper1

    • mapping 4 bit

  • lambda_op_2_false_wrapper2

    • multiplication with 0x4e6a44b9

  • lambda_op_2_false_wrapper3

    • process input with xor and rol

  • lambda_op_2_false_wrapper4

    • should be looping

lambda_1_op3

Do validation for processed input and static values

After getting all the algorithm we reconstruct the whole process in python.

def print_hex(arr):
    tmp = []
    for i in arr:
        tmp.append(hex(i))
    print(tmp)

w='0123456789abcdef'
x='3e1a49568bf2dc07'
rev_mapper={}
mapper={}
mul=0x4e6a44b9
for i in range(16):
    rev_mapper[x[i]]=w[i]
    mapper[w[i]]=x[i]

def mpp(byt):
    return int.from_bytes(bytes.fromhex(''.join([mapper[c] for c in byt])),"little")

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))



p=b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}"
BLOCK=[p[i:i+4].hex() for i in range(0,len(p),4)]


ctr=12

for _ in range(8):
    BLOCK=[mpp(BLOCK[i]) for i in range(len(BLOCK))]
    res=[]
    counter = 0
    for a4 in range(ctr,ctr-13,-1):
        res=[ROL(BLOCK[((a4+3)%16)],29,32)^ROL(BLOCK[((a4+2)%16)],17,32) ^ ROL(BLOCK[((a4+1)%16)],7,32) ^ BLOCK[(a4%16)]]+res
        counter += 1
    for i in range(len(res)):
        BLOCK[(i+_)%16]=res[i]
    BLOCK=[int.to_bytes(i,4,'little').hex() for i in BLOCK]
    ctr=ctr+1
print(ctr)

Last, reverse the algorithm

  • create res variable

  • recover block one by one, because there are 3 blocks unmodified

block[12] = rol(block[15]) ^ rol(block[14]) ^ rol(block[13]) ^ res[0]
block[11] = rol(block[14]) ^ rol(block[13]) ^ rol(block[12]) ^ res[1]
block[10] = rol(block[13]) ^ rol(block[12]) ^ rol(block[11]) ^ res[2]
  • brute the original value for multiplication (because the result was wrapped to 32 bit)

  • unmap the value

  • do 8 times

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

void find_x(uint32_t y, uint32_t multiplier) {
    for (uint32_t x = 0; ; x++) {
        if (x * multiplier == y) {
            printf("%08x\n", x);
        }

        if (x == UINT32_MAX) {
            break;
        }
    }
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <result_y>\n", argv[0]);
        return 1;
    }

    uint32_t y = strtoul(argv[1], NULL, 0);
    uint32_t multiplier = 0x4e6a44b9;

    find_x(y, multiplier);

    return 0;
}
import os

def print_hex(arr):
    tmp = []
    for i in arr:
        tmp.append(hex(i))
    print(tmp)

def get_val(a1):
    return os.popen(f"./a.out {a1}").read().strip()

def unmm(a1):
    test = a1
    outt = []

    for i in range(len(test)):
        tmp_out = ''
        test[i] = test[i].zfill(8)
        for j in range(len(test[i])):
            tmp_out += rev_mapper[test[i][j].lower()]
        print(tmp_out)
        tmp_out = hex(int.from_bytes(bytes.fromhex(tmp_out), "little"))[2:]
        outt.append(tmp_out)
    return outt

w='0123456789abcdef'
x='3e1a49568bf2dc07'
rev_mapper={}
mapper={}
mul=0x4e6a44b9
for i in range(16):
    rev_mapper[x[i]]=w[i]
    mapper[w[i]]=x[i]

def mpp(byt):
    return int.from_bytes(bytes.fromhex(''.join([mapper[c] for c in byt])),"little")

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))


BLOCK  = ['13307911','9becd288','ddcc0c2c','69f8ae97','607cbc34','d7826cf8','d95b7b92','8c9a68d5','1a151f45','4a9c380f','afd5c1d2','f41c8482','5a7718bd','4dfe8abe','52c60419','a4a2e9b7']

ctr = 19
import tqdm

for _ in range(ctr - 12, -1, -1):
    print(ctr)
    
    for i in range(len(BLOCK)):
        BLOCK[i] = int.from_bytes(bytes.fromhex(BLOCK[i].zfill(8)),  "little")
    
    res = [0 for _ in range(13)]
    
    for i in range(len(res)):
        res[i] = BLOCK[(i+_)%16]
    
    
    for i in range(13):
        BLOCK[(ctr-i) % 16] = ROL(BLOCK[(ctr + 3 - i) % 16],29,32)^ROL(BLOCK[(ctr + 2 - i) % 16],17,32) ^ ROL(BLOCK[(ctr + 1 - i) % 16],7,32) ^ res[12-i]
    
    for i in tqdm.tqdm(range(len(BLOCK))):
        BLOCK[i] = get_val(BLOCK[i])
    
    BLOCK = unmm(BLOCK)
    print(BLOCK)
    ctr -= 1

print(BLOCK)
# flaggg.py
a = ['53454343', '4f4e7b66', '556e4374', '31306e34', '6c5f7052', '6f477234', '6d4d316e', '365f3173', '5f705234', '63376943', '346c4c79', '5f615f70', '5572335f', '30626675', '35633454', '316f4e7d']
flag = b""
for i in a:
	flag += bytes.fromhex(i)
print(flag) 

Flag: SECCON{fUnCt10n4l_pRoGr4mM1n6_1s_pR4c7iC4lLy_a_pUr3_0bfu5c4T1oN}

Jump

Description

Solution

Given ELF file with architecture aarch64. Open it using IDA.

Looking at start function, we will see sub_4004f4 which will act as main function. Going deeper. we will found the argument length check and if it is not 2 it will printout incorrect string.

So basically this program is a flag checker, through analyzing each function we found interesting function that do validation.

All of the functions has the same pattern which is it uses check_val (global variable) to store the result. So we can get all of the check function by looking at its xref.

from idaapi import *
import idautils
import idc

def print_hex(a1):
    tmp = []
    for i in a1:
        tmp.append(hex(i))
    print(tmp)

address = 0x412030
list_addr = []
for i in idautils.XrefsTo(address):
    f = ida_funcs.get_func(i.frm)
    if f.start_ea not in list_addr:
        list_addr.append(f.start_ea)

print_hex(list_addr)

Below is the output

['0x400648', '0x4006ac', '0x400710', '0x400774', '0x4007fc', '0x400884', '0x40090c', '0x400964', '0x400c48']

Set breakpoint on all logic/arithmetic operation on that function except the last one (0x400c48). Then trigger the input, check the input validated, and bypass the validation.

#!/usr/bin/python3

import string

class SolverEquation(gdb.Command):
    def __init__ (self):
        super (SolverEquation, self).__init__ ("solve-equation",gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        
        gdb.execute("del")
        
        list_bp = ["0x4006D0","0x40066C","0x400734","0x4007BC","0x0400844","0x4008CC","0x400930","0x4009AC"]

        for i in list_bp:
            gdb.execute(f"b *{hex(int(i,16))}")
        
        gdb.execute("run SECCON{0123456789abcdefghijklmnopqrstuv}")

        list_ins = []
        list_val = []
        list_cmp = []
        
        arch = gdb.selected_frame().architecture()
        
        for i in range(3):
            print(i)
            current_pc = addr2num(gdb.selected_frame().read_register("pc"))
            disa = arch.disassemble(current_pc)[0]
            list_ins.append(f"{hex(disa['addr'])} : {disa['asm']} ")

            w8 = addr2num(gdb.selected_frame().read_register("w8"))
            w9 = addr2num(gdb.selected_frame().read_register("w9"))
            list_val.append([w8, w9])

            for _ in range(100):
                current_pc = addr2num(gdb.selected_frame().read_register("pc"))
                disa = arch.disassemble(current_pc)[0]
                if "cset" in disa["asm"]:
                    w9 = addr2num(gdb.selected_frame().read_register("w9"))
                    list_cmp.append(w9)
                    gdb.execute("si")
                    gdb.execute("set $w8 = 0x1")
                    gdb.execute("c")
                    break
                else:
                    gdb.execute("si")

        print(list_val)
        print(list_cmp)
        


def parse(f):
    f = f.split("\n")
    result = []
    for i in f:
        tmp = i.split("\t")
        for j in range(1,len(tmp)):
            result.append(int(tmp[j],16))
    return result

def addr2num(addr):
    try:
        return int(addr)
    except:
        return long(addr)

SolverEquation()

From trial and error we found that there are only 3 breakpoints hitted.

Each value compared it also from our input (except the validation that use 1 constant and 1 variable)

from Crypto.Util.number import *

list_val = [[875770417, 3405691582], [1667391801, 943142453], [1802135912, 1734763876]]
list_cmp = [4187328214, 2496897492, 2644337301]

for i in list_val:
	print(long_to_bytes(i[0])[::-1], long_to_bytes(i[1])[::-1])

Now we know the pattern used, which is block[i] block[i-1] (which is same like in the code). But the problem is we dont know what is the value of i used on each function. So i decided to map each function manually.

address  valid_input block
0x400930 SECC        -> 0
0x400734 ON{5        -> 1 
0x40066C h4k3        -> 2
0x4006D0 _1t_        -> 3
0x400844 ????????    -> 4 & 3
???????? ????????    -> 5 & 4
0x4007BC ????????    -> 6 & 5
???????? ????????    -> 7 & 6

So for validation that use one block, we can get the valid input by reversing the algorithm. But for the two blocks validation we cannot directly get the valid input but we can use the previous known input and do a little bruteforce recursively.

from Crypto.Util.number import *
from itertools import product
import string

def rec(known, arr, opr, leaked, prev):
	if len(leaked) == 16:
		print(flag + leaked)
		return
	
	for i in range(len(arr)):
		for val in arr[i]:
			for j in opr[i]:
				if "+" == j:
					tmp = long_to_bytes((val + known) & 0xffffffff)[::-1]
					if all(c in list_char for c in tmp):
						if tmp != prev:
							leaked += tmp
							known2 = bytes_to_long(tmp[::-1])
							rec(known2, arr[1:], opr[1:], leaked, long_to_bytes(known)[::-1])
				else:
					tmp = long_to_bytes((val - known) & 0xffffffff)[::-1]
					if all(c in list_char for c in tmp):
						if tmp != prev:
							leaked += tmp
							known2 = bytes_to_long(tmp[::-1])
							rec(known2, arr[1:], opr[1:], leaked, long_to_bytes(known)[::-1])


list_char = list(string.printable.encode())

flag = b"SECC"
flag += long_to_bytes(0xDEADBEEF ^ 0xEBD6F0A0)[::-1]
flag += long_to_bytes(0xCAFEBABE ^ 0xF9958ED6)[::-1]
flag += long_to_bytes(0xC0FFEE ^ 0x5FB4CEB1)[::-1]

arr = [[0x94D3A1D4], [0x9D9D6295, 0x47CB363B], [0x9D949DDD], [0x9D9D6295, 0x47CB363B]]
opr = [["+", "-"] for _ in range(4)]

known = bytes_to_long(flag[-4:][::-1])
leaked = b""

rec(known, arr, opr, leaked, b"")
	
image

Flag: SECCON{5h4k3_1t_up_5h-5h-5h5hk3}

Reaction

Description

-

Solution

Given ELF, open using IDA. Look at main function.

Code above basically do 3 things

  • Generate seed using flag and use Mersenne Twister to generate random

  • Call Environment::update which consist almost all of the algorithm in the code

  • Validate flag_counter (which processed in Environment::update), if flag_counter > 13 it will produce flag

So lets take a look on Environment::update

Our input received at Environment::set, each input consist of 2 bytes value.

From code above we can see that the second byte can be only 0,1,2, or 3. And the first value is limited to < 0xe (which is the same size like maximum index of array initialized in main function). After having this information we tried to debug it and take a look on each data processed.

===================
inp[1] == 0
inp[0] < 0xe

loop 2 times
*(0x55555556d930 + inp[0]*4) != 0 return 0
*(0x55555556d8f0 + inp[0]*4) != 0 return 0

===================
inp[1] == 1
inp[0] < 0xe

loop 2 times
*(0x55555556d930 + inp[0] * 4) != 0 return 0
*(0x55555556d930 + (inp[0] + 1) * 4) != 0 return 0

===================
inp[1] == 2
inp[0] < 0xe

loop 2 times
*(0x55555556d8f0 + inp[0]*4) != 0 return 0
*(0x55555556d930 + inp[0]*4) != 0 return 0

===================
inp[1] == 3
inp[0] < 0xe

loop 2 times
*(0x55555556d930 + (inp[0] + 1) * 4) != 0 return 0
*(0x55555556d930 + inp[0] * 4) != 0 return 0

At first i though it was a maze direction, so i tried to findout where is the random generated. The generated random actually in the same function which is Environment::set, below is the code to generate the random number.

After found it, i convert the whole random number generation process.

# coefficients for MT19937
(w, n, m, r) = (32, 624, 397, 31)
a = 0x9908B0DF
(u, d) = (11, 0xFFFFFFFF)
(s, b) = (7, 0x9D2C5680)
(t, c) = (15, 0xEFC60000)
l = 18
f = 1812433253


# make a arry to store the state of the generator
MT = [0 for i in range(n)]
index = n+1
lower_mask = 0x7FFFFFFF #(1 << r) - 1 // That is, the binary number of r 1s
upper_mask = 0x80000000 #lowest w bits of (not lower_mask)


# initialize the generator from a seed
def mt_seed(seed):
    # global index
    # index = n
    MT[0] = seed
    for i in range(1, n):
        temp = f * (MT[i-1] ^ (MT[i-1] >> (w-2))) + i
        MT[i] = temp & 0xffffffff


# Extract a tempered value based on MT[index]
# calling twist() every n numbers
def extract_number():
    global index
    if index >= n:
        twist()
        index = 0

    y = MT[index]
    y = y ^ ((y >> u) & d)
    y = y ^ ((y << s) & b)
    y = y ^ ((y << t) & c)
    y = y ^ (y >> l)

    index += 1
    return y & 0xffffffff


# Generate the next n values from the series x_i
def twist():
    for i in range(0, n):
        x = (MT[i] & upper_mask) + (MT[(i+1) % n] & lower_mask)
        xA = x >> 1
        if (x % 2) != 0:
            xA = xA ^ a
        MT[i] = MT[(i + m) % n] ^ xA


WRAP_32 = 2**32 - 1
flag = b"SECCON{abcdefghijklmnopqrs}"
v6 = 1
for i in range(len(flag)):
    v6 *= flag[i]
v6 &= WRAP_32
mt_seed(v6)

list_blocks = []

inp = b"abcdefgh"
for i in range(99):
    lol = extract_number()
    block = []
    for _ in range(2):
        v4 = extract_number() & 3
        if v4 == 2:
            v5 = 3
        else:
            v5 = 1 - ((1 if v4 == 0 else 0) - 1) # 1 or 2
            if v4 >= 3:
                v5 = 4
        block.append(v5)
    list_blocks.append(block)
print(list_blocks)

Output:

[[1, 4], [4, 1], [2, 1], [4, 4], [1, 1], [2, 2], [3, 1], [3, 4], [1, 1], [1, 3], [3, 1], [4, 3], [4, 3], [2, 2], [3, 3], [1, 4], [3, 3], [3, 4], [2, 3], [3, 3], [3, 1], [1, 1], [4, 2], [2, 1], [4, 2], [1, 4], [2, 2], [4, 1], [3, 3], [1, 3], [4, 1], [3, 1], [3, 1], [2, 1], [1, 2], [4, 1], [4, 1], [1, 3], [4, 3], [2, 4], [2, 1], [2, 1], [1, 2], [3, 4], [4, 4], [4, 1], [2, 1], [4, 2], [4, 2], [2, 2], [2, 4], [1, 3], [3, 1], [2, 1], [1, 2], [4, 1], [1, 2], [1, 2], [2, 4], [2, 3], [2, 3], [3, 2], [1, 2], [2, 2], [1, 1], [1, 3], [4, 3], [1, 4], [1, 2], [1, 1], [2, 3], [2, 2], [3, 3], [3, 3], [1, 3], [1, 1], [3, 3], [4, 1], [4, 2], [1, 1], [4, 2], [4, 1], [4, 2], [4, 1], [3, 4], [1, 3], [4, 2], [1, 1], [1, 4], [4, 1], [2, 2], [3, 4], [1, 3], [1, 2], [3, 3], [2, 1], [1, 4], [3, 4], [2, 1]]

The output doesnt look like a value in maze. So i continue to analyze the algorithm that will process above values. Our objective is to findout a way to make the flag_counter > 13 and i found that Environment::react will affect the flag_counter.

From analysis we found that Environment::react process information based on column and row. The total of column and row is 14x14 and it is the same address like we got from the Environment::set. So in this step we tried to dump the table to know the mapping for our input.

#!/usr/bin/python3

def write_payload(data):
    f = open("payload", "wb")
    f.write(data)
    f.close()

def print_hex(arr):
    tmp = []
    for i in arr:
        tmp.append(hex(i))
    print(tmp)



class SolverEquation(gdb.Command):
    def __init__ (self):
        super (SolverEquation, self).__init__ ("solve-equation",gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        payload =  b"\x00\x00"
        payload += b"\x00\x03"

        write_payload(payload)

        known_addr = ['0x55555556d930', '0x55555556d8f0', '0x55555556d8b0', '0x55555556d870', '0x55555556d830', '0x55555556d7f0', '0x55555556d7b0', '0x55555556d770', '0x55555556d730', '0x55555556d6f0', '0x55555556d6b0', '0x55555556d670', '0x55555556d630', '0x55555556d5f0']

        gdb.execute("del")
        gdb.execute("b *0x555555557B81") # envset
        gdb.execute("r < payload")
        all_mapp = []
        
        block =  [[1, 4], [4, 1], [2, 1], [4, 4], [1, 1], [2, 2], [3, 1], [3, 4], [1, 1], [1, 3], [3, 1], [4, 3], [4, 3], [2, 2], [3, 3], [1, 4], [3, 3], [3, 4], [2, 3], [3, 3], [3, 1], [1, 1], [4, 2], [2, 1], [4, 2], [1, 4], [2, 2], [4, 1], [3, 3], [1, 3], [4, 1], [3, 1], [3, 1], [2, 1], [1, 2], [4, 1], [4, 1], [1, 3], [4, 3], [2, 4], [2, 1], [2, 1], [1, 2], [3, 4], [4, 4], [4, 1], [2, 1], [4, 2], [4, 2], [2, 2], [2, 4], [1, 3], [3, 1], [2, 1], [1, 2], [4, 1], [1, 2], [1, 2], [2, 4], [2, 3], [2, 3], [3, 2], [1, 2], [2, 2], [1, 1], [1, 3], [4, 3], [1, 4], [1, 2], [1, 1], [2, 3], [2, 2], [3, 3], [3, 3], [1, 3], [1, 1], [3, 3], [4, 1], [4, 2], [1, 1], [4, 2], [4, 1], [4, 2], [4, 1], [3, 4], [1, 3], [4, 2], [1, 1], [1, 4], [4, 1], [2, 2], [3, 4], [1, 3], [1, 2], [3, 3], [2, 1], [1, 4], [3, 4], [2, 1]]
        
        for j in range((len(payload) // 2) + 1):
            mapp = []
            for i in known_addr:
                mapp.append(parse(gdb.execute(f"x/14wx {i}", to_string=True)))
            all_mapp.append(mapp)
            gdb.execute("c")
        all_mapp = all_mapp[1:]

        for i in range(len(all_mapp)):
            
            col = payload[2*i]
            direct = payload[2*i + 1]
            if direct == 0:
                block_print = [[block[i][0]], [block[i][1]]]
            elif direct == 1:
                block_print = [block[i][0], block[i][1]]
            elif direct == 2:
                block_print = [[block[i][1]], [block[i][0]]]
            else:
                block_print = [block[i][1], block[i][0]]

            for j in all_mapp[i]:
                print(j)
            print()


def parse(f):
    f = f.split("\n")
    result = []
    for i in f:
        tmp = i.split("\t")
        for j in range(1,len(tmp)):
            result.append(int(tmp[j],16))
    return result


def addr2num(addr):
    try:
        return int(addr)  # Python 3
    except:
        return long(addr) # Python 2

SolverEquation()
image

I dont know what kind of puzzle is this, so i just shared it to the discord and my teammate found that it was a puyo puyo. So the next step i did is trying to find out how to get the block and then i realize that the block was leaked from the response. To get the block i just send the most possible combinations that can be made if there is no block explodes.

from pwn import *

r = remote("reaction.seccon.games", 5000)

for i in range(8):
	for j in range(0xe):
		r.send(bytes([i * 2]) + b"\x01")
tmp = r.recvuntil(b"Wrong...")

tmp = tmp.strip(b"Wrong...")

block = []
for i in range(0, len(tmp), 2):
	block.append(list(tmp[i:i+2]))

print(block)

After that my teammate tried to solve it manually, he has experience with this kind of puzzle/game.

To solve, I (zafirr) played puyo puyo manually on google sheets 😁 and recorded the moves. This is the state of the board before the last move. It results in a 15 chain

During the solving process, after he found the solution there is a little step missing, so i decide to do a modification on my gdb script to help him to find out the actual map of the block. To do that, basically i change the value written to the map with the value from the server. Below is the updated gdb script.

#!/usr/bin/python3

def write_payload(data):
    f = open("payload", "wb")
    f.write(data)
    f.close()

def print_hex(arr):
    tmp = []
    for i in arr:
        tmp.append(hex(i))
    print(tmp)



class SolverEquation(gdb.Command):
    def __init__ (self):
        super (SolverEquation, self).__init__ ("solve-equation",gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        payload = b'\x00\x00'
        payload += b'\x00\x02'
        write_payload(payload)

        known_addr = ['0x55555556d930', '0x55555556d8f0', '0x55555556d8b0', '0x55555556d870', '0x55555556d830', '0x55555556d7f0', '0x55555556d7b0', '0x55555556d770', '0x55555556d730', '0x55555556d6f0', '0x55555556d6b0', '0x55555556d670', '0x55555556d630', '0x55555556d5f0']

        gdb.execute("del")
        gdb.execute("b *0x5555555569EF") # map
        gdb.execute("b *0x555555557B81") # envset
        gdb.execute("r < payload")
        all_mapp = []
        
        block =  [[0, 0], [4, 4], [3, 2], [1, 3], [2, 4], [1, 4], [3, 3], [1, 4], [4, 4], [4, 4], [4, 3], [4, 3], [3, 1], [1, 3], [1, 2], [3, 3], [3, 2], [2, 2], [4, 4], [1, 1], [4, 3], [3, 2], [3, 2], [3, 2], [2, 4], [4, 3], [1, 2], [2, 3], [1, 1], [4, 3], [1, 2], [2, 2], [1, 2], [2, 3], [1, 1], [4, 4], [4, 3], [3, 1], [1, 3], [4, 4], [4, 3], [4, 1], [1, 2], [1, 1], [3, 2], [4, 2], [1, 2], [1, 3], [3, 2], [3, 1], [1, 2], [4, 2], [4, 4], [4, 2], [1, 2], [4, 1], [4, 2], [1, 2], [4, 3], [4, 2], [2, 2], [3, 2], [2, 1], [1, 4], [1, 1], [3, 1], [3, 3], [2, 2], [1, 4], [2, 4], [3, 1], [4, 1], [2, 2], [3, 4], [1, 1], [4, 4], [4, 2], [3, 2], [1, 3], [3, 1], [4, 3], [2, 2], [4, 4], [3, 1], [2, 3], [4, 2], [1, 1], [4, 3], [4, 3], [1, 2], [3, 2], [2, 3], [3, 2], [3, 1], [3, 4], [2, 3], [2, 1], [2, 2], [4, 1], [1, 3]]
        arr = [[4, 4], [3, 2], [1, 3], [2, 4], [1, 4], [3, 3], [1, 4], [4, 4], [4, 4], [4, 3], [4, 3], [3, 1], [1, 3], [1, 2], [3, 3], [3, 2], [2, 2], [4, 4], [1, 1], [4, 3], [3, 2], [3, 2], [3, 2], [2, 4], [4, 3], [1, 2], [2, 3], [1, 1], [4, 3], [1, 2], [2, 2], [1, 2], [2, 3], [1, 1], [4, 4], [4, 3], [3, 1], [1, 3], [4, 4], [4, 3], [4, 1], [1, 2], [1, 1], [3, 2], [4, 2], [1, 2], [1, 3], [3, 2], [3, 1], [1, 2], [4, 2], [4, 4], [4, 2], [1, 2], [4, 1], [4, 2], [1, 2], [4, 3], [4, 2], [2, 2], [3, 2], [2, 1], [1, 4], [1, 1], [3, 1], [3, 3], [2, 2], [1, 4], [2, 4], [3, 1], [4, 1], [2, 2], [3, 4], [1, 1], [4, 4], [4, 2], [3, 2], [1, 3], [3, 1], [4, 3], [2, 2], [4, 4], [3, 1], [2, 3], [4, 2], [1, 1], [4, 3], [4, 3], [1, 2], [3, 2], [2, 3], [3, 2], [3, 1], [3, 4], [2, 3], [2, 1], [2, 2], [4, 1], [1, 3]]

        for j in range((len(payload) // 2) + 1):
            mapp = []
            for i in known_addr:
                mapp.append(parse(gdb.execute(f"x/14wx {i}", to_string=True)))
            all_mapp.append(mapp)
            gdb.execute("c")
            gdb.execute(f"set $edx={arr[j][0]}") # block[0]
            gdb.execute("c")
            gdb.execute(f"set $edx={arr[j][1]}") # block[1]
            gdb.execute("c")
        
        for i in range(len(all_mapp)):
            print(f"{i} - Curr : {block[i]}, Next: {block[i+1]}")
            print()
            print([i for i in range(14)])
            for j in all_mapp[i]:
                print(j)
            print()


def parse(f):
    f = f.split("\n")
    result = []
    for i in f:
        tmp = i.split("\t")
        for j in range(1,len(tmp)):
            result.append(int(tmp[j],16))
    return result


def addr2num(addr):
    try:
        return int(addr)  # Python 3
    except:
        return long(addr) # Python 2

SolverEquation()

Below is the example of the output to debug the puyo puyo

Last step, just send the moves and solved:

from pwn import *

# context.log_level = 'debug'
r = remote("reaction.seccon.games", 5000)

moves = ['4,4 1', '2,3 2', '1,3 3', '4.2 1', '4,1 1', '3,3 1', '4,1 2', '4,4 3', '4,4 14', '4,3 3', '3.4 4', '1.3 2', '1.3 2', '2,1 4', '3,3 4', '3,2 4', '2,2 4', '4.4 3', '1,1 14', '3,4 3', '3,2 4', '3.2 5', '3.2 5', '4.2 5', '4.3 5', '1.2 5', '2.3 5', '1,1 5', '4,3 5', '1.2 7', '2,2 13', '1.2 7', '2,3 7', '1,1 8', '4,4 5', '3,4 7', '3.1 9', '3.1 9', '4,4 11', '3.4 10', '4,1 11', '2,1 12', '1,1 11', '2.3 9', '4,2 9', '2,1 12', '3,1 1', '2,3 1', '3,1 1', '2,1 14', '2,4 12', '4,4 14', '2,4 14', '2,1 13']
puyoes = [[4, 4], [3, 2], [1, 3], [2, 4], [1, 4], [3, 3], [1, 4], [4, 4], [4, 4], [4, 3], [4, 3], [3, 1], [1, 3], [1, 2], [3, 3], [3, 2], [2, 2], [4, 4], [1, 1], [4, 3], [3, 2], [3, 2], [3, 2], [2, 4], [4, 3], [1, 2], [2, 3], [1, 1], [4, 3], [1, 2], [2, 2], [1, 2], [2, 3], [1, 1], [4, 4], [4, 3], [3, 1], [1, 3], [4, 4], [4, 3], [4, 1], [1, 2], [1, 1], [3, 2], [4, 2], [1, 2], [1, 3], [3, 2], [3, 1], [1, 2], [4, 2], [4, 4], [4, 2], [1, 2], [4, 1], [4, 2], [1, 2], [4, 3], [4, 2], [2, 2], [3, 2], [2, 1], [1, 4], [1, 1], [3, 1], [3, 3], [2, 2], [1, 4], [2, 4], [3, 1], [4, 1], [2, 2], [3, 4], [1, 1], [4, 4], [4, 2], [3, 2], [1, 3], [3, 1], [4, 3], [2, 2], [4, 4], [3, 1], [2, 3], [4, 2], [1, 1], [4, 3], [4, 3], [1, 2], [3, 2], [2, 3], [3, 2], [3, 1], [3, 4], [2, 3], [2, 1], [2, 2], [4, 1], [1, 3]]

f = open("payload", "wb")
for i in range(len(moves)):
    move = moves[i]
    orientation, column = move.split(" ")
    puyo = puyoes[i]
    print(move, puyo)

    if orientation[1] == ',':
        a,b = list(map(int, orientation.split(',')))
        if(a == puyo[0]):
            direction = 0
        else:
            direction = 2
    if orientation[1] == '.':
        a,b = list(map(int, orientation.split('.')))
        if(a == puyo[0]):
            direction = 1
        else:
            direction = 3
    payload = bytes([int(column)-1]) + bytes([direction])
    f.write(payload)
    print(payload)
    r.send(payload)
    sleep(0.1)

FLAG: SECCON{puyoyo_mo_yoyo_mo_yoyo_no_uchi}

Last updated