3.6 Return to another function
Return-to-libc의 overwrite 상황을 가정하면, 기존 frame의 ret(pop eip), call(push eip)가 무시되고 원하는 함수의 prologue로 넘어간다.
Tracking the ebp value
- ret(pop eip): esp가 한 칸 증가, ebp는 유지
- 함수 B(system())의 prologue에서는 ebp를 esp로 다시 설정하게 되어 x+4; 함수 A의 ebp가 함수 b의 ebp보다 4 byte 높은 주소를 가리킨다.
- Function parameter는 ebp+8 위치에 준비되어야 하므로 함수 A 기준으로 12byte 위를 덮어씌우면 함수 B(system())의 parameter로 인식된다.
exploit은 old ebp 기반으로 payload를 쌓으니 old ebp+12가 system의 인자가 된다.
3.7 How to find system()'s argument address?
- Need to understand how the ebp and esp registers change whith the function calls
- Between the time when return address is modified and system argument is used,
vul_func() epilogue and system() prologue begins
3.8 Memory map to understand system() argument
- (a): ESP가 buffer의 시작 가리키고 있음
- (b): epilogue가 실행되어 ESP가 pop ebp, pop eip를 거치면서 system()의 frame으로 넘어감
- (c): system()의 prologue에서는 최초 ebp 기준(a)으로부터 12 byte 떨어진 위치가 system()의 첫 번째 인자가 된다.
공격자는 vul_func()의 strcpy() 시점에 한 번만 overwrite가 가능하며, 계산된 offset을 기준으로 값들을 정확하게 배치해야 한다.
- str: dummy or exit()의 시작 주소
- exit(): 프로세스가 정상 종료처럼 보이게 하는 트릭
- 그 위(offset 12byte): /bin/sh의 시작 주소
Flow chart to understand system() argument
Malicious code
- 112 = 108(EBP - buffer의 시작 주소) + 4(EBP size)
Launch the attack
- Execute the libc_exploit.py and then the vulnerable code
Reuse attack도 buffer overflow를 기반으로 하는 exploit이다.
3.9 Return-to-Libc attacks
Basic idea of return-to-libc attacks
- Overwrite RET addr with addr of libc function
- Use existing code instead of injecting code(No injected code)
- Subvert the usual execution flow by redirecting it to functions in linked system libraries
- The process's image consists of
- writable memory areas such as the code segment and the linked system libraries
- The target for useful code can be found in the C library libc
The library libc
- libc i linked to nearly every Unix/Linux program
- This library defines system calls and other basic facilities such as open(), malloc(), printf(), system(), execve(), etc
- E.g., system("/bin/sh")
3.10 Defense techniques
ASLR
- ASLR randomizes the base address of code and data segments per execution run
- Hence, the memory location of code that the adversary attempts to use will reside at a random memory location
완전히 공격을 막을 수 있는 것이 아니며, 메모리 주소가 random하게 바뀌기 때문에 운이 좋으면 exploit에 성공할 수 있다.
CFI (Control-flow integrity)
- CFI offers a generic defense against code reuse attack by validating the integrity of a program's control-flow based on predefined control-flow grap(CFG) at runtime
- Control-flow graph를 기준으로 프로그램의 실제 실행 흐름을 검증하여 코드 재사용 공격을 원천적으로 차단하는 기술이다.
- Intened control-flow 내에서만 동작하는지 실시간으로 확인하며, 허용된 경로를 벗어나면 integrity가 훼손된 것으로 판단해 공격을 차단한다.
- 실제 경우에는 control-flow 분석은 매우 복잡하고, 모든 분기마다 검증해야 하므로 runtime overhead가 매우 크다.
4. Return-Oriented Programming
4.1 We have "Cheated"
Let /bin/sh point to /bin/zsh
sudo ln -sf /bin/zsh /bin/sh
기본적으로 /bin/sh은 dash 쉘에 연결되어 있으며 dash는 Set-UID 비트로 인한 권한 상승을 차단한다. 따라서 실습을 위해 /bin/sh 링크를 zsh로 변경한다.
- system("/bin/sh")
- system("/bin/zsh")
- Don't get a root shell
- 내부적으로 항상 '/bin/sh -c' 형태로 실행되기 때문에 단순히 zsh 명령을 넣어도 zsh로 바로 실행되지 않는다.
- 상대에게 zsh 자체가 없을 수도 있다.
system()
- 내부적으로 항상 '/bin/sh -c ' 형태로 명령 실행
- 명령어가 무엇이든 system()은 /bin/sh를 실행하고, 그 후 명령어를 -c 옵션을 통해 인자로 넘긴다.
- 따라서 인자로 단순히 zsh의 경로를 넣어도 실제로는 /bin/sh가 실행될 뿐이다.
4.2 Return-Oriented Programming
Typical function invocation
- 함수 A에서 B를 호출할 때,
call 현재 위치를 stack에 push
- 함수 B 실행 후,
ret으로 stack에 저장된 주소로 복귀(pop)
ROP
call 없이 stack의 return ad를 공격자가 직접 조작
- 공격자는 실행하고 싶은 함수의 주소들을 차례대로 쌓아두고
ret 가 실행될 때마다 stack에 있는 다음 주소로 이동하게 하여 연속적으로 코드가 실행된다.
ROP에서 복귀는 원래 위치로 돌아가는 것이 아니라, 공격자가 지정한 다음 주소로 넘어간다.
4.3 The revised vulnerable program
gcc -m32 -fno-stack-protector -z noexecstack -fcf-protection=none -fno-pie -o stack_rop stack_rop.c
foo()에서 bar(), baz()를 직접 호출하지 않고, main()에서도 bar(), baz()를 호출하지 않고 foo()만 호출한다.
- asm(): C 코드 안에 x86 assembly 명령어를 삽입해 현재 EBP 값을 얻는다.
static : 함수 내부에서만 접근 가능하며 함수가 여러 번 호출되어도 변수 i는 한 번만 초기화되고 값이 누적된다.
The goal
- Execute multiple the bar() function
단일 호출이 아닌 여러 RET 주소에 bar()의 주소를 집어넣으면, 여러 번 bar() 함수가 실행된다. 이는 ROP의 기본적인 원리이다.
4.4 Tracking the ebp value
main()에서 foo()만 직접 호출하지만, buffer overflow를 이용해 이를 foo()의 return ad를 bar()의 주소로 덮으면 foo()가 종료될 때 bar()로 흐름을 넘길 수 있다.
- strcpy()를 이용해 buffer overflow시키면,
foo()가 return할 때 저장된 return ad를 bar()의 주소로 덮을 수 있다.
- 공격자는 원하는 함수의 주소를 삽입함으로써 임의의 함수를 실행시킬 수 있다.
Badfile construction
| 0xaa ... (112)| 0xFF ... (4)| bar() ... (40) | exit() (4) |
- Offset(112): buffer - EBP
- Previous EBP(4): Offset과 마찬가지로 의미 없는 값으로 덮음
- Return Ad(40): 4 byte bar()의 주소 * 10
- 실제로 foo()가 return할 때 bar()가 여러 번 실행됨
마지막 4 byte는 정상적인 종료로 보이기 위해 exit()의 주소를 삽입한다.
4.5 Chaining function calls without arguments
- Buffer + EBP + Return Ad * n번 + exit() 주소
The challenge?
- baz()처럼 파라미터가 필요한 함수를 여러 번 호출하려면 문제 발생
- strcpy()가 payload를 단 한 번만 복사하기 때문에 설정 고정됨
- EBP + 8이므로 3번까지 baz() 호출 가능
Y를 이용하면 여러 번 함수 호출 시 frame이 chaining되고, 각 freme이 다음 room을 활용해 더 많은 공간을 활용할 수 있다.
4.6 Finding room on the stack for arguments
- main() → foo()
- foo() 실행 후 return
- pop eip: bar()'s prologue
- bar()가 실행되며 새로운 frame 생성
bar()가 종료되어 return하면, 이전 ebp로 복구되고 다음 return ad가 pop되어 또 다른 bar() 실행으로 chaining된다.
4.7 Chaining function calls with arguments
Idea: skipping function prologue
4.8 An example
The baz() function
- bar()'s prologue(2개의 instruction)를 건너뛰고(baz+3) 바로 실질적인 코드로 jump하면
- prologue에서 stack frame을 설정하지 않으므로 payload에 직접 설계한 인자를 바로 전달할 수 있다.
- Y값을 기점으로 함수 chaining이 가능해져 여러 함수에 인자를 직접 넘기고 exploit이 동작한다.
Badfile construction
