x86 assembly
x86 아키텍처(intel, amd)는 32bit를 나타낸다. 간혹 x86_64, x64로 표현된 것은 64bit 환경을 나타낸다.
해당 아키텍처가 어느 문법으로 세팅됐는지 확인하려면, gdb에서 show disassembly-flavor 명령어를 치면 된다.
다른 문법으로 바꾸고 싶다면 set disassembly-flavor intel 이런 식으로 하면 된다.
표현방법 1. AT&T
operation에서 source first.
ex) mov reg1, reg2 #reg1의 값이 reg2로 옮겨진다
표현방법 2. Intel
destination first. reg 표기 시 % 표시가 없음!
ex) mov reg1, reg2 #reg2의 값이 reg1로 옮겨진다
Numerical representation
Binary (0, 1) : prefix[0b10011100, unix(intel, at&t)], suffix[10011100b, intel]
Hexadecimal (0, ..., F) : "0x" vs "h" , prefix[0xABCD1234], suffix[ABCD1234h]
Traditional registers in x86
1. general purpose registers
AX, BX, CX, DX
2. Pseudo general purpose registers
Stack : SP(stack pointer), BP(base pointer, arm에는 존재하지 않음)
Strings : SI(sourse index), DI(destination index)
3. Special purpose registers
IP(instruction pointer, 현재 실행되는 명령어의 주소를 담은 reg), EFLAGS(계산 상태를 저장하는 reg)
GPR usage
보통 16bit이고, low 8bit, high 8bit로 나뉜다. 예를 들어, AX는 16bit, AH는 8bit, AL은 8bit 이렇게 나뉜다는 것이다.
AX, BX, CX, DX는 각각 Accumulator, Base, Counter, Data의 역할을 가진다. 물론 꼭 지켜지는 것은 아니고, 암묵적인 룰이 그렇다는 것이다.
EAX, ESP는 32bit를, RAX, RSP는 64bit 환경을 나타낸다. 64bit에는 추가적으로 R8~15까지의 register가 존재한다.

RAX가 64, EAX가 32, AX가 16bit 이런 식으로 된다.
Endianness
ex. 0A0B0C0Dh
Big-endian : highest order byte first : 0A 0B 0C 0D
Little-endian : lowest order byte first : 0D 0C 0B 0A
Operands in x86
mov EAX, EBX (EBX의 내용을 EAX에 copy)
mov EAX, 10h (10h, 즉 상수를 EAX에 copy)
mov EAX, [10h] (10h라는 메모리주소에 있는 값을 EAX에 copy) - direct
mov EAX, [EBX] (EBX 주소의 값을 EAX에 copy) - indirect
mov AL, [EBX + ECX * 4 + 10h] (EBX라는 주소를 ECX*4 + 10h로 indexing, 즉 BX[4*CX+0x10] 위치의 값을 AL에 copy - indexed
mov AL, byte ptr [BX] (ptr가 가리키는 값이 byte 크기라는 것)
mov AL, BL
EBX의 값을 EAX에 copy
mov AL, 10h
10h값을 EAX에 copy
mov AL, [10h]
10h 주소에 있는 값을 EAX에 copy
mov AL, [EBX]
EBX의 값인 20h 주소의 값을 EAX에 copy
mov AL, [EBX+ECX]

EBX 주소인 20h에서, ECX인 02h만큼 이동한 22h 주소의 값을 EAX에 copy
MOV 대체 명령어
XCHG : src와 dst 값 교환
PUSG : src를 stack에 저장
POP : stack의 가장 위 값을 꺼내서 dst에 저장
LEA : 주소 연산. MOV EAX, [EBX + 10h]를 하면, EBX 주소값 + 10h 를 한 주소에 있는 값을 EAX에 copy하는 것인데, LEA EAX, [EBX + 10h]를 하면 EBX 주소값 + 10h값을 EAX에 copy하게 된다.
그럼 그냥 MOV EAX, EBX+10h를 하면 안되나..? 싶겠지만, 이는 문법에 어긋나기에 LEA로 작성하는 것이다. lea를 안 쓰려면 add ebx, 10h; mov eax, ebx 이렇게 두 줄로 서야 한다.
Stack
ESP(stack pointer)는 freely manipulated하다. ESP에 바로 mov/add를 적용할 수 있다는 뜻!
stack은 MOV에 의해 바로 접근될 수 있다. 메모리이기에!
stack은 밑으로 자란다. downwards.

요렇게 위(높은주소)에서 아래(낮은주소)로 표현된다.
여기에 만약 push 9ABCDEF0h 를 한다면,

이렇게 된다는 뜻! 이건 지금 Little Endian이고, ESP가 가장 낮은 주소를 가리키게 된다.
그럼 여기서 pop을 하면,

EAX에 9ABCDEF0값이 들어가고, ESP는 그 바로 윗주소를 가리키게 된다.
Arithmetic & logic operation
ADD, SUB, AND, OR, XOR, Shift, ...
MUL & DIV는 특정 register만을 사용한다.
Conditional statements
Evaluators : cmp와 같은 condition 계산 연산을 해서 conditional flag를 세팅하는 애들
- TEST
- 만약 TEST EAX, EAX를 하면 뭐 register의 값이 AND 연산되어 바뀌는 게 아니라 AND 연산에 따라 ZF의 값이 바뀌는 것이다. 근데 이렇게 같은 register를 인자로 넣는 것은 무슨 의미냐면, EAX의 값이 0인지 아닌지를 탐지하는 것이다.
- 만약 TEST AL, 00010000b 를 한다면, AL과 00010000을 AND 연산하여 ZF의 값을 바꾸게 된다. 저 4번째 자리 값이 1인지 아닌지를 ZF에 저장하는 것이다.
- CMP
- CMP EAX, EBX를 하면 sign, overflow, zf가 세팅된다.
conditional jumps : 조건에 따라 jump함
즉, Expression(명령어 표현)에 따라 Evaluator가 그 결과값들을 세팅하고, EFLAGS를 통해 flag값들을 체크해서, jump를 할지 말지 판단하게 되는 것이다.

unconditional jumps
조건에 관계없이 무조건 jmp해라
absolute jump : 절대경로로 jmp(direct:주소가 적혀짐, indirect:프로그램이 실행되면서 주소가 정해짐)
relative jump : 상대경로로 jmp(near, far, constant offset 등..)
control flow
명령어가 실행되는 flow, 이는 조건문에 의해 변경된다.
Stack이란?
local variable이든, 프로그램이 다른 함수를 호출될 때 return address가 저장되는 메모리
x86 32bit에서, 스택은 상위 1GB가 OS kernel 공간으로 정의되고, 나머지 3GB가 user memory로 할당이 된다.
가장 상위에는 stack이 위치하고, 밑에는 Heap/BSS/Data/Text 영역이 정의된다.

호출 규약(어떤 함수에서 다른 함수를 호출할 때, 어떤식으로 파라미터를 저장하고 가져올지에 대한 규약) 중 하나로, cdecl이 있다.
cdecl에 따라 stack이 어떻게 구성되는지 살펴보겠다.

A에서 B를 호출한다고 하면, A가 caller가 된다. B는 current frame에 추가가 된다.
일단 b가 요구하는 argument를 caller에서 정의하고, A의 B를 호출하는 코드 뒷부분을 return address로 저장해놓는다.
그리고 새롭게 b에 대한 stack memory를 정의하기 전에, 함수 A에 대한 EBP값을 우선 저장을 한다. A함수의 stack 바닥 부분을 이후에 다시 가리켜야 하기 때문이다. 이후 B에서 사용하는 지역변수들을 할당할 메모리가 존재하고, array 형태의 buffer가 위치한다.
예시를 살펴보겠다. 스택 구조를 다 넣을 수가 없어서 그냥 말로 대체하겟다ㅜㅜ

- 0x080483f6 주소에서 main함수가 실행된다.
- 가장 상단에 ebp가 있을 것이고, 가장 하단에 esp가 있을 것이다.
- 쭉 실행되다가, 0x08048406에서 새로운 함수를 호출한다.
- 그러면 이때, 원래 다음에 main에서 실행될 add 명령을 하는 주소인 0x0804840b를 stack에 push하게 된다. 그럼 자연스럽게 esp가 한 칸 내려갈 것이다. 이 과정은 함수 실행 후 add명령으로 넘어갈 수 있게 해준다.
- 그다음, 호출된 함수를 실행하기 전에, 현재의 ebp값(0xbfffee88)을 스택에 push한다. 이를 통해 다시 main함수로 돌아왔을 때 ebp 주소를 찾을 수 있다.
- 그리고 이제 새로운 함수를 쓸 것이기에, esp주소를 ebp에 copy한다. 새로운 스택 메모리가 시작되는 것이다. 함수가 쓸 공간 10만큼을 할당하기 위해 esp에서 10을 빼준다.
- 이후 함수가 쭉 진행되고, 함수가 마무리될 때 leave 명령어를 만나게 된다. 이 명령어는 mov esp, ebp;pop ebp와 같다. 다시 main 스택으로 돌아갈 준비를 하는 것이다. 현재의 ebp값이 main의 가장 하단 주소일 것이므로 ebp를 esp에 copy해주고, 아까 저장해뒀던 main의 ebp 주소인 0xbfffee88를 pop해서 ebp에 넣어준다.
- 이후 ret 명령어를 통해, main에서 함수 call 다음 명령어인 add의 주소인 0x0804840b를 pop하여 eip에 copy한다.
- 그럼 최종적으로 다시 ebp, esp, eip가 main에 맞춰지게 된다.
Stack Buffer Overflow
스택 내부의 buffer가 넘쳐흘러서 발생하는 버그
메모리에 저장되는 이것저것이 있는데, 그 중 buffer를 쓸 때, 입력을 받았는데 buffer의 크기보다 큰 값이 들어와서 다른 obj를 침범하는 경우 overflow & overwritten이 발생하게 된다.

그래서 만약 overwrite되는 obj 중 return address가 있다면, 그 return address를 변경할 수 있다. 그러면 공격자가 원하는 로직을 실행할 수 있게 된다. 이렇게 return address가 바뀌는 걸 control flow hijack이라 한다.
Control Flow Hijack
기존에 수행해야 하는 Basic Block들이 있는데, 만약 buffer overflow로 인해 control flow hijacking이 발생한다면 Basic Block이 아닌 Malicious code를 실행하게 된다.
#include <stdio.h>
void printthis()
{
printf("Success!\n");
}
void main()
{
char buffer[256];
scanf("%s", buffer); //입력 길이<=256인지 체크하지 않음
printf("%s\n", buffer);
}
예를 들어 이런 코드가 있다고 했을 때, scanf에서 입력값의 크기를 확인하지 않는 취약점이 있다.
이를 공격하기 위해,
1. return address가 stack의 어디에 위치하는지 찾아야 한다.
2. return address의 값을 buffer overflow로 overwrite한다.
3. 새롭게 overwrite된 return address 부분의 값이 printthis func의 주소라면 main()함수 실행 시 printthis()를 호출하지 않았음에도 Success! 가 출력된다.
이를 해결하는 과정을 아래 설명하겠다.
1. return address를 알기 위해, x/10x $esp 를 실행하면

이렇게 나온다. 이는 esp의 현재 위치로부터 10개 메모리의 값인데, 그 중 가장 앞에 나온 0xb7e21637이 return address의 값이다. 그 return address가 저장된 위치는 0xbfffee9c이다.
2. overwrite

stack의 크기는 sub esp, 0x100으로 된 걸 보아, 0x100, 즉 256 크기이다. 그럼 buffer의 시작점은 esp에서 0x100만큼 더한 곳에 존재함을 알 수 있다.
그리고 scanf가 호출되는 부분은 0x08048483임을 알 수 있다.


이렇게 스택 구조가 생겼다고 치면, buffer 부분 256 바이트, EBP 부분 4바이트, 그리고 아까 찾은 return address 주소 부분 4바이트를 덮어쓰면 printthis함수를 실행시킬 수 있는 것이다.
그럼 "A"*256+"B"*4+"printthis address"를 페이로드로 넣으면 main함수가 끝나는 return 부분에서 printthis를 실행하게 된다.
3. 그럼 이제 printthis 함수의 address만 찾으면 된다.
gdb에서 disas printthis 명령어를 실행시켜 가장 처음 나온 주소가 printthis의 주소이다.

여기서는 0x0804845b가 printthis의 주소가 된다.
그럼 최종적으로 페이로드는 python -c 'print "A"*260+"\x5b\x84\x04\x08"' | ./bof 가 된다.
"A"를 260번 하든 "A"*256+"B"*4를 하든 개수만 똑같으면 된다.
printthis address는 little endian으로 적는다.
'정보보안 > 소프트웨어및시스템보안' 카테고리의 다른 글
| Code Reuse Attack (1) | 2025.10.21 |
|---|---|
| Control Flow Hijacking - Shellcode (0) | 2025.10.20 |
| Set UID (0) | 2025.10.10 |