Neven Marić
FortID 2026

OSINTFortID 2026

Peak OSINT

A hiking programme PDF reuses 17 summit photos across 38 trips. A Croatian peak-bagging booklet in the cover portrait is the key: each summit's control-point number, with the dot removed, is an ASCII code.

  • pdf
  • substitution-cipher
  • ascii
  • geolocation
  • landmark-identification
  • croatia

What this one teaches

  • How to turn a repeating image sequence into a substitution cipher by hashing the embedded image streams and writing the order out as symbols.
  • A public register (here a mountaineering association's control-point list) can act as a codebook where each place stands for one character.
  • A partial decode is the best error check. Once most characters spell words, each wrong identification stands out and the missing character narrows the candidates.
  • Same-name places are a trap, so match the exact register entry, not just the name.

Hints

Stuck? Open one hint, then go back and try again before reading the next.

Hint 1 of 3Show

Count the distinct images in the PDF, not the pages.

Hint 2 of 3Show

The key is in the introduction page's portrait, not in the trip photos.

Hint 3 of 3Show

Each peak has an official number. Think about what you can do with a number.

Full solution below. Spoilers ahead.

Task description

The task had no description text. The handout is dino_trails_2026.pdf, a 40-page hiking programme for a fictional guide called “Dino Trails.” It has a cover, an introduction and 38 trip pages. Each trip page gives a date (every Saturday from 4 April to 19 December 2026), a 07:00 departure and one uncaptioned summit photo. The introduction ends with:

I like hiding secret messages in my hiking programmes. Can you find it?

The PDF has nothing hidden in it

Text, metadata, links, annotations and colours held nothing: no JavaScript, no embedded files, no optional content. The outline only lists “Trip #1” through “Trip #38,” and the photos carry no EXIF or GPS data. So whatever message there is has to be in the choice and order of the photos themselves.

The trips reuse photos. Hashing each embedded image stream (so two visually identical placements collapse to one symbol) reduces the 38 trip photos to 17 distinct images, which I labelled a to q in order of first appearance. In trip order the sequence reads:

a b c d a e f g b c h i d j c k l m d g n o c d a b c d p c m i d o n c q m

Thirty-eight symbols drawn from 17 values is the shape of a substitution cipher where one peak stands for one character. The repeat counts already hint at the plaintext: c appears seven times and d six, which are good candidates for an underscore and a common leet digit in a FortID{...} flag.

The key is the booklet in the portrait

The introduction page shows the guide holding a small blue booklet labelled “Hrvatska planinarska obilaznica” (HPO), the long-distance peak-bagging circuit run by the Croatian Mountaineering Association. Each HPO control point (kontrolna tocka, KT) has an official number of the form region.point, published on the association’s website. That register is the codebook.

Photo d carries a sign reading “RUDNIK 1052 m,” and Rudnik is HPO KT 9.5. Drop the dot and 95 is the ASCII code for _, which matches the guess that the most frequent symbol is an underscore. That gave the whole rule:

character = chr(int(KT number with the dot removed))
Why the HPO numbering lands in the ASCII range

Printable ASCII runs from 32 (space) to 126 (~), so any scheme that maps a place to a character needs its numbers to fall in that band. The HPO regions are numbered with one or two digits and the points within a region likewise, so region.point with the dot removed produces two- or three-digit numbers, and the ones the author chose sit neatly inside printable ASCII:

  • region 5, points 1 to 5 give 3 4 5 7 (codes 51, 52, 53, 55)
  • region 8, points 4 to 7 give T U V W (codes 84 to 87)
  • region 9 gives _ (95) and b (98)
  • regions 10 and 11 give g h i k and o p s u w

A two-digit point number such as 8.11 would give the three-digit code 811, far outside printable ASCII, which quickly rules some candidate peaks out without even identifying them. The introduction’s “about me” paragraph (a software engineer who hates teapots and heavy traffic) is flavour; it may nod at ASCII and HTTP 418, but the booklet in the portrait is the real clue.

Identifying the peaks

Some photos carried signs (Rudnik, Knezgrad 612, Sisol 833, a “PLESIVICA” post beside a trig pillar), which fixed those directly. The rest I matched by view and landmarks, using the partial plaintext as a running check. (Croatian diacritics are dropped from the peak names here; the originals carry them, for example Okic is Okić and Ostrc is Oštrc.)

Photo Peak KT Char
a Javorova kosa viewing pyramid (near the town of Ravna Gora) 8.4 T
b Veliki Planik 10.4 h
c Okic (castle ruin) 5.1 3
d Rudnik (sign) 9.5 _
e Senjsko bilo, Jadiceva plan 11.1 o
f Krajacev kuk 11.7 u
g Zupanj vrh 10.3 g
h Ostrc 5.3 5
i Norsicka Plesivica (sign and trig pillar) 5.5 7
j Veliki Zavizan 11.2 p
k Plesivica (Samobor hills) 5.2 4
l Sisol (sign) 10.7 k
m Mali Rajinac 11.5 s
n Knezgrad (sign) 10.5 i
o Samarske stijene 8.6 V
p Jelenc 9.8 b
q Bijele stijene 8.7 W

Decoding

kt = {'a':8.4,'b':10.4,'c':5.1,'d':9.5,'e':11.1,'f':11.7,'g':10.3,'h':5.3,'i':5.5,
      'j':11.2,'k':5.2,'l':10.7,'m':11.5,'n':10.5,'o':8.6,'p':9.8,'q':8.7}
seq = "a b c d a e f g b c h i d j c k l m d g n o c d a b c d p c m i d o n c q m".split()
out = "".join(chr(int(str(kt[s]).replace('.', ''))) for s in seq)
print("FortID{" + out + "}", "len", len(out))

Running it against the table above gives the flag:

FortID{Th3_Tough357...   len 38   sha256(flag)=bfc895695174

The 38-character inner string decodes to “The toughest peaks give the best views.” The decode is deterministic from the KT table, and it lands exactly on the flag.

Wrong guesses the plaintext exposed

A partial decode like Th3_To?gh357_p34ks_giV3_Th3_b3s7_Vi3?s shows which identifications were wrong, because the right ones spell English and leet and the wrong ones do not:

  • a was first taken for a hill called Ravna gora in Zagorje (KT 3.6, giving $). The tower is the viewing pyramid near the town of Ravna Gora in Gorski kotar (8.4, T). Same name, different place.
  • f was first taken for Bijele stijene (8.7, giving W and “ToWgh”). The slot needs u, so it is Krajacev kuk (11.7), or possibly Bjelolasica (8.5).
  • q was first taken for Risnjak (9.6, a backtick). It needs w or W, so it is Bijele stijene (8.7) or Golubic (11.9, a discontinued KT).

I could not settle the letter case of u and W from the photos alone. The submitted reading, Krajacev kuk and Bijele stijene, was the one that was accepted.

The flagRevealFortID{Th3_Tough357_p34ks_giV3_Th3_b3s7_Vi3Ws}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.