
In the
previous article, we discussed how reverse engineering can help in gaining any advantages over other users. Today we will talk about one more application of reverse engineering - fixing bugs in the absence of application source codes. The reasons for doing such things can be the whole sea - the development of the program has long been abandoned, and the author has not provided the public with it. The development is carried out in a completely different way, and the authors do not care about your bug / etc, but they are united by a common The goal is to fix broken functionality that constantly annoys you.
Well, more to the point. There is such a widely known program in narrow circles called
“Govorilka” . As the author explains, this is nothing more than a “voice reading program”. In fact, the way it is. With the help of it, a lot of popular and not-so-wide videos were distributed throughout the network. The program has a console version called
“Govorilka_cp” , which is convenient to call from your own applications, which, in fact, I did in one of my projects.
')
Unfortunately, in the process of distributing my software, a rather strange moment was discovered - on some machines a talker falls absolutely on any phrases, and the fall was caused not by my interaction with this program, but by the talker itself. In an attempt to find out as much detail as possible about the error that was occurring, I discovered that on two seemingly completely identical systems, the talker behaves in the opposite way — on the one it works stably without any errors, and on the other it falls on every report sent to it. as an argument to the phrase. This situation was pretty much bothering me, and I decided to deal with this problem at all costs.
Considering that the speaker has not been updated for several years, and the author himself left this “message” on his website

, I realized that I had no hope in anyone, and I would have to solve the problem myself.
How was the process, and what came of it, read under the cut (carefully, a
lot of screenshots ).
Before loading the speaker to
OllyDbg , let's see if it is protected by some protector. We take in the hands of
DiE and see the following picture:

Judging by his conclusion, the program is packed with
ASPack . For credibility, we will use another analyzer -
PEiD :

Hoping that two analyzers cannot be wrong at the same time, we come to the conclusion that the talker is really covered with ASPack. Well, nothing, let's take it off.
Getting rid of an executable file from ASPack can be divided into three main stages:
- Search OEP (Original Entry Point - the address from which the program would start if it were not packed)
- Dumping
- IAT Recovery
We begin, of course, with the OEP search. One of the easiest ways to find the original entry point is to put a hardware breakpoint on
ESP-4 , because most of the packers restore the stack after their work. We start the talker in OllyDbg, open the Command Line with Alt-F1 (the window of the standard plug-in that comes with the debugger itself), enter the command
hr esp-4 , press F9 and stop at this place:

Now we need to go to the nearest return using Ctrl-F9, press F8 and ... find ourselves in OEP, which in this case is located at
0x0045A210 :

It's time to take a dump. Download and install a plugin for OllyDbg called
OllyDump , restart debugging with Ctrl-F2, stop at OEP again and select the menu item “Dump debugged process” located in Plugins -> OllyDump:

Press the "Dump" button, select the name of the output executable file and ... we get this when trying to start it:

It means that something is wrong with imports. Nothing, restore them manually.
Dump the talker process again, but this time we remove the checkmark from the Rebuild Import Checkbox. Download
ImpREC , select the talk process from the list, enter the address
5A210 in the field for OEP -
0x0045A210 - image base (
0x400000 ) =
0x5A210 and click on the "IAT AutoSearch" button:

We do, as they said, and see the following picture:

Obviously, this is not normal. We try to specify the RVA and size, which the program itself offered us to use in case of failure, and click on the “Get Imports” button again:

Something is clearly not going as expected. Let's try to find the boundaries of IAT manually. We are looking for a call to any WinAPI function in the disassembled listing. For example, this one:

Jump on it (left click -> Enter) and see where all the JMPs are going:

Go to any of the addresses specified in this place (right-click -> Follow in Dump -> Memory address), select the appropriate view (right-click on the Memory Dump window -> Long -> Address) and see the following:

We run our eyes to the beginning of the list of addresses of functions:

Double-click on
ntdll.RtlDeleteCriticalSection and look for the end of the list:

Specify the start address of IAT (
0005F168 ) and size (
0000066C ), again click on the “Get Imports” button and get the following result:

Already better, but still there are invalid imports, and the strangest thing is that we seem to have done everything correctly ... Let's use another application for IAT rebuilding -
Scylla :

What is going on? And let's try to do the same thing on another system - for example, in Windows 7 x32 (before this, experiments were conducted in Windows 8 x64).
In a miraculous way, we got valid imports:

Honestly, I'm not sure that this is a bug of ImpRec and Scylla, a feature of Windows 8 x64 or something else, but the main thing is that we restored the IAT. More precisely, for this we need to click on the “Fix Dump” button, select the dump made earlier and try to run the resulting executable file. Yes, when launching a speaker without arguments, it works as it should be, but in the case of the transfer of any phrases for dubbing, it falls, as in the packed version.
So ASPack is filmed. What's next? And then, in fact, we have to deal with a bug arising on some computers.
First of all, let's take a look at the standard Windows window, which reports that the application is more beautiful:

You should notice that Exception Offset is
0x000591D4 . We load the govorilka into OllyDbg, put the software breakpoint on this address (more precisely, on the image base +
0x000591D4 - in my case it is
0x004591D4 ), press F9 and we get the following:

As you can see, the value of the
ESI register at the time of the read operation at
ESI + 38 is zero, which, of course, causes the application to crash. Two instructions above noticeably that the value in
ESI falls from the
EAX register. The value of the
EAX register before this procedure does not change, so we look at the Call Stack, from where we were called:

Jump there (right click -> Show Call) and see that just before the procedure call is the command
MOV EAX, DWORD PTR DS: [45EC5C] :

As can be seen from the previous screenshot, at the address
0x45EC5C is also zero. We put a hardlock breakpoint on the record at this address (right click -> Breakpoint -> Hardware, on write -> Dword), run the application again and find that no entry is made at this address. Let's analyze how an application behaves if it is successful in another system. The entry at
0x45EC5C in this case does occur:

, with the result that
ESI + 38 gives some meaningful meaning. Let's find out how we got to this place. Call Stack at the time of execution of this instruction is empty, so I had the impression that this is the main procedure of the program. To understand exactly where the application began to behave differently, let's run Trace over (Ctrl-F12) on both systems (where the application crashes and where it works).
In the case of a system where the application is constantly falling, it reached a bang on access to
ESI + 38 , where it fell. The last line of code in the trace log was
CALL at
0x004597C8 :

while in the system where the application is working normally, after calling this procedure (
0x004597C8 ), other instructions are executed, on one of which the hardware breakpoint is triggered at the address
0x45EC5C :

Perhaps something goes wrong in the procedure
0x004597C8 . We put a software breakpoint on its call (
0x0045AD5B ) and find that installing it leads to the fact that on the system where everything worked fine, the hardware breakpoint does not work at the address
0x45EC5C , as a result of which the application crashes, as in the case another system. With hardware breyk to perform everything is the same.
It was experimentally found that installing breakpoints on several preceding
0x0045AD5B instructions leads to the same result. Install the breakpoint with the subsequent normal operation of the application was obtained only on this instruction:

Perhaps, by installing breakpoints in these places, we interfered with the measurement of time, which is exactly what is done with the help of calls to the
GetTickCount function.
We start Trace into (Ctrl-F11), starting with this instruction, and we find out that the differences in execution start from this place:
; , 004597EC Main MOV EAX,DWORD PTR DS:[45EC98] ; EAX=F2B47801 004597F1 Main ADD EAX,64 ; EAX=F2B47865 004597F4 Main CDQ ; EDX=FFFFFFFF 004597F5 Main CMP EDX,DWORD PTR SS:[ESP+4] 004597F9 Main JNZ SHORT Govorilk.00459807 00459807 Main POP EDX ; EDX=F2B47802
; , 004597EC Main MOV EAX,DWORD PTR DS:[45EC98] 004597F1 Main ADD EAX,64 ; EAX=064A1501 004597F4 Main CDQ 004597F5 Main CMP EDX,DWORD PTR SS:[ESP+4] 004597F9 Main JNZ SHORT Govorilk.00459807 004597FB Main CMP EAX,DWORD PTR SS:[ESP]
Hence, it is noticeable that the result of executing the instruction
CMP EDX, DWORD PTR SS: [ESP + 4] influences the further work of the procedure. In the case of the system where it works, the flag register flag Z was set at the time of execution of this instruction, while in the case of the system where the application crashes, this flag was not set:

Let's see what affects it. So, what is happening here?
- The GetTickCount function is called , the result of its call is written to the EAX register.
- EDX register contents reset
- The contents of the EDX ( 0x0 ) and EAX registers (the result of the GetTickCount function) are pushed onto the stack.
- EAX registers the value from address 0x45EC98
- 0x64 is added to it.
- CDQ instruction in progress
- Finally, the content of the EDX register is compared with the value at the ESP-4 address where the previous value of this register is stored, i.e. 0x0
Putting the hardware breakpoint on the record at
0x45EC98 , you can see that the result of the
GetTickCount function is also placed
there :

By the way, why does
GetTickCount return so much value? After all, the system was turned off by me even this morning. In fact, hybrid boot and shutdown play a role here in Windows 8. You can read more about it, for example,
here .
But back to the main topic of our discussion. What is the problem then? Let's take a closer look at the contents of general registers in this piece of code on both systems. You should notice that after executing the
CDQ instruction on the system where the application is working correctly, the
EDX register takes the value
0x0 , and in the system where the program crashes,
0xFFFFFFFF , as can be seen, for example, in the previous screenshot. Carefully read the description of the instructions
CDQ :
Converts a signed word in EDX: EAX by EAX through EDX
And now let's take a look at the signature of the GetTickCount function:
DWORD WINAPI GetTickCount(void);
As you can see, it returns a
DWORD that is “expanded” in an unsigned long, i.e.
unsigned type
I don’t know if the bug is a developer or compiler, but the author obviously didn’t count on such behavior, because in the current situation, the talk will fall on systems that run longer than 24.8 and less than 49.7 days. Why so? The lower limit is determined by the maximum value that can fit in the signed 4-byte variable - 2147483647:
2147483647/1000/60/60/24 = 24.8551348032
The upper bound is determined by the
GetTickCount function
itself , which is described in the documentation (in fact, it is determined by the maximum value that can fit into an unsigned 4-byte variable):
The elapsed time is stored as a DWORD value. Therefore, the system will run continuously for 49.7 days.
Well, it's time to fix this assumption. One of the ways to correct the problem is to somehow “replace” the call to the
GetTickCount function
with our code, in which we will “reduce” the value returned from it so that it fits into a significant 4-byte variable.
There are several options for solving this problem, but let's write a code cave. We are looking for free space (the easiest way is to find it at the “end” of the CPU window) and put the following code there:
; 0045BAAA 60 PUSHAD ; 0045BAAB 9C PUSHFD ; EIP 0045BAAC E8 00000000 CALL Govorilk.0045BAB1 0045BAB1 5E POP ESI ; GetTickCount 0045BAB2 FF96 2F380000 CALL DWORD PTR DS:[ESI+382F] ; ESP-4 0045BAB8 894424 FC MOV DWORD PTR SS:[ESP-4],EAX ; 0045BABC 9D POPFD ; 0045BABD 61 POPAD ; EAX GetTickCount 0045BABE 8B4424 D8 MOV EAX,DWORD PTR SS:[ESP-28] ; "AND" , EAX 0045BAC2 25 FFFFFF0F AND EAX,0FFFFFFF 0045BAC7 C3 RETN
The value of
382F was obtained by calculating the difference between the address in the IAT, which stores the address of the
GetTickCount function (
0x0045F2E0 ), and the current address (
0x0045BAB1 ).
To get the value of the
EIP register directly will not work - it can not be accessed either for reading or writing.
Now go to the place where
JMP is implemented on the function
GetTickCount
and replace the address to which the
JMP jumps to the start address of our code cave (in my case it is
0x0045BAAA ):

Save the modified executable file (right-click on the CPU window -> Copy to executable -> All modifications -> Copy all -> right-click on the appeared window -> Save file) and check its performance. Yes, it does work, but when you start talking, the UAC window, familiar to almost all Windows users, is now displayed. Why this is happening can be read, for example,
here , and to solve the problem in our case, it is enough just to give our executable file the original name - “Govorilka_cp.exe”.
Afterword
As you can see, reverse engineering can be used not only for cheating or searching for vulnerabilities, but also for quite useful purposes. Try to correct the problems, and not to score on them - it is quite possible that in the process of correcting the next mistake you will discover something new. And I, in turn, again hope that the article was useful to someone.
I also express my
deep gratitude to the person with the nickname
tihiy_grom - without him this article simply would not exist.