GP-6291 better handling for obfuscated golang binaries

Allow the user to manually specify a version if it can't be detected in
the binary's metadata.

Favor the ftab's function address over the funcdata's invalid function
address.

Detect duff function body extents manually instead of relying on go
metadata.

Remove some unhelpful fail-detection logic so user won't be prevented
from re-trying the golang analysis a second time.

Added markup for encoded pcvalue sequences referenced from the funcdata.
This commit is contained in:
dev747368
2026-01-07 17:03:26 +00:00
parent aedf80762b
commit d5ff03dfdf
6 changed files with 188 additions and 25 deletions

View File

@@ -43,6 +43,8 @@ import ghidra.framework.cmd.BackgroundCommand;
import ghidra.framework.options.Options;
import ghidra.framework.store.LockException;
import ghidra.program.model.address.*;
import ghidra.program.model.block.CodeBlock;
import ghidra.program.model.block.MultEntSubModel;
import ghidra.program.model.data.*;
import ghidra.program.model.data.DataUtilities.ClearDataMode;
import ghidra.program.model.lang.Language;
@@ -124,6 +126,10 @@ public class GolangSymbolAnalyzer extends AbstractAnalyzer {
this.program = program;
aam = AutoAnalysisManager.getAnalysisManager(program);
if (!analyzerOptions.fallbackGoVer.isEmpty()) {
GoBuildInfo.setFallbackVersion(program, analyzerOptions.fallbackGoVer);
}
goBinary = GoRttiMapper.getSharedGoBinary(program, monitor);
if (goBinary == null) {
Msg.error(this, "Go symbol analyzer error: unable to get GoRttiMapper");
@@ -613,7 +619,10 @@ public class GolangSymbolAnalyzer extends AbstractAnalyzer {
}
String ccName = duffFunc.getCallingConventionName();
Namespace funcNS = duffFunc.getParentNamespace();
AddressSet funcBody = new AddressSet(funcData.getBody());
// need to use our own logic instead of relying on possibly obfuscated go metadata
AddressSet funcBody = getDuffBody(duffFunc, monitor);
String duffComment = program.getListing()
.getCodeUnitAt(duffFunc.getEntryPoint())
.getComment(CommentType.PLATE);
@@ -647,6 +656,19 @@ public class GolangSymbolAnalyzer extends AbstractAnalyzer {
return true;
}
private AddressSet getDuffBody(Function func, TaskMonitor monitor) {
MultEntSubModel bm = new MultEntSubModel(func.getProgram());
CodeBlock cb;
try {
cb = bm.getCodeBlockAt(func.getEntryPoint(), monitor);
return new AddressSet(cb);
}
catch (CancelledException e) {
// fail, fall thru, return 1 byte range
}
return new AddressSet(func.getBody());
}
}
//--------------------------------------------------------------------------------------------
@@ -1283,19 +1305,24 @@ public class GolangSymbolAnalyzer extends AbstractAnalyzer {
public boolean fixupGcWriteBarrierFlag = true;
static final String FALLBACK_GOVER_OPTIONNAME = "Fallback Go Version";
static final String FALLBACK_GOVER_DESC = """
Go version to use if the Go metadata has been obfuscated.
""";
public String fallbackGoVer;
void registerOptions(Options options, Program program) {
options.registerOption(GolangAnalyzerOptions.OUTPUT_SOURCE_INFO_OPTIONNAME,
outputSourceInfo, null, GolangAnalyzerOptions.OUTPUT_SOURCE_INFO_DESC);
options.registerOption(GolangAnalyzerOptions.FIXUP_DUFF_FUNCS_OPTIONNAME,
fixupDuffFunctions, null, GolangAnalyzerOptions.FIXUP_DUFF_FUNCS_DESC);
options.registerOption(GolangAnalyzerOptions.PROP_RTTI_OPTIONNAME, propagateRtti, null,
GolangAnalyzerOptions.PROP_RTTI_DESC);
options.registerOption(GolangAnalyzerOptions.FIXUP_GCWRITEBARRIER_OPTIONNAME,
fixupGcWriteBarrierFunctions, null,
GolangAnalyzerOptions.FIXUP_GCWRITEBARRIER_FUNCS_DESC);
options.registerOption(GolangAnalyzerOptions.FIXUP_GCWRITEBARRIER_FLAG_OPTIONNAME,
fixupGcWriteBarrierFlag, null,
GolangAnalyzerOptions.FIXUP_GCWRITEBARRIER_FLAG_DESC);
options.registerOption(OUTPUT_SOURCE_INFO_OPTIONNAME, outputSourceInfo, null,
OUTPUT_SOURCE_INFO_DESC);
options.registerOption(FIXUP_DUFF_FUNCS_OPTIONNAME, fixupDuffFunctions, null,
FIXUP_DUFF_FUNCS_DESC);
options.registerOption(PROP_RTTI_OPTIONNAME, propagateRtti, null, PROP_RTTI_DESC);
options.registerOption(FIXUP_GCWRITEBARRIER_OPTIONNAME, fixupGcWriteBarrierFunctions,
null, FIXUP_GCWRITEBARRIER_FUNCS_DESC);
options.registerOption(FIXUP_GCWRITEBARRIER_FLAG_OPTIONNAME, fixupGcWriteBarrierFlag,
null, FIXUP_GCWRITEBARRIER_FLAG_DESC);
options.registerOption(FALLBACK_GOVER_OPTIONNAME, "", null, FALLBACK_GOVER_DESC);
}
void optionsChanged(Options options, Program program) {
@@ -1312,6 +1339,7 @@ public class GolangSymbolAnalyzer extends AbstractAnalyzer {
fixupGcWriteBarrierFlag =
options.getBoolean(GolangAnalyzerOptions.FIXUP_GCWRITEBARRIER_FLAG_OPTIONNAME,
fixupGcWriteBarrierFlag);
fallbackGoVer = options.getString(FALLBACK_GOVER_OPTIONNAME, "");
}
}
@@ -1326,4 +1354,5 @@ public class GolangSymbolAnalyzer extends AbstractAnalyzer {
Options options = program.getOptions(Program.PROGRAM_INFO);
return options.getBoolean(ANALYZED_FLAG_OPTION_NAME, false);
}
}

View File

@@ -46,6 +46,8 @@ public class GoBuildInfo implements ElfInfoItem {
public static final String ELF_SECTION_NAME = ".go.buildinfo";
public static final String MACHO_SECTION_NAME = "go_buildinfo";
public static final String FALLBACK_GOVER_OPTION = "Go version fallback";
// Defined in Go's src/debug/buildinfo/buildinfo.go
// NOTE: ISO_8859_1 charset is required to not mangle the \u00ff when converting to bytes
private static final byte[] GO_BUILDINF_MAGIC =
@@ -147,6 +149,21 @@ public class GoBuildInfo implements ElfInfoItem {
return readStringInfo(reader, inlineStr, program, pointerSize, struct);
}
public static void setFallbackVersion(Program program, String fallbackGoVerStr) {
program.getOptions(Program.PROGRAM_INFO).setString(FALLBACK_GOVER_OPTION, fallbackGoVerStr);
}
public static GoBuildInfo fromFallbackInfo(Program program) {
String fallbackGoVerStr =
program.getOptions(Program.PROGRAM_INFO).getString(FALLBACK_GOVER_OPTION, "");
if (fallbackGoVerStr.isEmpty() || GoVer.parse(fallbackGoVerStr).isInvalid()) {
return null;
}
return new GoBuildInfo(program.getDefaultPointerSize(),
program.getMemory().isBigEndian() ? Endian.BIG : Endian.LITTLE, fallbackGoVerStr,
"unknown path", null, List.of(), List.of(), null);
}
/**
* Probes the specified InputStream and returns true if it starts with a Go buildinfo magic
* signature.

View File

@@ -66,11 +66,17 @@ public class GoFuncData implements StructureMarkup<GoFuncData> {
private long deferreturn;
@FieldMapping
@MarkupReference("getPcfileRefAddress")
private long pcfile; // offset in moduledata.pctab where file info starts
@FieldMapping
@MarkupReference("getPclnRefAddress")
private long pcln; // offset in moduledata.pctab where line num info starts
@FieldMapping
@MarkupReference("getPcspRefAddress")
private long pcsp; // offset in moduledata.pctab, -1.15=int32, 1.16+=uint32
@FieldMapping
private int npcdata; // number of elements in varlen pcdata array
@@ -90,6 +96,7 @@ public class GoFuncData implements StructureMarkup<GoFuncData> {
//--------------------------------------------------------------------------------------
private Address funcAddress; // set when entryoff or entry are set
private boolean funcAddressOverride;
/**
* Sets the function's entry point via a relative offset value
@@ -118,6 +125,11 @@ public class GoFuncData implements StructureMarkup<GoFuncData> {
this.funcAddress = context.getDataTypeMapper().getCodeAddress(entry);
}
public void setFuncAddressOverride(Address addr) {
funcAddress = addr;
funcAddressOverride = true;
}
/**
* {@return the address of this function}
*/
@@ -175,6 +187,30 @@ public class GoFuncData implements StructureMarkup<GoFuncData> {
return getPcDataStart(npcdata + tableIndex);
}
public Address getPcfileRefAddress() {
GoModuledata moduledata = getModuledata();
GoSlice pctab = moduledata.getPctab();
return pctab != null
? programContext.getDataAddress(pctab.getElementOffset(1, pcfile))
: null;
}
public Address getPclnRefAddress() {
GoModuledata moduledata = getModuledata();
GoSlice pctab = moduledata.getPctab();
return pctab != null
? programContext.getDataAddress(pctab.getElementOffset(1, pcln))
: null;
}
public Address getPcspRefAddress() {
GoModuledata moduledata = getModuledata();
GoSlice pctab = moduledata.getPctab();
return pctab != null
? programContext.getDataAddress(pctab.getElementOffset(1, pcsp))
: null;
}
/**
* Returns a value from the specified pc->value lookup table, for a specific
* address (that should be within the function's footprint).
@@ -304,7 +340,7 @@ public class GoFuncData implements StructureMarkup<GoFuncData> {
* @return String description
*/
public String getDescription() {
return getName() + "@" + getFuncAddress();
return getName() + "@" + getFuncAddress() + (funcAddressOverride ? " (overridden)" : "");
}
/**
@@ -470,6 +506,21 @@ public class GoFuncData implements StructureMarkup<GoFuncData> {
session.labelAddress(deferreturnAddr, funcName.asString() + "_deferreturn",
funcName.packagePath());
}
try {
GoPcValueEvaluator pceval = new GoPcValueEvaluator(this, pcfile);
pceval.markup(session);
pceval = new GoPcValueEvaluator(this, pcln);
pceval.markup(session);
pceval = new GoPcValueEvaluator(this, pcsp);
pceval.markup(session);
}
catch (IOException e) {
// ignore
}
}
//-------------------------------------------------------------------------------------------

View File

@@ -16,6 +16,7 @@
package ghidra.app.util.bin.format.golang.rtti;
import java.io.IOException;
import java.util.Objects;
import ghidra.app.util.bin.format.golang.structmapping.*;
import ghidra.program.model.address.Address;
@@ -86,9 +87,19 @@ public class GoFunctabEntry {
@Markup
public GoFuncData getFuncData() throws IOException {
GoModuledata moduledata = getModuledata();
return funcoff != 0 && moduledata != null
GoFuncData result = funcoff != 0 && moduledata != null
? moduledata.getFuncDataInstance(funcoff)
: null;
if (result != null && !Objects.equals(funcAddress, result.getFuncAddress())) {
// defeat obfuscated GoFuncData func address values with good addr from ftab entry
if (programContext.getProgram()
.getMemory()
.getLoadedAndInitializedAddressSet()
.contains(funcAddress)) {
result.setFuncAddressOverride(funcAddress);
}
}
return result;
}
/**

View File

@@ -20,7 +20,13 @@ import java.util.ArrayList;
import java.util.List;
import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.LEB128Info;
import ghidra.app.util.bin.format.dwarf.DWARFUtil;
import ghidra.app.util.bin.format.golang.structmapping.MarkupSession;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.LEB128;
import ghidra.program.model.data.UnsignedLeb128DataType;
import ghidra.program.model.listing.CommentType;
/**
* Evaluates a sequence of (value_delta,pc_delta) leb128 pairs to calculate a value for a certain
@@ -31,6 +37,7 @@ public class GoPcValueEvaluator {
private final long funcEntry;
private final BinaryReader reader;
private final long startPosition;
private final long pctabOffset;
private int value = -1;
private long pc;
@@ -49,6 +56,7 @@ public class GoPcValueEvaluator {
this.pcquantum = moduledata.getGoBinary().getMinLC();
this.reader = moduledata.getPcValueTable().getElementReader(1, (int) offset);
this.startPosition = reader.getPointerIndex();
this.pctabOffset = offset;
this.funcEntry = func.getFuncAddress().getOffset();
this.pc = funcEntry;
@@ -126,4 +134,55 @@ public class GoPcValueEvaluator {
return true;
}
public void markup(MarkupSession session) throws IOException {
Address startAddr = session.getMappingContext().getDataAddress(startPosition);
if (session.getMarkedupAddresses().contains(startAddr)) {
return;
}
session.labelAddress(startAddr, "pctab[0x%x]".formatted(pctabOffset));
int count = 0;
while (markupStep(session)) {
count++;
}
long size = reader.getPointerIndex() - startPosition;
String msg = "stepcount=%d,size=%d".formatted(count, size);
DWARFUtil.appendComment(session.getProgram(), startAddr, CommentType.PRE, "", msg, ",");
}
private boolean markupStep(MarkupSession session) throws IOException {
LEB128Info uvdeltaInfo = reader.readNext(LEB128Info::unsigned);
Address uvdeltaAddr = session.getMappingContext().getDataAddress(uvdeltaInfo.getOffset());
if (session.getMarkedupAddresses().contains(uvdeltaAddr)) {
return false;
}
session.markupAddress(uvdeltaAddr, UnsignedLeb128DataType.dataType,
uvdeltaInfo.getLength());
int uvdelta = uvdeltaInfo.asUInt32();
if (uvdelta == 0 && pc != funcEntry) {
// a delta of 0 is only valid on the first element
DWARFUtil.appendComment(session.getProgram(), uvdeltaAddr, CommentType.EOL, "", "end",
",");
return false;
}
int vdelta = -(uvdelta & 1) ^ (uvdelta >> 1);
value += vdelta;
String msg = "value+%d=0x%x".formatted(vdelta, value);
DWARFUtil.appendComment(session.getProgram(), uvdeltaAddr, CommentType.EOL, "", msg, ",");
LEB128Info pcdeltaInfo = reader.readNext(LEB128Info::unsigned);
Address pcdeltaAddr = session.getMappingContext().getDataAddress(pcdeltaInfo.getOffset());
session.markupAddress(pcdeltaAddr, UnsignedLeb128DataType.dataType,
pcdeltaInfo.getLength());
int pcdelta = pcdeltaInfo.asUInt32();
pc += pcdelta * pcquantum;
msg = "pc+0x%x=+0x%08x".formatted(pcdelta * pcquantum, pc - funcEntry);
DWARFUtil.appendComment(session.getProgram(), pcdeltaAddr, CommentType.EOL, "", msg, ",");
return true;
}
}

View File

@@ -82,8 +82,6 @@ public class GoRttiMapper extends DataTypeMapper implements DataTypeMapperContex
private static final List<String> SECTION_PREFIXES =
List.of("." /* ELF */, "__" /* macho sections */);
private static final String FAILED_FLAG = "FAILED TO FIND GOLANG BINARY";
/**
* Returns a shared {@link GoRttiMapper} for the specified program, or null if the binary
* is not a supported Go binary.
@@ -102,10 +100,6 @@ public class GoRttiMapper extends DataTypeMapper implements DataTypeMapperContex
*
*/
public static GoRttiMapper getSharedGoBinary(Program program, TaskMonitor monitor) {
if (TransientProgramProperties.hasProperty(program, FAILED_FLAG)) {
// don't try to do any work if we've failed earlier
return null;
}
GoRttiMapper goBinary = TransientProgramProperties.getProperty(program, GoRttiMapper.class,
TransientProgramProperties.SCOPE.ANALYSIS_SESSION, GoRttiMapper.class, () -> {
// cached instance not found, create new instance
@@ -128,10 +122,6 @@ public class GoRttiMapper extends DataTypeMapper implements DataTypeMapperContex
logAnalyzerMsg(program, e.getMessage());
}
// this sets the failed flag
TransientProgramProperties.getProperty(program, FAILED_FLAG,
TransientProgramProperties.SCOPE.PROGRAM, Boolean.class, () -> true);
return null;
});
@@ -161,6 +151,12 @@ public class GoRttiMapper extends DataTypeMapper implements DataTypeMapperContex
public static GoRttiMapper getGoBinary(Program program, TaskMonitor monitor)
throws BootstrapInfoException, IOException {
GoBuildInfo buildInfo = GoBuildInfo.fromProgram(program);
if (buildInfo == null || buildInfo.getGoVer().isInvalid()) {
GoBuildInfo fallbackBI = GoBuildInfo.fromFallbackInfo(program);
if (fallbackBI != null) {
buildInfo = fallbackBI;
}
}
if (buildInfo == null) {
// probably not a Go binary
return null;