[Dreamhack-system] Stack Buffer Overflow(1)

박정원·2023년 4월 12일

보안공부

목록 보기
18/43

Background : Calling Convention

x86호출 규약 : cdecl

  • x86아키텍처는 레지스터의 수가 적으므로, 스택을 통해 인자를 전달함
  • 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있음
  • 스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push함
void __attribute__((cdecl)) callee(int a1, int a2){
}

void caller(){
callee(1,2);
}
; Name: cdecl.s
	.file	"cdecl.c"
	.intel_syntax noprefix
	.text
	.globl	callee
	.type	callee, @function
callee:
	nop
	ret ; 스택을 정리하지 않고 리턴합니다.
	.size	callee, .-callee
	.globl	caller
	.type	caller, @function
caller:
	push	2 ; 2를 스택에 저장하여 callee의 인자로 전달합니다.
	push	1 ; 1를 스택에 저장하여 callee의 인자로 전달합니다.
	call	callee
	add	esp, 8 ; 스택을 정리합니다. (push를 2번하였기 때문에 8byte만큼 esp가 증가되어 있습니다.)
	nop
	ret
	.size	caller, .-caller
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

x86-64호출 규약 : SYSV

리눅스는 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어졌음
SYSV에서 정의한 함수 호출 규약은 다음의 특징을 가짐

  1. 6개의 인자를 RDI, RSI, RDX, RCS, R8, R9에 순서대로 저장하여 전달함, 더 많은 인자를 사용해야 할 때는 스택을 추가로 이용함
  2. Caller에서 인자 전달에 사용한 스택을 정리함
  3. 함수의 반환 값은 RAX로 전달함
#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(); }

위의 코드를 gdb로 자세히 분석해봄

1. 인자 전달

gdb로 sysv를 로드한 후 중단점을 설정하여 caller 함수까지 실행함

$ gdb -q sysv
pwndbg: loaded 187 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from sysv...(no debugging symbols found)...done.
pwndbg> b *caller
Breakpoint 1 at 0x652
pwndbg> r
Starting program: /home/dreamhack/sysv
Breakpoint 1, 0x0000555555554652 in caller ()
...
───────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 0x555555554652 <caller>       push   rbp
   0x555555554653 <caller+1>     mov    rbp, rsp
   0x555555554656 <caller+4>     push   7
   0x555555554658 <caller+6>     mov    r9d, 6
   0x55555555465e <caller+12>    mov    r8d, 5
   0x555555554664 <caller+18>    mov    ecx, 4
   0x555555554669 <caller+23>    mov    edx, 3
   0x55555555466e <caller+28>    mov    esi, 2
   0x555555554673 <caller+33>    movabs rdi, 0x1b69b4bacd05f15
   0x55555555467d <caller+43>    call   callee <callee>
 
   0x555555554682 <caller+48>    add    rsp, 8
...

context의 DISASM을 보면, caller+6부터 caller+33까지 6개의 인자를 각각의 레지스터에 설정하고 있고, caller+4에서는 7번째 인자인 7을 스택으로 전달함

callee함수를 호출하기 전까지 실행하고, 레지스터와 스택을 확인해본다.

pwndbg> b *caller+43
Breakpoint 2 at 0x55555555467d
pwndbg> c
Continuing.
Breakpoint 2, 0x000055555555467d in caller ()
...
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 RAX  0x0
 RBX  0x0
*RCX  0x4
*RDX  0x3
*RDI  0x1b69b4bacd05f15 ;= 123456789123456789
*RSI  0x2
*R8   0x5
*R9   0x6
 R10  0x0
 R11  0x0
...
pwndbg> x/4gx $rsp
0x7fffffffdf78:	0x0000000000000007	0x00007fffffffdf90
0x7fffffffdf88:	0x0000555555554697	0x00005555555546a0

소스 코드에서 callee(123456789123456789, 2, 3, 4, 5, 6, 7)로 함수를 호출했는데, 인자들이 순서대로 rdi, rsi, rdx, rcx, r8, r9 그리고 [rsp]에 설정되어 있는 것을 확인할 수 있음

2. 반환 주소 저장

si 명령어로 한 단계 더 실행시키면, call이 실행되고 스택을 확인해보면 0x555555554682가 반환 주소로 지정되어 있음
gdb로 확인해보면 0x555555554682는 callee 호출 다음 명령어의 주소임
callee에서 반환되었을 때, 이 주소를 꺼내어 원래의 실행 흐름으로 돌아갈 수 있음

pwndbg> si
0x00005555555545fa in callee ()
...
pwndbg> x/4gx $rsp
0x7fffffffdf70:	0x0000555555554682	0x0000000000000007
0x7fffffffdf80:	0x00007fffffffdf90	0x0000555555554697
pwndbg> x/10i 0x0000555555554682 - 5
   0x55555555467d <caller+43>:	call   0x5555555545fa <callee>
   0x555555554682 <caller+48>:	add    rsp,0x8

3. 스택 프레임 저장

x/5i $rip 명령어로 callee 함수의 도입부를 살펴보면, 가장 먼저 push rbp를 통해 호출자의 rbp를 저장하고 있음
callee에서 반환될 때, sfp를 꺼내어 caller의 스택 프레임으로 돌아갈 수 있음

si로 push rbp를 실행하고, 스택을 확인해보면 rbp값인 0x00007fffffffdf80가 저장된 것을 확인할 수 있음

pwndbg> x/5i $rip
=> 0x5555555545fa <callee>:	push   rbp
   0x5555555545fb <callee+1>:	mov    rbp,rsp
   0x5555555545fe <callee+4>:	mov    QWORD PTR [rbp-0x18],rdi
   0x555555554602 <callee+8>:	mov    DWORD PTR [rbp-0x1c],esi
   0x555555554605 <callee+11>:	mov    DWORD PTR [rbp-0x20],edx
pwndbg> si
0x00005555555545fb in callee ()
...
pwndbg> x/4gx $rsp
0x7fffffffdf68:	0x00007fffffffdf80	0x0000555555554682
0x7fffffffdf78:	0x0000000000000007	0x00007fffffffdf90
pwndbg> print $rbp
$1 = (void *) 0x7fffffffdf80

4. 스택 프레임 할당

이제 mov rbp, rsp로 rbp와 rsp가 같은 주소를 가리키게 됨
바로 다음에 rsp의 값을 빼게 되면, rbp와 rsp의 사이 공간을 새로운 스택 프레임으로 할당하는 것이지만, callee 함수는 지역 변수를 사용하지 않으므로, 새로운 스택 프레임을 만들지 않음

si로 실행하고, 레지스터를 보면 이 둘이 같은 주소를 가리키는 것을 확인할 수 있음

pwndbg> x/5i $rip
=> 0x5555555545fb <callee+1>:	mov    rbp,rsp
   0x5555555545fe <callee+4>:	mov    QWORD PTR [rbp-0x18],rdi
   0x555555554602 <callee+8>:	mov    DWORD PTR [rbp-0x1c],esi
   0x555555554605 <callee+11>:	mov    DWORD PTR [rbp-0x20],edx
   0x555555554608 <callee+14>:	mov    DWORD PTR [rbp-0x24],ecx
pwndbg> print $rbp
$2 = (void *) 0x7fffffffdf68
pwndbg> print $rsp
$3 = (void *) 0x7fffffffdf68

callee함수에서 선언한 지역 변수 ret은?

ret은 반환 값을 저장하는 용도 외로는 사용되지 않고 있으므로, gcc는 이런 변수에 대해 스택을 할당하지 않고, rax를 직접 사용함

5. 반환값 전달

덧셈 연산을 모두 마치고, 함수의 종결부에 도달하면 반환값을 rax에 옮김

pwndbg> b *callee+87
Breakpoint 3 at 0x555555554651
pwndbg> c
Continuing.
Breakpoint 3, 0x0000555555554651 in callee ()
...
pwndbg> x/5i $rip
=> 0x555555554645 <callee+75>:	add    rax,rdx
   0x555555554648 <callee+78>:	mov    QWORD PTR [rbp-0x8],rax
   0x55555555464c <callee+82>:	mov    rax,QWORD PTR [rbp-0x8]
   0x555555554650 <callee+86>:	pop    rbp
   0x555555554651 <callee+87>:	ret
pwndbg> c
pwndbg> print $rax
$4 = 123456789123456816

6. 반환

반환은 저장해두었던 스택 프레임과 반환 주소를 꺼내면서 이루어짐.
여기서는 callee 함수가 스택 프레임을 만들지 않았기 때문에 pop rbp로 스택 프레임을 꺼낼 수 있지만, 일반적으로는 leave로 스택 프레임을 꺼냄

스택 프레임을 꺼낸 뒤에는 ret로 호출자로 복귀함

pwndbg> x/5i $rip
=> 0x555555554650 <callee+86>:	pop    rbp
   0x555555554651 <callee+87>:	ret
pwndbg> si
pwndbg> si
pwndbg> x/4i $rip
=> 0x555555554682 <caller+48>:	add    rsp,0x8
   0x555555554686 <caller+52>:	nop
   0x555555554687 <caller+53>:	leave  
   0x555555554688 <caller+54>:	ret
pwndbg> print $rbp
$3 = (void *) 0x7fffffffdf80
pwndbg> print $rip
$4 = (void (*)()) 0x555555554682 <caller+48>
profile
보안, 프런트엔드 공부 중!

0개의 댓글