简介
在这篇文章中,我们将为读者详细介绍我们的小组成员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”。
漏洞详情
关于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的请求,为此,它首先要找到以\r\n\r\n结尾的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 Continue\r\n\r\n"); 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 Continue\r\n\r\n" 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()函数——这正是我们的兴趣之所在。
// /!\ does not read from pCurrent pCurrent = strstr(buf_read, "Content-Length: "); if ( !pCurrent ) { // Handle chunked HTTP encoding ... } // no chunk encoding here, normal http request pCurrent += 0x10; pUnderscoreLQ_or_CRCL = strstr(pCurrent, "\r\n"); if ( !pUnderscoreLQ_or_CRCL ) return 0xFFFFFFFF; Underscore = *pUnderscoreLQ_or_CRCL; *pUnderscoreLQ_or_CRCL = 0; content_len = atoi(pCurrent); *pUnderscoreLQ_or_CRCL = Underscore; memset(recv_buf, 0, sizeof(recv_buf)); count_read = recv(client_sock, recv_buf, 8u, 0);// 8 bytes are read only initially if ( count_read != 8 ) return 0xFFFFFFFF; if ( (recv_buf[2] || recv_buf[3] != 2) && (recv_buf[2] || recv_buf[3] != 6) ) { ret_1 = do_airippWithContentLength(kc_client_, content_len, recv_buf); if ( ret_1 < 0 ) return 0xFFFFFFFF; return 0; } ...
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\0" 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 ... }
漏洞利用
已有的缓解措施
我们的目标是覆盖返回地址以获得$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/如若转载,请注明原文地址