1. Preparation

Before we begin the walkthrough, please ensure to have the following:

  1. The binary challenge
  2. Ghidra or IDA for static analysis
  3. GDB for dynamic analysis

2. Static Analysis in Ghidra

To objectively narrow down the scope, we can cheat a little by searching for “picoCTF” strings in the binary.

ghidra-flag-strings
Flag Substring Search

From the figure above, Ghidra suggests that the string is found at 0x00102005.

Clicking on it leads us to where the string was defined. However, you will notice that the flag is incomplete, and it is referenced in main().

ghidra-string-definition-offset
Flag Suffix Definition

Again, click on the reference address, and it will bring us to main().

partial-flag-reference-in-main
Partial Flag Reference in main()

Looking at the decompiled code, the first part of the flag is assigned to the local_248, which is a pointer to memory in heap, where the constructed string will be placed. Subsequently, other strings are defined as well. (See Listing 1)

undefined8 main(void) { 
  ...
  std::__cxx11::string::string(local_248,"picoCTF{wELF_d0N3_mate_",&local_249);
  ...
  std::__cxx11::string::string(local_228,"0",&local_249)
  ...
  std::__cxx11::string::string(local_208,"5",&local_249);
  ...
}
Strings Initialization

After initializing a bunch of strings, it proceeds to append the other defined strings to the flag through a bunch of checks.

undefined8 main(void) { 
  ...
  if (*pcVar2 < 'B') {
    std::__cxx11::string::operator+=(local_248,local_c8);
  }
  pcVar2 = (char *)std::__cxx11::string::operator[]((ulong)local_a8);
  if (*pcVar2 != 'A') {
    std::__cxx11::string::operator+=(local_248,local_68);
  }
  ...
  if ((int)cVar1 - (int)*pcVar2 == 3) {
    std::__cxx11::string::operator+=(local_248,local_1c8);
  }
  ...
  // Flag's ending tag is appended
  std::__cxx11::string::operator+=(local_248,'}');
  ...
  // Truncated: Calls to string deconstructor
}
Flag Construction

There are a few things we need to figure out before proceeding to dynamic analysis with GDB:

  1. What is the base address of main()?
    0x00101289
    
  2. Which instruction appends the flag’s ending tag?
    0010185b e8 a0 f8        CALL       <EXTERNAL>::std::__cxx11::string::operator+=
              ff ff
    
  3. What is that instruction’s offset from main()?
    >>> hex(0x0010185b - 0x00101289)
    0x5d2
    

3. Dynamic Analysis in GDB

3.1 Method 1 - Stepping Over Last String Concat

Start GDB by loading the binary and break on main() + 0x5d2, before running the binary.

$ gdb ./bin
(gdb) set pag off
(gdb) set disassembly-flavor intel
(gdb) set output-radix 16
(gdb) b *(main + 0x5d2)
(gdb) run
Setting Breakpoint & Run

Once our breakpoint is hit, ensure that we really are at the correct instruction by listing 3 instructions before RIP and 6 instructions after RIP.

Tip
$pc is an alias for $rip

Breakpoint 1, 0x000055555555585b in main ()
(gdb) x/10i $pc-15
   0x55555555584c <main+1475>:  lea    rax,[rbp-0x240]
   0x555555555853 <main+1482>:  mov    esi,0x7d
   0x555555555858 <main+1487>:  mov    rdi,rax
=> 0x55555555585b <main+1490>:  call   0x555555555100 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEpLEc@plt>
   0x555555555860 <main+1495>:  mov    ebx,0x0
   0x555555555865 <main+1500>:  lea    rax,[rbp-0x40]
   0x555555555869 <main+1504>:  mov    rdi,rax
   0x55555555586c <main+1507>:  call   0x5555555550f0 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED1Ev@plt>
   0x555555555871 <main+1512>:  lea    rax,[rbp-0x60]
   0x555555555875 <main+1516>:  mov    rdi,rax
Checking Instructions

Looking at the listing above, the flag pointer is stored in RDI just before the call. We can verify this by examining the string that is pointed to by the address in RDI.

(gdb) x/gx $rdi
0x7fffffffd530: 0x000055555556b2d0

(gdb) x/s 0x000055555556b2d0
0x55555556b2d0: "picoCTF{wELF_d0N3_mate_e9da2c0e"

# For convenience we store this address into a variable
(gdb) set $flag = {unsigned long long}$rdi
Getting Address of Flag

For completeness, step over this call and print the flag again.

(gdb) ni
(gdb) x/s $flag
0x55555556b2d0: "picoCTF{wELF_d0N3_mate_e9da2c0e}"
Flag

If you are rather lazy and just want to get the flag before it is freed, you can set a breakpoint at the ret instruction. This happens to be at main+0x9ca.

(gdb) b *(main + 1504)
(gdb) run
Break At RET Instruction

When the breakpoint hits, identify where the start and end of the heap is.

(gdb) info proc mappings
...
0x0000555555559000 0x000055555557a000 0x21000            0x0                rw-p  [heap]
...
(gdb) set $heap_start = 0x0000555555559000
(gdb) set $heap_end = 0x000055555557a000 - 1
Get Start and End of Heap Memory Region

We are now able to search through the heap for the substring “pico” to get its address.

(gdb) find /b $heap_start, $heap_end, 'p', 'i', 'c', 'o'
0x55555556b2d0
1 pattern found.
Finding Address of Flag

As only 1 pattern is found, this becomes straightforward by examining the buffer stored in that address.

(gdb) x/s 0x55555556b2d0
0x55555556b2d0: "picoCTF{wELF_d0N3_mate_e9da2c0e}"
Flag

4. Resources