Detect and print Confuser version

This commit is contained in:
de4dot 2012-08-09 11:47:18 +02:00
parent d92ff23740
commit 291040abfe
17 changed files with 584 additions and 23 deletions

View File

@ -86,6 +86,7 @@
<Compile Include="deobfuscators\Confuser\ConstantsFolder.cs" />
<Compile Include="deobfuscators\Confuser\ConstantsInliner.cs" />
<Compile Include="deobfuscators\Confuser\Deobfuscator.cs" />
<Compile Include="deobfuscators\Confuser\IVersionProvider.cs" />
<Compile Include="deobfuscators\Confuser\JitMethodsDecrypter.cs" />
<Compile Include="deobfuscators\Confuser\MemoryMethodsDecrypter.cs" />
<Compile Include="deobfuscators\Confuser\MethodsDecrypterBase.cs" />
@ -93,6 +94,7 @@
<Compile Include="deobfuscators\Confuser\ResourceDecrypter.cs" />
<Compile Include="deobfuscators\Confuser\StringDecrypter.cs" />
<Compile Include="deobfuscators\Confuser\Unpacker.cs" />
<Compile Include="deobfuscators\Confuser\VersionDetector.cs" />
<Compile Include="deobfuscators\Confuser\x86Emulator.cs" />
<Compile Include="deobfuscators\dotNET_Reactor\v4\ProxyCallFixer.cs" />
<Compile Include="deobfuscators\ILProtector\MethodReader.cs" />

View File

@ -22,7 +22,7 @@ using Mono.Cecil.Cil;
using de4dot.blocks;
namespace de4dot.code.deobfuscators.Confuser {
class AntiDebugger {
class AntiDebugger : IVersionProvider {
ModuleDefinition module;
MethodDefinition initMethod;
@ -109,5 +109,10 @@ namespace de4dot.code.deobfuscators.Confuser {
}
return false;
}
public bool getRevisionRange(out int minRev, out int maxRev) {
minRev = maxRev = 0;
return false;
}
}
}

View File

@ -22,7 +22,7 @@ using Mono.Cecil.Cil;
using de4dot.blocks;
namespace de4dot.code.deobfuscators.Confuser {
class AntiDumping {
class AntiDumping : IVersionProvider {
ModuleDefinition module;
MethodDefinition initMethod;
@ -113,5 +113,10 @@ namespace de4dot.code.deobfuscators.Confuser {
return true;
}
public bool getRevisionRange(out int minRev, out int maxRev) {
minRev = maxRev = 0;
return false;
}
}
}

View File

@ -27,7 +27,7 @@ using de4dot.blocks;
using de4dot.PE;
namespace de4dot.code.deobfuscators.Confuser {
abstract class ConstantsDecrypterBase {
abstract class ConstantsDecrypterBase : IVersionProvider {
protected ModuleDefinition module;
protected byte[] fileData;
protected ISimpleDeobfuscator simpleDeobfuscator;
@ -258,6 +258,7 @@ namespace de4dot.code.deobfuscators.Confuser {
this.simpleDeobfuscator = simpleDeobfuscator;
}
public abstract bool getRevisionRange(out int minRev, out int maxRev);
public abstract void initialize();
protected void add(DecrypterInfo info) {

View File

@ -231,5 +231,44 @@ namespace de4dot.code.deobfuscators.Confuser {
byte[] decryptConstant_v17_r73404_normal(DecrypterInfo info, byte[] encrypted, uint offs) {
return ConfuserUtils.decrypt(info.key0 ^ offs, encrypted);
}
public override bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v15_r60785_normal:
case ConfuserVersion.v15_r60785_dynamic:
minRev = 60785;
maxRev = 72989;
return true;
case ConfuserVersion.v17_r73404_normal:
minRev = 73404;
maxRev = 73605;
return true;
case ConfuserVersion.v17_r73740_dynamic:
minRev = 73740;
maxRev = 73740;
return true;
case ConfuserVersion.v17_r73764_dynamic:
case ConfuserVersion.v17_r73764_native:
minRev = 73764;
maxRev = 73791;
return true;
case ConfuserVersion.v17_r73822_normal:
case ConfuserVersion.v17_r73822_dynamic:
case ConfuserVersion.v17_r73822_native:
minRev = 73822;
maxRev = 74637;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -438,5 +438,50 @@ namespace de4dot.code.deobfuscators.Confuser {
}
return -1;
}
public override bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v17_r74708_normal:
case ConfuserVersion.v17_r74708_dynamic:
case ConfuserVersion.v17_r74708_native:
minRev = 74708;
maxRev = 74708;
return true;
case ConfuserVersion.v17_r74788_normal:
case ConfuserVersion.v17_r74788_dynamic:
case ConfuserVersion.v17_r74788_native:
minRev = 74788;
maxRev = 74788;
return true;
case ConfuserVersion.v17_r74816_normal:
case ConfuserVersion.v17_r74816_dynamic:
case ConfuserVersion.v17_r74816_native:
minRev = 74816;
maxRev = 74852;
return true;
case ConfuserVersion.v17_r75056_normal:
case ConfuserVersion.v17_r75056_dynamic:
case ConfuserVersion.v17_r75056_native:
minRev = 75056;
maxRev = 75184;
return true;
case ConfuserVersion.v18_r75257_normal:
case ConfuserVersion.v18_r75257_dynamic:
case ConfuserVersion.v18_r75257_native:
minRev = 75257;
maxRev = 75349;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -29,7 +29,7 @@ using de4dot.PE;
namespace de4dot.code.deobfuscators.Confuser {
// Since v1.8 r75367
class ConstantsDecrypterV18 {
class ConstantsDecrypterV18 : IVersionProvider {
ModuleDefinition module;
byte[] fileData;
ISimpleDeobfuscator simpleDeobfuscator;
@ -708,5 +708,29 @@ namespace de4dot.code.deobfuscators.Confuser {
installMethod.Body.Instructions.Clear();
installMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
}
public bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v18_r75367_normal:
case ConfuserVersion.v18_r75367_dynamic:
case ConfuserVersion.v18_r75367_native:
minRev = 75367;
maxRev = 75367;
return true;
case ConfuserVersion.v18_r75369_normal:
case ConfuserVersion.v18_r75369_dynamic:
case ConfuserVersion.v18_r75369_native:
minRev = 75369;
maxRev = int.MaxValue;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -78,8 +78,8 @@ namespace de4dot.code.deobfuscators.Confuser {
AntiDumping antiDumping;
ResourceDecrypter resourceDecrypter;
ConstantsDecrypterV18 constantsDecrypterV18;
ConstantsDecrypterV15 constantsDecrypterV15;
ConstantsDecrypterV17 constantsDecrypterV17;
ConstantsDecrypterV15 constantsDecrypterV15;
Int32ValueInliner int32ValueInliner;
Int64ValueInliner int64ValueInliner;
SingleValueInliner singleValueInliner;
@ -149,19 +149,22 @@ namespace de4dot.code.deobfuscators.Confuser {
}
protected override void scanForObfuscator() {
jitMethodsDecrypter = new JitMethodsDecrypter(module, DeobfuscatedFile);
try {
jitMethodsDecrypter.find();
}
catch {
}
if (jitMethodsDecrypter.Detected)
return;
memoryMethodsDecrypter = new MemoryMethodsDecrypter(module, DeobfuscatedFile);
memoryMethodsDecrypter.find();
if (memoryMethodsDecrypter.Detected)
return;
initTheRest();
do {
jitMethodsDecrypter = new JitMethodsDecrypter(module, DeobfuscatedFile);
try {
jitMethodsDecrypter.find();
}
catch {
}
if (jitMethodsDecrypter.Detected)
break;
memoryMethodsDecrypter = new MemoryMethodsDecrypter(module, DeobfuscatedFile);
memoryMethodsDecrypter.find();
if (memoryMethodsDecrypter.Detected)
break;
initTheRest();
} while (false);
initializeObfuscatorName();
}
void initTheRest() {
@ -200,6 +203,41 @@ namespace de4dot.code.deobfuscators.Confuser {
initializeStringDecrypter();
unpacker = new Unpacker(module);
unpacker.find(DeobfuscatedFile, this);
initializeObfuscatorName();
}
void initializeObfuscatorName() {
var versionString = getVersionString();
if (string.IsNullOrEmpty(versionString))
obfuscatorName = DeobfuscatorInfo.THE_NAME;
else
obfuscatorName = string.Format("{0} {1}", DeobfuscatorInfo.THE_NAME, versionString);
}
string getVersionString() {
var versionProviders = new IVersionProvider[] {
jitMethodsDecrypter,
memoryMethodsDecrypter,
proxyCallFixer,
antiDebugger,
antiDumping,
resourceDecrypter,
constantsDecrypterV18,
constantsDecrypterV17,
constantsDecrypterV15,
stringDecrypter,
unpacker,
};
var vd = new VersionDetector();
foreach (var versionProvider in versionProviders) {
if (versionProvider == null)
continue;
int minRev, maxRev;
if (versionProvider.getRevisionRange(out minRev, out maxRev))
vd.addRevs(minRev, maxRev);
}
return vd.getVersionString();
}
byte[] getFileData() {
@ -313,6 +351,10 @@ namespace de4dot.code.deobfuscators.Confuser {
public override void deobfuscateBegin() {
base.deobfuscateBegin();
var versionString = getVersionString();
if (!string.IsNullOrEmpty(versionString))
Log.v("Detected version: {0}", versionString);
removeObfuscatorAttribute();
initializeConstantsDecrypterV18();
initializeConstantsDecrypterV17();

View File

@ -0,0 +1,24 @@
/*
Copyright (C) 2011-2012 de4dot@gmail.com
This file is part of de4dot.
de4dot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
de4dot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with de4dot. If not, see <http://www.gnu.org/licenses/>.
*/
namespace de4dot.code.deobfuscators.Confuser {
interface IVersionProvider {
bool getRevisionRange(out int minRev, out int maxRev);
}
}

View File

@ -58,6 +58,8 @@ namespace de4dot.code.deobfuscators.Confuser {
public JitMethodsDecrypter(ModuleDefinition module, ISimpleDeobfuscator simpleDeobfuscator, JitMethodsDecrypter other)
: base(module, simpleDeobfuscator, other) {
if (other != null)
this.version = other.version;
}
protected override bool checkType(TypeDefinition type, MethodDefinition initMethod) {
@ -689,5 +691,40 @@ namespace de4dot.code.deobfuscators.Confuser {
sb.Append((char)(reader.ReadUInt16() ^ key5));
return sb.ToString();
}
public override bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v17_r73404:
minRev = 73404;
maxRev = 73430;
return true;
case ConfuserVersion.v17_r73477:
minRev = 73477;
maxRev = 73477;
return true;
case ConfuserVersion.v17_r73479:
minRev = 73479;
maxRev = 73822;
return true;
case ConfuserVersion.v17_r74021:
minRev = 74021;
maxRev = 75369;
return true;
case ConfuserVersion.v18_r75402:
minRev = 75402;
maxRev = int.MaxValue;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -46,6 +46,8 @@ namespace de4dot.code.deobfuscators.Confuser {
public MemoryMethodsDecrypter(ModuleDefinition module, ISimpleDeobfuscator simpleDeobfuscator, MemoryMethodsDecrypter other)
: base(module, simpleDeobfuscator, other) {
if (other != null)
this.version = other.version;
}
protected override bool checkType(TypeDefinition type, MethodDefinition initMethod) {
@ -381,5 +383,45 @@ namespace de4dot.code.deobfuscators.Confuser {
}
return true;
}
public override bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v14_r57884:
minRev = 57884;
maxRev = 57884;
return true;
case ConfuserVersion.v14_r58004:
minRev = 58004;
maxRev = 58446;
return true;
case ConfuserVersion.v14_r58564:
minRev = 58564;
maxRev = 58919;
return true;
case ConfuserVersion.v15_r59014:
minRev = 59014;
maxRev = 70489;
return true;
case ConfuserVersion.v16_r71742:
minRev = 71742;
maxRev = 72989;
return true;
case ConfuserVersion.v17_r73605:
minRev = 73605;
maxRev = int.MaxValue;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -27,7 +27,7 @@ using de4dot.blocks;
using de4dot.PE;
namespace de4dot.code.deobfuscators.Confuser {
abstract class MethodsDecrypterBase {
abstract class MethodsDecrypterBase : IVersionProvider {
protected ModuleDefinition module;
protected ISimpleDeobfuscator simpleDeobfuscator;
protected MethodDefinition initMethod;
@ -64,6 +64,8 @@ namespace de4dot.code.deobfuscators.Confuser {
return DeobUtils.lookup(module, def, errorMessage);
}
public abstract bool getRevisionRange(out int minRev, out int maxRev);
public void find() {
find(DotNetUtils.getModuleTypeCctor(module));
}

View File

@ -27,7 +27,7 @@ using de4dot.blocks;
using de4dot.PE;
namespace de4dot.code.deobfuscators.Confuser {
class ProxyCallFixer : ProxyCallFixer2 {
class ProxyCallFixer : ProxyCallFixer2, IVersionProvider {
MethodDefinitionAndDeclaringTypeDict<ProxyCreatorInfo> methodToInfo = new MethodDefinitionAndDeclaringTypeDict<ProxyCreatorInfo>();
FieldDefinitionAndDeclaringTypeDict<List<MethodDefinition>> fieldToMethods = new FieldDefinitionAndDeclaringTypeDict<List<MethodDefinition>>();
string ourAsm;
@ -945,5 +945,59 @@ namespace de4dot.code.deobfuscators.Confuser {
cctor.Body.Instructions.Clear();
cctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
}
public bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v10_r42915:
minRev = 42915;
maxRev = 48509;
return true;
case ConfuserVersion.v10_r48717:
minRev = 48717;
maxRev = 58446;
return true;
case ConfuserVersion.v14_r58564:
minRev = 58564;
maxRev = 58852;
return true;
case ConfuserVersion.v14_r58857:
minRev = 58857;
maxRev = 73605;
return true;
case ConfuserVersion.v17_r73740_normal:
case ConfuserVersion.v17_r73740_native:
minRev = 73740;
maxRev = 74637;
return true;
case ConfuserVersion.v17_r74708_normal:
case ConfuserVersion.v17_r74708_native:
minRev = 74708;
maxRev = 75349;
return true;
case ConfuserVersion.v18_r75367_normal:
case ConfuserVersion.v18_r75367_native:
minRev = 75367;
maxRev = 75926;
return true;
case ConfuserVersion.v19_r76101_normal:
case ConfuserVersion.v19_r76101_native:
minRev = 76101;
maxRev = int.MaxValue;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -25,7 +25,7 @@ using Mono.Cecil.Cil;
using de4dot.blocks;
namespace de4dot.code.deobfuscators.Confuser {
class ResourceDecrypter {
class ResourceDecrypter : IVersionProvider {
ModuleDefinition module;
ISimpleDeobfuscator simpleDeobfuscator;
MethodDefinition handler;
@ -386,5 +386,40 @@ namespace de4dot.code.deobfuscators.Confuser {
return;
ConfuserUtils.removeResourceHookCode(blocks, handler);
}
public bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v14_r55802:
minRev = 55802;
maxRev = 72989;
return true;
case ConfuserVersion.v17_r73404:
minRev = 73404;
maxRev = 73791;
return true;
case ConfuserVersion.v17_r73822:
minRev = 73822;
maxRev = 75349;
return true;
case ConfuserVersion.v18_r75367:
minRev = 75367;
maxRev = 75367;
return true;
case ConfuserVersion.v18_r75369:
minRev = 75369;
maxRev = int.MaxValue;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -26,7 +26,7 @@ using Mono.Cecil.Cil;
using de4dot.blocks;
namespace de4dot.code.deobfuscators.Confuser {
class StringDecrypter {
class StringDecrypter : IVersionProvider {
ModuleDefinition module;
MethodDefinition decryptMethod;
EmbeddedResource resource;
@ -449,5 +449,41 @@ namespace de4dot.code.deobfuscators.Confuser {
public string decrypt(MethodDefinition caller, int magic) {
return decrypter.decrypt(caller, magic);
}
public bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v10_r42915:
minRev = 42915;
maxRev = 48771;
return true;
case ConfuserVersion.v10_r48832:
minRev = 48832;
maxRev = 49238;
return true;
case ConfuserVersion.v11_r49299:
minRev = 49299;
maxRev = 58741;
return true;
case ConfuserVersion.v13_r55604_safe:
minRev = 55604;
maxRev = 58741;
return true;
case ConfuserVersion.v14_r58802_safe:
case ConfuserVersion.v14_r58802_dynamic:
minRev = 58802;
maxRev = 60408;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -64,7 +64,7 @@ namespace de4dot.code.deobfuscators.Confuser {
}
}
class Unpacker {
class Unpacker : IVersionProvider {
ModuleDefinition module;
EmbeddedResource mainAsmResource;
uint key0, key1;
@ -537,5 +537,65 @@ namespace de4dot.code.deobfuscators.Confuser {
return;
ConfuserUtils.removeResourceHookCode(blocks, asmResolverMethod);
}
public bool getRevisionRange(out int minRev, out int maxRev) {
switch (version) {
case ConfuserVersion.Unknown:
minRev = maxRev = 0;
return false;
case ConfuserVersion.v10_r42915:
minRev = 42915;
maxRev = 58446;
return true;
case ConfuserVersion.v14_r58564:
minRev = 58564;
maxRev = 58741;
return true;
case ConfuserVersion.v14_r58802:
minRev = 58802;
maxRev = 58817;
return true;
case ConfuserVersion.v14_r58852:
minRev = 58852;
maxRev = 60408;
return true;
case ConfuserVersion.v15_r60785:
minRev = 60785;
maxRev = 72989;
return true;
case ConfuserVersion.v17_r73404:
minRev = 73404;
maxRev = 73430;
return true;
case ConfuserVersion.v17_r73477:
minRev = 73477;
maxRev = 75056;
return true;
case ConfuserVersion.v17_r75076:
minRev = 75076;
maxRev = 75158;
return true;
case ConfuserVersion.v18_r75184:
minRev = 75184;
maxRev = 75349;
return true;
case ConfuserVersion.v18_r75367:
minRev = 75367;
maxRev = int.MaxValue;
return true;
default: throw new ApplicationException("Invalid version");
}
}
}
}

View File

@ -0,0 +1,108 @@
/*
Copyright (C) 2011-2012 de4dot@gmail.com
This file is part of de4dot.
de4dot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
de4dot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with de4dot. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
namespace de4dot.code.deobfuscators.Confuser {
class VersionDetector {
int minRev = -1, maxRev = int.MaxValue;
static readonly int[] revs = new int[] {
42519, 42915, 42916, 42917, 42919, 42960, 43055, 45527,
48493, 48509, 48717, 48718, 48771, 48832, 48863, 49238,
49299, 49300, 49966, 50359, 50378, 50661, 54225, 54254,
54312, 54431, 54564, 54566, 54574, 55346, 55604, 55608,
55609, 55764, 55802, 56535, 57588, 57593, 57699, 57778,
57884, 58004, 58172, 58446, 58564, 58741, 58802, 58804,
58817, 58852, 58857, 58919, 59014, 60052, 60054, 60111,
60408, 60785, 60787, 61954, 62284, 64574, 65189, 65282,
65297, 65298, 65299, 65747, 66631, 66853, 66883, 67015,
67058, 69339, 69666, 70489, 71742, 71743, 71847, 72164,
72434, 72819, 72853, 72868, 72989, 73404, 73430, 73477,
73479, 73566, 73593, 73605, 73740, 73764, 73770, 73791,
73822, 74021, 74184, 74476, 74482, 74520, 74574, 74578,
74637, 74708, 74788, 74816, 74852, 75056, 75076, 75077,
75131, 75152, 75158, 75184, 75257, 75267, 75288, 75291,
75306, 75318, 75349, 75367, 75369, 75402, 75459, 75461,
75573, 75719, 75720, 75725, 75806, 75807, 75926, 76101,
76119, 76163, 76186,
};
static Dictionary<int, Version> revToVersion = new Dictionary<int, Version> {
{ 42519, new Version(1, 0) }, // May 01 2010
{ 49299, new Version(1, 1) }, // Jul 13 2010
{ 50661, new Version(1, 2) }, // Jul 23 2010
{ 54574, new Version(1, 3) }, // Aug 31 2010
{ 55609, new Version(1, 4) }, // Sep 15 2010
{ 58919, new Version(1, 5) }, // Nov 29 2010
{ 60787, new Version(1, 6) }, // Jan 10 2011
{ 72989, new Version(1, 7) }, // Mar 09 2012
{ 75131, new Version(1, 8) }, // May 31 2012
{ 75461, new Version(1, 9) }, // Jun 23 2012
};
static VersionDetector() {
Version currentVersion = null;
int prevRev = -1;
foreach (var rev in revs) {
if (rev <= prevRev)
throw new ApplicationException();
Version version;
if (revToVersion.TryGetValue(rev, out version))
currentVersion = version;
else if (currentVersion == null)
throw new ApplicationException();
else
revToVersion[rev] = currentVersion;
}
}
public void addRevs(int min, int max) {
if (min < 0 || max < 0 || min > max)
throw new ArgumentOutOfRangeException();
if (!revToVersion.ContainsKey(min) || (max != int.MaxValue && !revToVersion.ContainsKey(max)))
throw new ArgumentOutOfRangeException();
if (min > minRev)
minRev = min;
if (max < maxRev)
maxRev = max;
}
public string getVersionString() {
if (minRev > maxRev || minRev < 0)
return null;
var minVersion = revToVersion[minRev];
if (maxRev == int.MaxValue)
return string.Format("v{0}.{1}+ (r{2}+)", minVersion.Major, minVersion.Minor, minRev);
var maxVersion = revToVersion[maxRev];
if (minVersion == maxVersion) {
if (minRev == maxRev)
return string.Format("v{0}.{1} (r{2})", minVersion.Major, minVersion.Minor, minRev);
return string.Format("v{0}.{1} (r{2}-r{3})", minVersion.Major, minVersion.Minor, minRev, maxRev);
}
return string.Format("v{0}.{1} - v{2}.{3} (r{4}-r{5})", minVersion.Major, minVersion.Minor, maxVersion.Major, maxVersion.Minor, minRev, maxRev);
}
public override string ToString() {
return getVersionString() ?? "<no version>";
}
}
}