Control Hijacking Attacks (1)

Eunji·2025년 11월 18일

System Security

목록 보기
12/16

1. Control hijacking attacks

Control flow

  • Order in which individual statements, instructions of function calls of a program are executed or evaluated

Control Hijacking Attacks(Runtime exploit)

  • Exploit a program error, particularly a memory corruption vulnerability, at application runtime to subvert the intended control-flow of a program
  • Control-hijacking attacks == Control-flow hijacking attacks
    • Change of control flow
      • Alter a code pointer or, Gain control of the instruction pointer %eip
      • Change memory region that should not be accessed

E.g.,) Code injection attacks, Code reuse attacks

프로그램이 동작하는 런타임 환경에서 발생하며, 가상 메모리 손상 취약점을 통해 정상적인 실행 흐름을 탈취하여 공격자의 목표대로 동작하도록 만든다.

Control Flow Graphs(CFG)

  • A basic block is a linear sequence of program instructions having one entry point(the first instruction executed) and one exit point(the last instruction executed)
  • A control flow graph is a directed graph in which the nodes represent basic blocks and the edges represent control flow paths

2. Code injection attacks vs. Code reuse attacks

2.1 Code injection attacks

  • Subvert the intended control-flow of a program to previously injected malicious code

Shellcode

  • Code supplied by attacker
    • Often saved in buffer being overflowed
    • Traditionally transferred control to a shell(user command-line interpreter)
  • Machine code
    • Specific to processor and OS
    • Traditionally needed good assembly language skills to create
    • More recently have automated sites/tools

An example of malicious code is shellcode

  • 버퍼의 중간에 Return Address를 overwrite하여, 함수가 반환될 때 POP EIP가 발생하면서 shellcode의 위치로 jump하게 만든다.
  • 이렇게 되면 control flow가 공격자가 주입한 A, B(injection code)로 이동하게 된다.
  • 이 과정 전체가 control-flow hijacking attack의 code injection에 해당한다.

2.2 Code-reuse attacks

  • Subvert the intended control-flow of a program to invoke an unintended execution path inside the original program code
  • Code-reuse attacks are software exploits in which an attacker directs control flow through existing code with a malicious result
  • e.g.,
    • Return-to-Libc Attacks(Ret2Libc)
    • Return-Oriented Programming(ROP)
    • Jump-Oriented Programming(JOP)

Code-injection은 공격자가 자신이 준비한 새로운 코드를 프로그램에 삽입해서 실행시키는 방식이고, code-reuse는 기존 프로그램이 가지고 있던 코드 영역으로 제어 흐름을 이동시켜, 정상 코드가 공격자의 의도대로 실행되는 방식이다.

Where are normally executable codes located?

  1. inject malicious
    • machine code(instruction)가 아니라, 기존 프로그램의 code block을 가리키는 포인터 값들
  2. exploit program error
    • 3번에서 4번으로 jmp한 뒤, 원래는 종료되어야 할 흐름을 의도적으로 1번으로 jmp하도록 조작한다.
    • EIP 변조를 통해 이루어지며, 정상 코드 블록을 재사용해 악의적인 흐름 생성

History of code-reuse attacks

2.3 Summary of machine code level attacks

그림은 상단이 code 영역, 하단이 dynamic data(stack/heap) 영역이며, 빨간색은 공격자의 제어할 수 있는 영역을 나타낸다.

  • DEX/NX(Non-Executable eXecute)
    • dynamic 영역에서 실행되는 코드를 불허하는 defense mechanism
    • 이를 통해, 공격자가 buffer overflow 등으로 stack에 주입한 malicous 실행을 막는다.
  • Code-reuse attack 우회 원리
    • 공격자는 EIPcode 영역 내 특정 위치로 변조함으로써, 기존 프로그램이 가진 정상 코드 또는 라이브러리 코드로 흐름을 전환한다.

3. Return-to-libc attacks

3.1 Function pointer in C

  • Function pointer를 선언할 때, function prototype(return & parameter의 type)을 반드시 정확하게 지정해야 원하는 함수의 주소를 올바르게 가리킬 수 있다.
  • 선언과 동시에 대입 가능 void (*fun_ptr)(int) = fun;
  • 선언된 함수 포인터 변수를 통해 호출 가능 (*fun_ptr)(10);

3.2 Non-executable stack

Running shellcode in C program

  • main() 안에서 shellcode를 담을 buffer를 생성하고, strcpy로 buffer에 복사한다.
    • 0x00이 없기 때문에 온전한 buffer로 복사됨
  • buffer의 주소를 function pointer로 typecasting
  • parameter와 return 없는 함수의 시작 주소로 해석하게 되어, 해당 위치의 값을 instruction으로 실행한다.

'buffer' 대신 'code'를 넣으면 실행되는가?

  • buffer는 지역 변수라서 stack에 올라가고 -z execstack으로 컴파일하면 해당 바이너리의 stack segment는 실행 가능으로 설정된다.
  • code는 전역 상수 배열이라 data 영역에 위치, 최신 리눅스에서는 기본적으로 NX라서 segmentation fault가 발생한다.

With executable stack

  • 위 조건대로 실행 시 shell을 얻는다.
  • -z execstack 옵션으로 컴파일하면 stack 실행 권한 부여
  • 마지막 줄을 code로 바꾸면 실행 위치가 NX인 data 영역으로 바뀌어 seg fault

With non-executable stack

  • -z noexecstack으로 컴파일하면 stack 실행 권한 제거되어 stack에 있는 byte를 실행하려는 순간 seg fault
  • 마지막 줄을 code로 바꿔도 data 영역 역시 NX이므로 seg fault

Set/clear executable stack bit via execstack

  • s: stack 영역을 executable로 설정하여 shellcode가 정상적으로 실행
    • code로 바꾸면 영역이 여전히 NX라서 seg fault
  • c: stack exec 플래그를 다시 clear해서 non-executable stack으로 만들어 buffer에 있는 shellcode seg fault
    • code로 바꾸도 seg fault

3.3 How to defeat this countermeasure

대부분 non-executable stack 정책을 적용하므로, stack에 임의 코드를 삽입해서 직접 실행하는 code injection 방식은 더 이상 exploit이 어렵다.
  • header 안에는 함수의 원형(name, parameter, return type) 선언
    • 따라서 코드 안에서 printf()와 같은 함수를 바로 써도 오류가 발생하지 않는다.
  • 함수의 body는 libc와 같은 라이브러리에 존재하며, 실행 시 해당 라이브러리가 함께 메모리에 로드된다.
  • system()은 libc에 내장된 함수로, command 인자를 받아 실행한다.
  • Return address를 libc 내 'system()' 함수 시작 주소로 덮어쓰고, 명령어 인자 '/bin/sh'을 넣으면 쉘을 얻는다.

쉘을 따기 위해서 Return address를 system()으로 바꾼 뒤, 인자 부분에 원하는 명령 문자열을 위치시키는 것이 핵심이다.

3.4 Basic principle of Return-to-Libc attack

  • 환경변수는 프로그램 메모리 영역에 미리 적재(e.g., $PATH, /bin/sh)
  • Code: binary code, instruction
  • Libraries: 운영체제가 미리 적재해두는 공유 라이브러리(libc)

Local buffer의 시작 주소를 기준으로, RT를 덮어써서 system()의 메모리 주소가 되도록 만들고, 환경변수 영역에 /bin/sh이 존재하는 주소를 인자로 넘긴다.

  • Buffer overflow 끝부분의 EIP가 system()의 주소로 변경되고, system() 실행 이후에는 exit()의 주소를 바라보게 만듦으로써 흐름을 제어한다.

Addr(system) 바로 위에 Addr(exit)가 존재하는 이유는 system 함수가 return한 뒤에 EIP의 위치이므로 프로그램이 정상적인 종료처럼 보이게 하는 것이다.

Reuse란 있는 기존에 메모리에 적재되어 있던 라이브러리 코드(libc)를 활용해서 jump하는 기법이다.


Return-to-Libc(1)

  • Adversary transmits malicious input
  • Using existing code(e.g., libc function) instead of injecting code
    • E.g.,) system("/bin/sh");

buffer(80 byte)를 채우고, ebp를 덮고, return address(4 byte)를 system 함수의 실제 주소로 변경하고, system 함수에 넘길 인자 '/bin/sh'를 이어 붙인다.

  • "A"*80: buffer를 80 byte로 채워서 overflow 유발
  • "B"*4: saved base pointer를 4 byte 더미 값으로 덮음
  • "\xe0\x8a\x05\x40": system() 메모리 주소를 채워 RT에 쓴다.
  • "AAAA": 의미 없는 4 byte
    • system()의 RT
  • "\x9f\xbf\x05\x40": '/bin/sh' 문자열의 주소

문자열 값은 기본적으로 little endian 형식으로 작성한다. exploit에서는 ebp에 더미 값을 넣어도 공격 자체에 문제가 없다.

Return-to-Libc(2)

  • Input contains pattern bytes, ..., a new ret_addr pointing to sytem(), ...

Return-to-Libc(3)

  • ..., and a pointer to the /bin/sh string

Return-to-Libc(4)

  • When echo() returns, system() launches a new shell

3.5 Environment setup

  • This code has potential buffer overflow problem in vul_func()
  • "Non executable stack" countermeasure is switched on, StackGuard protection is switched off and address randomization is turned off

Option

  • Non-executable stack: 코드 인젝션 불가
  • Stack guard off: 버퍼 오버플로우 유발
  • Randomization off: 공격 가능성 증가
  • Root owned Set-UID program

Overview of the attack

  • Task A: Find address of system()
    • To overwrite return address with system()'s address
  • Task B: Find address of the "/bin/sh" string
    • To run command "/bin/sh" from system()
  • Task C: Construct arguments for system()
    • To find location in the stack to place "/bin/sh" address (argument for system())

Task A: To find system()'s address

  • Debug the vulnerable program using gdb
  • Using p(print) command, print address of system() and exit()

randomization을 off하여 stack을 껐다 켜도 동일한 주소값이 나온다.

Task B: To find "/bin/sh" string address

  • MYSHELL 환경 변수가 취약점이 있는 프로그램에 전달되어 스택에 저장되고, 해당 문자열의 주소를 찾을 수 있다.
  • Export: 환경변수를 실행되는 하위 프로세스에서도 사용할 수 있게 설정

$ export MYSHELL = "/bin/sh"

Code to display address of environment variable

  • 환경변수 'MYSHELL'의 값을 getenv 함수로 읽어 shell 포인터에 저장한다.
    • %s: 환경변수의 실제 값 출력
    • %x: 환경변수 값의 시작 주소를 16진수 형태로 출력 \rightarrow exploit에 활용
    • unsigned int: 주소값을 4 byte 정수형으로 출력하기 위해 사용

Export MYSHELL environment variable and execute the code

  • 컴파일 후, 실행 파일 이름을 'env55'로 지정한다.
  • export로 환경변수 등록하면, 자식 프로세스들도 MYSHELL 참조 가능

컴파일한 프로그램(env55)을 실행하면 getenv 함수가 환경변수 MYSHELL의 값을 정상적으로 읽어온다. 하지만 MYSHELL이 export되지 않은 경우, 자식 프로세스에서는 해당 변수를 볼 수 없으므로 getenv는 NULL을 반환한다.

Task B: Some considerations

  • Address of MYSHELL environment variable is sensitive to the length of the program name
  • If the program name is changed from env55 to env7777, we get a different address
  • 0xbffffffd0: 실행 중인 binary의 절대 경로 문자열, 모든 프로그램이 exec될 때, 경로 저장
    • 경로가 길수록 환경 변수의 시작 주소가 뒤로 밀린다.
    • exploit을 준비할 때 파일명을 짧게 지정하는 이유는 환경변수의 메모리 위치가 일정하기 유지되도록 하기 위함이다.

stack 기반 주소 예측에 있어 환경변수 값의 전체 길이가 동일해야 address offset이 일관성 있게 유지될 수 있다.

x command in gdb:

  • Displays the memory contents at a given address using the specified format
  • x / [Length][Format][Address expression]
  • x / 100s *((char **) environ )
    • For a given address, Display 100 strings with each address

Task C: Argument for system()

Function Prologue

  • ESP: Stack pointer
  • EBP: Frame pointer

(1) CALL: PUSH EIP
(2~3) Prologue
(4) N byte for local variable

Function Epilogue

(4) ret(POP EIP): ESP가 가리키는 값(RA)을 빼서 EIP로

ESP를 기준으로 위 영역만이 스택포인터가 가리키는 "실제로 쓸 수 있는" 안전한 스택 영역이다.

E.g., Function prologue and epilogue

  • leave
movl %ebp, %esp 
popl %ebp
  • a = x;
    • ebp + 8 byte : parameter x
      • parameter x를 eax에 복사
    • ebp - 4 byte : local variable a
movl 8(%ebp), %eax
movl %eax, -4(%ebp)

Task C: Argument for system()

  • Arguments are accessesd with respect to ebp
  • Argument for system() needs to be on the stack

Frame for the system() function

  • ebp + 8: function's parameter
  • 정상적인 함수 호출에서 항상 같은 방식으로 parameter 위치가 저장되지만, Return to libc는 EBP 값이 달라질 수 있다.

0개의 댓글