Control Hijacking Attacks (2)

dmswl·2025년 11월 19일

System Security

목록 보기
13/15

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 parameterebp+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

  1. main() \rightarrow foo()
  2. foo() 실행 후 return
    • pop eip: bar()'s prologue
  3. bar()가 실행되며 새로운 frame 생성
    • ebp = X + 100

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

0개의 댓글