우리 집에 GDB 있는데 메모리 보고 갈래? (2)
#include <string.h>
#include <stdio.h>
void func2() {
puts("func2()"); // "func2()"를 출력
}
void sum(int a, int b) { // int형 매개 변수 a, b를 받음
printf("sum : %d\n", a+b); // 더한 값을 출력
func2(); // func2 함수 실행
}
int main(int argc, char *argv[]) {
int num=0;
char arr[10];
sum(1,2); // sum 함수에 인자 1, 2를 보냄
strcpy(arr,argv[1]); // 실행시 입력 받은 인자를 arr로 복사(취약점 발생!!)
printf("arr: %s\n", arr); // arr 출력
if(num==1){ // num이 1이라면 쉘 실행
system("/bin/sh");
}
return 0;
}
2에서는 위 코드를 gcc로 실행파일로 만들어 gdb로 분석하며, 동시에 gdb 사용법에 대해 배운다.
위 코드는 먼저 num, arr 변수를 선언하고 sum 함수에 1, 2를 인자로 실행한다. sum 함수에서 1, 2를 더한 값을 출력하고 func2를 실행하는데, func2는 "func2()"를 출력하고 종료한다. 다시 메인으로 돌아와서 strcpy로 실행시 인자로 입력한 값을 arr로 복사하는데, 이 과정에서 얼만큼 복사할지가 정해져 있지 않으므로 버퍼 오버플로우가 일어난다.(arr은 10 바이트 크기를 가지지만 더 큰 값을 넣으면 넘치게 된다.)
마지막으로 num의 값이 1이라면 쉘을 실행하게 되는데, 만약 setuid가 root로 걸려있다면 관리자 권한을 얻을 수 있다.
사실상 num의 값은 정상적인 루트로는 1로 바뀔 수 없다. 어중간하게 쌓은 포너블 지식으로 봤을 때는 stycpy 함수로 arr에 버퍼 오버플로우를 일으켜 num의 값을 1로 바꿔줘야할 것 같다.
cat > test.c
ftz trainer에서 배운 cat > 명령어로 test.c를 새로 만들어 위 코드를 넣었다.
gcc -fno-stack-protector -o test test.c
그리고 리눅스 컴파일러인 gcc를 이용해 test.c를 컴파일 했는데 여기서 -fno-stack-protecter를 써준 이유는 gcc가 스택을 보호하기 위해 canary 값을 삽입하는데 bof가 발생해 만약 이 canary 값이 덮인다면 이를 감지하고 프로그램이 강제 종료되게 되는데(Stack Smashing Protection), 공격의 목적으로 프로그램을 컴파일 하는 것이므로 이를 방지하기 위해 -fno-stack-protecter 옵션으로 보호 기법을 해제한 것이다.
반대로 모든 프로시져에 보호기법을 적용하기 위해서는 -fstack-protector-all 옵션을 사용하면 된다.
-GDB-
gdb는 오픈 소스 디버거로 코드에서 어떤 라인을 실행했을 때 어떤 값이 메모리에 올라가고, 어떤 동작을 하는지 등의 과정을 보여주는 것이다. ELF 파일과 같은 linux 기반 실행파일을 동적으로 따라가며 분석할 때 유용하다고 한다.
gdb 디버거에 프로그램을 올리는 방법은 gdb <프로그램 경로>를 쓰면 된다.
ftz training에서 배운 상대 경로를 이용해 gdb에서 test 파일을 열러 봤다. 참고로 열었을 때 나오는 문구들은 gdb로 파일을 열때 -q 옵션을 줘서 안보이게 할 수 있다.
-set disassembly-flavor [명령어 형식]
gdb로 파일을 분석한다면 보통 맨 처음에 쳐야하는 명령어다. set disassembly-flavor는 어셈블리 코드 문법을 설정하는 명령어로 at&t와 intel 중 선택할 수 있다. 대부분 intel 형식으로 설정하지만 특이하게 at&t를 원한다면 기본 값이기 때문에 그냥 설정 없이 바로 분석 들어가면 된다.
(gdb) set disassembly-flavor intel
나는 intel 명령어로 설정했다.
-disas [함수명]
disas [함수명] 명령어는 원하는 함수의 어셈블리 코드를 보게 해주는 명령어다.
disas main 명령어를 입력하면 main의 어셈블리 코드를 보여주는 것을 볼 수 있다.
-b(breakpoint) *[메모리 주소]
b는 breakpoint의 약자로 ollydbg에서 f2를 눌러서 bp를 거는 것과 같은 역할을 하는 명령어다.
b는 breakpoint를 원하는 위치에 걸어주는 역할을 한다.
메모리 주소, 함수 이름, offset<+0> 등으로 bp 위치를 지정할 수 있고 주소 앞에는 *를 꼭 붙여줘야한다.
위는 disas main에서 본 정보를 바탕으로 main 함수의 시작점에 각기 다른 방법으로 bp를 걸어본 모습이다. 첫 번째는 함수명으로 bp를 걸었고, 두 번째는 상대 주소로 main 함수의 시작 부분에서 0만큼 떨어진 부분(결국 main 함수 시작 부분)에 bp를 걸었다. 세 번째는 0x11fa 주소에 bp를 걸려했지만 실수로 0x를 안써줘서 오류가 떳었다. b *0x11fa를 써주니 정상적으로 0x11fa 주소에(main의 시작 부분) bp가 걸렸다.
-info b
info b 명령을 사용하면 지금까지 건 bp의 정보들을 확인할 수 있다.
위에서 b 명령어로 같은 위치에 3개의 bp를 걸었으니 당연히 3개의 bp가 출력된다.
-d [bp 번호]
d [bp 번호]는 bp를 삭제해주는 명령어다. info b를 했을 때 보이는 Num부분의 번호를 [bp 번호]에 입력하면 해당 bp를 삭제해준다.
d 3, d 2로 2, 3번의 bp를 지우고 info b로 bp의 정보를 확인해보면 2, 3번 bp가 지워진 것을 볼 수 있다.
-run [매개변수]
gdb 내부에서 프로그램을 실행하는 명령어다. 프로그램이 실행할 때 인자를 받아야한다면 [매개 변수] 자리에다가 인자를 입력하면 되고, 만약 없다면 그냥 run만 적으면 된다.
인자로 AAAA를 주고 run으로 실행하니 test AAAA 이런식으로 자동으로 매개변수를 줘서 실행해주는 것을 볼 수 있다.
또한 위에서 main의 시작 지점에 bp를 걸었기 때문에 bp에 걸려서 멈춘 것을 알 수 있다.
disas main 명령어로 main의 어셈블리를 출력해보면 화살표로 현재 위치가 나타내지고 있는 것을 볼 수 있다.
-ni
ni는 next instruction의 약자로 다음 명령을 실행해주는 역할을 한다.
ni로 여러 번 다음 명령을 실행한 후 disas main으로 main의 어셈블리를 확인해보면 화살표의 위치가 2칸 아래로 바뀐 것을 볼 수 있다.
실수로 64bit로 빌드했다는 것을 깨달아서 32bit로 급하게 빌드했다. 64bit 리눅스에서 32bit 프로그램 빌드 방법은
sudo apt-get install gcc-multilib -> gcc로 빌드할 때 뒤에 -m32 옵션을 주면 된다.
한 번 더 ni 명령을 사용하고 disas main으로 main의 어셈블리를 보면 화살표 이전 코드가 mov ebp, esp인 것을 볼 수 있다. 이 역시 리버싱에서 흔히 볼 수 있는 그 유명한 스택프레임 생성 부분이다.(esp, ebp는 스택 관련 레지스터다.)
메모리에 어떤 값이 담겨 있는지 gdb로 확인해 보겠다.
-메모리 출력 방법
메모리 출력은 몇 바이트만큼 몇 진법으로 출력할 건지를 옵션을 주면 된다.
b : 1바이트
h : 2바이트
w : 4바이트
x : 16진수
u : 10진수
보통 출력은 x/<바이트, 진법 옵션>로 사용하면 된다.
위는 각각 16진수, 10진수, 1바이트, 2바이트, 4바이트 그리고 마지막으로 혼합해서 16진수로 10바이트 만큼 출력해본 모습이다. 이렇게 b, h, w등을 쓰지 않아도 원하는 바이트의 숫자를 지정해서 출력해줄 수 있다.
이렇게 2w를 적으면 2+w(4바이트)가 아닌 2*w(4바이트) 한 8 바이트만큼 출력해준다.
그리고 위처럼 x/wx, x/4b를 비교해보면 다르게 출력되는 것을 볼 수 있다. 둘다 같은 주소에서 4바이트만큼 읽어오는 것인데 x/wx는 0x04244c8d가 출력된지만 x/4b는 0x8d 0x4c 0x24 0x04 이렇게 값이 거꾸로 출력되는 것을 볼 수 있다.
이를 리틀 엔디언(little endian)이라 부르는데 intel cpu가 바이트를 배열할 때 거꾸로 쓰기 때문에 일어나는 현상이다.
예를들어 0x12345678을 저장한다 가정하면 0x78563412로 바이트 단위로 거꾸로 저장된다. gdb에서는 x/wx 같이 한 번에 여러 값을 알아볼 때는 보기 편하게 하기 위해서 빅 엔디안(big endian : 0x12345678을 0x12345678로 저장하는 것(그대로)) 형식으로 보여주기 때문에 x/wx와 x/4b가 다르게 출력된 것이다.
특정 레지스터의 값은 $<레지스터 명>으로 확인하거나 info reg $<레지스터 명>, i r $<레지스터 명>으로도 확인 가능하다.
-stack frame-
먼저 간단히 레지스터 몇 개를 정리해 보겠다.
esp : 스택의 가장 상단의 주소를 가진 레지스터
ebp : 스택의 가장 밑바닥의 주소를 가진 레지스터
eip : 현재 실행중인 명령의 주소를 가진 레지스터
현재 eip는 0x565562aa를 가리키고 있고
push ebp
mov ebp, esp
이 두 줄의 명령어가 실행됐었다. 이 두 줄의 명령어는 스택 프레임을 생성하는 명령어다.
이 두 줄의 의미는 함수가 실행될 때 이전의 ebp는 스택에 넣어두고(백업 같은?) 현재 esp의 주소를 ebp에 저장하는(새로운 ebp 생성) 뜻이다.
스택 프레임을 더 자세히 확인해보기 위해 sum 함수를 호출하는 main+50 부분에 bp를 걸고 이 부분까지 왔다.(continue 명령어를 사용하면 된다. run은 다시 시작해버리지만 conti는 현재 시점에서 실행하는 기능을 한다.)
i r로 esp, ebp의 값을 확인하고 x/8wx로 esp 주소의 32 바이트 만큼 값들을 확인해보면 먼저 매개 변수인 1, 2가 0xffffcff0, 0xffffcff4에 존재하는 것을 볼 수 있고, ebp, esp가 각 끝에 존재하는 것을 볼 수 있다.
sum에 bp를 걸고 들어와서 스택 상황을 확인해보면 esp가 +4 된 것을 확인할 수 있고 스택에 main에 있던 주소인 0x5665562d0이 저장된 것을 볼 수 있다. 이게 바로 return address다.(복귀 주소)
스택 프레임 코드까지 실행한 후 스택 상황을 보면 push ebp로 예전 스택 프레임(main)의 ebp인 0xffffd028이 esp 주소에 들어 있는 것을 볼 수 있다. mov ebp, esp 코드를 실행하면 esp의 위치가 ebp의 위치가 되므로 현재 esp의 위치를 sum 스택 프레임의 가장 밑바닥으로 만들어준 것이다.
스택 상황의 윗 부분만 보면
위 사진과 같음을 알 수 있다.(주소가 다르기에 값은 다를 수 밖에 없다.) 새로운 스택 프레임 ebp를 기준으로 ebp - 8, ebp - 12가 매개 변수 1, 2가 들어있는 부분이고, ebp - 4는 아까 정리했듯이 return address(sum 함수가 끝나고 돌아갈 주소), ebp는 main에서의 ebp 스택 프레임 값을 백업하고 있다.(sum 함수가 끝난 후 pop으로 해당 값을 다시 회수한다.)
지금까지 배웠던 것이 간단한 코드지만 생각보다 복잡한 원리인 스택 프레임이었다.
-Leave, Ret-
위는 sum 함수의 leave, ret를 실행했을 때의 각 스택 상태다.
leave를 실행하면 sum의 스택 프레임이 모두 정리된 것을 볼 수 있다. ebp의 주소가 push 해뒀던 main의 ebp로 바뀌고 esp도 main에서의 esp로 바뀌었다.
ret를 만나면 스택에 있던 복귀주소인 0x565562b5로 리턴하고 끝으로 sum의 스택 프레임이 완전히 정리된다.
원본 링크 : 우리 집에 GDB 있는데.... 메모리 보고 갈래? (2)
'Old (2021.01 ~ 2021.12) > Pwnable' 카테고리의 다른 글
Return Address Overwrite (0) | 2021.07.11 |
---|---|
Buffer Overflow (0) | 2021.07.04 |
[Linux-x86] shellcode 제작 (0) | 2021.06.26 |
우리 집에 GDB 있는데 메모리 보고 갈래? (3) (0) | 2021.03.13 |
우리 집에 GDB 있는데 메모리 보고 갈래? (1) (0) | 2021.03.10 |