Post

Toddler

Crashing a C++ binary

Toddler

Description

You don’t have to exploit this, just crash it… SIGSEGV FTW!. The binary has been stolen and is attached to this challenge. If you need inspiration, try this.

It’s running here:

1
2
ssh toddler@babyre.youcanhack.me
password: NoWhiningAllowed

Writeup

As always, I first disassembled the binary in Ghidra, which resulted in the following main function:

main function main function

At first glance, this seemed to take in input from the user and run the do_line function on it, which looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
void do_line(char *line)

{
  bool bVar1;
  bool bVar2;
  bool bVar3;
  size_t sVar4;
  char *__s1;
  char *pcVar5;
  int iVar6;
  char *opt;
  
  sVar4 = strlen(line);
  if (0 < (int)sVar4) {
    iVar6 = 0;
    bVar2 = false;
    bVar3 = false;
    pcVar5 = (char *)0x0;
    __s1 = line;
    do {
      bVar1 = (bool)(bVar3 | bVar2);
      if (bVar1) {
        if (*__s1 != '\0') {
          if (bVar3) {
            if ((*__s1 != ' ') && (pcVar5 = __s1, bVar3 = bVar2, bVar2)) {
              opt = line + iVar6;
              goto LAB_00101db7;
            }
          }
          else {
            opt = __s1;
            bVar2 = bVar1;
            if (*__s1 != ' ') goto LAB_00101cc7;
          }
        }
      }
      else {
        bVar2 = bVar1;
        bVar3 = bVar1;
        if (*__s1 == ' ') {
          if (pcVar5 == (char *)0x0) {
            *__s1 = '\0';
            bVar3 = true;
          }
          else {
            *__s1 = '\0';
            bVar2 = true;
          }
        }
      }
      iVar6 = iVar6 + 1;
      __s1 = __s1 + 1;
    } while ((int)sVar4 != iVar6);
    opt = (char *)0x0;
LAB_00101cc7:
    __s1 = pcVar5;
    if (__s1 != (char *)0x0) {
LAB_00101db7:
      iVar6 = get_number(line);
      if (iVar6 != 0) {
        return;
      }
      iVar6 = strcmp(__s1,"create");
      if (iVar6 == 0) {
        do_create(__s1,opt);
        return;
      }
      iVar6 = strcmp(__s1,"delete");
      if (iVar6 == 0) {
        do_delete(__s1,opt);
        return;
      }
      iVar6 = strcmp(__s1,"speak");
      if (iVar6 != 0) {
        iVar6 = strcmp(__s1,"rollover");
        if (iVar6 == 0) {
          do_rollover(__s1,opt);
          return;
        }
        iVar6 = strcmp(__s1,"debug");
        if (iVar6 != 0) {
          iVar6 = strcmp(__s1,"name");
          if (iVar6 == 0) {
            do_name(__s1,opt);
            return;
          }
          put("invalid cmd\n");
          return;
        }
        do_debug(__s1,opt);
        return;
      }
      do_speak(__s1,opt);
      return;
    }
  }
  puts("no command?");
  return;
}

It appeared to be some sort of command parser. Running the binary confirmed as much, but it seemed to want a very specific input in order to progress beyond “no command?”.

First run

By playing around with the binary and by looking at the decompilation of main, do_line and some of the individaual command functions, I was able to determine that the program wanted input in this format:

1
<sequence number> <command> <args>

The sequence number needs to increment by one each time you send a command (but can start at any arbitrary value). The command is any command seen in do_line, and the required arguments vary by which command is being run. For example:

Example commands

The available commands are as follows:

  • create <type>: create a new pet of kind type
  • delete <i>: delete the pet at index i
  • speak: print status of all pets “speaking”
  • rollover: print status of all pets “rolling over”
  • debug: print some sort of “debug” data about each pet
  • name <i> <name>: give name name to the pet at index i

Getting to this point comprised a significant portion of the time it took me to solve this challenge. There was a lot of code to look through, so (mostly) understanding what it did enough to use the program was a challenge.

However, once I knew what all the commands did, I knew it was likely that the bug was in the implementation of one of them. At first, the delete command stood out to me since I wanted to see if you could make it delete something it shouldn’t be able to in order to cause a seg fault. I noticed that the mechanism for parsing the number from the string was custom made and did not use a C standard library function. Because of this, I discovered that it would let you input characters with values less than the ASCII numbers, like /, to cause the unsigned character value to roll over to 0xff. However, after some more digging, I determined that this wasn’t actually exploitable due to value checks later in the function.

delete function do_delete function

This then gave me the idea to look at any other commands that took i as an argument, since they would allow you to modify existing data perhaps in unintended ways. This left the name command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
void do_name(char *cmd,char *opt)

{
  char cVar1;
  Pet *pPVar2;
  size_t sVar3;
  size_t sVar4;
  char *pcVar5;
  long lVar6;
  long lVar7;
  int x;
  undefined8 *puVar8;
  undefined8 *puVar9;
  uint uVar10;
  ulong uVar11;
  byte bVar12;
  
  bVar12 = 0;
  if (opt == (char *)0x0) {
    puts("missing option");
  }
  else {
    cVar1 = *opt;
    x = 0;
    lVar6 = 1;
    if ((byte)(cVar1 - 0x30U) < 10) {
      do {
        lVar7 = lVar6;
        x = cVar1 + -0x30 + x * 10;
        cVar1 = opt[lVar7];
        if (cVar1 == '\0') goto LAB_0010181c;
        lVar6 = lVar7 + 1;
      } while ((byte)(cVar1 - 0x30U) < 10);
      if (cVar1 == ' ') {
        puVar8 = (undefined8 *)(opt + lVar7 + 1);
        if (*(char *)puVar8 != '\0') {
          if (myList.n <= x) {
            put("you don\'t have that many pets\n");
            return;
          }
          pPVar2 = petlist::nth(&myList,x);
          if (pPVar2 == (Pet *)0x0) {
            return;
          }
          sVar3 = strlen((char *)puVar8);
          if (199 < sVar3) {
            return;
          }
          sVar4 = strlen(pPVar2->name);
          pcVar5 = pPVar2->name + sVar4;
          uVar11 = sVar3 + 1;
          uVar10 = (uint)uVar11;
          if (uVar10 < 8) {
            if ((uVar11 & 4) != 0) {
              *(undefined4 *)pcVar5 = *(undefined4 *)puVar8;
              *(undefined4 *)(pcVar5 + ((uVar11 & 0xffffffff) - 4)) =
                   *(undefined4 *)((long)puVar8 + ((uVar11 & 0xffffffff) - 4));
              return;
            }
            if (uVar10 == 0) {
              return;
            }
            *pcVar5 = *(char *)puVar8;
            if ((uVar11 & 2) == 0) {
              return;
            }
            *(undefined2 *)(pcVar5 + ((uVar11 & 0xffffffff) - 2)) =
                 *(undefined2 *)((long)puVar8 + ((uVar11 & 0xffffffff) - 2));
            return;
          }
          *(undefined8 *)pcVar5 = *puVar8;
          *(undefined8 *)(pcVar5 + ((uVar11 & 0xffffffff) - 8)) =
               *(undefined8 *)((long)puVar8 + ((uVar11 & 0xffffffff) - 8));
          lVar6 = (long)pcVar5 - (long)(undefined8 *)((ulong)(pcVar5 + 8) & 0xfffffffffffffff8);
          puVar8 = (undefined8 *)((long)puVar8 - lVar6);
          puVar9 = (undefined8 *)((ulong)(pcVar5 + 8) & 0xfffffffffffffff8);
          for (uVar11 = (ulong)(uVar10 + (int)lVar6 >> 3); uVar11 != 0; uVar11 = uVar11 - 1) {
            *puVar9 = *puVar8;
            puVar8 = puVar8 + (ulong)bVar12 * -2 + 1;
            puVar9 = puVar9 + (ulong)bVar12 * -2 + 1;
          }
          return;
        }
LAB_0010181c:
        put("missing name\n");
        return;
      }
    }
    put("bad number\n");
  }
  return;
}

As I was playing around with the name command, I noticed something odd- when you named a pet twice, rather than the pet adopting the new name in place of the old one, it just added on the new name to the original one:

double name

This gave me the thought to see how long I could make the second name. In looking at the Pet class structure, I could see that the name was initialized to a maximum of 200 characters. While the do_name function does check that the input name is less than 200 characters long, because it adds it onto the existing name, we can overflow the value to cause the program to attempt to modify a restricted area of memory.

responsible code Vulnerable do_name code

I tested this locally, and success- SIGSEGV!

sigsegv

Solve

I ran it on remote to get the flag:

flag

This post is licensed under CC BY 4.0 by the author.