为了实现ip碎片重组以及流还原等功能,检测出分片发送的攻击报文。snort中进行了流缓存相关的操作。
这里首先在顺便提一下插件的初始化,在前面的章节中提到了,ips相关的插件,都是存在s_options
集合中,而在snort进行初始化的时候,同时也会调用IpsManager::setup_options();
对相关插件进行初始化。Snort::thread_init_unprivileged();
这个函数进行了一系列的初始化操作,其中就包括了插件的初始化,这个函数本身在Analyzer对象的run函数中执行
void IpsManager::setup_options()
{
for ( auto* p : s_options )
if ( p->init && p->api->tinit )
p->api->tinit(SnortConfig::get_conf());
}
而需要进行流缓存,是要进行一定的内存分配的,这里的内存预分配就是在流相关的模块初始化的地发进行的。
这里对应的模块的代码是在src/stream/base/stream_base.cc中
void StreamBase::tinit()
{
assert(!flow_con);
flow_con = new FlowControl;
InspectSsnFunc f;
StreamHAManager::tinit();
if ( config.ip_cfg.max_sessions )
{
if ( (f = InspectorManager::get_session(PROTO_BIT__IP)) )
flow_con->init_proto(PktType::IP, config.ip_cfg, f);
}
if ( config.icmp_cfg.max_sessions )
{
if ( (f = InspectorManager::get_session(PROTO_BIT__ICMP)) )
flow_con->init_proto(PktType::ICMP, config.icmp_cfg, f);
}
if ( config.tcp_cfg.max_sessions )
{
if ( (f = InspectorManager::get_session(PROTO_BIT__TCP)) )
flow_con->init_proto(PktType::TCP, config.tcp_cfg, f);
}
if ( config.udp_cfg.max_sessions )
{
if ( (f = InspectorManager::get_session(PROTO_BIT__UDP)) )
flow_con->init_proto(PktType::UDP, config.udp_cfg, f);
}
if ( config.user_cfg.max_sessions )
{
if ( (f = InspectorManager::get_session(PROTO_BIT__PDU)) )
flow_con->init_proto(PktType::PDU, config.user_cfg, f);
}
if ( config.file_cfg.max_sessions )
{
if ( (f = InspectorManager::get_session(PROTO_BIT__FILE)) )
flow_con->init_proto(PktType::FILE, config.file_cfg, f);
}
uint32_t max = config.tcp_cfg.max_sessions + config.udp_cfg.max_sessions
+ config.user_cfg.max_sessions;
if ( max > 0 )
flow_con->init_exp(max);
FlushBucket::set(config.footprint);
}
这里初始化了FlowControl对象,并调用init_proto
函数对各种协议类型相关的流进行了初始化。
void FlowControl::init_proto(
PktType type, const FlowConfig& fc, InspectSsnFunc get_ssn)
{
if ( !fc.max_sessions || !get_ssn )
return;
//这里首先根据类型获取相应的proto对象,这个数组本身的下标就是协议类型
auto& con = proto[to_utype(type)];
//新建流缓存对象并申请内存,顺带一提,snort在默认配置下,这部分占用了总共消耗的3/4甚至更多的内存,这里一条流对应一个flow对象
con.cache = new FlowCache(fc);
con.mem = (Flow*)snort_calloc(fc.max_sessions, sizeof(Flow));
for ( unsigned i = 0; i < fc.max_sessions; ++i )
con.cache->push(con.mem + i);
con.get_ssn = get_ssn;
types.emplace_back(type);
}
流缓存本身在stream模块中的process函数中被使用到,它是在eval函数中被执行的
bool FlowControl::process(PktType type, Packet* p)
{
auto& con = proto[to_utype(type)];
if ( !con.cache )
return false;
FlowKey key; //这里存着ip端口一类的信息
set_key(&key, p);
Flow* flow = con.cache->find(&key); //这里应该是就是根据包中的源目ip信息等,看看有没有对应的流
if ( !flow ) //这里进行了一些检查,应该是对这个包需不需要就行流缓存或者跟踪做的判断
{
if ( !want_flow(type, p) )
return true;
flow = con.cache->get(&key); //这里和find的主要区别是,flow有一个hash table存储,这里调用get会新建相关hash table的结点
if ( !flow )
return true;
}
if ( !flow->session ) //如果没有session这里会进行初始化
{
flow->init(type);
flow->session = con.get_ssn(flow);
}
con.num_flows += process(flow, p);
// FIXIT-M refactor to unlink_uni immediately after session
// is processed by inspector manager (all flows)
if ( flow->next && is_bidirectional(flow) )
con.cache->unlink_uni(flow);
return true;
}
接下来调用的process(flow, p);
函数实现如下
unsigned FlowControl::process(Flow* flow, Packet* p)
{
unsigned news = 0;
flow->previous_ssn_state = flow->ssn_state;
p->flow = flow;
p->disable_inspect = flow->is_inspection_disabled();
last_pkt_type = p->type();
preemptive_cleanup();
flow->set_direction(p);
flow->session->precheck(p);
if ( flow->flow_state != Flow::FlowState::SETUP )
{
set_inspection_policy(SnortConfig::get_conf(), flow->inspection_policy_id);
set_ips_policy(SnortConfig::get_conf(), flow->ips_policy_id);
set_network_policy(SnortConfig::get_conf(), flow->network_policy_id);
}
else
{
init_roles(p, flow);
DataBus::publish(FLOW_STATE_SETUP_EVENT, p);
if ( flow->flow_state == Flow::FlowState::SETUP ||
(flow->flow_state == Flow::FlowState::INSPECT &&
(!flow->ssn_client || !flow->session->setup(p))) )
flow->set_state(Flow::FlowState::ALLOW);
++news;
}
// This requires the packet direction to be set
if ( p->proto_bits & PROTO_BIT__MPLS )
flow->set_mpls_layer_per_dir(p);
if ( p->type() == PktType::PDU ) // FIXIT-H cooked or PDU?
DetectionEngine::onload(flow);
switch ( flow->flow_state )
{
case Flow::FlowState::SETUP:
flow->set_state(Flow::FlowState::ALLOW);
break;
case Flow::FlowState::INSPECT:
assert(flow->ssn_client);
assert(flow->ssn_server);
break;
case Flow::FlowState::ALLOW:
if ( news )
Stream::stop_inspection(flow, p, SSN_DIR_BOTH, -1, 0);
else
DetectionEngine::disable_all(p);
p->ptrs.decode_flags |= DECODE_PKT_TRUST;
break;
case Flow::FlowState::BLOCK:
if ( news )
Stream::drop_traffic(flow, SSN_DIR_BOTH);
else
Active::block_again();
DetectionEngine::disable_all(p);
break;
case Flow::FlowState::RESET:
if ( news )
Stream::drop_traffic(flow, SSN_DIR_BOTH);
else
Active::reset_again();
Stream::blocked_flow(flow, p);
DetectionEngine::disable_all(p);
break;
}
return news;
}
这里给相应的包对象设置流成员对象的值,而session 对应各种协议的会话 例如 tcp udp 等等, 在InspectorManager::execute中调用对应的process函数
p->flow->session->process(p);
例如tcpsession,它可能会调用 TcpEventLogger::log_tcp_events() 对相应的build in 事件进行触发,例如teardrop攻击的检测。
总的来说流缓存下来用于各种检查和分析,流本身对应一个flowkey,这里保存流的源目的ip地址一类的信息,需要处理或者获取对应流缓存实例的时候,均通过这个key,流缓存类有一个hashtable,通过它来存储。
上面提到的调用对应的process函数,而ip碎片的重组,就是通过调用int IpSession::process(Packet* p)
来实现的,其中会调用void Defrag::process(Packet* p, FragTracker* ft)
,进行碎片重组的操作,这里首先会检查是否过期等等,然后调用int Defrag::insert(Packet* p, FragTracker* ft, FragEngine* fe)
对碎片信息进行检查,例如teardrop攻击的检测。并且找到(如果存在的话)当前包的左侧和右侧的包,这里处理的是包可能存在部分重叠的问题。然后会调用int Defrag::add_frag_node
函数,将包插入FragTracker的链表中。
int Defrag::add_frag_node(
FragTracker* ft,
FragEngine*,
const uint8_t* fragStart,
int16_t fragLength,
char lastfrag,
int16_t len,
uint16_t slide,
uint16_t trunc,
uint16_t frag_offset,
Fragment* left,
Fragment** retFrag)
{
Fragment* newfrag = nullptr; /* new frag container */
int16_t newSize = len - slide - trunc;
if (newSize <= 0)
{
/*
* zero size frag
*/
trace_logf(stream_ip,
"zero size frag after left & right trimming "
"(len: %d slide: %d trunc: %d)\n",
len, slide, trunc);
ip_stats.discards++;
#ifdef DEBUG_MSGS
newfrag = ft->fraglist;
while (newfrag)
{
trace_logf(stream_ip,
"Size: %d, offset: %d, len %d, "
"Prev: 0x%p, Next: 0x%p, This: 0x%p, Ord: %d, %s\n",
newfrag->size, newfrag->offset,
newfrag->flen, (void*) newfrag->prev,
(void*) newfrag->next, (void*) newfrag, newfrag->ord,
newfrag->last ? "Last" : "");
newfrag = newfrag->next;
}
#endif
return FRAG_INSERT_ANOMALY;
}
newfrag = new Fragment(fragLength, fragStart, ft->ordinal++);
/*
* twiddle the frag values for overlaps
*/
newfrag->data = newfrag->fptr + slide;
newfrag->size = newSize;
newfrag->offset = frag_offset;
newfrag->last = lastfrag;
trace_logf(stream_ip,
"[+] Adding new frag, offset %d, size %d\n"
" nf->data = nf->fptr(%p) + slide (%d)\n"
" nf->size = len(%d) - slide(%d) - trunc(%d)\n",
newfrag->offset, newfrag->size, newfrag->fptr,
slide, fragLength, slide, trunc);
/*
* insert the new frag into the list
*/
add_node(ft, left, newfrag);
trace_logf(stream_ip,
"[*] Inserted new frag %d@%d ptr %p data %p prv %p nxt %p\n",
newfrag->size, newfrag->offset, (void*) newfrag, newfrag->data,
(void*) newfrag->prev, (void*) newfrag->next);
/*
* record the current size of the data in the fraglist
*/
ft->frag_bytes += newfrag->size;
trace_logf(stream_ip,
"[#] accumulated bytes on FragTracker %u, count"
" %d\n", ft->frag_bytes, ft->fraglist_count);
*retFrag = newfrag;
return FRAG_INSERT_OK;
}
首先根据碎片的起始地址和长度,新建fragment对象,然后调用add_node(ft, left, newfrag);
,将包对应的fragment对象插入链表中,这里会传入一个left对象,它是当前碎片左边的碎片包,如果不存在的话,则会将新建的这个frag的结点,作为FragTracker* ft这个里面的fraglist的第一个结点。
当int Defrag::insert(Packet* p, FragTracker* ft, FragEngine* fe)
被调用过后,process函数中会调用FragIsComplete
来检查碎片是否合并完全,这里首先会确认,frag的第一个和最后一个,是不是都到了,然后确认长度等信息是否已知,然后调用 FragRebuild,对包进行重建,以确保完成ip碎片整理的操作。
在重建完成后,调用
snort::Snort::process_packet(dpkt, dpkt->pkth, dpkt->pkt, true);
将这里的包传入snort的检测系统中,进行下一步的检测。
首先tcp会话创建和初始化类似上文中的ip重组,他们的接口是一样的,都是通过调用process函数。
重组模块的初始化在执行process后的TcpSession::set_os_policy()
中,首先创建了TcpSegmentDescriptor对象。
然后通过调用queue_packet_for_reassembly
对包进行重组,这里有两个参数TcpReassemblerState为重组时相关的信息,TcpSegmentDescriptor这里为对应seg的描述信息,包括流和包等成员对象
,如果seg_count为0 则将seg插入空的seglist中,seglist的结点为TcpSegmentNode对象,由上面两个参数共同的数据初始化完成,插入时还传入了left节点,这里如果没有left,则将传入的节点作为头节点
后续的left查找操作类似ip重组,这里的操作过程其实和ip流几乎是一样的。