Hi,
Intro
We finished #21 in this CTF, we didn't manage to solve many easy tasks. We are always looking for people to join the team and help us doing better results.
Only 8 teams solved this challenge that's why I decided to publish a write-up to explain how I solved it. I earned the second most points in the reverse engineering catgory by solving all the tasks and wining bonus points for 2 tasks.
The challenge:
Given a snake game the goal is to eat all the provided foods in order to win and get the flag.
The number of foods is computed based on total used allocation units of the disk.
The number of foods is computed based on total used allocation units of the disk.
The first appraoch would be take the binary and reverse engineer the game logic, patch either the memory variable containing the score or the remaining foods.
I decided from the beginning not to go through this approach since I estimated that it will be time consuming because I need to unpack the binary and evade the anti-debug tricks present.
My idea was just simple, make the number of remaining foods 0 or negative.
My idea was just simple, make the number of remaining foods 0 or negative.
How this number is calculated ? To go with more details let's first of all trace the execution of the program using procmon.
Just before typing enter to start the game we can see that there is an invoked operation (highlighted) QueryFullSizeInformationVolume
We can conclude taht the number of remaining food us computed as follows:
Total Foods = UsedAllocationUnits = (TotalAllocationUnits - ActualAvailableAllocationUnits) * SectorsPerAllocationUnit * BytesPerSector
Just before typing enter to start the game we can see that there is an invoked operation (highlighted) QueryFullSizeInformationVolume
We can conclude taht the number of remaining food us computed as follows:
Total Foods = UsedAllocationUnits = (TotalAllocationUnits - ActualAvailableAllocationUnits) * SectorsPerAllocationUnit * BytesPerSector
TotalAllocationUnits: 26214399
CallerAvailableAllocationUnits: 1599263
ActualAvailableAllocationUnits: 1599263
SectorsPerAllocationUnit: 8
Imagine if we hook or intercept this call and set the value of TotalAllocationUnits to 0 what would be the total number of food ? negative ? Win ?
Lets visualize the stack call trace around this operation.
GetDiskFreeSpaceExW function sounds a good interception point.
Lets visualize the stack call trace around this operation.
GetDiskFreeSpaceExW function sounds a good interception point.
Referring to MSDN documentation, it retrieves information about the amount of space that is available on a disk volume, which is the total amount of space, the total amount of free space, and the total amount of free space available to the user that is associated with the calling thread.
BOOL WINAPI GetDiskFreeSpaceEx(
_In_opt_ LPCTSTR lpDirectoryName,
_Out_opt_ PULARGE_INTEGER lpFreeBytesAvailable,
_Out_opt_ PULARGE_INTEGER lpTotalNumberOfBytes,
_Out_opt_ PULARGE_INTEGER lpTotalNumberOfFreeBytes
);
By setting lpTotalNumberOfBytes to 0 we achieve our goal.
To do this we need to write a small C code which before GetDiskFreeSpaceEx call get pointer to lpTotalNumberOfBytes and after the call set the value of lpTotalNumberOfBytes to 0 using the saved pointer.
I downloaded Visual Studio, I already have pin and I wrote the following code:
Build and run#include "pin.H"
#include <fstream>
#include <iomanip>
#include <iostream>
#include <list>
char * lpDirectoryName;
ADDRINT lpFreeBytesAvailable;
ADDRINT lpTotalNumberOfBytes;
ADDRINT lpTotalNumberOfFreeBytes;
INT32 Usage()
{
return -1;
}
VOID GetDiskFreeSpaceExWArg(CHAR * name, char * arg1, ADDRINT arg2, ADDRINT arg3, ADDRINT arg4)
{
lpDirectoryName = arg1;
lpFreeBytesAvailable= arg2;
lpTotalNumberOfBytes = arg3;
lpTotalNumberOfFreeBytes = arg4;
cout << name << "(" << arg1 << ")" << arg2 << " " << arg3 << " "<< arg4 << endl;
}
VOID GetDiskFreeSpaceExWAfter(ADDRINT ret)
{
cout << "\tReturned handle: " << ret << endl;
*((unsigned __int64 *)lpTotalNumberOfBytes) = 0;
cout << "\lpDirectoryName: " << lpDirectoryName << endl;
cout << "\tlpFreeBytesAvailable: " << *((unsigned __int64 *)lpFreeBytesAvailable) << endl;
cout << "\lpTotalNumberOfBytes: " << *((unsigned __int64 *)lpTotalNumberOfBytes) << endl;
cout << "\lpTotalNumberOfFreeBytes: " << *((unsigned __int64 *)lpTotalNumberOfFreeBytes) << endl;
}
VOID Image(IMG img, VOID *v)
{
RTN cfwRtn = RTN_FindByName(img, "GetDiskFreeSpaceExW");
if (RTN_Valid(cfwRtn))
{
RTN_Open(cfwRtn);
RTN_InsertCall(cfwRtn, IPOINT_BEFORE, (AFUNPTR)GetDiskFreeSpaceExWArg,
IARG_ADDRINT, "GetDiskFreeSpaceExW",
IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
IARG_FUNCARG_ENTRYPOINT_VALUE, 1,
IARG_FUNCARG_ENTRYPOINT_VALUE, 2,
IARG_FUNCARG_ENTRYPOINT_VALUE, 3,
IARG_END);
RTN_InsertCall(cfwRtn, IPOINT_AFTER, (AFUNPTR)GetDiskFreeSpaceExWAfter,
IARG_FUNCRET_EXITPOINT_VALUE, IARG_END);
RTN_Close(cfwRtn);
}
}
int main(int argc, char *argv[])
{
PIN_InitSymbols();
if (PIN_Init(argc, argv)) {
return Usage();
}
IMG_AddInstrumentFunction(Image, 0);
PIN_StartProgram();
return 0;
}
C:\CTF\inst>pin -t snake.dll -- snake.exe
Bingo !
We won the game with 0 points and the flag was SharifCTF{c98128632a4d6741d0f2b9d616f4af09}
We won the game with 0 points and the flag was SharifCTF{c98128632a4d6741d0f2b9d616f4af09}