diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/blobs/CliBlobCustomAttrib.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/blobs/CliBlobCustomAttrib.java new file mode 100644 index 0000000000..0814299b0f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/blobs/CliBlobCustomAttrib.java @@ -0,0 +1,475 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.pe.cli.blobs; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.format.pe.cli.blobs.CliAbstractSig.CliElementType; +import ghidra.app.util.bin.format.pe.cli.blobs.CliAbstractSig.CliParam; +import ghidra.app.util.bin.format.pe.cli.streams.CliStreamMetadata; +import ghidra.app.util.bin.format.pe.cli.tables.CliAbstractTableRow; +import ghidra.app.util.bin.format.pe.cli.tables.CliTableCustomAttribute.CliCustomAttributeRow; +import ghidra.app.util.bin.format.pe.cli.tables.CliTableMemberRef.CliMemberRefRow; +import ghidra.app.util.bin.format.pe.cli.tables.CliTableMethodDef.CliMethodDefRow; +import ghidra.app.util.bin.format.pe.cli.tables.CliTypeTable; +import ghidra.app.util.bin.format.pe.cli.tables.indexes.CliIndexCustomAttributeType; +import ghidra.program.model.data.*; +import ghidra.util.Msg; +import ghidra.util.exception.InvalidInputException; + +public class CliBlobCustomAttrib extends CliBlob { + + private CliFixedArg[] fixedArgs; + private CliNamedArg[] namedArgs; + private short numNamed; + + // Fixed constants for validating the structure + private static final short CLIBLOBCUSTOMATTRIB_PROLOG = 0x0001; + private static final byte CLIBLOBCUSTOMATTRIB_TYPE_FIELD = 0x53; + private static final byte CLIBLOBCUSTOMATTRIB_TYPE_PROPERTY = 0x54; + + private static final int CLIBLOBCUSTOMATTRIB_STRING_BOUNDARY_128 = 0x80; + private static final int CLIBLOBCUSTOMATTRIB_STRING_BOUNDARY_192 = 0xC0; + + private static final int CLIBLOBCUSTOMATTRIB_STRING_SIZE_ONE = 0x01; + private static final int CLIBLOBCUSTOMATTRIB_STRING_SIZE_TWO = 0x02; + private static final int CLIBLOBCUSTOMATTRIB_STRING_SIZE_FOUR = 0x03; + private static final int CLIBLOBCUSTOMATTRIB_STRING_SIZE_BITMASK = 0x3F; + + private static final int CLIBLOBCUSTOMATTRIB_STRING_INDICATOR_SHIFT = 0x06; + private static final int CLIBLOBCUSTOMATTRIB_STRING_INDICATOR_BITMASK = 0x03; + + // UTF-8 boundaries that help detect the end of a string where + // lengths aren't specified in FixedArg + private static final int CLIBLOBCUSTOMATTRIB_UTF8_LOW = 0x1F; + private static final int CLIBLOBCUSTOMATTRIB_UTF8_HIGH = 0x7F; + + private class CliFixedArg { + private CliElementType elem; + private Object value; + + public CliFixedArg(CliElementType elem, Object value) { + this.elem = elem; + this.value = value; + } + + public CliElementType getElem() { + return elem; + } + + public Object getValue() { + return value; + } + } + + private class CliNamedArg { + private int fieldOrProp; + private CliElementType fieldOrPropType; + private String fieldOrPropName; + + public CliNamedArg(int fieldOrProp, CliElementType fieldOrPropType, + String fieldOrPropName) { + this.fieldOrProp = fieldOrProp; + this.fieldOrPropType = fieldOrPropType; + this.fieldOrPropName = fieldOrPropName; + } + + public int getFieldOrProp() { + return fieldOrProp; + } + + public CliElementType getFieldOrPropType() { + return fieldOrPropType; + } + + public String getFieldOrPropName() { + return fieldOrPropName; + } + } + + public CliBlobCustomAttrib(CliBlob blob, CliCustomAttributeRow row, + CliStreamMetadata metadataStream) throws IOException { + super(blob); + + BinaryReader reader = blob.getContentsReader(); + + // Validate the blob prolog + short prolog = reader.readNextShort(); + if (prolog != CLIBLOBCUSTOMATTRIB_PROLOG) { + Msg.warn(this, + getName() + " had unexpected prolog (0x" + Integer.toHexString(prolog) + ")"); + return; + } + + // The location in the blob table for the actual CustomAttrib blob + int valueIndex = row.valueIndex; + + // The entry in the MethodRef or MethodDef table that corresponds to the method + // This is a CustomAttributeType coded index + int typeIndex = row.typeIndex; + + // The entry of the parent table index, also a CustomAttributeType coded index + int parentIndex = row.parentIndex; + + // The FixedArg parameters in the CustomAttrib blob are stored concatenated + // against each other without delimeters or type indicators, so you have to look + // back to the originating method signature to figure out what's what. + + CliParam[] params = null; + try { + // Get the table type and row for the attribute and depending on the type + // get the parameters + CliTypeTable tableType = CliIndexCustomAttributeType.getTableName(typeIndex); + int tableRowIndex = CliIndexCustomAttributeType.getRowIndex(typeIndex); + CliAbstractTableRow tableRow = metadataStream.getTable(tableType).getRow(tableRowIndex); + + if (tableType == CliTypeTable.MemberRef) { + CliMemberRefRow memberRefRow = (CliMemberRefRow) tableRow; + CliBlob memberRefBlob = + metadataStream.getBlobStream().getBlob(memberRefRow.signatureIndex); + CliSigMethodRef methodRefSig = new CliSigMethodRef(memberRefBlob); + params = methodRefSig.getParams(); + } + else if (tableType == CliTypeTable.MethodDef) { + CliMethodDefRow methodDefRow = (CliMethodDefRow) tableRow; + CliBlob methodDefBlob = + metadataStream.getBlobStream().getBlob(methodDefRow.sigIndex); + CliSigMethodDef methodDefSig = new CliSigMethodDef(methodDefBlob); + params = methodDefSig.getParamTypes(); + } + } + catch (InvalidInputException e) { + Msg.warn(this, "Unable to process the parameters in " + getName()); + return; + } + + // Process zero to multiple FixedArgs + fixedArgs = processFixedArgs(reader, params).toArray(CliFixedArg[]::new); + + // Process zero to multiple NamedArgs here + namedArgs = processNamedArgs(reader).toArray(CliNamedArg[]::new); + } + + @Override + public DataType getContentsDataType() { + StructureDataType struct = new StructureDataType(new CategoryPath(PATH), getName(), 0); + struct.add(WORD, "PROLOG", "Magic (0x0001)"); + + // Display the FixedArgs + if (fixedArgs != null) { + for (int i = 0; i < fixedArgs.length; i++) { + CliElementType elem = fixedArgs[i].elem; + + switch (elem) { + case ELEMENT_TYPE_CHAR: + struct.add(UTF16, "FixedArg_" + i, "Elem (" + fixedArgs[i].getElem() + ")"); + break; + + case ELEMENT_TYPE_I1: + case ELEMENT_TYPE_U1: + case ELEMENT_TYPE_BOOLEAN: + struct.add(BYTE, "FixedArg_" + i, "Elem (" + fixedArgs[i].getElem() + ")"); + break; + + case ELEMENT_TYPE_I2: + case ELEMENT_TYPE_U2: + struct.add(WORD, "FixedArg_" + i, "Elem (" + fixedArgs[i].getElem() + ")"); + break; + + case ELEMENT_TYPE_I4: + case ELEMENT_TYPE_U4: + case ELEMENT_TYPE_R4: + case ELEMENT_TYPE_VALUETYPE: + struct.add(DWORD, "FixedArg_" + i, "Elem (" + fixedArgs[i].getElem() + ")"); + break; + + case ELEMENT_TYPE_I8: + case ELEMENT_TYPE_U8: + case ELEMENT_TYPE_R8: + struct.add(QWORD, "FixedArg_" + i, "Elem (" + fixedArgs[i].getElem() + ")"); + + case ELEMENT_TYPE_STRING: + String s = (String) fixedArgs[i].value; + int l = s.length(); + if (l < CLIBLOBCUSTOMATTRIB_STRING_BOUNDARY_128) { + struct.add(BYTE, "PackedLen", ""); + } + else if (l < CLIBLOBCUSTOMATTRIB_STRING_BOUNDARY_192) { + struct.add(WORD, "PackedLen", ""); + } + else { + struct.add(DWORD, "PackedLen", ""); + + } + struct.add(UTF8, ((String) fixedArgs[i].value).length(), "FixedArg_" + i, + ""); + break; + + case ELEMENT_TYPE_I: + struct.add(BYTE, "ELEMENT_TYPE_I", ""); + struct.add(UTF8, ((String) fixedArgs[i].value).length(), "FixedArg_" + i, + ""); + break; + + default: + Msg.warn(this, "Unprocessed FixedArg element type in CustomAttr #" + + (i + 1) + ": " + fixedArgs[i].getElem().name()); + break; + } + } + } + + struct.add(WORD, "NumNamed", "Number of NamedArgs to follow"); + + // Display the NamedArgs + if (namedArgs != null) { + for (CliNamedArg cliNamedArg : namedArgs) { + int fieldOrProp = cliNamedArg.getFieldOrProp(); + if (fieldOrProp == CLIBLOBCUSTOMATTRIB_TYPE_FIELD) { + struct.add(BYTE, "FieldOrProp", "FIELD"); + } + else if (fieldOrProp == CLIBLOBCUSTOMATTRIB_TYPE_PROPERTY) { + struct.add(BYTE, "FieldOrProp", "PROPERTY"); + } + else { + struct.add(BYTE, "FieldOrProp", "Unknown value"); + } + + struct.add(BYTE, "FieldOrPropType", cliNamedArg.getFieldOrPropType().name()); + + int nameLen = cliNamedArg.getFieldOrPropName().length(); + if (nameLen < CLIBLOBCUSTOMATTRIB_STRING_BOUNDARY_128) { + struct.add(BYTE, "PackedLen", ""); + } + else if (nameLen < CLIBLOBCUSTOMATTRIB_STRING_BOUNDARY_192) { + struct.add(WORD, "PackedLen", ""); + } + else { + struct.add(DWORD, "PackedLen", ""); + + } + + struct.add(UTF8, nameLen, "FieldOrPropName", ""); + } + } + + return struct; + } + + @Override + public String getContentsName() { + return "CustomAttrib"; + } + + @Override + public String getContentsComment() { + return "A CustomAttrib blob stores values of fixed or named parameters supplied when " + + "instantiating a custom attribute"; + } + + @Override + public String getRepresentation() { + return "Blob (" + getContentsDataType().getDisplayName() + ")"; + } + + // SerStrings ("serialized strings") have a length field that varies in size + // based on the length of the string. This measures and decodes the Byte, Word, + // or DWord length field and returns it. + private int readSerStringLength(BinaryReader reader) throws IOException { + byte[] lengthBytes; + int length = 0; + ByteBuffer buf; + + // The first byte is either the size or an indicator that we have more + // size bytes ahead. Values contained in more than one bytes are stored + // big-endian. + byte firstByte = reader.readNextByte(); + + // Shift the highest two bits to the bottom and mask off to detect the + // size of the field holding the size of the string (1, 2, or 4 bytes), + // then cut the indicator bits off the first byte of the length. + byte stringSizeIndicator = (byte) (firstByte >> CLIBLOBCUSTOMATTRIB_STRING_INDICATOR_SHIFT & + CLIBLOBCUSTOMATTRIB_STRING_INDICATOR_BITMASK); + firstByte = (byte) (firstByte & CLIBLOBCUSTOMATTRIB_STRING_SIZE_BITMASK); + + if (stringSizeIndicator <= CLIBLOBCUSTOMATTRIB_STRING_SIZE_ONE) { + length = firstByte; + } + else if (stringSizeIndicator == CLIBLOBCUSTOMATTRIB_STRING_SIZE_TWO) { + lengthBytes = new byte[] { firstByte, reader.readNextByte() }; + + // Convert from big-endian + buf = ByteBuffer.wrap(lengthBytes); + buf.order(ByteOrder.BIG_ENDIAN); + length = buf.getShort(); + } + else if (stringSizeIndicator == CLIBLOBCUSTOMATTRIB_STRING_SIZE_FOUR) { + lengthBytes = new byte[] { firstByte, reader.readNextByte(), reader.readNextByte(), + reader.readNextByte() }; + + // Convert from big-endian + buf = ByteBuffer.wrap(lengthBytes); + buf.order(ByteOrder.BIG_ENDIAN); + length = buf.getInt(); + } + + return length; + } + + private ArrayList processFixedArgs(BinaryReader reader, CliParam[] params) + throws IOException { + ArrayList processFixedArgs = new ArrayList<>(); + if (params == null) { + return processFixedArgs; + } + + for (CliParam param : params) { + byte elemByte = reader.peekNextByte(); + if (elemByte == CliElementType.ELEMENT_TYPE_I.id()) { + reader.readNextByte(); + + // IntPtr followed by a string of the name of the element, the + // length of which is not specified and must be read until a + // non-printable UTF-8 character is encountered to signal the + // end of the name + + StringBuilder sb = new StringBuilder(); + while (((reader.peekNextByte() & + CLIBLOBCUSTOMATTRIB_UTF8_HIGH) > CLIBLOBCUSTOMATTRIB_UTF8_LOW) && + ((reader.peekNextByte() & + CLIBLOBCUSTOMATTRIB_UTF8_HIGH) < CLIBLOBCUSTOMATTRIB_UTF8_HIGH)) { + sb.append((char) reader.readNextByte()); + } + + processFixedArgs.add(new CliFixedArg(CliElementType.ELEMENT_TYPE_I, sb.toString())); + } + else { + // Process Elem types + CliElementType baseTypeCode = param.getType().baseTypeCode; + switch (baseTypeCode) { + case ELEMENT_TYPE_BOOLEAN: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextByte()); + break; + + case ELEMENT_TYPE_CHAR: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextShort()); + break; + + case ELEMENT_TYPE_I1: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextByte()); + break; + + case ELEMENT_TYPE_U1: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextUnsignedByte()); + break; + + case ELEMENT_TYPE_I2: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextShort()); + break; + + case ELEMENT_TYPE_U2: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextUnsignedShort()); + break; + + case ELEMENT_TYPE_I4: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextInt()); + break; + + case ELEMENT_TYPE_U4: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextUnsignedInt()); + break; + + case ELEMENT_TYPE_I8: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextByte()); + processFixedArgs.add( + new CliFixedArg(param.getType().baseTypeCode, reader.readNextLong())); + break; + + case ELEMENT_TYPE_U8: + addFixedArg(processFixedArgs, baseTypeCode, + reader.readNextByteArray(LongLongDataType.dataType.getLength())); + break; + + case ELEMENT_TYPE_R4: + addFixedArg(processFixedArgs, baseTypeCode, + reader.readNextByteArray(Float4DataType.dataType.getLength())); + break; + + case ELEMENT_TYPE_R8: + addFixedArg(processFixedArgs, baseTypeCode, + reader.readNextByteArray(Float8DataType.dataType.getLength())); + break; + + case ELEMENT_TYPE_STRING: + int length = readSerStringLength(reader); + if (length > 0) { + addFixedArg(processFixedArgs, baseTypeCode, new String( + reader.readNextByteArray(length), StandardCharsets.UTF_8)); + } + break; + + case ELEMENT_TYPE_VALUETYPE: + addFixedArg(processFixedArgs, baseTypeCode, reader.readNextInt()); + break; + + default: + Msg.info(this, + "A CustomAttrib with an unprocessed element type was deteceted: " + + param.getRepresentation()); + } + } + } + + return processFixedArgs; + } + + private void addFixedArg(ArrayList fixedArgs, CliElementType baseTypeCode, + Object value) { + fixedArgs.add(new CliFixedArg(baseTypeCode, value)); + } + + private ArrayList processNamedArgs(BinaryReader reader) throws IOException { + numNamed = reader.readNextShort(); + + // Process zero to multiple NamedArgs here + ArrayList processNamedArgs = new ArrayList<>(); + for (int i = 0; i < numNamed; i++) { + byte fieldOrProp = reader.readNextByte(); + if ((fieldOrProp != CLIBLOBCUSTOMATTRIB_TYPE_FIELD) && + fieldOrProp != CLIBLOBCUSTOMATTRIB_TYPE_PROPERTY) { + Msg.warn(this, "Invalid FieldOrProp value in NamedArg #" + (i + 1) + ": 0x" + + Integer.toHexString(fieldOrProp)); + continue; + } + + CliElementType fieldOrPropType = CliElementType.fromInt(reader.readNextByte()); + + // +1 to account for the null terminator + int nameLen = readSerStringLength(reader) + 1; + String fieldOrPropName = + new String(reader.readNextByteArray(nameLen), StandardCharsets.UTF_8); + + processNamedArgs.add(new CliNamedArg(fieldOrProp, fieldOrPropType, fieldOrPropName)); + } + + return processNamedArgs; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/streams/CliStreamMetadata.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/streams/CliStreamMetadata.java index 65d8faf138..0644f3d572 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/streams/CliStreamMetadata.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/streams/CliStreamMetadata.java @@ -435,7 +435,7 @@ public class CliStreamMetadata extends CliAbstractStream { table.markup(program, isBinary, monitor, log, ntHeader); } catch (Exception e) { - Msg.error(this, "Failed to markup " + table); + Msg.error(this, "Failed to markup " + table + ": " + e.toString()); } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/tables/CliTableCustomAttribute.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/tables/CliTableCustomAttribute.java index 4f5bad550f..6323c15a43 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/tables/CliTableCustomAttribute.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/cli/tables/CliTableCustomAttribute.java @@ -18,22 +18,30 @@ package ghidra.app.util.bin.format.pe.cli.tables; import java.io.IOException; import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.format.pe.cli.streams.CliStreamMetadata; +import ghidra.app.util.bin.format.pe.NTHeader; +import ghidra.app.util.bin.format.pe.cli.blobs.CliBlobCustomAttrib; +import ghidra.app.util.bin.format.pe.cli.streams.*; import ghidra.app.util.bin.format.pe.cli.tables.indexes.CliIndexCustomAttributeType; import ghidra.app.util.bin.format.pe.cli.tables.indexes.CliIndexHasCustomAttribute; +import ghidra.app.util.importer.MessageLog; +import ghidra.program.model.address.Address; import ghidra.program.model.data.CategoryPath; import ghidra.program.model.data.StructureDataType; +import ghidra.program.model.listing.Program; +import ghidra.program.model.util.CodeUnitInsertionException; +import ghidra.util.exception.DuplicateNameException; import ghidra.util.exception.InvalidInputException; +import ghidra.util.task.TaskMonitor; /** - * Describes the CustomAttribute table. + * Describes the CustomAttribute table. */ public class CliTableCustomAttribute extends CliAbstractTable { public class CliCustomAttributeRow extends CliAbstractTableRow { public int parentIndex; public int typeIndex; public int valueIndex; - + public CliCustomAttributeRow(int parentIndex, int typeIndex, int valueIndex) { super(); this.parentIndex = parentIndex; @@ -45,13 +53,17 @@ public class CliTableCustomAttribute extends CliAbstractTable { public String getRepresentation() { String parentRep, typeRep; try { - parentRep = getRowRepresentationSafe(CliIndexHasCustomAttribute.getTableName(parentIndex), CliIndexHasCustomAttribute.getRowIndex(parentIndex)); + parentRep = + getRowRepresentationSafe(CliIndexHasCustomAttribute.getTableName(parentIndex), + CliIndexHasCustomAttribute.getRowIndex(parentIndex)); } catch (InvalidInputException e) { parentRep = Integer.toHexString(parentIndex); } try { - typeRep = getRowRepresentationSafe(CliIndexCustomAttributeType.getTableName(parentIndex), CliIndexCustomAttributeType.getRowIndex(parentIndex)); + typeRep = + getRowRepresentationSafe(CliIndexCustomAttributeType.getTableName(parentIndex), + CliIndexCustomAttributeType.getRowIndex(parentIndex)); } catch (InvalidInputException e) { typeRep = Integer.toHexString(typeIndex); @@ -59,23 +71,47 @@ public class CliTableCustomAttribute extends CliAbstractTable { return String.format("Parent %s Type %s Value %x", parentRep, typeRep, valueIndex); } } - public CliTableCustomAttribute(BinaryReader reader, CliStreamMetadata stream, CliTypeTable tableId) throws IOException { + + public CliTableCustomAttribute(BinaryReader reader, CliStreamMetadata stream, + CliTypeTable tableId) throws IOException { super(reader, stream, tableId); for (int i = 0; i < this.numRows; i++) { - CliCustomAttributeRow row = new CliCustomAttributeRow(CliIndexHasCustomAttribute.readCodedIndex(reader, stream), + CliCustomAttributeRow row = new CliCustomAttributeRow( + CliIndexHasCustomAttribute.readCodedIndex(reader, stream), CliIndexCustomAttributeType.readCodedIndex(reader, stream), readBlobIndex(reader)); rows.add(row); blobs.add(row.valueIndex); } reader.setPointerIndex(this.readerOffset); } - + @Override public StructureDataType getRowDataType() { - StructureDataType rowDt = new StructureDataType(new CategoryPath(PATH), "CustomAttribute Row", 0); + StructureDataType rowDt = + new StructureDataType(new CategoryPath(PATH), "CustomAttribute Row", 0); rowDt.add(CliIndexHasCustomAttribute.toDataType(metadataStream), "Parent", null); rowDt.add(CliIndexCustomAttributeType.toDataType(metadataStream), "Type", null); rowDt.add(metadataStream.getBlobIndexDataType(), "Value", null); return rowDt; } + + @Override + public void markup(Program program, boolean isBinary, TaskMonitor monitor, MessageLog log, + NTHeader ntHeader) + throws DuplicateNameException, CodeUnitInsertionException, IOException { + CliStreamBlob blobStream = metadataStream.getBlobStream(); + + for (CliAbstractTableRow row : rows) { + CliCustomAttributeRow customRow = (CliCustomAttributeRow) row; + int valueIndex = customRow.valueIndex; + + Address addr = CliAbstractStream.getStreamMarkupAddress(program, isBinary, monitor, log, + ntHeader, blobStream, valueIndex); + + // Create CustomAttrib Blob object + CliBlobCustomAttrib blob = + new CliBlobCustomAttrib(blobStream.getBlob(valueIndex), customRow, metadataStream); + blobStream.updateBlob(blob, addr, program); + } + } }