06 Oct 2017, by torusrxxx
A year ago I wrote about architecture of x64dbg. Basically, I think it is of good quality for a software with extremely complex requirements, although there’s some legacy. For example, if you have experience with other debuggers, you might wonder why label view in x64dbg doesn’t update automatically, and doesn’t support deleting all labels. Today I will talk about limitations of x64dbg, and possible future enhancements. If you are going to contribute a feature which is listed below, be prepared for additional complexity you might not thought of before.
Export functions
Adding a feature that interacts with DBG usually means exposing one or more features across the Bridge. This usually means modifications to all three components are necessary, and also less stability of API. Reading some information from DBG is also more difficult. This is why script commands are most preferred and stable interface between DBG and GUI. The original design of using Bridge to separate DBG and GUI, now seems a bit obsolete. The fact that debug thread and GUI thread are different also means certain tasks, such as saving a copy of current dump to compare it later with new dump content, to highlight the changed bytes in red, now requires thread synchronization and is thus harder to code than in single-threaded model.
Access to features
As mentioned earlier, x64dbg doesn’t support many features in references view. It is because, unlike other views, references view is multi-purposed. The references view can be used as labels view, search result view, or variables view. Because the code in references view doesn’t have the code to do specific task in given context, tasks such as updating automatically can’t be done.
A more general problem exists in other views. In info box below disassembly view, you can see many features are missing, for example, follow in specified dump window. Although the code to accomplish this task already exists, info box has not been updated to use it.
The problem of mixing code for features and container views in the same class has resulted in much code duplication and lack of features in certain views. To solve the problem, x64dbg is moving feature menus into dedicated class files. BreakpointMenu class is the first successful attempt that not only simplifies code, but also brings hardware breakpoint features to other views.
A proposed enhancement to references view is to let code in references view have more insight of current context. I added GuiReferenceAddCommand function to references view. First tried in main window, this function can add a context menu in references view to execute a common task, such as deleting labels. If references view has further information about its context, more tasks can certainly be done. If this information is made visible to all other views in a standard interface, it can be possible to implement tasks that require interaction of references view in other views, such as go to next match. Of course, this will indicate a new enhancement in x64dbg architecture and therefore must be considered carefully.
Some users are complaining about why copying a table doesn’t copy all content, but only visible part. Some users are disappointed by the fact that dump view doesn’t support UTF-16 very well. And perhaps most users have noticed the inability of x64dbg to set a memory breakpoint with specified size. In fact, these operations are somewhat expensive. For example, a memory breakpoint usually decreases stepping performance significantly. x64dbg sets a very high standard for performance, and in order to achieve that, some features are not implemented yet, or disabled and only should be used when necessary.
In my opinion, “performance” not only refers to software performance, but also means the ability for users to complete a task in shorter time. Therefore, expensive operations such as memory breakpoints and Unicode dump, will eventually be implemented if we find a way to implement it efficiently. If an operation is slow and will not help most users, that feature is likely to be left out. Setting a high standard for performance is not an actual limitation in x64dbg, but it does mean x64dbg will have less features than you might expect.
Comments
07 Jul 2017, by ThunderCls
Introduction
Have you ever been trying to reverse a specific function in an application, but cannot really find it? For instance, let us assume you want to locate the code that is being called right after a click on a button or a keystroke. With certain applications (Delphi, CBuilder, Visual Basic, etc) it is as easy as dropping the executable inside a decompiler and locating the corresponding events/addresses in a matter of seconds. Sometimes it is not that easy, whether a packer or anti-decompiler technique is involved, or just for the simple reason that the application is not an event-driven one. What can you do in that case to obtain those addresses in a similar approach with the least effort?
Using Windows Messages
Let us take a look at a sample crackme for demonstration purposes. In this case we have a simple executable coded in Visual C++:
If we try to enter a text and click on the Check!
button nothing is happening, not a text message, no nothing. At this point we could get creative and start looking for other alternatives to locate the exact location where our serial is processed and yes, we would probably succeed, but what if I tell you that there is an easier way for us to land just after the press of that button? Just like we would with any Delphi, Visual Basic or any other event-driven language? Let us find out how it works.
After loading and executing the file in x64dbg, we go and enter some text and just before pressing the Check!
button we go to the Handles
tab and refresh the view to obtain a window list of the debuggee. We can then see the button there so we right click over it and select the option to set a message breakpoint on it:
Now we are given a window with some options to choose from in order to set a specific message breakpoint in our button control. In this case the configuration I am going to use is something like this:
We are specifying that the execution is stopping as soon as a WM_LBUTTONUP
(left mouse click is up) message is sent to our button control. Right after our breakpoint is set we click the button in the crackme and soon after that we step in our breakpoint.
At this point we achieved what we wanted. We just stopped the execution right after the button click, on the other hand we are in user32.dll
and our purpose is to be in the main module code. Getting there is as simple as just using a breakpoint in the code section of our executable. You can also use the Run to user code
option (Ctrl+F9).
When trying to resume the execution, the debugger is going to stop the execution once again, but this time right in the middle of the code we were looking for. In this case in the DLGPROC
function (the callback in charge of processing the messages being sent to every window control in the main window dialog).
Event-driven programming
If you are a programmer, or have been in contact with programming languages in general and coding tasks, you should know the concept of the so called event-driven programming. Event-driven programming is a programming paradigm in which the stream of a program execution is dictated by events; a user action, a mouse click, a key press, etc. An event-driven application is intended to identify events as they happen, and afterwards manage them, utilizing a suitable event-handling procedure. Some programming languages are particularly intended to encourage event-driven programming, and give an IDE that halfway computerizes the generation of code, and gives an extensive choice of inherent built-in objects and controls, Visual Basic, Borland Delphi/CBuilder, C#, Java, etc are some of these types of languages. (1)
Window Messages
Even if a programmer is not using one of the above languages or even if they are using them in a non event-driven manner, Microsoft Windows applications are event-driven by nature, which means that you are going to be dealing with window messages anyway. According to the MSDN:
The system passes all input for an application to the various windows in the application. Each window has a function, called a window procedure, that the system calls whenever it has input for the window. The window procedure processes the input and returns control to the system.
The system sends a message to a window procedure with a set of four parameters: a window handle, a message identifier, and two values called message parameters. The window handle identifies the window for which the message is intended. The system uses it to determine which window procedure should receive the message.
A message identifier is a named constant that identifies the purpose of a message. When a window procedure receives a message, it uses a message identifier to determine how to process the message. (2)
Window Procedures
According to the previous information, in order to intercept window messages for a certain window, we need to first locate the window procedure of the desired “control”. To so do from the application that contains the window procedure is quite easy, it can be located by using the following functions:
LONG WINAPI GetWindowLong(
_In_ HWND hWnd,
_In_ int nIndex
);
DWORD WINAPI GetClassLong(
_In_ HWND hWnd,
_In_ int nIndex
);
nIndex = GWL_WNDPROC
: Retrieves the address of the window procedure, or a handle representing the address of the window procedure. You must use the CallWindowProc
function to call the window procedure.
BOOL WINAPI GetClassInfo(
_In_opt_ HINSTANCE hInstance,
_In_ LPCTSTR lpClassName,
_Out_ LPWNDCLASS lpWndClass
);
BOOL WINAPI GetClassInfoEx(
_In_opt_ HINSTANCE hinst,
_In_ LPCTSTR lpszClass,
_Out_ LPWNDCLASSEX lpwcx
);
typedef struct tagWNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
...
} WNDCLASS, *PWNDCLASS;
lpfnWndProc
: A pointer to the window procedure.
At this point everything looks very straightforward, but nonetheless, there is one limitation imposed by our OS: Microsoft Windows will not let you get this information from an external application (such as a debugger). If you want to get the window procedure address of a given window or control that is owned by another process using one of the above functions, you will end up with an ACCESS_VIOLATION
exception. In our case x64dbg will be no different and hence none of the previous functions will work properly…well…yes and no. Here comes the workaround used by x64dbg to get the correct window procedure address.
Getting External Window Procedures
At this point it is not clear why this behavior occurs and whether it is OS bug. The thing is, that it can be used in all previous Windows versions. The important function here is:
DWORD WINAPI GetClassLong(
_In_ HWND hWnd,
_In_ int nIndex
);
The hack relies on testing for the given window’s charset before calling the correct function version of GetClassLong
(ANSI/UNICODE) accordingly. The code used by x64dbg is something as simple as this:
duint wndProc;
if(IsWindowUnicode(hWnd))
wndProc = GetClassLongPtrW(hWnd, GCLP_WNDPROC);
else
wndProc = GetClassLongPtrA(hWnd, GCLP_WNDPROC);
To write code that is compatible with both 32-bit and 64-bit versions of Windows, you have to use GetClassLongPtr
. When compiling for 32-bit Windows, GetClassLongPtr
is defined as a call to the usual GetClassLong
function.
Intercepting Messages
Now that the window procedure is located, any message could be intercepted with a proper conditional expression, but before that, let us check the logic behind this. The structure being processed each time by the window procedure looks like this:
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, *LPMSG;
As we can see the structure give us some useful information at this point, most importantly hwnd
and message
. According to these fields we could know to which specific control what message is being sent to. Before going any further let us see an example for an specific message (WM_LBUTTONUP
) being sent to a given Button
control.
After clicking the OK button we step on the breakpoint and when we inspect the stack arguments we can see something like this
The first as can be seen is the handle corresponding to our Button
control and the second corresponding to the message WM_LBUTTONUP
(0x202).
WinProc Conditional Breakpoints
The last thing to get this feature fully working is the possibility to pause the application only when specifics handles and messages are in play. As you can read in the help, x64dbg integrates a very nice and powerful set of expressions to allow this. As shown in the above picture there are three options involved:
- Break on any window: Using this option we stop on the given message regardless the window handle. For this we need the simplest expression:
bpcnd WINPROC, "arg.get(1) == MESSAGE"
- Break on current window only: This feature will add an additional condition to the expression in order to stop the execution only when the handle of the specific window is involved, the expression in this case would be:
bpcnd WINPROC, "arg.get(1) == MESSAGE && arg.get(0) == HANDLE"
- Use TranslateMessage: Sometimes the winproc technique will not give the expected results so this other feature goes out of the scope of the previous technique as it relies in the TranslateMessage API to intercept messages and not in the window procedures themselves. Althought the logic is more or less the same.
BOOL WINAPI TranslateMessage(
_In_ const MSG *lpMsg
);
As seen the function uses the same MSG
structure that we saw before, hence the functioning with the expressions will be more of the same with some minor changes depending on the OS platform:
ifdef _WIN64
bpcnd TranslateMessage, "4:[arg.get(0)+8] == MESSAGE"
bpcnd TranslateMessage, "4:[arg.get(0)+8] == MESSAGE && 4:[arg.get(0)] == HANDLE"
#else //x86
bpcnd TranslateMessage, "[arg.get(0)+4] == MESSAGE"
bpcnd TranslateMessage, "[arg.get(0)+4] == MESSAGE && [arg.get(0)] == HANDLE"
#endif //_WIN64
Use Cases
As seen in this post, this is a very convenient and strong feature in x64dbg and can be used in numerous scenarios. Having the possibility to control on which events to pause a debuggee, even if it is not and event-driven application like a Delphi or Visual Basic, open the doors and give the reverser even more resources to debug. If you want to pause the execution when entering a char in an Edit control in a MASM application just set a messages breakpoint on the control itself with the message WM_KEYUP, simple as that. Same goes for Button clicks, showing windows, etc. There are a whole bunch of messages options to choose from.
Final Words
With these lines I tried to give an in-depth view of the messages breakpoints feature and some of the multiple scenarios where and how to use it. And this is all for this post, see ya around.
ThunderCls signing out
References
- https://www.technologyuk.net/computing/software-development/event-driven-programming.shtml
- https://msdn.microsoft.com/en-us/library/windows/desktop/ms644927(v=vs.85).asp
Comments
08 Jun 2017, by mrexodia
Recently, a friend of mine asked me to look into a packed kernel driver. I decided to take a stab at it and it turned out to be quite an interesting experience!
Tools required:
Stop reading now if you would like to try this yourself as a challenge. You can find hashes of two samples I found in the bottom of this post.
Initial analysis
Checking the file with CFF Explorer shows us some basic things. The file is a 64-bit native executable (driver) with a bunch of imports from fltmgr.sys. And only one import from ntoskrnl.exe
(MmIsAddressValid). This is already suspicious, because even a very small driver like beep.sys
already has 25 imports from ntoskrnl.exe
.
In order to be able to open this file with x64dbg, we have to make some changes to the PE header. Go to Optional Header
and change the Subsystem
(near the bottom) from Native
to Windows GUI
. This is the first step in making Windows load this driver as a user-mode executable. After saving the file as aksdf.exe
and loading it in x64dbg you will be greeted with a nice error message:
The reason for this is that the loader will try loading ntoskrnl.exe
and/or fltmgr.sys
in the executable address space, but since these are native executables it does not work well. In addition to this, some of the PE directory structures appear to be “corrupted” (for user-mode at least), but this is a topic for another time.
Faking the kernel imports
To fake the ntoskrnl.exe
and fltmgr.sys
exports I wrote a small tool in C#. It expects a module name and a CFF Explorer export table (Ctrl+A, Ctrl+C) as input:
using System;
using System.Collections.Generic;
using System.IO;
using System.Globalization;
namespace faker
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Usage: faker libname exports.txt");
return;
}
var def = new List<string>();
def.Add(string.Format("LIBRARY \"{0}\"", args[0]));
def.Add("EXPORTS");
var fake = new List<string>();
fake.Add("#define FAKE(x) void* x() { return #x; }");
foreach (var line in File.ReadAllLines(args[1]))
{
var split = line.Split(' ');
var ord = int.Parse(
split[0].TrimStart('0'),
NumberStyles.HexNumber);
var name = split[split.Length - 1];
if (name == "N/A")
{
def.Add(string.Format("noname{0} @{0} NONAME", ord));
fake.Add(string.Format("FAKE(noname{0})", ord));
}
else
{
def.Add(string.Format("{0}={0}_FAKE @{1}", name, ord));
fake.Add(string.Format("FAKE({0}_FAKE)", name));
}
}
def.Add("");
File.WriteAllLines(args[0] + ".def", def);
File.WriteAllLines(args[0] + ".cpp", fake);
}
}
}
After running this tool, you will get fltmgr.cpp and fltmgr.def, which can be added to an empty DLL in Visual Studio and then compiled to a DLL with fake exports, which perfectly match the ones from your desired driver. You can find the complete source code here, the relevant binaries can be found in the releases.
As a final step, extract the fake ntoskrnl.exe
and fltmgr.sys
to the same directory as aksdf.exe
. Loading the file in x64dbg and running to the entry point should look like this:
I got a tweet that linked to an alternative library (with more emulated functions) that you can use.
Unpacking
When stepping around you’ll see that the code is quite unreadable. There are many opaque predicates and branches inside other instructions. You can slightly improve the readability by manually pressing B
(Right Click -> Analysis -> Treat from selection as -> Byte
), marking the irrelevant bytes as data, but I would not recommend this approach since tracing is a much simpler option.
When stepping around a little bit, it can be observed that the MmIsAddressValid
function address (that suspicious one) is pushed on the stack:
23F02 lea rax,qword ptr ds:[<&MmIsAddressValid>]
23F09 push qword ptr ds:[rax]
A bit more stepping after that, you can see that this instruction throws an exception:
The exception code is EXCEPTION_PRIV_INSTRUCTION
, which makes sense because the driver is loaded in user mode. The value moved into dr7
is 0x400
(bit 10), which (I believe) clears any possible kernel-mode hardware breakpoints. Because we are not debugging in kernel mode, we add an exception breakpoint to automatically skip the instruction that throws this exception:
Then edit the breakpoint and set the following fields:
Now restart and when you reach the entry point, enable the trace record (Right Click -> Trace record -> Word
). Also bind a hotkey (I use Ctrl+/
) to the Debug -> Trace into beyond trace record
option:
When done correctly, pressing Ctrl+/
will allow you to step only to places you’ve never seen before. This can be very useful when stepping in obfuscated code, because the branches can get very confusing and the trace record can help you understand what pieces of code you have already seen:
After a while of pressing Ctrl+/
, the traces will take longer and longer to complete and you should land on a ret
instruction somewhere. After stepping over the ret
, press G
and then O
to see the graph overview:
In the graph, the nodes are colored differently depending on their contents. Red nodes are so-called terminal nodes. These usually end in a ret
or jmp reg
. The blue nodes are nodes that contain indirect calls. In this case:
24044 call qword ptr ds:[r14]
Put a hardware breakpoint on both these calls and let the program run. You will see that the function called is MmIsAddressValid
, which (obviously) checks if an address is valid. To continue we have to actually implement this function in the fake ntoskrnl.exe
:
#include <windows.h>
#pragma optimize("", off)
BOOL MmIsAddressValid_FAKE(LPCVOID addr)
{
__try
{
auto x = *(char*)addr;
return TRUE;
}
__except(EXCEPTION_ACCESS_VIOLATION)
{
return FALSE;
}
}
#pragma optimize("", on)
Restart the executable and run again to get to the same location (the hardware breakpoints should be saved in the database so you do not have to set them again). After stepping over the MmIsAddressValid
call and stepping some more, some interesting code starts to emerge (slightly deobfuscated by me):
@again:
24037 sub r15,1000
2403E mov rcx,r15
24044 call qword ptr ds:[r14] ; 'MmIsAddressValid'
2404A or al,al
2404C je @again
24060 mov dx,5A4D ; 'MZ'
24064 mov rax,r15
24067 cmp dx,word ptr ds:[rax]
24074 jne @again
This code is scanning for the beginning of the PE header. After it finds a header, it will check for the "PE"
signature (MmIsAddressValid
is used again before reading the signature). Keep stepping until you reach a call
.
As it turns out, this call (I named it resolveImport
) is used to resolve a single import from ntoskrnl.exe
. The function walks the exports of the given module and calls a function I called hashExportName
on all of them. If the name hash matches the required hash, the virtual address of the export is returned.
I leave it up to you to find your way out of the import resolving loop. Roughly what I did was look at the last import resolved and put a hardware breakpoint on write on this location. This should get you to the last iteration of the loop. A bit more stepping should show you a lot of pop
instructions (to restore the original registers) and eventually you will land on the original entry point:
22100 mov qword ptr ss:[rsp+10],rdx
22105 mov qword ptr ss:[rsp+8],rcx
2210A sub rsp,C8
22111 mov byte ptr ss:[rsp+40],0
22116 mov qword ptr ss:[rsp+48],0
2211F mov rax,qword ptr ss:[rsp+D0]
22127 mov qword ptr ds:[201C0],rax
2212E call aksdf.10C00
Finding a faster way of unpacking
Because we now know the original entry point, it is possible to find faster ways of unpacking this executable. Take a look at the entry point for example:
23E5E lea rax,qword ptr ds:[22100] ; loads the address of oep in rax
Simply putting a hardware breakpoint at 22100
will get you to the original entry point. Another method is the famous trick of putting a hardware breakpoint on [rsp]
after a bunch of registers have been pushed:
Dumping + Rebuilding
Because this executable is aligned funny (0x80
), most dumper tools (including Scylla) will not do a good job of dumping this executable. I only managed to get CHimpREC working.
Before we can dump this executable, there are two problems to fix:
-
The jumps at 1E200
do not point to anything at all:
-
The imports are somewhat scattered (RtlInitUnicodeString
at 10EE8
vs PsSetCreateProcessNotifyRoutine
at 225C5
).
Fixing the first problem turns out to be actually quite easy. When I checked in CFF Explorer it looks like 1E8000
is actually the starting address for the fltmgr.sys
IAT. Apparently the Windows loader does not expect this kind of format (alignment again?) for usermode programs and silently fails loading the import table by itself.
Some copy pasta from the CFF Explorer export table and a bit of regex produces a simple x64dbg script that you can use to put the correct addresses in place. Just make sure to update x64dbg, because the loadlib
command proved to be broken…
loadlib fltmgr.sys
base=aksdf:$E800
i=0
[base+i*8]=fltmgr:FltCloseClientPort;i++
[base+i*8]=fltmgr:FltReleaseContext;i++
[base+i*8]=fltmgr:FltSetVolumeContext;i++
[base+i*8]=fltmgr:FltGetDiskDeviceObject;i++
[base+i*8]=fltmgr:FltGetVolumeProperties;i++
[base+i*8]=fltmgr:FltAllocateContext;i++
[base+i*8]=fltmgr:FltStartFiltering;i++
[base+i*8]=fltmgr:FltFreeSecurityDescriptor;i++
[base+i*8]=fltmgr:FltCreateCommunicationPort;i++
[base+i*8]=fltmgr:FltBuildDefaultSecurityDescriptor;i++
[base+i*8]=fltmgr:FltUnregisterFilter;i++
[base+i*8]=fltmgr:FltRegisterFilter;i++
[base+i*8]=fltmgr:FltObjectDereference;i++
[base+i*8]=fltmgr:FltCloseCommunicationPort;i++
[base+i*8]=fltmgr:FltGetVolumeFromName;i++
[base+i*8]=fltmgr:FltClose;i++
[base+i*8]=fltmgr:FltFlushBuffers;i++
[base+i*8]=fltmgr:FltQueryInformationFile;i++
[base+i*8]=fltmgr:FltCreateFileEx;i++
[base+i*8]=fltmgr:FltParseFileName;i++
[base+i*8]=fltmgr:FltReleaseFileNameInformation;i++
[base+i*8]=fltmgr:FltGetFileNameInformation;i++
[base+i*8]=fltmgr:FltSetCallbackDataDirty;i++
[base+i*8]=fltmgr:FltSetInformationFile;i++
[base+i*8]=fltmgr:FltSendMessage;i++
[base+i*8]=fltmgr:FltGetBottomInstance;i++
[base+i*8]=fltmgr:FltFreePoolAlignedWithTag;i++
[base+i*8]=fltmgr:FltDoCompletionProcessingWhenSafe;i++
[base+i*8]=fltmgr:FltReadFile;i++
[base+i*8]=fltmgr:FltGetRequestorProcess;i++
[base+i*8]=fltmgr:FltLockUserBuffer;i++
[base+i*8]=fltmgr:FltAllocatePoolAlignedWithTag;i++
[base+i*8]=fltmgr:FltGetVolumeContext;i++
[base+i*8]=fltmgr:FltGetFilterFromInstance;i++
[base+i*8]=fltmgr:FltGetVolumeFromInstance;i++
[base+i*8]=fltmgr:FltWriteFile;i++
[base+i*8]=fltmgr:FltGetTopInstance;i++
[base+i*8]=fltmgr:FltIsOperationSynchronous;i++
[base+i*8]=fltmgr:FltFsControlFile;i++
[base+i*8]=fltmgr:FltCompletePendedPreOperation;i++
[base+i*8]=fltmgr:FltCancelIo;i++
[base+i*8]=fltmgr:FltSetCancelCompletion;i++
[base+i*8]=fltmgr:FltClearCancelCompletion;i++
[base+i*8]=fltmgr:FltParseFileNameInformation;i++
[base+i*8]=fltmgr:FltGetVolumeFromFileObject;i++
ret
After running those jumps look fine:
The second problem is also easy to fix, thanks to SmilingWolf and his nice tool called WannabeUIF. This tool allows you to rebase an import table. Just enter the start/end of the code and the new IAT address and it will do the work for you:
Once this is done, you can use CHimpREC to dump and fix the executable. Just make sure to check the Rebuild Original FT
option:
Opening the executable in x64dbg should now directly take you to the entry point. Obviously you cannot do much from here because it is usermode, but changing the Subsystem
back to Native
and opening the file in IDA should allow you to do further analysis. You might even be able to run the driver in testsigning mode if you re-sign it with your own certificate, but I did not try this myself.
Conclusion
I hope this blog post has been educational and entertaining. I definitely had fun unpacking and restoring the driver, even though the process was obviously not as straightforward from the start.
You can find the aksdf.exe
database (File -> Import database
) here. The import resolving routine and hashing routines have been worked a little, to show you a better picture of the code. It also has a few comments and labels to help you navigate the code better.
Hope to see you again soon!
Hashes (sample used here)
MD5: 3190c577746303ca4c65114441192fe2
SHA1: e97cd85c0ef125dd666315ea14d6c1b47d97f938
SHA256: aee970d59e9fb314b559cf0c41dd2cd3c9c9b5dd060a339368000f975f4cd389
VirusTotal, Hybrid-Analysis.
Hashes (another sample)
MD5: db262badd56d97652d5e726b7c2ed9df
SHA1: 31a4910427f062c4641090b3721382fc7cf88648
SHA256: 55bb0857c9f5bbd47ddc598ba67f276eb264f1fe225a06c6546bf1556ddf60d4
VirusTotal, Hybrid-Analysis.
Comments