从 GO 二进制挑战中查找标志

逆向工程 补丁反转
2021-07-05 10:26:31

我有来自挑战的 GO 二进制文件(已经结束)。

我花了大约 3 天时间才找到没有结果的标志。

我正在尝试使用 gdb 并使用以下命令加载文件:

首先,试图找到入口点:

info files

获得入口点后,我做了break *<ENTRYPOINTADDR> 然后layout asm,然后我只是尝试确定何时需要在要求我输入密码的打印之前添加断点。

还尝试使用info frameshow registers和打印值,但没有找到。

我只想学习如何从这个 ELF 文件中找到标志

我很想知道如何解决它

GO二进制文件

2个回答

既然你已经花了很多时间,我就一步一步告诉你如何解决它。我建议您重复我所描述的步骤,因为最好的学习方法是练习。

工具

我将使用radare2来解决这个挑战,但当然也可以使用其他工具。我选择它的原因很简单:它识别go库函数,因此更容易找到可执行文件中有趣的部分。

静态分析

一开始,我们必须找出从哪里开始我们的分析。跑:

r2 -c aei -d revengme

在打开该程序的调试模式radare2然后键入aaa以分析整个二进制文件。可能需要一些时间;等到它完成。

现在,要转到该main函数,请键入:

s sym.go.main.main

您现在可以通过键入来查看其内容pdf您将在那里看到几个库函数。由于此时我们只知道程序打印一些东西并等待一些输入,因此寻找执行这些任务的例程是很自然的。要查找对函数打印输出的每个调用,您可以像这样使用radare2internal grep

pdf ~print

,'print'pdf输出中搜索模式您可以对Read执行相同的操作以查看sym.go.bufio.__Reader_.ReadLine.

由于分析程序控制流更容易graph mode,让我们通过键入V!然后切换到它G你会看到这样的事情:

r2GraphMode

您可以使用箭头进行导航以查看图形的其余部分。在上图中,我们可以发现两个有趣的函数:sym.go.bufio.__Reader_.ReadLine如前所述,sym.go.main.ObfStr这表明大多数重要的字符串都按照我们的预期进行了混淆

现在,在读取用户输入后,程序必须以某种方式检查它是否有效。所以我们正在寻找负责它的片段。 检查字符串有效性

在上图中,您可能会发现一个sym.go.runtime.memequal似乎在做这项工作的电话。你可能会在这里看到它的签名,但它基本上需要两个指向内存区域的指针和长度来比较它们。此外,仅当值 inrcx等于[rsp+20h]上矩形中所示时才会调用它

因此,我们可以得出结论,[rsp+20h]实际上包含我们试图获取的密码的长度。事实上,我们看到的更多:我们知道被比较的字符串的地址存储在哪里!这些分别[rsp][rsp+8h]我们的静态分析到此结束。我们现在可以进行动态的。

动态分析

我们radare2在调试模式下运行以便能够动态分析程序,现在我们将这样做。

我们只是将断点放在我们想要查看堆栈中存储的实际内存的位置。我们必须知道两件事:

  1. 我们的字符串的预期长度是多少。
  2. 以及它的样子。

因此,我们的兴趣点是出现在第二张图像上的那些点。由于它不是与位置无关的代码,因此它们的地址不会在不同的执行过程中发生变化。这些是:

  • 0x00488e99
  • 0x00488f68

要在那里放置断点,请使用db address命令。要继续执行直到命中,请使用dc. 系统会提示您输入密码,但可以输入任何内容——此时我们只想获取字符串的预期长度。打到第一个断点后,V!再次运行看看: r2StackView 在右上角可以看到堆栈内容。现在我们只对长度感兴趣,所以[rsp+20h]. 存储在那里的值显示在第三行 - 它是0x26,因此密码包含38字符。

现在,键入ood以重新启动应用程序(但是它将保留断点)。因为您知道密码长度38只是38在提示时键入随机字符。然后,当您遇到第二个断点时,您可以再次读取堆栈内容,您将看到两个字符串的地址:一个是您刚刚键入的,另一个是您想要的。

重要提示:指针将以小端格式显示,因此从最低有效字节开始,以最高有效字节结束。

您会对 感兴趣0xc000018240,它包含您要查找的字符串,即:

BSidesTLV{revenge is best served cold}

注2:为了radare2n特定字节视为当前位置的数据,可以使用Cd n命令。

bart1e 正确回答了您的问题,这只是他的回答的扩展,使用不同的工具ghidra并在不运行二进制文件的情况下完全静态地反转。

golang 使用不同的方法来调用函数,它在堆栈中提供一个内存槽用于函数的参数以及它可以从函数返回的多个值

例如,一个函数可以在同一个函数中返回两个整数的和和乘积,例如

func doMagic (x,y) { return x+y ,x*y )

所以粗略地反汇编看起来像
(它只是细节的简化,而不是绝对语法或使用确切类型)

它只是试图重申 golang 如何使用堆栈
而不是传统的x64 abi 寄存器或 x86 abi 内存/堆栈作为参数rax/eax 中的常规
返回

mov [stack] , x
mov [stack] , y
mov [stack] , &fret1
mov [stack] , &fret2
call doMagic

and inside doMagic 
t1 = x 
t2 = y
t3 = t1  + t2
t4 = t1  * t2
fret1 = t3
fret2 = t4
ret

所以阅读我发现golang在.gopclntab部分中嵌入了函数名称及其地址,并且正在重命名函数并使用下面的脚本成功重命名函数但反编译仍然看起来很糟糕

from ghidra import *
mem = currentProgram.getMemory()
start = mem.getBlock(".gopclntab").getStart()
provider = ghidra.app.util.bin.MemoryByteProvider(mem,start)
bread = ghidra.app.util.bin.BinaryReader(provider,True)
numfun = bread.readInt(8)
for i in range(0x10,numfun*0x10,0x10):  
    faddr = bread.readInt(bread.readInt(i+8))
    fname = bread.readAsciiString(bread.readInt(bread.readInt(i+8) + 8))
    print( "creating function at " + hex(faddr) + "\t" + fname )
    removeFunctionAt(toAddr(faddr))
    createFunction(toAddr(faddr),fname)

所以我四处寻找并找到了felberj 的一个 GitHub 条目,一个用于 golang 的 ghidra 脚本

下载了 9.04 版本的脚本,按照项目窗口中的指示将其复制到 ghidra/extension 目录执行 File->installExtension 并重新启动 ghidra

现在我这次再次导入了二进制文件,确保我使用了新添加的 golang 语言条目而不是默认的 gcc x64 ghidra 建议

除了重命名函数之外,此扩展还解码了返回类型并将参数的存储类型更改为自定义存储,并为所有参数和返回值分配了适当的堆栈地址。

现在 main.main 的反编译看起来更具可读性

void main.main(void)

{

  byte Wrong [17];
  byte EPas [20];
  byte good [30];
  byte Ans [38];
  byte retry [40];


  ppbVar1 = *(in_FS_OFFSET + 0xfffffff8) + 0x10;
  if (good + 4 < *ppbVar1 || good + 4 == *ppbVar1) {
    runtime.morestack_noctxt();
    main.main();
    return;
  }
  EPas._0_8_ = 0xc5d38ad8cfdec4ef;EPas._8_4_ = 0xda8ad8df;EPas._12_4_ = 0xddd9d9cb;
  EPas._16_4_ = 0x90ced8c5;
  Wrong[0] = 0xf9; Wrong._1_4_ = 0xc2dec7c5;Wrong._5_4_ = 0x8acdc4c3;Wrong._9_4_ = 0xdd8ad9c3;
  Wrong._13_4_ = 0xcdc4c5d8;
  Ans._0_6_ = 0xd9cfcec3f9e8;  Ans._6_2_ = 0xe6fe;  Ans._8_2_ = 0xd1fc;  Ans._10_4_ = 0xcfdccfd8;
  Ans._14_4_ = 0x8acfcdc4;  Ans._18_4_ = 0xc88ad9c3;  Ans._22_4_ = 0x8aded9cf;  Ans._26_4_ = 0xdcd8cfd9;
  Ans._30_4_ = 0xc98acecf;  Ans._34_4_ = 0xd7cec6c5;
  good._0_4_ = 0x8adfc5f3;  good._4_4_ = 0xc9cbd8e9;  good._8_4_ = 0x8acecfc1;  good._12_2_ = 0xdec3;
  good._14_2_ = 0x8a86;  good._16_2_ = 0x8aeb;  good._18_4_ = 0xc5d8cfe2;  good._22_4_ = 0x8ad9c38a;
  good._26_4_ = 0xc4d8c5c8;
  retry._0_8_ = 0xc5fd8ade8dc4c5ee;  retry._8_4_ = 0x86d3d8d8;  retry._12_4_ = 0xc6cff88a;
  retry._16_4_ = 0x8a86d2cb;  retry._20_4_ = 0xc6c3c2e9;  retry._24_4_ = 0xc4cb8ac6;
  retry._28_4_ = 0xd8fe8ace;  retry._32_4_ = 0xcbc28ad3;  retry._36_4_ = 0xd8cfced8;

  uStack224 = 0x14;
  rVar3 = main.ObfStr(EPas,0x14,0x14);
  uStack0000000000000020 = SUB168(rVar3,0);
  uStack0000000000000028 = SUB168(rVar3 >> 0x40,0);
  uStack0000000000000018 = runtime.convTstring(in_stack_fffffffffffffe10,in_stack_fffffffffffffe18);
  pdStack232 = &DAT_0049a600;
  puVar7 = 0x1;
  lVar9 = 1;
  fmt.Fprint(&PTR_DAT_004d3a60,DAT_0055b7f0,&pdStack232,1,1);
  uStack152 = DAT_0055b7e8;
  apuStack96[0] = 0x0;
  FUN_00451d15();
  lVar5 = 0x1000;
  lVar6 = 0x1000;
  runtime.makeslice(&DAT_0049a740,0x1000,0x1000);
  puStack184 = 0x0;
  puVar8 = puVar7;
  FUN_00451d15();
  uStack176 = 0x1000;
  uStack168 = 0x1000;
  ppuStack160 = &PTR_DAT_004d3a40;
  uStack112 = 0xffffffffffffffff;
  uStack104 = 0xffffffffffffffff;
  puStack184 = puVar7;
  apuStack96[0] = puVar7;
  FUN_0045207a();
  rVar2 = bufio.(*Reader).ReadLine(apuStack96);
  uStack0000000000000010 = SUB488(rVar2,0);
  uStack0000000000000018 = SUB488(rVar2 >> 0x40,0);
  uStack0000000000000020 = SUB488(rVar2 >> 0x80,0);
  uStack0000000000000028 = SUB488(rVar2 >> 0xc0,0);
  if (lStack480 != 0) {
    uStack208 = 0x11;
    rVar3 = main.ObfStr(Wrong,0x11,0x11);
    uStack0000000000000020 = SUB168(rVar3,0);
    uStack0000000000000028 = SUB168(rVar3 >> 0x40,0);
    uStack0000000000000018 = runtime.convTstring(puVar8,lVar9);
    if (lStack480 != 0) {
      lStack480 = *(lStack480 + 8);
    }
    pdStack216 = &DAT_0049a600;
    puVar8 = 0x2;
    lVar9 = 2;
    lStack200 = lStack480;
    rVar4 = fmt.Fprintln(&PTR_DAT_004d3a60,DAT_0055b7f0,&pdStack216,2,2);
    uStack0000000000000040 = SUB248(rVar4 >> 0x80,0);
  }
  rVar3 = main.ObfStr(Ans,0x26,0x26);
  uStack0000000000000020 = SUB168(rVar3,0);
  uStack0000000000000028 = SUB168(rVar3 >> 0x40,0);
  if ((lVar9 == lVar6) &&
     (uStack0000000000000020 = runtime.memequal(lVar5,puVar8,lVar6), puVar8 != '\0')) {
    uStack240 = 0x1e;
    rVar3 = main.ObfStr(good,0x1e,0x1e);
    uStack0000000000000020 = SUB168(rVar3,0);
    uStack0000000000000028 = SUB168(rVar3 >> 0x40,0);
    uStack0000000000000018 = runtime.convTstring(puVar8,lVar9);
    pdStack248 = &DAT_0049a600;
    fmt.Fprintln(&PTR_DAT_004d3a60,DAT_0055b7f0,&pdStack248,1,1);
    return;
  }
  uStack256 = 0x28;
  rVar3 = main.ObfStr(retry,0x28,0x28);
  uStack0000000000000020 = SUB168(rVar3,0);
  uStack0000000000000028 = SUB168(rVar3 >> 0x40,0);
  uStack0000000000000018 = runtime.convTstring(puVar8,lVar9);
  pdStack264 = &DAT_0049a600;
  fmt.Fprintln(&PTR_DAT_004d3a60,DAT_0055b7f0,&pdStack264,1,1);
  return;
}

所以你可以看到它打印了一个字符串输入密码读取一行将输入的密码与一个混淆的(带有字节 0xaa 的简单异或)实际密码进行比较并打印好,你是英雄或打印错误重试

反混淆例程是 main.obfstr() 它需要一个字节数组和长度到异或并返回一个异或字符串

基于这些观察,我们可以编写一个简单的异或脚本

将字节地址与 xor 和 length 和 xor 的结果进行异或以在不运行二进制文件的情况下获得通行证

这是天真的异或脚本(天真,因为有一次我在我的机器上测试时它为我运行,并且可能有无数无法预料的极端情况错误)

#Desc xors a memory block of len unsigned bytes with a single unsigend byte like (0xff ^ 0xaa)
#@author      blabb 
#@category    _NEW_
#@keybinding  
#@menupath    none
#@toolbar     

import ghidra
def hexdump( a ):
    for j in range(0,len(a),16):
        for i in range(j,j+16,1):
            if( i < len(a)):
                print ( "%02x " % a[i]),
            else:
                print ( "%02x " % 0 ),  
        for i in range(j,j+16,1):
            if( i < len(a)):
                print ( "%c" % chr( a[i] ) ),
            else:
                print ( " " ),
        print("\n")

baseaddr = askAddress( "XOR MEMORY","Enter Base Addreess")
xorby    = askInt    ( "XOR MEMORY","Enter Byte to xor with")
xorlen   = askInt    ( "XOR MEMORY","enter length of xor block")
res = []
provider = ghidra.app.util.bin.MemoryByteProvider(currentProgram.getMemory(),baseaddr)
br =       ghidra.app.util.bin.BinaryReader(provider,True)
for i in range(0,xorlen,1):
    res.append((br.readUnsignedByte(i) ^ xorby))
hexdump(res)

运行它并提供密码 0x4d3ae0,xorbyte 0xaa,len 0x26 的内存地址

我们可以轻松获得密码

xormem.py> Finished!
xormem.py> Running...
42  53  69  64  65  73  54  4c  56  7b  72  65  76  65  6e  67  B S i d e s T L V { r e v e n g 

65  20  69  73  20  62  65  73  74  20  73  65  72  76  65  64  e   i s   b e s t   s e r v e d 

20  63  6f  6c  64  7d  00  00  00  00  00  00  00  00  00  00    c o l d }                     

xormem.py> Finished!