0

I have GO binary file from challenge (that already over).

I spent around 3 days to find the flag without results.

I'm trying to load the file with gdb and using the following command:

First, Trying to find the entry point:

info files

After I got the entry point I did break *<ENTRYPOINTADDR> Then layout asm and then I just try to figure when I need to add break point before the print that ask me to enter a password.

Also trying with info frame and show registers and to print the values but nothing found.

I just want to learn how to find the flag from this ELF file

I would love to know how to solve it

GO binary File

3

Since you have already spent a lot of time on it, I will just show you how to solve it step by step. I recommend you to repeat steps I'm describing, since the best way to learn is to practice.

Tool

I will use radare2 for solving this challenge, but it's of course possible to use other tools as well. The reason I chose it is simple: it recognises go library functions, so it will be easier to find interesting parts of the executable.

Static analysis

At the beginning, we have to find out where to start our analysis. Run:

r2 -c aei -d revengme

to open this program in debug mode in radare2. Then type aaa to analyse entire binary. It may take some time; just wait until it finishes.

Now, to go to the main function, type:

s sym.go.main.main

You may now see its contents by typing pdf. You will see several library functions there. Since, at this point we know only that the program prints something and waits for some input, it's natural to look for routines performing these tasks. To find every call to functions printing output, you can use radare2 internal grep like so:

pdf ~print

, to search for 'print' pattern in the pdf output. You may do the same thing with Read to see only one reference to sym.go.bufio.__Reader_.ReadLine.

Since analysing program control flow is easier in graph mode, let us switch to it by typing V! and then G. You will see something like this:

r2GraphMode

and you may navigate using arrows to see the rest of the graph. In the image above, we can spot two interesting functions: sym.go.bufio.__Reader_.ReadLine as stated previously and sym.go.main.ObfStr that suggests that most important strings are obfuscated as we could have expected.

Now, after reading user input the program has to somehow check if it's valid. So we are looking for the fragment responsible for it. checkingForStringValidity

On the image above, you may spot a call to sym.go.runtime.memequal that seems to do this job. You may see its signature here, but it basically takes two pointers to memory regions and the length to compare them. Additionally, it is only called when value in rcx is equal to [rsp+20h] as shown in the upper rectangle.

Hence, we may conclude that [rsp+20h] actually contains the length of the password we are trying to obtain. In fact we see even more: we know where the addresses of strings being compared are stored! These are [rsp] and [rsp+8h] respectively. And that is the end of our static analysis. We may now proceed to dynamic one.

Dynamic analysis

We run radare2 in debug mode to be able to dynamically analyse the program and now we will do so.

We just put the breakpoints where we want to see actual memory stored on the stack. We have to know two things:

  1. What is expected length of our string.
  2. And how it looks like.

Hence our points of interest are those two presented on the second image. Since it isn't position independent code, their addresses will not change throughout different executions. These are:

  • 0x00488e99
  • 0x00488f68

To put breakpoints there, use db address command. To continue execution until they are hit, use dc. You will be prompted to write the password, but write anything - at this moment we only want to get the expected length of the string. After the first breakpoint is hit, run V! once again to see: r2StackView On the upper right corner you can see the stack contents. Now we are only interested in length, so [rsp+20h]. The value stored there is displayed in the third row - it is 0x26, so the password contains 38 characters.

Now, type ood to restart the application (it will preserve the breakpoints however). Since you know that the password length is 38 just type 38 random characters when you are prompted to. Then, when you hit the second breakpoint, you may read the stack contents once again and you will see the addresses of two strings: one that you have just typed, and the second, desired one.

Important note: the pointers will be shown in little endian format, so starting with the least significant byte and ending with the most significant one.

You will be interested in 0xc000018240, that contains the string you are looking for, that is:

BSidesTLV{revenge is best served cold}

Note2: To cause radare2 to treat n specific bytes as data in current location, you can use Cd n command.

| improve this answer | |
  • Thank you!!!!!! – user28772 Jun 21 '19 at 12:58
1

bart1e answered your question right this is just an extension to his answer with a different tool ghidra and reversing purely statically without running the binary.

golang uses a different approach to calling functions it provides a memory slot in stack for arguments to the functions as well as multiple values it can return from a function

for example a function can return a sum and product of two integers in the same function like

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

so roughly the disassembly will look like
( it is just a simplification of details not absolute syntax or exact types are used )

it just tries to reiterate how golang uses stack
instead of traditional x64 abi registers or x86 abi memory / stack for arguments and conventional
returns in 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

so reading around I found golang embeds the function names and its address in .gopclntab section and was fooling around with renaming the functions and successfully renamed the functions with this hack of script below but the decompilation was still looking a horrible mess

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)

so i scoured around and hit upon a GitHub entry by felberj a ghidra script for golang

downloaded the script for 9.04 version copied it to ghidra/extension dir as instructed in the project window did File->installExtension and restarted ghidra

now i imported the binary again this time making sure i used the newly added golang language entry instead of default gcc x64 ghidra proposes

and apart from renaming functions this extension also decoded the return types and changed the storage type of arguments to custom storage and assigned proper stack address to all the arguments and return values.

now the decompilation of main.main was looking a bit more readable

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;
}

so as you can see it prints a string Enter password reads a line compares the entered pass with a obfuscated (simple xor with byte 0xaa ) actual password and prints good , you are a hero or prints wrong retry

the deobfuscation routine is main.obfstr() it takes a bytearray and length to xor and returns a xorred string back

based on these observations we can write a simple xor script

that takes an address the byte to xor and length and xor the results to get the pass without running the binary

here is naive xor script (naïve because it ran for me the one time i tested in my machine and can have innumerable unanticipated corner case bugs)

#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)

running this and providing the memory address for password 0x4d3ae0,xorbyte 0xaa,len 0x26

we can easily get the password

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!
| improve this answer | |

Not the answer you're looking for? Browse other questions tagged or ask your own question.