1

Can you check whether or not the macro code in Excel is protected by a password and even validate which password?
I did manage to find examples about password protected Excel workbooks and protected sheets, but not about locked macro code.

R. Oosterholt
  • 7,720
  • 2
  • 53
  • 77
  • I'm currently busy preparing POI 5.0.0, but here are two links [1](https://stackoverflow.com/questions/1026483), [2](https://exceloffthegrid.com/removing-cracking-excel-passwords-with-vba/). So basically you need to scan for that protection tag in the vba binary inside of the .xlsx (.zip file) – kiwiwings Dec 23 '20 at 18:26

1 Answers1

0

For re-engineering Office documents - you can use my POI-Visualizer - so you can view the structures and whereabouts of embedded elements easily.

Location of the VBA properties

The information is stored in the PROJECT stream of the vbaProject.bin. The MS-OVBA has an detailed example of it. With the following code the DPB element - the project password - can be validated.

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Properties;

public class VBAProtect {
    private static final String NO_PASSWORD_HASH = "0E0CD1ECDFF4E7F5E7F5E7";

    public static void main(String[] args) throws DecoderException, IOException, InvalidFormatException {
        try (FileInputStream is = new FileInputStream("vba-protected.xlsm");
             XSSFWorkbook wb = new XSSFWorkbook(is)) {
            PackagePartName pn = PackagingURIHelper.createPartName("/xl/vbaProject.bin");
            PackagePart part = wb.getPackage().getPart(pn);

            try (InputStream pis = part.getInputStream();
                 POIFSFileSystem ps = new POIFSFileSystem(pis);
                 InputStream dis = ps.createDocumentInputStream("PROJECT")) {

                Properties prop = new Properties();
                prop.load(dis);
                String DPB = prop.getProperty("DPB").replace("\"", "");

                if (NO_PASSWORD_HASH.equals(DPB)) {
                    System.out.println("no password");
                } else {
                    EncData ed = new EncData();
                    ed.parse(DPB);
                    PasswordHash ph = new PasswordHash();
                    ph.parse(ed);

                    String pass = "password";
                    System.out.println("pass <" + pass + "> matches? " + ph.matches(pass));
                }
            }
        }

    }

    public static class EncData {
        public byte seed;
        public byte version;
        public byte projKey;
        public int ignoredLength;
        public int dataLength;
        private byte[] raw;

        public byte[] getData() {
            byte[] data = new byte[dataLength];
            // Header (3 bytes) + Ignored bytes + length (4 bytes)
            System.arraycopy(raw, 3 + ignoredLength + 4, data, 0, dataLength);
            return data;
        }

        public void parse(String data) throws DecoderException {
            raw = Hex.decodeHex(data);
            seed = raw[0];
            byte VersionEnc = raw[1];
            byte ProjKeyEnc = raw[2];
            ignoredLength = ((seed & 6) / 2);

            version = (byte)((seed ^ VersionEnc) & 0xFF);
            projKey = (byte)((seed ^ ProjKeyEnc) & 0xFF);
            byte UnencryptedByte1 = projKey;
            byte EncryptedByte1 = ProjKeyEnc;
            byte EncryptedByte2 = VersionEnc;

            for (int offset = 3; offset < raw.length; offset++) {
                byte ByteEnc = raw[offset];
                byte Byte = (byte)((ByteEnc ^ (EncryptedByte2 + UnencryptedByte1)) & 0xFF);
                EncryptedByte2 = EncryptedByte1;
                EncryptedByte1 = ByteEnc;
                raw[offset] = UnencryptedByte1 = Byte;
            }

            dataLength = LittleEndian.getInt(raw, 3 + ignoredLength);
        }
    }

    public static class PasswordHash {
        public final byte[] key = new byte[4];
        public final byte[] passwordHash = new byte[20];

        public void parse(EncData data) {
            byte[] dpb = data.getData();
            // first byte of grbit is reserved, so ignore it
            int Grbit = LittleEndian.getInt(dpb, 0) >>> 8;
            final int offset = 4;
            for (int i=0; i<24; i++, Grbit >>>= 1) {
                if ((Grbit & 1) == 0) {
                    dpb[offset+i] = 0;
                }
            }
            System.arraycopy(dpb, offset, key, 0, 4);
            System.arraycopy(dpb, offset+4, passwordHash, 0, 20);
        }

        public boolean matches(String password) {
            // TODO: check MBCS windows encoding differences to UTF-8
            // https://stackoverflow.com/questions/3298569/difference-between-mbcs-and-utf-8-on-windows
            MessageDigest shaDig = CryptoFunctions.getMessageDigest(HashAlgorithm.sha1);
            shaDig.update(password.getBytes(StandardCharsets.UTF_8));
            shaDig.update(key);
            byte[] dig = shaDig.digest();
            return Arrays.equals(dig, passwordHash);
        }
    }
}
kiwiwings
  • 3,386
  • 1
  • 21
  • 57
  • There seems to be a problem in PasswordHash::parse. The Grbit, i.e. the bits telling when a 0x01 in the key / password hash is actually a 0x00, maybe need to be shifted by 3 and not 8 (this worked in one failing case). I suspect I might have a endian-order problem. I've tried to reproduce this several times, but usually the Grbit is 0x..FFFFFF hence all the key / password hash bytes are taken as-is. – kiwiwings Dec 27 '20 at 11:26
  • kiwiwings did you have any luck figuring out the strange case above? – R. Oosterholt Jan 04 '21 at 07:23
  • Nope, I've tried to create files with a GRBIT != 0x..FFFFFF, but to no avail. If you have further failing files, I'm happy to inspect those. If the files have confidential data in it, but the VBA is nothing special, you can send me the vbaProject.bin only. – kiwiwings Jan 05 '21 at 08:19