완료됨
rop 실행 흐름 질문

안녕하세요 ! 워게임 연결 강의 듣던 중 궁금증이 생겨서 이렇게 질문을 남기게 되었습니다.

# write(1, read_got, ...)

payload += p64(pop_rdi) + p64(0x1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0x0)
payload += p64(write_plt)

# read(0, read_got, ...)

payload += p64(pop_rdi) + p64(0x0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0x0)
payload += p64(read_plt)

# read('/bin/sh') == system('/bin/sh')

payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(read_plt)

p.sendafter(b'Buf: ', payload)

read = p.recvn(8)
system = u64(read) - read_system_offset

# read = p.recv(6)
# system = u64(read + b'\x00\x00') - read_system_offset

p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
  1. # read(0, read_got, ...) 부분에서 마지막에 read_plt를 통해 read 콜을 호출하게 되는데, read 콜이 호출된 이후 실행 흐름(rip)은 calling convention에 의해 알아서 바로 연속적으로 바로 다음에 저장된 주소인 pop rdi; ret으로 이동하게 되나요 ?
    정리하면, ROP로 중간에 read 콜처럼 함수를 호출할 때 함수가 호출된 이후 실행 흐름은 바로 뒤에 저장한 메모리 주소로 이동한다고 이해하면 되나요 ?

  2. 그리고 아래 부분에서 p.send(p64(system) + b'/bin/sh\x00')로 보낸 payload(p2)는 처음으로 보낸 payload(p1) 중간에 read 콜이 호출되는 과정에서 stdin으로 들어가게 되는 것이 맞나요 ?

이렇게 되면 처음 보낸 payload(p1)가 전부 수행된 후 뒤에 payload(p2)가 쌓여서 보내지는 것이 아닌 중간에 다른 메모리 주소에 p2가 쌓이는 것이 맞나요? <<이 질문은 조금 제 설명도 이상할 수 있어서 내용 자체가 너무 어긋났다면 답변하지 않아주셔도 괜찮습니다.

  1. 강의에서는 read의 got에 저장된 주소를 받을 때 read = p.recvn(8)이 아닌
    read = p.recvn(6) + b'\x00\x00'으로 받던데 그 이유가 무엇인가요 ?
#시스템해킹 #공격기법 #rop #return_oriented_programming
작성자 정보
더 깊이 있는 답변이 필요할 때
드림핵 팀과 멘토에게 직접 문의해 보세요!
답변 2
avatar
mini-chip
Perfect 10
avatar
mini-chip
Perfect 10

안녕하세요.

  1. 함수의 calling convention이라기보다는 read 함수가 끝날 때 ret명령어를 수행하기 때문입니다. 가젯도 (원하는 작업); ret 형태의 가젯을 이용하여 rop를 수행하였죠?? read함수도 (read하는 작업); ret형태의 코드가 되죠. 이러면 rop를 하는 원리 그대로 read수행 이후 pop rdi로 실행 흐름이 넘어간다는 사실을 이해하실 수 있으실 겁니다.
  2. 제가 질문을 정확하게 이해한 것인지는 모르겠습니다만, send를 하게 되면 일단 프로세스의 stdin으로 들어갑니다. stdin은 stream입니다. 첫 번째 p.send(payload)로 payload를 보낼 때도 해당 payload는 프로세스의 stdin에 들어가게 됩니다. 아마 프로세스의 입력 함수에서 stdin으로부터 입력 받아서 stdin에 있던 payload가 프로세스 내부의 메모리에 적재가 되었겠죠? 그리고 다시 p.send(system)을 보내게 되었습니다. 이것 또한 stdin에 들어가게 됩니다. stdin에 계속 있다가 payload를 통한 rop에서 read함수를 호출하였기 때문에 이 과정에서 stdin에 있던 system함수의 주소가 프로세스 내부의 메모리에 적재되는 작업이 일어나게 됩니다.
  3. write()함수는 문자열을 출력하는 함수입니다. 그러면 예를 들어서 8byte크기의 버퍼에 b"\x30\x30\x30\x30\x31\x31\x00\x00"값이 적재되어 있었다고 하면, 이 버퍼를 문자열로 출력하면 어떻게 출력될까요? "000011"이렇게만 출력되겠죠? 뒤의 2byte는 null이기 때문에 당연히 출력하지 않습니다. 그렇기에 write함수를 통해 read_got에 저장된 주소를 leak하면 뒤의 2byte는 null이라서 출력이 되지 않습니다. 그렇기에 8byte가 아닌 6byte만 출력합니다. 그럼 이것을 recv하는 입장에서 recvn(8)로 8byte가 출력될때까지 recv하고 있으면.... 영원히 안끝나겠죠?? 애초에 6byte만 출력되기 때문에 recvn(6)을 해주고 남은 null byte들을 수동으로 붙여주게 됩니다. (굳이 null 바이트를 붙여서 8byte로 만들어주는 이유는 u64의 인자가 반드시 8byte크기의 byte string이어야 하기 때문이죠)
2024.03.11. 07:48
avatar
mini-chip
Perfect 10
avatar
mini-chip
Perfect 10
  1. 저도 이 부분에 대한 자세하고 정확한 지식이 부족하지만 제가 이해하고 있는 바를 설명드리자면, read함수는 입력과 관련된 인터럽트를 발생시키므로 read가 호출되면 프로세스를 잠깐 멈추고 입력이 들어올 때까지 기다리게 됩니다. 그러면 입력이 완료되었다고 판단되면 다시 프로세스를 실행시키겠죠? 이 때, 입력이 완료되었다고 판단하는 이벤트가 몇 가지 종류가 있습니다. read로 읽으려는 버퍼 사이즈만큼 모두 읽었을 경우나, EOF를 마주쳤을 경우나, 에러가 발생했을 경우나 등등 다양한 이유로 인해서 더 이상 입력할 것이 없다는 이벤트가 발생하게 됩니다. p.send(p1+p2)p.send(p1), p.send(p2)는 한 번 보내냐 두 번 보내냐의 차이가 있죠. send를 한 번 할 때마다 입력이 끝났음을 알리는 이벤트를 발생시키는 것으로 생각됩니다.
  2. 항상 그런 것인지는 모르겠습니다만, 기본적으로 libc 영역은 0x7f??????????에 할당이 됩니다. 매핑되는 영역을 임의로 바꾸는 방법이 있는지는 모르겠네요. p.recv(8)은 최대 8byte를 recv하는 함수입니다. p.recvn(8)은 8byte를 recv할때까지 recv하는 함수가 되구요. p.recvn(8)을 하게 되면 익스가 안 될 겁니다.
2024.03.13. 07:32