Write Up Misc — HTB Apocalypse 2023 — (Janken, Calibrator)

Maulvi Alfansuri
8 min readMar 26, 2023

Introduction

A few weeks ago, I played CTF HTB Apocalypse 2023. The CTF ran for 5 days, and the challenges provided totaled 74, which was quite a lot.

In this CTF, I participated in several categories such as misc, reversing, and machine learning. I plan to write articles for each category, starting with this one, misc category.

Table of Content

· Introduction
· Table of Content
· Misc — Janken
· Misc — Calibrator
· Epilogue

Misc — Janken

Challenges points and decryption

The first misc challenge I solved was called Janken. It involved a socket connection and some provided files.

Here are the files provided to the user:

$ ls -lah
total 20K
drwxrwxrwx 1 alfan alfan 4.0K Mar 26 16:20 .
drwxrwxrwx 1 alfan alfan 4.0K Mar 26 16:20 ..
drwxrwxrwx 1 alfan alfan 4.0K Mar 13 21:27 .glibc
-rwxrwxrwx 1 alfan alfan 18K Mar 13 21:27 janken
$ file janken
janken: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter ./.glibc/ld-linux-x86-64.so.2, BuildID[sha1]=56b54cdae265aa352fe2ebb016f86af831fd58d3, for GNU/Linux 3.2.0, not stripped

When you try to run the game, it presents a simple janken game that requires the user to win in order to proceed to the next round.

Challenge game show janken simulation

Looking at the source code below in the game function, we can see that the program calls strstr on our input using the needle array.

game function code

The strstr function has two arguments: the first is our input, and the second is the needle pointer. The strstr function returns a non-zero value if there are any substrings. To bypass this restriction, we should be able to input “rockscissorpaper” and win the round.

input rockscissorspaper directly win the round

To automate this, I used pwntools and created a simple script. After running the script, the flag was acquired from the server.

from pwn import *
from sys import *

p = connect("178.62.64.13", 32538)

# p.recvuntil(">> ")
p.sendline("1")

for i in range(99):
print(i)
p.recvuntil(">> ")
p.sendline("rockscissorspaper")

p.interactive()
Flag acquired

Misc — Calibrator

The next misc challenge I solved was called Calibrator.

Challenge description and diffulty

Like the previous challenge, it involved a socket connection and some provided files.

Upon examining the files, there is a Python script that runs on the server, along with well-written documentation. Below is a screenshot of the game rules from the documentation:

Rule 1 that explain the behaviour
Rule 2: Visualitation of the game
Rule 3: Contraints and notes

The goal of this problem is to locate the center of a circle (radar) using the responses given. “DETECTED” means our coordinate guess is inside the circle, “UNDETECTED” means our coordinate is outside the circle, and if our guess is right on the center, the program returns “REFERENCE.”

The circle is large, so a naive method would not work, and we are only allowed 300 attempts at guessing. Another difficulty not mentioned in the documentation, but only in the code, is that we can only input integer values, which introduces floating-point errors if we use an algorithm.

Below is the server code:

Constrains from code
Server code show our input would converted to integer

So, how can we create an algorithm to solve this?

After trying and modifying several algorithms, I finally completed the challenge using binary search.

Binary search can be used in this challenge to find the circumference coordinate of the circle. For example, if we pick a random coordinate inside the circle (a number below the HI constraint circle) and perform a binary search on one side of the coordinate (e.g., binary search on Y), we should be able to get a coordinate close to the circumference (for example, if we choose X, A, the program returns “DETECTED,” and if we choose X, A+1, the program returns “UNDETECTED,” with A being the binary search calculation on coordinate Y).

With this approach, if we collect many circumference coordinates and spread them out, we can minimize the error. In the program I created, I performed 8 circumference coordinate searches.

After collecting all the circumference coordinates, we should be able to calculate the center of the circle if we have 3 or more points. Having more points would decrease center calculation errors.

Even after calculating the center, the program still has a chance of failing to detect the center. The remaining attempts can be used to guess the coordinates around our guess, hoping it will point to the center.

Below is my program to solve this case:

from pwn import *
import math

# while true ; do python3 final-final.py ; done
# while true ; do python3 solve.py ; done


ITERATIONS = 47
SIDE_LENGTH = 2 * 10 ** 9
ATTEMPTS = 300
HI = SIDE_LENGTH // 2
LO = -SIDE_LENGTH // 2
e = 2

# Connect to the challenge server
conn = remote('165.232.98.11', 30970)
# conn = process('python3 src/server.py'.split())

# print(log)
# Define binary search function
z = 0
global before

def binary_search(lo, hi):
# before = (lo, hi)
while lo <= hi:
mid = (lo + hi) // 2
conn.sendline(f"{mid} {mid}")
response = conn.recvline().decode().strip()
global z
print(z, lo, hi, mid, mid, repr(response))
z += 1
if mid == lo and mid == hi:
return (mid, mid)
elif "UNDETECTED" in response:
x, y = binary_search(lo, mid - 1)
if x is not None and y is not None:
return (x, y)
x, y = binary_search(mid - 1, mid - 1)
if x is not None and y is not None:
return (x, y)

elif "DETECTED" in response:
x, y = binary_search(mid + 1, hi)
if x is not None and y is not None:
return (x, y)
x, y = binary_search(mid + 1, mid + 1)
if x is not None and y is not None:
return (x, y)

return (None, None)


def binary_search_x(x_lo, x_hi, y, r):
while x_lo <= x_hi:
x_mid = (x_lo + x_hi) // 2
conn.sendline(f"{x_mid} {y}")
response = conn.recvline().decode().strip()
global z
print(z, x_mid, y, "|", x_mid, x_lo, x_hi, repr(response))
z += 1

if x_mid == x_hi and x_mid == x_lo :
return x_mid

elif "UNDETECTED" in response:
x = binary_search_x(x_lo, x_mid - 1, y, r)
if x is not None:
return x
x = binary_search_x(x_mid - 1, x_mid - 1, y, r)
if x is not None:
return x
elif "DETECTED" in response:
x = binary_search_x(x_mid + 1, x_hi, y, r)
if x is not None:
return x
x = binary_search_x(x_mid + 1, x_mid + 1, y, r)
if x is not None:
return x

return None

def binary_search_y(y_lo, y_hi, x, r):
while y_lo <= y_hi:
y_mid = (y_lo + y_hi) // 2
conn.sendline(f"{x} {y_mid}")
response = conn.recvline().decode().strip()

global z
print(z, x, y_mid, "|", y_mid, y_lo, y_hi, repr(response))
z += 1

if y_mid == y_hi and y_mid == y_lo :
return y_mid
elif "UNDETECTED" in response:
y = binary_search_y(y_mid + 1, y_hi, x, r)
if y is not None:
return y
y = binary_search_y(y_mid + 1, y_mid + 1, x, r)
if y is not None:
return y
elif "DETECTED" in response:
y = binary_search_y(y_lo, y_mid - 1, x, r)
if y is not None:
return y
y = binary_search_y(y_mid - 1, y_mid - 1, x, r)
if y is not None:
return y
return None

# Loop through iterations
for i in range(47):
z = 0
logx = conn.recvuntil("> ")

# Generate random circle parameters
R = random.randint(SIDE_LENGTH // 4, SIDE_LENGTH // 2)
X = random.randint(LO + R, HI - R)
Y = random.randint(LO + R, HI - R)

# Perform binary search

points = []


x, y = binary_search(LO, HI)
points.append((x, y))

# z = 0
MET = SIDE_LENGTH // 1000
x_mid = binary_search_x(LO, HI, MET, R)
# print(x_mid, MET)
points.append((x_mid, MET))

# z = 0
MET = -SIDE_LENGTH // 1000
x_mid = binary_search_x(LO, HI, MET, R)
# print(x_mid, MET)
points.append((x_mid, MET))


# z = 0
MET = SIDE_LENGTH // 100
x_mid = binary_search_x(LO, HI, MET, R)
# print(x_mid, MET)
points.append((x_mid, MET))

# z = 0
MET = -SIDE_LENGTH // 100

x_mid = binary_search_x(LO, HI, MET, R)
# print(x_mid, MET)
points.append((x_mid, MET))



# z = 0
MET = SIDE_LENGTH // 1000

y_mid = binary_search_y(LO, HI, MET, R)
# print(MET, y_mid)
points.append((MET, y_mid))

# z = 0
MET = -SIDE_LENGTH // 1000

y_mid = binary_search_y(LO, HI, MET, R)
# print(MET, y_mid)
points.append((MET, y_mid))

# z = 0
MET = SIDE_LENGTH // 100

y_mid = binary_search_y(LO, HI, MET, R)
# print(MET, y_mid)
points.append((MET, y_mid))

# z = 0
MET = -SIDE_LENGTH // 100

y_mid = binary_search_y(LO, HI, MET, R)
# print(MET, y_mid)
points.append((MET, y_mid))


# Input a list of points on the circle circumference
import numpy as np
# Create a matrix A and a vector b for the least squares circle fit equation
A = np.array([[2 * p[0], 2 * p[1], 1] for p in points])
b = np.array([-p[0] ** 2 - p[1] ** 2 for p in points])

# Solve the least squares circle fit equation to find the center of the circle
x, y, r = np.linalg.lstsq(A, b, rcond=None)[0]
center_x, center_y = int(-x), int(-y)

# Print the x and y coordinates of the center of the circle
print("The center of the circle is ({}, {}) {}".format(center_x, center_y, z))

if(b"DEBUGGGGG" in logx):
print(logx.split(b"DEBUGGGGG")[1].split(b"DEBUGGGGG")[0])

# conn.interactive()
# Send solution to challenge server
conn.sendline(f"{center_x} {center_y}")
# conn.interactive()
sisa = 300 - z
# tengah = int(sisa ** -1)
import math
tengah = int(math.sqrt(sisa)/2)

print(tengah)
print(z + tengah * 4, sisa)

data = conn.recvline()
print("CHECK COOR")
print(data)

if(b"REFERENCE" not in data):
print("FIXING COODINATE", tengah)
for aa in range(-tengah, tengah):
for bb in range(-tengah, tengah):
conn.sendline(f"{center_x+aa} {center_y+bb}")
z += 1
data = conn.recvline()
print(z, f"{center_x+aa} {center_y+bb}", repr(data))
if(b"REFERENCE" in data):
break
if(b"REFERENCE" in data):
break
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(i)
print(data)

conn.interactive()

Run the program on the VPS using the loop below:

# while true ; do python3 final-final.py ; done

The program will display the flag after a few attempts.

Flag acquired

Epilogue

I enjoyed the calibrator challenge a lot, as there was a lot of optimization involved in modifying the code. After successfully completing and accomplishing all miscellaneous challenges, it became evident that AI assistance, such as ChatGPT, can be helpful in coding. However, it cannot solve everything. There are still optimizations required to address specific cases, and to achieve this, programmers need to understand the provided code in order to modify the outcome effectively.

--

--