Background: There’s this sneaky trojan component called KuandaListener
lurking in the machine.
Mission: Dive into captured logs and figure out what the trojan is up to.
For all the marbles: Find anything that can help.
I almost fell off the wagon of not completing missions… not this time.
We do not have a lot to go on expect the logs.
KuandaLogs
| take 50
KuandaLogs
| summarize count() by substring(Message, 0, 10)
A sample sending an..
message looks like: Sending an encrypted message, will use Dekrypt(@'mbNYouohoQThoQKpoQNeou5eobUqoQUYoQUYozJ\N[hO9m0T9nZbVPjvVPN5ciR4\PjqVm159mX5VPjDMENqsgX51vO3MgY5\vp5MPRb\HZhFm\D9PlCNWAOcgZbVPjvVPjbNBl3N[zSlHZYciAuVPSbMEjbNBl3NWzvMvCTNBlkVPSCNBS0cElCMnZk9mI4cD[=', strcat_array(<active-user-encryption-tokens>, '')) for decoding.
Looking at the logs of a particular compromised user, you learn the token is sent in an operation message and is active until the operation is completed or the session is reset.
Can we find the active-user-encryption-tokens
?
.set-or-replace LogsEx <|
KuandaLogs
| where Message has "Sending an"
| distinct DetectiveId
| lookup kind=inner KuandaLogs on DetectiveId
| parse Message with "Operation id=" OpId:string " " *
| parse Message with * "token: '" Token:string "'." // parse opID and token from the message string
| extend Ev = iff(
Message has_all ("Operation", "completed"), "OpCompleted", iff(
Message has_all("Operation", "started"), "OpStarted", iff(
Message has "User session reset", "SessionReset", iff(
Message has "entered", "SessionStart", iff(
Message has "Sending", "MessageSent", "Unknown"))))) // create new column for events
| project DetectiveId, Timestamp, Ev, OpId, Token, Message
| partition hint.strategy=native by DetectiveId
(
order by Timestamp asc );
The task is to find active tokens that are used to send the encrypted messages.
LogsEx
| partition hint.strategy=native by DetectiveId
(
order by Timestamp asc
| scan declare(Active:string) with (
step start output=none: Ev has_any("SessionStart", "SessionReset") => Active="";
step ops output=none: Ev has_any ("OpStarted", "OpCompleted") => Active = iff(
Ev == "OpStarted", strcat(ops.Active, " ", OpId), replace_string(ops.Active, OpId,""));
step send: Ev has "MessageSent" => Active=ops.Active;
)
)
| project Timestamp, DetectiveId, OpId=Active, Message
...
Here, you are gathering operations (by Id) that would be active when the encrypted message is sent.
Now, grab the tokens corresponding for each message from the active operations.
...
| mv-expand split(OpId, " ") to typeof(string)
| join kind=inner (LogsEx | where Message has "token: '") on OpId
| sort by DetectiveId, Timestamp1 asc, Message, Timestamp
| summarize Key=make_list(Token) by DetectiveId, Timestamp, Message
| parse Message with "Sending an encrypted message, will use Dekrypt(@'" Encrypted "'," *
| project Message=Encrypted, Key=strcat_array(Key, "")
...
Then, decryption!
...
| invoke Dekrypt()
| where Result !has "Packing" and Result !has "Kuanda" // filter for interestingness
[2023-09-22T03:48:30.0000000Z] TODO [BUGBUG]: Validate: bitset_count_ones(hash_many('kvc2f916b75d21076bc100', tostring($user_answer))) < 54! Leaving as-is for now, the chance it will actually happen is very low. (O boy, these non-AMD processors are literally melting down on invalid instruction sets!)
The bug is because the user_answer
is wrong. The check is bitset_count_ones(hash_many('<kvc_cluster_id>', tostring($user_answer))) < 54
Can you brute force the correct user_answer
?
range answer from 0 to tolong(2147483647) step 1
| where bitset_count_ones(hash_many('<kvc_cluster_id>', tostring(answer))) > 54
| limit 1