CyKor, Korea University Hacking Club
DEFCON 33 CTF Write-Up Series #2: tinii (rev)
Written By Jimin Jeong
This challenge is a yet another flagchecker, but layout obfuscated. To analyze the binary, I had to deobfuscate it.
Deobfuscate
When I opened the binary with IDA, I could see two types of subroutines: the first type forms the backbone of the entire program, while the second consists of small code snippets. Below is a part of main()
which is the backbone of the binary.
Second type (small code snippet) looks like below.
The snippet subroutines consist of an add rsp, 8
instruction for stack frame cleanup, a single assembly instruction that constitutes part of the main binary routine, and a final jmp
instruction. They don’t ret
, but their jmp
targets are always caller. So, I could write a deobfuscator script to gathering core assembly instruction of the snippet subroutines and backbone subroutines.
However, I still had to examine the deobfuscated code in assembly. To address this, I implemented a deobfuscator that improves readability by labeling addresses related to jmp
instructions and adding comments showing the referenced strings for assembly instructions that access string data. This is the coolest feature of my deobfuscator.
Without those features, it would have been very difficult to figure out where each jmp
instruction was jumping to, and it would have been hard to make use of string information in the deobfuscated code.
Below is my deobfuscator script. If you append backbone subroutine address in funcs
list, the script will deobfuscate it.
from capstone import *
from elftools.elf.elffile import ELFFile
import re
class Disassembler:
def __init__(self, elf_path):
self.elf_path = elf_path
self._load_elf()
def _load_elf(self):
with open(self.elf_path, "rb") as f:
self.elf = ELFFile(f)
self.text_section = self.elf.get_section_by_name('.text')
if not self.text_section:
raise Exception(".text section not found")
self.text_addr = self.text_section['sh_addr']
self.text_offset = self.text_section['sh_offset']
self.text_data = self.text_section.data()
self.md = Cs(CS_ARCH_X86, CS_MODE_64)
def disasm_at(self, vaddr, count=3):
if not (self.text_addr <= vaddr < self.text_addr + len(self.text_data)):
raise Exception(f"address 0x{vaddr:x} not in .text section")
offset = vaddr - self.text_addr
code = self.text_data[offset:offset+0x200]
result = []
last_insn_end = vaddr
for i, insn in enumerate(self.md.disasm(code, vaddr)):
result.append({"addr": insn.address, "mnemonic": insn.mnemonic, "op_str": insn.op_str, "size": insn.size})
last_insn_end = insn.address + insn.size
if i + 1 == count:
break
return result, last_insn_end
def deof_func(d: Disassembler, start, end=0):
MAIN_START = start
result = []
label = []
cur_addr = MAIN_START
while True:
if end != 0:
if cur_addr > end:
break
legacy_cur = cur_addr
asm, cur_addr = d.disasm_at(cur_addr, count=1)
# call 명령어가 아니면 실제 실행되는 assembly
if asm[0]["mnemonic"] != "call":
result.append((asm[0], legacy_cur))
try:
label.append(int(asm[0]["op_str"], 0))
except:
pass
# call 명령어라면 3줄짜리 subroutine assembly 가져오기
elif asm[0]["mnemonic"] == "call":
sub_vaddr= int(asm[0]["op_str"], 0)
subroutine, _ = d.disasm_at(sub_vaddr)
if subroutine[0]["mnemonic"] != "add" or subroutine[0]["op_str"] != "rsp, 8":
raise Exception(f"subroutine 0x{sub_vaddr:x} stack frame is weird")
if subroutine[1]["mnemonic"] == "ret":
result.append((subroutine[1], legacy_cur))
break
if subroutine[1]["mnemonic"] == "jmp":
label.append(int(subroutine[1]["op_str"], 0))
if subroutine[2]["mnemonic"] != "jmp":
raise Exception(f"subroutine 0x{sub_vaddr:x} do not jmp")
cur_addr = int(subroutine[2]["op_str"], 0)
result.append((subroutine[1], legacy_cur))
return result, label
def extract_rip_val(s):
pattern = r"\[rip \+ (0x[0-9a-fA-F]+|\d+)\]"
matches = re.findall(pattern, s)
return [int(m, 0) for m in matches]
def get_dicval(d, key):
for k in d:
if int(k, 0) == key:
return d[k]
return None
def gen_deof_file(file, deof, label):
func_table = [
"free",
"strcpy",
"puts",
"fread",
"fclose",
"strlen",
"__stack_chk_fail",
"printf",
"memset",
"memcmp",
"malloc",
"fopen"
]
str_table = {
"0x33A5008": 'Usage: %s <license file>',
"0x33A5025": 'Failed to open license file.',
"0x33A5048": 'Your license file seems to be blank.',
"0x33A5080": 'Congratulations! The flag is %s.',
"0x33A50A2": 'Incorrect hash :(',
"0x33A50B4": 'Incorrect :('
}
with open(file, "w") as f:
for elem in deof:
asm = elem[0]
if elem[1] in label:
f.write(f"0x{elem[1]:x}" + "\n")
addr = asm["addr"]
mnem = asm["mnemonic"]
op = asm["op_str"]
if mnem == "call" and int(op, 0) >= 0x1030 and int(op, 0) <= 0x10e0:
op = func_table[(int(op, 0) - 0x1030) // 0x10]
ref_list = extract_rip_val(op)
if len(ref_list) > 0:
assert len(ref_list) == 1
ref_addr = ref_list[0] + asm["addr"] + asm["size"]
comments = get_dicval(str_table, ref_addr)
if comments != None:
op += " # " + comments
f.write(f"0x{addr:x}: \t{mnem}\t{op}" + "\n")
return
if __name__ == "__main__":
elf_path = "./tiniii"
d = Disassembler(elf_path)
MAIN = 0x33A1969
funcs = [0x219ee00, 0x33A1969, 0x219EAFB, 0x33a4154, 0x33a352a]
for func in funcs:
res, label = deof_func(d, func)
if func == MAIN:
gen_deof_file("main.asm", res, label)
else:
gen_deof_file(f"func_{hex(func)[2:]}.asm", res, label)
Analyzing main
subroutine
Now that the deobfuscator is complete, it’s time to dive into analyzing the binary routines. Let’s start by examining the main
subroutine. Below is the deobfuscated code generated by my deobfuscator.
0x33a1969: push rbp
0x219e3da: mov rbp, rsp
0x219e3e6: lea r11, [rsp - 0x61e000]
0x33a1977
0x219e3f7: sub rsp, 0x1000
0x219e407: or dword ptr [rsp], 0
0x219e414: cmp rsp, r11
0x33a198c: jne 0x33a1977
0x219e420: sub rsp, 0x6c0
0x219e430: mov dword ptr [rbp - 0x61e6b4], edi
0x219e43f: mov qword ptr [rbp - 0x61e6c0], rsi
0x219e44f: mov rax, qword ptr fs:[0x28]
0x219e461: mov qword ptr [rbp - 8], rax
0x219e46e: xor eax, eax
0x219e479: lea rax, [rbp - 0x61a820]
0x219e489: mov edx, 0x61a800
0x219e497: mov esi, 0
0x219e4a5: mov rdi, rax
0x219e4b1: call memset
0x219e4bf: lea rdx, [rbp - 0x61e6a0]
0x219e4cf: mov eax, 0
0x219e4dd: mov ecx, 0x3e8
0x219e4eb: mov rdi, rdx
0x219e4f7: rep stosq qword ptr [rdi], rax
0x219e503: lea rdx, [rbp - 0x61c760]
0x219e513: mov eax, 0
0x219e521: mov ecx, 0x3e8
0x219e52f: mov rdi, rdx
0x219e53b: rep stosq qword ptr [rdi], rax
0x219e547: lea rax, [rbp - 0x61a820]
0x219e557: mov rdi, rax
0x219e563: call 0x219f0c0
0x219e571: lea rax, [rbp - 0x61e6a0]
0x219e581: mov rdi, rax
0x219e58d: call 0x339bd66
0x219e59b: cmp dword ptr [rbp - 0x61e6b4], 1
0x33a1a69: jg 0x33a1ab9
0x219e5ab: mov rax, qword ptr [rbp - 0x61e6c0]
0x219e5bb: mov rax, qword ptr [rax]
0x219e5c7: mov rsi, rax
0x219e5d3: lea rax, [rip + 0x1206a2e] # Usage: %s <license file>
0x219e5e3: mov rdi, rax
0x219e5ef: mov eax, 0
0x219e5fd: call printf
0x219e60b: mov eax, 1
0x219e619: jmp 0x33a1d56
0x33a1ab9
0x219e627: mov rax, qword ptr [rbp - 0x61e6c0]
0x219e637: add rax, 8
0x219e644: mov rax, qword ptr [rax]
0x219e650: lea rdx, [rip + 0x12069cb]
0x219e660: mov rsi, rdx
0x219e66c: mov rdi, rax
0x219e678: call fopen
0x219e686: mov qword ptr [rbp - 0x61e6a8], rax
0x219e696: cmp qword ptr [rbp - 0x61e6a8], 0
0x33a1b07: jne 0x33a1b2d
0x219e6a7: lea rax, [rip + 0x1206977] # Failed to open license file.
0x219e6b7: mov rdi, rax
0x219e6c3: call puts
0x219e6d1: mov eax, 1
0x219e6df: jmp 0x33a1d56
0x33a1b2d
0x219e6ed: mov rdx, qword ptr [rbp - 0x61e6a8]
0x219e6fd: lea rax, [rbp - 0x61c760]
0x219e70d: mov rcx, rdx
0x219e719: mov edx, 0x3e8
0x219e727: mov esi, 8
0x219e735: mov rdi, rax
0x219e741: call fread
0x219e74f: mov rax, qword ptr [rbp - 0x61e6a8]
0x219e75f: mov rdi, rax
0x219e76b: call fclose
0x219e779: mov dword ptr [rbp - 0x61e6b0], 0
0x219e78c: mov dword ptr [rbp - 0x61e6ac], 0
0x219e79f: jmp 0x33a1bdf
0x33a1b98
0x219e7ad: mov eax, dword ptr [rbp - 0x61e6ac]
0x219e7bc: cdqe
0x219e7c7: mov eax, dword ptr [rbp + rax*8 - 0x61c760]
0x219e7d7: test eax, eax
0x33a1bb2: jne 0x33a1bd1
0x219e7e2: mov eax, dword ptr [rbp - 0x61e6ac]
0x219e7f1: cdqe
0x219e7fc: mov eax, dword ptr [rbp + rax*8 - 0x61c75c]
0x219e80c: test eax, eax
0x33a1bcf: je 0x33a1bd8
0x33a1bd1
0x219e817: add dword ptr [rbp - 0x61e6b0], 1
0x33a1bd8
0x219e827: add dword ptr [rbp - 0x61e6ac], 1
0x33a1bdf
0x219e837: cmp dword ptr [rbp - 0x61e6ac], 0x3e7
0x33a1bea: jle 0x33a1b98
0x219e84a: cmp dword ptr [rbp - 0x61e6b0], 0
0x33a1bf6: jne 0x33a1c1d
0x219e85a: lea rax, [rip + 0x12067e7] # Your license file seems to be blank.
0x219e86a: mov rdi, rax
0x219e876: call puts
0x219e884: mov eax, 1
0x219e892: jmp 0x33a1d56
0x33a1c1d
0x219e8a0: lea rdx, [rbp - 0x61c760]
0x219e8b0: lea rcx, [rbp - 0x61e6a0]
0x219e8c0: lea rax, [rbp - 0x61a820]
0x219e8d0: mov rsi, rcx
0x219e8dc: mov rdi, rax
0x219e8e8: call 0x219ee00
0x219e8f6: test eax, eax
0x33a1c5b: je 0x33a1d3b
0x219e901: mov qword ptr [rbp - 0x20], 0
0x219e912: mov qword ptr [rbp - 0x18], 0
0x219e923: lea rax, [rbp - 0x61c760]
0x219e933: mov rdi, rax
0x219e93f: call 0x219eafb
0x219e94d: lea rax, [rbp - 0x20]
0x219e95a: mov rsi, rax
0x219e966: lea rax, [rip + 0x1208913]
0x219e976: mov rdi, rax
0x219e982: call 0x33a4154
0x219e990: lea rax, [rbp - 0x20]
0x219e99d: mov edx, 0x10
0x219e9ab: lea rcx, [rip + 0x12066bb]
0x219e9bb: mov rsi, rcx
0x219e9c7: mov rdi, rax
0x219e9d3: call memcmp
0x219e9e1: test eax, eax
0x33a1ce7: jne 0x33a1d1f
0x219e9ec: lea rax, [rip + 0x120888d]
0x219e9fc: mov rsi, rax
0x219ea08: lea rax, [rip + 0x1206671] # Congratulations! The flag is %s.
0x219ea18: mov rdi, rax
0x219ea24: mov eax, 0
0x219ea32: call printf
0x219ea40: jmp 0x33a1d4f
0x33a1d1f
0x219ea4e: lea rax, [rip + 0x120664d] # Incorrect hash :(
0x219ea5e: mov rdi, rax
0x219ea6a: call puts
0x219ea78: jmp 0x33a1d4f
0x33a1d3b
0x219ea86: lea rax, [rip + 0x1206627] # Incorrect :(
0x219ea96: mov rdi, rax
0x219eaa2: call puts
0x33a1d4f
0x219eab0: mov eax, 0
0x33a1d56
0x219eabe: mov rdx, qword ptr [rbp - 8]
0x219eacb: sub rdx, qword ptr fs:[0x28]
0x33a1d64: je 0x33a1d70
0x219eadd: call __stack_chk_fail
0x33a1d70
0x219eaeb: leave
0x219eaf5: ret
Our main focus is on understanding what operations are perfomed on the binary’s input (the license file) and how the validation routine works. Therefore, I ignored the routines that execute before the binary receives its input (0x219e741: call fread
). If information about those routines is ever needed, it can be resolved through debugging. After ignoring some routines, I could figure out which subroutines I had to analyze: 0x219ee00
, 0x219eafb
, 0x33a4154
.
In case of the subroutine 0x33a4154
, it was easy to notice that it computes an MD5 hash (it contained numerous constants related to MD5, and throughout the main routine, there were many strings associated with hashing). Given the presence of both the strings “Incorrect” and “Incorrect hash”, it is likely that multiple inputs can pass the verify, but only one producing a matching hash are actually related to the flag. Therefore, the hash routine merely indicates that there may be multiple valid inputs, and the routine itself is not of particular interest to us.
Now, let’s dive into what truly matters to us: the routines at 0x219ee00
and 0x219eafb
.
Analyzing subroutine at 0x219ee00
0x219ee00: push rbp
0x173f: mov rbp, rsp
0x174b: sub rsp, 0x48
0x1758: mov qword ptr [rbp - 0x38], rdi
0x1765: mov qword ptr [rbp - 0x40], rsi
0x1772: mov qword ptr [rbp - 0x48], rdx
0x177f: mov dword ptr [rbp - 0x2c], 0
0x178f: jmp 0x219f09f
0x219ee36
0x179d: mov eax, dword ptr [rbp - 0x2c]
0x17a9: movsxd rdx, eax
0x17b5: imul rdx, rdx, 0x10624dd3
0x17c5: shr rdx, 0x20
0x17d2: sar edx, 6
0x17de: mov ecx, eax
0x17e9: sar ecx, 0x1f
0x17f5: sub edx, ecx
0x1800: imul ecx, edx, 0x3e8
0x180f: sub eax, ecx
0x181a: mov edx, eax
0x1825: movsxd rax, edx
0x1831: lea rdx, [rax*8]
0x1842: mov rax, qword ptr [rbp - 0x40]
0x184f: add rax, rdx
0x185b: mov rax, qword ptr [rax]
0x1867: mov qword ptr [rbp - 0x10], rax
0x1874: mov qword ptr [rbp - 0x20], 0
0x1885: mov qword ptr [rbp - 0x18], 0
0x1896: mov eax, dword ptr [rbp - 0x2c]
0x18a2: movsxd rdx, eax
0x18ae: imul rdx, rdx, 0x10624dd3
0x18be: shr rdx, 0x20
0x18cb: sar edx, 6
0x18d7: mov ecx, eax
0x18e2: sar ecx, 0x1f
0x18ee: sub edx, ecx
0x18f9: imul ecx, edx, 0x3e8
0x1908: sub eax, ecx
0x1913: mov edx, eax
0x191e: movsxd rax, edx
0x192a: lea rdx, [rax*8]
0x193b: mov rax, qword ptr [rbp - 0x48]
0x1948: add rax, rdx
0x1954: mov rax, qword ptr [rax]
0x1960: mov qword ptr [rbp - 8], rax
0x196d: mov eax, dword ptr [rbp - 8]
0x1979: cmp eax, 0xc34ff
0x219ef5b: ja 0x219ef6d
0x1987: mov eax, dword ptr [rbp - 4]
0x1993: cmp eax, 0xc34ff
0x219ef6b: jbe 0x219ef7e
0x219ef6d
0x19a1: mov eax, 0
0x19af: jmp 0x219f0b2
0x219ef7e
0x19bd: mov dword ptr [rbp - 0x28], 0
0x19cd: jmp 0x219efc6
0x219ef8c
0x19db: mov eax, dword ptr [rbp - 0x28]
0x19e7: cdqe
0x19f2: lea rdx, [rax*8]
0x1a03: mov rax, qword ptr [rbp - 0x38]
0x1a10: add rax, rdx
0x1a1c: mov rax, qword ptr [rax]
0x1a28: add qword ptr [rbp - 0x20], rax
0x1a35: add dword ptr [rbp - 0x28], 1
0x219efc6
0x1a42: mov edx, dword ptr [rbp - 8]
0x1a4e: mov eax, dword ptr [rbp - 0x28]
0x1a5a: cmp edx, eax
0x219efde: ja 0x219ef8c
0x1a65: mov dword ptr [rbp - 0x24], 0xc34ff
0x1a75: jmp 0x219f02c
0x219eff2
0x1a83: mov eax, dword ptr [rbp - 0x24]
0x1a8f: cdqe
0x1a9a: lea rdx, [rax*8]
0x1aab: mov rax, qword ptr [rbp - 0x38]
0x1ab8: add rax, rdx
0x1ac4: mov rax, qword ptr [rax]
0x1ad0: add qword ptr [rbp - 0x18], rax
0x1add: sub dword ptr [rbp - 0x24], 1
0x219f02c
0x1aea: mov eax, dword ptr [rbp - 4]
0x1af6: mov edx, 0xc3500
0x1b04: sub edx, eax
0x1b0f: mov eax, dword ptr [rbp - 0x24]
0x1b1b: cmp edx, eax
0x219f050: jbe 0x219eff2
0x1b26: cmp qword ptr [rbp - 0x20], 0
0x219f05c: je 0x219f098
0x1b34: cmp qword ptr [rbp - 0x18], 0
0x219f064: je 0x219f098
0x1b42: mov rdx, qword ptr [rbp - 0x20]
0x1b4f: mov rax, qword ptr [rbp - 0x18]
0x1b5c: add rax, rdx
0x1b68: cmp qword ptr [rbp - 0x10], rax
0x219f089: je 0x219f098
0x1b75: mov eax, 0
0x1b83: jmp 0x219f0b2
0x219f098
0x1b91: add dword ptr [rbp - 0x2c], 1
0x219f09f
0x1b9e: cmp dword ptr [rbp - 0x2c], 0x4e1f
0x219f0a6: jle 0x219ee36
0x1bae: mov eax, 1
0x219f0b2
0x1bbc: leave
0x1bc6: ret
I ported the above routine to Python. The result is as follows:
def func_219ee00(rdi, rsi, user_input):
for counter in range(0x4e20):
remainder = counter % 1000
expected = rsi[remainder]
countValue = (user_input[remainder] >> 32) & 0xffffffff
limitVal = (user_input[remainder] & 0xffffffff)
if countValue > 0xc34ff or limitVal > 0xc34ff:
return 0
sum1 = sum(rdi[:countValue])
start = 0xc3500 - limitVal
end = 0xc34ff + 1
sum2 = sum(rdi[start:end])
if sum1 == 0 or sum2 == 0 or (sum1 + sum2 != expected):
return 0
return 1
rdi
and rsi
values are initialized value, so it can be obtained by debugging. Since the purpose of the routine is to return 1
, it can be used to impose a condition on the input. While it’s certainly possible to find inputs that satisfy this condition, the number of such input is likely very large. Given that the remaining routine at 0x219eafb
is relatively short, let’s hold off on solving this for now and finishing analyzing the rest routine.
Analyzing subroutine at 0x219eafb
0x219eafb: push rbp
0x11ed: mov rbp, rsp
0x11f9: sub rsp, 0xc0
0x1209: mov qword ptr [rbp - 0xb8], rdi
0x1219: mov rax, qword ptr fs:[0x28]
0x122b: mov qword ptr [rbp - 8], rax
0x1238: xor eax, eax
0x1243: mov qword ptr [rbp - 0x90], 0
0x1257: mov qword ptr [rbp - 0x88], 0
0x126b: mov qword ptr [rbp - 0x80], 0
0x127c: mov qword ptr [rbp - 0x78], 0
0x128d: mov qword ptr [rbp - 0x70], 0
0x129e: mov qword ptr [rbp - 0x68], 0
0x12af: mov qword ptr [rbp - 0x60], 0
0x12c0: mov qword ptr [rbp - 0x58], 0
0x12d1: mov qword ptr [rbp - 0x50], 0
0x12e2: mov qword ptr [rbp - 0x48], 0
0x12f3: mov qword ptr [rbp - 0x40], 0
0x1304: mov qword ptr [rbp - 0x38], 0
0x1315: mov qword ptr [rbp - 0x30], 0
0x1326: mov qword ptr [rbp - 0x28], 0
0x1337: mov qword ptr [rbp - 0x20], 0
0x1348: mov dword ptr [rbp - 0x18], 0
0x1358: mov byte ptr [rbp - 0x14], 0
0x1365: mov dword ptr [rbp - 0xa8], 0
0x1378: mov dword ptr [rbp - 0xa4], 0
0x138b: mov dword ptr [rbp - 0xa0], 0
0x139e: mov dword ptr [rbp - 0x9c], 0
0x13b1: jmp 0x219eca5
0x219ebdd
0x13bf: mov eax, dword ptr [rbp - 0x9c]
0x13ce: cdqe
0x13d9: lea rdx, [rax*8]
0x13ea: mov rax, qword ptr [rbp - 0xb8]
0x13fa: add rax, rdx
0x1406: mov rax, qword ptr [rax]
0x1412: mov qword ptr [rbp - 0x98], rax
0x1422: shl dword ptr [rbp - 0xa4], 1
0x1431: mov eax, dword ptr [rbp - 0x98]
0x1440: test eax, eax
0x219ec2c: jne 0x219ec42
0x144b: mov eax, dword ptr [rbp - 0x94]
0x145a: test eax, eax
0x219ec40: je 0x219ec49
0x219ec42
0x1465: or dword ptr [rbp - 0xa4], 1
0x219ec49
0x1475: add dword ptr [rbp - 0xa0], 1
0x1485: cmp dword ptr [rbp - 0xa0], 8
0x219ec56: jne 0x219ec9e
0x1495: mov eax, dword ptr [rbp - 0xa4]
0x14a4: mov edx, eax
0x14af: mov eax, dword ptr [rbp - 0xa8]
0x14be: cdqe
0x14c9: mov byte ptr [rbp + rax - 0x90], dl
0x14d9: add dword ptr [rbp - 0xa8], 1
0x14e9: mov dword ptr [rbp - 0xa4], 0
0x14fc: mov dword ptr [rbp - 0xa0], 0
0x219ec9e
0x150f: add dword ptr [rbp - 0x9c], 1
0x219eca5
0x151f: cmp dword ptr [rbp - 0x9c], 0x3e7
0x219ecac: jle 0x219ebdd
0x1532: lea rax, [rbp - 0x90]
0x1542: mov rsi, rax
0x154e: lea rax, [rip + 0x33a5d2b]
0x155e: mov rdi, rax
0x156a: call strcpy
0x1578: nop
0x1582: mov rdx, qword ptr [rbp - 8]
0x158f: sub rdx, qword ptr fs:[0x28]
0x219eceb: je 0x219ecf4
0x15a1: call __stack_chk_fail
0x219ecf4
0x15af: leave
0x15b9: ret
Similary, we ported the assembly into C-style pseudocode:
fun_219eafb(a1)
{
var_a0 = 0;
var_a4 = 0;
var_a8 = 0;
for (int i=0; i<= 999; i++) {
var_a4 << 1;
if (a1[i] != 0) {
var_a4 |= 1;
}
var_a0 += 1;
if (var_a0 == 8) {
var_90[var_a8] = var_a4;
var_a4 = 0;
var_a9 = 0;
}
}
strcpy(0x33a7280, var_90);
}
The subroutine takes the binary’s input as its parameter, and upon examining the routine, there’s something particularly interesting: not all parts of the input that satisfy the previous routine’s conditions and produce the correct hash are actually used in the flag computation.
More precisely, by simply determining which indices in the input have valid solutions according to the previous routine, we can fully reconstruct the flag.
Get the flag
Below is the solver code that reconstructs the flag by checking, for each index, whether there exists a solution that satisfies the 0x219ee00
subroutine. Using a prefix sum makes the computation straightforward.
from const import const, const2
sums_front = [0]
for i in range(800000):
assert const[i] > 0
sums_front.append(sums_front[-1] + const[i])
sums_back = [0]
for i in range(800000):
sums_back.append(sums_back[-1] + const[799999 - i])
sb = {}
for i in range(800000):
sb[sums_back[i]] = i
chk = []
from tqdm import tqdm
for idx, e in tqdm(enumerate(const2)):
roots = []
for i in range(800000):
s = sums_front[i]
ss = e - s
if ss < 0:
break
if ss in sb:
root = [i, sb[ss]]
assert i + sb[ss] < 800000
roots.append(root)
chk.append(int(len(roots) > 0))
assert len(chk) % 8 == 0
byte_values = [
int("".join(str(b) for b in chk[i:i+8]), 2)
for i in range(0, len(chk), 8)
]
print(bytes(byte_values))
$ python3 solve.py
1000it [00:26, 38.03it/s]
b'flag{stick_t0_0ne_thing_until_y0u_get_there_just_like_a_stamp!!!!!!}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00'
flag : flag{stick_t0_0ne_thing_until_y0u_get_there_just_like_a_stamp!!!!!!}