4

I am currently attempting to reverse engineer a simple function from within a 16-Bit Windows 3.1 (NE) DLL, which from what I can tell is used to display a message box when required.

I would assume that the two arguments of the ShowMessageBox function are used to set the Title and Message (this is a DLL so I assume it doesn't bother with a HWND?). I can see that those two parameters get passed in and are pushed to the stack, but can't quite work out how they are passed into the system call.

Ghidra seems a little confused as the C source ignores the two parameters and passes in the two other values that are added into the stack (0x41 and 0x41b).

I have added the following function signature into the MessageBox system call:

int MessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, uint uType)

An example of the message box produced by this function is:

Message Box Example

Any suggestions would be greatly appreciated.

The assembly and Ghidra generated C source are included below.

**************************************************************
*                          FUNCTION                          *
**************************************************************
void __cdecl16far ShowMessageBox(undefined param_1,undefined param_2)
                       assume DS = 0x1008
             void              <VOID>         <RETURN>
             undefined         Stack[0x4]:1   param_1
             undefined         Stack[0x6]:1   param_2                                  
       1000:001c c8 00 00 00     ENTER      0x0,0x0
       1000:0020 57              PUSH       DI
       1000:0021 56              PUSH       SI
       1000:0022 6a 00           PUSH       0x0
       1000:0024 ff 76 08        PUSH       word ptr [BP + param_2]
       1000:0027 ff 76 06        PUSH       word ptr [BP + param_1]
       1000:002a 1e              PUSH       DS
       1000:002b 68 1b 04        PUSH       0x41b
       1000:002e 6a 41           PUSH       0x41
       1000:0030 9a 5c 00        CALLF      USER::MessageBox
                 18 10
       1000:0035 e9 00 00        JMP        LAB_1000_0038

void __cdecl16far ShowMessageBox(undefined param_1,undefined param_2)
{
  HWND unaff_CS;
  
  MessageBox(unaff_CS,(LPCTSTR)0x41,(LPCTSTR)0x41b,0x1008);
  return;
}

Thanks, James.

0

3 Answers 3

3

I've not looked at 16-bit windows in a while but I'd interpret the assembly as follows -

void ShowMessageBox( LPCSTR text )
{
    MessageBox( NULL,     // no owner window
                text,     // text to display in box
                DS:041B,  // caption
                MB_ICONINFORMATION | MB_OKCANCEL /* 0x41 */ );
    ...
}
7
  • Thanks, that seems accurate and lines up with the message box that is output by the DLL at runtime. Is there an easy way to get Ghidra to see it like this in the decompiled C source? Thanks.
    – jttri777
    Apr 7, 2023 at 19:16
  • 1
    Sorry, I've not used Ghidra much and not at all with 16-bit code. Perhaps telling it that param1 is an LPCTSTR might help ?
    – Ian Cook
    Apr 8, 2023 at 11:20
  • 2
    Iirc windows api are _stdcallfar16 not cdecl. You can edit the function signature of Messagebox. And redefine the arg 2 , arg 3 as segoffset that will make ghidra to either get the string if it is data section like 1068:0f2a. Or ghidra will concat the push ds push ax into a single argument
    – blabb
    Apr 9, 2023 at 5:37
  • 1
    added an answer take a look
    – blabb
    Apr 10, 2023 at 18:21
  • 1
    edited the answer to add concat of SEG:OFFSET
    – blabb
    Apr 10, 2023 at 18:58
3

A Typical listing in ghidra of USER!MESSAGEBOX in a 16 Bit NE Exe post analysis

   LAB_1000_07f0                        XREF[2]:     1000:0770(j), 1000:0775(j)  
   1000:07f0 ff 76 0e               PUSH       word ptr [BP + param_3]
   1000:07f3 1e                     PUSH       DS
   1000:07f4 68 c4 01               PUSH       0x1c4
   1000:07f7 1e                     PUSH       DS
   1000:07f8 68 c0 01               PUSH       0x1c0
   1000:07fb 6a 00                  PUSH       0x0
   1000:07fd 9a 9c 00 50 10         CALLF      USER::MESSAGEBOX     undefined MESSAGEBOX()

and its decompilation

uVar7 = param_3;
MESSAGEBOX((int)&DAT_1008_1050,0,0x1c0,(int)s_run-time_error_1008_1001 + 7,0x1c4,
           (int)s_run-time_error_1008_1001 + 7);

as you can see the MESSAGEBOX is undefined()

Right click on the MESSAGEBOX (in decompiled window or in listing) and edit the function signature as shown in screen shot and see the Decompiled output

enter image description here

that is far better but it still misses the Caption String
double click the 32bit addres 100801c0

you will see this an undefined ascii string (probably coz ghidra default str length is > 3)

   1008:01c0 53                     ??         53h    S
   1008:01c1 50                     ??         50h    P
   1008:01c2 59                     ??         59h    Y
   1008:01c3 00                     ??         00h

define it as a string using right click -> Data ->Terminatedcstring

to make the listing

   1008:01c0 53 50 59 00            ds         "SPY"

and the decompilation will be

  if ((local_8[0] < 1) || (0xfa < local_8[0])) {
    MESSAGEBOX(0,s_SPY_1008_01c0,s_Window_lines_should_be_between_1_1008_01c4,param_3);

ANOTHER EXAMPLE WHERE THE STRING IS DYNAMIC and cant be found statically MESSAGEBOX that shows the concat of seg:offset

listing and decompilation

   LAB_1020_2b78                   XREF[1]:     1020:2b6f(j)  
   1020:2b78 57                     PUSH       DI
   1020:2b79 ff 76 12               PUSH       word ptr [BP + param_7]
   1020:2b7c ff 76 10               PUSH       word ptr [BP + param_6]
   1020:2b7f ff 76 0e               PUSH       word ptr [BP + param_5]
   1020:2b82 ff 76 0c               PUSH       word ptr [BP + param_4]
   1020:2b85 ff 76 0a               PUSH       word ptr [BP + param_3]
   1020:2b88 9a 54 02 c0 1b         CALLF      USER::MESSAGEBOX  

   MESSAGEBOX(param_3,CONCAT22(param_5,param_4),CONCAT22(param_7,param_6),wVar1);
4
  • 1
    Thanks for that! That's a great help, and has got it perfect apart from the fact that the parameters are the wrong way around again. MessageBox(0x41,s_XSPY_1008_041b,param_1,0); It's got the caption parameter in there now though, and it's pulling in the text parameter from the outer function's argument.
    – jttri777
    Apr 10, 2023 at 19:05
  • 1
    the last pushed value is 0x41in your disasm so it would be the first argument i dont know why a hardcoded value is passed to hwnd maybe it is not stdcall but some esiteric custom storage ?
    – blabb
    Apr 11, 2023 at 7:12
  • 1
    Look up pascal calling convention it passes ltr (left to right) instead of rtl( right to lreft ) as in stdcalk btw if it is pascal the param from outer can be a variable message and caption a static string
    – blabb
    Apr 11, 2023 at 7:24
  • Yes that's it! I remember seeing some of the header files at some point for this application and it did use the Pascal calling convention. That would match up as the function always sets the window caption to "XSPY" which is what is contained within the static string, and the window message is variable, as the multiple possibilities are passed into this function throughout the program. I think that's it all about solved then! Thanks for all your help. From what I can gather, Ghidra doesn't directly support the pascal calling convention, so I'll have to do that manually. Thanks again.
    – jttri777
    Apr 11, 2023 at 15:47
2

As per the comment made by @blabb on his answer, I looked at the Win16 WINDOWS.H header file (from the Windows 3.1 Driver Development Kit (DDK)) to see which calling conventions were used for the Win16 API functions at the time.

At the beginning of the file, the following macro is defined:

#define WINAPI _far _pascal

This macro is then appended to the beginning of most of the functions in the file, for example:

int WINAPI MessageBox(HWND, LPCSTR, LPCSTR, UINT);

UINT WINAPI GetPrivateProfileInt(LPCSTR, LPCSTR, int, LPCSTR);

LPSTR WINAPI lstrcpy(LPSTR, LPCSTR);

So it seems as though most of the Win16 API functions were defined with the Pascal calling convention, which explains why I can see the parameters being PUSHed backwards onto the stack before the CALL instruction is utilised.

So by implementing the function this way:

Ghidra implementation of the WIN16 MessageBox function with the Pascal calling convention.

The parameters are pulled in correctly and the decompilation is accurate.

Ghidra decompilation of the WIN16 MessageBox function using the Pascal calling convention.

Currently, I'm having to setup the parameter storage manually as Ghidra does not support the Pascal calling convention, an issue on GitHub has been opened here: https://github.com/NationalSecurityAgency/ghidra/issues/496

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.