x86, x64 Architecture
x86은 x86계열 32비트 CPU의 ISA이다. x64는 x86계열 cpu들의 64bit버전이다.
x64는 기본적으로 x86 Architecture를 지원하는데 그 이유는 아래 레지스터에 대한 설명을 보면 알 수 있다.
Registers
레지스터에는 상태 레지스터와 범용 레지스터 2가지가 있는데 범용 레지스터만 살펴보겠다.
위 사진은 x64에서 사용하는 레지스터와 x86에서 사용하는 레지스터들이다.
앞에 r이 붙은 레지스터들이 x64에서 사용하는 범용 레지스터이고 e가 붙은 레지스터들이 x86에서 사용하는 범용 레지스터들이다. xmm0, xmm1등의 레지스터는 부동소수점을 다룰때 쓰이는데 여기서는 다루지 않겠다.
사진을 자세히 보면 rax의 하위 4byte가 eax이다. eax의 하위 2byte는 ax레지스터이고 다시 ax레지스터의 상위 1byte는 ah이고 하위 1byte는 al이다. 따라서 x64에서 eax레지스터를 사용할 수 있다. eax는 물론이고 ax, ah, al까지 전부 사용할 수 있다. 하위 비트 Architecture를 사용하는 바이너리와 호환이 되는 구조이다. 64bit운영체제에서 32bit프로그램이 돌아가는 이유이다.
범용 레지스터들은 사전적으로 정해진 용도가 있다.
rax(eax) : 산술, 논리 연산과 함수 return값을 저장하는데 사용된다.
rbx(ebx) : 메모리 주소를 저장하기 위해 사용된다.
rcx(ecx) : 반복문에 카운터 변수로 사용된다.
rdx(edx) : rax와 함께 복잡한 연산을 위해 추가적인 데이터를 저장할 때 사용한다.
rsi(esi) : 데이터 복사할 때 출발지 주소를 저장한다.
rdi(edi) : 데이터 복사할 때 목적지 주소를 저장한다.
rbp(ebp) : 현재 함수에서의 stack base pointer를 저장하고 있다.
rsp(esp) : 현재 스택 주소를 담고 있다.
이렇다는데 프로그램 많이 분석해보면 알겠지만 rax가 함수 return값이라는것과 rbp, rsp의 용도 말고는 전부 안지켜지고 자기 맘대로 변수처럼 사용이 된다. 필기시험 보는거 아니면 저 내용은 기억할 필요가 없다. 오히려 리버싱 처음 할때 레지스터가 사용되는 용도를 머릿속에 떠올리면서 분석하면 혼동만 올뿐 전혀 도움이 안된다. 그렇다고 아예 쓸모없는 내용은 아니다. 어느정도 틀은 저렇게 잡혀있다. 단지 모든 상황에서 꼭 저런 규칙이 딱딱 지켜지는게 아니니까 굳이 기억할 필요가 없다는 말이다.
x64, x86 calling convention
함수 호출 규약은 함수를 호출하는 방식에 대한 약속이다. 여러가지 함수 호출 규약이 있다.
x86에서의 호출 규약부터 알아보겠다.
__cdecl
C언어의 표준 호출 규약이며 마지막 인자부터 스택에 push한다.
스택은 LIFO구조여서 마지막 인자부터 push하면 첫번째 인자가 rsp와 가장 가깝게 된다.
caller(호출자)가 스택을 정리한다. 따라서 가변 인자 함수를 쓸때는 무조건 __cdecl규약으로 호출해야 한다.
__stdcall
Win32 API, C++의 표준 호출 규약이다. 인자 전달 방식은 __cdecl과 동일하다.
차이점은 callee(피호출자)가 스택을 정리한다는 것이다. 따라서 함수에 인자가 몇개 들어올지 알 수 없는 가변 인자 함수의 경우에는 __stdcall호출 규약으로 호출할 수 없다.
__fastcall
인자가 적을경우 함수 호출을 좀더 빠르게 하기 위한 호출 규약이다. 첫번째 인자를 ecx, 2번째 인자를 edx에 담아서 호출한다. 따라서 인자가 적을 경우에는 레지스터를 사용하므로 호출이 더 빨라진다. 하지만 2번째 이후로는 앞의 호출 규약들과 같이 스택을 사용하므로 인자가 많을 경우에는 속도에 크게 이점이 없을것이다.
__stdcall과 마찬가지로 callee가 스택을 정리한다.
__thiscall
C++에서 클래스의 멤버 함수를 호출하는 규약이다. this포인터는 ecx에 저장되고 나머지는 스택을 이용한다.
callee에 의해서 스택이 정리된다.
x64에서의 호출 규약은 fastcall로 통일되었다.
Linux ELF바이너리의 경우에는 rdi, rsi, rdx, rcx, r8, r9순서로 인자가 전달되고 이것보다 많으면 스택을 사용한다.
Windows PE바이너리의 경우에는 rcx, rdx, r8, r9순서로 인자가 전달되고 이것보다 많으면 스택을 사용한다.
x64에서의 __thiscall도 ELF의 경우에는 rdi에 this포인터가 들어가고 rsi부터 r9까지 인자가 들어간다. PE의 경우에도 동일하게 rcx에 this포인터가 들어가고 rdx부터 r9까지 인자가 들어간다. 첫번째 인자로 this포인터를 전달하는거 말고는 __fastcall호출 규약과 완전히 동일하다. this포인터는 멤버 함수가 호출된 객체의 주소를 가리키는 포인터이다.
EFLAGS
EFLAGS Register는 범용 레지스터가 아닌 상태 레지스터이다. 나는 바이너리 분석을 많이 해봤지만 얘네는 ZF빼고는 잘 모른다. 분석할때 그렇게 필요한 애들도 아니다. 그렇기 때문에 간단하게만 짚고 넘어가겠다.
CF(Carry Flag)
자리 올림이 발생하면 1이 된다.
ZF(Zero Flag)
cmp와 같은 명령어들의 연산 결과가 0이면 1이 된다.
je, jz, jnz등 ZF에 따라서 jmp를 할지말지 결정하는 instruction들이 있다.
얘네들은 분기문에서 쓰인다.
SF(Sign Flag)
가장 최근에 수행된 산술 연산 결과의 부호 비트를 저장한다.
OF(Overflow Flag)
연산 결과가 허용 범위를 벗어나서 Overflow가 발생하면 1로 세팅된다.
메모리 구조
프로그램의 메모리에는 크게 4가지 영역이 있다.
Code : 실행할 프로그램의 기계어 코드가 들어가있는 영역이다. 읽기 권한과 실행 권한을 가지고 있다.
Data : 전역변수와 정적 변수가 들어있는 영역이다. 세부적으로 들어가면 .bss, .rodata등 섹션이 세부적으로 나뉘지만 전역변수와 정적 변수가 저장된다는 기본적인 틀은 같다. .rodata섹션의 경우에는 읽기 권한만을, .bss나 .data의 경우에는 읽기와 쓰기 권한 모두 가지고있다.
Stack : 지역변수와 매개변수가 위치하는 영역이다. 함수 프롤로그에서 함수에서 사용할 스택 공간을 할당해두고 에필로그에서 다시 원래대로 돌려둔다. 따라서 함수가 호출된 후에는 함수에서 사용된 지역 변수가 저장된 공간을 다른 함수에서 사용하게 될 수 있다. 이것이 지역 변수는 함수 밖으로 가면 소멸된다는 이유이다.
Heap : 프로그래머가 동적으로 할당하고 해제할 수 있는 메모리이다. heap은 할당받을때 heap chunk라는 구조체가 할당되고 해제하면 bin에 들어간다. bin에 들어간 청크는 연결리스트 형태로 서로 연결하고 있다. 그리고 다시 malloc요청이 들어오면 일반적으로 bin에 있는 적당한 size의 chunk를 재할당해준다.
Assembly
어셈블리는 기계어와 1대1로 매칭되는 저수준 언어이다. 기계어를 사람이 이해하기 편하게 문자로 바꾼것이다.
x64 Architecture기준으로 자주 쓰이는 instruction들을 적어보겠다.
push : 피연산자를 스택에 넣는다. (내부적으로 rsp - 8연산이 수행된다.)
pop :피연산자를 스택에서 꺼낸다. (내부적으로 rsp + 8연산이 수행된다.)
mov : 두번째 피연산자의 값을 첫번째 피연산자로 복사한다. (ex mov rax, rbx => rax = rbx)
lea : mov와 유사하지만 두번째 피연산자의 값을 복사한다. (ex lea rax, qword ptr[rsp+8] => rax = &qword ptr[rsp+8])
or : 2개의 피연산자를 비트 or연산한 결과를 첫번째 피연산자에 저장한다.
and : 2개의 피연산자를 비트 and연산한 결과를 첫번째 피연산자에 저장한다.
xor : 2개의 피연산자를 비트 xor연산한 결과를 첫번째 피연산자에 저장한다.
같은 값을 xor하면 0이 된다는 특성때문에 xor rax, rax와 같이 레지스터를 0으로 초기화할때 많이 쓰인다.
add : 2개의 피연산자를 더한 값을 첫번째 피연산자에 저장한다.
sub : 첫번째 피연산자에서 2번째 피연산자를 뺀 값을 첫번째 피연산자에 저장한다.
call : Stack에 return address를 push하고 함수를 호출한다.
ret : 스택에 저장된 return address로 복귀한다.
jmp : 피연산자로 온 주소로 점프한다.
je : ZF가 세팅되었을 경우에 피연산자로 온 주소로 점프한다.
jne : je와 반대로 ZF가 세팅되지 않았으면 점프한다.
ja : SF를 확인해서 A > B면 점프한다.
jb : SF를 확인해서 A < B면 점프한다.
jae : SF와 ZF를 확인해서 A >= B면 점프한다.
jbe : SF와 ZF를 확인해서 A <= B면 점프한다.
cmp : 피연산자 2개에 뺄셈 연산을 수행해서 0이면 Zero Flag를 세팅한다.
Stack Frame
https://sechack.tistory.com/62
[시스템 해킹] 4강 - Stack Buffer Overflow 기초
이번 시간에는 시스템 해킹의 기초이자 매우 중요한 취약점인 Stack Buffer Overflow에 대해서 알아보겠습니다. Buffer Overflow는 할당된 메모리 공간을 넘어서 다른 메모리에까지 데이터를 입력할 수 있
sechack.tistory.com
함수 에필로그에서 새로운 Stack base pointer를 할당받고 프롤로그에서 이전 함수의 Stack base pointer로 복귀시키는 과정은 위 게시글에서 잘 설명해두었다. 따라서 자세한 설명은 생략하고 대략적인 구조만 설명하겠다.
1. call instruction으로 함수가 호출되는데 call instruction은 기본적으로 push nextrip; jmp rip와 같은 동작을 한다. (rip레지스터는 피연산자로 올 수 없지만 이해를 돕기 위해서 저렇게 썼습니다.)
2. 함수가 호출된 직후 rbp는 이전 함수의 Stack base pointer를 담고 있다. push rbp로 스택에 이전 함수의 base pointer를 백업해두고 mov rbp, rsp로 새로운 Stack base pointer를 할당한다. 그러면 rbp는 백업해둔 base pointer가 위치한 주소를 가리키게 된다. 이 위치를 Stack frame pointer라고 부른다. (사실 Stack base pointer랑 Stack frame pointer 2개의 표현이 헷갈린다. 정확히 아시는 분은 댓글로 알려주시면 감사하겠습니다..)
3. 필요한 공간만큼 rsp를 빼준다. 그러면 rbp와 rsp사이에 공간이 생긴다.
4. 에필로그에서는 rsp를 빼준 만큼 다시 더해준다. 그리고 leave; ret의 과정을 수행하는데 leave는 pop rbp; mov rsp, rbp이다. pop rbp로 백업해두었던 이전 함수의 Stack base pointer를 다시 rbp로 가져가고 mov rsp, rbp로 이전 함수의 rsp를 복귀시킨다.
5. ret instruction이 실행되는데 pop rip; jmp rip와 같은 동작을 합니다. 스택에 push된 return address를 참조해서 다시 이전 함수로 돌아가는 것이다.
Pwndbg 명령어
ubuntu21.10부턴가 gef가 에러가 나길래 높은 버전에서는 pwndbg를 쓰고 있고 최근에 진행한 Dice CTF에서도 불편했지만 pwndbg를 이용해서 heap debugging을 한 경험이 있어서 pwndbg만의 유용한 명령어들을 정리해보려고 한다. (사실 정리하는거 과제임 ㅎㅎ) 일반 gdb명령어들은 생략하고 pwndbg만의 명령어들만 정리하겠다.
heap
그냥 heap만 쳐도 현재 청크의 상태를 싹다 보여준다. Allocated된 chunk인지 Free된 chunk인지 Free chunk인 경우에는 tcache bin인지 fastbin인지 smallbin인지 다 보여준다. Free chunk의 경우는 fd까지 보여준다. 이거 하나만으로도 꽤 강력하다.
또 다른 기능으로는 heap명령어 뒤에 메모리 주소를 주면 heap chunk구조체의 형식에 맞게 보여준다. fake chunk찾을때 유용할것 같다.
piebase
pie base주소를 알려준다. 굳이 메모리맵 안봐도 이렇게 간단하게 볼 수 있다.
dumpargs
함수의 인자로 들어가는 레지스터들만 보여준다. 함수에 전달되는 인자를 편하게 볼때 유용할것 같다.
canary
카나리 값을 보여준다. 이것도 꽤 강력한 명령어이다.
retaddr
함수의 return address를 알 수 있다. BOF를 좀 더 편하게 할 수 있는 명령어인듯 싶다.
'Layer7' 카테고리의 다른 글
Layer7 - 리버싱 5차시 과제 (0) | 2022.10.04 |
---|---|
Layer7 - 리버싱 4차시 과제 (0) | 2022.10.04 |
Layer7 - 리버싱 2차시 과제 (0) | 2022.10.04 |
Layer7 - 하드웨어 2차시 과제 (0) | 2022.10.04 |
Layer7 - 하드웨어 1차시 과제 (0) | 2022.10.04 |