
Once, having decided to play something unusual, I turned my eyes on
MUDs - text-based computer multiplayer games with chat. You can play them either with the help of specialized clients written for specific servers, or via
telnet .
Choosing one of the currently existing servers (https://www.bat.org/), I armed myself with the default telnet client for Windows and ... I felt frustrated. No, it's not about the game at all, but about how telnet.exe interacts with this game. Sad to know, but none of the characters I entered (character name, various actions etc) were displayed on the console screen. Yes, the teams were sent by pressing the Enter key, but the absence of even a minimal interactive made such a game almost impossible (it was especially inconvenient to delete the characters entered earlier, because you had to count in your mind how many characters you had already deleted and on which you are currently).
')
Without thinking twice, I decided to try to connect to the same server using
putty and ...
Wow ! I see the characters I entered!
Why does echo not work in telnet.exe? Is there any way to fix this? Let's figure it out.
How was the process, and what came of it, read under the cut. Before reading this article, I also strongly recommend that you familiarize yourself with the
previous ones , since they have already explained many of the points outlined here.
The first step is to get the test subject. Install the telnet client (Win-R -> appwiz.cpl -> Turn Windows features on or off -> tick the box next to “Telnet Client” and click on the “OK” button) and copy the executable file telnet.exe from “% WINDIR% \ System32 "to any other directory.
The next step is to arm yourself with the necessary tools. Download
PE Tools and
OllyDbg , which I have already mentioned many times in
previous articles , and unpack them in any convenient directory.
Next, you need to understand whether ASLR technology is enabled for the binary that we are going to explore. Start PE Tools, press Alt-1, select telnet.exe and click on the “Optional Header” button:

Yes, ASLR is enabled. Let's turn it off - we replace 0x8140 with 0x8100 (why this has already been explained earlier - see, for example,
here ) and click on the “Ok” button.
So, what thoughts? The first thing that came to my mind is that the application can explicitly disable echo using the
SetConsoleMode WinAPI function. We launch our binary in OllyDbg, open the window with the list of intermodule calls and see that the calls of this function are really present in the application:

We set breakpoints on them, press F9 and stop at one of the breakpoints:

Let's look at the arguments in the stack window:

We read documentation:
ENABLE_ECHO_INPUT
0x0004
The characters are read by the ReadFile or ReadConsole. This mode can be used only if the ENABLE_LINE_INPUT mode is also enabled
Exactly what is needed! However, there is an easier way to do this - simply do not call this function:
ENABLE_WINDOW_INPUT are enabled by default when a console is created.
Let's restart debugging, close its call.

and check if echo works now. No, the result is the same as before - the entered characters are not displayed on the console screen.
Okay, let's wait for the moment when the game asks for a name.

, and press F12 (Pause) in OllyDbg.
I propose to look around to understand where we are at the moment. First, open the Call Stack by pressing Alt-K:

So, we hang somewhere in the depths of user32.dll. We jump to the closest “user” code (that is, the code that belongs to the telnet module) located at
0x0100D0D0 , from where we ended up in user32.dll:

An experienced Windows developer should already understand what address of the function is most likely in the
EDI register at the time of execution of the selected instruction -
GetMessage . But let's see for yourself. Set the bryak to this address, restart debugging and press F9 before getting to the required place:

As you can see, this is really
GetMessage . The problem in our case is that this function will not return control to the code that called it before pressing the Enter key, which means that it has nothing to do with echo.
Then let's take a look at what the other streams are doing at this moment (if they, of course, exist at all). Run the program again for further execution using F9, press F12 and open the “Threads” window (View -> Threads):

Open each of them in the CPU window (right-click on the corresponding line in the Threads window -> Open in CPU), except for the one highlighted in red (this is the current thread we just looked at), and look at their Call Stacks. Your attention should have attracted the flow with the following call stack:
ReadConsoleInput is a more interesting function in our case. We put the bryak on her call, restart debugging and ... We stop on it every time we transfer the focus to the telnet window:

Notice that there is a switch nearby, in which, most likely, a jump is made to the handler of the corresponding event. By running in the debugger, you can see that in case of a change of focus, control is transferred to the default case:

Judging by the code analysis carried out by OllyDbg, there are not so many options here - besides the default case, we also have cases 10 and 1, the first of which, after executing several instructions, jumps to the newly considered default case . Let's try to remove the breakpoint from the call to the
ReadConsoleInput function and put the breakpoint on case 1:

Restart debugging, wait for a message asking to enter a name, press '1' and dwell on this very case block:

What can we do now? And now you can verify the operation of telnet.exe in case of a connection to bat.org and, say, smtp.gmail.com, where, as I remember, echo worked correctly. Open the window “Run trace” (View -> Run trace), right-click on it, select the menu item called “Log to file”, select any file name and press Ctrl-F11 (Trace into). After performing the trace, close the file (right-click on the window “Run trace” -> Close log file) and do the same in the case of smtp.gmail.com:25 (if you explicitly specify the port, telnet needs to separate the IP address from it with a space character, i.e. the command should look like this - “telnet.exe smtp.gmail.com 25”).
A noticeable difference in behavior begins with the address
0x0100A2F9 :
In the case of bat.org Address Thread Command; Registers and comments
0100AB9F 00002EA0 JNZ telnet.0100AED2
0100ABA5 00002EA0 TEST BYTE PTR SS: [EBP-24], 3
0100ABA9 00002EA0 JE telnet.0100AED2
[...]
0100A2F7 00002EA0 TEST EAX, EAX
0100A2F9 00002EA0 JNZ SHORT telnet.0100A304
0100A2FB 00002EA0 TEST BYTE PTR DS: [1010740], 10
[...]
In the case of smtp.gmail.com Address Thread Command; Registers and comments
0100AB9F 00002EA0 JNZ telnet.0100AED2
0100ABA5 00002EA0 TEST BYTE PTR SS: [EBP-24], 3
0100ABA9 00002EA0 JE telnet.0100AED2
[...]
0100A2F7 000031D4 TEST EAX, EAX
0100A2F9 000031D4 JNZ SHORT telnet.0100A304
0100A304 000031D4 PUSH EDI; Arg4 = 01024CA0
[...]
In the case when telnet.exe communicates with bat.org, the jump to the address
0x0100A304 is not performed. Let's make the instruction at address
0x0100A2F9 an unconditional jump. Restart debugging, go to the “telnet” module, press Ctrl-G, enter the address
0x0100A2F9 in the window that
appears and press Enter. Now press the space bar and replace the
JNZ instruction with
JMP :

Press F9, enter '1' in the telnet window to the request to choose one of the options or enter a name and ... See the character entered by us:

If you run in the debugger, you can see that now we are in the code branch, where calls of such WinAPI functions, such as
SetConsoleCursorPosition and
WriteConsoleOutputCharacter, are made :

Why, then, did we not get here before? Let's see what made the decision to make the jump:

It depended on the result of the operation
TEST EAX, EAX , and the value of
EAX in the register fell from the address
0x01010754 , as seen in the previous screenshot. Well, let's try to understand why it was zero in the case of bat.org.
In order to find out, I propose to put a hardware breakpoint on the record at
0x01010754 . To jump on it, right-click on the instruction located at
0x0100A2BD -> Follow in Dump -> Memory address:

Right-click on the first byte at the specified address -> Breakpoint -> Hardware, on write -> Dword. Restart debugging and find the last access to the address
0x01010754 , when it gets zero. Such an appeal is here:

If we look at the Call Stack and skip to the procedure from which we were called here, we will see the call to the recv function and then parse the incoming data:

Note the constant 0xFF. According to
the telnet
specification , then this byte is followed by the commands used in this protocol:
The following are the defined TELNET commands. Note that these codes
immediately
preceded by an IAC.
NAME CODE MEANING
SE 240 End of subnegotiation parameters.
NOP 241 No operation.
Data Mark 242 The Synch.
This should always be accompanied
by a TCP Urgent notification.
Break 243 NVT character BRK.
Interrupt Process 244 The function IP.
Abort output 245 The function AO.
Are You There 246 The function AYT.
Erase character 247 The function EC.
Erase Line 248 The function EL.
Go ahead 249 The GA signal.
SB 250 Indicates that what follows is
subnegotiation of the indicated
option.
WILL (option code) 251 Indicates the desire to begin
performing, or confirmation that
you are now performing, the
indicated option.
WON'T (option code) 252 Indicates the refusal to perform,
or continue performing, the
indicated option.
DO (option code) 253 Indicates the request that the
other party perform, or
confirmation that you are expecting
to the party
indicated option.
DON'T (option code) 254 Indicates the demand that the
other party stop performing,
or confirmation that you are no
longer expecting the other party
to perform, the indicated option.
IAC 255 Data Byte 255.
Looking at the stack, you can see that we are confronted with the byte sequence 0xFF 0xF9, which stands for a command called “Go ahead”. About her on the Microsoft website
reported the following:
The original Telnet implementation defaulted to half duplex operation. It can be used as a guideline. "This is what the radio has adopted."
For some reason, in the implementation of the telnet client from Microsoft, this command affects echo without returning the contents at
0x01010754 to a
nonzero value.
You can verify this by writing a small server in Python:
import socket, threading s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', 1900)) s.listen(1) class daemon(threading.Thread): def __init__(self, (socket, address)): threading.Thread.__init__(self) self.socket = socket self.address = address def run(self): self.socket.send('Greetings!') while True: data = self.socket.recv(1024) if data[0] == '1': data = 'Response' elif data[0] == '2': data = bytearray() data.append(0xFF) data.append(0xF9) self.socket.send(data); self.socket.close() while True: daemon(s.accept()).start()
If you start this server and connect to it using the “telnet.exe 127.0.0.1 1900” command, then we will be able to observe that echo will work exactly until we receive an answer to our command '2':
Greetings! 1Response1Response1Response1Response2ResponseResponseResponseResponseResponseResponse
But that is not all! In fact, other teams also have similar behavior. For example, the byte sequence 0xFF 0xF1, denoting “No operation,” absolutely also “disables” echo in a telnet client.
Bug Feature? Who knows. The main thing is that now we have taught our telnet.exe to the correct game of MUDs!
Afterword
Of course, the solution is not perfect yet. For example, when you press the Backspace key, the character in front of the cursor is not deleted (however, the “internal” view of the command entered by the user changes, as expected). Yes, this is just a cosmetic moment and you can put up with it, but we started this article with cosmetic inconveniences, right?
Thank you for your attention, and again I hope that the article was useful to someone.