hubertf

Writeup: TFC CTF 2025 - Reverse: font league by hubertf

Aug 31st, 2025
835
1
Never
23
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 3.87 KB | Cybersecurity | 1 0
  1. #!python3
  2. #
  3. # TFC CTF 2025 - Reverse: font league
  4. #
  5. # *** Idea:
  6. # - Analysis done with https://fontdrop.info/
  7. # - TTF file has ligature glyphs that are composed of a base and a composite glyph
  8. # - glyphs with IDs <1000 are of interest
  9. # - frequency analysis shows that glyph IDs 19-28 and 68-73 correspond to hex digits
  10. # - looking through glyphs, several print "O", e.g. 2723
  11. # - take the base and composite glyph of ligature glyph 2723
  12. # - recursively decompose into "atomic" glyphs with ID <1000
  13. # - get the atomic characters, translate into hex digits, wrap as flag
  14. #
  15. # *** Run it:
  16. # (venv-TFCCTF2025) neuland% python3 9atomglyph.py 2723
  17. # Atomic glyph IDs in correct order:
  18. # [20, 73, 27, 28, 68, 28, 24, 26, 68, 19, 27, 20, 25, 72, 22, 69, 72, 68, 22, 73, 68, 19, 21, 25, 70, 71, 28, 68, 23, 26, 70, 73, 20, 27, 20, 73, 69, 21, 70, 19, 72, 19, 70, 28, 72, 28, 23, 23, 21, 68, 21, 70, 26, 27, 22, 69, 19, 20, 70, 19, 27, 22, 71, 21]
  19. #
  20. # Translated atomic glyphs to characters:
  21. # 1f89a957a0816e3bea3fa026cd9a47cf181fb2c0e0c9e9442a2c783b01c083d2
  22. #
  23. # *** Flag:
  24. # TFCCTF{1f89a957a0816e3bea3fa026cd9a47cf181fb2c0e0c9e9442a2c783b01c083d2}
  25. #
  26. # Greetings from hubertf to the MadMonday CTF Team
  27. #
  28.  
  29. import sys
  30. from fontTools.ttLib import TTFont
  31.  
  32. glyph_id_to_char = {
  33.     19: '0',
  34.     20: '1',
  35.     21: '2',
  36.     22: '3',
  37.     23: '4',
  38.     24: '5',
  39.     25: '6',
  40.     26: '7',
  41.     27: '8',
  42.     28: '9',
  43.     68: 'a',
  44.     69: 'b',
  45.     70: 'c',
  46.     71: 'd',
  47.     72: 'e',
  48.     73: 'f',
  49. }
  50.  
  51. def load_ligature_map(font):
  52.     gsub = font['GSUB'].table
  53.     lig_map = {}
  54.  
  55.     for lookup in gsub.LookupList.Lookup:
  56.         if lookup.LookupType == 4:  # Ligature substitution
  57.             for subtable in lookup.SubTable:
  58.                 if subtable.__class__.__name__ == "LigatureSubst":
  59.                     ligatures = subtable.ligatures
  60.                     for base_glyph_name, lig_list in ligatures.items():
  61.                         base_gid = font.getGlyphID(base_glyph_name)
  62.                         for lig in lig_list:
  63.                             lig_gid = font.getGlyphID(lig.LigGlyph)
  64.                             component_gids = [font.getGlyphID(c) for c in getattr(lig, 'Components', getattr(lig, 'Component', []))]
  65.                             lig_map[lig_gid] = (base_gid, component_gids)
  66.     return lig_map
  67.  
  68. def resolve_glyph(gid, lig_map, atomic_threshold=1000):
  69.     """Rekursiv Ligatur auflösen und atomare Glyphen in korrekter Reihenfolge zurückgeben."""
  70.     if gid < atomic_threshold or gid not in lig_map:
  71.         return [gid]  # atomare Glyphe als Liste
  72.  
  73.     base_gid, components = lig_map[gid]
  74.     result = []
  75.  
  76.     # Zuerst Basis rekursiv auflösen (Reihenfolge wichtig)
  77.     result.extend(resolve_glyph(base_gid, lig_map, atomic_threshold))
  78.  
  79.     # Dann alle Komponenten in Reihenfolge rekursiv auflösen
  80.     for comp_gid in components:
  81.         result.extend(resolve_glyph(comp_gid, lig_map, atomic_threshold))
  82.  
  83.     return result
  84.  
  85. def main():
  86.     font_path = "font-leagues/Arial-custom.ttf"
  87.     font = TTFont(font_path)
  88.     lig_map = load_ligature_map(font)
  89.  
  90.     if len(sys.argv) < 2:
  91.         print("Usage: python3 6list-list.py <ligature_glyph_id>")
  92.         return
  93.  
  94.     try:
  95.         start_gid = int(sys.argv[1])
  96.     except ValueError:
  97.         print("Please provide a valid integer ligature glyph ID.")
  98.         return
  99.  
  100.     if start_gid not in lig_map:
  101.         print(f"Ligature glyph ID {start_gid} not found in the font's ligature map.")
  102.         return
  103.  
  104.     atomic_glyphs_in_order = resolve_glyph(start_gid, lig_map)
  105.  
  106.     print("Atomic glyph IDs in correct order:")
  107.     print(atomic_glyphs_in_order)
  108.  
  109.     # Zeichenkette aus der Mapping-Tabelle
  110.     translated = "".join(glyph_id_to_char.get(gid, '?') for gid in atomic_glyphs_in_order)
  111.     print("\nTranslated atomic glyphs to characters:")
  112.     print(translated)
  113.  
  114. if __name__ == "__main__":
  115.     main()
  116.  
Tags: CTF TFCCTF
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • Newimuq
    20 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=lijtoggmdu
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment