Written
on
Swisscom 2025 security.txt challenge: full writeup
In Accidentally solving the Swisscom 2025 security.txt challenge I briefly hinted at the steps that I took to solve the challenge.
Seeing that Antoine published full writeup, here’s mine:
#!/usr/bin/env ruby
=begin
What's this?
https://github.com/swisscom/securitytxt/blob/master/challenges/2025.woz
How did I solve this? (how to replicate)
1. convert 2025.woz to a.dsk using https://github.com/leesaudan2/woz2dsk
1. patch the dsk with this file
1. load the a.dsk up in https://www.scullinsteel.com/apple2/
(or https://github.com/AppleWin/AppleWin)
1. type in "brun flag" (you will get flag)
Useful stuff:
- dos fs description: http://fileformats.archiveteam.org/wiki/Apple_DOS_file_system
- 6502 disassembler: https://masswerk.at/6502/disassembler.html
- a2tools to dump stuff from .dsk: https://github.com/seanwallawalla/CatsEye_A2Tools
- debugging some of the hangs via `call -151` and then 80.99 (to dump memory)
=end
require 'pp'
def read_ts(file, track, sector)
file.seek((track*16+sector)*256)
file.read(256)
end
def write_ts(file, track, sector, offset, bytes)
file.seek((track*16+sector)*256 + offset)
file.write(bytes)
end
def human_name(name)
name.map { |x| (x ^ 0x80).chr }.join.strip
end
File.open('a.dsk', 'r+b:ascii-8bit') do |f|
# read catalog, figure out where it lives
sec = read_ts(f, 17, 0).bytes
# read first page of catalog (directory), find files
cat = read_ts(f, sec[1], sec[2]).bytes
for file in cat[0xb..-1].each_slice(35).to_a
track, sector, type = *file[0,3]
name = file[3,30]
len = file[0x21,2]
#p [track, sector, type, len, human_name(name)] unless [0, 0xff].include?(track)
if ['FLAG', 'GLOBAL THERMONUCLEAR WAR'].include?(human_name(name))
puts human_name(name)
pp({ts: [track, sector], type: type, len: len})
# get tracklist
tl = read_ts(f, track, sector).bytes # tracklist
#p tl
next_tl = tl[1,2]
sec_off = tl[5,2] # sector offset
pp({next_tl: next_tl, sec_off: sec_off})
file_sectors = tl[0xc..-1].each_slice(2).to_a.reject { |x| x == [0,0] }
p :data
for sec in file_sectors
pp({sector: sec})
sec_data = read_ts(f, *sec).bytes
pp({sector_data: sec_data[0,15]})
break
end
# patch it?
if human_name(name) == 'FLAG'
write_ts(f, *file_sectors.first, 0, [
3, 8, # load addr
0xff-4, 80, # size
0x20, 0x58, 0xfc, # clr scrn
# this took a while to debug... otherwise it was hanging
# but strangely typing `X` before the `brun flag` made it
# work. so then just grep for `LDA \$..$`, dump, and minify
0xa9, 0x58, # lda ...
0x85, 0x81, # sta 81
0x20, 0, 0x40, # JSR $4000
0x60, # rts
# FIXME it hangs after displaying flag and <Esc>, but meh
].pack('C*'))
pp({patched: read_ts(f, *file_sectors.first).bytes[0,15]})
puts %[Patching done, now load it up... then: "brun flag"]
end
puts
end
end
end
Closing words
Antoine’s writeup is much more thorough than this… and actually patches this whole thing at WOZ level. While mine just accidentally recovers just enough to get the whole thing running.
Well, oopsie. :-D
Btw, the 2026 edition is out. And it was significantly easier (for me). Give it a try?