Fix a bug without source codes

    image

    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

    image

    , 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:

    image

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

    image

    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:

    image

    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 :

    image

    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:

    image

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

    image

    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:

    image

    Do as we said, and observe the following picture:

    image

    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:

    image

    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:

    image

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

    image

    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:

    image

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

    image

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

    image

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

    image

    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 :

    image

    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:

    image

    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 :

    image

    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:

    image

    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:

    image

    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] :

    image

    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:,

    image

    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 :,

    image

    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 :

    image

    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:

    image

    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:

    image

    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 :

    image

    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

    image

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

    image

    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.

    Also popular now: