Fix a bug without source codes

In a previous article, we looked at how reverse engineering can help you gain any advantages over other users. Today we’ll talk about another reverse engineering application - fixing bugs in the absence of application source codes. The reasons for doing such things can be a whole sea - the development of the program was abandoned a long time ago, and the author didn’t provide the public with it / the development is in a completely different direction, and the authors have nothing to do with the bug / etc that you have, but they are united by a common the goal is to fix broken functionality that constantly annoys you.
Well, closer to the point. There is such a program widely known in narrow circles called “Govorilka”. As its author explains, this is nothing more than a "program for reading texts by voice." In fact, the way it is. With the help of it, a lot of popular and not so videos were voiced that spread throughout the network. The program has a console version called "Govorilka_cp" , which is convenient to call from my own applications, which, in fact, I did in one of my projects.
Unfortunately, during the distribution of my software, a rather strange moment was discovered - on some machines, the talker falls on absolutely any phrases, and the drop was caused not by my interaction with this program, but by the talker itself. In an attempt to find out as much as possible details about the error that occurred, I found that on two seemingly identical systems, the talker behaves in the opposite way - on one it works stably without any errors, and on the other it falls on each transferred to it in as an argument to a phrase. This situation is pretty fed up with me, and I decided by all means to deal with this problem.
Given that the talker has not been updated for several years, and the author himself left this “message” on his website

, I realized that I have no one to hope for, and I will have to solve the problem myself.
How the process went, and what came of it, read under the cut (carefully, many screenshots ).
Before loading the talker into OllyDbg , let's see if it is protected by some kind of tread. We take DiE into our hands and see the following picture:

Judging by its conclusion, the program is packaged by ASPack . For persuasiveness, we will use another analyzer - PEiD :

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

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

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

Press the button “Dump”, select the name of the output executable file and ... we get this when we try it run:

So something is wrong with imports. Nothing, restore them manually.
Dump the talk process again, but this time uncheck the CheckBox “Rebuild Import”. 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:

Do as we said, and observe the following picture:

Obviously, this is not normal. We try to specify the RVA and size, which the program itself suggested 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 the IAT manually. We are looking for a call to any WinAPI function in a disassembled listing. For example, this one:

We jump on it (left click -> Enter) and see where all the JMPs

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

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

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

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

It’s better, but all the same there are invalid imports, and the strangest thing is that we seem to have everything did it right ... Let's use another IAT recovery application - Scylla :

What is happening? And let's try to do the same in another system - for example, in Windows 7 x32 (before that, experiments were conducted in Windows 8 x64).
Miraculously, we received 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 you start the talker with no arguments, it works as it should, but if you send any phrases for scoring, it crashes, as in the packed version.
So, ASPack is removed. What's next? And then, in fact, we have to deal with the bug that arises on some computers.
First of all, let's look at the standard Windows window reporting an application crash :

You should notice that the Exception Offset is 0x000591D4. We load the talk in OllyDbg, put the software break at this address (more precisely, at image base + 0x000591D4 - in my case it is 0x004591D4 ), press F9 and 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, leads to an application crash. With the two instructions above, it is noticeable that in ESI the value falls from the EAX register . The value of the EAX register has not been changed in this procedure before, so we look at the Call Stack from where we were called:

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

As you can see from the previous screenshot, the address 0x45EC5C also contains zero. We put the hardware breakpoint on the record at this address (right click -> Breakpoint -> Hardware, on write -> Dword), run the application again and find out that no recording at this address occurs. Let's analyze how the application behaves in case of successful work in another system. The write to the address 0x45EC5C in this case really happens:,

resulting in ESI + 38gives some meaningful meaning. Let's find out how we got to this place. Call Stack is empty at the time of this instruction, so I got 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 constantly crashes, it has reached the crack on access to ESI + 38 , where it crashed . The last line of code in the trace log was CALL at the address 0x004597C8 :,

while on the system where the application was working fine, after calling this procedure ( 0x004597C8) other instructions are executed, on one of which the hardware breakdown for writing to the address 0x45EC5C is triggered :

Maybe something goes wrong in the procedure 0x004597C8 . We put the software breaker on its call ( 0x0045AD5B ) and find that installing it leads to the fact that on the system where everything worked fine, the hardware breaker for writing to the address 0x45EC5C does not work now , as a result of which the application crashes, as in the case another system. With hardware breakdowns, everything is the same.
Experimentally, it was found that the installation of breaks on several previous 0x0045AD5Binstructions leads to the same result. It was possible to install the breakdown with the subsequent normal operation of the application only on this instruction:

Perhaps, by installing the breakdowns in these places, we interfered with the time measurements, which are just carried out using the GetTickCount function calls .
We start Trace into (Ctrl-F11), starting with this instruction, and we find that the differences in execution start here from this point:
; если приложение выполняется в системе, где оно падает
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 CMP EDX, DWORD PTR SS: [ESP + 4] instruction affects the further operation of the procedure. In the case of the system where it works, the flag Z of the flag register was set at the time this instruction was executed, while in the case of the system where the application crashed, this flag was not set:

Let's see what affects this. So what is going on here?
- GetTickCount function is called , the result of its call is written to the EAX register
- Resets the contents of the EDX register
- The contents of the registers EDX ( 0x0 ) and EAX (the result of the GetTickCount function ) are pushed onto the stack
- The value from the address 0x45EC98 is placed in the EAX register
- 0x64 is added to it.
- CDQ instruction executed
- And finally, the contents of the EDX register are compared with the value at ESP-4 , 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 such a large value? After all, the system turned off by me this morning. In fact, here hybrid boot and shutdown play a role in Windows 8. You can read more about this, for example, here .
But back to the main topic of our discussion. What is the problem then? Let's take a close look at the contents of general registers in this piece of code on both systems. You should be struck by the fact that after executing the CDQ instruction on a system where the application is working correctly, the registerEDX takes the value 0x0 , and in the system where the program crashes, 0xFFFFFFFF , which can be seen, for example, in the previous screenshot. Carefully read the description of the CDQ instruction :
Converts signed DWORD in EAX to a signed quad word in EDX: EAX by extending the high order bit of EAX throughout EDX
Now take a look at the signature of the GetTickCount function:
DWORD WINAPI GetTickCount(void);
As you can see, it returns a DWORD , which is “expanded” in an unsigned long, i.e. unsigned type.
I don’t know if the bug is a developer or a compiler, but the author clearly didn’t expect 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 bound is determined by the maximum value that can fit in a signed 4-byte variable - 2147483647:
2147483647/1000/60/60/24 = 24.8551348032 The
upper bound is determined by the GetTickCount function itself , as described in the documentation (in fact, it is determined by the maximum value, which can fit in an unsigned 4-byte variable):
The elapsed time is stored as a DWORD value. Therefore, the time will wrap around to zero if the system is run continuously for 49.7 days
Well, it's time to fix this assumption. One way to fix the problem is to somehow “replace” the GetTickCount function call with your code, in which we will “reduce” the value returned from it so that it fits into the signed 4-byte variable.
There are several options for solving this problem, but let's write code cave. We are looking for free space (the easiest way to find it is 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 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 ).
It is not possible to get the value of the EIP register directly - it cannot be accessed either for reading or writing.
Now we go to the place where JMP is implemented with the GetTickCount function

and replace the address where JMP jumps to with the address of the beginning of our code cave (in my case, it is 0x0045BAAA ):

We save the modified executable file (right-click on the CPU window -> Copy to executable -> All modifications -> Copy all -> right-click on the window that appears -> Save file) and check its operability. Yes, it really works, but when you start talking, now you get the familiar UAC window for almost all Windows users. Why exactly this is happening can be read, for example, here , and to solve the problem in our case, it’s enough to just 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 fix the problems, rather than hammer on them - it is quite possible that in the process of correcting another mistake you will discover something new. And I, in turn, hope again that the article was useful to someone.
I also express my deep gratitude to the person with the nickname tihiy_grom - without this article there simply would not have been.