Puzzle

I found this challenge from: https://safiire.github.io/blog/2019/01/07/integer-overflow-puzzle/

Most Int overflow challenges I found don't give you a binary, and instead just have you connect to a server. So the sources for these might be a bit different.

Let's take a look at the binary here:

$    file puzzle
puzzle: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4e7bd9eb9ab969b8ba61f3b6283f846934c74009, for GNU/Linux 3.2.0, not stripped
$    ./puzzle
Segmentation fault (core dumped)
$    ./puzzle 15935728

So we can see that we are dealing with a 64 bit binary, that appears to take in input via arguments.

Reversing

When we take a look at the main function in Ghidra, we see this:

undefined8 main(undefined8 argc,long argv)

{
  if (**(long **)(argv + 8) * 0x1064deadbeef4601 == -0x2efc72d1f84bda97) {
    system("/bin/sh");
  }
  return 0;
}

So we can see that it is taking our first argument, multiplying it by 0x1064deadbeef4601, then checking to see if it is equal to 0xD1038D2E07B42569. If it is, then it will run system("/bin/sh"). The reason why the offset is 8 from argv for our first argument, is realistically it's the second argument. The first is the process's name. Also the reason why it displays -0x2efc72d1f84bda97 instead of 0xD1038D2E07B42569 is because that is the signed representation of the unsigned value. Looking at the disassembly shows us the unsigned value:

        00101155 48 b8 01        MOV        RAX,0x1064deadbeef4601
                 46 ef be
                 ad de 64 10
        0010115f 48 0f af c2     IMUL       RAX,RDX
        00101163 48 ba 69        MOV        RDX,0xD1038D2E07B42569
                 25 b4 07
                 2e 8d 03 d1
        0010116d 48 39 d0        CMP        RAX,RDX

Exploitation

So we need to set the product of our input and 0x1064deadbeef4601 equal to 0xD1038D2E07B42569. We will do this using an Integer Overflow. First off let's talk a bit about how an Integer Overflow works.

Data types can only contain so much data. For x64 bit integers, they contain 8 bytes worth of data. So what happens if we try to store a value larger than 8 bytes in an integer? For instance:

0x1064deadbeef4601 * 0xD1038D2E07B42569 = 0xd629404f62e95bf5b815e3124f5db69

Thing is, in instances like this, it will only store the lower 8 bytes. So here, the result would be 0x5b815e3124f5db69. So realistically, the actual "equation" we need to solve is this:

(((argv[1] * 0x1064deadbeef4601) & 0xffffffffffffffff) == 0xD1038D2E07B42569)

We can write a simple z3 script to solve this for us:

from z3 import *

foREVer = Solver()

x = BitVec("0", 64)

foREVer.add(((x * 0x1064deadbeef4601) & 0xffffffffffffffff) == 0xd1038d2e07b42569)

if foREVer.check() == sat:
    solution = foREVer.model()
    solution = hex(int(str(solution[x])))
    solution = solution[2:]

    # We have to reverse the value because the binary is least endian
    value = ""
    i = len(solution) / 2
    while i > 0:
        i -= 1
        y = solution[(i*2):(i*2) + 2]
        value += chr(int("0x" + y, 16))

    print "Now I think, I understand: " + value
else:
    print "Not solvable, I would recommend crying, a lot"

When we run it:

$    python rev.py
Now I think, I understand: io64pass
$    ./puzzle io64pass
$ w
 11:01:12 up  1:31,  1 user,  load average: 1.62, 1.51, 1.27
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu :0       :0               09:29   ?xdm?   3:58   0.00s /usr/lib/gdm3/g
$ ls
puzzle    readme.md  rev.py

Just like that, we solved the challenge!