WinBox v3.43 ESR (bugfixes, improvements)

Version 3.

Download: winbox64_ESR_3.zip (755.3 KB)


  1. Fixed invisible selection in Message field of 'Log Entry' window. Relevant to RouterOS 7.20+, where this field has become multiline. The only problem is that it selects all text, when the window becomes focused after being unfocused. (may be they've "fixed" this problem by just hiding selection... :neutral_face:). Will try to fix it later in a normal way.

  2. Fixed filter value field was cleared when changing filter parameter.

  3. Added ability to change the order of user@host and identity in main window title. The new setting is available under 'Settings' menu in main window. It's saved on app close and stored in
    %AppData%\Roaming\MikroTik\WinBox\settings.dat, i. e. in the same place as original WinBox's settings.cfg.viw file. I had no wish to figure out in its format, so added own file. It will be used to store other settings in future releases. The file is saved on app close and only if a setting was modified.

  4. Fixed own bug that was causing an exception. It's not critical and unnoticable, because it was occuring only when the app was closed, but anyway, it wasn't good.


For advanced users:

Added new own .text, .rdata and .data sections to executable.

This solves many current and potential future problems:

  • Existing code cave at address 0x531460 is already not enough to store my own code, new .text section removes this size limitation and can be increased as needed
  • Better data organization - can store strings and other constants in new .rdata section, no need to store them in the code
  • Runtime data, like addresses and some values will be stored in new .data section
  • No risk to interfere with anything (like in the issue 4)
Log Entry selection fix

The multiline message field is a standard Windows' "EDIT" class, and it's created with CreateWindowEx WinAPI function. In original, its dwClass parameter is equal to 0x40200044, i. e. it consists of these values: WS_CHILD | WS_VSCROLL | ES_AUTOVSCROLL | ES_MULTILINE. It also adds other values, depending on some conditions, but I wasn't looking into that. The parameter is calculated at address 0x4445E2 while creating the object. I've just added ES_NOHIDESEL value (0x100) and it solved the problem. So, the whole fix is just changing one bit (0x40200044 -> 0x40200144).

Filter field clear fix

There are 2 possible field types in filters - TextEdit and ComboBox. When you add a new filter, it creates both objects and hides one of them, depending on selected filter parameter. For example, if you add a new firewall filter, you see 'Action' parameter by default and a ComboBox field to choose or type a value. If you then select 'Comment' parameter, it hides ComboBox and shows TextField. But in background both object do exist, the app only changes their visibility. So, to keep the typed value, it's required not only to intercept string removal, but also to copy it between TextEdit and ComboBox, when needed.
Everything happens in the function, that is located at address 0x4566C8. It's called, when you change a filter parameter. It clears the field value just before making it visible, i. e., when the field is hidden in background, it keeps the value. Attached original code with some comments. If selected parameter requires a ComboBox, it executes the code at address 0x45675F, if it requires TextField, it executes the code at address 0x45679E. In both cases rbx register is loaded with the address of a method, that clears the value and then executed in call rbx instruction.

Some info about objects:
TextField object contains a pointer to its string value address at offset [base+60h]
ComboBox consists of the same TextField object and the button. Inside ComboBox, a pointer to TextField object is located at offset [base+48h]

I've modified 2 instructions in the code above (marked as Inject1 and Inject2) so, that rbx is loaded with the addresses of my own code, that handles moving string values between TextField and ComboBox, when needed. Original values emptying code is not executed. Below is the code itself.

// Call ComboBox clear (from Inject1)
// Need to copy value from TextEdit to ComboBox

  mov rcx, [r12+50h]		// Load ComboBox object pointer
  call IWindow::isVisible

  test al, al				// If visible, then skip
  jnz @End

  mov rsi, [r12+48h]    	// Pointer to TextEdit object (source)
  mov rbx, [rsi+60h]		// String address in TextEdit object (source)

  mov rdi, [r12+50h]    	// Pointer to ComboBox object (destination)
  mov rdi, [rdi+48h]		// Pointer to TextEdit object of ComboBox
  lea rcx, [rdi+60h]		// Pointer to string address in TextEdit object of ComboBox (destination)
  cmp [rcx], Zero_Addr		// Check if string was zeroed before replacing
  je @SkipFree1

  call FreeStringMem		// sub_4CF0DA

@SkipFree1:
  mov [rdi+60h], rbx		// Move string address from TextEdit to ComboBox
  mov [rsi+60h], Zero_Addr	// Load zero string address to avoid app crash on objects destroy
  ret

// ---

// Call TextEdit clear (from Inject2)
// Need to copy value from ComboBox to TextEdit

  mov rcx, [r12+48h]		// Load TextEdit object pointer
  call IWindow::isVisible

  test al, al				// If visible, then skip
  jnz @End

  mov rsi, [r12+50h]    	// Pointer to ComboBox object (source)
  mov rsi, [rsi+48h]		// Pointer to TextEdit object of ComboBox
  mov rbx, [rsi+60h]		// String address in TextEdit object of ComboBox (source)

  mov rdi, [r12+48h]		// Pointer to TextEdit object (destination)
  lea rcx, [rdi+60h]		// Pointer to string address in TextEdit object (destination)
  cmp [rcx], Zero_Addr		// Check if string was zeroed before replacing
  je @SkipFree2

  call FreeStringMem		// sub_4CF0DA
  
@SkipFree2:
  mov [rdi+60h], rbx		// Move string address from ComboBox to TextEdit
  mov [rsi+60h], Zero_Addr	// Load zero string address to avoid app crash on objects destroy
@End:
  ret
Swap title order mod

Main menu for main window is created in sub_4B53CA. Injected redirection to my item adding code at address 0x4B6255.
New item adding code:

  mov eax, Proc_FreeString_Addr			// Replaced original instruction
  call rax

  mov rcx, [r12+50h]
  mov edx, $999
  mov rax, [rcx]
  call qword ptr [rax+28h]				// Add separator (sub_441540)

  mov r14, [r12+50h]
  mov edx, ItemCaption
  mov rcx, r13
  mov rax, [r14]
  mov rsi, [rax+18h]
  mov eax, Proc_ExtractStoredString_Addr
  call rax
  xor r9d, r9d
  mov r8, r13
  mov edx, SwapTitleOrderMenuItemID
  mov rcx, r14
  call rsi								// Add menu item
  mov rcx, r13
  mov eax, Proc_FreeString_Addr
  call rax

  mov rcx, [r12+50h]
  mov edx, SwapTitleOrderMenuItemID		// item ID
  mov r8d, data_TitleOrder_Addr			// Address of checked flag value in .data
  mov r8, [r8]
  and r8, 1
  mov rax, [rcx]
  call qword ptr [rax+38h]				// Set checked state (sub_4415DA)
  ret

Menu click handling for main window is performed in sub_4B41BA. Injected redirection to own item handling code at address 0x4B41F8.
New menu item click handling code:

  cmp dx, SwapTitleOrderMenuItemID - 64h		// DX = Menu item ID - 64h
  je @ProcessSwapTitle							// New menu item clicked

  cmp dx, 0Fh									// Original replaced code
  ja 0x4B42D9
  jmp 0x4B4202									// Some other item clicked, return back

@ProcessSwapTitle:
  xor byte ptr [data_TitleOrder_Addr], 1		// Invert value
  or qword ptr [data_ModifiedFlags_Addr], 1		// Flag as modified

  mov r8b, byte ptr [data_TitleOrder_Addr]		// Load checked state value

  mov rcx, [r12+50h]
  mov edx, SwapTitleOrderMenuItemID             // item ID
  mov rax, [rcx]
  call qword ptr [rax+38h]                      // Set checked state (sub_4415DA)

  mov rcx, r12
  mov eax, MakeTitleString_Addr                 // Update window title (sub_4B46E8)
  call rax

  jmp 0x4B42D9									// default case

To load/save menu item state, wrote 2 functions that read/write its state to settings.dat file.

Current file format:

Offset (bytes) Description Data Data size (bytes) Notes
0h Signature 0xAA555354 4
4h Title order 0 or 1 4 0 - standard order; 1 - swapped admin@host and Identity

The file is read on app initialization in sub_4B8994. Injected redirection to reading function at address 0x4BC758.
Reading code:

  mov eax, GetAppDataDirSub_Addr			// call GetAppDataDir (sub_4AEF59, replaced instruction)
  call rax

  mov rdx, r14								// r14 contains a pointer to the address of AppData path
  mov r8d, rdata_SettingsFileName_Addr

  mov ecx, data_FullSettingsFileName_Addr	// Address in .data to receive full file name
  mov eax, ConcatStringsSub_Addr
  call rax									// call ConcatStringsSub (sub_4B6C4C)

  mov ecx, [data_FullSettingsFileName_Addr]	// Full file name string
  add rcx, 4
  mov edx, GENERIC_READ
  mov r8d, FILE_SHARE_READ
  xor r9, r9
  mov dword ptr [rsp+20h], OPEN_EXISTING
  mov dword ptr [rsp+28h], FILE_ATTRIBUTE_NORMAL
  mov [rsp+30h], 0
  call CreateFileA

  mov FileHandle, rax
  cmp FileHandle, INVALID_HANDLE_VALUE
  je @KeepDefaultValue

@DoRead:
  mov rcx, FileHandle
  lea rdx, Buffer
  mov r8d, 8
  lea r9, BytesCount
  mov [rsp+20h], 0
  call ReadFile

  test eax, eax								// some read error
  jz @CloseAndLeaveDefaultValues

  cmp BytesCount, 8							// check read bytes
  jne @CloseAndLeaveDefaultValues

  mov rax, [Buffer]
  cmp eax, SettingsFileSignature			// Check signature
  jne @CloseAndLeaveDefaultValues

  ror rax, 32								// Swap halves
  mov edx, eax
  and edx, $FFFFFFFE						// Check if 0 or 1, otherwise incorrect value
  test edx, edx
  jnz @CloseAndLeaveDefaultValues

  mov ecx, data_TitleOrder_Addr				// Load the value read from the file into address in .data
  mov byte ptr [rcx], al

@CloseAndLeaveDefaultValues:
  mov rcx, FileHandle
  call CloseHandle (modify address) $C4

@KeepDefaultValue:
  ret

If the file doesn't exist or contains any error, default value is loaded (standard title order).


Writing to the file occurs on app close. Placed redirection to writing code right after return from the main application loop at address 0x42DAFC.
Writing code:

  cmp dword ptr [data_ModifiedFlags_Addr], 0	// Check if some parameter was modified
  jz @End

  mov ecx, [data_FullSettingsFileName_Addr]		// Full file name kept in .data after reading on app initialization
  add rcx, 4

  mov edx, GENERIC_WRITE
  xor r8, r8
  xor r9, r9
  mov dword ptr [rsp+20h], CREATE_ALWAYS
  mov dword ptr [rsp+28h], FILE_ATTRIBUTE_NORMAL
  mov [rsp+30h], 0
  call CreateFileA

  mov FileHandle, rax
  cmp FileHandle, INVALID_HANDLE_VALUE
  je @End

@DoWrite:
  mov dword ptr [Buffer+0], SettingsFileSignature
  mov ecx, [data_TitleOrder_Addr]
  mov dword ptr [Buffer+4], ecx					// Move title order value to buffer

  mov rcx, FileHandle
  lea rdx, Buffer
  mov r8d, 8
  lea r9, BytesCount
  mov [rsp+20h], 0
  call WriteFile

  mov rcx, FileHandle
  call CloseHandle

@End:
  mov ecx, data_FullSettingsFileName_Addr		// Full file name ptr
  call FreeStringMem							// sub_4CF0DA

  mov rcx, off_53DC00							// replaced original instruction
  ret
Exception bug

I've noticed, that the app was throwing an exception at address 0x4D3530 after closing. There is some cleanup happening in this place, and for some reason the cleaning code is reading the address of a code cave (0x531460) and it was looking for zero bytes, that were originally in this place. These zeros act as a condition to complete the cleanup loop. So, I just shifted all my code 8 bytes down and left first 8 zero bytes in a code cave to make cleanup code finish successfully.