BKSec Earlybird Writeups

dungeon-in-1983

The .zip Dream gave me containes 2 binaries. Brief inspection showed that their decompiled pseudocode is identical so I’m just going to analyze the deploy/prob.

The binary reads our input and validates it using FUN_00101407. Provide the correct spell 10 times to retrieve the flag.

Analysis of FUN_00101407

Diving into FUN_00101407. There is only one return line: return local_10 == param_2 when it is done iterating through param_1

Regarding local_10, i can see that

  • local_10 = local_10 + 1 if param_1[i] == 'A'
  • local_10 = local_10 << 1 if param_1[i] == 'B'

Solution

Since param_1 is our input, i got this for constructing the spell if given param_2 (or local_1b8):

for chr in f"{by:b}":
	if chr == '1': spell = spell + 'A'
	spell = spell + 'B'

Stepping back to main, local_1b8 receives bytes from local_1b0, which reads from /dev/urandom. There is FUN_0010138d prints the monster’s stats using all the bytes from local_1b8.

From that, I have this to retrieve the random byte local_1b8: local_1b8 = (hp << 0x30) | str | (agi << 8) | (vit << 0x0) | (int << 0x18) | (end << 0x20) | (dex << 0x28)

Finally, I wrote this script that did the job:

import re
from pwn import *

p = process("./prob")
#p would change

for i in range(10):
	p.recvuntil(b']:')
	
	monster = p.recvuntil(b'Cast your spell!: ').decode()
	in4 = [int(s) for s in re.findall(r'\d+', monster)]
	print(in4)
	
	by=0
	for j in [0,6,5,4,3,2,1]:
		by = by + in4[j]
		by = by << 8
	by = by >> 8
	
	spell = ""
	for chr in f"{by:b}":
		if chr == '1': spell = spell + 'A'
		spell = spell + 'B'
	spell = spell[:-1]
	
	print(spell)
	p.sendline(spell.encode())
p.interactive()

Flag

At the time of writing, I was having a problem with pwntools remote() so I’ll update the flag later.

photographer

Dream gave me a binary and an encoded flag.

At a glance, it seems that byte stream from the flag is parsed into local_448 (as this later be used in the check loop):

Loop Analysis

In the loops: local_480 is the index;

FUN_00102bb6 simply gets the pointer to the next byte

FUN_001024c2 is shift left, FUN_00102489 is shift right

*puVar7 = uVar3 updates the new value

With that in mind, i wrote the decoder, with r for the rand():

if i % 3 == 2:
	dec = (current_byte + 0x18) ^ r
elif i % 3 == 0:
	dec = ((current_byte >> 4 | current_byte << 4) - r) % 256
	dec = dec >> 1 | dec << 7
else:
	r = r % 8
	dec = current_byte << r | curr_by >> (8-r)

Full Script

Knowing that the program uses srand(0xbeef), we can replicate all the rand() in the binary, the full script is written as below:

import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.srand(0xbeef)

with open("flag.bmp.enc", 'rb') as data, open("flag.bmp", 'wb') as flag:
	i = 0
	while(True):
		cur = data.read(1)
		if (not cur): break
		current_byte = int.from_bytes(cur)
		r = libc.rand() % 256
		
		if i % 3 == 2:
			dec = (current_byte + 0x18) ^ r
		elif i % 3 == 0:
			dec = ((current_byte >> 4 | current_byte << 4) - r) % 256
			dec = dec >> 1 | dec << 7
		else:
			r = r % 8
			dec = current_byte << r | curr_by >> (8-r)
		
		flag.write(bytes([dec % 256]))
		i = i + 1

Flag

DH{d85a130283f58035eb327b56e37d2dd087156ba629687a5d52bd4af3c5bfa4f4}

similar

The program gives us a list of list, each containing 3 integers ranging from -100 to 100. After showing, it then sorts that list using FUN_001017c8. If we get the sorted order right, we get the flag.

Analysis of FUN_001017c8

This fuction calculates value from the list of 3 integers and compares them:

Let’s check the FUN_00106a3

While examining, there is this param_1 >> 0x20 appeared a little confusing. Suspecting that Ghidra might have analyzed it wrong, I take a look at the assembly:

By default int is 4 bytes so [RBP + local_48 + 0x4] = param_1[1].

Further inspection of the assembly in FUN_001017c8 revealed that the value loaded into FUN_00106a3 was actually param_1[2].

Implementation

Now we know how it sorts, implement it and we’re done:

from math import sqrt
from pwn import *
from functools import cmp_to_key

def cal(a): 
	return (1- (a[1] + a[2] + a[0])/ (sqrt(3) * sqrt(a[1]*a[1] + a[0]*a[0] + a[2]*a[2])))

def compar(a,b):
	x = cal(a)
	y = cal(b)
	if x<y: return -1
	elif x>y: return 1
	else: return 0

r = remote('host8.dreamhack.games', 15703)
data = r.recvuntil(b'Result?').decode()
arr = [list(map(int, line.split(':')[1].split())) + [int(line.split(':')[0])] for line in data.splitlines() if ':' in line and line.split(':')[0].strip().isdigit()]
sor = sorted(arr, key=cmp_to_key(compar))
payload = " ".join(str(a[3]) for a in sor)
r.sendline(payload.encode())
r.interactive()

Flag

DH{s1m1l4r_in_c0s1n3:2OL5p3IWpaiDXt3T0dPUkg==}

CrabME

I spotted flagchecker(), lVar5 serves as the index. The validation process begins when lVar5 == 64.

I was confused about that thing with uVar6, uVar7 below. After some time looking it up, it’s handling the reconstruction of 1, 2, 3 or 4-byte character.

Inspecting the Logic

I rewrite the check logic with x := uVar1 as below:

(((x >> 2 & 0b00100000 |  x >> 4 & 0b00000100 | (x << 3 & 0b10000000 |
   x >> 2 & 0b00000010 | (x >> 1 & 0b00000001 |  x << 3 & 0b00001000)
+ (x << 2 & 0b00010000) + (x << 1 & 0b01000000)) ^ 99) + 0x22 & 0xff

Turns out it shuffles the bits of x and do some simple operations.

Reversing

Reverse it and now we’re done…

enc = bytearray(b'\xae\x00\x00\x00\x6d\x00\x00\x00\x9b\x00\x00\x00\x92\x00\x00\x00\x13\x00\x00\x00\x2b\x00\x00\x00\xc6\x00\x00\x00\xc9\x00\x00\x00\xe5\x00\x00\x00\xfa\x00\x00\x00\x96\x00\x00\x00\x0b\x00\x00\x00\x64\x00\x00\x00\x31\x00\x00\x00\xb8\x00\x00\x00\x08\x00\x00\x00\xc8\x00\x00\x00\x48\x00\x00\x00\xd2\x00\x00\x00\x30\x00\x00\x00\x60\x00\x00\x00\x04\x00\x00\x00\xfa\x00\x00\x00\x7b\x00\x00\x00\x88\x00\x00\x00\xb0\x00\x00\x00\x2f\x00\x00\x00\x7c\x00\x00\x00\xb3\x00\x00\x00\xb3\x00\x00\x00\x58\x00\x00\x00\x61\x00\x00\x00')

def shufflelelele(b):
	crab_map = [1, 3, 6 , 0, 2, 7, 5, 4]
	return sum((((((b - 0x22) & 0xff) ^ 99) >> i) & 1) << crab_map[i] for i in range(8))

print(''.join(chr(shufflelelele(b)) for b in enc[::4]))

…or not.

Notice that in the main, our flag is 64 chr long, but there is only 32 given encoded bytes. The loop in main check if the input chr is [a-f] or [0-9]:

I try to convert the decoded bytes to hexes and it works:

print(''.join(f"{shufflelelele(b):02x}" for b in enc[::4]))

Flag

DH{fb810d0e1ca97a70909f4e1982e1f65272623ee367129f8d42f3e987bcbc6665}